@omneedia/socketcluster
Version:
SocketCluster - A Highly parallelized WebSocket server cluster to make the most of multi-core machines/instances.
702 lines (574 loc) • 19.8 kB
JavaScript
var EventEmitter = require('events').EventEmitter;
var scBroker = require('sc-broker');
var async = require('async');
var ClientCluster = require('./clientcluster').ClientCluster;
var SCChannel = require('sc-channel').SCChannel;
var hash = require('sc-hasher').hash;
var scErrors = require('sc-errors');
var BrokerError = scErrors.BrokerError;
var ProcessExitError = scErrors.ProcessExitError;
var AbstractDataClient = function (dataClient) {
this._dataClient = dataClient;
};
AbstractDataClient.prototype = Object.create(EventEmitter.prototype);
AbstractDataClient.prototype.set = function () {
this._dataClient.set.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.expire = function () {
this._dataClient.expire.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.unexpire = function () {
this._dataClient.unexpire.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.add = function () {
this._dataClient.add.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.get = function () {
this._dataClient.get.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.getRange = function () {
this._dataClient.getRange.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.getAll = function () {
this._dataClient.getAll.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.count = function () {
this._dataClient.count.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.remove = function () {
this._dataClient.remove.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.removeRange = function () {
this._dataClient.removeRange.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.removeAll = function () {
this._dataClient.removeAll.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.splice = function () {
this._dataClient.splice.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.pop = function () {
this._dataClient.pop.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.hasKey = function () {
this._dataClient.hasKey.apply(this._dataClient, arguments);
};
AbstractDataClient.prototype.extractKeys = function (object) {
return this._dataClient.extractKeys(object);
};
AbstractDataClient.prototype.extractValues = function (object) {
return this._dataClient.extractValues(object);
};
/*
exec(queryFn,[ data, callback])
*/
AbstractDataClient.prototype.exec = function () {
var options = {};
var callback;
if (arguments[1] instanceof Function) {
callback = arguments[1];
} else {
options.data = arguments[1];
callback = arguments[2];
}
if (arguments[1] && !(arguments[1] instanceof Function)) {
options.data = arguments[1];
}
this._dataClient.exec(arguments[0], options, callback);
};
var SCExchange = function (privateClientCluster, publicClientCluster, ioClusterClient) {
AbstractDataClient.call(this, publicClientCluster);
this._privateClientCluster = privateClientCluster;
this._publicClientCluster = publicClientCluster;
this._ioClusterClient = ioClusterClient;
this._channelEmitter = new EventEmitter();
this._channels = {};
this._messageHander = this._handleChannelMessage.bind(this);
this._ioClusterClient.on('message', this._messageHander);
};
SCExchange.prototype = Object.create(AbstractDataClient.prototype);
SCExchange.prototype.destroy = function () {
this._ioClusterClient.removeListener('message', this._messageHander);
};
SCExchange.prototype._handleChannelMessage = function (message) {
var channelName = message.channel;
if (this.isSubscribed(channelName)) {
this._channelEmitter.emit(channelName, message.data);
}
};
SCExchange.prototype._triggerChannelSubscribe = function (channel) {
var channelName = channel.name;
channel.state = channel.SUBSCRIBED;
channel.emit('subscribe', channelName);
EventEmitter.prototype.emit.call(this, 'subscribe', channelName);
};
SCExchange.prototype._triggerChannelSubscribeFail = function (err, channel) {
var channelName = channel.name;
channel.state = channel.UNSUBSCRIBED;
channel.emit('subscribeFail', err, channelName);
EventEmitter.prototype.emit.call(this, 'subscribeFail', err, channelName);
};
SCExchange.prototype._triggerChannelUnsubscribe = function (channel, newState) {
var channelName = channel.name;
var oldState = channel.state;
if (newState) {
channel.state = newState;
} else {
channel.state = channel.UNSUBSCRIBED;
}
if (oldState === channel.SUBSCRIBED) {
channel.emit('unsubscribe', channelName);
EventEmitter.prototype.emit.call(this, 'unsubscribe', channelName);
}
};
SCExchange.prototype.send = function (data, mapIndex, callback) {
if (mapIndex == null) {
// Send to all brokers in cluster if mapIndex is not provided
mapIndex = '*';
}
var targetClients = this._privateClientCluster.map({mapIndex: mapIndex}, 'send');
var len = targetClients.length;
var tasks = [];
for (var i = 0; i < len; i++) {
(function (client) {
tasks.push(function (cb) {
client.send(data, cb);
});
})(targetClients[i]);
}
async.parallel(tasks, callback);
};
SCExchange.prototype.publish = function (channelName, data, callback) {
this._ioClusterClient.publish(channelName, data, callback);
};
SCExchange.prototype.subscribe = function (channelName) {
var self = this;
var channel = this._channels[channelName];
if (!channel) {
channel = new SCChannel(channelName, this);
this._channels[channelName] = channel;
}
if (channel.state === channel.UNSUBSCRIBED) {
channel.state = channel.PENDING;
this._ioClusterClient.subscribe(channelName, function (err) {
if (err) {
self._triggerChannelSubscribeFail(err, channel);
} else {
self._triggerChannelSubscribe(channel);
}
});
}
return channel;
};
SCExchange.prototype.unsubscribe = function (channelName) {
var channel = this._channels[channelName];
if (channel) {
if (channel.state !== channel.UNSUBSCRIBED) {
this._triggerChannelUnsubscribe(channel);
// The only case in which unsubscribe can fail is if the connection is closed or dies.
// If that's the case, the server will automatically unsubscribe the client so
// we don't need to check for failure since this operation can never really fail.
this._ioClusterClient.unsubscribe(channelName);
}
}
};
SCExchange.prototype.channel = function (channelName) {
var currentChannel = this._channels[channelName];
if (!currentChannel) {
currentChannel = new SCChannel(channelName, this);
this._channels[channelName] = currentChannel;
}
return currentChannel;
};
SCExchange.prototype.destroyChannel = function (channelName) {
var channel = this._channels[channelName];
channel.unwatch();
channel.unsubscribe();
delete this._channels[channelName];
};
SCExchange.prototype.subscriptions = function (includePending) {
var subs = [];
var channel, includeChannel;
for (var channelName in this._channels) {
if (this._channels.hasOwnProperty(channelName)) {
channel = this._channels[channelName];
if (includePending) {
includeChannel = channel && (channel.state === channel.SUBSCRIBED ||
channel.state === channel.PENDING);
} else {
includeChannel = channel && channel.state === channel.SUBSCRIBED;
}
if (includeChannel) {
subs.push(channelName);
}
}
}
return subs;
};
SCExchange.prototype.isSubscribed = function (channelName, includePending) {
var channel = this._channels[channelName];
if (includePending) {
return !!channel && (channel.state === channel.SUBSCRIBED ||
channel.state === channel.PENDING);
}
return !!channel && channel.state === channel.SUBSCRIBED;
};
SCExchange.prototype.watch = function (channelName, handler) {
this._channelEmitter.on(channelName, handler);
};
SCExchange.prototype.unwatch = function (channelName, handler) {
if (handler) {
this._channelEmitter.removeListener(channelName, handler);
} else {
this._channelEmitter.removeAllListeners(channelName);
}
};
SCExchange.prototype.watchers = function (channelName) {
return this._channelEmitter.listeners(channelName);
};
SCExchange.prototype.setMapper = function (mapper) {
this._publicClientCluster.setMapper(mapper);
};
SCExchange.prototype.getMapper = function () {
return this._publicClientCluster.getMapper();
};
SCExchange.prototype.map = function () {
return this._publicClientCluster.map.apply(this._publicClientCluster, arguments);
};
var Server = module.exports.Server = function (options) {
var self = this;
var dataServer;
this._dataServers = [];
this._shuttingDown = false;
var readyCount = 0;
var len = options.brokers.length;
var firstTime = true;
var startDebugPort = options.debug;
var startInspectPort = options.inspect;
var triggerBrokerStart = function (brokerInfo) {
self.emit('brokerStart', brokerInfo);
};
for (var i = 0; i < len; i++) {
var launchServer = function (i) {
var socketPath = options.brokers[i];
dataServer = scBroker.createServer({
id: i,
debug: startDebugPort ? startDebugPort + i : null,
inspect: startInspectPort ? startInspectPort + i : null,
instanceId: options.instanceId,
socketPath: socketPath,
secretKey: options.secretKey,
expiryAccuracy: options.expiryAccuracy,
downgradeToUser: options.downgradeToUser,
brokerControllerPath: options.appBrokerControllerPath,
processTermTimeout: options.processTermTimeout,
ipcAckTimeout: options.ipcAckTimeout,
brokerOptions: options.brokerOptions
});
self._dataServers[i] = dataServer;
if (firstTime) {
dataServer.on('ready', function (brokerInfo) {
if (++readyCount >= options.brokers.length) {
firstTime = false;
self.emit('ready');
}
triggerBrokerStart({
id: brokerInfo.id,
pid: brokerInfo.pid,
respawn: false
});
});
} else {
dataServer.on('ready', function (brokerInfo) {
triggerBrokerStart({
id: brokerInfo.id,
pid: brokerInfo.pid,
respawn: true
});
});
}
dataServer.on('error', function (err) {
self.emit('error', err);
});
dataServer.on('exit', function (brokerInfo) {
var exitMessage = 'Broker server at socket path ' + socketPath + ' exited with code ' + brokerInfo.code;
if (brokerInfo.signal != null) {
exitMessage += ' and signal ' + brokerInfo.signal;
}
var err = new ProcessExitError(exitMessage, brokerInfo.code);
err.pid = process.pid;
if (brokerInfo.signal != null) {
err.signal = brokerInfo.signal;
}
self.emit('error', err);
self.emit('brokerExit', {
id: brokerInfo.id,
pid: brokerInfo.pid,
code: brokerInfo.code,
signal: brokerInfo.signal
});
if (!self._shuttingDown) {
launchServer(i);
}
});
dataServer.on('brokerMessage', function (brokerId, data, callback) {
self.emit('brokerMessage', brokerId, data, callback);
});
};
launchServer(i);
}
};
Server.prototype = Object.create(EventEmitter.prototype);
Server.prototype.sendToBroker = function (brokerId, data, callback) {
var targetBroker = this._dataServers[brokerId];
if (targetBroker) {
targetBroker.sendToBroker(data, callback);
} else {
var err = new BrokerError('Broker with id ' + brokerId + ' does not exist');
err.pid = process.pid;
this.emit('error', err);
callback && callback(err);
}
};
Server.prototype.killBrokers = function () {
for (var i in this._dataServers) {
if (this._dataServers.hasOwnProperty(i)) {
this._dataServers[i].destroy();
}
}
};
Server.prototype.destroy = function () {
this._shuttingDown = true;
this.killBrokers();
};
var Client = module.exports.Client = function (options) {
var self = this;
this.options = options;
this._ready = false;
var dataClient;
var dataClients = [];
for (var i in options.brokers) {
if (options.brokers.hasOwnProperty(i)) {
var socketPath = options.brokers[i];
dataClient = scBroker.createClient({
socketPath: socketPath,
secretKey: options.secretKey,
pubSubBatchDuration: options.pubSubBatchDuration,
connectRetryErrorThreshold: options.connectRetryErrorThreshold
});
dataClients.push(dataClient);
}
}
var hasher = function (key) {
return hash(key, dataClients.length);
};
var channelMethods = {
publish: true,
subscribe: true,
unsubscribe: true,
isSubscribed: true
};
this._defaultMapper = function (key, method, clientIds) {
if (channelMethods[method]) {
if (key == null) {
return clientIds;
}
return hasher(key);
} else if (method === 'query' || method === 'exec' || method === 'send') {
var mapIndex = key.mapIndex;
if (mapIndex) {
// A mapIndex of * means that the action should be sent to all
// brokers in the cluster.
if (mapIndex === '*') {
return clientIds;
} else {
if (mapIndex instanceof Array) {
var hashedIndexes = [];
var len = mapIndex.length;
for (var i = 0; i < len; i++) {
hashedIndexes.push(hasher(mapIndex[i]));
}
return hashedIndexes;
} else {
return hasher(mapIndex);
}
}
}
return 0;
} else if (method === 'removeAll') {
return clientIds;
}
return hasher(key);
};
var emitError = function (error) {
self.emit('error', error);
};
var emitWarning = function (warning) {
self.emit('warning', warning);
};
// The user cannot change the _defaultMapper for _privateClientCluster.
this._privateClientCluster = new ClientCluster(dataClients);
this._privateClientCluster.setMapper(this._defaultMapper);
this._privateClientCluster.on('error', emitError);
this._privateClientCluster.on('warning', emitWarning);
// The user can provide a custom mapper for _publicClientCluster.
// The _defaultMapper is used by default.
this._publicClientCluster = new ClientCluster(dataClients);
this._publicClientCluster.setMapper(this._defaultMapper);
this._publicClientCluster.on('error', emitError);
this._publicClientCluster.on('warning', emitWarning);
this._sockets = {};
this._exchangeSubscriptions = {};
this._exchangeClient = new SCExchange(this._privateClientCluster, this._publicClientCluster, this);
this._clientSubscribers = {};
this._clientSubscribersCounter = {};
var readyNum = 0;
var firstTime = true;
var dataClientReady = function () {
if (++readyNum >= dataClients.length && firstTime) {
firstTime = false;
self._ready = true;
self.emit('ready');
}
};
for (var j in dataClients) {
if (dataClients.hasOwnProperty(j)) {
dataClients[j].on('ready', dataClientReady);
}
}
this._privateClientCluster.on('message', this._handleExchangeMessage.bind(this));
};
Client.prototype = Object.create(EventEmitter.prototype);
Client.prototype.destroy = function (callback) {
this._privateClientCluster.removeAll(callback);
};
Client.prototype.on = function (event, listener) {
if (event === 'ready' && this._ready) {
listener();
} else {
EventEmitter.prototype.on.apply(this, arguments);
}
};
Client.prototype.exchange = function () {
return this._exchangeClient;
};
Client.prototype._dropUnusedSubscriptions = function (channel, callback) {
var self = this;
var subscriberCount = this._clientSubscribersCounter[channel];
if (subscriberCount == null || subscriberCount <= 0) {
delete this._clientSubscribers[channel];
delete this._clientSubscribersCounter[channel];
if (!this._exchangeSubscriptions[channel]) {
self._privateClientCluster.unsubscribe(channel, callback);
return;
}
}
callback && callback();
};
Client.prototype.publish = function (channelName, data, callback) {
this._privateClientCluster.publish(channelName, data, callback);
};
Client.prototype.subscribe = function (channel, callback) {
var self = this;
if (!this._exchangeSubscriptions[channel]) {
this._exchangeSubscriptions[channel] = 'pending';
this._privateClientCluster.subscribe(channel, function (err) {
if (err) {
delete self._exchangeSubscriptions[channel];
self._dropUnusedSubscriptions(channel);
} else {
self._exchangeSubscriptions[channel] = true;
}
callback && callback(err);
});
} else {
callback && callback();
}
};
Client.prototype.unsubscribe = function (channel, callback) {
delete this._exchangeSubscriptions[channel];
this._dropUnusedSubscriptions(channel, callback);
};
Client.prototype.unsubscribeAll = function (callback) {
var self = this;
var tasks = [];
for (var channel in this._exchangeSubscriptions) {
if (this._exchangeSubscriptions.hasOwnProperty(channel)) {
delete this._exchangeSubscriptions[channel];
(function (channel) {
tasks.push(function (cb) {
self._dropUnusedSubscriptions(channel, cb);
});
})(channel);
}
}
async.parallel(tasks, callback);
};
Client.prototype.isSubscribed = function (channel, includePending) {
if (includePending) {
return !!this._exchangeSubscriptions[channel];
}
return this._exchangeSubscriptions[channel] === true;
};
Client.prototype.subscribeSocket = function (socket, channel, callback) {
var self = this;
var addSubscription = function (err) {
if (!err) {
if (!self._clientSubscribers[channel]) {
self._clientSubscribers[channel] = {};
self._clientSubscribersCounter[channel] = 0;
}
if (!self._clientSubscribers[channel][socket.id]) {
self._clientSubscribersCounter[channel]++;
}
self._clientSubscribers[channel][socket.id] = socket;
}
callback && callback(err);
};
this._privateClientCluster.subscribe(channel, addSubscription);
};
Client.prototype.unsubscribeSocket = function (socket, channel, callback) {
var self = this;
if (this._clientSubscribers[channel]) {
if (this._clientSubscribers[channel][socket.id]) {
this._clientSubscribersCounter[channel]--;
delete this._clientSubscribers[channel][socket.id];
if (this._clientSubscribersCounter[channel] <= 0) {
delete this._clientSubscribers[channel];
delete this._clientSubscribersCounter[channel];
}
}
}
this._dropUnusedSubscriptions(channel, function () {
callback && callback.apply(self, arguments);
});
};
Client.prototype.setSCServer = function (scServer) {
this.scServer = scServer;
};
Client.prototype._handleExchangeMessage = function (channel, message, options) {
var packet = {
channel: channel,
data: message
};
var emitOptions = {};
if (this.scServer) {
// Optimization
try {
emitOptions.stringifiedData = this.scServer.codec.encode({
event: '#publish',
data: packet
});
} catch (err) {
this.emit('error', err);
return;
}
}
var subscriberSockets = this._clientSubscribers[channel];
for (var i in subscriberSockets) {
if (subscriberSockets.hasOwnProperty(i)) {
subscriberSockets[i].emit('#publish', packet, null, emitOptions);
}
}
this.emit('message', packet);
};