comedy
Version:
Node.js actor framework.
1,611 lines (1,357 loc) • 50.4 kB
JavaScript
/*
* 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';
/* eslint no-path-concat: "off" */
let common = require('./utils/common.js');
let { Logger } = require('./utils/logger.js');
let Actor = require('./actor.js');
let ClientActorProxy = require('./client-actor-proxy.js');
let InMemoryActor = require('./in-memory-actor.js');
let ForkedActorParent = require('./forked-actor-parent.js');
let ForkedActorChild = require('./forked-actor-child.js');
let RemoteActorParent = require('./remote-actor-parent.js');
let RemoteActorChild = require('./remote-actor-child.js');
let ThreadedActorParent = require('./threaded-actor-parent.js');
let ThreadedActorChild = require('./threaded-actor-child.js');
let RootActor = require('./root-actor.js');
let DisabledActor = require('./disabled-actor.js');
let RoundRobinBalancerActor = require('./balancers/round-robin-balancer-actor.js');
let RandomBalancerActor = require('./balancers/random-balancer-actor.js');
let CustomBalancerActor = require('./balancers/custom-balancer-actor.js');
let MessageSocket = require('./net/message-socket.js');
let ForkedActorReferenceMarshaller = require('./marshallers/forked-actor-reference-marshaller.js');
let RemoteActorReferenceMarshaller = require('./marshallers/remote-actor-reference-marshaller.js');
let childProcess = require('child_process');
let appRootPath = require('app-root-path');
let requireDir = require('require-dir');
let toSource = require('tosource');
let bson = require('bson');
let P = require('bluebird');
let _ = require('underscore');
let s = require('underscore.string');
let randomString = require('randomstring');
let globalRequire = require;
let fs = require('fs');
let net = require('net');
let http = require('http');
let os = require('os');
let tooBusy = require('toobusy-js');
let SystemBus = require('./system-bus.js');
P.promisifyAll(fs);
// Default actor system instance reference.
let defaultSystem;
// Default listening port for remote actor system.
const defaultListeningPort = 6161;
/**
* An actor system.
*/
class ActorSystem {
/**
* @param {Object} [options] Actor system options.
* - {Object} [logger] Custom logger implementation.
* - {Object} [loggerParams] Custom logger parameters.
* - {Object} [loggerConfig] Logger level configuration.
* - {Boolean} [test] If true, sets this system into test mode.
* - {Boolean} [debug] If true, sets this system into debug mode.
* - {Boolean} [forceInMemory] If true, all actors will be launched in 'in-memory' mode.
* - {Object} [root] Root actor behaviour.
* - {Object} [rootParameters] Root actor custom parameters.
* - {Object} [rootParametersMarshalledTypes] Value marshalling information for custom parameters.
* - {Number} [busyLagLimit] Length of event loop lag in milliseconds, after which the system
* is considered to be busy.
* - {Array} [marshallers] Custom marshallers.
* - {Array} [balancers] Custom balancers.
*/
constructor(options = {}) {
this.debugPortCounter = 1;
this.log = common.isPlainObject(options.loggerConfig) ?
Logger.fromConfigurationObject(options.loggerConfig, 'Default', !options.test || !!options.debug) :
Logger.fromConfigurationFile(options.loggerConfig);
if (options.logger) {
let loggerImpl = this._instantiate(options.logger, options.loggerParams);
this.log.setImplementation(loggerImpl);
}
this.bus = new SystemBus({ log: this.log });
this.options = _.clone(options);
this.resourceDefPromises = {};
this.resourceDefClassesPromise = this._loadResourceDefinitions(options.resources);
this.marshallers = {};
this.balancers = {};
if (options.test) this.log.setLevel(this.log.levels().Silent); // Do not output anything in tests.
if (options.debug) {
try {
P.longStackTraces();
}
catch (err) {
this.log.warn('Failed to enable long stack traces: ' + err);
}
this.log.setLevel(this.log.levels().Debug);
}
let additionalRequires = this.options.additionalRequires;
if (additionalRequires) {
_.isArray(additionalRequires) || (additionalRequires = [additionalRequires]);
_.each(additionalRequires, path => {
require(path);
});
}
if (options.root) {
// Create root with custom behaviour.
this.rootActorPromise = P.resolve()
.then(() => {
if (options.rootActorConfig &&
options.rootActorConfig.customParameters &&
options.rootParametersMarshalledTypes) {
// Un-marshall custom parameters.
return P
.reduce(_.pairs(options.rootParametersMarshalledTypes), (memo, kv) => {
let marshalledType = kv[1];
if (marshalledType && marshalledType != 'SocketHandle') {
let marshaller;
if (marshalledType == 'InterProcessReference') {
marshaller = this.getForkedActorReferenceMarshaller();
}
else if (marshalledType == 'InterHostReference') {
marshaller = this.getRemoteActorReferenceMarshaller();
}
else {
marshaller = this.marshallers[marshalledType];
}
if (!marshaller) throw new Error(`Don't know how to un-marshall custom parameter ${kv[0]}`);
return marshaller.unmarshall(memo[kv[0]])
.then(ref => {
memo[kv[0]] = ref;
return memo;
});
}
return memo;
}, _.clone(options.rootActorConfig.customParameters))
.then(unmarshalledCustomParameters => {
return _.extend(options.rootActorConfig, { customParameters: unmarshalledCustomParameters });
});
}
else {
return options.rootActorConfig;
}
})
.then(actorConfig => this.createActor(options.root, null, _.defaults({
mode: 'in-memory',
id: options.rootId,
name: options.rootName
}, actorConfig)))
.then(actorProxy => actorProxy.getWrapped());
if (options.parent && options.mode) {
if (options.mode == 'forked') {
// Create forked root with proper parent.
this.rootActorPromise = this.rootActorPromise.then(rootActor => {
let forkedActorChild = new ForkedActorChild({
system: this,
bus: process,
actor: rootActor,
definition: options.root,
parentId: options.parent.id
});
this.bus.addForkedRecipient(forkedActorChild);
return forkedActorChild;
});
}
else if (options.mode == 'remote') {
// Create remote root with proper parent.
this.rootActorPromise = this.rootActorPromise.then(rootActor => {
let remoteActorChild = new RemoteActorChild({
system: this,
actor: rootActor,
definition: options.root,
parentId: options.parent.id
});
this.bus.addForkedRecipient(remoteActorChild);
return remoteActorChild;
});
}
else if (options.mode == 'threaded') {
// Create threaded root with proper parent.
this.rootActorPromise = this.rootActorPromise.then(rootActor => {
let threadedActorChild = new ThreadedActorChild({
system: this,
actor: rootActor,
definition: options.root,
parentId: options.parent.id
});
this.bus.addForkedRecipient(threadedActorChild);
return threadedActorChild;
});
}
else {
this.rootActorPromise = P.throw(new Error(`Unknown child system mode: ${options.mode}.`));
}
}
}
else {
// Create default root.
this.rootActorPromise = P.resolve(new RootActor(this, { forked: !!options.forked }));
}
// Initialize custom marshallers, if any.
if (options.marshallers) {
this.rootActorPromise = this.rootActorPromise.tap(() => this._initializeMarshallers(options.marshallers));
}
// Initialize custom balancers, if any.
if (options.balancers) {
this.rootActorPromise = this.rootActorPromise.tap(() => this._initializeBalancers(options.balancers));
}
this.rootActorPromise = this.rootActorPromise
.tap(() => this._loadConfiguration(options.config))
.tap(actor => actor.initialize())
.tap(() => {
this.unListenConfig = this._listenConfiguration(options.config);
});
// Kill child process if self process is killed.
this.sigintHandler = () => {
this.log.info('Received SIGINT, exiting');
process.exit(0);
};
this.sigtermHandler = () => {
this.log.info('Received SIGTERM, exiting');
process.exit(0);
};
process.once('SIGINT', this.sigintHandler);
process.once('SIGTERM', this.sigtermHandler);
}
/**
* @returns {*} Logger for this system.
*/
getLog() {
return this.log;
}
/**
* @returns {*} Message bus for this system.
*/
getBus() {
return this.bus;
}
/**
* Returns a marshaller for a given type name.
*
* @param {String} typeName Type name.
* @returns {Object|undefined} Marshaller for a given message or undefined, if a marshaller for a given
* message was not found.
*/
getMarshaller(typeName) {
return this.marshallers[typeName];
}
/**
* Returns a marshaller for a given message.
*
* @param {*} message Message.
* @returns {Object|undefined} Marshaller for a given message or undefined, if a marshaller for a given
* message was not found.
*/
getMarshallerForMessage(message) {
return this.marshallers[this._typeName(message)];
}
/**
* Returns a marshaller for sending actor reference to a forked actor.
*
* @returns {ForkedActorReferenceMarshaller} Marshaller instance.
*/
getForkedActorReferenceMarshaller() {
let ret = this.forkedActorReferenceMarshaller;
if (!ret) {
ret = this.forkedActorReferenceMarshaller = new ForkedActorReferenceMarshaller(this);
ret.type = 'InterProcessReference';
}
return ret;
}
/**
* Returns a marshaller for sending actor reference to a remote actor.
*
* @returns {RemoteActorReferenceMarshaller} Marshaller instance.
*/
getRemoteActorReferenceMarshaller() {
let ret = this.remoteActorReferenceMarshaller;
if (!ret) {
ret = this.remoteActorReferenceMarshaller = new RemoteActorReferenceMarshaller(this);
ret.type = 'InterHostReference';
}
return ret;
}
/**
* Returns actor ping timeout, defined for this system.
*
* @returns {Number} Ping timeout in milliseconds.
*/
getPingTimeout() {
return this.options.pingTimeout || 15000;
}
/**
* @returns {P} Promise which yields root actor for this system.
*/
rootActor() {
return this.rootActorPromise;
}
/**
* Returns actor configuration for given actor name.
*
* @param {String} actorName Actor name.
* @returns {P} Actor configuration object promise.
*/
actorConfiguration(actorName) {
return P.resolve(this.config && this.config[actorName] || {});
}
/**
* Creates an actor.
*
* @param {Object|String} Definition Actor definition object or module path.
* @param {Actor} parent Actor parent.
* @param {Object} [config] Actor persistent configuration options.
* @returns {*} Promise that yields a created actor reference.
*/
createActor(Definition, parent, config = {}) {
return this._createActor(Definition, parent, config).then(actor => {
return new ClientActorProxy(actor);
});
}
/**
* Internal method for actually creating an actor.
*
* @param {Object|String} Definition Actor definition object or module path.
* @param {Actor} parent Actor parent.
* @param {Object} [config] Actor persistent configuration options.
* @returns {*} Promise that yields a created actor.
* @private
*/
_createActor(Definition, parent, config = {}) {
return P.resolve()
.then(() => {
if (_.isString(Definition)) {
// Module path is specified => load actor module.
return this._loadDefinition(Definition);
}
return Definition;
})
.then(Definition0 => {
let actorName = config.name || this._actorName(Definition0);
// Determine actor configuration.
if (this.config && actorName) {
let actorConfig = this.config[actorName] || this.config[s.decapitalize(actorName)];
config = _.extend({ mode: 'in-memory' }, actorConfig, config);
}
if (this.options.forceInMemory && config.mode != 'in-memory') {
this.log.warn('Forcing in-memory mode due to forceInMemory flag for actor:', actorName);
config = _.extend({}, config, { mode: 'in-memory' });
}
// Actor creation.
switch (config.mode || 'in-memory') {
case 'in-memory':
return this._createInMemoryActor(Definition, parent, _.defaults({ name: actorName }, config));
case 'forked':
return P.resolve(this._createForkedActor(Definition, parent, _.defaults({ name: actorName }, config)));
case 'remote':
return P.resolve(this._createRemoteActor(Definition, parent, _.defaults({ name: actorName }, config)));
case 'threaded':
return P.resolve(this._createThreadedActor(Definition, parent, _.defaults({ name: actorName }, config)));
case 'disabled':
return new DisabledActor({ system: this });
default:
return P.resolve().throw(new Error('Unknown actor mode: ' + config.mode));
}
});
}
/**
* Starts network port listening, allowing remote actor creation by other systems.
*
* @param {Number} [port] Listening port (default is 6161).
* @param {String} [host] Listening host address (default is all addresses).
* @returns {P} Promise, which is resolved once server is ready to accept requests or a
* listening error has occurred.
*/
listen(port = defaultListeningPort, host) {
if (!this.serverPromise) {
this.serverPromise = P.fromCallback(cb => {
this.server = net.createServer();
this.server.listen(port, host);
this.server.on('listening', () => {
this.log.info(`Listening on ${this.server.address().address}:${this.server.address().port}`);
cb();
});
this.server.on('error', err => {
this.log.error('Net server error: ' + err.message);
cb(err);
});
this.server.on('connection', socket => {
let msgSocket = new MessageSocket(socket);
msgSocket.on('message', msg => {
if (msg.type != 'create-actor') return;
let psArgs = [];
if (msg.body.name) {
this.log.info(`Creating remote actor ${msg.body.name}`);
psArgs.push(msg.body.name);
}
else {
this.log.info('Creating remote actor (name unknown)');
}
let workerProcess = childProcess.fork(__dirname + '/forked-actor-worker.js', psArgs);
workerProcess.send(msg, (err) => {
if (err) return msgSocket.write({ error: 'Failed to create remote actor process: ' + err.message });
// Redirect forked process response to parent actor.
workerProcess.once('message', msg => {
msgSocket.write(msg);
msgSocket.end();
// Close IPC channel to make worker process fully independent.
workerProcess.disconnect();
workerProcess.unref();
});
});
// Handle forked process startup failure.
workerProcess.once('error', err => {
msgSocket.write({ error: 'Failed to create remote actor process: ' + err.message });
});
});
});
});
}
return this.serverPromise;
}
/**
* Returns an IP address of this system's host, through which remote systems can
* communicate with this one.
*
* @returns {String|undefined} Public IP address or undefined, if no such address exists.
*/
getPublicIpAddress() {
let ifaces = os.networkInterfaces();
let result;
_.some(ifaces, iface => {
return _.some(iface, part => {
if (part.internal === false && part.family == 'IPv4') {
result = part.address;
return true;
}
});
});
return result;
}
/**
* Initializes message marshallers.
*
* @param {Array} marshallerDefs Marshaller definitions.
* @returns {P} Initialization promise.
* @private
*/
_initializeMarshallers(marshallerDefs) {
// Validate marshaller array.
let marshallerTypes = _.countBy(marshallerDefs, marshallerDef => typeof marshallerDef);
if (_.keys(marshallerTypes).length > 1) {
return P.reject(new Error('Mixed types in marshallers configuration array are not allowed.'));
}
return P
.reduce(marshallerDefs, (memo, marshallerDef) => {
return P.resolve()
.then(() => {
if (_.isString(marshallerDef)) {
return this._loadDefinition(marshallerDef);
}
return marshallerDef;
})
.then(marshallerDef => {
if (_.isFunction(marshallerDef)) {
return this._injectResources(marshallerDef);
}
else {
return _.clone(marshallerDef);
}
})
.then(marshallerInstance => {
let types = this._readProperty(marshallerInstance, 'type');
_.isArray(types) || (types = [types]);
_.each(types, type => {
let typeName = _.isString(type) ? type : this._typeName(type);
if (!typeName) throw new Error('Failed to determine type name for marshaller: ' + marshallerInstance);
marshallerInstance.type = typeName;
memo[typeName] = marshallerInstance;
});
return memo;
});
}, {})
.then(marshallers => {
this.marshallers = marshallers;
});
}
/**
* Initializes custom balancers.
*
* @param {Array} defs Balancer definitions.
* @returns {P} Initialization promise.
* @private
*/
_initializeBalancers(defs) {
// Validate marshaller array.
let componentTypes = _.countBy(defs, def => typeof def);
if (_.keys(componentTypes).length > 1) {
return P.reject(new Error('Mixed types in balancers configuration array are not allowed.'));
}
return P
.reduce(defs, (memo, def) => {
return P.resolve()
.then(() => {
if (_.isString(def)) {
return this._loadDefinition(def);
}
return def;
})
.then(def => {
let name = this._definitionName(def);
memo[name] = def;
return memo;
});
}, {})
.then(balancers => {
this.balancers = balancers;
});
}
/**
* Creates a process-local (in-memory) actor.
*
* @param {Object|Function} Definition Actor behaviour definition.
* @param {Actor} parent Actor parent.
* @param {Object} config Actor configuration.
* @returns {*} Promise that yields a newly-created actor.
* @private
*/
_createInMemoryActor(Definition, parent, config) {
return P.resolve()
.then(() => {
if (_.isString(Definition)) {
// Module path is specified => load actor module.
return this._loadDefinition(Definition);
}
return Definition;
})
.then(definition => {
if (_.isFunction(definition)) {
return this._injectResources(definition);
}
return definition;
})
.then(definition => {
// Perform clusterization, if needed. We clusterize in-memory actors in test mode only.
if (this.options.test && config.clusterSize > 1) {
return this._createBalancerActor(Definition, parent, config)
.then(balancerActor => {
let childPromises = _.times(config.clusterSize, () =>
balancerActor.createChild(definition, _.extend({}, config, { clusterSize: 1 })));
return P.all(childPromises).return(balancerActor);
});
}
return new InMemoryActor({
system: this,
parent: parent,
definition: definition,
origDefinition: Definition,
id: config.id,
name: config.name,
config: _.omit(config, 'id', 'name')
});
});
}
/**
* Creates a forked actor.
*
* @param {Object|String} definition Actor behaviour definition or module path.
* @param {Actor} parent Actor parent.
* @param {Object} [config] Actor configuration.
* @returns {Promise} Promise that yields a newly-created actor.
* @private
*/
async _createForkedActor(definition, parent, config = {}) {
// Perform clusterization, if needed.
if (config.clusterSize > 1) {
let balancerActor = await this._createBalancerActor(definition, parent, config);
let childPromises = _.times(config.clusterSize, () =>
balancerActor.createChild(definition, _.extend({}, config, { clusterSize: 1 })));
return P.all(childPromises).return(balancerActor);
}
let actor =
new ForkedActorParent({
system: this,
parent: parent,
definition: definition,
additionalOptions: config
});
this.bus.addForkedRecipient(actor);
return actor;
}
/**
* Creates a remote actor.
*
* @param {Object|String} definition Actor behaviour definition or module path.
* @param {Actor} parent Actor parent.
* @param {Object} [config] Actor configuration.
* @returns {Promise} Promise that yields a newly-created actor.
* @private
*/
async _createRemoteActor(definition, parent, config) {
let host = config.host;
let cluster = config.cluster;
let clusterDef;
if (!host && !cluster)
throw new Error('Neither "host" nor "cluster" option specified for "remote" mode.');
if (cluster) {
clusterDef = this.options.clusters[cluster];
if (!clusterDef) throw new Error(`Cluster with name "${cluster}" is not defined.`);
}
else if (_.isArray(host)) {
clusterDef = host;
}
else if (config.clusterSize > 1) {
clusterDef = [host];
}
// Create clustered actor, if needed.
if (clusterDef) {
let balancerActor = await this._createBalancerActor(definition, parent, config);
let clusterSize = config.clusterSize || clusterDef.length;
let childPromises = _.times(clusterSize, idx => {
let hostPort = clusterDef[idx % clusterDef.length];
let hostPort0 = hostPort.split(':');
if (hostPort0.length > 1) {
hostPort0[1] = parseInt(hostPort0[1]);
}
else {
hostPort0.push(defaultListeningPort);
}
return balancerActor.createChild(
definition,
_.chain(config).omit('cluster').extend({ host: hostPort0[0], port: hostPort0[1], clusterSize: 1 }).value()
);
});
return P.all(childPromises).return(balancerActor);
}
let actor = new RemoteActorParent({
system: this,
parent: parent,
definition: definition,
pingChild: config.onCrash == 'respawn',
additionalOptions: config
});
this.bus.addForkedRecipient(actor);
return actor;
}
/**
* Creates a threaded actor.
*
* @param {Object|String} definition Actor behaviour definition or module path.
* @param {Actor} parent Actor parent.
* @param {Object} [config] Actor configuration.
* @returns {Promise} Promise that yields a newly-created actor.
* @private
*/
async _createThreadedActor(definition, parent, config = {}) {
// Perform clusterization, if needed.
if (config.clusterSize > 1) {
let balancerActor = await this._createBalancerActor(definition, parent, config);
let childPromises = _.times(config.clusterSize, () =>
balancerActor.createChild(definition, _.extend({}, config, { clusterSize: 1 })));
return P.all(childPromises).return(balancerActor);
}
let actor =
new ThreadedActorParent({ system: this, parent: parent, definition: definition, additionalOptions: config });
this.bus.addForkedRecipient(actor);
return actor;
}
/**
* Creates a balancer actor.
*
* @param {Object|String} definition Actor behaviour definition or module path.
* @param {Actor} parent Parent actor.
* @param {Object} config Actor configuration.
* - {String} [balancer] Type of balancer to use.
* - {String} name Actor name prefix.
* - {String} mode Child actor mode.
* @returns {Actor} Balancer actor instance.
* @private
*/
async _createBalancerActor(definition, parent, config) {
let config0 = _.omit(config, 'name');
if (!config.balancer || config.balancer == 'round-robin') {
return new RoundRobinBalancerActor({
system: this,
parent: parent,
namePrefix: config.name,
mode: config.mode,
definition,
config: config0
});
}
else if (config.balancer == 'random') {
return new RandomBalancerActor({
system: this,
parent: parent,
namePrefix: config.name,
mode: config.mode,
definition,
config: config0
});
}
else {
let balancerDef = this.balancers[config.balancer];
if (!balancerDef) {
throw new Error('Unknown balancer implementation: ' + config.balancer);
}
let balancerDefInstance = await this._injectResources(balancerDef);
let balancerName = this._definitionName(balancerDefInstance);
let balancerInstance = new CustomBalancerActor({
implDefinition: balancerDefInstance,
system: this,
parent: parent,
mode: config.mode,
name: balancerName,
definition,
config: config0
});
await balancerInstance.initialize();
return balancerInstance;
}
}
/**
* Generates actor creation message.
*
* @param {Object|String} definition Actor behaviour definition or module path.
* @param {Actor} actor Local endpoint actor.
* @param {Object} config Actor configuration.
* - {String} mode Actor mode ('forked' or 'remote').
* - {String} [name] Actor name.
* - {Object} [customParameters] Custom actor parameters.
* @returns {Promise} Actor creation message promise.
*/
generateActorCreationMessage(definition, actor, config) {
let createMsg = {
type: 'create-actor',
body: {
id: actor.getId(),
name: actor.getName(),
definition: _.isString(definition) ? definition : this._serializeDefinition(definition),
definitionFormat: _.isString(definition) ? 'modulePath' : 'serialized',
config: this.config,
resources: this.options.resources,
test: this.options.test,
debug: this.options.debug,
parent: {
id: actor.getParent().getId()
},
mode: config.mode,
actorConfig: _.omit(config, 'mode', 'name'),
loggerConfig: this.options.loggerConfig,
loggerParams: this.options.loggerParams,
additionalRequires: this.options.additionalRequires,
pingTimeout: this.getPingTimeout(),
clusters: this.options.clusters
}
};
config.name && (createMsg.body.name = config.name);
if (this.options.marshallers) {
let marshallerFormat = 'modulePath';
createMsg.body.marshallers = _.map(this.options.marshallers, marshaller => {
if (!_.isString(marshaller)) {
marshallerFormat = 'serialized';
return this._serializeDefinition(marshaller);
}
else {
return marshaller;
}
});
createMsg.body.marshallerFormat = marshallerFormat;
}
if (this.options.balancers) {
let balancerFormat = 'modulePath';
createMsg.body.balancers = _.map(this.options.balancers, balancer => {
if (!_.isString(balancer)) {
balancerFormat = 'serialized';
return this._serializeDefinition(balancer);
}
else {
return balancer;
}
});
createMsg.body.balancerFormat = balancerFormat;
}
if (this.options.resources && !_.isString(this.options.resources)) {
let resourceFormat = [];
createMsg.body.resources = _.map(this.options.resources, resourceDef => {
if (!_.isString(resourceDef)) {
resourceFormat.push('serialized');
return this._serializeDefinition(resourceDef);
}
else {
resourceFormat.push('modulePath');
return resourceDef;
}
});
createMsg.body.resourceFormat = resourceFormat;
}
if (this.options.logger) {
if (_.isString(this.options.logger)) {
createMsg.body.logger = this.options.logger;
createMsg.body.loggerFormat = 'modulePath';
}
else {
createMsg.body.logger = this._serializeDefinition(this.options.logger);
createMsg.body.loggerFormat = 'serialized';
}
}
return P.resolve()
.then(() => {
if (config.customParameters) {
let customParametersMarshalledTypes = {};
return P
.reduce(_.pairs(config.customParameters), (memo, kv) => {
let key = kv[0];
let value = kv[1];
if (value instanceof ClientActorProxy) {
value = value.getWrapped();
}
if (value instanceof Actor) {
let marshaller = this.getForkedActorReferenceMarshaller();
return marshaller.marshall(value)
.then(marshalledValue => {
memo[key] = marshalledValue;
customParametersMarshalledTypes[key] = 'InterProcessReference';
})
.return(memo);
}
else if (value instanceof http.Server || value instanceof net.Server) {
if (createMsg.socketHandle) throw new Error('Only one socket handle is allowed in custom parameters.');
createMsg.socketHandle = value;
customParametersMarshalledTypes[key] = 'SocketHandle';
memo[key] = value instanceof http.Server ? 'http.Server' : 'net.Server';
}
else {
memo[key] = value;
}
return memo;
}, {})
.then(customParameters => {
createMsg.body.actorConfig.customParameters = customParameters;
if (!_.isEmpty(customParametersMarshalledTypes)) {
createMsg.body.customParametersMarshalledTypes = customParametersMarshalledTypes;
}
});
}
})
.return(createMsg);
}
/**
* Generates a new ID for an actor.
*
* @returns {String} New actor ID.
*/
generateActorId() {
return new bson.ObjectID().toString();
}
/**
* Helper function to correctly import modules in different processes with
* different directory layout. If a module path ends with /, imports the whole
* directory. If a module path starts with //, lookup will be made by absolute path.
* If a module path starts with /, lookup will be made relative to project root.
*
* @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) {
if (modulePath[0] != '/' && modulePath[0] != '.') {
return globalRequire(modulePath);
}
else if (_.last(modulePath) == '/') {
return this.requireDirectory(modulePath);
}
else if (s.startsWith(modulePath, '//')) {
return globalRequire(modulePath.substr(common.isWindows() ? 2 : 1));
}
else {
return globalRequire(appRootPath + modulePath);
}
}
/**
* Imports all modules from a given directory.
*
* @param {String} path Directory path. If starts with //, the path will be absolute.
* If starts with /, the path will be relative to a project directory (the one with package.json file).
* @returns {Object} Module file name -> loaded module map object.
*/
requireDirectory(path) {
let path0 = path;
if (s.startsWith(path, '//')) {
path0 = path0.substr(1);
}
else if (path0[0] == '/') {
path0 = appRootPath + path0;
}
return requireDir(path0);
}
/**
* Destroys this system. All actors will be destroyed and all destroy hooks will be called.
*
* @returns {P} Operation promise.
*/
destroy() {
if (this.destroying) return this.destroyPromise;
this.destroying = true;
process.removeListener('SIGINT', this.sigintHandler);
process.removeListener('SIGTERM', this.sigtermHandler);
this.unListenConfig && this.unListenConfig();
this.destroyPromise = this.rootActorPromise
.then(rootActor => rootActor.destroy())
.catch(_.noop) // Initialization and destruction errors are being logged.
// Destroy marshallers.
.then(() => {
if (this.forkedActorReferenceMarshaller) {
return this.forkedActorReferenceMarshaller.destroy();
}
})
.then(() => {
if (this.remoteActorReferenceMarshaller) {
return this.remoteActorReferenceMarshaller.destroy();
}
})
// Destroy system resources.
.then(() => {
return P.map(_.values(this.resourceDefPromises), resource => {
if (resource && _.isFunction(resource.destroy)) {
return resource.destroy();
}
});
})
.then(() => {
if (this.server) {
this.server.close();
}
})
.finally(() => {
if (this.options.mode == 'forked' || this.options.mode == 'remote') {
this.log.info('Killing forked system process.');
process.exit();
}
});
return this.destroyPromise;
}
/**
* Checks whether this system is overloaded, i.e. when event loop lag is
* greater that a configured threshold.
*
* @returns {Boolean} True if overloaded, false otherwise.
*/
isOverloaded() {
let busyLagLimit = this.options.busyLagLimit;
if (busyLagLimit <= 0) return false; // If 0 or negative, the system is never busy.
busyLagLimit = busyLagLimit || 3000; // Take 3 seconds default if not specified.
return tooBusy.lag() > busyLagLimit;
}
/**
* Loads actor resource definitions.
*
* @param {Function[]|String[]|String} resources Array of resource classes or module paths, or a
* path to a directory with resource modules.
* @returns {P} Resource definition array promise.
* @private
*/
_loadResourceDefinitions(resources) {
if (!resources) return P.resolve([]);
if (_.isArray(resources)) {
return P.map(resources, resource => {
if (_.isString(resource)) return this._loadDefinition(resource);
return resource;
});
}
else if (_.isString(resources)) {
return P.resolve(_.map(this.requireDirectory(resources), module => module.default || module));
}
else {
return P.reject(new Error('Illegal value for "resources" option.'));
}
}
/**
* Loads actor behaviour definition from a given module.
*
* @param {String} path Actor behaviour module path.
* @returns {P} Operation promise, which yields an actor behaviour.
* @private
*/
_loadDefinition(path) {
return P.resolve().then(() => {
let ret = this.require(path);
// TypeScript default export support.
if (ret.default) {
ret = ret.default;
}
return ret;
});
}
/**
* Determines a given definition name.
*
* @param {Object|Function} Definition Behaviour definition.
* @param {String} [nameField] Name of an additional field to use for name resolution.
* @returns {String} Definition name or empty string, if name is not defined.
* @private
*/
_definitionName(Definition, nameField) {
// Use 'getName' getter, if present.
if (_.isFunction(Definition.getName)) return Definition.getName();
// Take name field, if present.
if (nameField && Definition[nameField]) return _.result(Definition, nameField);
// Take 'name' field, if present.
if (Definition.name) return _.result(Definition, 'name');
// Use class name, if present.
let typeName = this._typeName(Definition);
if (typeName) return typeName;
if (_.isFunction(Definition)) {
return this._actorName(new Definition());
}
return '';
}
/**
* Determines actor name based on actor definition.
*
* @param {Object|Function} Definition Actor behaviour definition.
* @returns {String} Actor name or empty string, if actor name is not defined.
* @private
*/
_actorName(Definition) {
return this._definitionName(Definition, 'actorName');
}
/**
* Determines resource name based on resource definition.
*
* @param {Object|Function} Definition Resource definition.
* @returns {String} Resource name or empty string, if name is not defined.
* @private
*/
_resourceName(Definition) {
return this._definitionName(Definition, 'resourceName');
}
/**
* Attempts to determine a name of a given type.
*
* @param {*} type Type of interest.
* @returns {String|undefined} Type name or undefined, if type name cannot be determined.
* @private
*/
_typeName(type) {
if (!type) return;
if (_.isFunction(type)) {
return type.typeName || type.name;
}
if (type.constructor) {
return _.result(type.constructor, 'typeName') || type.constructor.name;
}
}
/**
* Performs actor definition resource injection.
*
* @param {Function} Definition Definition class.
* @returns {P} Promise of definition instance with injected resources.
* @private
*/
_injectResources(Definition) {
let resourceNames = _.result(Definition, 'inject');
if (resourceNames && !_.isArray(resourceNames)) {
resourceNames = [resourceNames];
}
// Resource injection.
if (resourceNames && _.isFunction(Definition)) {
return P
.map(resourceNames, resourceName => {
return this._initializeResource(resourceName)
.then(resourceDef => resourceDef && resourceDef.getResource())
.tap(resource => {
if (!resource) {
throw new Error(
`Failed to inject resource "${resourceName}" to actor ${this._actorName(Definition)}, ` +
`definition ${Definition}`);
}
});
})
// Create an instance of actor definition, passing resources as constructor arguments.
.then(resources => new Definition(...resources));
}
return P.resolve(new Definition());
}
/**
* Initializes resource with a given name. An existing resource is returned, if already initialized.
*
* @param {String} resourceName Resource name.
* @param {String[]} [depPath] Resource dependency path for detecting cyclic dependencies.
* @returns {Promise} Initialized resource definition instance promise. Resolves to undefined,
* if resource with given name is not found.
* @private
*/
_initializeResource(resourceName, depPath) {
if (this.resourceDefPromises[resourceName]) return this.resourceDefPromises[resourceName];
depPath = depPath || [resourceName];
let resourceDefPromise = this.resourceDefClassesPromise
.then(resourceDefClasses => {
// Attempt to find a resource definition class.
let ResourceDefCls = _.find(resourceDefClasses, ResourceDefCls => {
let resourceName0 = this._resourceName(ResourceDefCls);
return resourceName0 == resourceName;
});
if (ResourceDefCls) {
let depsPromise = P.resolve([]);
if (_.isFunction(ResourceDefCls.inject)) {
depsPromise = P.map(
ResourceDefCls.inject(),
resourceDep => {
let newDepPath = depPath.concat(resourceDep);
if (_.contains(depPath, resourceDep))
throw new Error('Cyclic resource dependency: ' + newDepPath.join('->'));
return this._initializeResource(resourceDep, newDepPath).then(resourceDef => {
if (!resourceDef) throw new Error(`Resource with name ${resourceDep} not found.`);
return resourceDef.getResource();
});
});
}
return depsPromise.then(deps => {
let resourceInstance = ResourceDefCls;
if (!common.isPlainObject(ResourceDefCls)) {
resourceInstance = new ResourceDefCls(...deps);
}
if (_.isFunction(resourceInstance.initialize)) {
return P.resolve(resourceInstance)
.tap(() => resourceInstance.initialize(this));
}
else {
return resourceInstance;
}
});
}
});
this.resourceDefPromises[resourceName] = resourceDefPromise;
return resourceDefPromise;
}
/**
* Reads a given property from an object. Attempts to read either directly by name or by getter (if present).
*
* @param {Object} object Object of interest.
* @param {String} propName Property name.
* @returns {*} Property value or undefined.
* @private
*/
_readProperty(object, propName) {
let ret = object[propName];
if (!ret) {
let getterName = `get${s.capitalize(propName)}`;
if (_.isFunction(object[getterName])) {
ret = object[getterName]();
}
}
return ret;
}
/**
* Serializes a given actor behaviour definition for transferring to other process.
*
* @param {Object|Function|Array} def Actor behaviour definition.
* @returns {String} Serialized actor behaviour.
* @private
*/
_serializeDefinition(def) {
if (_.isArray(def)) {
return toSource(_.map(def, item => this._serializeDefinition(item)));
}
if (common.isPlainObject(def)) return toSource(def);
if (_.isFunction(def)) { // Class-defined behaviour.
return this._serializeClassDefinition(def);
}
throw new Error('Cannot serialize actor definition: ' + def);
}
/**
* Serializes a given class-defined actor behaviour.
*
* @param {Function} def Class-defined actor behaviour.
* @returns {String} Serialized actor behaviour.
* @private
*/
_serializeClassDefinition(def) {
// Get a base class for behaviour class.
let base = Object.getPrototypeOf(def);
let baseBehaviour = '';
if (base && base.name) {
// Have a user-defined super class. Serialize it as well.
baseBehaviour = this._serializeClassDefinition(base);
}
let selfString = def.toString();
if (s.startsWith(selfString, 'function')) {
selfString = this._serializeEs5ClassDefinition(def, selfString, base.name);
}
else if (s.startsWith(selfString, 'class')) {
selfString += '; ' + def.name + ';';
}
return baseBehaviour + selfString;
}
/**
* Serializes a given ES5 class actor behaviour definition.
*
* @param {Function} def Actor behaviour definition in ES5 class form.
* @param {String} [selfString] Stringified class head.
* @param {String} [baseName] Base class name.
* @returns {String} Serialized actor behaviour.
* @private
*/
_serializeEs5ClassDefinition(def, selfString, baseName) {
let clsName = this._actorName(def);
if (!clsName) {
clsName = randomString.generate({
length: 12,
charset: 'alphabetic'
});
}
let expressions = [`var ${clsName} = ${selfString || def.toString()};\n`];
if (baseName) {
expressions.push(`_inherits(${clsName}, ${baseName});`);
}
let staticMemberNames = Object.getOwnPropertyNames(def);
_.each(staticMemberNames, memberName => {
if (memberName != 'length' && memberName != 'prototype' && memberName != 'name') {
expressions.push(`${clsName}.${memberName} = ${def[memberName].toString()};\n`);
}
});
let membersNames = Object.getOwnPropertyNames(def.prototype);
_.each(membersNames, memberName => {
if (memberName != 'constructor') {
expressions.push(`${clsName}.prototype.${memberName} = ${def.prototype[memberName].toString()};\n`);
}
});
expressions.push(`${clsName};`);
return expressions.join('');
}
/**
* Generates a lightweight proxy object for this system to expose only
* specific methods to a client.
*
* @returns {Object} Proxy object.
* @private
*/
_selfProxy() {
return {
require: this.require.bind(this),
getLog: this.getLog.bind(this)
};
}
/**
* Loads actor configuration.
*
* @param {Object|String} config Actor configuration object or file path.
* @returns {P} Operation promise.
* @private
*/
_loadConfiguration(config) {
if (_.isObject(config)) {
this.config = config;
this.options.mode || this.log.info('Using programmatic actor configuration.');
return P.resolve();
}
// Do not load configuration from file in test mode.
if (this.options.test) return P.resolve();
this.config = {};
let defaultPath = appRootPath + '/actors.json';
return fs.readFileAsync(defaultPath)
.then(data => {
this.log.info('Loaded default actor configuration file: ' + defaultPath);
this.config = JSON.parse(data);
})
.catch(() => {
this.log.info(
'Didn\'t find (or couldn\'t load) default configuration file ' + defaultPath + '.');
})
.then(() => {
if (!_.isString(config)) return;
// Config path specified => read custom configuration and extend default one.
return fs.readFileAsync(config)
.then(data => {
this.log.info('Loaded external actor configuration file: ' + config);
if (!_.isEmpty(this.config)) {
this.log.info('Extending default actor configuration (' + defaultPath +
') with external actor configuration (' + config + ')');
}
this.config = _.extend(this.config, JSON.parse(data));
})
.catch(() => {
this.log.info(
'Didn\'t find (or couldn\'t load) external actor configuration file ' + config +
', leaving default configuration.');
});
})
.then(() => {
this.log.info('Resulting actor configuration: ' + JSON.stringify(this.config, null, 2));
});
}
/**
* Listens for changes in actor configuration files and applies configuration changes.
*
* @param {Object|String} config Actor configuration.
* @returns {Function|undefined} Un-subscribe function in case of successful subscription.
* @private
*/
_listenConfiguration(config) {
// Only listen to configuration changes in root system.
if (this.options.parent) return;
// Do not listen in case of programmatic configuration.
if (_.isObject(config)) return;
// Do not listen for config changes in test mode.
if (this.options.test) return;
let listener = (cur, prev) => {
if (_.isEqual(cur, prev)) return;
this.log.info('Configuration file changed, re-reading configuration...');
this._loadConfiguration(config)
.then(() => this.rootActorPromise)
.then(rootActor => rootActor.changeGlobalConfiguration(this.config))
.catch(err => {
this.log.warn('Failed to re-load actor system configuration: ' + err.message);
});
};
let defaultPath = appRootPath + '/actors.json';
fs.watchFile(defaultPath, listener);
if (_.isString(config)) {
fs.watchFile(config, listener);
}
return () => {
fs.unwatchFile(defaultPath, listener);
if (_.isString(config)) {
fs.unwatchFile(config, listener);
}
};
}
/**
* Instantiates a given item. An item can be a string (in which case it is loaded using require()),
* a class (in which case it is instantiated with "new"), or an object (in which case it is simply
* returned).
*
* @param {String|Function|Object} Item Item to instantiate.
* @param {*} [params] Item initialization parameters.
* @returns {Object} Instantiated object.
* @private
*/
_instantiate(Item, params) {
if (_.isString(Item)) {
Item = this.require(Item);
}
if (_.isFunction(Item)) {
Item = new Item(params);
}
return Item;
}
/**
* @returns {ActorSystem} Default actor system.
*/
static default() {
if (defaultSystem) {
defaultSystem = new ActorSystem();
}
return defaultSystem;
}
/**
* A recommended function for ES5 class inheritance. If this function is used for inheritance,
* the actors are guaranteed to be successfully transferred to forked/remote nodes.
*
* @param {Function} subClass Sub cla