dl
Version:
DreamLab Libs
503 lines (395 loc) • 16.5 kB
JavaScript
var WebSocket = require('ws');
var WebSocketServer = WebSocket.Server;
var ZooKeeper = require('zookeeper');
var AbstractDataProvider = require('./AbstractDataProvider.js').AbstractDataProvider;
var WebSocketDataProvider = function (params) {
AbstractDataProvider.apply(this, arguments);
this._wss = null;
this._params = params;
this._connections = [];
this._readyCallback = null;
this._reloadedCallback = null;
this._ready = false;
this._messageId = 0;
this._lastUseConnIdx = -1;
this._callbacks = {};
/* lista zapiętych watcherów: {key: klucz, callback: callback} */
this._watchers = [];
this._requestQueue = [];
this._ephemeralOwner = {};
this._ephemeralKeys = {};
};
WebSocketDataProvider.prototype = Object.create(AbstractDataProvider.prototype);
WebSocketDataProvider.prototype._sendThrough = function (connIdx, method, params, callback) {
var messageId = ++this._messageId;
if (method !== 'get') {
console.info('WebSocketDataProvider/_sendThrough ->', connIdx, messageId, method, params.key);
}
var data = JSON.stringify({
id: messageId,
method: method,
params: params
});
var ws = this._connections[connIdx];
var that = this;
this._callbacks[connIdx][messageId] = callback;
ws.send(data, { compress: !!this._params.compress, binary: !!this._params.binary }, function (err) {
if (err) {
console.error('WebSocketDataProvider/_sendThrough wsc=%s id=%d error: %s', connIdx, messageId, err);
delete that._callbacks[connIdx][messageId];
callback(err);
}
if (method !== 'get') {
console.info('WebSocketDataProvider/_sendThrough done ->', connIdx, messageId, method, params.key);
}
});
};
WebSocketDataProvider.prototype._send = function (method, params, callback) {
var connIdx = this._getConnectionIdx(params.key);
this._sendThrough(connIdx, method, params, callback);
};
WebSocketDataProvider.prototype._getOpenConnections = function () {
var openConnections = [];
for (var i = 0, l = this._connections.length; i < l; i++) {
if (this._connections[i] && this._connections[i].readyState == WebSocket.OPEN) {
openConnections.push(i);
}
}
return openConnections;
};
WebSocketDataProvider.prototype._getConnectionIdx = function (ephemeralKey) {
if (ephemeralKey && this._ephemeralOwner.hasOwnProperty(ephemeralKey)) {
return this._ephemeralOwner[ephemeralKey];
}
var openConnections = this._getOpenConnections();
this._lastUseConnIdx = (this._lastUseConnIdx + 1) % openConnections.length;
return openConnections[this._lastUseConnIdx];
};
WebSocketDataProvider.prototype._addConnection = function (ws) {
var that = this;
// find first empty slot - null
var idx = this._connections.indexOf(null);
if (idx > -1) {
this._connections[idx] = ws;
} else {
idx = this._connections.length;
this._connections.push(ws);
}
this._callbacks[idx] = {};
this._ephemeralKeys[idx] = [];
console.info('DataProvider[WebSocket]: Entered connected state', idx);
ws.on('close', function () {
console.info('DataProvider[WebSocket]: Entered closed state', idx);
that._removeConnection(this);
// when one connection is closed it may hold some ephemeral nodes that must be registered again
if (that.isConnected()) {
that._reloadedCallback();
that._assignWatchers();
}
});
ws.on('message', function (message) {
try {
message = JSON.parse(message);
} catch (ex) {
console.error('DataProvider[WebSocket]: Failed to parse message');
return;
}
if (message.method && message.method === 'notify') {
var params = message.params;
console.info('DataProvider[WebSocket]: On watch notify: %s wsc: %d', params.key, idx);
for (var i = 0, l = that._watchers.length; i < l; i++) {
var watcher = that._watchers[i];
if (watcher.key === params.key && watcher.connIdx === idx) {
console.info('DataProvider[WebSocket]: On watch notify: %s, launching...', watcher.key);
process.nextTick((function (_watcher, _rc, _error, _data, _stat) {
return function () {
if (!params.rc) {
_watcher.callback(null, _data, _stat);
} else {
_watcher.callback(_rc, _error);
}
};
})(watcher, params.rc, params.error, params.data, params.stat));
}
}
return;
} else if (message.method && message.method === 'reloaded') {
console.info('DataProvider[WebSocket]: Reloaded wsc: %d', idx);
return that._reloadedCallback();
} else if (!message.id) {
console.warn('DataProvider[WebSocket]: Message has no id');
return;
}
//console.log('DataProvider[WebSocket] <-', message.id);
if (!that._callbacks[idx][message.id]) {
console.warn('DataProvider[WebSocket]: No callback for message id:', message.id);
return;
}
process.nextTick(function () {
var cb = that._callbacks[idx][message.id];
delete that._callbacks[idx][message.id];
cb(message.rc, message.error, message.data, message.stat);
});
});
};
WebSocketDataProvider.prototype._removeConnection = function (ws) {
var index = this._connections.indexOf(ws);
console.info('DataProvider[WebSocket]: removing connection', index);
// remove ephemeral owner for all keys owned by dropped connection
for (var i = 0, l = this._ephemeralKeys[index].length; i < l; i++) {
delete this._ephemeralOwner[this._ephemeralKeys[index][i]];
}
// remove list of keys owned by dropped connection
delete this._ephemeralKeys[index];
// invalidate all waiting callbacks
var cbKeys = Object.keys(this._callbacks[index]);
var cb;
console.info('WebSocketDataProvider/_removeConnection invalidating %d callbacks wsc=%d', cbKeys.length, index);
for (var j = 0, jl = cbKeys.length; j < jl; j++) {
var msgId = cbKeys[j];
cb = this._callbacks[index][msgId];
delete this._callbacks[index][msgId];
cb(ZooKeeper.ZCONNECTIONLOSS);
}
delete this._callbacks[index];
// mark watches as orphaned
for (var k = 0, lk = this._watchers.length; k < lk; k++) {
var watcher = this._watchers[k];
if (watcher.connIdx === index) {
console.info('WebSocketDataProvider/_removeConnection remove watcher connection wsc=%d key=%s',
index, watcher.key);
watcher.connIdx = null;
}
}
this._connections[index] = null;
};
WebSocketDataProvider.prototype.init = function (readyCallback, reloadedCallback) {
var that = this;
this._readyCallback = function () {
console.log('DataProvider[WebSocket]: ready callback');
readyCallback(null, true);
};
this._reloadedCallback = function () {
console.log('DataProvider[WebSocket]: reloaded callback');
reloadedCallback(null, true);
};
console.info('DataProvider[WebSocket]: init');
this._wss = new WebSocketServer(this._params);
this._wss.on('connection', function (ws) {
that._addConnection(ws);
that._assignWatchers();
that._reloadedCallback();
that._flushQueue();
/* pierwsze uruchomienie */
if (!that._ready) {
that._readyCallback();
}
that._ready = true;
});
};
WebSocketDataProvider.prototype.get = function (key, callback) {
if (!this.isConnected()) {
this._deferRequest(arguments.callee.bind(this, key, callback), callback);
return;
}
var that = this;
this._send('get', { key: key }, function (rc, error, data, stat) {
switch (rc) {
case ZooKeeper.ZOK:
callback(null, data, stat);
break;
case ZooKeeper.ZCONNECTIONLOSS:
case ZooKeeper.ZOPERATIONTIMEOUT:
console.info('DataProvider[WebSocket]:', rc, ' - defering request', 'GET', key);
that._deferRequest(that.get.bind(that, key, callback), callback);
break;
default:
console.warn('DataProvider[WebSocket]:', 'Unhandled error', 'GET', rc, error, key);
callback(rc, error);
}
});
};
WebSocketDataProvider.prototype.set = function (key, value, options, callback) {
if (!this.isConnected()) {
this._deferRequest(arguments.callee.bind(this, key, value, options, callback), callback);
return;
}
options = options || {};
var that = this;
var flags = options.flags || 0;
var ownerIdx = this._getConnectionIdx(key);
var ephemeral = flags & 1; // ZOO_EPHEMERAL
this._sendThrough(ownerIdx, 'set', {
key: key,
value: value,
options: options
}, function (rc, error, stat) {
switch (rc) {
case ZooKeeper.ZOK:
if (ephemeral && stat && stat.createdInThisSession && !that._ephemeralOwner[key]) {
that._ephemeralOwner[key] = ownerIdx;
that._ephemeralKeys[ownerIdx].push(key);
}
callback(null, stat);
break;
case ZooKeeper.ZCONNECTIONLOSS: /* stracilismy polaczenie do ZK */
case ZooKeeper.ZOPERATIONTIMEOUT:
console.warn('DataProvider[WebSocket]:', rc, ' - defering request', 'SET', key);
that._deferRequest(that.set.bind(that, key, value, options, callback), callback);
break;
default:
console.warn('DataProvider[WebSocket]:', 'Unhandled error', 'SET', rc, error, key);
callback(rc, error);
}
});
};
WebSocketDataProvider.prototype.sync = function (key, callback) {
if (!this.isConnected()) {
this._deferRequest(arguments.callee.bind(this, key, callback), callback);
return;
}
var that = this;
this._send('sync', { key: key }, function (rc, error) {
switch (rc) {
case ZooKeeper.ZOK:
callback(null, null);
break;
case ZooKeeper.ZCONNECTIONLOSS:
case ZooKeeper.ZOPERATIONTIMEOUT:
console.warn('DataProvider[WebSocket]:', rc, ' - defering request', 'SYNC', key);
that._deferRequest(that.sync.bind(that, key, callback), callback);
break;
default:
console.warn('DataProvider[WebSocket]:', 'Unhandled error', 'SYNC', rc, error, key);
callback(rc, error);
}
});
};
WebSocketDataProvider.prototype.remove = function (key, options, callback) {
if (!this.isConnected()) {
this._deferRequest(arguments.callee.bind(this, key, options, callback), callback);
return;
}
var that = this;
this._send('remove', { key: key, options: options }, function (rc, error) {
switch (rc) {
case ZooKeeper.ZOK:
callback(null, null);
break;
case ZooKeeper.ZCONNECTIONLOSS:
case ZooKeeper.ZOPERATIONTIMEOUT:
console.warn('DataProvider[WebSocket]:', rc, ' - defering request', 'REMOVE', key);
that._deferRequest(that.remove.bind(that, key, options, callback), callback);
break;
default:
console.warn('DataProvider[WebSocket]:', 'Unhandled error', 'REMOVE', rc, error, key);
callback(rc, error);
}
});
};
WebSocketDataProvider.prototype.exists = function (key, callback) {
if (!this.isConnected()) {
this._deferRequest(arguments.callee.bind(this, key, callback), callback);
return;
}
var that = this;
this._send('exists', { key: key }, function (rc, error, stat) {
switch (rc) {
case ZooKeeper.ZOK:
callback(null, stat);
break;
case ZooKeeper.ZCONNECTIONLOSS:
case ZooKeeper.ZOPERATIONTIMEOUT:
console.warn('DataProvider[WebSocket]:', rc, ' - defering request', 'EXISTS', key);
that._deferRequest(that.exists.bind(that, key, callback), callback);
break;
default:
console.warn('DataProvider[WebSocket]:', 'Unhandled error', 'EXISTS', rc, error, key);
callback(rc, error);
}
});
};
WebSocketDataProvider.prototype.watch = function (key, callback) {
console.info('DataProvider[WebSocket]: creating watch on %s', key);
// register data for future reconnects / notifications
var watcher = {
key: key,
callback: callback,
connIdx: undefined // <- not yet set
};
this._watchers.push(watcher);
this._watch(key, watcher);
};
WebSocketDataProvider.prototype._watch = function (key, watcher) {
if (!this.isConnected()) {
this._deferRequest(arguments.callee.bind(this, key, watcher));
return;
}
var connIdx = this._getConnectionIdx(key);
var that = this;
// stick key to one connection to prevent multiple through different connections
if (!this._ephemeralOwner.hasOwnProperty(key)) {
this._ephemeralOwner[key] = connIdx;
this._ephemeralKeys[connIdx].push(key);
}
watcher.connIdx = connIdx;
console.info('DataProvider[WebSocket]: watching %s on connection %d', key, connIdx);
this._sendThrough(connIdx, 'watch', { key: key }, function (rc, error, data, stat) {
switch (rc) {
case ZooKeeper.ZOK:
console.info('DataProvider[WebSocket]: watch %s created on %d', key, connIdx);
watcher.callback(null, data, stat);
break;
case ZooKeeper.ZCONNECTIONLOSS:
case ZooKeeper.ZOPERATIONTIMEOUT:
console.info('DataProvider[WebSocket]:', rc, ' - defering request', 'WATCH', key);
that._deferRequest(that._watch.bind(that, key, watcher));
break;
default:
console.warn('DataProvider[WebSocket]:', 'Unhandled error', 'WATCH', rc, key);
watcher.callback(rc, error);
}
});
};
WebSocketDataProvider.prototype._assignWatchers = function () {
console.info('DataProvider[WebSocket]: assigning watches after reconnect');
var tmpWatchers = this._watchers;
this._watchers = [];
for (var i = 0, len = tmpWatchers.length; i < len; i++) {
if (tmpWatchers[i].connIdx === null) {
console.info('DataProvider[WebSocket]: reassign watch', tmpWatchers[i].key);
this.watch(tmpWatchers[i].key, tmpWatchers[i].callback);
} else {
console.info('DataProvider[WebSocket]: restore watch wsc=%s key=%s',
tmpWatchers[i].connIdx, tmpWatchers[i].key);
this._watchers.push(tmpWatchers[i]);
}
}
};
WebSocketDataProvider.prototype.isConnected = function () {
return this._getOpenConnections().length > 0;
};
WebSocketDataProvider.prototype._deferRequest = function (fnc, timeoutCb) {
this._requestQueue.push(fnc);
if (timeoutCb) {
var that = this;
setTimeout(function () {
var idx = that._requestQueue.indexOf(fnc);
if (idx === -1) {
return;
}
that._requestQueue.splice(idx, 1);
return timeoutCb(new Error('Timed out'));
}, WebSocketDataProvider.DEFER_TIMEOUT);
}
};
WebSocketDataProvider.prototype._flushQueue = function () {
console.info('DataProvider[WebSocket]: Flushing queue', this._requestQueue.length);
var requestQueue = this._requestQueue.slice();
this._requestQueue = [];
while (requestQueue.length) {
requestQueue.shift()();
}
};
WebSocketDataProvider.DEFER_TIMEOUT = 60000;
exports.WebSocketDataProvider = WebSocketDataProvider;