This version contains the most changes in the history of Moleculer! More than 200 commits with 17k additions and a lot of new features.
Breaking changes
Github organization is renamed
The Github organization name (Ice Services) has been renamed to MoleculerJS. Please update your bookmarks.
- Github organization: https://github.com/moleculerjs
- Website: https://moleculer.services or http://moleculerjs.com/
- Gitter chat: https://gitter.im/moleculerjs/moleculer
Mixin merging logic is changed
To support #188, mixin merging logic is changed at actions
. Now it uses defaultsDeep
for merging. It means you can extend the actions definition of mixins, no need to redeclare the handler
.
Add extra action properties but handler
is untouched
// mixin.service.js
module.exports = {
actions: {
create(ctx) {
// Action handler without `params`
}
}
};
// my.service.js
module.exports = {
mixins: [MixinService]
actions: {
create: {
// Add only `params` property to the `create` action
// The handler is merged from mixin
params: {
name: "string"
}
}
}
};
Wrapper removed from transporter options
If you are using transporter options, you will need to migrate them. The transporter specific wrapper has been removed from options (nats
, redis
, mqtt
, amqp
).
Before
// NATS transporter
const broker = new ServiceBroker({
transporter: {
type: "NATS",
options: {
nats: {
user: "admin",
pass: "1234"
}
}
}
});
// Redis transporter
const broker = new ServiceBroker({
transporter: {
type: "Redis",
options: {
redis: {
port: 6379,
db: 0
}
}
}
});
// MQTT transporter
const broker = new ServiceBroker({
transporter: {
type: "MQTT",
options: {
mqtt: {
user: "admin",
pass: "1234"
}
}
}
});
// AMQP transporter
const broker = new ServiceBroker({
transporter: {
type: "AMQP",
options: {
amqp: {
prefetch: 1
}
}
}
});
After
// NATS transporter
const broker = new ServiceBroker({
transporter: {
type: "NATS",
options: {
user: "admin",
pass: "1234"
}
}
});
// Redis transporter
const broker = new ServiceBroker({
transporter: {
type: "Redis",
options: {
port: 6379,
db: 0
}
}
});
// MQTT transporter
const broker = new ServiceBroker({
transporter: {
type: "MQTT",
options: {
user: "admin",
pass: "1234"
}
}
});
// AMQP transporter
const broker = new ServiceBroker({
transporter: {
type: "AMQP",
options: {
prefetch: 1
}
}
});
Default nodeID
generator changed
When nodeID
didn't define in broker options, the broker generated it from hostname (os.hostname()
). It could cause problem for new users when they tried to start multiple instances on the same computer. Therefore, the broker generates nodeID
from hostname and process PID. The newly generated nodeID looks like server-6874
where server
is the hostname and 6874
is the PID.
Protocol changed
The transport protocol is changed. The new version is 3
. Check the changes.
It means, the >=0.12.x versions can't communicate with old <=0.11 versions.
Changes:
- the
RESPONSE
packet has a new fieldmeta
. - the
EVENT
packet has a new fieldbroadcast
. - the
port
field is removed fromINFO
packet. - the
INFO
packet has a new fieldhostname
.
New features
New ServiceBroker options
There are some new properties in ServiceBroker option: middlewares
, created
, started
, stopped
.
They can be useful when you use broker config file and start your project with Moleculer Runner.
// moleculer.config.js
module.exports = {
logger: true,
// Add middlewares
middlewares: [myMiddleware()],
// Fired when the broker created
created(broker) {
},
// Fired when the broker started
started(broker) {
// You can return Promise
return broker.Promise.resolve();
},
// Fired when the broker stopped
stopped(broker) {
// You can return Promise
return broker.Promise.resolve();
}
};
Broadcast events with group filter
The broker.broadcast
function has a third groups
argument similar to broker.emit
.
// Send to all "mail" service instances
broker.broadcast("user.created", { user }, "mail");
// Send to all "user" & "purchase" service instances.
broker.broadcast("user.created", { user }, ["user", "purchase"]);
CPU usage-based strategy
There is a new CpuUsageStrategy
strategy. It selects a node which has the lowest CPU usage.
Due to the node list can be very long, it gets samples and selects the node with the lowest CPU usage from only samples instead of the whole node list.
There are 2 options for the strategy:
sampleCount
: the number of samples. Default:3
lowCpuUsage
: the low CPU usage percent. The node which has lower CPU usage than this value is selected immediately. Default:10
Usage:
const broker = new ServiceBroker({
registry: {
strategy: "CpuUsage"
}
});
Usage with custom options
const broker = new ServiceBroker({
registry: {
strategy: "CpuUsage",
strategyOptions: {
sampleCount: 3,
lowCpuUsage: 10
}
}
});
Starting logic is changed
The broker & services starting logic has been changed.
Previous logic: the broker starts transporter connecting. When it's done, it starts all services (calls service started
handlers). It has a disadvantage because other nodes can send requests to these services, while they are still starting and not ready yet.
New logic: the broker starts transporter connecting but it doesn't publish the local service list to remote nodes. When it's done, it starts all services (calls service started
handlers). Once all services start successfully, broker publishes the local service list to remote nodes. Hence other nodes send requests only after all local service started properly.
Please note: you can make dead-locks when two services wait for each other. E.g.:
users
service hasdependencies: [posts]
andposts
service hasdependencies: [users]
. To avoid it remove the concerned service fromdependencies
and usewaitForServices
method out ofstarted
handler instead.
Metadata is sent back to requester
At requests, ctx.meta
is sent back to the caller service. You can use it to send extra meta information back to the caller.
E.g.: send response headers back to API gateway or set resolved logged in user to metadata.
Export & download a file with API gateway:
// Export data
export(ctx) {
const rows = this.adapter.find({});
// Set response headers to download it as a file
ctx.meta.headers = {
"Content-Type": "application/json; charset=utf-8",
"Content-Disposition": 'attachment; filename=\"book.json\"'
}
return rows;
}
Authenticate:
auth(ctx) {
let user = this.getUserByJWT(ctx.params.token);
if (ctx.meta.user) {
ctx.meta.user = user;
return true;
}
throw new Forbidden();
}
Better ES6 class support
If you like better ES6 classes than Moleculer service schema, you can write your services in ES6 classes.
There are two ways to do it:
-
Native ES6 classes with schema parsing
Define
actions
andevents
handlers as class methods. Call theparseServiceSchema
method in constructor with schema definition where the handlers pointed to these class methods.const Service = require("moleculer").Service; class GreeterService extends Service { constructor(broker) { super(broker); this.parseServiceSchema({ name: "greeter", version: "v2", meta: { scalable: true }, dependencies: [ "auth", "users" ], settings: { upperCase: true }, actions: { hello: this.hello, welcome: { cache: { keys: ["name"] }, params: { name: "string" }, handler: this.welcome } }, events: { "user.created": this.userCreated }, created: this.serviceCreated, started: this.serviceStarted, stopped: this.serviceStopped, }); } // Action handler hello() { return "Hello Moleculer"; } // Action handler welcome(ctx) { return this.sayWelcome(ctx.params.name); } // Private method sayWelcome(name) { this.logger.info("Say hello to", name); return `Welcome, ${this.settings.upperCase ? name.toUpperCase() : name}`; } // Event handler userCreated(user) { this.broker.call("mail.send", { user }); } serviceCreated() { this.logger.info("ES6 Service created."); } serviceStarted() { this.logger.info("ES6 Service started."); } serviceStopped() { this.logger.info("ES6 Service stopped."); } } module.exports = GreeterService;
-
Use decorators
Thanks for @ColonelBundy, you can use ES7/TS decorators as well: moleculer-decorators
Please note, you need to use Typescript or Babel to compile decorators.
Example service
const moleculer = require('moleculer'); const { Service, Action, Event, Method } = require('moleculer-decorators'); const web = require('moleculer-web'); const broker = new moleculer.ServiceBroker({ logger: console, logLevel: "debug", }); @Service({ mixins: [web], settings: { port: 3000, routes: [ ... ] } }) class ServiceName { @Action() Login(ctx) { ... } // With options @Action({ cache: false, params: { a: "number", b: "number" } }) Login2(ctx) { ... } @Event 'event.name'(payload, sender, eventName) { ... } @Method authorize(ctx, route, req, res) { ... } hello() { // Private ... } started() { // Reserved for moleculer, fired when started ... } created() { // Reserved for moleculer, fired when created ... } stopped() { // Reserved for moleculer, fired when stopped ... } } broker.createService(ServiceName); broker.start();
Event group option
The broker groups the event listeners by group name. The group name is the name of the service where your event handler is declared. You can change it in the event definition.
module.export = {
name: "payment",
events: {
"order.created": {
// Register handler to "other" group instead of "payment" group.
group: "other",
handler(payload) {
// ...
}
}
}
}
New experimental TCP transporter with UDP discovery
There is a new built-in zero-config TCP transporter. It uses Gossip protocol to disseminate node info, service list and heartbeats. It has an integrated UDP discovery to detect new nodes on the network. It uses multicast discovery messages.
If the UDP is prohibited on your network, you can use urls
option. It is a list of remote endpoints (host/ip, port, nodeID). It can be a static list in your configuration or a file path which contains the list.
Please note you don't need to list all remote nodes. It's enough at least one node which is online. For example, you can create a "serviceless" gossiper node, which does nothing, just shares remote nodes addresses by gossip messages. So all nodes need to know only the gossiper node address to be able to detect all other nodes.
Use TCP transporter with default options
const broker = new ServiceBroker({
transporter: "TCP"
});
Use TCP transporter with static node list
const broker = new ServiceBroker({
transporter: "tcp://172.17.0.1:6000/node-1,172.17.0.2:6000/node-2"
});
or
const broker = new ServiceBroker({
nodeID: "node-1",
transporter: {
type: "TCP",
options: {
udpDiscovery: false,
urls: [
"172.17.0.1:6000/node-1",
"172.17.0.2:6000/node-2",
"172.17.0.3:6000/node-3"
]
}
}
});
All TCP transporter options with default values
const broker = new ServiceBroker({
logger: true,
transporter: {
type: "TCP",
options: {
// Enable UDP discovery
udpDiscovery: true,
// Reusing UDP server socket
udpReuseAddr: true,
// UDP port
udpPort: 4445,
// UDP bind address (if null, bind on all interfaces)
udpBindAddress: null,
// UDP sending period (seconds)
udpPeriod: 30,
// Multicast address.
udpMulticast: "239.0.0.0",
// Multicast TTL setting
udpMulticastTTL: 1,
// Send broadcast (Boolean, String, Array<String>)
udpBroadcast: false,
// TCP server port. Null or 0 means random port
port: null,
// Static remote nodes address list (when UDP discovery is not available)
urls: null,
// Use hostname as preffered connection address
useHostname: true,
// Gossip sending period in seconds
gossipPeriod: 2,
// Maximum enabled outgoing connections. If reach, close the old connections
maxConnections: 32,
// Maximum TCP packet size
maxPacketSize: 1 * 1024 * 1024
}
}
});
New experimental transporter for Kafka
There is a new transporter for Kafka. It is a very simple implementation. It transfers Moleculer packets to consumers via pub/sub. There are not implemented offset, replay...etc features.
Please note, it is an experimental transporter. Do not use it in production yet!
To use it, install
kafka-node
withnpm install kafka-node --save
command.
Connect to Zookeeper
const broker = new ServiceBroker({
logger: true,
transporter: "kafka://192.168.51.29:2181"
});
Connect to Zookeeper with custom options
const broker = new ServiceBroker({
logger: true,
transporter: {
type: "kafka",
options: {
host: "192.168.51.29:2181",
// KafkaClient options. More info: https://github.com/SOHU-Co/kafka-node#clientconnectionstring-clientid-zkoptions-noackbatchoptions-ssloptions
client: {
zkOptions: undefined,
noAckBatchOptions: undefined,
sslOptions: undefined
},
// KafkaProducer options. More info: https://github.com/SOHU-Co/kafka-node#producerclient-options-custompartitioner
producer: {},
customPartitioner: undefined,
// ConsumerGroup options. More info: https://github.com/SOHU-Co/kafka-node#consumergroupoptions-topics
consumer: {
},
// Advanced options for `send`. More info: https://github.com/SOHU-Co/kafka-node#sendpayloads-cb
publish: {
partition: 0,
attributes: 0
}
}
}
});
New experimental transporter for NATS Streaming
There is a new transporter for NATS Streaming. It is a very simple implementation. It transfers Moleculer packets to consumers via pub/sub. There are not implemented offset, replay...etc features.
Please note, it is an experimental transporter. Do not use it in production yet!
To use it, install
node-nats-streaming
withnpm install node-nats-streaming --save
command.
Connect to NATS Streaming server
// Shorthand to local server
const broker = new ServiceBroker({
logger: true,
transporter: "STAN"
});
// Shorthand
const broker = new ServiceBroker({
logger: true,
transporter: "stan://192.168.0.120:4222"
});
// Shorthand with options
const broker = new ServiceBroker({
logger: true,
transporter: {
type: "STAN",
options: {
url: "stan://127.0.0.1:4222",
clusterID: "my-cluster"
}
}
});
Define custom REPL commands in broker options
You can define your custom REPL commands in broker options to extend Moleculer REPL commands.
const broker = new ServiceBroker({
logger: true,
replCommands: [
{
command: "hello <name>",
description: "Call the greeter.hello service with name",
alias: "hi",
options: [
{ option: "-u, --uppercase", description: "Uppercase the name" }
],
types: {
string: ["name"],
boolean: ["u", "uppercase"]
},
//parse(command, args) {},
//validate(args) {},
//help(args) {},
allowUnknownOptions: true,
action(broker, args) {
const name = args.options.uppercase ? args.name.toUpperCase() : args.name;
return broker.call("greeter.hello", { name }).then(console.log);
}
}
]
});
broker.repl();
Changes
- MemoryCacher clears all cache entries after the transporter connected/reconnected.
broker.loadServices
file mask is changed from*.service.js
to**/*.service.js
in order to load all services from subfolders too.ServiceNotFoundError
andServiceNotAvailableError
errors are retryable errors.Strategy.select
method gets only available endpoint list.- old unavailable nodes are removed from registry after 10 minutes.
- CPU usage in
HEARTBEAT
packet is working properly in Windows too. - register middlewares before internal service (
$node.*
) loading. broker.getAction
deprecated method is removed.PROTOCOL_VERSION
constant is available via broker asServiceBroker.PROTOCOL_VERSION
orbroker.PROTOCOL_VERSION
- serialization functions are moved from transit to transporter codebase.
ctx.broadcast
shortcut method is created to send broadcast events from action handler.broker.started
property is created to indicate broker starting state.
Fixes
- handles invalid
dependencies
value in service schema #164 - fix event emit error if payload is
null
,