UNPKG

moleculer

Version:

Fast & powerful microservices framework for Node.JS

1,907 lines (1,670 loc) 48.9 kB
/* * moleculer * Copyright (c) 2025 MoleculerJS (https://github.com/moleculerjs/moleculer) * MIT Licensed */ "use strict"; const EventEmitter2 = require("eventemitter2").EventEmitter2; const _ = require("lodash"); const { globSync } = require("glob"); const path = require("path"); const { format } = require("util"); const Transit = require("./transit"); const Registry = require("./registry"); const E = require("./errors"); const utils = require("./utils"); const LoggerFactory = require("./logger-factory"); const Validators = require("./validators"); //const AsyncStorage = require("./async-storage"); const Cachers = require("./cachers"); const Transporters = require("./transporters"); const Serializers = require("./serializers"); const Errors = require("./errors"); const H = require("./health"); const MiddlewareHandler = require("./middleware"); const cpuUsage = require("./cpu-usage"); const { MetricRegistry, METRIC } = require("./metrics"); const { Tracer } = require("./tracing"); const C = require("./constants"); /** * Import types * * @typedef {import("./context")} Context * @typedef {import("./registry/endpoint-action")} ActionEndpoint * @typedef {import("./service")} Service * @typedef {import("./service").ServiceSchema} ServiceSchema * @typedef {import("./service").ServiceDependency} ServiceDependency * @typedef {import("./service").ActionHandler} ActionHandler * @typedef {import("./service-broker")} ServiceBrokerClass * @typedef {import("./service-broker").BrokerOptions} BrokerOptions * @typedef {import("./service-broker").CallingOptions} CallingOptions * @typedef {import("./service-broker").NodeHealthStatus} NodeHealthStatus * @typedef {import("./service-broker").MCallDefinition} MCallDefinition * @typedef {import("./service-broker").MCallCallingOptions} MCallCallingOptions * @typedef {import("./logger-factory").Logger} Logger * @typedef {import("./registry").NodeRawInfo} NodeRawInfo * @typedef {import("./registry/service-item")} ServiceItem */ /** * Default broker options * * @type {BrokerOptions} */ const defaultOptions = { namespace: "", nodeID: null, logger: true, logLevel: null, transporter: null, //"TCP", errorRegenerator: null, requestTimeout: 0 * 1000, retryPolicy: { enabled: false, retries: 5, delay: 100, maxDelay: 1000, factor: 2, // @ts-ignore check: err => err && !!err.retryable }, contextParamsCloning: false, maxCallLevel: 0, heartbeatInterval: 10, heartbeatTimeout: 30, tracking: { enabled: false, shutdownTimeout: 5000 }, disableBalancer: false, registry: { strategy: "RoundRobin", preferLocal: true, stopDelay: 100, discoverer: "Local" }, circuitBreaker: { enabled: false, threshold: 0.5, windowTime: 60, minRequestCount: 20, halfOpenTime: 10 * 1000, // @ts-ignore check: err => err && err.code >= 500 }, bulkhead: { enabled: false, concurrency: 10, maxQueueSize: 100 }, transit: { maxQueueSize: 50 * 1000, // 50k ~ 400MB, maxChunkSize: 256 * 1024, // 256KB disableReconnect: false, disableVersionCheck: false, serviceChangedDebounceTime: 1000 }, uidGenerator: null, errorHandler: null, cacher: null, serializer: null, validator: true, metrics: { enabled: false }, tracing: { enabled: false }, internalServices: true, internalMiddlewares: true, dependencyInterval: 1000, dependencyTimeout: 0, hotReload: false, middlewares: null, replOptions: null, metadata: {}, skipProcessEventRegistration: false, /** * Maximum size of objects that can be serialized * * On serialization process, check each object property size (based on length or size property value) * and trim it, if object size bigger than maxSafeObjectSize value * * @type {(number|null)} */ maxSafeObjectSize: null // ServiceFactory: null, // ContextFactory: null // Promise: null }; const INTERNAL_MIDDLEWARES = [ "ActionHook", "Validator", "Bulkhead", "Cacher", "ContextTracker", "CircuitBreaker", "Timeout", "Retry", "Fallback", "ErrorHandler", "Tracing", "Metrics", "Debounce", "Throttle" ]; /** * Service broker class * * @class ServiceBroker * @implements {ServiceBrokerClass} */ class ServiceBroker { /** * Creates an instance of ServiceBroker. * * @param {BrokerOptions} options * * @memberof ServiceBroker */ constructor(options) { try { this.options = _.defaultsDeep(options, defaultOptions); // Custom Promise lib if (this.options.Promise) { this.Promise = this.options.Promise; } else { // Use native Promise lib this.Promise = Promise; } utils.polyfillPromise(this.Promise); // Broker started flag this.started = false; /** @type {Boolean} Broker is starting inital services flag*/ this.servicesStarting = false; /** @type {Boolean} Broker stopping flag*/ this.stopping = false; // Class factories this.ServiceFactory = this.options.ServiceFactory || require("./service"); this.ContextFactory = this.options.ContextFactory || require("./context"); // Namespace this.namespace = this.options.namespace || ""; // Metadata this.metadata = this.options.metadata || {}; // Self nodeID this.nodeID = this.options.nodeID || utils.getNodeID(); // Instance ID this.instanceID = utils.generateToken(); // Internal maps this.services = []; // Internal event bus this.localBus = new EventEmitter2({ wildcard: true, maxListeners: 100 }); // Log Factory this.loggerFactory = new LoggerFactory(this); this.loggerFactory.init(this.options.logger); // Logger this.logger = this.getLogger("broker"); this.logger.info(`Moleculer v${this.MOLECULER_VERSION} is starting...`); this.logger.info(`Namespace: ${this.namespace || "<not defined>"}`); this.logger.info(`Node ID: ${this.nodeID}`); // Async storage for Contexts //this.scope = new AsyncStorage(this); // Metrics Registry this.metrics = new MetricRegistry(this, this.options.metrics); this.metrics.init(this); this.registerMoleculerMetrics(); // Middleware handler this.middlewares = new MiddlewareHandler(this); this.middlewares.middlewareInterceptors["call"] = this.interceptCallMiddleware; // Service registry this.registry = new Registry(this); // Cacher this.cacher = Cachers.resolve(this.options.cacher); if (this.cacher) { this.cacher.init(this); const name = utils.getConstructorName(this.cacher); this.logger.info(`Cacher: ${name}`); } // Serializer this.serializer = Serializers.resolve(this.options.serializer); this.serializer.init(this); // Error regenerator this.errorRegenerator = Errors.resolveRegenerator(this.options.errorRegenerator); this.errorRegenerator.init(this); const serializerName = utils.getConstructorName(this.serializer); this.logger.info(`Serializer: ${serializerName}`); // Validator if (this.options.validator) { this.validator = Validators.resolve(this.options.validator); if (this.validator) { const validatorName = utils.getConstructorName(this.validator); this.logger.info(`Validator: ${validatorName}`); this.validator.init(this); } } // Tracing this.tracer = new Tracer(this, this.options.tracing); this.tracer.init(); // Register middlewares this.registerMiddlewares(this.options.middlewares); // Transit & Transporter if (this.options.transporter) { const tx = Transporters.resolve(this.options.transporter); this.transit = new Transit(this, tx, this.options.transit); const txName = utils.getConstructorName(tx); this.logger.info(`Transporter: ${txName}`); if (this.options.disableBalancer) { if (tx.hasBuiltInBalancer) { this.logger.info("The broker built-in balancer is DISABLED."); } else { this.logger.warn( `The ${txName} has no built-in balancer. Broker balancer is ENABLED.` ); this.options.disableBalancer = false; } } } // Change the call method if balancer is disabled if (this.options.disableBalancer) { this.call = this.callWithoutBalancer; } if (this.options.transit.serviceChangedDebounceTime > 0) { // Create debounced localServiceChanged const origLocalServiceChanged = this.localServiceChanged; this.localServiceChanged = _.debounce( () => origLocalServiceChanged.call(this), this.options.transit.serviceChangedDebounceTime ); } this.registry.init(); // Register internal actions if (this.options.internalServices) this.registerInternalServices(this.options.internalServices); // Call `created` event handler in middlewares this.callMiddlewareHookSync("created", [this]); // Call `created` event handler from options if (utils.isFunction(this.options.created)) this.options.created(this); // Graceful exit this._closeFn = () => { /* istanbul ignore next */ this.stop() .catch(err => this.logger.error(err)) .then(() => process.exit(0)); }; process.setMaxListeners(0); if (this.options.skipProcessEventRegistration === false) { process.on("beforeExit", this._closeFn); process.on("exit", this._closeFn); process.on("SIGINT", this._closeFn); process.on("SIGTERM", this._closeFn); } } catch (err) { if (this.logger) this.fatal("Unable to create ServiceBroker.", err, true); else { /* eslint-disable-next-line no-console */ console.error("Unable to create ServiceBroker.", err); process.exit(1); } } } /** * Register middlewares (user & internal) * * @param {MiddlewareHandler.Middleware[]} userMiddlewares * @memberof ServiceBroker */ registerMiddlewares(userMiddlewares) { // Register user middlewares if (Array.isArray(userMiddlewares) && userMiddlewares.length > 0) { _.compact(userMiddlewares).forEach(mw => this.middlewares.add(mw)); } if (this.options.internalMiddlewares) { // Register internal middlewares INTERNAL_MIDDLEWARES.forEach(mw => this.middlewares.add(mw)); if (this.options.hotReload) { // 14. Hot Reload this.middlewares.add("HotReload"); } } this.logger.info(`Registered ${this.middlewares.count()} middleware(s).`); this.createService = this.wrapMethod("createService", this.createService); this.registerLocalService = this.wrapMethod( "registerLocalService", this.registerLocalService ); this.destroyService = this.wrapMethod("destroyService", this.destroyService); this.call = this.wrapMethod("call", this.call); this.callWithoutBalancer = this.wrapMethod("call", this.callWithoutBalancer); this.mcall = this.wrapMethod("mcall", this.mcall); this.emit = this.wrapMethod("emit", this.emit); this.broadcast = this.wrapMethod("broadcast", this.broadcast); this.broadcastLocal = this.wrapMethod("broadcastLocal", this.broadcastLocal); this.metrics.set(METRIC.MOLECULER_BROKER_MIDDLEWARES_TOTAL, this.middlewares.count()); } /** * It is necessary to keep the context of the call when using call middleware. */ interceptCallMiddleware(createMiddleware) { return next => { let result = null; const call = createMiddleware((...args) => (result = next(...args))); return (...args) => { const promise = call(...args); if (result) promise.ctx = result.ctx; return promise; }; }; } /** * Register Moleculer Core metrics. */ registerMoleculerMetrics() { if (!this.isMetricsEnabled()) return; // --- MOLECULER NODE METRICS --- this.metrics .register({ name: METRIC.MOLECULER_NODE_TYPE, type: METRIC.TYPE_INFO, description: "Moleculer implementation type" }) .set("nodejs"); this.metrics .register({ name: METRIC.MOLECULER_NODE_VERSIONS_MOLECULER, type: METRIC.TYPE_INFO, description: "Moleculer version number" }) .set(ServiceBroker.MOLECULER_VERSION); this.metrics .register({ name: METRIC.MOLECULER_NODE_VERSIONS_PROTOCOL, type: METRIC.TYPE_INFO, description: "Moleculer protocol version" }) .set(ServiceBroker.PROTOCOL_VERSION); // --- MOLECULER BROKER METRICS --- this.metrics .register({ name: METRIC.MOLECULER_BROKER_NAMESPACE, type: METRIC.TYPE_INFO, description: "Moleculer namespace" }) .set(this.namespace); this.metrics .register({ name: METRIC.MOLECULER_BROKER_STARTED, type: METRIC.TYPE_GAUGE, description: "ServiceBroker started" }) .set(0); this.metrics .register({ name: METRIC.MOLECULER_BROKER_LOCAL_SERVICES_TOTAL, type: METRIC.TYPE_GAUGE, description: "Number of local services" }) .set(0); this.metrics .register({ name: METRIC.MOLECULER_BROKER_MIDDLEWARES_TOTAL, type: METRIC.TYPE_GAUGE, description: "Number of local middlewares" }) .set(0); } /** * Start broker. If has transporter, transporter.connect will be called. * * @memberof ServiceBroker */ start() { const startTime = Date.now(); return this.Promise.resolve() .then(() => { //this.tracer.restartScope(); //this.scope.enable(); }) .then(() => { return this.callMiddlewareHook("starting", [this]); }) .then(() => { if (this.transit) return this.transit.connect(); }) .then(() => { // Call service `started` handlers const startingServices = this.services.map(svc => svc._start.call(svc)); // Set servicesStarting, so new services created from now on will be started when registered this.servicesStarting = true; // Wait for services `started` handlers return this.Promise.all(startingServices).catch(err => { /* istanbul ignore next */ this.logger.error("Unable to start all services.", err); throw err; }); }) .then(() => { this.started = true; this.servicesStarting = false; this.metrics.set(METRIC.MOLECULER_BROKER_STARTED, 1); this.broadcastLocal("$broker.started"); }) .then(() => { if (this.transit) return this.transit.ready(); }) .then(() => { return this.callMiddlewareHook("started", [this]); }) .then(() => { if (utils.isFunction(this.options.started)) return this.options.started(this); }) .then(() => { const duration = Date.now() - startTime; this.logger.info( `✔ ServiceBroker with ${ this.services.length } service(s) started successfully in ${utils.humanize(duration)}.` ); }); } /** * Stop broker. If has transporter, transporter.disconnect will be called. * * @memberof ServiceBroker */ stop() { this.started = false; return this.Promise.resolve() .then(() => { if (this.transit) { this.registry.regenerateLocalRawInfo(true, true); // Send empty node info in order to block incoming requests return this.registry.discoverer.sendLocalNodeInfo(); } }) .then(() => { return this.Promise.delay(this.options.registry.stopDelay); }) .then(() => { this.stopping = true; return this.callMiddlewareHook("stopping", [this], { reverse: true }); }) .then(() => { // Call service `stopped` handlers return this.Promise.all(this.services.map(svc => svc._stop.call(svc))).catch( err => { /* istanbul ignore next */ this.logger.error("Unable to stop all services.", err); this.broadcastLocal("$broker.error", { error: err, module: "broker", type: C.FAILED_STOPPING_SERVICES }); } ); }) .then(() => { if (this.transit) { return this.transit.disconnect(); } }) .then(() => { if (this.cacher) { return this.cacher.close(); } }) .then(() => { if (this.metrics) { return this.metrics.stop(); } }) .then(() => { if (this.tracer) { return this.tracer.stop(); } }) .then(() => { return this.registry.stop(); }) .then(() => { return this.callMiddlewareHook("stopped", [this], { reverse: true }); }) .then(() => { if (utils.isFunction(this.options.stopped)) return this.options.stopped(this); }) .catch(err => { /* istanbul ignore next */ this.logger.error(err); }) .then(() => { this.logger.info("ServiceBroker is stopped. Good bye."); this.metrics.set(METRIC.MOLECULER_BROKER_STARTED, 0); this.broadcastLocal("$broker.stopped"); if (this.options.skipProcessEventRegistration === false) { process.removeListener("beforeExit", this._closeFn); process.removeListener("exit", this._closeFn); process.removeListener("SIGINT", this._closeFn); process.removeListener("SIGTERM", this._closeFn); } }) .then(() => { return this.loggerFactory.stop(); }) .catch(() => { // Silent }); } /** * Switch the console to REPL mode. * * @example * broker.start().then(() => broker.repl()); * @returns */ repl() { let repl; try { repl = require("moleculer-repl"); } catch (error) { // eslint-disable-next-line no-console console.error( "The 'moleculer-repl' package is missing. Please install it with 'npm install moleculer-repl' command." ); this.logger.error( "The 'moleculer-repl' package is missing. Please install it with 'npm install moleculer-repl' command." ); this.logger.debug("ERROR", error); return; } if (repl) { return repl(this, this.options.replOptions); } } /** * Global error handler. * * @param {Error} err * @param {object} info * @returns * @memberof ServiceBroker */ errorHandler(err, info) { if (this.options.errorHandler) { return this.options.errorHandler.call(this, err, info); } throw err; } /** * Wrap a method with middlewares * * @param {string} name * @param {Function} handler * @param {any=} bindTo * @param {Object=} opts * @returns {any} * * @memberof ServiceBroker */ wrapMethod(name, handler, bindTo, opts) { return this.middlewares.wrapMethod(name, handler, bindTo, opts); } /** * Call a handler asynchronously in all middlewares * * @param {String} name * @param {Array<any>} args * @param {Object=} opts * @returns {Promise} * * @memberof ServiceBroker */ callMiddlewareHook(name, args, opts) { return this.middlewares.callHandlers(name, args, opts); } /** * Call a handler synchronously in all middlewares * * @param {String} name * @param {Array<any>} args * @param {Object=} opts * @returns * * @memberof ServiceBroker */ callMiddlewareHookSync(name, args, opts) { return this.middlewares.callSyncHandlers(name, args, opts); } /** * Check metrics are enabled. * * @returns {boolean} * @memberof ServiceBroker */ isMetricsEnabled() { return this.metrics.isEnabled(); } /** * Check tracing is enabled. * * @returns {boolean} * @memberof ServiceBroker */ isTracingEnabled() { return this.tracer.isEnabled(); } /** * Get a custom logger for sub-modules (service, transporter, cacher, context...etc) * * @param {String} mod Name of module * @param {Record<string, any>=} props Module properties (service name, version, ...etc * @returns {Logger} * * @memberof ServiceBroker */ getLogger(mod, props) { let bindings = Object.assign( { nodeID: this.nodeID, ns: this.namespace, mod }, props ); return this.loggerFactory.getLogger(bindings); } /** * Fatal error. Print the message to console and exit the process (if need) * * @param {String} message * @param {Error=} err * @param {boolean=} [needExit=true] * * @memberof ServiceBroker */ fatal(message, err, needExit = true) { if (this.logger) this.logger.fatal(message, err); else console.error(message, err); // eslint-disable-line no-console if (needExit) process.exit(1); } /** * Load services from a folder * * @param {string} [folder="./services"] Folder of services * @param {string} [fileMask="**\/*.service.js"] Service filename mask * @returns {Number} Number of found services * * @memberof ServiceBroker */ loadServices(folder = "./services", fileMask = "**/*.service.js") { this.logger.debug(`Search services in '${folder}/${fileMask}'...`); let serviceFiles; if (Array.isArray(fileMask)) serviceFiles = fileMask.map(f => path.join(folder, f)); else serviceFiles = globSync(folder + "/" + fileMask); if (serviceFiles) serviceFiles.forEach(filename => this.loadService(filename)); return serviceFiles.length; } /** * Load a service from file * * @param {string} filePath * @returns {Service} * * @memberof ServiceBroker */ loadService(filePath) { let fName, schema; try { fName = require.resolve(path.resolve(filePath)); this.logger.debug(`Load service '${path.basename(fName)}'...`); const r = require(fName); schema = r.default != null ? r.default : r; let svc; schema = this.normalizeSchemaConstructor(schema); if (utils.isInheritedClass(schema, this.ServiceFactory)) { // Service implementation // @ts-ignore svc = new schema(this); // If broker is started, call the started lifecycle event of service if (this.started || this.servicesStarting) this._restartService(svc); } else if (utils.isFunction(schema)) { // Function svc = schema(this); if (!utils.isInheritedClass(svc, this.ServiceFactory)) { svc = this.createService(svc); } else { // If broker is started, call the started lifecycle event of service if (this.started || this.servicesStarting) this._restartService(svc); } } else if (schema) { // Schema object svc = this.createService(schema); } if (svc) { svc.__filename = fName; } return svc; } catch (e) { this.logger.error(`Failed to load service '${filePath}'`, e); this.broadcastLocal("$broker.error", { error: e, module: "broker", type: C.FAILED_LOAD_SERVICE }); throw e; } } /** * Create a new service by schema * * @param {ServiceSchema} schema Schema of service or a Service class * @param {ServiceSchema=} schemaMods Modified schema * @returns {Service} * * @memberof ServiceBroker */ createService(schema, schemaMods) { /** @type {Service} */ let service; schema = this.normalizeSchemaConstructor(schema); if (Object.prototype.isPrototypeOf.call(this.ServiceFactory, schema)) { // @ts-ignore service = new schema(this, schemaMods); } else { service = new this.ServiceFactory(this, schema, schemaMods); } // If broker has began to start its initial services yet, call the started lifecycle event of service if (this.started || this.servicesStarting) this._restartService(service); return service; } /** * Restart a hot-reloaded service after creation. * * @param {Service} service * @returns {Promise} * @memberof ServiceBroker */ _restartService(service) { return service._start.call(service).catch(err => { this.logger.error("Unable to start service.", err); this.broadcastLocal("$broker.error", { error: err, module: "broker", type: C.FAILED_RESTART_SERVICE }); }); } /** * Add a local service instance * * @param {Service} service * @memberof ServiceBroker */ addLocalService(service) { this.services.push(service); this.metrics.set(METRIC.MOLECULER_BROKER_LOCAL_SERVICES_TOTAL, this.services.length); } /** * Register a local service to Service Registry * * @param {ServiceItem} registryItem * @memberof ServiceBroker */ registerLocalService(registryItem) { this.registry.registerLocalService(registryItem); return null; } /** * Destroy a local service * * @param {Service|string|ServiceDependency} service * @returns Promise<void> * @memberof ServiceBroker */ destroyService(service) { let serviceName; let serviceVersion; /** @type {Service} */ let svc; if (utils.isString(service)) { serviceName = service; svc = this.getLocalService(service); } else if (utils.isPlainObject(service)) { serviceName = service.name; serviceVersion = service.version; svc = this.getLocalService(service); } else { svc = service; } if (!svc) { return this.Promise.reject( new E.ServiceNotFoundError({ service: serviceName, version: serviceVersion }) ); } return this.Promise.resolve() .then(() => svc._stop()) .catch(err => { /* istanbul ignore next */ this.logger.error(`Unable to stop '${svc.fullName}' service.`, err); this.broadcastLocal("$broker.error", { error: err, module: "broker", type: C.FAILED_DESTRUCTION_SERVICE }); }) .then(() => { utils.removeFromArray(this.services, svc); this.registry.unregisterService(svc.fullName, this.nodeID); this.logger.info(`Service '${svc.fullName}' is stopped.`); this.servicesChanged(true); this.metrics.set( METRIC.MOLECULER_BROKER_LOCAL_SERVICES_TOTAL, this.services.length ); }); } /** * It will be called when a new local or remote service * is registered or unregistered. * * @memberof ServiceBroker */ servicesChanged(localService = false) { this.broadcastLocal("$services.changed", { localService }); // Should notify remote nodes, because our service list is changed. if (localService && this.transit) { this.localServiceChanged(); } } /** * It's a debounced method to send INFO packets to remote nodes. */ localServiceChanged() { if (!this.stopping) { this.registry.discoverer.sendLocalNodeInfo(); } } /** * Register internal services * @param {Partial<ServiceSchema>?} opts * * @memberof ServiceBroker */ registerInternalServices(opts) { opts = utils.isObject(opts) ? opts : {}; /** @type {import("./service").ServiceSchema} */ const internalsSchema = require("./internals")(); // If it's present any custom definition, define it as the root schema and the default one as a mixin if (opts["$node"]) { const definitiveSchema = opts["$node"]; if (!definitiveSchema.mixins) definitiveSchema.mixins = []; definitiveSchema.mixins.push(internalsSchema); this.createService(definitiveSchema); } else { // Otherwise, just use the default one this.createService(internalsSchema); } } /** * Get a local service by name * * Example: * getLocalService("v2.posts"); * getLocalService({ name: "posts", version: 2 }); * * @param {String|ServiceDependency} name * @returns {Service} * * @memberof ServiceBroker */ getLocalService(name) { if (utils.isString(name)) return this.services.find(service => service.fullName == name); else if (utils.isPlainObject(name)) return this.services.find( service => service.name == name.name && service.version == name.version ); } /** * Wait for other services * * @param {String|Array<String>|ServiceDependency|Array<ServiceDependency>} service * @param {Number=} timeout Timeout in milliseconds * @param {Number=} interval Check interval in milliseconds * @returns {Promise} * * @memberof ServiceBroker */ waitForServices( service, timeout = this.options.dependencyTimeout, interval = this.options.dependencyInterval, logger = this.logger ) { let serviceNames = Array.isArray(service) ? service : [service]; serviceNames = utils.uniq( _.compact( serviceNames.map(x => { if (utils.isPlainObject(x) && x.name) { if (Array.isArray(x.version)) { return x.version.map(v => this.ServiceFactory.getVersionedFullName(x.name, v) ); } else { return this.ServiceFactory.getVersionedFullName(x.name, x.version); } } else if (utils.isString(x)) { return x; } }) ) ); if (serviceNames.length === 0) return this.Promise.resolve({ services: [], statuses: [] }); logger.info( `Waiting for service(s) '${serviceNames .map(n => (Array.isArray(n) ? n.join(" OR ") : n)) .join(", ")}'...` ); const startTime = Date.now(); return new this.Promise((resolve, reject) => { const check = () => { const serviceStatuses = serviceNames.map(name => { if (Array.isArray(name)) { return name.map(n => ({ name: n, available: this.registry.hasService(n) })); } else { return { name, available: this.registry.hasService(name) }; } }); const flattenedStatuses = _.flatMap(serviceStatuses, s => s); const names = flattenedStatuses.map(s => s.name); const availableServices = flattenedStatuses.filter(s => s.available); const isReady = serviceStatuses.every(status => Array.isArray(status) ? status.some(n => n.available) : status.available ); if (isReady) { logger.info( `Service(s) '${availableServices .map(s => s.name) .join(", ")}' are available.` ); return resolve({ services: names, statuses: flattenedStatuses }); } const unavailableServices = flattenedStatuses.filter(s => !s.available); logger.debug( format( "%d (%s) of %d services are available. %d (%s) are still unavailable. Waiting further...", availableServices.length, availableServices.map(s => s.name).join(", "), serviceStatuses.length, unavailableServices.length, unavailableServices.map(s => s.name).join(", ") ) ); if (timeout && Date.now() - startTime > timeout) return reject( new E.MoleculerServerError( "Services waiting is timed out.", 500, "WAITFOR_SERVICES", { services: names, statuses: flattenedStatuses } ) ); setTimeout(check, interval); }; check(); }); } /** * Find the next available endpoint for action * * @param {String |ActionEndpoint} actionName * @param {Object?} opts * @param {Context?} ctx * @returns {ActionEndpoint|E.MoleculerRetryableError} * * @performance-critical * @memberof ServiceBroker */ findNextActionEndpoint(actionName, opts, ctx) { if (typeof actionName !== "string") { return actionName; } else { if (opts && opts.nodeID) { const nodeID = opts.nodeID; // Direct call const endpoint = this.registry.getActionEndpointByNodeId(actionName, nodeID); if (!endpoint) { this.logger.warn(`Service '${actionName}' is not found on '${nodeID}' node.`); return new E.ServiceNotFoundError({ action: actionName, nodeID }); } return endpoint; } else { // Get endpoint list by action name const epList = this.registry.getActionEndpoints(actionName); if (!epList) { this.logger.warn(`Service '${actionName}' is not registered.`); return new E.ServiceNotFoundError({ action: actionName }); } // Get the next available endpoint const endpoint = epList.next(ctx); if (!endpoint) { const errMsg = `Service '${actionName}' is not available.`; this.logger.warn(errMsg); return new E.ServiceNotAvailableError({ action: actionName }); } return endpoint; } } } /** * Call an action * * @param {String} actionName name of action * @param {Object=} params params of action * @param {CallingOptions=} opts options of call (optional) * @returns {Promise} * * @performance-critical * @memberof ServiceBroker */ call(actionName, params, opts = {}) { if (params === undefined) params = {}; // Backward compatibility // Create context let ctx; if (opts.ctx != null) { const endpoint = this.findNextActionEndpoint(actionName, opts, opts.ctx); if (endpoint instanceof Error) { return this.Promise.reject(endpoint).catch(err => this.errorHandler(err, { actionName, params, opts }) ); } // Reused context ctx = opts.ctx; ctx.endpoint = endpoint; ctx.nodeID = endpoint.id; ctx.action = endpoint.action; ctx.service = endpoint.action.service; } else { // New root context ctx = this.ContextFactory.create(this, null, params, opts); const endpoint = this.findNextActionEndpoint(actionName, opts, ctx); if (endpoint instanceof Error) { return this.Promise.reject(endpoint).catch(err => this.errorHandler(err, { actionName, params, opts }) ); } ctx.setEndpoint(endpoint); } if (ctx.endpoint.local) { this.logger.debug("Call action locally.", { action: ctx.action.name, requestID: ctx.requestID }); // Stream redirection if (opts.stream) { ctx.stream = opts.stream; } } else { this.logger.debug("Call action on remote node.", { action: ctx.action.name, nodeID: ctx.nodeID, requestID: ctx.requestID }); } //this.setCurrentContext(ctx); let p = ctx.endpoint.action.handler(ctx); // Pointer to Context p.ctx = ctx; return p; } /** * Call an action without built-in balancer. * You don't call it directly. Broker will replace the * original 'call' method to this if you disable the * built-in balancer with the "disableBalancer" option. * * @param {String} actionName name of action * @param {Object=} params params of action * @param {Object=} opts options of call (optional) * @returns {Promise} * * @memberof ServiceBroker */ callWithoutBalancer(actionName, params, opts = {}) { if (params === undefined) params = {}; // Backward compatibility let nodeID = null; /** @type {ActionEndpoint} */ let endpoint; if (typeof actionName !== "string") { endpoint = actionName; actionName = endpoint.action.name; nodeID = endpoint.id; } else { if (opts.nodeID) { nodeID = opts.nodeID; endpoint = this.registry.getActionEndpointByNodeId(actionName, nodeID); if (!endpoint) { this.logger.warn(`Service '${actionName}' is not found on '${nodeID}' node.`); return this.Promise.reject( new E.ServiceNotFoundError({ action: actionName, nodeID }) ).catch(err => this.errorHandler(err, { nodeID, actionName, params, opts })); } } else { // Get endpoint list by action name const epList = this.registry.getActionEndpoints(actionName); if (epList == null) { this.logger.warn(`Service '${actionName}' is not registered.`); return this.Promise.reject( new E.ServiceNotFoundError({ action: actionName }) ).catch(err => this.errorHandler(err, { actionName, params, opts })); } endpoint = epList.getFirst(); if (endpoint == null) { const errMsg = `Service '${actionName}' is not available.`; this.logger.warn(errMsg); return this.Promise.reject( new E.ServiceNotAvailableError({ action: actionName }) ).catch(err => this.errorHandler(err, { actionName, params, opts })); } } } // Create context let ctx; if (opts.ctx != null) { // Reused context ctx = opts.ctx; if (endpoint) { ctx.endpoint = endpoint; ctx.action = endpoint.action; } } else { // New root context ctx = this.ContextFactory.create(this, endpoint, params, opts); } ctx.nodeID = nodeID; this.logger.debug("Call action on a node.", { action: ctx.action.name, nodeID: ctx.nodeID, requestID: ctx.requestID }); let p = endpoint.action.remoteHandler(ctx); // Pointer to Context p.ctx = ctx; return p; } /** * * @param {string} actionName * @param {Context=} ctx * @returns */ _getLocalActionEndpoint(actionName, ctx) { // Find action by name let epList = this.registry.getActionEndpoints(actionName); if (epList == null || !epList.hasLocal()) { this.logger.warn(`Service '${actionName}' is not registered locally.`); throw new E.ServiceNotFoundError({ action: actionName, nodeID: this.nodeID }); } // Get local endpoint let endpoint = epList.nextLocal(ctx); if (!endpoint) { this.logger.warn(`Service '${actionName}' is not available locally.`); throw new E.ServiceNotAvailableError({ action: actionName, nodeID: this.nodeID }); } return endpoint; } /** * @overload * @param {Record<string, MCallDefinition>} def * @param {MCallCallingOptions=} opts * @returns {Promise<Record<string, TResult>>} */ /** * @overload * @param {MCallDefinition[]} def * @param {MCallCallingOptions=} opts * @returns {Promise<TResult[]>} */ /** * Multiple action calls. * * @template TResult * @param {Record<string, MCallDefinition>|MCallDefinition[]} def * @param {MCallCallingOptions=} opts * @returns {Promise<Record<string, TResult> | TResult[]>} * @memberof ServiceBroker */ mcall(def, opts = {}) { const { settled, ...options } = opts; if (Array.isArray(def)) { return /** @type {Promise<TResult[]>} */ ( utils.promiseAllControl( def.map(item => this.call(item.action, item.params, item.options || options)), settled, this.Promise ) ); } else if (utils.isObject(def)) { /** @type {Record<string, TResult>} */ const results = {}; const promises = Object.keys(def).map(name => { const item = def[name]; const callOptions = item.options || options; return this.call(item.action, item.params, callOptions).then( res => (results[name] = res) ); }); const p = utils.promiseAllControl(promises, settled, this.Promise); // Pointer to Context // @ts-ignore p.ctx = promises.map(promise => promise.ctx); return p.then(() => results); } else { return this.Promise.reject( new E.MoleculerServerError("Invalid calling definition.", 500, "INVALID_PARAMETERS") ); } } /** * Emit an event (grouped & balanced global event) * * @param {string} eventName * @param {any=} payload * @param {Object=} opts * @returns {Promise<any>} * * @memberof ServiceBroker */ emit(eventName, payload, opts) { if (Array.isArray(opts) || utils.isString(opts)) opts = { groups: opts }; else if (opts == null) opts = {}; if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups]; const promises = []; const ctx = this.ContextFactory.create(this, null, payload, opts); ctx.eventName = eventName; ctx.eventType = "emit"; ctx.eventGroups = opts.groups; this.logger.debug( `Emit '${eventName}' event` + (opts.groups ? ` to '${opts.groups.join(", ")}' group(s)` : "") + "." ); // Call local/internal subscribers if (/^\$/.test(eventName)) this.localBus.emit(eventName, payload); if (!this.options.disableBalancer) { const endpoints = this.registry.events.getBalancedEndpoints( eventName, opts.groups, ctx ); // Grouping remote events (reduce the network traffic) const groupedEP = {}; endpoints.forEach(([ep, group]) => { if (ep.id === this.nodeID) { // Local service, call handler const newCtx = ctx.copy(ep); promises.push( this.registry.events.callEventHandler(newCtx).catch(err => { // Catch and log the error because it's a local event handler, not throwing further. this.logger.error(err); }) ); } else { // Remote service const e = groupedEP[ep.id]; if (e) e.groups.push(group); else groupedEP[ep.id] = { ep, groups: [group] }; } }); if (this.transit) { // Remote service _.forIn(groupedEP, item => { const newCtx = ctx.copy(item.ep); newCtx.eventGroups = item.groups; promises.push(this.transit.sendEvent(newCtx)); }); } } else if (this.transit) { // Disabled balancer case let groups = opts.groups; if (!groups || groups.length === 0) { // Apply to all groups groups = this.getEventGroups(eventName); } if (groups.length === 0) return this.Promise.resolve(true); ctx.eventGroups = groups; promises.push(this.transit.sendEvent(ctx)); } const p = this.Promise.allSettled(promises).then(results => { const err = results.find(r => r.status == "rejected"); if (err) return this.Promise.reject(err.reason); return true; }); if (opts.throwError) { return p; } return p.catch(() => { // swallow the error. It's already logged. }); } /** * Broadcast an event for all local & remote services * * @param {string} eventName * @param {any=} payload * @param {Object=} opts * @returns {Promise} * * @memberof ServiceBroker */ broadcast(eventName, payload, opts) { if (Array.isArray(opts) || utils.isString(opts)) opts = { groups: opts }; else if (opts == null) opts = {}; if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups]; const promises = []; this.logger.debug( `Broadcast '${eventName}' event` + (opts.groups ? ` to '${opts.groups.join(", ")}' group(s)` : "") + "." ); if (this.transit) { const ctx = this.ContextFactory.create(this, null, payload, opts); ctx.eventName = eventName; ctx.eventType = "broadcast"; ctx.eventGroups = opts.groups; if (!this.options.disableBalancer) { const endpoints = this.registry.events.getAllEndpoints(eventName, opts.groups); // Send to remote services endpoints.forEach(ep => { if (ep.id != this.nodeID) { const newCtx = ctx.copy(ep); promises.push(this.transit.sendEvent(newCtx)); } }); } else { // Disabled balancer case let groups = opts.groups; if (!groups || groups.length === 0) { // Apply to all groups groups = this.getEventGroups(eventName); } if (groups.length === 0) return; // Return here because balancer disabled, so we can't call the local services. const endpoints = this.registry.events.getAllEndpoints(eventName, groups); // Return here because balancer disabled, so we can't call the local services. endpoints.forEach(ep => { const newCtx = ctx.copy(ep); newCtx.eventGroups = groups; promises.push(this.transit.sendEvent(newCtx)); }); } } if (!this.options.disableBalancer) { // Send to local services promises.push(this.broadcastLocal(eventName, payload, opts)); } const p = this.Promise.allSettled(promises).then(results => { const err = results.find(r => r.status == "rejected"); if (err) return this.Promise.reject(err.reason); return true; }); if (opts.throwError) { return p; } return p.catch(() => { // swallow the error. It's already logged. }); } /** * Broadcast an event for all local services * * @param {string} eventName * @param {any=} payload * @param {Object=} opts * @returns * * @memberof ServiceBroker */ broadcastLocal(eventName, payload, opts) { if (Array.isArray(opts) || utils.isString(opts)) opts = { groups: opts }; else if (opts == null) opts = {}; if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups]; this.logger.debug( `Broadcast '${eventName}' local event` + (opts.groups ? ` to '${opts.groups.join(", ")}' group(s)` : "") + "." ); // Call internal subscribers if (/^\$/.test(eventName)) this.localBus.emit(eventName, payload); const ctx = this.ContextFactory.create(this, null, payload, opts); ctx.eventName = eventName; ctx.eventType = "broadcastLocal"; ctx.eventGroups = opts.groups; const p = this.emitLocalServices(ctx); if (opts.throwError) { return p; } return p.catch(err => { // Catch and log the error because it's a local event handler, not throwing further. this.logger.error(err); }); } /** * Send ping to a node (or all nodes if nodeID is null) * * @param {String|Array<String>?} nodeID * @param {Number?} timeout * @returns {Promise} * @memberof ServiceBroker */ ping(nodeID, timeout = 2000) { if (this.transit && this.transit.connected) { if (utils.isString(nodeID)) { // Ping a single node return new this.Promise(resolve => { const timer = setTimeout(() => { this.localBus.off("$node.pong", handler); resolve(null); }, timeout); const handler = pong => { if (pong.nodeID == nodeID) { clearTimeout(timer); this.localBus.off("$node.pong", handler); resolve(pong); } }; this.localBus.on("$node.pong", handler); this.transit.sendPing(nodeID); }); } else { const pongs = {}; let nodes = nodeID; if (!nodes) { nodes = this.registry .getNodeList({ onlyAvailable: true }) .filter(node => node.id != this.nodeID) .map(node => node.id); } nodes.forEach(id => (pongs[id] = null)); const processing = new Set(nodes); // Ping multiple nodes return new this.Promise(resolve => { const timer = setTimeout(() => { this.localBus.off("$node.pong", handler); resolve(pongs); }, timeout); const handler = pong => { pongs[pong.nodeID] = pong; processing.delete(pong.nodeID); if (processing.size === 0) { clearTimeout(timer); this.localBus.off("$node.pong", handler); resolve(pongs); } }; this.localBus.on("$node.pong", handler); nodes.forEach(id => this.transit.sendPing(id)); }); } } return this.Promise.resolve(nodeID ? null : []); } /** * Get local node health status * * @returns {NodeHealthStatus} * @memberof ServiceBroker */ getHealthStatus() { return H.getHealthStatus(); } /** * Get local node info. * * @returns {NodeRawInfo} * @memberof ServiceBroker */ getLocalNodeInfo() { return this.registry.getLocalNodeInfo(); } /** * Get event groups by event name * * @param {String} eventName * @returns * @memberof ServiceBroker */ getEventGroups(eventName) { return this.registry.events.getGroups(eventName); } /** * Has registered event listener for an event name? * * @param {String} eventName * @returns {boolean} */ hasEventListener(eventName) { return this.registry.events.getAllEndpoints(eventName).length > 0; } /** * Get all registered event listener for an event name. * * @param {String} eventName * @returns {Array<Object>} */ getEventListeners(eventName) { return this.registry.events.getAllEndpoints(eventName); } /** * Emit event to local nodes. It is called from transit when a remote event received * or from `broadcastLocal` * * @param {Context} ctx * @returns {Promise<any>} * @memberof ServiceBroker */ emitLocalServices(ctx) { return this.registry.events.emitLocalServices(ctx); } /** * Set the current Context to the async storage. * * @param {Context} ctx * @memberof ServiceBroker * setCurrentContext(ctx) { this.scope.setSessionData(ctx); }*/ /** * Get the current Context from the async storage. * * @returns {Context?} * @memberof ServiceBroker * getCurrentContext() { return this.scope.getSessionData(); }*/ /** * Get node overall CPU usage * * @returns {Promise<object>} * @memberof ServiceBroker */ getCpuUsage() { return cpuUsage(); } /** * Generate an UUID. * * @returns {String} uuid */ generateUid() { if (this.options.uidGenerator) return this.options.uidGenerator.call(this, this); return utils.generateToken(); } /** * Only for backward compatibility */ getConstructorName(obj) { return utils.getConstructorName(obj); } /** * Ensure the service schema will be prototype of ServiceFactory; * * @param {ServiceSchema} schema * @returns {ServiceSchema} * */ normalizeSchemaConstructor(schema) { if (Object.prototype.isPrototypeOf.call(this.ServiceFactory, schema)) { return schema; } // Sometimes the schame was loaded from another node_module or is a object copy. // Then we will check if the constructor name is the same, asume that is a derivate object // and adjust the prototype of the schema. let serviceName = utils.getConstructorName(this.ServiceFactory); let target = utils.getConstructorName(schema); if (serviceName === target) { Object.setPrototypeOf(schema, this.ServiceFactory); return schema; } // Depending how the schema was create the correct constructor name (from base class) will be locate on __proto__. target = utils.getConstructorName(Object.getPrototypeOf(schema)); if (serviceName === target) { Object.setPrototypeOf(Object.getPrototypeOf(schema), this.ServiceFactory); return schema; } // This is just to handle some idiosyncrasies from Jest. if (schema._isMockFunction) { target = utils.getConstructorName(Object.getPrototypeOf(schema.prototype)); if (serviceName === target) { Object.setPrototypeOf(schema, this.ServiceFactory); return schema; } } return schema; } } /** * Version of Moleculer */ ServiceBroker.MOLECULER_VERSION = require("../package.json").version; ServiceBroker.prototype.MOLECULER_VERSION = ServiceBroker.MOLECULER_VERSION; /** * Version of Protocol */ ServiceBroker.PROTOCOL_VERSION = "5"; ServiceBroker.prototype.PROTOCOL_VERSION = ServiceBroker.PROTOCOL_VERSION; /** * Internal middlewares (order) */ ServiceBroker.INTERNAL_MIDDLEWARES = INTERNAL_MIDDLEWARES; /** * Default configuration */ ServiceBroker.defaultOptions = defaultOptions; module.exports = ServiceBroker;