UNPKG

comedy

Version:

Node.js actor framework.

688 lines (588 loc) 19.1 kB
/* * Copyright (c) 2016-2018 Untu, Inc. * This code is licensed under Eclipse Public License - v 1.0. * The full license text can be found in LICENSE.txt file and * on the Eclipse official site (https://www.eclipse.org/legal/epl-v10.html). */ 'use strict'; let common = require('./utils/common.js'); let ActorLogger = require('./utils/actor-logger.js'); let ParentActorProxy = require('./parent-actor-proxy.js'); let EventEmitter = require('events').EventEmitter; let P = require('bluebird'); let _ = require('underscore'); /** * A basic actor. */ class Actor extends EventEmitter { /** * @param {Object} options Actor creation options. * - {ActorSystem} system Actor system. * - {Actor} parent Parent actor. * - {Object} definition Actor behaviour definition. * - {Object} [origDefinition] Original actor definition. * - {String} id Actor ID. * - {Object} [config] Actor configuration. * - {String} [name] Actor name. */ constructor(options) { super(); this.origDefinition = options.origDefinition || options.definition; if (common.isPlainObject(options.definition)) { // Plain object behaviour. this.definition = _.clone(options.definition); this.origDefinition = this.origDefinition || _.clone(options.definition); } else { // Class-defined behaviour. this.definition = options.definition; } this.system = options.system; this.parent = options.parent; this.id = options.id; this.config = options.config || {}; this.name = options.name || ''; this.childPromises = []; this.forwardList = []; this.log = new ActorLogger(options.system.getLog(), this, this.name); this.state = 'new'; } /** * Actor initialization function that is called before any interaction with actor starts. * This function may return promise, in which case the actor will only be available for * communication after the returned promise is resolved. */ initialize() { } /** * Creates a child actor. * * @param {Object|Function} definition Child actor behaviour definition. Can be a plain object or a * class reference. In case of class reference, an actor object is automatically instantiated. * @param {Object} [config] Actor configuration. * - {String} mode Actor creation mode. * - {Number} clusterSize Number of actor instances to create. * - {Object} customParameters Custom actor parameters. * @returns {P} Promise that yields a child actor once it is created. */ createChild(definition, config) { if (this.getState() != 'new' && this.getState() != 'ready') return this._notReadyErrorPromise(); let log = this.getLog(); let childPromise = this.system.createActor(definition, this._parentReference(), config) .tap(actor => actor.initialize()) .tap(actor => { actor.once('destroyed', () => { this.childPromises = _.without(this.childPromises, childPromise); }); log.debug('Created child actor ' + actor); }) .catch(err => { this.childPromises = _.without(this.childPromises, childPromise); throw err; }); this.childPromises.push(childPromise); return childPromise; } /** * Creates one child actor per module in a given directory. * * @param {String} moduleDir Module directory to read child actor definitions from. * @param {Object} [options] Actor creation options, that are passed to each created child actor. * @returns {P} Operation promise, which yields initialized child instance array. */ createChildren(moduleDir, options) { return P.resolve() .then(() => _.keys(this.system.requireDirectory(moduleDir))) .map(moduleName => this.createChild(moduleDir + '/' + moduleName, options)); } /** * Performs a hot configuration change for this actor. Actor remains operational * during and after configuration change. * * @param {Object} config New actor configuration. * @returns {Actor} Augmented actor instance. */ async changeConfiguration(config = { mode: 'in-memory' }) { if (_.isEqual(_.omit(this.config, 'customParameters'), config)) return this; this.getLog() .info('Changing actor configuration, currentConfiguration=', this.config, ', newConfiguration=', config); let newActor = await this.system._createActor( this.origDefinition || this.definition, this.parent, _.extend({ name: this.getName(), customParameters: this.getCustomParameters() }, config)); await newActor.initialize(); await this.parent._augmentChild(this.getId(), newActor); this.emit('augmented', newActor); this.getLog().info('Actor configuration changed, new actor ID: ' + newActor.getId()); return newActor; } /** * Recursively applies new global configuration to this actor an all it's * child sub-tree. * * @param {Object} config Global actor configuration. */ async changeGlobalConfiguration(config) { this.getLog().debug('changeGlobalConfiguration(), config=', config); let self = await this.changeConfiguration(config[this.getName()]); await self.changeGlobalConfigurationForChildren(config); } /** * Changes global configuration settings for child actors. * * @param {Object} config New global configuration. */ async changeGlobalConfigurationForChildren(config) { await P.map(this.childPromises, child => child.changeGlobalConfiguration(config)); } /** * Checks whether this actors has a parent. * * @returns {Boolean} TRUE if actor has a parent, FALSE otherwise. */ hasParent() { return !!this.parent; } /** * Synchronously returns this actor's ID. * * @returns {String} This actor ID. */ getId() { return this.id; } /** * Synchronously returns this actor's name. * * @returns {String} This actor's name or empty string, if there is no name for this actor. */ getName() { return this.name; } /** * Synchronously returns this actor's context. * * @returns {*} A context of this actor's system. */ getContext() { return this.system.getContext(); } /** * Synchronously returns this actor's parent. * * @returns {ParentActorProxy} This actor's parent reference. */ getParent() { return new ParentActorProxy(this.parent); } /** * Synchronously returns a logger for this actor. * * @returns {ActorLogger} Actor logger. */ getLog() { return this.log; } /** * Synchronously returns custom actor parameters, if any. * * @returns {Object|undefined} Custom actor parameters or undefined, if custom parameters * were not set. */ getCustomParameters() { return _.clone(this.config.customParameters); } /** * Synchronously returns this actor's system. * * @returns {ActorSystem} Actor system instance. */ getSystem() { return this.system; } /** * Synchronously returns this actor's system bus. * * @returns {SystemBus} Actor system bus. */ getBus() { return this.system.bus; } /** * Synchronously returns this actor's mode. * * @returns {String} Actor mode. */ getMode() { return common.abstractMethodError('getMode'); } /** * Synchronously returns this actor's state. * * @returns {String} Actor state. */ getState() { return this.state; } /** * Sets new state for this actor. * * @param {String} newState New actor state. * @protected */ _setState(newState) { this.state = newState; } /** * Gets current actor configuration. * * @returns {Object} Actor configuration. * @protected */ _getConfig() { return this.config; } /** * Saves new actor configuration information. No actual changes are * made to actor state. * * @param {Object} newConfig New actor configuration. * @protected */ _setConfig(newConfig) { this.config = newConfig; } /** * Gets actor definition. * * @returns {Object|String} Actor definition. * @protected */ _getDefinition() { return this.origDefinition; } /** * Sends a message to this actor. The message is handled according to specified behaviour. * A result of message handling is completely ignored, even if it has generated an error. * * @param {String} topic Message topic. * @param {*} message Message to send. Variable arguments supported. * @returns {P} Promise which is resolved once the message is sent. */ send(topic, ...message) { if (this.getState() != 'ready') return this._notReadyErrorPromise(); if (this._checkOverload()) return this._overloadedErrorPromise(); let fwActor = this._checkForward(topic); if (fwActor) { if (this.getLog().isDebug()) { this.getLog().debug('Forwarding message to other actor, topic=', topic, 'message=', JSON.stringify(message, null, 2), 'actor=', fwActor.toString()); } return fwActor.send.apply(fwActor, arguments); } return this.send0(topic, ...message); } /** * Sends a message to this actor and receives a response. The message is handled according * to specified behaviour. * * @param {String} topic Message topic. * @param {*} message Message to send. Variable arguments supported. * @returns {P} Promise which yields the actor response, if any. */ sendAndReceive(topic, ...message) { if (this.getState() != 'ready') return this._notReadyErrorPromise(); if (this._checkOverload()) return this._overloadedErrorPromise(); let fwActor = this._checkForward(topic); if (fwActor) { if (this.getLog().isDebug()) { this.getLog().debug('Forwarding message to other actor, topic=', topic, 'message=', JSON.stringify(message, null, 2), 'actor=', fwActor.toString()); } return fwActor.sendAndReceive.apply(fwActor, arguments); } return this.sendAndReceive0(topic, ...message); } /** * Broadcasts a given message to all instances of a clustered actor. * For ordinary (non-clustered) actor it's just the same as send(). * * @param {String} topic Message topic. * @param {*} message Message to send. Variable arguments supported. * @returns {P} Promise which is resolved once the message is sent to all clustered instances. */ broadcast(topic, ...message) { return this.send.apply(this, arguments); } /** * Broadcasts a given message to all instances of a clustered actor, and collects results. * For ordinary (non-clustered) actor it's just the same as sendAndReceive(). * * @param {String} topic Message topic. * @param {*} message Message to send. Variable arguments supported. * @returns {P} Promise which yields an array with all clustered actor instance responses. */ broadcastAndReceive(topic, ...message) { return this.sendAndReceive.apply(this, arguments).then(result => ([result])); } /** * Sets this actor to forward messages with given topics to it's parent. * * @param {String|RegExp|Boolean} topics Topic name strings or regular expressions. * If true is specified, all unknown message topics will be forwarded to parent. */ forwardToParent(...topics) { if (topics.length === 1 && topics[0] === true) { this.forwardAllUnknown = this.getParent(); return; } if (topics.length === 0) return; topics.forEach(topic => { this.forwardList.push([topic, this.getParent()]); }); } /** * Sets this actor to forward messages with given topics to a given child. * * @param {Actor} childActor Actor to forward messages to. * @param {String|RegExp} topics Message topic strings or regular expressions. * @returns {P} Operation result promise. */ forwardToChild(childActor, ...topics) { return P.all(this.childPromises).then(children => { if (!_.contains(children, childActor)) { throw new Error('Cannot forward ' + topics + ' messages to ' + childActor + ' actor, because it\'s not a child of ' + this + ' actor.'); } topics.forEach(topic => { this.forwardList.push([topic, childActor]); }); }); } /** * Destroys this actor. * * @returns {P} Promise which is resolved when actor is destroyed. */ destroy() { if (this.destroyPromise) return this.destroyPromise; this._setState('destroying'); this.log.debug('Destroying...'); this.destroyPromise = P .map(this.childPromises, child => { return child.destroy() .catch(err => { this.log.warn('Error while destroying child actor, actor=' + child, err); }); }) .then(() => this.destroy0()) .then(() => { this._setState('destroyed'); this.emit('destroyed', this); this.log.debug('Destroyed.'); }); return this.destroyPromise; } /** * Helper function to correctly import modules in different processes with * different directory layout. * * @param {String} modulePath Path of the module to import. If starts with /, a module * is searched relative to project directory. * @returns {*} Module import result. */ require(modulePath) { return this.system.require(modulePath); } /** * Returns a JSON tree representation of this actor's hierarchy. * * @returns {P} Operation promise which yields a hierarchy data object. */ tree() { let selfObj = { id: this.getId(), name: this.getName(), mode: this.getMode() }; return P.resolve() .then(() => this.location0()) .then(location => selfObj.location = location) .then(() => this._children()) .map(child => child.tree()) .then(childTrees => { _.isEmpty(childTrees) || (selfObj.children = childTrees); }) .return(selfObj); } /** * Returns metrics for this actor and all of it's sub-actor tree. * * @returns {P} Operation promise, which yields metrics data object. */ metrics() { return P.resolve() .then(() => this.metrics0()) .then(selfMetrics => { let ret = {}; if (!_.isEmpty(selfMetrics)) { ret = selfMetrics; } return P.reduce(this._children(), (memo, child) => { return child.metrics().then(childMetrics => { memo[child.getName()] = childMetrics; return memo; }); }, ret); }); } /** * Actual send implementation. To be overridden by subclasses. * * @param {String} topic Message topic. * @param {*} message Message to send. * @returns {P} Promise which is resolved once the message is sent. */ send0(topic, ...message) { return common.abstractMethodError('send0', topic, message); } /** * Actual sendAndReceive implementation. To be overridden by subclasses. * * @param {String} topic Message topic. * @param {*} message Message to send. * @returns {P} Promise which yields the actor response. */ sendAndReceive0(topic, ...message) { return common.abstractMethodError('sendAndReceive0', topic, message); } /** * Actual destroy implementation. To be overridden by subclasses. * * @returns {P} Promise which is resolved when actor is destroyed. */ destroy0() { return P.resolve(); } /** * Returns this actor's location description object. * * @returns {Object|P} Location description object or a promise returning such object. */ location0() { return common.abstractMethodError('location0'); } /** * Returns this actor's metrics data object. * * @returns {Object|P} Metrics data object or promise returning such object. */ metrics0() { return common.abstractMethodError('metrics0'); } /** * Returns either a self object, or a proxy object, through which child actors should * interact with this actor. * * @returns {Actor} Self or actor proxy object. * @protected */ _parentReference() { return this; } /** * Returns child actors for this actor. * * @returns {P[]} Array with child promises. * @protected */ _children() { return _.clone(this.childPromises); } toString() { return 'Actor(' + this.id + ')'; } /** * Augments a child actor with a given ID with the one provided. * * @param {String} id ID of a child to augment. * @param {Actor} newActor New child actor. * @private */ async _augmentChild(id, newActor) { let children = await P.all(this.childPromises); let idx = _.findIndex(children, child => child.getId() == id); if (idx < 0) throw new Error(`Failed to find child with ID=${id} during augmentation.`); let oldActor = children[idx]; this.childPromises[idx] = P.resolve(newActor); oldActor.destroy().catch(err => { this.log.error('Error while destroying augmented actor:', err); }); } /** * @returns {P} Promise which throws 'not ready' error. * @private */ _notReadyErrorPromise() { switch (this.getState()) { case 'new': return P.reject(new Error('Actor has not yet been initialized.')); case 'crashed': return P.reject(new Error('Actor crashed, no interaction possible.')); case 'destroying': case 'destroyed': return P.reject(new Error('destroy() has been called for this actor, no further interaction possible')); default: return P.reject(new Error(`Actor cannot accept messages, because it is in "${this.getState()}" state.`)); } } /** * @returns {P} Promise which throws 'overloaded' error. * @private */ _overloadedErrorPromise() { return P.reject(new Error('Message was dropped due to system overload.')); } /** * Checks if actor should forward a message with a given topic to some other actor. * * @param {String} topic Topic name. * @returns {Actor|null} Actor to forward a message with given topic to. If null * is returned - no forwarding should occur. * @private */ _checkForward(topic) { // Forward unknown topic, if configured. if (this.forwardAllUnknown && !this.definition[topic]) return this.forwardAllUnknown; // Forward topic, if it is present in forward list. let fwItem = _.find(this.forwardList, item => { let fwTopic = item[0]; if (fwTopic instanceof RegExp) return topic.match(fwTopic); return fwTopic == topic; }); if (fwItem) return fwItem[1]; return null; } /** * Checks whether a message should be dropped due to system overload. * Performs necessary actions if it should. * * @returns {Boolean} True if message should be dropped, false otherwise. * @private */ _checkOverload() { if (this.config.dropMessagesOnOverload && this.system.isOverloaded()) { this.log.warn('Dropping message due to system overload.'); this.emit('message-dropped-overload'); return true; } return false; } } module.exports = Actor;