UNPKG

@moleculer/channels

Version:
467 lines (409 loc) 13.5 kB
/* * @moleculer/channels * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) * MIT Licensed */ "use strict"; const _ = require("lodash"); const { Context, METRIC } = require("moleculer"); const { BrokerOptionsError, ServiceSchemaError, MoleculerError } = require("moleculer").Errors; const Adapters = require("./adapters"); const C = require("./constants"); /** * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance * @typedef {import("moleculer").LoggerInstance} Logger Logger instance * @typedef {import("moleculer").Service} Service Moleculer service * @typedef {import("moleculer").Middleware} Middleware Moleculer middleware * @typedef {import("./adapters/base")} BaseAdapter Base adapter class */ /** * @typedef {Object} DeadLetteringOptions Dead-letter-queue options * @property {Boolean} enabled Enable dead-letter-queue * @property {String} queueName Name of the dead-letter queue * @property {String} exchangeName Name of the dead-letter exchange (only for AMQP adapter) * @property {Object} exchangeOptions Options for the dead-letter exchange (only for AMQP adapter) * @property {Object} queueOptions Options for the dead-letter queue (only for AMQP adapter) */ /** * @typedef {Object} Channel Base consumer configuration * @property {String} id Consumer ID * @property {String} name Channel/Queue/Stream name * @property {String} group Consumer group name * @property {Boolean} context Create Moleculer Context * @property {Boolean} unsubscribing Flag denoting if service is stopping * @property {Number?} maxInFlight Maximum number of messages that can be processed simultaneously * @property {Number} maxRetries Maximum number of retries before sending the message to dead-letter-queue * @property {DeadLetteringOptions?} deadLettering Dead-letter-queue options * @property {Function} handler User defined handler */ /** * @typedef {Object} ChannelRegistryEntry Registry entry * @property {Service} svc Service instance class * @property {String} name Channel name * @property {Channel} chan Channel object */ /** * @typedef {Object} AdapterConfig * @property {String} type Adapter name * @property {import("./adapters/base").BaseDefaultOptions & import("./adapters/amqp").AmqpDefaultOptions & import("./adapters/kafka").KafkaDefaultOptions & import("./adapters/nats").NatsDefaultOptions & import("./adapters/redis").RedisDefaultOptions} options Adapter options */ /** * @typedef {Object} MiddlewareOptions Middleware options * @property {String|AdapterConfig} adapter Adapter name or connection string or configuration object. * @property {String?} schemaProperty Property name of channels definition in service schema. * @property {String?} sendMethodName Method name to send messages. * @property {String?} adapterPropertyName Property name of the adapter instance in broker instance. * @property {String?} channelHandlerTrigger Method name to add to service in order to trigger channel handlers. * @property {boolean?} context Using Moleculer context in channel handlers by default. */ /** * Initialize the Channels middleware. * * @param {MiddlewareOptions} mwOpts * @returns Middleware */ module.exports = function ChannelsMiddleware(mwOpts) { mwOpts = _.defaultsDeep({}, mwOpts, { adapter: null, schemaProperty: "channels", sendMethodName: "sendToChannel", adapterPropertyName: "channelAdapter", channelHandlerTrigger: "emitLocalChannelHandler", context: false }); /** @type {ServiceBroker} */ let broker; /** @type {Logger} */ let logger; /** @type {BaseAdapter} */ let adapter; let started = false; /** @type {Array<ChannelRegistryEntry>}} */ let channelRegistry = []; /** * Register cannel * @param {Service} svc * @param {Channel} chan */ function registerChannel(svc, chan) { unregisterChannel(svc, chan); channelRegistry.push({ svc, name: chan.name, chan }); } /** * Remove channel from registry * @param {Service} svc * @param {Channel=} chan */ function unregisterChannel(svc, chan) { channelRegistry = channelRegistry.filter( item => !(item.svc.fullName == svc.fullName && (chan == null || chan.name == item.name)) ); } /** * * @param {ServiceBroker} broker */ function registerChannelMetrics(broker) { if (!broker.isMetricsEnabled()) return; broker.metrics.register({ type: METRIC.TYPE_COUNTER, name: C.METRIC_CHANNELS_MESSAGES_SENT, labelNames: ["channel"], rate: true, unit: "call" }); broker.metrics.register({ type: METRIC.TYPE_COUNTER, name: C.METRIC_CHANNELS_MESSAGES_TOTAL, labelNames: ["channel", "group"], rate: true, unit: "msg" }); broker.metrics.register({ type: METRIC.TYPE_GAUGE, name: C.METRIC_CHANNELS_MESSAGES_ACTIVE, labelNames: ["channel", "group"], rate: true, unit: "msg" }); broker.metrics.register({ type: METRIC.TYPE_HISTOGRAM, name: C.METRIC_CHANNELS_MESSAGES_TIME, labelNames: ["channel", "group"], quantiles: true, unit: "msg" }); } return { name: "Channels", /** * Create lifecycle hook of service * @param {ServiceBroker} _broker */ created(_broker) { broker = _broker; logger = broker.getLogger("Channels"); // Create adapter if (!mwOpts.adapter) throw new BrokerOptionsError("Channel adapter must be defined.", { opts: mwOpts }); adapter = Adapters.resolve(mwOpts.adapter); adapter.init(broker, logger); // Populate broker with new methods if (!broker[mwOpts.sendMethodName]) { broker[mwOpts.sendMethodName] = broker.wrapMethod( "sendToChannel", (channelName, payload, opts) => { broker.metrics.increment( C.METRIC_CHANNELS_MESSAGES_SENT, { channel: channelName }, 1 ); // Transfer Context properties if (opts && opts.ctx) { if (!opts.headers) opts.headers = {}; opts.headers.$requestID = opts.ctx.requestID; opts.headers.$parentID = opts.ctx.id; opts.headers.$tracing = "" + opts.ctx.tracing; opts.headers.$level = "" + opts.ctx.level; if (opts.ctx.service) { opts.headers.$caller = opts.ctx.service.fullName; } opts.headers.$meta = adapter.serializer .serialize(opts.ctx.meta) .toString("base64"); if (opts.ctx.headers) { opts.headers.$headers = adapter.serializer .serialize(opts.ctx.headers) .toString("base64"); } delete opts.ctx; } return adapter.publish(adapter.addPrefixTopic(channelName), payload, opts); } ); } else { throw new BrokerOptionsError( `broker.${mwOpts.sendMethodName} method is already in use by another Channel middleware`, null ); } // Add adapter reference to the broker instance if (!broker[mwOpts.adapterPropertyName]) { broker[mwOpts.adapterPropertyName] = adapter; } else { throw new BrokerOptionsError( `broker.${mwOpts.adapterPropertyName} property is already in use by another Channel middleware`, null ); } registerChannelMetrics(broker); }, /** * Created lifecycle hook of service * * @param {Service} svc */ async serviceCreated(svc) { if (_.isPlainObject(svc.schema[mwOpts.schemaProperty])) { //svc.$channels = {}; // Process `channels` in the schema await broker.Promise.mapSeries( Object.entries(svc.schema[mwOpts.schemaProperty]), async ([name, def]) => { /** @type {Partial<Channel>} */ let chan; if (_.isFunction(def)) { chan = { handler: def }; } else if (_.isPlainObject(def)) { chan = _.cloneDeep(def); } else { throw new ServiceSchemaError( `Invalid channel definition in '${name}' channel in '${svc.fullName}' service!` ); } if (!_.isFunction(chan.handler)) { throw new ServiceSchemaError( `Missing channel handler on '${name}' channel in '${svc.fullName}' service!` ); } if (!chan.name) chan.name = adapter.addPrefixTopic(name); if (!chan.group) chan.group = svc.fullName; if (chan.context == null) chan.context = mwOpts.context; // Consumer ID chan.id = adapter.addPrefixTopic( `${broker.nodeID}.${svc.fullName}.${chan.name}` ); chan.unsubscribing = false; // Wrap the original handler let handler = broker.Promise.method(chan.handler).bind(svc); // Wrap the handler with custom middlewares const handler2 = broker.middlewares.wrapHandler( "localChannel", handler, chan ); let wrappedHandler = handler2; // Wrap the handler with context creating if (chan.context) { wrappedHandler = (msg, raw) => { let parentCtx, caller, meta, ctxHeaders; const headers = adapter.parseMessageHeaders(raw); if (headers) { if (headers.$requestID) { parentCtx = { id: headers.$parentID, requestID: headers.$requestID, tracing: headers.$tracing === "true", level: headers.$level ? parseInt(headers.$level) : 0 }; caller = headers.$caller; } if (headers.$meta) { meta = adapter.serializer.deserialize( Buffer.from(headers.$meta, "base64") ); } if (headers.$headers) { ctxHeaders = adapter.serializer.deserialize( Buffer.from(headers.$headers, "base64") ); } } const ctx = Context.create(broker, null, msg, { parentCtx, caller, meta, headers: ctxHeaders }); return handler2(ctx, raw); }; } chan.handler = wrappedHandler; // Add metrics for the handler if (broker.isMetricsEnabled()) { chan.handler = (...args) => { const labels = { channel: name, group: chan.group }; const timeEnd = broker.metrics.timer( C.METRIC_CHANNELS_MESSAGES_TIME, labels ); broker.metrics.increment(C.METRIC_CHANNELS_MESSAGES_TOTAL, labels); broker.metrics.increment(C.METRIC_CHANNELS_MESSAGES_ACTIVE, labels); return wrappedHandler(...args) .then(res => { timeEnd(); broker.metrics.decrement( C.METRIC_CHANNELS_MESSAGES_ACTIVE, labels ); return res; }) .catch(err => { timeEnd(); broker.metrics.decrement( C.METRIC_CHANNELS_MESSAGES_ACTIVE, labels ); throw err; }); }; } //svc.$channels[name] = chan; logger.debug( `Registering '${chan.name}' channel in '${svc.fullName}' service with group '${chan.group}'...` ); registerChannel(svc, chan); if (started) { // If middleware has already started, we should subscribe to the channel right now. await adapter.subscribe(chan, svc); } } ); // Attach method to simplify unit testing if (!svc[mwOpts.channelHandlerTrigger]) { /** * Call a local channel event handler. Useful for unit tests. * * @param {String} channelName * @param {Object} payload * @param {Object} raw * @returns */ svc[mwOpts.channelHandlerTrigger] = (channelName, payload, raw) => { svc.logger.debug( `${mwOpts.channelHandlerTrigger} called '${channelName}' channel handler` ); if (!svc.schema[mwOpts.schemaProperty][channelName]) return Promise.reject( new MoleculerError( `'${channelName}' is not registered as local channel event handler`, 500, "NOT_FOUND_CHANNEL", { channelName } ) ); // Shorthand definition if (typeof svc.schema[mwOpts.schemaProperty][channelName] === "function") return svc.schema[mwOpts.schemaProperty][channelName].call( svc, // Attach reference to service payload, raw ); // Object definition return svc.schema[mwOpts.schemaProperty][channelName].handler.call( svc, // Attach reference to service payload, raw ); }; } else { throw new BrokerOptionsError( `service.${mwOpts.channelHandlerTrigger} method is already in use by another Channel middleware`, null ); } } }, /** * Service stopping lifecycle hook. * Need to unsubscribe from the channels. * * @param {Service} svc */ async serviceStopping(svc) { await Promise.all( channelRegistry .filter(item => item.svc.fullName == svc.fullName) .map(async ({ chan }) => { await adapter.unsubscribe(chan); }) ); unregisterChannel(svc); }, /** * Start lifecycle hook of service */ async started() { logger.info("Channel adapter is connecting..."); await adapter.connect(); logger.debug("Channel adapter connected."); logger.info(`Subscribing to ${channelRegistry.length} channels...`); await broker.Promise.mapSeries( channelRegistry, async ({ chan, svc }) => await adapter.subscribe(chan, svc) ); started = true; }, /** * Stop lifecycle hook of service */ async stopped() { logger.info("Channel adapter is disconnecting..."); await adapter.disconnect(); logger.debug("Channel adapter disconnected."); started = false; } }; };