elasticsearch
Version:
The official low-level Elasticsearch client for Node.js and the browser.
523 lines (447 loc) • 13.6 kB
JavaScript
/**
* Class that manages making request, called by all of the API methods.
* @type {[type]}
*/
module.exports = Transport;
var _ = require('lodash');
var utils = require('./utils');
var errors = require('./errors');
var Host = require('./host');
var patchSniffOnConnectionFault = require('./transport/sniff_on_connection_fault');
var findCommonProtocol = require('./transport/find_common_protocol');
function Transport(config) {
var self = this;
config = self._config = config || {};
var LogClass =
typeof config.log === 'function' ? config.log : require('./log');
config.log = self.log = new LogClass(config);
// setup the connection pool
var ConnectionPool = utils.funcEnum(
config,
'connectionPool',
Transport.connectionPools,
'main'
);
self.connectionPool = new ConnectionPool(config);
// setup the serializer
var Serializer = utils.funcEnum(
config,
'serializer',
Transport.serializers,
'json'
);
self.serializer = new Serializer(config);
// setup the nodesToHostCallback
self.nodesToHostCallback = utils.funcEnum(
config,
'nodesToHostCallback',
Transport.nodesToHostCallbacks,
'main'
);
// setup max retries
self.maxRetries = config.hasOwnProperty('maxRetries') ? config.maxRetries : 3;
// setup endpoint to use for sniffing
self.sniffEndpoint = config.hasOwnProperty('sniffEndpoint')
? config.sniffEndpoint
: '/_nodes/_all/http';
// setup requestTimeout default
self.requestTimeout = config.hasOwnProperty('requestTimeout')
? config.requestTimeout
: 30000;
self.pingTimeout = config.hasOwnProperty('pingTimeout')
? config.pingTimeout
: 3000;
if (config.hasOwnProperty('defer')) {
self.defer = config.defer;
}
// randomizeHosts option
var randomizeHosts = config.hasOwnProperty('randomizeHosts')
? !!config.randomizeHosts
: true;
if (config.host) {
config.hosts = config.host;
}
if (config.hosts) {
var hostsConfig = utils.createArray(config.hosts, function(val) {
if (_.isPlainObject(val) || _.isString(val) || val instanceof Host) {
return val;
}
});
if (!hostsConfig) {
throw new TypeError(
'Invalid hosts config. Expected a URL, an array of urls, a host config object, ' +
'or an array of host config objects.'
);
}
if (randomizeHosts) {
hostsConfig = _.shuffle(hostsConfig);
}
self.setHosts(hostsConfig);
}
if (config.hasOwnProperty('sniffedNodesProtocol')) {
self.sniffedNodesProtocol = config.sniffedNodesProtocol || null;
} else {
self.sniffedNodesProtocol =
findCommonProtocol(self.connectionPool.getAllHosts()) || null;
}
if (config.hasOwnProperty('sniffedNodesFilterPath')) {
self.sniffedNodesFilterPath = config.sniffedNodesFilterPath;
} else {
self.sniffedNodesFilterPath = [
'nodes.*.http.publish_address',
'nodes.*.name',
'nodes.*.hostname',
'nodes.*.host',
'nodes.*.version',
].join(',');
}
if (config.sniffOnStart) {
self.sniff();
}
if (config.sniffInterval) {
self._timeout(function doSniff() {
self.sniff();
self._timeout(doSniff, config.sniffInterval);
}, config.sniffInterval);
}
if (config.sniffOnConnectionFault) {
patchSniffOnConnectionFault(self);
}
}
Transport.connectionPools = {
main: require('./connection_pool'),
};
Transport.serializers = require('./serializers');
Transport.nodesToHostCallbacks = {
main: require('./nodes_to_host'),
};
Transport.prototype.defer = function() {
if (typeof Promise === 'undefined') {
throw new Error(
'No Promise implementation found. In order for elasticsearch-js to create promises ' +
'either specify the `defer` configuration or include a global Promise shim'
);
}
var defer = {};
defer.promise = new Promise(function(resolve, reject) {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
};
/**
* Perform a request with the client's transport
*
* @method request
* @todo async body writing
* @todo abort
* @todo access to custom headers, modifying of request in general
* @param {object} params
* @param {Number} params.requestTimeout - timeout for the entire request (inculding all retries)
* @param {Number} params.maxRetries - number of times to re-run request if the
* original node chosen can not be connected to.
* @param {string} [params.path="/"] - URL pathname. Do not include query string.
* @param {string|object} [params.query] - Query string.
* @param {String} params.method - The HTTP method for the request
* @param {String} params.body - The body of the HTTP request
* @param {Function} cb - A function to call back with (error, responseBody, responseStatus)
*/
Transport.prototype.request = function(params, cb) {
var self = this;
var remainingRetries = this.maxRetries;
var requestTimeout = this.requestTimeout;
var connection; // set in sendReqWithConnection
var aborted = false; // several connector will respond with an error when the request is aborted
var requestAborter; // an abort function, returned by connection#request()
var requestTimeoutId; // the id of the ^timeout
var ret; // the object returned to the user, might be a promise
var defer; // the defer object, will be set when we are using promises.
var body = params.body;
var headers = !params.headers
? {}
: _.transform(params.headers, function(headers, val, name) {
headers[String(name).toLowerCase()] = val;
});
self.log.debug('starting request', params);
// determine the response based on the presense of a callback
if (typeof cb === 'function') {
// handle callbacks within a domain
if (process.domain) {
cb = process.domain.bind(cb);
}
ret = {
abort: abortRequest,
};
} else {
defer = this.defer();
ret = defer.promise;
ret.abort = abortRequest;
}
if (body && params.method === 'GET') {
utils.nextTick(
respond,
new TypeError('Body can not be sent with method "GET"')
);
return ret;
}
// serialize the body
if (body) {
var serializer = self.serializer;
var serializeFn = serializer[params.bulkBody ? 'bulkBody' : 'serialize'];
body = serializeFn.call(serializer, body);
if (!headers['content-type']) {
headers['content-type'] = serializeFn.contentType;
}
}
if (params.hasOwnProperty('maxRetries')) {
remainingRetries = params.maxRetries;
}
if (params.hasOwnProperty('requestTimeout')) {
requestTimeout = params.requestTimeout;
}
const pingRequest = params.path === '/' && params.method === 'HEAD';
if (pingRequest) {
const requestParam =
params.hasOwnProperty('requestTimeout') && params.requestTimeout;
requestTimeout = requestParam || this.pingTimeout;
}
params.req = {
method: params.method,
path: params.path || '/',
query: params.query,
body: body,
headers: headers,
};
function sendReqWithConnection(err, _connection) {
if (aborted) {
return;
}
if (err) {
respond(err);
} else if (_connection) {
connection = _connection;
requestAborter = connection.request(params.req, checkRespForFailure);
} else {
self.log.warning('No living connections');
respond(new errors.NoConnections());
}
}
function checkRespForFailure(err, body, status, headers) {
if (aborted) {
return;
}
requestAborter = void 0;
if (err instanceof errors.RequestTypeError) {
self.log.error('Connection refused to execute the request', err);
respond(err, body, status, headers);
return;
}
if (err) {
connection.setStatus('dead');
var errMsg = err.message || '';
errMsg =
'\n' +
params.req.method +
' ' +
connection.host.makeUrl(params.req) +
(errMsg.length ? ' => ' : '') +
errMsg;
if (remainingRetries) {
remainingRetries--;
self.log.error('Request error, retrying' + errMsg);
self.connectionPool.select(sendReqWithConnection);
} else {
self.log.error('Request complete with error' + errMsg);
respond(new errors.ConnectionFault(err));
}
} else {
self.log.debug('Request complete');
respond(void 0, body, status, headers);
}
}
function respond(err, body, status, headers) {
if (aborted) {
return;
}
self._timeout(requestTimeoutId);
var parsedBody;
var isJson =
!headers ||
(headers['content-type'] &&
~headers['content-type'].indexOf('application/json'));
if (!err && body) {
if (isJson) {
parsedBody = self.serializer.deserialize(body);
if (parsedBody == null) {
err = new errors.Serialization();
parsedBody = body;
}
} else {
parsedBody = body;
}
}
// does the response represent an error?
if (
(!err || err instanceof errors.Serialization) &&
(status < 200 || status >= 300) &&
(!params.ignore || !_.includes(params.ignore, status))
) {
var errorMetadata = _.pick(params.req, ['path', 'query', 'body']);
errorMetadata.statusCode = status;
errorMetadata.response = body;
if (status === 401 && headers && headers['www-authenticate']) {
errorMetadata.wwwAuthenticateDirective = headers['www-authenticate'];
}
if (errors[status]) {
err = new errors[status](parsedBody && parsedBody.error, errorMetadata);
} else {
err = new errors.Generic('unknown error', errorMetadata);
}
}
// can we cast notfound to false?
if (params.castExists) {
if (err && err instanceof errors.NotFound) {
parsedBody = false;
err = void 0;
} else {
parsedBody = !err;
}
}
// how do we send the response?
if (typeof cb === 'function') {
if (err) {
cb(err, parsedBody, status);
} else {
cb(void 0, parsedBody, status);
}
} else if (err) {
err.body = parsedBody;
err.status = status;
defer.reject(err);
} else {
defer.resolve(parsedBody);
}
}
function abortRequest() {
if (aborted) {
return;
}
aborted = true;
remainingRetries = 0;
self._timeout(requestTimeoutId);
if (typeof requestAborter === 'function') {
requestAborter();
}
}
if (requestTimeout && requestTimeout !== Infinity) {
requestTimeoutId = this._timeout(function() {
respond(
new errors.RequestTimeout(
'Request Timeout after ' + requestTimeout + 'ms'
)
);
abortRequest();
}, requestTimeout);
}
if (connection) {
sendReqWithConnection(void 0, connection);
} else {
self.connectionPool.select(sendReqWithConnection);
}
return ret;
};
Transport.prototype._timeout = function(cb, delay) {
if (this.closed) return;
var id;
var timers = this._timers || (this._timers = []);
if (typeof cb !== 'function') {
id = cb;
cb = void 0;
}
if (cb) {
// set the timer
id = setTimeout(function() {
_.pull(timers, id);
cb();
}, delay);
timers.push(id);
return id;
}
if (id) {
clearTimeout(id);
var i = this._timers.indexOf(id);
if (i !== -1) {
this._timers.splice(i, 1);
}
}
};
/**
* Ask an ES node for a list of all the nodes, add/remove nodes from the connection
* pool as appropriate
*
* @param {Function} cb - Function to call back once complete
*/
Transport.prototype.sniff = function(cb) {
var self = this;
var nodesToHostCallback = this.nodesToHostCallback;
var log = this.log;
var sniffedNodesProtocol = this.sniffedNodesProtocol;
var sniffedNodesFilterPath = this.sniffedNodesFilterPath;
// make cb a function if it isn't
cb = typeof cb === 'function' ? cb : _.noop;
this.request(
{
path: this.sniffEndpoint,
query: { filter_path: sniffedNodesFilterPath },
method: 'GET',
},
function(err, resp, status) {
if (!err && resp && resp.nodes) {
var hostsConfigs;
try {
hostsConfigs = nodesToHostCallback(resp.nodes);
} catch (e) {
log.error(
new Error(
'Unable to convert node list from ' +
self.sniffEndpoint +
' to hosts durring sniff. Encountered error:\n' +
(e.stack || e.message)
)
);
return;
}
_.forEach(hostsConfigs, function(hostConfig) {
if (sniffedNodesProtocol) hostConfig.protocol = sniffedNodesProtocol;
});
self.setHosts(hostsConfigs);
}
cb(err, resp, status);
}
);
};
/**
* Set the host list that the transport should use.
*
* @param {Array<HostConfig>} hostsConfigs - an array of Hosts, or configuration objects
* that will be used to create Host objects.
*/
Transport.prototype.setHosts = function(hostsConfigs) {
var globalConfig = this._config;
this.connectionPool.setHosts(
_.map(hostsConfigs, function(conf) {
return conf instanceof Host ? conf : new Host(conf, globalConfig);
})
);
};
/**
* Close the Transport, which closes the logs and connection pool
* @return {[type]} [description]
*/
Transport.prototype.close = function() {
this.log.close();
this.closed = true;
_.each(this._timers, clearTimeout);
this._timers = null;
this.connectionPool.close();
};