@loke/ipc
Version:
Inter-process/service communications lib
335 lines (272 loc) • 10.1 kB
JavaScript
var CHANNEL_LIMIT = 8;
var LOCALHOST_AMQP = 'amqp://localhost';
var amqplib = require('amqplib');
var Q = require('q');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
exports.Communications = Communications;
var externalServiceBuilder = require('./externalservicebuilder');
var rpc = require('./rpc');
var pubsub = require('./pubsub');
/**
* LOKE IPC Communications
* @param {Object} [options] - options
* @param {ILogger} [options.logger] - the logger to use
* @param {string} [options.amqpUri] - the AMQP URI to connect to. defaults to localhost.
* @param {Object} [options.newRelic] - new relic instance that will be used for tracing if provided
*/
function Communications(options) {
EventEmitter.call(this);
this.options = options;
this.amqpUri = options && options.amqpUri;
this.channels = [];
this.rpcClient = rpc.createRpcClient();
this._serviceToClose = [];
// a logger can be optionally specified, else console.* will be used
this._logger = (options && options.logger) || require('./console-logger');
this.serviceRepliers = {};
this.serviceMetaRepliers = {};
}
util.inherits(Communications, EventEmitter);
///////////////////////
// Lifecycle Methods //
///////////////////////
Communications.prototype.start = function () {
var self = this;
self._logger.debug('Starting Communications');
return Q.try(function () {
return self._connect();
})
.then(function () {
return self.getRpcClient();
})
.then(function () {
self._logger.debug('Started Communications');
});
// @TODO: if we've got the logging service, add a context builder method here.
};
Communications.prototype.stop = function () {
var self = this;
self._logger.debug('stopped Communications');
return self.closeServices()
.then(function() {
return self._connectionPromise
.then(function (connection) {
return connection.close();
});
});
};
///////////////////
// SETUP METHODS //
///////////////////
Communications.prototype._connect = function () {
var self = this;
var amqpUri = self.amqpUri || LOCALHOST_AMQP;
self._logger.debug('Connecting to rabbitmq at ' + amqpUri);
self._connectionPromise = Q.when(amqplib.connect(amqpUri));
return self._connectionPromise
.then(function (connection) {
self.connection = connection;
// @NOTE: if we lose connection to rabbit, KILL PROCESS.....
// we do this b/c we lose alot of certainty about the state of this system after losing
// the connection and the safest thing to do is stop and start again via upstart or w/e
//
// ------------
// TODO: rather than process.exit from a module (which is just wierd) throw an 'error' event from this instance.
// If nothing handles the error the process will exit anyway.
// But it gives the application using the module to handle the exit in a more graceful way
// - DW
// ------------
//
connection.on('error', function (err) {
if (self.onerror) {
return self.onerror(err);
}
throw new Error('KILLING PROCESS, Received an error in rabbitmq connection: \n'+err.stack);
});
connection.on('close', function (err) {
if (self.onclose) {
return self.onclose(err);
}
throw new Error('KILLING PROCESS, Rabbitmq connection closed: \n'+err.stack);
});
});
};
// @TODO: add command to get status (sockets connected, public interface, exposed name, general rabbit info)
/**
* Exposes a public interface that can be used to make rpc requests into this service
* Interface is retrieved via the servicename-metadata queue.
* Creates a service replies queue for rpc to this service
* Also creates a broadcast queue to make rpc requests that should hit all services intances.
* classes is the wired class constructors, NOT instances.
* instanceContainer is typically the IOC class. It should return a class instance with .get().
* @param {number} version - tbd
* @param {string[]} services - tbd
* @param {ServiceContainer} instanceContainer - tbd
* @param {Object} [options] - tbd
* @return {Promise} - promise that resolves when done
*/
Communications.prototype.exposeServices = function (version, services, instanceContainer) {
var self = this;
if (!Array.isArray(services)) {
throw new Error('services must be an array of strings');
} else {
return Q.all(services.map(function (serviceName) {
var serviceInstance = instanceContainer.get(serviceName);
return self.exposeService(version, serviceInstance);
}));
}
};
/**
* Exposes (publishes) a service
* @param {Object} serviceInstance - the service being published
* @param {Object} definition - the service publish definition
* @param {Object} [options] - the service options
* @param {NewRelic} [options.newRelic] - optionally provide an instance of the new relic module. If provided it will be used for request.
* @return {Promise<?>} [description]
*/
Communications.prototype.exposeService = function (serviceInstance, definition) {
var self = this;
var options = this.options;
// if no definition supplied, look for definition as metadata properties on the serviceInstance constructor
// (for legacy shit)
if (!definition) {
definition = serviceInstance.constructor;
}
if (!definition.$expose) {
throw new Error('Service requires $expose on the constructor');
}
var version = definition.$version || 0;
var serviceRpc = rpc.createFromService(definition, serviceInstance, options);
this._bindRequestEvents(serviceRpc);
var metaRpc = rpc.createRpcService(serviceRpc.name, version, true, options);
rpc.createMetaInterfaceMethods(serviceRpc, metaRpc);
self.serviceRepliers[serviceRpc.name] = serviceRpc;
self.serviceMetaRepliers[metaRpc.name] = metaRpc;
self._serviceToClose.push(serviceRpc, metaRpc);
self._logger.debug('Exposing service ' + serviceRpc.name);
return Q.try(function () {
return serviceRpc.setupSocket(self);
})
.then(function () {
return metaRpc.setupSocket(self);
})
.then(function () {
return {
service: serviceRpc,
meta: metaRpc
};
});
};
Communications.prototype._bindRequestEvents = function (serviceRpc) {
var proxyRequestStart = this._proxyEvent.bind(this, 'request:start');
var proxyRequestComplete = this._proxyEvent.bind(this, 'request:complete');
var proxyRequestError = this._proxyEvent.bind(this, 'request:error');
var proxyRequestUncaughtError = this._proxyEvent.bind(this, 'request:uncaughterr');
serviceRpc.on('request:start', proxyRequestStart);
serviceRpc.on('request:complete', proxyRequestComplete);
serviceRpc.on('request:error', proxyRequestError);
serviceRpc.on('request:uncaughterr', proxyRequestUncaughtError);
serviceRpc.on('close', function() {
serviceRpc.removeListener('request:start', proxyRequestStart);
serviceRpc.removeListener('request:complete', proxyRequestComplete);
serviceRpc.removeListener('request:error', proxyRequestError);
serviceRpc.removeListener('request:uncaughterr', proxyRequestUncaughtError);
});
};
Communications.prototype._proxyEvent = function (eventName, eventArgs) {
this.emit(eventName, eventArgs);
};
Communications.prototype.closeServices = function() {
return Q.all(this._serviceToClose.map(function (service) {
return service.close();
}));
};
Communications.prototype.getChannel = function () {
var chanPromise;
if (this.channels.length < CHANNEL_LIMIT) {
chanPromise = this._connectionPromise.post('createChannel');
} else {
chanPromise = this.channels.shift();
}
this.channels.push(chanPromise);
return chanPromise;
};
Communications.prototype.getRpcClient = function () {
if (!this._rpcClientPromise) {
this._rpcClientPromise = this.rpcClient.setupSocket(this)
.thenResolve(this.rpcClient);
}
return this._rpcClientPromise;
};
Communications.prototype.getSubscriber = function () {
if (!this._rpcSubscriberPromise) {
this._rpcSubscriber = new pubsub.Subscriber(this._logger);
this._rpcSubscriberPromise = this._rpcSubscriber.setupSocket(this)
.thenResolve(this._rpcSubscriber);
}
return this._rpcSubscriberPromise;
};
Communications.prototype.createPublisher = function (serviceName) {
var publisher = new pubsub.Publisher(serviceName);
return publisher.setupSocket(this)
.thenResolve(publisher);
};
Communications.prototype.reportError = function (msg, error) {
this._logger.error(msg, error);
};
Communications.prototype.debugLog = function (msg, data) {
this._logger.debug(msg, data);
};
Communications.prototype.create = function(externalServiceName, versionNumber) {
var Ctor = Communications.create(externalServiceName, versionNumber);
var service = new Ctor(this, this._logger);
return service.start()
.then(function() {
return service;
});
};
// Used for the creation of convenience rpc methods for external service interfaces
Communications.create = function(externalServiceName, versionNumber) {
return externalServiceBuilder(externalServiceName, versionNumber);
};
module.exports = exports = Communications;
/**
* Container that provides services.
* @interface ServiceContainer
*/
/**
* Gets a container by it's name or key
* @function
* @name ServiceContainer#get
* @param {string} name - the name or key of the service
* @returns {*} an instance of the named service
*/
/**
* Container that provides services.
* @interface ILogger
*/
/**
* Gets a container by it's name or key
* @function
* @name ILogger#info
* @param {string} arg - the argument to log
*/
/**
* Gets a container by it's name or key
* @function
* @name ILogger#warn
* @param {string} arg - the argument to log
*/
/**
* Gets a container by it's name or key
* @function
* @name ILogger#error
* @param {string} arg - the argument to log
*/
/**
* Gets a container by it's name or key
* @function
* @name ILogger#debug
* @param {string} arg - the argument to log
*/