UNPKG

moleculer

Version:

Fast & powerful microservices framework for Node.JS

631 lines (542 loc) 14.5 kB
/* * moleculer * Copyright (c) 2017 Ice Services (https://github.com/ice-services/moleculer) * MIT Licensed */ "use strict"; const Promise = require("bluebird"); const Context = require("./context"); const P = require("./packets"); const { getIpList } = require("./utils"); // Prefix for logger const LOG_PREFIX = "TRANSIT"; /** * Transit class * * @class Transit */ class Transit { /** * Create an instance of Transit. * * @param {ServiceBroker} Broker instance * @param {Transporter} Transporter instance * @param {Object?} opts * * @memberOf Transit */ constructor(broker, transporter, opts) { this.broker = broker; this.logger = broker.getLogger(LOG_PREFIX); this.nodeID = broker.nodeID; this.tx = transporter; this.opts = opts; this.nodes = new Map(); this.pendingRequests = new Map(); this.heartbeatTimer = null; this.checkNodesTimer = null; this.stat = { packets: { sent: 0, received: 0 } }; if (this.tx) this.tx.init(this, this.messageHandler.bind(this), this.afterConnect.bind(this)); this.__connectResolve = null; } /** * It will be called after transporter connected or reconnected. * * @param {any} wasReconnect * @returns {Promise} * * @memberof Transit */ afterConnect(wasReconnect) { return Promise.resolve() .then(() => { if (!wasReconnect) this.makeSubscriptions(); }) .then(() => this.discoverNodes()) .then(() => this.sendNodeInfo()) .then(() => { if (this.__connectResolve) { this.__connectResolve(); this.__connectResolve = null; } }); } /** * Connect with transporter. If failed, try again after 5 sec. * * @memberOf Transit */ connect() { this.logger.info("Connecting to transporter..."); return new Promise(resolve => { this.__connectResolve = resolve; const doConnect = () => { /* istanbul ignore next */ this.tx.connect().catch(err => { this.logger.warn("Connect failed!", err.message); this.logger.debug("ERROR!", err); setTimeout(() => { this.logger.info("Reconnecting..."); doConnect(); }, 5 * 1000); }); }; doConnect(); }) .then(() => { // Start timers this.heartbeatTimer = setInterval(() => { /* istanbul ignore next */ this.sendHeartbeat(); }, this.broker.options.heartbeatInterval * 1000); this.heartbeatTimer.unref(); this.checkNodesTimer = setInterval(() => { /* istanbul ignore next */ this.checkRemoteNodes(); }, this.broker.options.heartbeatTimeout * 1000); this.checkNodesTimer.unref(); }); } /** * Disconnect with transporter * * @memberOf Transit */ disconnect() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.checkNodesTimer) { clearInterval(this.checkNodesTimer); this.checkNodesTimer = null; } if (this.tx.connected) { return this.sendDisconnectPacket() .then(() => this.tx.disconnect()); } /* istanbul ignore next */ return Promise.resolve(); } /** * Send DISCONNECT to remote nodes * * @returns {Promise} * * @memberOf Transit */ sendDisconnectPacket() { this.logger.debug("Send DISCONNECT to nodes"); this.publish(new P.PacketDisconnect(this)); return Promise.resolve(); } /** * Subscribe to topics for transportation * * @memberOf Transit */ makeSubscriptions() { // Subscribe to broadcast events this.subscribe(P.PACKET_EVENT); // Subscribe to requests this.subscribe(P.PACKET_REQUEST, this.nodeID); // Subscribe to node responses of requests this.subscribe(P.PACKET_RESPONSE, this.nodeID); // Discover handler this.subscribe(P.PACKET_DISCOVER); // NodeInfo handler this.subscribe(P.PACKET_INFO); // Broadcasted INFO. If a new node connected this.subscribe(P.PACKET_INFO, this.nodeID); // Response INFO to DISCOVER packet // Disconnect handler this.subscribe(P.PACKET_DISCONNECT); // Heart-beat handler this.subscribe(P.PACKET_HEARTBEAT); } /** * Emit an event to remote nodes * * @param {any} eventName * @param {any} data * * @memberOf Transit */ emit(eventName, data) { this.publish(new P.PacketEvent(this, eventName, data)); } /** * Message handler for incoming packets * * @param {Array} topic * @param {String} msg * @returns * * @memberOf Transit */ messageHandler(cmd, msg) { if (msg == null) { throw new Error("Missing packet!"); } this.stat.packets.received = this.stat.packets.received + 1; const packet = P.Packet.deserialize(this, cmd, msg); const payload = packet.payload; // Check payload if (!payload) { /* istanbul ignore next */ throw new Error("Missing response payload!"); } if (payload.sender == this.nodeID) return Promise.resolve(); // Request if (cmd === P.PACKET_REQUEST) { return this._requestHandler(payload); } // Response else if (cmd === P.PACKET_RESPONSE) { return this._responseHandler(payload); } // Event else if (cmd === P.PACKET_EVENT) { //this.logger.debug("Event received", payload); this.broker.emitLocal(payload.event, payload.data, payload.sender); return; } // Node info else if (cmd === P.PACKET_INFO || cmd === P.PACKET_DISCOVER) { this.processNodeInfo(payload.sender, payload); if (cmd == "DISCOVER") { //this.logger.debug("Discover received from " + payload.sender); this.sendNodeInfo(payload.sender); } return; } // Disconnect else if (cmd === P.PACKET_DISCONNECT) { this.nodeDisconnected(payload.sender); return; } // Heartbeat else if (cmd === P.PACKET_HEARTBEAT) { //this.logger.debug("Node heart-beat received from " + payload.sender); this.nodeHeartbeat(payload.sender, payload); return; } } /** * Handle incoming request * * @param {Object} packet * @returns {Promise} * * @memberOf Transit */ _requestHandler(payload) { this.logger.debug(`Request '${payload.action}' from '${payload.sender}'. Params:`, payload.params); // Recreate caller context const ctx = new Context(this.broker); ctx.action = { name: payload.action }; ctx.id = payload.id; ctx.parentID = payload.parentID; ctx.level = payload.level; ctx.metrics = payload.metrics; ctx.meta = payload.meta; ctx.setParams(payload.params); return this.broker.call(payload.action, payload.params, { ctx: ctx }) .then(res => this.sendResponse(payload.sender, payload.id, res, null)) .catch(err => this.sendResponse(payload.sender, payload.id, null, err)); } /** * Process incoming response of request * * @param {Object} packet * @returns {Promise} * * @memberOf Transit */ _responseHandler(packet) { const id = packet.id; const req = this.pendingRequests.get(id); // If not exists (timed out), we skip to process the response if (req == null) return Promise.resolve(); // Remove pending request this.removePendingRequest(id); this.logger.debug(`Response '${req.action.name}' is received from '${req.nodeID}'.`); if (!packet.success) { // Recreate exception object let err = new Error(packet.error.message + ` (NodeID: ${packet.sender})`); // TODO create the original error object if it's available // let constructor = errors[packet.error.name] // let error = Object.create(constructor.prototype); err.name = packet.error.name; err.code = packet.error.code; err.type = packet.error.type; err.nodeID = packet.error.nodeID || packet.sender; err.data = packet.error.data; if (packet.error.stack) err.stack = packet.error.stack; return req.reject(err); } return req.resolve(packet.data); } /** * Send a request to a remote service. It returns a Promise * what will be resolved when the response received. * * @param {<Context>} ctx Context of request * @returns {Promise} * * @memberOf Transit */ request(ctx) { // Expanded the code that v8 can optimize it. (TryCatchStatement disable optimizing) return new Promise((resolve, reject) => this._doRequest(ctx, resolve, reject)); } /** * Do a remote request * * @param {<Context>} ctx Context of request * @param {Function} resolve Resolve of Promise * @param {Function} reject Reject of Promise * * @memberOf Transit */ _doRequest(ctx, resolve, reject) { const request = { nodeID: ctx.nodeID, action: ctx.action, //ctx, resolve, reject }; const packet = new P.PacketRequest(this, ctx.nodeID, ctx); this.logger.debug(`Send '${ctx.action.name}' request to '${ctx.nodeID}'. Params:`, ctx.params); // Add to pendings this.pendingRequests.set(ctx.id, request); //return resolve(ctx.params); // Publish request this.publish(packet); } /** * Remove a pending request * * @param {any} id * * @memberOf Transit */ removePendingRequest(id) { this.pendingRequests.delete(id); } /** * Send back the response of request * * @param {String} nodeID * @param {String} id * @param {any} data * @param {Error} err * * @memberOf Transit */ sendResponse(nodeID, id, data, err) { //this.logger.debug(`Send response back to '${nodeID}'`); // Publish the response return this.publish(new P.PacketResponse(this, nodeID, id, data, err)); } /** * Get Node information to DISCOVER & INFO packages * * @returns {Object} * * @memberof Transit */ getNodeInfo() { const services = this.broker.serviceRegistry.getServiceList({ onlyLocal: true, withActions: true }); const uptime = process.uptime(); const ipList = getIpList(); const versions = { node: process.version, moleculer: this.broker.MOLECULER_VERSION }; return { services, ipList, versions, uptime }; } /** * Discover other nodes. It will be called after success connect. * * @memberOf Transit */ discoverNodes() { return this.publish(new P.PacketDiscover(this)); } /** * Send node info package to other nodes. It will be called with timer * * @memberOf Transit */ sendNodeInfo(nodeID) { const info = this.getNodeInfo(); return this.publish(new P.PacketInfo(this, nodeID, info)); } /** * Send a node heart-beat. It will be called with timer * * @memberOf Transit */ sendHeartbeat() { const uptime = process.uptime(); this.publish(new P.PacketHeartbeat(this, uptime)); } /** * Subscribe via transporter * * @param {String} topic * @param {String=} nodeID * * @memberOf Transit */ subscribe(topic, nodeID) { return this.tx.subscribe(topic, nodeID); } /** * Publish via transporter * * @param {Packet} Packet * * @memberOf Transit */ publish(packet) { this.stat.packets.sent = this.stat.packets.sent + 1; return this.tx.publish(packet); } /** * Serialize the object * * @param {Object} obj * @returns {String} * * @memberOf Transit */ serialize(obj, type) { return this.broker.serializer.serialize(obj, type); } /** * Deserialize the incoming string to object * * @param {String} str * @returns {any} * * @memberOf Transit */ deserialize(str, type) { if (str == null) return null; return this.broker.serializer.deserialize(str, type); } /** * Process remote node info (list of actions) * * @param {String} nodeID * @param {Object} payload * * @memberOf Transit */ processNodeInfo(nodeID, payload) { if (nodeID == null) { this.logger.error("Missing nodeID from node info package!"); return; } let isNewNode = !this.nodes.has(nodeID); const node = Object.assign(this.nodes.get(nodeID) || {}, payload); let isReconnected = !node.available; node.lastHeartbeatTime = Date.now(); node.available = true; node.id = nodeID; this.nodes.set(nodeID, node); if (isNewNode) { this.broker.emitLocal("node.connected", node); this.logger.info(`Node '${nodeID}' connected!`); } else if (isReconnected) { this.broker.emitLocal("node.reconnected", node); this.logger.info(`Node '${nodeID}' reconnected!`); } if (node.services) { // Register remote services node.services.forEach(service => this.broker.registerRemoteService(nodeID, service)); } } /** * Check the given nodeID is available * * @param {any} nodeID Node ID * @returns {boolean} * * @memberOf Transit */ isNodeAvailable(nodeID) { let info = this.nodes.get(nodeID); if (info) return info.available; return false; } /** * Save a heart-beat time from a remote node * * @param {any} nodeID * @param {Object} payload * * @memberOf Transit */ nodeHeartbeat(nodeID, payload) { if (this.nodes.has(nodeID)) { let node = this.nodes.get(nodeID); node.lastHeartbeatTime = Date.now(); node.uptime = payload.uptime; node.available = true; } } /** * Node disconnected event handler. * Remove node and remove remote actions of node * * @param {any} nodeID * @param {Boolean=} isUnexpected * * @memberOf Transit */ nodeDisconnected(nodeID, isUnexpected) { if (this.nodes.has(nodeID)) { let node = this.nodes.get(nodeID); if (node.available) { node.available = false; this.broker.unregisterServicesByNode(nodeID); this.broker.emitLocal(isUnexpected ? "node.broken" : "node.disconnected", node); //this.nodes.delete(nodeID); this.logger.warn(`Node '${nodeID}' disconnected!`); } } } /** * Check all registered remote nodes is live. * * @memberOf Transit */ checkRemoteNodes() { let now = Date.now(); this.nodes.forEach(node => { if (now - (node.lastHeartbeatTime || 0) > this.broker.options.heartbeatTimeout * 1000) { this.nodeDisconnected(node.id, true); } }); } } module.exports = Transit;