Hemera
A Node.js microservices toolkit for the NATS messaging system
π Getting Started
Hemera is a small wrapper around the nats driver. We want to provide a toolkit to develop microservices in an easy and powerful way. We use bloom filters to provide a pattern matching RPC style. You don't have to worry about the transport. NATS is powerful.
With Hemera you have the best of both worlds. Efficient pattern matching to have the most flexibility in defining your RPC's.
It doesn't matter where your server or client lives. You can add the same add
as many as you want on different hosts to ensure maximal availability. Thanks to the Request Reply pattern you can work with that as if you do a normal http request. The only dependency you have is a single binary of 7MB. Mind your own business NATS do the rest for you:
Service Discovery
- Any subscription is managed by NATS. You don't need any service discovery. Totally location transparency.
Scalability
- Filtering on the subject name enables services to divide work (perhaps with locality) e.g
topic:auth:germany
- Queue group name allow load balancing of services.
Fault tolerance
- Auto-heals when new services are added.
- Configure cluster mode to be more reliable.
Load Balancing
- Queue groups are used by default in Hemera.
+ Hemera = Pattern-driven micro services.
Prerequisites
NATS messaging system
We use the Request Reply concept to realize this toolkit. Request Reply
Installing
npm i nats-hemera
Example
'use strict';
const Hemera = require('hemera');
const nats = require('nats').connect(authUrl);
const hemera = new Hemera(nats, { logLevel: 'info' });
hemera.ready(() => {
hemera.add({ topic: 'math', cmd: 'add' }, (resp, cb) => {
cb(null, resp.a + resp.b);
});
hemera.add({ topic: 'email', cmd: 'send' }, (resp, cb) => {
cb();
})
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 2, timeout$: 5000 }, (err, resp) => {
console.log('Result', resp);
});
hemera.act({ topic: 'email', cmd: 'send', email: 'foobar@mail.com', msg: 'Hi' });
});
Writing an application
Define your service
hemera.add({ topic: 'math', cmd: 'add' }, (resp, cb) => {
cb(null, resp.a + resp.b);
});
Call your service
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 }, (err, resp) => {
console.log(resp);
});
Pattern matching rules
A match happens when all properties of the added pattern matches with the one in the passed obj.
Matched!
hemera.add({ topic: 'math', cmd: 'add' }, (resp, cb) => {
cb(resp.a + resp.b)
});
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 });
Not matched!
hemera.add({ topic: 'math', cmd: 'add', foo: 'bar' }, (resp, cb) => {
cb(resp.a + resp.b)
});
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 });
Error handling
Reply an error
hemera.add({ topic: 'math', cmd: 'add' }, (resp, cb) => {
cb(new CustomError('Invalid operation'));
});
Error-first-callbacks
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 }, (err, resp) => {
err instanceOf CustomError
});
Handle timeout errors
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 }, (err, resp) => {
err instanceOf TimeoutError
});
Fatal errors
Fatal errors will crash your server. You should implement a gracefully shutdown and use a process watcher like PM2 to come back in a clear state. Optional you can disable this behaviour by crashOnFatal: false
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 }, (err, resp) => {
throw new Error('Upps');
});
hemera.add({ topic: 'math', cmd: 'add' }, (resp, cb) => {
err instanceOf FatalError
});
Listen on transport errors
const hemera = new Hemera(nats, { logLevel: 'info' });
hemera.events.on('error', ...)
hemera.events.on('disconnect', ...)
hemera.events.on('connect', ...)
Specify custom timeout per act
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1, timeout$: 5000 }, (err, resp) => {
});
Delegation
* Notice the use of this
Metadata
If you want to transfer metadata to a service you can use the meta$
property before sending. It will be passed in all nested act
.
E.g you can add a JWT token as metadata to express if your action is legitimate.
hemera.add({ topic: 'math', cmd: 'add' }, function (resp, cb) {
let meta = resp.meta$
cb(null, resp.a + resp.b);
});
Will set the metadata only for this act
and all nested act
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1, meta$: { a: 'test' } }, function (err, resp) {
this.act({ topic: 'math', cmd: 'add', a: 1, b: 5 });
});
Will set the metadata on all act
hemera.meta$.token = 'ABC1234'
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1}, function (err, resp) {
this.meta$.token = 'ABC1234';
this.act({ topic: 'math', cmd: 'add', a: 1, b: 5 });
});
Context
If you want to set a context across all act
you can use the context$
property.
hemera.context$.a = 'foobar';
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1 }, function (err, resp) {
this.act({ topic: 'math', cmd: 'add', a: 1, b: 5 }, function (err, resp) {
this.context$.a
});
});
If you want to set a context only for this act
and all nested act
hemera.act({ topic: 'math', cmd: 'add', a: 1, b: 1, context$: 1 }, function (err, resp) {
this.act({ topic: 'math', cmd: 'add', a: 1, b: 5 }, function (err, resp) {
this.context$
});
});
Payload validation
Hemera includes a payload validator called parambulator
hemera.add({
topic: 'math',
cmd: 'add',
a: {
type$: 'number'
}
}, (resp, cb) => {
cb(null, {
result: resp.a + resp.b
});
});
Handling
hemera.act({ topic: 'math', cmd: 'add', a: '1' }, function (err, resp) {
err instanceOf PayloadValidationError
});
Plugins
let myPlugin = function (options) {
let hemera = this;
hemera.add({
topic: 'math',
cmd: 'add'
}, (resp, cb) => {
cb(null, {
result: resp.a + resp.b
});
});
};
hemera.use({ plugin: myPlugin, attributes: { name: 'myPlugin' }, options: { } })
Logging
const hemera = new Hemera(nats, { logLevel: 'info' });
[2016-11-17T21:04:47.608Z] INFO (app/18196 on starptech): ACT
topic: "math"
cmd: "add"
a: 1
b: 2
[2016-11-17T21:04:47.613Z] INFO (app/18196 on starptech): ACT_RESP
topic: "math"
cmd: "add"
a: 1
b: 2
time$: 2
Tracing
hemera.on('outbound', (msg) => {
console.log('Outbound', msg)
})
hemera.on('inbound', (msg) => {
console.log('Inbound', msg)
})
Protocol
Format: JSON
Request
{
"pattern": "<msg>",
"meta$": "<msg>",
"request$": "<msg>"
}
Response
{
"result": "<msg>",
"error": "<serialized_error>",
"meta$": "<msg>",
"response$": "<msg>"
}
Best practice
Think in small parts. A topic is like a service. You can define a service like auth
which is responsible for authenticate users.
This service has actions like:
hemera.add({ topic: 'auth', cmd: 'authenticate' })
hemera.add({ topic: 'auth', cmd: 'passwordReset' })
...
Create multiple instances of your service.
Now your service is scaled.
node service.js
node service.js
Create another NATS Server and create a cluster.
Now your service is fault-tolerant.
var servers = ['nats://nats.io:4222', 'nats://nats.io:5222', 'nats://nats.io:6222'];
var nc = nats.connect({'servers': servers});
new Hemera(nc);
TODO
Introduction to NATS
https://www.youtube.com/watch?v=NfL0WO44pqc
NATS Limits & features
http://nats.io/documentation/faq/
Are you the only one who use NATS for microservice architectures?
The simplicity and focus of NATS enables it to deliver superior performance and stability with a lightweight footprint. It has the potential of becoming the de-facto transport for microservice architectures and event driven systems in this new era.
Asim Aslam, Creator of Micro
"I discovered NATS for its performance, and stayed for its simplicity. Itβs been a wonderfully reliable layer that connects our microservice architecture at Pressly. The source is easily readable and approachable, and the future developments keep me excited!
Peter Kieltyka - CTO, Pressly
Running the tests
Set the path to the gnatsd
before start testing.
npm run test
Monitoring
Easy and beauitiful tool to monitor you app. natsboard
Nginx integration for NATS
nginx-nats
Built With
- Bloomrun - A js pattern matcher based on bloom filters
- Node Nats Driver - Node.js client for NATS, the cloud native messaging system.
Contributing
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
Versioning
We use SemVer for versioning. For the versions available, see the tags on this repository.
Authors
See also the list of contributors who participated in this project.
License
This project is licensed under the MIT License - see the LICENSE.md file for details
Inspiration
Seneca - A microservices toolkit for Node.js.