node-busmq
Version:
A high performance, highly-available and scalable, message bus and queueing system for node.js backed by Redis
209 lines (182 loc) • 6.42 kB
JavaScript
var events = require('events');
var util = require('util');
var logger = require('./logger');
var WSMux = require('./wsmux');
/**
* A pool of websockets that keeps a minimum of open websockets to a list of bus federation servers
* @param bus the bus owning this pool
* @param options additional options
* @constructor
*/
function WSPool(bus, options) {
events.EventEmitter.call(this);
this.setMaxListeners(0);
this.bus = bus;
this.logger = bus.logger.withTag(bus.id+':wspool');
this.closed = false;
this.pool = {};
this.notificationChannels = {};
this.options = options || {};
this.options.secret = this.options.secret || 'notsosecret';
if (!this.options.poolSize || this.options.poolSize <= 0) {
this.options.poolSize = 10;
}
this.options.replaceDelay = this.options.replaceDelay || 5000;
this.options.urls = this.options.urls || [];
this._setupPool();
}
util.inherits(WSPool, events.EventEmitter);
/**
* Setup the pool of websockets for all the federation urls
* @private
*/
WSPool.prototype._setupPool = function() {
var _this = this;
this.logger.isDebug() && this.logger.debug('setting up ' + this.options.poolSize + ' websockets per pool for urls: ' + JSON.stringify(this.options.urls));
this.options.urls.forEach(function(url) {
_this.pool[url] = [];
for (var i = 0; i < _this.options.poolSize; ++i) {
_this._add(url);
}
});
};
WSPool.prototype._createNotificationsChannel = function(url) {
var _this = this;
var fedPubsub = this.bus.federate(this.bus.pubsub(this.bus.notificationsChannel), url);
fedPubsub.once('ready', function(pubsub) {
pubsub.on('message', function(message) {
try {
message = JSON.parse(message);
} catch(e) {
_this.logger.error('received unparseable notification message from ' + url + ': ' + message);
return;
}
_this.emit('notification', message, url);
});
pubsub.on('error', function(err) {
_this.logger.error('error on notifications channel from ' + url + ': ' + err);
});
pubsub.once('subscribed', function() {
// we are now federating to the target url
_this.emit('federating', url);
});
pubsub.subscribe();
});
fedPubsub.on('error', function(err) {
_this.logger.error('error on federation of notifications channel from ' + url + ': ' + err);
});
fedPubsub.on('unauthorized', function() {
_this.logger.error('unauthorized federation of notifications channel to ' + url);
});
fedPubsub.on('close', function() {
delete _this.notificationChannels[url];
});
return fedPubsub;
};
/**
* Add a new websocket to the pool
* @param url the url to open the websocket to
* @private
*/
WSPool.prototype._add = function(url) {
if (this.closed) {
return;
}
if (!this.pool[url]) {
this.logger.isDebug() && this.logger.debug('cannot add websocket to ' + url + ': url is not recognized');
return;
}
var _this = this;
this.logger.isDebug() && this.logger.debug('opening websocket to ' + url);
var wsmux = new WSMux(url, this.options);
this.pool[url].push(wsmux);
var onOpen = function() {
_this.logger.isDebug() && _this.logger.debug('websocket to ' + url + ' added to pool');
// if this is the first access to this url, create the notifications channel
if (!_this.notificationChannels[url]) {
_this.notificationChannels[url] = _this._createNotificationsChannel(url);
}
};
var onClose = function() {
_this.logger.isDebug() && _this.logger.debug('websocket to ' + url + ' closed');
};
var onError = function(error) {
_this.logger.isDebug() && _this.logger.debug('websocket to ' + url + ' error: ' + JSON.stringify(error));
};
var onReopen = function() {
_this.logger.isDebug() && _this.logger.debug('websocket to ' + url + ' is reopened');
};
var onFatal = function(error) {
_this.logger.isDebug() && _this.logger.debug('websocket to ' + url + ' fatal error (placing url in error state): ' + JSON.stringify(error));
_this.pool[url] = error;
};
var onShutdown = function() {
wsmux.removeListener('open', onOpen);
onOpen = undefined;
wsmux.removeListener('close', onClose);
onClose = undefined;
wsmux.removeListener('error', onError);
onError = undefined;
wsmux.removeListener('fatal', onFatal);
onFatal = undefined;
wsmux.removeListener('reopen', onReopen);
onReopen = undefined;
wsmux.removeListener('shutdown', onShutdown);
onShutdown = undefined;
wsmux.close();
wsmux = undefined;
};
wsmux.on('open', onOpen);
wsmux.on('close', onClose);
wsmux.on('error', onError);
wsmux.on('fatal', onFatal);
wsmux.on('reopen', onReopen);
wsmux.on('shutdown', onShutdown);
};
/**
* Get a websocket channel from the pool for the specified url, a new channel on a random websocket will be created.
* @param url the url to get the websocket for. if none is available right now it will be retrieved once one is available.
* @param cb receives the websocket channel
*/
WSPool.prototype.get = function(url, cb) {
// the url is not supported
if (!this.pool[url]) {
process.nextTick(function() {
cb && cb('url ' + url + ' is not recognized');
});
return;
}
var _this = this;
// the url is in error state
if (typeof this.pool[url] === 'string') {
process.nextTick(function() {
cb && cb(_this.pool[url]);
});
return;
}
// create a new channel over a websocket from the pool selected in round-robin
var wsmux = _this.pool[url].shift();
var channel = wsmux.channel();
_this.pool[url].push(wsmux);
cb && cb(null, channel);
};
/**
* Close the pool and disconnect all open websockets
*/
WSPool.prototype.close = function() {
this.closed = true;
var _this = this;
Object.keys(this.notificationChannels).forEach(function(url) {
_this.notificationChannels[url].close();
});
this.notificationChannels = {};
Object.keys(this.pool).forEach(function(url) {
if (typeof _this.pool[url] !== 'string') {
_this.pool[url].forEach(function(wsmux) {
wsmux.emit('shutdown');
});
}
});
this.pool = {};
};
exports = module.exports = WSPool;