moleculer

Progressive microservices framework for Node.js

MIT License

Downloads
187.9K
Stars
6K
Committers
147

Bot releases are visible (Hide)

moleculer - v0.14.0-beta7

Published by icebob almost 5 years ago

Minimum Node version is 10

The Node version 8 LTS lifecycle has been ended on December 31, 2019, so the minimum required Node version is 10.

Event parameter validation

Similar to action parameter validation, the event parameter validation is supported.
Like in action definition, you should define params in even definition and the built-in Validator validates the parameters in events.

// mailer.service.js
module.exports = {
    name: "mailer",
    events: {
        "send.mail": {
            // Validation schema
            params: {
                from: "string|optional",
                to: "email",
                subject: "string"
            },
            handler(ctx) {
                this.logger.info("Event received, parameters OK!", ctx.params);
            }
        }
    }
};

The validation errors are not sent back to the caller, they are logged or you can catch them with the new global error handler.

Other fixes

moleculer - v0.13.12

Published by icebob almost 5 years ago

Changes

  • fix expire time updating issue in MemoryCacher. #630
  • fix action hook calling issue in mixins. #631
  • fix NATS transporter "Invalid Subject" issue. #620
  • update dependencies.
moleculer - v0.14.0-beta6

Published by icebob almost 5 years ago

Fastest validator upgraded to 1.x.x

fastest-validator, the default validation has been upgraded to the 1.0.0 version. It means breaking changes but the new version faster and contains many sanitization functions.
If you use custom rules, you should upgrade your codes. Check the changes here.

Problematic metrics dependencies removed

The gc-stats and event-loop-stats optional dependencies removed and moved to devDependencies. If you want to use them, you should install manually.

moleculer - v0.13.11

Published by icebob about 5 years ago

Changes

  • fix retry issue in case of remote calls & disabled preferLocal options. #599
  • update dependencies.
moleculer - v0.13.10

Published by icebob about 5 years ago

New

Customizable serializer for Redis cacher by @shawnmcknight #589

The default serializer is the JSON Serializer but you can change it in Redis cacher options.

You can use any built-in Moleculer serializer or use a custom one.

Example to set the built-in MessagePack serializer:

const broker = new ServiceBroker({
    nodeID: "node-123",
    cacher: {
        type: "Redis",
        options: {
            ttl: 30,

            // Using MessagePack serializer to store data.
            serializer: "MsgPack",

            redis: {
                host: "my-redis"
            }
        }
    }
});

Cluster mode of Redis cacher by Gadi-Manor #539

Redis cacher supports cluster mode.

Example

const broker = new ServiceBroker({
    cacher: {
        type: "Redis",
        options: {
            ttl: 30, 

            cluster: {
                nodes: [
                    { port: 6380, host: "127.0.0.1" },
                    { port: 6381, host: "127.0.0.1" },
                    { port: 6382, host: "127.0.0.1" }
                ],
                options: { /* More information: https://github.com/luin/ioredis#cluster */ }
            }	
        }
    }
});

Changes

  • update dependencies.
  • update Typescript definitions by @shawnmcknight.
  • fix Protocol Buffer definitions by @fugufish.
moleculer - v0.14.0-beta1

Published by icebob over 5 years ago

Migration guide from 0.13 to 0.14

Breaking changes

Communication protocol has been changed

The Moleculer communication protocol has been changed. The new protocol version is 4.
It means the new Moleculer 0.14 nodes can't communicate with old <= 0.13 nodes.

Validation settings changed

The validation: true broker options was removed to follow other module configuration. Use validator option, instead.

Enable validation with built-in validator (default option)

const broker = new ServiceBroker({
    validator: true
});

Disable validation/validator

const broker = new ServiceBroker({
    validator: false
});

Use custom validation

const broker = new ServiceBroker({
    validator: new MyCustomValidator()
});

The broker.use removed

The broker.use has been deprecated in version 0.13 and now it is removed. Use middleware: [] broker options to define middlewares.

loading middleware after the broker has started is no longer available.

The $node.health response changed

The $node.health action's response has been changed. The transit property is removed. To get transit metrics, use the new $node.metrics internal action.

Middleware shorthand definition is dropped

In previous versions you could define middleware which wraps the localAction hook with a simple Function.
In version 0.14 this legacy shorthand is dropped. When you define a middleware as a Function, the middleware handler will call it as an initialization and pass the ServiceBroker instance as a parameter.

Old shorthand middleware definition as a Function

const MyMiddleware = function(next, action) {
    return ctx => next(ctx);
};

const broker = new ServiceBroker({
    middlewares: [MyMiddleware]
});

New middleware definition as a Function

const MyMiddleware = function(broker) {
    // Create a custom named logger
    const myLogger = broker.getLogger("MY-LOGGER");

    return {
        localAction: function(next, action) {
            return ctx => {
                myLogger.info(`${action.name} has been called`);
                return next(ctx);
            }
        }
    }
};

const broker = new ServiceBroker({
    middlewares: [MyMiddleware]
});

The localEvent middleware hook signature changed

Old signature

// my-middleware.js
module.exports = {
    // Wrap local event handlers
    localEvent(next, event) {
        return (payload, sender, event) => {
            return next(payload, sender, event);
        };
    },
};

New context-based signature

// my-middleware.js
module.exports = {
    // Wrap local event handlers
    localEvent(next, event) {
        return (ctx) => {
            return next(ctx);
        };
    },
};

New

Context-based events

The new 0.14 version comes context-based event handler. It is very useful when you are using event-driven architecture and you would like to tracing the event. The Event Context is same as Action Context. They are the same properties except a few new properties related to the event.
It doesn't mean you should rewrite all existing event handlers. Moleculer detects the signature if your event handler. If it finds that the signature is "user.created(ctx) { ... }, it will call it with Event Context. If not, it will call with old arguments & the 4th argument will be the Event Context, like "user.created"(payload, sender, eventName, ctx) {...}

Use Context-based event handler & emit a nested event

module.exports = {
    name: "accounts",
    events: {
        "user.created"(ctx) {
            console.log("Payload:", ctx.params);
            console.log("Sender:", ctx.nodeID);
            console.log("We have also metadata:", ctx.meta);
            console.log("The called event name:", ctx.eventName);

            ctx.emit("accounts.created", { user: ctx.params.user });
        }
    }
};

New built-in metrics

Moleculer v0.14 comes with a brand-new and entirely rewritten metrics module. It is now a built-in module. It collects a lot of internal Moleculer & process metric values. You can easily define your custom metrics. There are several built-in metrics reporters like Console, Prometheus, Datadog, ...etc.
Multiple reporters can be defined.

Enable metrics & define console reporter

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            "Console"
        ]
    }
});

Define custom metrics

// posts.service.js
module.exports = {
    name: "posts",

    actions: {
        get(ctx) {
            // Update metrics
            this.broker.metrics.increment("posts.get.total");
            return posts;
        }
    },

    created() {
        // Register new custom metrics
        this.broker.metrics.register({ type: "counter", name: "posts.get.total" });
    }
};

Enable metrics & define Prometheus reporter with filtering

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Prometheus",
                options: {
                    port: 3030,
                    includes: ["moleculer.**"],
                    excludes: ["moleculer.transit.**"]
                }
            }
        ]
    }
});

Supported metric types

  • counter - A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero. For example, you can use a counter to represent the number of requests served, tasks completed, or errors.

  • gauge - A gauge is a metric that represents a single numerical value that can arbitrarily go up and down. Gauges are typically used for measured values like current memory usage, but also "counts" that can go up and down, like the number of concurrent requests.

  • histogram - A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets. It also provides a sum of all observed values and calculates configurable quantiles over a sliding time window.

  • info - An info is a single string or number value like process arguments, hostname or version numbers.

Internal metrics

Process metrics

  • process.arguments (info)
  • process.pid (info)
  • process.ppid (info)
  • process.eventloop.lag.min (gauge)
  • process.eventloop.lag.avg (gauge)
  • process.eventloop.lag.max (gauge)
  • process.eventloop.lag.count (gauge)
  • process.memory.heap.size.total (gauge)
  • process.memory.heap.size.used (gauge)
  • process.memory.rss (gauge)
  • process.memory.external (gauge)
  • process.memory.heap.space.size.total (gauge)
  • process.memory.heap.space.size.used (gauge)
  • process.memory.heap.space.size.available (gauge)
  • process.memory.heap.space.size.physical (gauge)
  • process.memory.heap.stat.heap.size.total (gauge)
  • process.memory.heap.stat.executable.size.total (gauge)
  • process.memory.heap.stat.physical.size.total (gauge)
  • process.memory.heap.stat.available.size.total (gauge)
  • process.memory.heap.stat.used.heap.size (gauge)
  • process.memory.heap.stat.heap.size.limit (gauge)
  • process.memory.heap.stat.mallocated.memory (gauge)
  • process.memory.heap.stat.peak.mallocated.memory (gauge)
  • process.memory.heap.stat.zap.garbage (gauge)
  • process.uptime (gauge)
  • process.internal.active.handles (gauge)
  • process.internal.active.requests (gauge)
  • process.versions.node (info)
  • process.gc.time (gauge)
  • process.gc.total.time (gauge)
  • process.gc.executed.total (gauge)

OS metrics

  • os.memory.free (gauge)
  • os.memory.total (gauge)
  • os.uptime (gauge)
  • os.type (info)
  • os.release (info)
  • os.hostname (info)
  • os.arch (info)
  • os.platform (info)
  • os.user.uid (info)
  • os.user.gid (info)
  • os.user.username (info)
  • os.user.homedir (info)
  • os.network.address (info)
  • os.network.mac (info)
  • os.datetime.unix (gauge)
  • os.datetime.iso (info)
  • os.datetime.utc (info)
  • os.datetime.tz.offset (gauge)
  • os.cpu.load.1 (gauge)
  • os.cpu.load.5 (gauge)
  • os.cpu.load.15 (gauge)
  • os.cpu.utilization (gauge)
  • os.cpu.user (gauge)
  • os.cpu.system (gauge)
  • os.cpu.total (gauge)
  • os.cpu.info.model (info)
  • os.cpu.info.speed (gauge)
  • os.cpu.info.times.user (gauge)
  • os.cpu.info.times.sys (gauge)

Moleculer metrics

  • moleculer.node.type (info)
  • moleculer.node.versions.moleculer (info)
  • moleculer.node.versions.protocol (info)
  • moleculer.broker.namespace (info)
  • moleculer.broker.started (gauge)
  • moleculer.broker.local.services.total (gauge)
  • moleculer.broker.middlewares.total (gauge)
  • moleculer.registry.nodes.total (gauge)
  • moleculer.registry.nodes.online.total (gauge)
  • moleculer.registry.services.total (gauge)
  • moleculer.registry.service.endpoints.total (gauge)
  • moleculer.registry.actions.total (gauge)
  • moleculer.registry.action.endpoints.total (gauge)
  • moleculer.registry.events.total (gauge)
  • moleculer.registry.event.endpoints.total (gauge)
  • moleculer.request.bulkhead.inflight (gauge)
  • moleculer.request.timeout.total (counter)
  • moleculer.request.retry.attempts.total (counter)
  • moleculer.request.fallback.total (counter)
  • moleculer.request.total (counter)
  • moleculer.request.active (gauge)
  • moleculer.request.error.total (counter)
  • moleculer.request.time (histogram)
  • moleculer.request.levels (counter)
  • moleculer.event.emit.total (counter)
  • moleculer.event.broadcast.total (counter)
  • moleculer.event.broadcast-local.total (counter)
  • moleculer.event.received.total (counter)
  • moleculer.transit.publish.total (counter)
  • moleculer.transit.receive.total (counter)
  • moleculer.transit.requests.active (gauge)
  • moleculer.transit.streams.send.active (gauge)
  • moleculer.transporter.packets.sent.total (counter)
  • moleculer.transporter.packets.sent.bytes (counter)
  • moleculer.transporter.packets.received.total (counter)
  • moleculer.transporter.packets.received.bytes (counter)

Built-in reporters

All reporters have the following options:

{
    includes: null,
    excludes: null,

    metricNamePrefix: null,
    metricNameSuffix: null,

    metricNameFormatter: null,
    labelNameFormatter: null
}

Console reporter

This is a debugging reporter which prints metrics to the console periodically.

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Console",
                options: {
                    interval: 5 * 1000,
                    logger: null,
                    colors: true,
                    onlyChanges: true
                }
            }
        ]
    }
});

CSV reporter

CSV reporter saves changed to CSV file.

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "CSV",
                options: {
                    folder: "./reports/metrics",
                    delimiter: ",",
                    rowDelimiter: "\n",

                    mode: MODE_METRIC, // MODE_METRIC, MODE_LABEL

                    types: null,

                    interval: 5 * 1000,

                    filenameFormatter: null,
                    rowFormatter: null,
                }
            }
        ]
    }
});

Datadog reporter

Datadog reporter sends metrics to the Datadog server.

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Datadog",
                options: {
                    host: "my-host",
                    apiVersion: "v1",
                    path: "/series",
                    apiKey: process.env.DATADOG_API_KEY,
                    defaultLabels: (registry) => ({
                        namespace: registry.broker.namespace,
                        nodeID: registry.broker.nodeID
                    }),
                    interval: 10 * 1000
                }
            }
        ]
    }
});

Event reporter

Event reporter sends Moleculer events with metric values.

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Event",
                options: {
                    eventName: "$metrics.snapshot",

                    broadcast: false,
                    groups: null,

                    onlyChanges: false,

                    interval: 5 * 1000,
                }
            }
        ]
    }
});

Prometheus reporter

Prometheus reporter publishes metrics in Prometheus format. The Prometheus server can collect them. Default port is 3030.

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Prometheus",
                options: {
                    port: 3030,
                    path: "/metrics",
                    defaultLabels: registry => ({
                        namespace: registry.broker.namespace,
                        nodeID: registry.broker.nodeID
                    })
                }
            }
        ]
    }
});

StatsD reporter

The StatsD reporter sends metric values to StatsD server via UDP.

const broker = new ServiceBroker({
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "StatsD",
                options: {
                    protocol: "udp",
                    host: "localhost",
                    port: 8125,

                    maxPayloadSize: 1300,
                }
            }
        ]
    }
});

New tracing feature

An enhanced tracing middleware has been implemented in version 0.14. It support several exporters, custom tracing spans and integration with instrumentation libraries (like dd-trace).

Enable tracing

const broker = new ServiceBroker({
    tracing: true
});

Tracing with console exporter

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Console",
                options: {
                    width: 80,
                    colors: true,
                }
            }
        ]        
    }
});

Tracing with Zipkin exporter

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Zipkin",
                options: {
                    baseURL: "http://zipkin-server:9411",
                }
            }
        ]        
    }
});

Add context values to span tags

In action defintion you can define which Context params or meta values want to add to the span tags.

Example

// posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                // Add `ctx.params.id` and `ctx.meta.loggedIn.username` values
                // to tracing span tags.
                tags: {
                    params: ["id"],
                    meta: ["loggedIn.username"],
                    response: ["id", "title"] // add data to tags from the action response.
            },
            async handler(ctx) {
                // ...
            }
        }
    }
});

Example with all properties of params without meta (actually it is the default)

// posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                // Add all params without meta
                tags: {
                    params: true,
                    meta: false,
            },
            async handler(ctx) {
                // ...
            }
        }
    }
});

Example with custom function
Please note, the tags function will be called two times in case of success execution. First with ctx, and second times with ctx & response as the response of action call.

// posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                tags(ctx, response) {
                    return {
                        params: ctx.params,
                        meta: ctx.meta,
                        custom: {
                            a: 5
                        },
                        response
                    };
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
});

Example with all properties of params in event definition

// posts.service.js
module.exports = {
    name: "posts",
    events: {
        "user.created": {
            tracing: {
                // Add all params without meta
                tags: {
                    params: true,
                    meta: false,
            },
            async handler(ctx) {
                // ...
            }
        }
    }
});

Built-in exporters

Console exporter

This is a debugging exporter which prints the full local trace to the console.

Please note that it can't follow remote calls, only locals.

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Console",
                options: {
                    logger: null,
                    colors: true,
                    width: 100,
                    gaugeWidth: 40
                }
            }
        ]
    }
});

Datadog exporter

Datadog exporter sends tracing data to Datadog server via dd-trace. It is able to merge tracing spans between instrumented Node.js modules and Moleculer modules.

TODO screenshot

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Datadog",
                options: {
                    agentUrl: process.env.DD_AGENT_URL || "http://localhost:8126",
                    env: process.env.DD_ENVIRONMENT || null,
                    samplingPriority: "AUTO_KEEP",
                    defaultTags: null,
                    tracerOptions: null,
                }
            }
        ]
    }
});

To use this exporter, install the dd-trace module with npm install dd-trace --save command.

Event exporter

Event exporter sends Moleculer events ($tracing.spans) with tracing data.

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Event",
                options: {
                    eventName: "$tracing.spans",

                    sendStartSpan: false,
                    sendFinishSpan: true,

                    broadcast: false,

                    groups: null,

                    /** @type {Number} Batch send time interval. */
                    interval: 5,

                    spanConverter: null,

                    /** @type {Object?} Default span tags */
                    defaultTags: null

                }
            }
        ]
    }
});

Event (legacy) exporter

This is another event exporter which sends legacy moleculer events (metrics.trace.span.start & metrics.trace.span.finish). It is compatible with <= 0.13 Moleculer metrics trace events.

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            "EventLegacy"
        ]
    }
});

Jaeger exporter

Jaeger exporter sends tracing spans information to a Jaeger server.

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Jaeger",
                options: {
                    /** @type {String?} HTTP Reporter endpoint. If set, HTTP Reporter will be used. */
                    endpoint: null,                    
                    /** @type {String} UDP Sender host option. */
                    host: "127.0.0.1",
                    /** @type {Number?} UDP Sender port option. */
                    port: 6832,

                    /** @type {Object?} Sampler configuration. */
                    sampler: {
                        /** @type {String?} Sampler type */
                        type: "Const",

                        /** @type: {Object?} Sampler specific options. */
                        options: {}
                    },

                    /** @type {Object?} Additional options for `Jaeger.Tracer` */
                    tracerOptions: {},

                    /** @type {Object?} Default span tags */
                    defaultTags: null
                }
            }
        ]
    }
});

To use this exporter, install the jaeger-client module with npm install jaeger-client --save command.

Zipkin exporter

Zipkin exporter sends tracing spans information to a Zipkin server.

const broker = new ServiceBroker({
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Zipkin",
                options: {
                    /** @type {String} Base URL for Zipkin server. */
                    baseURL: process.env.ZIPKIN_URL || "http://localhost:9411",

                    /** @type {Number} Batch send time interval. */
                    interval: 5,

                    /** @type {Object} Additional payload options. */
                    payloadOptions: {

                        /** @type {Boolean} Set `debug` property in v2 payload. */
                        debug: false,

                        /** @type {Boolean} Set `shared` property in v2 payload. */
                        shared: false
                    },

                    /** @type {Object?} Default span tags */
                    defaultTags: null
                }
            }
        ]
    }
});

Custom tracing spans

// posts.service.js
module.exports = {
    name: "posts",
    actions: {
        async find(ctx) {
            const span1 = ctx.startSpan("get data from DB", {
                tags: {
                    ...ctx.params
                }
            }); 
            const data = await this.getDataFromDB(ctx.params);
            span1.finish();

            const span2 = ctx.startSpan("populating");
            const res = await this.populate(data);
            span2.finish();

            return res;
        }
    }
};

Caller action

There is a new caller property in Context. It contains the action name of the caller when you use ctx.call in action handlers.

broker2.createService({
    name: "greeter",
    actions: {
        hello(ctx) {
            this.logger.info(`This action is called from '${ctx.caller}' on '${ctx.nodeID}'`);
        }
    }
});

NodeID conflict handling

Having remote nodes with same nodeID in the same namespace can cause communication problems. In v0.14 ServiceBroker checks the nodeIDs of remote nodes. If some node has the same nodeID, the broker will throw a fatal error and stop the process.

Sharding built-in strategy

There is a new built-in shard invocation strategy. It uses a key value from context params or meta to route the request a specific node. It means the same key value will be route to the same node.

Example with shard key as name param in context

const broker = new ServiceBroker({
    registry: {
        strategy: "Shard",
        strategyOptions: {
            shardKey: "name"
        }
    }
});

Example with shard key as user.id meta value in context

const broker = new ServiceBroker({
    registry: {
        strategy: "Shard",
        strategyOptions: {
            shardKey: "#user.id"
        }
    }
});

All available options of Shard strategy

const broker = new ServiceBroker({
    registry: {
        strategy: "Shard",
        strategyOptions: {
            shardKey: "#user.id",
            vnodes: 10,
            ringSize: 1000,
            cacheSize: 1000
        }
    }
});

Extending internal services

Now the internal services can be extended. You can define mixin schema for every internal service under internalServices broker option.

// moleculer.config.js
module.exports = {
    nodeID: "node-1",
    logger: true,
    internalServices: {
        $node: {
            actions: {
                // Call as `$node.hello`
                hello(ctx) {
                    return `Hello Moleculer!`;
                }
            }
        }
    }
};

Action hook inside action definition

Sometimes it's better to define action hooks inside action definition instead of service hooks property.

broker.createService({
    name: "greeter",
    hooks: {
        before: {
            "*"(ctx) {
                broker.logger.info(chalk.cyan("Before all hook"));
            },
            hello(ctx) {
                broker.logger.info(chalk.magenta("  Before hook"));
            }
        },
        after: {
            "*"(ctx, res) {
                broker.logger.info(chalk.cyan("After all hook"));
                return res;
            },
            hello(ctx, res) {
                broker.logger.info(chalk.magenta("  After hook"));
                return res;
            }
        },
    },

    actions: {
        hello: {
            hooks: {
                before(ctx) {
                    broker.logger.info(chalk.yellow.bold("    Before action hook"));
                },
                after(ctx, res) {
                    broker.logger.info(chalk.yellow.bold("    After action hook"));
                    return res;
                }
            },

            handler(ctx) {
                broker.logger.info(chalk.green.bold("      Action handler"));
                return `Hello ${ctx.params.name}`;
            }
        }
    }
});    

Output

INFO  - Before all hook
INFO  -   Before hook
INFO  -     Before action hook
INFO  -       Action handler
INFO  -     After action hook
INFO  -   After hook
INFO  - After all hook

Metadata in broker options

There is a new metadata property in broker options to store custom values. You can use the metadata property in your custom middlewares or strategies.

const broker2 = new ServiceBroker({
    nodeID: "broker-2",
    transporter: "NATS",
    metadata: {
        region: "eu-west1"
    }
});

This information is available in response of $node.list action.

Enhanced hot-reload feature

In v0.14 the built-in hot-reload feature was entirely rewritten. Now, it can detect dependency-graph between service files and other loaded (with require) files. This means that the hot-reload mechanism now watches the service files and their dependencies. Every time a file change is detected the hot-reload mechanism will track the affected services and will restart them.

New middleware hooks

There are some new middleware hooks.

registerLocalService

It's called before registering a local service instance.

Signature

// my-middleware.js
module.exports = {
    registerLocalService(next) {
        return (svc) => {
            return next(svc);
        };
    }
}

serviceCreating

It's called before a local service instance creating. At this point the service mixins are resolved, so the service schema is merged completely.

Signature

// my-middleware.js
module.exports = {
    serviceCreating(service, schema) {
        // Modify schema
        schema.myProp = "John";
    }
}

transitPublish

It's called before communication packet publishing.

Signature

// my-middleware.js
module.exports = {
    transitPublish(next) {
        return (packet) => {
            return next(packet);
        };
    },
}

transitMessageHandler

It's called before transit receives & parses an incoming message

Signature

// my-middleware.js
module.exports = {
    transitMessageHandler(next) {
        return (cmd, packet) => {
            return next(cmd, packet);
        };
    }
}

transporterSend

It's called before transporter send a communication packet (after serialization). Use it to encrypt or compress the packet buffer.

Signature

// my-middleware.js
module.exports = {
    transporterSend(next) {
        return (topic, data, meta) => {
            // Do something with data
            return next(topic, data, meta);
        };
    }
}

transporterReceive

It's called after transporter received a communication packet (before serialization). Use it to decrypt or decompress the packet buffer.

Signature

// my-middleware.js
module.exports = {
    transporterReceive(next) {
        return (cmd, data, s) => {
            // Do something with data
            return next(cmd, data, s);
        };
    }
}

New built-in middlewares

Encryption

AES encryption middleware protects all inter-services communications that use the transporter module.
This middleware uses built-in Node crypto library.

const { Middlewares } = require("moleculer");

// Create broker
const broker = new ServiceBroker({
    middlewares: [
        Middlewares.Transmit.Encryption("secret-password", "aes-256-cbc", initVector) // "aes-256-cbc" is the default
    ]
});

Compression

Compression middleware reduces the size of messages that go through the transporter module.
This middleware uses built-in Node zlib library.

const { Middlewares } = require("moleculer");

// Create broker
const broker = new ServiceBroker({
    middlewares: [
        Middlewares.Transmit.Compression("deflate") // or "deflateRaw" or "gzip"
    ]
});

Transit Logger

Transit logger middleware allows to easily track the messages that are exchanged between services.

const { Middlewares } = require("moleculer");

// Create broker
const broker = new ServiceBroker({
    middlewares: [
        Middlewares.Debugging.TransitLogger({
            logPacketData: false,
            folder: null,
            colors: {
                send: "magenta",
                receive: "blue"
            },
            packetFilter: ["HEARTBEAT"]
        })
    ]
});

Action Logger

Action Logger middleware tracks "how" service actions were executed.

const { Middlewares } = require("moleculer");

// Create broker
const broker = new ServiceBroker({
    middlewares: [
        Middlewares.Debugging.ActionLogger({
            logParams: true,
            logResponse: true,
            folder: null,
            colors: {
                send: "magenta",
                receive: "blue"
            },
            whitelist: ["**"]
        })
    ]
});

Load middlewares by names

To load built-in middlewares, use its names in middleware broker option.

const { Middlewares } = require("moleculer");

// Extend with custom middlewares
Middlewares.MyCustom = {
    created(broker) {
        broker.logger.info("My custom middleware is created!");
    }
};


const broker1 = new ServiceBroker({
    logger: true,
    middlewares: [
        // Load by middleware name
        "MyCustom"
    ]
});    

Global error handler

There is a new global error handler in ServiceBroker. It can be defined in broker options as errorHandler(err, info).
It catches unhandled errors in action & event handlers.

Catch, handle & log the error

const broker = new ServiceBroker({
    errorHandler(err, info) {

        this.logger.warn("Error handled:", err);
    }
});

Catch & throw further the error

const broker = new ServiceBroker({
    errorHandler(err, info) {
        this.logger.warn("Error handled:", err);
        throw err; // Throw further
    }
});

The info object contains the broker and the service instances, the current context and the action or the event definition.

Async storage for current context

ServiceBroker has a continuous local storage in order to store the current context. It means you don't need to always pass the ctx from actions to service methods. You can get it with this.currentContext.

// greeter.service.js
module.exports = {
    name: "greeter",
    actions: {
        hello(ctx) {
            return this.Promise.resolve()
                .then(() => this.doSomething());

        }
    },
    methods: {
        doSomething() {
            const ctx = this.currentContext;
            return ctx.call("other.service");
        }
    }
});

Buffer supporting improved in serializers

In earlier version, if request, response or event data was a Buffer, the schema-based serializers convert it to JSON string which was not very efficient. In this version all schema-based serializers (ProtoBuf, Avro, Thrift) can detect the type of data & convert it based on the best option and send always as binary data.

Other notable changes

  • Kafka transporter upgrade to support kafka-node@4.
  • rename ctx.metrics to ctx.tracing.
  • broker.hotReloadService method has been removed.
  • new hasEventListener & getEventListeners broker method.
  • new uidGenerator broker options to overwrite the default UUID generator code.
moleculer - v0.13.9

Published by icebob over 5 years ago

New

Cache locking feature by @tiaod #490

Example to enable cacher locking:

cacher: {
  ttl: 60,
  lock: true, // Set to true to enable cache locks. Default is disabled.
}

//  Or
cacher: {
  ttl: 60,
  lock: {
    ttl: 15, //the maximum amount of time you want the resource locked in seconds
    staleTime: 10, // If the ttl is less than this number, means that the resources are staled
  }
}

// Disable the lock
cacher: {
  ttl: 60,
  lock: {
    enable: false, // Set to false to disable.
    ttl: 15, //the maximum amount of time you want the resource locked in seconds
    staleTime: 10, // If the ttl is less than this number, means that the resources are staled
  }
}

Example for Redis cacher with redlock library:

const broker = new ServiceBroker({
  cacher: {
    type: "Redis",
    options: {
      // Prefix for keys
      prefix: "MOL",
      // set Time-to-live to 30sec.
      ttl: 30,
      // Turns Redis client monitoring on.
      monitor: false,
      // Redis settings
      redis: {
        host: "redis-server",
        port: 6379,
        password: "1234",
        db: 0
      },
      lock: {
        ttl: 15, //the maximum amount of time you want the resource locked in seconds
        staleTime: 10, // If the ttl is less than this number, means that the resources are staled
      },
      // Redlock settings
      redlock: {
        // Redis clients. Support node-redis or ioredis. By default will use the local client.
        clients: [client1, client2, client3],
        // the expected clock drift; for more details
        // see http://redis.io/topics/distlock
        driftFactor: 0.01, // time in ms

        // the max number of times Redlock will attempt
        // to lock a resource before erroring
        retryCount: 10,

        // the time in ms between attempts
        retryDelay: 200, // time in ms

        // the max time in ms randomly added to retries
        // to improve performance under high contention
        // see https://www.awsarchitectureblog.com/2015/03/backoff.html
        retryJitter: 200 // time in ms
      }
    }
  }
});

Changes

  • fix event wildcard handling in case of NATS transporter and disabled balancer #517
  • update typescript d.ts file. #501 #521
  • fix context calling options cloning.
  • service modification support for ES6 classes #514
  • fix null, 0 & false return value issue in case of ProtoBuf serializer #511
moleculer - v0.13.8

Published by icebob over 5 years ago

Changes

  • fix missing field in ProtoBuf & Thrift serializers #496
moleculer - v0.13.7

Published by icebob over 5 years ago

Changes

  • fix ioredis dependency in typescript definition file #476
moleculer - v0.13.6

Published by icebob over 5 years ago

New

Secure service settings

To protect your tokens & API keys, define a $secureSettings: [] property in service settings and set the protected property keys.
The protected settings won't be published to other nodes and it won't appear in Service Registry. They are only available under this.settings inside the service functions.

Example

// mail.service.js
module.exports = {
    name: "mailer",
    settings: {
        $secureSettings: ["transport.auth.user", "transport.auth.pass"],

        from: "[email protected]",
        transport: {
            service: 'gmail',
            auth: {
                user: '[email protected]',
                pass: 'yourpass'
            }
        }
    }        
    // ...
};

Changes

  • fix cacher.clean issue #435
  • add disableVersionCheck option for broker transit options. It can disable protocol version checking logic in Transit. Default: false
  • improve Typescript definition file. #442 #454
  • waitForServices accept versioned service names (e.g.: v2.posts).
  • update dependencies (plus using semver ranges in dependencies)
moleculer - v0.13.5

Published by icebob almost 6 years ago

New

Conditional caching

It's a common issue that you enable caching for an action but sometimes you don't want to get data from cache. To solve it you may set ctx.meta.$cache = false before calling and the cacher won't send cached responses.

Example

// Turn off caching for this request
broker.call("greeter.hello", { name: "Moleculer" }, { meta: { $cache: false }}))

Other solution is that you use a custom function which enables or disables caching for every request. The function gets the ctx Context instance so it has access any params or meta data.

Example

// greeter.service.js
module.exports = {
    name: "greeter",
    actions: {
        hello: {
            cache: {
                enabled: ctx => ctx.params.noCache !== true,
                keys: ["name"]
            },
            handler(ctx) {
                this.logger.debug(chalk.yellow("Execute handler"));
                return `Hello ${ctx.params.name}`;
            }
        }
    }
};

// Use custom `enabled` function to turn off caching for this request
broker.call("greeter.hello", { name: "Moleculer", noCache: true }))

LRU memory cacher

An LRU memory cacher has been added to the core modules. It uses the familiar lru-cache library.

Example

let broker = new ServiceBroker({ cacher: "MemoryLRU" });
let broker = new ServiceBroker({
    logLevel: "debug",
    cacher: {
        type: "MemoryLRU",
        options: {
            // Maximum items
            max: 100,
            // Time-to-Live
            ttl: 3
        }
    }
});

Changes

  • throw the error further in loadService method so that Runner prints the correct error stack.
  • new packetLogFilter transit option to filter packets in debug logs (e.g. HEARTBEAT packets) by @faeron
  • the Redis cacher clean & del methods handle array parameter by @dkuida
  • the Memory cacher clean & del methods handle array parameter by @icebob
  • fix to handle version: 0 as a valid version number by @ngraef
moleculer - v0.13.4

Published by icebob almost 6 years ago

Changes

  • catch errors in getCpuUsage() method.
  • support multiple urls in AMQP transporter by @urossmolnik
  • fix AMQP connection recovery by @urossmolnik
  • add transit.disableReconnect option to disable reconnecting logic at broker starting by @Gadi-Manor
  • catch os.userInfo errors in health action by @katsanva
  • allow specifying 0 as retries #404 by @urossmolnik
  • fix GraceFulTimeoutError bug #400
  • fix event return handling to avoid localEvent error handling issue in middleware #403
  • update fastest-validator to the 0.6.12 version
  • update all dependencies
moleculer - 0.13.3

Published by icebob about 6 years ago

Changes

  • update dependencies
  • fix MQTTS connection string protocol from mqtt+ssl:// to mqtts:// by @AndreMaz
  • Moleculer Runner supports typescript configuration file moleculer.config.ts
  • fix to call service start after hot-reloading.
  • fix Bluebird warning in service loading #381 by @faeron
  • fix waitForServices definition in index.d.ts #358
  • fix cpuUsage issue #379 by @faeron
moleculer - v0.13.2

Published by icebob about 6 years ago

Changes

  • update dependencies
  • add Notepack (other MsgPack) serializer
  • skipProcessEventRegistration broker option to disable process.on shutdown event handlers which stop broker.
  • make unique service dependencies
  • add socketOptions to AMQP transporter options. #330
  • fix unhandled promise in AMQP transporter connect method.
  • add autoDeleteQueues option to AMQP transporter. #341
  • ES6 support has improved. #348
  • add qos transporter option to MQTT transporter. Default: 0
  • add topicSeparator transporter option to MQTT transporter. Default: .
  • fix MQTT transporter disconnect logic (waiting for in-flight messages)
  • add support for non-defined defaultOptions variables #350
  • update ioredis to v4
moleculer - v0.13.1

Published by icebob over 6 years ago

Changes

  • improve index.d.ts
  • support Duplex streams #325
  • update dependencies
moleculer - v0.13.0

Published by icebob over 6 years ago

Migration guide from v0.12.x to v0.13.x is here.

Breaking changes

Streaming support

Built-in streaming support has just been implemented. Node.js streams can be transferred as request params or as response. You can use it to transfer uploaded file from a gateway or encode/decode or compress/decompress streams.

Why is it a breaking change?

Because the protocol has been extended with a new field and it caused a breaking change in schema-based serializators (ProtoBuf, Avro). Therefore, if you use ProtoBuf or Avro, you won't able to communicate with the previous (<=0.12) brokers. Using JSON or MsgPack serializer, there is nothing extra to do.

Examples

Send a file to a service as a stream

const stream = fs.createReadStream(fileName);

broker.call("storage.save", stream, { meta: { filename: "avatar-123.jpg" }});

Please note, the params should be a stream, you cannot add any more variables to the request. Use the meta property to transfer additional data.

Receiving a stream in a service

module.exports = {
    name: "storage",
    actions: {
        save(ctx) {
            const s = fs.createWriteStream(`/tmp/${ctx.meta.filename}`);
            ctx.params.pipe(s);
        }
    }
};

Return a stream as response in a service

module.exports = {
    name: "storage",
    actions: {
        get: {
            params: {
                filename: "string"
            },
            handler(ctx) {
                return fs.createReadStream(`/tmp/${ctx.params.filename}`);
            }
        }
    }
};

Process received stream on the caller side

const filename = "avatar-123.jpg";
broker.call("storage.get", { filename })
    .then(stream => {
        const s = fs.createWriteStream(`./${filename}`);
        stream.pipe(s);
        s.on("close", () => broker.logger.info("File has been received"));
    })

AES encode/decode example service

const crypto = require("crypto");
const password = "moleculer";

module.exports = {
    name: "aes",
    actions: {
        encrypt(ctx) {
            const encrypt = crypto.createCipher("aes-256-ctr", password);
            return ctx.params.pipe(encrypt);
        },

        decrypt(ctx) {
            const decrypt = crypto.createDecipher("aes-256-ctr", password);
            return ctx.params.pipe(decrypt);
        }
    }
};

Better Service & Broker lifecycle handling

The ServiceBroker & Service lifecycle handler logic has already been improved. The reason for amendment was a problem occuring during loading more services locally; they could call each others' actions before started execution. It generally causes errors if database connecting process started in the started event handler.

This problem has been fixed with a probable side effect: causing errors (mostly in unit tests) if you call the local services without broker.start().

It works in the previous version

const { ServiceBroker } = require("moleculer");

const broker = new ServiceBroker();

broker.loadService("./math.service.js");

broker.call("math.add", { a: 5, b: 3 }).then(res => console.log);
// Prints: 8

From v0.13 it throws a ServiceNotFoundError exception, because the service is only loaded but not started yet.

Correct logic

const { ServiceBroker } = require("moleculer");

const broker = new ServiceBroker();

broker.loadService("./math.service.js");

broker.start().then(() => {
    broker.call("math.add", { a: 5, b: 3 }).then(res => console.log);
    // Prints: 8
});

or with await

broker.loadService("./math.service.js");

await broker.start();

const res = await broker.call("math.add", { a: 5, b: 3 });
console.log(res);
// Prints: 8

Similar issue has been fixed at broker shutdown. Previously when you stopped a broker, which while started to stop local services, it still acccepted incoming requests from remote nodes.

The shutdown logic has also been changed. When you call broker.stop, at first broker publishes an empty service list to remote nodes, so they route the requests to other instances.

Default console logger

No longer need to set logger: console in broker options, because ServiceBroker uses console as default logger.

const broker = new ServiceBroker();
// It will print log messages to the console

Disable loggging (e.g. in tests)

const broker = new ServiceBroker({ logger: false });

Changes in internal event sending logic

The $ prefixed internal events will be transferred if they are called by emit or broadcast. If you don't want to transfer them, use the broadcastLocal method.

From v0.13, the $ prefixed events mean built-in core events instead of internal "only-local" events.

Improved Circuit Breaker

Threshold-based circuit-breaker solution has been implemented. It uses a time window to check the failed request rate. Once the threshold value is reached, it trips the circuit breaker.

const broker = new ServiceBroker({
    nodeID: "node-1",
    circuitBreaker: {
        enabled: true,
        threshold: 0.5,
        minRequestCount: 20,
        windowTime: 60, // in seconds
        halfOpenTime: 5 * 1000,
        check: err => err && err.code >= 500
    }
});

Instead of failureOnTimeout and failureOnReject properties, there is a new check() function property in the options. It is used by circuit breaker in order to detect which error is considered as a failed request.

You can override these global options in action definition, as well.

module.export = {
    name: "users",
    actions: {
        create: {
            circuitBreaker: {
                // All CB options can be overwritten from broker options.
                threshold: 0.3,
                windowTime: 30
            },
            handler(ctx) {}
        }
    }
};

CB metrics events removed

The metrics circuit breaker events have been removed due to internal event logic changes.
Use the $circuit-breaker.* events instead of metrics.circuit-breaker.* events.

Improved Retry feature (with exponential backoff)

The old retry feature has been improved. Now it uses exponential backoff for retries. The old solution retries the request immediately in failures.
The retry options have also been changed in the broker options. Every option is under the retryPolicy property.

const broker = new ServiceBroker({
    nodeID: "node-1",
    retryPolicy: {
        enabled: true,
        retries: 5,
        delay: 100,
        maxDelay: 2000,
        factor: 2,
        check: err => err && !!err.retryable
    }
});

Overwrite the retries value in calling option
The retryCount calling options has been renamed to retries.

broker.call("posts.find", {}, { retries: 3 });

There is a new check() function property in the options. It is used by the Retry middleware in order to detect which error is a failed request and needs a retry. The default function checks the retryable property of errors.

These global options can be overridden in action definition, as well.

module.export = {
    name: "users",
    actions: {
        find: {
            retryPolicy: {
                // All Retry policy options can be overwritten from broker options.
                retries: 3,
                delay: 500
            },
            handler(ctx) {}
        },
        create: {
            retryPolicy: {
                // Disable retries for this action
                enabled: false
            },
            handler(ctx) {}
        }
    }
};

Changes in context tracker

There are also some changes in context tracker configuration.

const broker = new ServiceBroker({
    nodeID: "node-1",
    tracking: {
        enabled: true,
        shutdownTimeout: 5000
    }
});

Disable tracking in calling option at calling

broker.call("posts.find", {}, { tracking: false });

The shutdown timeout can be overwritten by $shutdownTimeout property in service settings.

Removed internal statistics module

The internal statistics module ($node.stats) is removed. Yet you need it, download from here, load as a service and call the stat.snapshot to receive the collected statistics.

Renamed errors

Some errors have been renamed in order to follow name conventions.

  • ServiceNotAvailable -> ServiceNotAvailableError
  • RequestRejected -> RequestRejectedError
  • QueueIsFull -> QueueIsFullError
  • InvalidPacketData -> InvalidPacketDataError

Context nodeID changes

The ctx.callerNodeID has been removed. The ctx.nodeID contains the target or caller nodeID. If you need the current nodeID, use ctx.broker.nodeID.

Enhanced ping method

It returns Promise with results of ping responses. Moreover, the method is renamed to broker.ping.

Ping a node with 1sec timeout

broker.ping("node-123", 1000).then(res => broker.logger.info(res));

Output:

{ 
    nodeID: 'node-123', 
    elapsedTime: 16, 
    timeDiff: -3 
}

Ping all known nodes

broker.ping().then(res => broker.logger.info(res));

Output:

{ 
    "node-100": { 
        nodeID: 'node-100', 
        elapsedTime: 10, 
        timeDiff: -2 
    } ,
    "node-101": { 
        nodeID: 'node-101', 
        elapsedTime: 18, 
        timeDiff: 32 
    }, 
    "node-102": { 
        nodeID: 'node-102', 
        elapsedTime: 250, 
        timeDiff: 850 
    } 
}

Amended cacher key generation logic

When you didn't define keys at caching, the cacher hashed the whole ctx.params and used as a key to store the content. This method was too slow and difficult to implement to other platforms. Therefore we have changed it. The new method is simpler, the key generator concatenates all property names & values from ctx.params.

However, the problem with this new logic is that the key can be very long. It can cause performance issues when you use too long keys to get or save cache entries. To avoid it, there is a maxParamsLength option to limit the key length. If it is longer than the configured limit, the cacher calculates a hash (SHA256) from the full key and add it to the end of key.

The minimum of maxParamsLength is 44 (SHA 256 hash length in Base64).

To disable this feature, set it to 0 or null.

Generate a full key from the whole params

cacher.getCacheKey("posts.find", { id: 2, title: "New post", content: "It can be very very looooooooooooooooooong content. So this key will also be too long" });
// Key: 'posts.find:id|2|title|New post|content|It can be very very looooooooooooooooooong content. So this key will also be too long'

Generate a limited key with hash

const broker = new ServiceBroker({
    logger: console,
    cacher: {
        type: "Memory",
        options: {
            maxParamsLength: 60
        }
    }
});

cacher.getCacheKey("posts.find", { id: 2, title: "New post", content: "It can be very very looooooooooooooooooong content. So this key will also be too long" });
// Key: 'posts.find:id|2|title|New pL4ozUU24FATnNpDt1B0t1T5KP/T5/Y+JTIznKDspjT0='

Of course, you can use your custom solution with keygen cacher options like earlier.

Cacher matcher changed

The cacher matcher code also changed in cacher.clean method. The previous (wrong) matcher couldn't handle dots (.) properly in patterns. E.g the posts.* pattern cleaned the posts.find.something keys, too. Now it has been fixed, but it means that you should use posts.** pattern because the params and meta values can contain dots.

Changed Moleculer errors signature

The following Moleculer Error classes constructor arguments is changed to constructor(data):

  • ServiceNotFoundError
  • ServiceNotAvailableError
  • RequestTimeoutError
  • RequestSkippedError
  • RequestRejectedError
  • QueueIsFullError
  • MaxCallLevelError
  • ProtocolVersionMismatchError
  • InvalidPacketDataError

Before

throw new ServiceNotFoundError("posts.find", "node-123");

Now

throw new ServiceNotFoundError({ action: "posts.find",  nodeID: "node-123" });

New

New state-of-the-art middlewares

We have been improved the current middleware handler and enriched it with a lot of useful features. As a result, you can hack more internal flow logic with custom middlewares (e.g. event sending, service creating, service starting...etc)

The new one is an Object with hooks instead of a simple Function. However, the new solution is backward compatible, so you don't need to migrate your old middlewares.

A new middleware with all available hooks

const MyCustomMiddleware = {
    // Wrap local action handlers (legacy middleware handler)
    localAction(next, action) {

    },

    // Wrap remote action handlers
    remoteAction(next, action) {

    },

    // Wrap local event handlers
    localEvent(next, event) {

    }

    // Wrap broker.createService method
    createService(next) {

    }

    // Wrap broker.destroyService method
    destroyService(next) {

    }

    // Wrap broker.call method
    call(next) {

    }

    // Wrap broker.mcall method
    mcall(next) {

    }

    // Wrap broker.emit method
    emit(next) {

    },

    // Wrap broker.broadcast method
    broadcast(next) {

    },

    // Wrap broker.broadcastLocal method
    broadcastLocal(next) {

    },

    // After a new local service created (sync)
    serviceCreated(service) {

    },

    // Before a local service started (async)
    serviceStarting(service) {

    },

    // After a local service started (async)
    serviceStarted(service) {

    },

    // Before a local service stopping (async)
    serviceStopping(service) {

    },

    // After a local service stopped (async)
    serviceStopped(service) {

    },

    // After broker is created (async)
    created(broker) {

    },

    // Before broker starting (async)
    starting(broker) {

    },

    // After broker started (async)
    started(broker) {

    },

    // Before broker stopping (async)
    stopping(broker) {

    },

    // After broker stopped (async)
    stopped(broker) {

    }
}

Use it in broker options

const broker = new ServiceBroker({
    middlewares: [
        MyCustomMiddleware
    ]
});

Wrapping handlers

Some hooks are wrappers. It means you need to wrap the original handler and return a new Function.
Wrap hooks where the first parameter is next.

Wrap local action handler

const MyDoSomethingMiddleware = {
    localAction(next, action) {
        if (action.myFunc) {
            // Wrap the handler

            return function(ctx) {
                doSomethingBeforeHandler(ctx);

                return handler(ctx)
                    .then(res => {
                        doSomethingAfterHandler(res);
                        // Return the original result
                        return res;
                    })
                    .catch(err => {
                        doSomethingAfterHandlerIfFailed(err);

                        // Throw further the error
                        throw err;
                    });
            }
        }

        // If the feature is disabled we don't wrap it, return the original handler
        // So it won't cut down the performance for actions where the feature is disabled.
        return handler;
    }
};

Decorate broker (to extend functions)

Other hooks are to help you to decorate new features in ServiceBroker & services.

Decorate broker with a new allCall method

const broker = new ServiceBroker({
    middlewares: [
        {
            // After broker is created
            created(broker) {
                // Call action on all available nodes
                broker.allCall = function(action, params, opts = {}) {
                    const nodeIDs = this.registry.getNodeList({ onlyAvailable: true })
                        .map(node => node.id);

                    // Make direct call to the given Node ID
                    return Promise.all(nodeIDs.map(nodeID => broker.call(action, params, Object.assign({ nodeID }, opts))));
                }
            }
        }
    ]
});

await broker.start();

// Call `$node.health` on every nodes & collect results
const res = await broker.allCall("$node.health");

Decorate services with a new method

const broker = new ServiceBroker({
    middlewares: [
        {
            // After a new local service created
            serviceCreated(service) {
                // Call action on all available nodes
                service.customFunc = function() {
                    // Do something
                }.bind(service);
            }
        }
    ]
});

In service schema:

module.export = {
    name: "users",
    actions: {
        find(ctx) {
            // Call the new custom function
            this.customFunc();
        }
    }
};

The mixins can do similar things, so we prefer mixins to this decorating.

Many internal features are exposed to internal middlewares

Due to the new advanced middlewares, we could bring out many integrated features to middlewares. They are available under require("moleculer").Middlewares property, but they load automatically.

New internal middlewares:

  • Action hook handling
  • Validator
  • Bulkhead
  • Cacher
  • Context tracker
  • Circuit Breaker
  • Timeout
  • Retry
  • Fallback
  • Error handling
  • Metrics

Turn off the automatic loading with internalMiddlewares: false broker option. In this case you have to add them to middlewares: [] broker option.

The broker.use method is deprecated. Use middlewares: [] in the broker options instead.

Action hooks

Define action hooks to wrap certain actions coming from mixins.
There are before, after and error hooks. Assign it to a specified action or all actions (*) in service.
The hook can be a Function or a String. The latter must be a local service method name.

Before hooks

const DbService = require("moleculer-db");

module.exports = {
    name: "posts",
    mixins: [DbService]
    hooks: {
        before: {
            // Define a global hook for all actions
            // The hook will call the `resolveLoggedUser` method.
            "*": "resolveLoggedUser",

            // Define multiple hooks
            remove: [
                function isAuthenticated(ctx) {
                    if (!ctx.user)
                        throw new Error("Forbidden");
                },
                function isOwner(ctx) {
                    if (!this.checkOwner(ctx.params.id, ctx.user.id))
                        throw new Error("Only owner can remove it.");
                }
            ]
        }
    },

    methods: {
        async resolveLoggedUser(ctx) {
            if (ctx.meta.user)
                ctx.user = await ctx.call("users.get", { id: ctx.meta.user.id });
        }
    }
}

After & Error hooks

const DbService = require("moleculer-db");

module.exports = {
    name: "users",
    mixins: [DbService]
    hooks: {
        after: {
            // Define a global hook for all actions to remove sensitive data
            "*": function(ctx, res) {
                // Remove password
                delete res.password;

                // Please note, must return result (either the original or a new)
                return res;
            },
            get: [
                // Add a new virtual field to the entity
                async function (ctx, res) {
                    res.friends = await ctx.call("friends.count", { query: { follower: res._id }});

                    return res;
                },
                // Populate the `referrer` field
                async function (ctx, res) {
                    if (res.referrer)
                        res.referrer = await ctx.call("users.get", { id: res._id });

                    return res;
                }
            ]
        },
        error: {
            // Global error handler
            "*": function(ctx, err) {
                this.logger.error(`Error occurred when '${ctx.action.name}' action was called`, err);

                // Throw further the error
                throw err;
            }
        }
    }
};

The recommended use case is to create mixins filling up the service with methods and in hooks set method names.

Mixin

module.exports = {
    methods: {
        checkIsAuthenticated(ctx) {
            if (!ctx.meta.user)
                throw new Error("Unauthenticated");
        },
        checkUserRole(ctx) {
            if (ctx.action.role && ctx.meta.user.role != ctx.action.role)
                throw new Error("Forbidden");
        },
        checkOwner(ctx) {
            // Check the owner of entity
        }
    }
}

Use mixin methods in hooks

const MyAuthMixin = require("./my.mixin");

module.exports = {
    name: "posts",
    mixins: [MyAuthMixin]
    hooks: {
        before: {
            "*": ["checkIsAuthenticated"],
            create: ["checkUserRole"],
            update: ["checkUserRole", "checkOwner"],
            remove: ["checkUserRole", "checkOwner"]
        }
    },

    actions: {
        find: {
            // No required role
            handler(ctx) {}
        },
        create: {
            role: "admin",
            handler(ctx) {}
        },
        update: {
            role: "user",
            handler(ctx) {}
        }
    }
};

New Bulkhead fault-tolerance feature

Bulkhead feature is an internal middleware in Moleculer. Use it to control the concurrent request handling of actions.

Global settings in the broker options. Applied to all registered local actions.

const broker = new ServiceBroker({
    bulkhead: {
        enabled: true,
        concurrency: 3,
        maxQueueSize: 10,
    }
});

The concurrency value restricts the concurrent request executions. If maxQueueSize is bigger than 0, broker queues additional requests, if all slots are taken. If queue size reaches maxQueueSize limit or it is 0, broker will throw QueueIsFull error for every addition request.

These global options can be overriden in action definition, as well.

module.export = {
    name: "users",
    actions: {
        find: {
            bulkhead: {
                enabled: false
            },
            handler(ctx) {}
        },
        create: {
            bulkhead: {
                // Increment the concurrency value
                // for this action
                concurrency: 10
            },
            handler(ctx) {}
        }
    }
};

Fallback in action definition

Due to the exposed Fallback middleware, fallback response can be set in the action definition, too.

Please note, this fallback response will only be used if the error occurs within action handler. If the request is called from a remote node and the request is timed out on the remote node, the fallback response is not be used. In this case, use the fallbackResponse in calling option.

Fallback as function

module.exports = {
    name: "recommends",
    actions: {
        add: {
            fallback: (ctx, err) => "Some cached result",
            //fallback: "fakeResult",
            handler(ctx) {
                // Do something
            }
        }
    }
};

Fallback as method name string

module.exports = {
    name: "recommends",
    actions: {
        add: {
            // Call the 'getCachedResult' method when error occurred
            fallback: "getCachedResult",
            handler(ctx) {
                // Do something
            }
        }
    },

    methods: {
        getCachedResult(ctx, err) {
            return "Some cached result";
        }
    }
};

Action visibility

The action has a new visibility property to control the visibility & callability of service actions.

Available values:

  • published or null: public action. It can be called locally, remotely and can be published via API Gateway
  • public: public action, can be called locally & remotely but not published via API GW
  • protected: can be called only locally (from local services)
  • private: can be called only internally (via this.actions.xy() within service)
module.exports = {
    name: "posts",
    actions: {
        // It's published by default
        find(ctx) {},
        clean: {
            // Callable only via `this.actions.clean`
            visibility: "private",
            handler(ctx) {}
        }
    },
    methods: {
        cleanEntities() {
            // Call the action directly
            return this.actions.clean();
        }
    }
}

The default value is null (means published) due to backward compatibility.

New Thrift serializer

There is a new built-in Thrift serializer.

const broker = new ServiceBroker({
    serializer: "Thrift"
});

To use this serializer install the thrift module with npm install thrift --save command.

Enhanced log level configuration

A new module-based log level configuration was added. The log level can be set for every Moleculer module. Use of wildcard is allowed.

const broker = new ServiceBroker({
    logger: console,
    logLevel: {
        "MY.**": false,         // Disable logs
        "TRANS": "warn",        // Only 'warn ' and 'error' log entries
        "*.GREETER": "debug",   // All log entries
        "**": "debug",          // All other modules use this level
    }
});

Please note, it works only with default console logger. In case of external loggers (Pino, Windows, Bunyan, ...etc), these log levels must be applied.

These settings are evaluated from top to bottom, so the ** level must be the last property.

Internal modules: BROKER, TRANS, TX as transporter, CACHER, REGISTRY.

For services, the name comes from the service name. E.g. POSTS.
A version is used as a prefix. E.g. V2.POSTS

The old global log level settings works, as well.

const broker = new ServiceBroker({
    logger: console,
    logLevel: "warn"
});

New short log formatter

A new short log formatter was also added. It is similar to the default, but doesn't print the date and nodeID.

const broker = new ServiceBroker({
    logFormatter: "short"
});

Output

[19:42:49.055Z] INFO  MATH: Service started.

Load services also with glob patterns

Moleculer Runner loads services also from glob patterns. It is useful when loading all services except certain ones.

$ moleculer-runner services !services/others/**/*.service.js services/others/mandatory/main.service.js

Explanations:

  • services - legacy mode. Load all services from the services folder with **/*.service.js file mask
  • !services/others/**/*.service.js - skip all services in the services/others folder and sub-folders.
  • services/others/mandatory/main.service.js - load the exact service

Glob patterns work in the SERVICES enviroment variables, as well.

MemoryCacher cloning

There is a new clone property in the MemoryCacher options. If it's true, the cacher clones the cached data before returning.
If received value is modified, enable this option. Note: it cuts down the performance.

Enable cloning

const broker = new ServiceBroker({ 
    cacher: {
        type: "Memory",
        options: {
            clone: true
        }
    }
});

This feature uses the lodash _.cloneDeep method. To change cloning method set a Function to the clone option instead of a Boolean.

Custom clone function with JSON parse & stringify:

const broker = new ServiceBroker({ 
    cacher: {
        type: "Memory",
        options: {
            clone: data => JSON.parse(JSON.stringify(data))
        }
    }
});

Changes

  • service instances has a new property named fullName containing service version & service name.
  • the Action has a rawName property containing action name without service name.
  • new $node.options internal action to get the current broker options.
  • Context.create & new Context signature changed.
  • removed Context metrics methods. All metrics feature moved to the Metrics middleware.
  • ctx.timeout moved to ctx.options.timeout.
  • removed ctx.callerNodeID.
  • ctx.endpoint is a new property pointing to target Endpoint. For example you can check with ctx.endpoint.local flag whether the request is remote or local.
  • lazily generated ctx.id, i.e. only generated at access. ctx.generateID() was removed.
  • renamed service lifecycle methods in service instances (not in service schema!)
  • extended transit.stat.packets with byte-based statistics.
  • utils.deprecate method was created for deprecation.
  • Transporter supports mqtt+ssl://, rediss:// & amqps:// protocols in connection URIs.
  • fixed circular objects handling in service schema (e.g.: Joi validator problem)

Deprecations

  • broker.use() has been deprecated. Use middlewares: [...] in broker options instead.
moleculer - v0.12.8

Published by icebob over 6 years ago

Fix typescript def.

moleculer - v0.12.7

Published by icebob over 6 years ago

Changes

  • fix action disabling with mixins #298
  • Fix metrics options and add findNextActionEndpoint to index.d.ts
  • update dependencies
  • set maxReconnectAttempts to -1 in NATS client to try reconnecting continuously
moleculer - v0.12.6

Published by icebob over 6 years ago

Changes

  • update dependencies
  • The breakLength is changed to Infinity (single-line printing) for better log processing when logger prints objects and arrays.
  • adds ability to customise console object/array printing #285
    const util = require("util");
    
    const broker = new ServiceBroker({
        logger: true,
        logObjectPrinter: o => util.inspect(o, { depth: 4, colors: false, breakLength: 50 }) // `breakLength: 50` activates multi-line object
    });    
    
moleculer - v0.12.5

Published by icebob over 6 years ago

Changes

  • fix AMQP logs. #270
  • fix transferred retryable error handling
  • broker.createService supports ES6 classes
  • fix broken promise chain if trackContext is enabled