UNPKG

dl

Version:

DreamLab Libs

554 lines (434 loc) 18.5 kB
var core = require('core'); var WebSocket = require('ws'); var Types = core.common.Types; var Logger = require('../logger').Logger; var ConfigurationManager = require('./ConfigurationManager.js').ConfigurationManager; var ForwardingConfigurationManager = function () { ConfigurationManager.apply(this, arguments); this._endpoints = []; this._intervals = {}; this._reconnectTimers = {}; this._ephemeral = {}; this._endpointsLabels = {}; this._ws = {}; this._watches = {}; this._endpointsWatches = {}; this._pingTimer = null; this._pingTimeout = ForwardingConfigurationManager.DEFAULT_PING_TIMEOUT; this._pingTimeoutTimers = {}; this._pingTimeouts = {}; this._maxPingTimeouts = ForwardingConfigurationManager.MAX_PING_TIMEOUTS; this._stats = {}; this._queue = []; this._queueRunning = 0; this._queueMax = ForwardingConfigurationManager.MAX_CONCURRENT_QUERIES; }; ForwardingConfigurationManager.prototype = Object.create(ConfigurationManager.prototype); ForwardingConfigurationManager.prototype.setMaxConcurrentQueries = function (count) { this._queueMax = count; return this; }; ForwardingConfigurationManager.prototype.setPingTimeout = function (timeout) { this._pingTimeout = timeout; return this; }; ForwardingConfigurationManager.prototype.setPingInterval = function (interval) { var that = this; clearInterval(this._pingTimer); this._pingTimer = setInterval(function () { for (var i = 0, l = that._endpoints.length; i < l; i++) { that._ping(that._endpoints[i]); } }, interval); return this; }; ForwardingConfigurationManager.prototype.setClients = function (endpoints) { var oldEndpoints = this._endpoints.slice(0); var newEndpoints = []; var that = this; // connect new endpoints endpoints.forEach(function (endpoint) { if (Types.isString(endpoint)) { newEndpoints.push(endpoint); that.addClient(endpoint) } else { newEndpoints.push(endpoint.url); that.addClient(endpoint.url, endpoint.label); } }); // disconnect endpoints not longer wanted oldEndpoints.forEach(function (endpoint) { if (newEndpoints.indexOf(endpoint) === -1) { that.removeClient(endpoint); } }); return this; }; ForwardingConfigurationManager.prototype.addClient = function (endpoint, label) { console.info('ForwardingConfigurationManager/addClient', endpoint); if (this._endpoints.indexOf(endpoint) > -1) { console.warn('ForwardingConfigurationManager/addClient', endpoint, 'already exists'); return false; } this._endpoints.push(endpoint); this._endpointsLabels[endpoint] = label; this._connectEndpoints(); return true; }; ForwardingConfigurationManager.prototype.removeClient = function (endpoint) { console.info('ForwardingConfigurationManager/removeClient', endpoint); var index = this._endpoints.indexOf(endpoint); if (index === -1) { console.warn('ForwardingConfigurationManager/removeClient', endpoint, 'not exists'); return false; } this._endpoints.splice(index, 1); this._disconnect(endpoint); delete this._intervals[endpoint]; delete this._stats[endpoint]; delete this._endpointsLabels[endpoint]; return true; }; ForwardingConfigurationManager.prototype.initialize = function () { if (this.isReady()) { console.warn('ForwardingConfigurationManager/initialize already initialized'); return; } console.info('ForwardingConfigurationManager/initialize'); ConfigurationManager.prototype.initialize.call(this); var that = this; this._connectEndpoints(); // on reload reconnect all endpoints since they may have some ephemeral nodes to restore this.addEventListener(ForwardingConfigurationManager.Event.RELOADED, function () { console.info('ForwardingConfigurationManager/initialize reloading, endpoints:', that._endpoints.length); var endpoint; for (var i = 0, l = that._endpoints.length; i < l; i++) { endpoint = that._endpoints[i]; console.info('ForwardingConfigurationManager/initialize reloading, endpoint', endpoint); that._send(endpoint, { method: 'reloaded' }); } }); setInterval(function () { Logger.gauge('dl.ForwardingConfigurationManager.queue.running', that._queueRunning); Logger.gauge('dl.ForwardingConfigurationManager.queue.waiting', that._queue.length); if (that._queueRunning > 0 || that._queue.length > 0) { console.info('ForwardingConfigurationManager queue running: %d, waiting: %d', that._queueRunning, that._queue.length); } }, ForwardingConfigurationManager.QUEUE_STATS_INTERVAL); }; ForwardingConfigurationManager.prototype._ping = function (endpoint) { var that = this; if (!this._ws[endpoint] || this._ws[endpoint].readyState !== WebSocket.OPEN) { console.warn('ForwardingConfigurationManager/_ping', endpoint, 'not connected, skipping...'); return; } this._ws[endpoint].ping(Date.now(), {}, true); this._pingTimeoutTimers[endpoint] = setTimeout(function () { console.warn('ForwardingConfigurationManager/_ping timeout', endpoint); if (isNaN(parseInt(that._pingTimeouts[endpoint]))) { that._pingTimeouts[endpoint] = 0; } that._pingTimeouts[endpoint]++; if (that._pingTimeouts[endpoint] >= that._maxPingTimeouts) { that._scheduleReconnect(endpoint, 0); } }, this._pingTimeout); }; ForwardingConfigurationManager.prototype._connectEndpoints = function () { var that = this; this._endpoints.forEach(function (endpoint) { if (!that._ws[endpoint]) { that._connect(endpoint); } }); }; ForwardingConfigurationManager.prototype._scheduleReconnect = function (endpoint, currentInterval) { if (currentInterval > ForwardingConfigurationManager.RECONNECT_MAX_INTERVAL) { currentInterval = ForwardingConfigurationManager.RECONNECT_MAX_INTERVAL; } console.log('ForwardingConfigurationManager/_scheduleReconnect: reconnect', endpoint, 'after', currentInterval); var that = this; this._disconnect(endpoint); this._reconnectTimers[endpoint] = setTimeout(function () { that._intervals[endpoint] = currentInterval * ForwardingConfigurationManager.RECONNECT_DECAY; that._connect(endpoint); }, currentInterval); }; ForwardingConfigurationManager.prototype._onClose = function (endpoint, code, lastInterval) { console.info('ForwardingConfigurationManager/_onClose:', endpoint); while (this._ephemeral[endpoint] && this._ephemeral[endpoint].length > 0) { var key = this._ephemeral[endpoint].shift(); console.info('ForwardingConfigurationManager/_onClose: removing ephemeral key', key, 'for', endpoint); this.remove(key, {}, function () {}); } delete this._ephemeral[endpoint]; if (!this._ws[endpoint]) { console.info('ForwardingConfigurationManager/_onClose: client', endpoint, 'has been disconnected'); return; } this._scheduleReconnect(endpoint, lastInterval); }; ForwardingConfigurationManager.prototype._addClientWatch = function (endpoint, msgId, key) { var that = this; var creating = true; var onResult = function (cb, rc, data, stat) { if (creating) { that._send(endpoint, { id: msgId, method: 'client-watch-init', rc: rc || 0, data: data, stat: stat, error: null }); creating = false; cb(); } else { that._onWatchNotify(key, rc, data, stat); } }; if (this._watches.hasOwnProperty(key)) { // already watching this key if (this._watches[key].indexOf(endpoint) === -1) { console.info('ForwardingConfigurationManager/_addClientWatch: has key, new endpoint', key, '->', msgId, endpoint); // new client for this key this._watches[key].push(endpoint); this._endpointsWatches[endpoint].push(key); } else { console.info('ForwardingConfigurationManager/_addClientWatch: has key, has endpoint', key, '->', msgId, endpoint); } // just get this._enqueue(function (cb) { that.get(key, onResult.bind(that, cb)); }); } else { console.info('ForwardingConfigurationManager/_addClientWatch: new key', key, '->', msgId, endpoint); this._watches[key] = [ endpoint ]; this._endpointsWatches[endpoint].push(key); this._enqueue(function (cb) { that.watch(key, onResult.bind(that, cb)); }); } }; ForwardingConfigurationManager.prototype._onWatchNotify = function (key, rc, data, stat) { var that = this; var endpoints = this._watches[key]; if (!endpoints.length) { console.error('ForwardingConfigurationManager/_onWatchNotify no endpoints to notify for', key); return; } console.info('ForwardingConfigurationManager/_onWatchNotify:', key); endpoints.forEach(function (endpoint) { console.info('ForwardingConfigurationManager/_onWatchNotify:', key, '->', endpoint); that._send(endpoint, { method: 'notify', params: { key: key, rc: rc, data: data, stat: stat, error: null } }); }); }; ForwardingConfigurationManager.prototype._enqueue = function (fn) { var that = this; //mam wolne sloty to wykonuje funkcje odrazu if (this._queueRunning < this._queueMax) { console.info('ForwardingConfigurationManager/enqueue: executing, left:', this._queueRunning, this._queue.length); //najpierw wykonuje elementy na kolejce var fnx = this._queue.shift(); if (fnx && fn) { this._queue.push(fn); } else if (!fnx && fn) { fnx = fn; } if (fnx) { this._queueRunning++; fnx(function () { that._queueRunning--; that._enqueue(); }); } } else if (fn) { //dodaje do kolejki jak za duzo naraz wykonuje this._queue.push(fn); console.info('ForwardingConfigurationManager/enqueue: enqueued, count: %d', this._queueRunning, this._queue.length); } }; ForwardingConfigurationManager.prototype._onMessage = function (endpoint, msg) { // jshint ignore: line if (msg.method !== 'get') { console.info('ForwardingConfigurationManager/_onMessage:', endpoint, ' -> ', msg.id, msg.method, msg.params ? msg.params.key : ''); } var ephemeral = this._ephemeral[endpoint]; var that = this; var onResult = function (msg, cb, rc, data, stat) { that._send(endpoint, { id: msg.id, method: msg.method, rc: rc || 0, data: data, stat: stat, error: null }); cb(); }; if (!this._driver.isConnected()) { console.warn('ForwardingConfigurationManager/_onMessage: manager not ready yet, endpoint:', endpoint); return onResult(msg, function () {}, -4 /* ZCONNECTIONLOSS */); } var params = msg.params; if (!params || !params.key) { console.warn('ForwardingConfigurationManager/_onMessage: wrong params, endpoint:', endpoint); return onResult(msg, function () {}, -1002, 'Wrong params'); } this._stats[endpoint] = !this._stats[endpoint] ? 1 : ++this._stats[endpoint]; switch (msg.method) { case 'get': this._enqueue(function (cb) { that.get(params.key, onResult.bind(that, msg, cb)); }); break; case 'watch': that._addClientWatch(endpoint, msg.id, params.key); break; case 'set': this._enqueue(function (cb) { that.set(params.key, params.value, params.options, onResult.bind(that, msg, cb)); if (params.options && params.options.flags === 1 && ephemeral.indexOf(params.key) === -1) { console.info('ForwardingConfigurationManager/_onMessage: creating ephemeral key', params.key, 'for', endpoint); ephemeral.push(params.key); } }); break; case 'sync': this._enqueue(function (cb) { that.sync(params.key, onResult.bind(that, msg, cb)); }); break; case 'remove': this._enqueue(function (cb) { that.remove(params.key, params.options, onResult.bind(that, msg, cb)); var ephemeralIdx = ephemeral.indexOf(params.key); if (ephemeralIdx > -1) { console.info('ForwardingConfigurationManager/_onMessage: deleting ephemeral key', params.key, 'for', endpoint); ephemeral.splice(ephemeralIdx, 1); } }); break; case 'exists': this._enqueue(function (cb) { that.exists(params.key, onResult.bind(that, msg, cb)); }); break; default: console.warn('ForwardingConfigurationManager/_onMessage: unknown method', msg.method, 'from', endpoint); return onResult(msg, function () {}, -1001, 'Unknown method'); } }; ForwardingConfigurationManager.prototype._send = function (endpoint, data) { if (this._ws[endpoint] && this._ws[endpoint].readyState === WebSocket.OPEN) { console.info('ForwardingConfigurationManager/send: sending to: %s', endpoint, data.id, data.method); this._ws[endpoint].send(JSON.stringify(data), { compress: false, binary: false }, function (err) { if (err) { console.error('ForwardingConfigurationManager/send failed:', err); } }); } else { console.warn('ForwardingConfigurationManager/send, unable to send: %s, no socket or socket in wrong state', endpoint); } }; ForwardingConfigurationManager.prototype._disconnect = function (endpoint) { console.info('ForwardingConfigurationManager/_disconnect', endpoint); clearTimeout(this._pingTimeoutTimers[endpoint]); clearTimeout(this._reconnectTimers[endpoint]); if (this._endpointsWatches.hasOwnProperty(endpoint)) { for (var i = 0, l = this._endpointsWatches[endpoint].length; i < l; i++) { var key = this._endpointsWatches[endpoint][i]; var idx = this._watches[key].indexOf(endpoint); console.info('ForwardingConfigurationManager/_disconnect removing watch', key, '->', endpoint); this._watches[key].splice(idx, 1); } } delete this._endpointsWatches[endpoint]; delete this._pingTimeoutTimers[endpoint]; delete this._pingTimeouts[endpoint]; delete this._reconnectTimers[endpoint]; if (this._ws.hasOwnProperty(endpoint)) { var ws = this._ws[endpoint]; delete this._ws[endpoint]; ws.close(); } }; ForwardingConfigurationManager.prototype._connect = function (endpoint) { if (!this.isReady()) { console.warn('ForwardingConfigurationManager/_connect', endpoint, 'not ready'); return; } console.info('ForwardingConfigurationManager/_connect', endpoint); var that = this; var endpointStr = this._endpointsLabels[endpoint] || 'UNKNOWN'; var currentInterval = this._intervals[endpoint] || ForwardingConfigurationManager.RECONNECT_INTERVAL; var ws = this._ws[endpoint] = new WebSocket(endpoint); this._ephemeral[endpoint] = []; this._endpointsWatches[endpoint] = []; ws.on('open', function () { console.info('ForwardingConfigurationManager/_connect: connected to', endpoint); currentInterval = ForwardingConfigurationManager.RECONNECT_INTERVAL; }); ws.on('pong', function (data) { var pingDate = parseInt(data); var latency = Date.now() - pingDate; Logger.gauge(['dl.ForwardingConfigurationManager.endpoints', endpointStr, 'latency'], latency); Logger.gauge(['dl.ForwardingConfigurationManager.endpoints', endpointStr, 'requests'], that._stats[endpoint]); if (latency > 100 || that._stats[endpoint] > 0) { console.info('ForwardingConfigurationManager/ping endpoint:', endpoint, 'latency:', Date.now() - pingDate, 'reqs:', that._stats[endpoint]); } that._pingTimeouts[endpoint] = 0; that._stats[endpoint] = 0; clearTimeout(that._pingTimeoutTimers[endpoint]); }); ws.once('close', function (code) { that._onClose(endpoint, code, currentInterval); }); ws.on('error', function (err) { Logger.counter(['dl.ForwardingConfigurationManager.endpoints', endpointStr, 'errors']); console.error('ForwardingConfigurationManager/_connect: error in connection to', endpoint, err); if (ws) { ws = null; that._scheduleReconnect(endpoint, currentInterval); } }); ws.on('message', function (data) { try { data = JSON.parse(data); } catch (ex) { console.error('ForwardingConfigurationManager/_connect: failed to parse data from', endpoint); return; } that._onMessage(endpoint, data); }); }; ForwardingConfigurationManager.DEFAULT_PING_INTERVAL = 5000; ForwardingConfigurationManager.DEFAULT_PING_TIMEOUT = 2500; ForwardingConfigurationManager.MAX_PING_TIMEOUTS = 2; ForwardingConfigurationManager.RECONNECT_INTERVAL = 500; ForwardingConfigurationManager.RECONNECT_DECAY = 1.5; ForwardingConfigurationManager.RECONNECT_MAX_INTERVAL = 5000; ForwardingConfigurationManager.MAX_CONCURRENT_QUERIES = 20; ForwardingConfigurationManager.QUEUE_STATS_INTERVAL = 5000; ForwardingConfigurationManager.Event = ConfigurationManager.Event; exports.ForwardingConfigurationManager = ForwardingConfigurationManager;