UNPKG

elasticsearch

Version:

The official low-level Elasticsearch client for Node.js and the browser.

374 lines (332 loc) 9.73 kB
/** * Manager of connections to a node(s), capable of ensuring that connections are clear and living * before providing them to the application * * @class ConnectionPool * @constructor * @param {Object} config - The config object passed to the transport. */ module.exports = ConnectionPool; var _ = require('lodash'); var utils = require('./utils'); var Log = require('./log'); function ConnectionPool(config) { config = config || {}; utils.makeBoundMethods(this); if (!config.log) { this.log = new Log(); config.log = this.log; } else { this.log = config.log; } // we will need this when we create connections down the road this._config = config; // get the selector config var this.selector = utils.funcEnum( config, 'selector', ConnectionPool.selectors, ConnectionPool.defaultSelector ); // get the connection class this.Connection = utils.funcEnum( config, 'connectionClass', ConnectionPool.connectionClasses, ConnectionPool.defaultConnectionClass ); // time that connections will wait before being revived this.deadTimeout = config.hasOwnProperty('deadTimeout') ? config.deadTimeout : 60000; this.maxDeadTimeout = config.hasOwnProperty('maxDeadTimeout') ? config.maxDeadTimeout : 18e5; this.calcDeadTimeout = utils.funcEnum( config, 'calcDeadTimeout', ConnectionPool.calcDeadTimeoutOptions, 'exponential' ); // a map of connections to their "id" property, used when sniffing this.index = {}; this._conns = { alive: [], dead: [], }; // information about timeouts for dead connections this._timeouts = []; } // selector options ConnectionPool.selectors = require('./selectors'); ConnectionPool.defaultSelector = 'roundRobin'; // get the connection options ConnectionPool.connectionClasses = require('./connectors'); ConnectionPool.defaultConnectionClass = ConnectionPool.connectionClasses._default; delete ConnectionPool.connectionClasses._default; // the function that calculates timeouts based on attempts ConnectionPool.calcDeadTimeoutOptions = { flat: function(attempt, baseTimeout) { return baseTimeout; }, exponential: function(attempt, baseTimeout) { return Math.min( baseTimeout * 2 * Math.pow(2, attempt * 0.5 - 1), this.maxDeadTimeout ); }, }; /** * Selects a connection from the list using the this.selector * Features: * - detects if the selector is async or not * - sync selectors should still return asynchronously * - catches errors in sync selectors * - automatically selects the first dead connection when there no living connections * * @param {Function} cb [description] * @return {[type]} [description] */ ConnectionPool.prototype.select = function(cb) { if (this._conns.alive.length) { if (this.selector.length > 1) { this.selector(this._conns.alive, cb); } else { try { utils.nextTick(cb, void 0, this.selector(this._conns.alive)); } catch (e) { cb(e); } } } else if (this._timeouts.length) { this._selectDeadConnection(cb); } else { utils.nextTick(cb, void 0); } }; /** * Handler for the "set status" event emitted but the connections. It will move * the connection to it's proper connection list (unless it was closed). * * @param {String} status - the connection's new status * @param {String} oldStatus - the connection's old status * @param {ConnectionAbstract} connection - the connection object itself */ ConnectionPool.prototype.onStatusSet = utils.handler(function( status, oldStatus, connection ) { var index; var died = status === 'dead'; var wasAlreadyDead = died && oldStatus === 'dead'; var revived = !died && oldStatus === 'dead'; var noChange = oldStatus === status; var from = this._conns[oldStatus]; var to = this._conns[status]; if (noChange && !died) { return true; } if (from !== to) { if (_.isArray(from)) { index = from.indexOf(connection); if (index !== -1) { from.splice(index, 1); } } if (_.isArray(to)) { index = to.indexOf(connection); if (index === -1) { to.push(connection); } } } if (died) { this._onConnectionDied(connection, wasAlreadyDead); } if (revived) { this._onConnectionRevived(connection); } }); /** * Handler used to clear the times created when a connection dies * @param {ConnectionAbstract} connection */ ConnectionPool.prototype._onConnectionRevived = function(connection) { var timeout; for (var i = 0; i < this._timeouts.length; i++) { if (this._timeouts[i].conn === connection) { timeout = this._timeouts[i]; if (timeout.id) { clearTimeout(timeout.id); } this._timeouts.splice(i, 1); break; } } }; /** * Handler used to update or create a timeout for the connection which has died * @param {ConnectionAbstract} connection * @param {Boolean} alreadyWasDead - If the connection was preivously dead this must be set to true */ ConnectionPool.prototype._onConnectionDied = function( connection, alreadyWasDead ) { var timeout; if (alreadyWasDead) { for (var i = 0; i < this._timeouts.length; i++) { if (this._timeouts[i].conn === connection) { timeout = this._timeouts[i]; break; } } } else { timeout = { conn: connection, attempt: 0, revive: function(cb) { timeout.attempt++; connection.ping(function(err) { connection.setStatus(err ? 'dead' : 'alive'); if (cb && typeof cb === 'function') { cb(err); } }); }, }; this._timeouts.push(timeout); } if (timeout.id) { clearTimeout(timeout.id); } var ms = this.calcDeadTimeout(timeout.attempt, this.deadTimeout); timeout.id = setTimeout(timeout.revive, ms); timeout.runAt = utils.now() + ms; }; ConnectionPool.prototype._selectDeadConnection = function(cb) { var orderedTimeouts = _.sortBy(this._timeouts, 'runAt'); var log = this.log; process.nextTick(function next() { var timeout = orderedTimeouts.shift(); if (!timeout) { cb(void 0); return; } if (!timeout.conn) { next(); return; } if (timeout.conn.status === 'dead') { timeout.revive(function(err) { if (err) { log.warning('Unable to revive connection: ' + timeout.conn.id); process.nextTick(next); } else { cb(void 0, timeout.conn); } }); } else { cb(void 0, timeout.conn); } }); }; /** * Returns a random list of nodes from the living connections up to the limit. * If there are no living connections it will fall back to the dead connections. * If there are no dead connections it will return nothing. * * This is used for testing (when we just want the one existing node) * and sniffing, where using the selector to get all of the living connections * is not reasonable. * * @param {string} [status] - optional status of the connection to fetch * @param {Number} [limit] - optional limit on the number of connections to return */ ConnectionPool.prototype.getConnections = function(status, limit) { var list; if (status) { list = this._conns[status]; } else { list = this._conns[this._conns.alive.length ? 'alive' : 'dead']; } if (limit == null) { return list.slice(0); } else { return _.shuffle(list).slice(0, limit); } }; /** * Add a single connection to the pool and change it's status to "alive". * The connection should inherit from ConnectionAbstract * * @param {ConnectionAbstract} connection - The connection to add */ ConnectionPool.prototype.addConnection = function(connection) { if (!connection.id) { connection.id = connection.host.toString(); } if (!this.index[connection.id]) { this.log.info('Adding connection to', connection.id); this.index[connection.id] = connection; connection.on('status set', this.bound.onStatusSet); connection.setStatus('alive'); } }; /** * Remove a connection from the pool, and set it's status to "closed". * * @param {ConnectionAbstract} connection - The connection to remove/close */ ConnectionPool.prototype.removeConnection = function(connection) { if (!connection.id) { connection.id = connection.host.toString(); } if (this.index[connection.id]) { delete this.index[connection.id]; connection.setStatus('closed'); connection.removeListener('status set', this.bound.onStatusSet); } }; /** * Override the internal node list. All connections that are not in the new host * list are closed and removed. Non-unique hosts are ignored. * * @param {Host[]} hosts - An array of Host instances. */ ConnectionPool.prototype.setHosts = function(hosts) { var connection; var i; var id; var host; var toRemove = _.clone(this.index); for (i = 0; i < hosts.length; i++) { host = hosts[i]; id = host.toString(); if (this.index[id]) { delete toRemove[id]; } else { connection = new this.Connection(host, this._config); connection.id = id; this.addConnection(connection); } } var removeIds = _.keys(toRemove); for (i = 0; i < removeIds.length; i++) { this.removeConnection(this.index[removeIds[i]]); } }; ConnectionPool.prototype.getAllHosts = function() { return _.values(this.index).map(function(connection) { return connection.host; }); }; /** * Close the conncetion pool, as well as all of it's connections */ ConnectionPool.prototype.close = function() { this.setHosts([]); }; ConnectionPool.prototype.empty = ConnectionPool.prototype.close;