elasticsearch
Version:
The official low-level Elasticsearch client for Node.js and the browser.
374 lines (332 loc) • 9.73 kB
JavaScript
/**
* 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;