@loke/ipc
Version:
Inter-process/service communications lib
278 lines (235 loc) • 7.63 kB
JavaScript
var DEFAULT_TIMEOUT = 1000;
var Q = require('q');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var rpcmethod = require('./rpcmethod');
var domain = require('domain');
var guid = require('uuid/v4');
var errors = require('./errors');
var timer = require('./timer');
// Wraps the request callbacks, sockets and emitters into a nice interface.
// Create it, then call repond in the callback for a reply
// For a request, create it and then just call request
// Holds REQ sockets. REP sockets managed by main Communications class
/**
* Creates a new RPC service connection
* @param {string} serviceName - the service's unique name
* @param {integer} version - the service version
* @param {Object} meta - service metadata
* @param {Object} options - options
* @param {ILogger} options.logger - logger to use
* @param {NewRelic} [options.newRelic] - new relic instance
*/
function RpcService (serviceName, version, meta, options) {
EventEmitter.call(this);
if (!serviceName) {
throw new Error('Cannot create service, no serviceName');
}
this._logger = options && options.logger;
this.name = serviceName;
this.version = version || 0;
this.methods = {};
this.isMeta = meta === true;
var queueTail = this.isMeta ? 'meta' : 'request';
this._closed = false;
this._stopDeffered = Q.defer();
this._pendingRequests = 0;
this.queueName = util.format('%s-%d-%s', this.name, this.version, queueTail);
}
util.inherits(RpcService, EventEmitter);
exports.RpcService = RpcService;
function formatLogMsg(rpc) {
var req = rpc.request;
var res = rpc.response;
if (res.error) {
if (req) {
return util.format('Rpc error: %s%s >!> Error: %s', req.method, JSON.stringify(req.params), res.error.message);
} else {
return util.format('Rpc request error: ??? >!> Error: %s', res.error.message);
}
} else {
return util.format('Rpc: %s%s >>> %s', req.method, JSON.stringify(req.params), JSON.stringify(res.result));
}
}
// return promise that resolves with what's emitted by respond
RpcService.prototype.setupSocket = function (comm) {
return comm.getChannel()
.then((ch) => {
this.ch = ch;
return ch.assertQueue(this.queueName, {durable: false});
})
.then(() => {
return this.ch.consume(this.queueName, this._msgHandler.bind(this));
})
.then((ok) => {
this._consumerTag = ok.consumerTag;
});
};
RpcService.prototype._logRpc = function(rpc) {
if (!this._logger) { return; }
var log = formatLogMsg(rpc);
if (rpc.response.error) {
this._logger.warn(log, rpc);
} else {
this._logger.debug(log, rpc);
}
};
RpcService.prototype._msgHandler = function (msg) {
var d = domain.create();
// var cid = d.cid = msg.properties.correlationId || guid();
// var eventArgs = {};
d.run(() => {
this._countUp();
this._handleMsg(msg)
.finally(function() {
this._countDown();
})
.done();
});
d.on('error', (err) => {
this.emit('request:uncaughterr', err);
if (this._logger) {
this._logger.error('BAD REQUEST ERROR: ' + err.message + '\n' + err.stack, err);
}
d.dispose();
});
};
RpcService.prototype._countUp = function () {
this._pendingRequests++;
};
RpcService.prototype._countDown = function () {
this._pendingRequests--;
if (this._closed && this._pendingRequests <= 0) {
this._stopDeffered.resolve();
}
};
RpcService.prototype._handleMsg = function(msg) {
var self = this;
var method;
// time the process
var executionTimer = timer.start();
return Q.try(function () {
return JSON.parse(msg.content.toString());
})
.fail(function (err) {
// modify error and rethrow...
err.code = errors.PARSE_ERROR; // JSON error
throw err;
})
.then(function (request) {
method = request.method;
self.emit('request:start', {
event: 'request:start',
method: method,
service: self.name,
request: request
});
return self._handleRequest(request)
.fail(self._handleMsgError.bind(self, method, request));
})
.fail(self._handleMsgError.bind(self, method, null))
.then(function (reqContext) {
return self._handleResponse(msg, reqContext.response)
.then(function () {
return self._logRpc(reqContext);
})
.finally(function() {
self.emit('request:complete', {
event: 'request:complete',
method: method,
service: self.name,
request: reqContext.request,
response: reqContext.response,
duration: timer.stop(executionTimer)
});
});
});
};
RpcService.prototype._handleMsgError = function (method, request, err) {
this.emit('request:error', {
event: 'request:error',
method: method,
service: this.name,
request:request,
error: err
});
return {
request: request || null,
response: rpcmethod.formatError(request && request.id, err)
};
};
RpcService.prototype.close = function() {
var self = this;
return self.ch.cancel(self._consumerTag)
.then(function () {
self._closed = true;
if (self._pendingRequests <= 0) {
self._stopDeffered.resolve();
}
self.emit('close', true);
return self._stopDeffered.promise;
});
};
RpcService.prototype.exposeMethod = function (methodName, listener, timeout, options) {
this.methods[methodName] = new rpcmethod.RpcMethod(null, listener, timeout || DEFAULT_TIMEOUT, methodName, this.name, options);
};
RpcService.prototype.exposeServiceMethod = function (service, methodName, options) {
if (typeof service[methodName] !== 'function') {
throw new Error('Service ' + service, ' has no method ' + methodName);
}
var timeout = service[methodName].$timeout || DEFAULT_TIMEOUT;
this.methods[methodName] = rpcmethod.createFromServiceMethod(service, methodName, this.name, timeout, options);
};
RpcService.prototype._handleRequest = function (request) {
return this.methods[request.method].execRequest(request) // RpcMethod#execRequest
.then((response) => {
return {
request: request,
response: response
};
});
};
RpcService.prototype._handleResponse = function (msg, response) {
var self = this;
var replyTo = msg.properties.replyTo;
var options = {
deliveryMode: true,
correlationId: process.domain && process.domain.cid || guid()
};
var chunk = new Buffer(JSON.stringify(response));
var pushed = self.ch.sendToQueue(replyTo, chunk, options);
return Q.Promise(function (resolve) {
if (!pushed) {
if (self._logger) { self._logger.warn('RPC RESPONSE BUFFERED'); }
self.ch.once('drain', resolve);
} else {
resolve();
}
})
.then(function () {
self.ch.ack(msg);
});
};
RpcService.prototype.getMeta = function () {
var self = this;
var interfaces = Object.keys(self.methods).map(function (methodName) {
return {
methodName: methodName,
methodTimeout: self.methods[methodName].timeout,
paramNames: self.methods[methodName].paramNames
};
});
return {
version: self.version,
serviceName: self.name,
dateExposed: new Date(),
interfaces: interfaces
};
};
// -32700 Parse error Invalid JSON was received by the server.
// An error occurred on the server while parsing the JSON text.
// -32600 Invalid Request The JSON sent is not a valid Request object.
// -32601 Method not found The method does not exist / is not available.
// -32602 Invalid params Invalid method parameter(s).
// -32603 Internal error Internal JSON-RPC error.
// -32000 to -32099 Server error Reserved for implementation-defined server-errors.