WIP: initial prototype
- Standardized event emitters - Refactored transport.request to have a better handling of the state - Added sniff event - Improved abort handling
This commit is contained in:
1
index.d.ts
vendored
1
index.d.ts
vendored
@ -468,6 +468,7 @@ declare const events: {
|
|||||||
RESPONSE: string;
|
RESPONSE: string;
|
||||||
REQUEST: string;
|
REQUEST: string;
|
||||||
ERROR: string;
|
ERROR: string;
|
||||||
|
SNIFF: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Client, Transport, ConnectionPool, Connection, Serializer, events, ApiResponse };
|
export { Client, Transport, ConnectionPool, Connection, Serializer, events, ApiResponse };
|
||||||
|
|||||||
9
index.js
9
index.js
@ -5,7 +5,8 @@ const Transport = require('./lib/Transport')
|
|||||||
const Connection = require('./lib/Connection')
|
const Connection = require('./lib/Connection')
|
||||||
const ConnectionPool = require('./lib/ConnectionPool')
|
const ConnectionPool = require('./lib/ConnectionPool')
|
||||||
const Serializer = require('./lib/Serializer')
|
const Serializer = require('./lib/Serializer')
|
||||||
const { ConfigurationError } = require('./lib/errors')
|
const errors = require('./lib/errors')
|
||||||
|
const { ConfigurationError } = errors
|
||||||
|
|
||||||
const buildApi = require('./api')
|
const buildApi = require('./api')
|
||||||
|
|
||||||
@ -94,7 +95,8 @@ class Client extends EventEmitter {
|
|||||||
const events = {
|
const events = {
|
||||||
RESPONSE: 'response',
|
RESPONSE: 'response',
|
||||||
REQUEST: 'request',
|
REQUEST: 'request',
|
||||||
ERROR: 'error'
|
ERROR: 'error',
|
||||||
|
SNIFF: 'sniff'
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -103,5 +105,6 @@ module.exports = {
|
|||||||
ConnectionPool,
|
ConnectionPool,
|
||||||
Connection,
|
Connection,
|
||||||
Serializer,
|
Serializer,
|
||||||
events
|
events,
|
||||||
|
errors
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,6 +77,15 @@ class Connection {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// updates the ended state
|
||||||
|
request.on('abort', () => {
|
||||||
|
debug('Request aborted', params)
|
||||||
|
if (ended === false) {
|
||||||
|
ended = true
|
||||||
|
this._openRequests--
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Disables the Nagle algorithm
|
// Disables the Nagle algorithm
|
||||||
request.setNoDelay(true)
|
request.setNoDelay(true)
|
||||||
|
|
||||||
|
|||||||
@ -274,6 +274,7 @@ class ConnectionPool {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the nodes objects to a host object.
|
* Transforms the nodes objects to a host object.
|
||||||
|
* TODO: handle ssl and agent options
|
||||||
*
|
*
|
||||||
* @param {object} nodes
|
* @param {object} nodes
|
||||||
* @returns {array} hosts
|
* @returns {array} hosts
|
||||||
|
|||||||
@ -44,6 +44,7 @@ class Serializer {
|
|||||||
qserialize (object) {
|
qserialize (object) {
|
||||||
debug('qserialize', object)
|
debug('qserialize', object)
|
||||||
if (object == null) return ''
|
if (object == null) return ''
|
||||||
|
if (typeof object === 'string') return object
|
||||||
// arrays should be serialized as comma separated list
|
// arrays should be serialized as comma separated list
|
||||||
const keys = Object.keys(object)
|
const keys = Object.keys(object)
|
||||||
for (var i = 0, len = keys.length; i < len; i++) {
|
for (var i = 0, len = keys.length; i < len; i++) {
|
||||||
|
|||||||
158
lib/Transport.js
158
lib/Transport.js
@ -11,7 +11,6 @@ const {
|
|||||||
} = require('./errors')
|
} = require('./errors')
|
||||||
|
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
const kRemainingAttempts = Symbol('elasticsearch-remaining-attempts')
|
|
||||||
|
|
||||||
class Transport {
|
class Transport {
|
||||||
constructor (opts = {}) {
|
constructor (opts = {}) {
|
||||||
@ -36,78 +35,99 @@ class Transport {
|
|||||||
|
|
||||||
request (params, callback) {
|
request (params, callback) {
|
||||||
callback = once(callback)
|
callback = once(callback)
|
||||||
const result = { body: null, statusCode: null, headers: null, warnings: null }
|
const meta = {
|
||||||
const attempts = params[kRemainingAttempts] || params.maxRetries || this.maxRetries
|
connection: null,
|
||||||
const connection = this.getConnection()
|
request: null,
|
||||||
if (connection === null) {
|
response: null,
|
||||||
return callback(new NoLivingConnectionsError('There are not living connections'), result)
|
attempts: 0,
|
||||||
|
aborted: false
|
||||||
}
|
}
|
||||||
|
const result = {
|
||||||
|
body: null,
|
||||||
|
statusCode: null,
|
||||||
|
headers: null,
|
||||||
|
warnings: null
|
||||||
|
}
|
||||||
|
const maxRetries = params.maxRetries || this.maxRetries
|
||||||
|
var request = { abort: noop }
|
||||||
|
|
||||||
params.headers = params.headers || {}
|
const makeRequest = () => {
|
||||||
// handle json body
|
if (meta.aborted === true) return
|
||||||
if (params.body != null) {
|
meta.connection = this.getConnection()
|
||||||
if (shouldSerialize(params.body) === true) {
|
if (meta.connection === null) {
|
||||||
try {
|
return callback(new NoLivingConnectionsError('There are not living connections'), result)
|
||||||
params.body = this.serializer.serialize(params.body)
|
}
|
||||||
} catch (err) {
|
|
||||||
return callback(err, result)
|
params.headers = params.headers || {}
|
||||||
|
// handle json body
|
||||||
|
if (params.body != null) {
|
||||||
|
if (shouldSerialize(params.body) === true) {
|
||||||
|
try {
|
||||||
|
params.body = this.serializer.serialize(params.body)
|
||||||
|
} catch (err) {
|
||||||
|
return callback(err, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.headers['Content-Type'] = 'application/json'
|
||||||
|
if (isStream(params.body) === false) {
|
||||||
|
params.headers['Content-Length'] = '' + Buffer.byteLength(params.body)
|
||||||
|
}
|
||||||
|
// handle ndjson body
|
||||||
|
} else if (params.bulkBody != null) {
|
||||||
|
if (shouldSerialize(params.bulkBody) === true) {
|
||||||
|
try {
|
||||||
|
params.body = this.serializer.ndserialize(params.bulkBody)
|
||||||
|
} catch (err) {
|
||||||
|
return callback(err, result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.body = params.bulkBody
|
||||||
|
}
|
||||||
|
params.headers['Content-Type'] = 'application/x-ndjson'
|
||||||
|
if (isStream(params.body) === false) {
|
||||||
|
params.headers['Content-Length'] = '' + Buffer.byteLength(params.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
params.headers['Content-Type'] = 'application/json'
|
|
||||||
if (isStream(params.body) === false) {
|
if (this.suggestCompression === true) {
|
||||||
params.headers['Content-Length'] = '' + Buffer.byteLength(params.body)
|
params.headers['Accept-Encoding'] = 'gzip,deflate'
|
||||||
}
|
|
||||||
// handle ndjson body
|
|
||||||
} else if (params.bulkBody != null) {
|
|
||||||
if (shouldSerialize(params.bulkBody) === true) {
|
|
||||||
try {
|
|
||||||
params.body = this.serializer.ndserialize(params.bulkBody)
|
|
||||||
} catch (err) {
|
|
||||||
return callback(err, result)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
params.body = params.bulkBody
|
|
||||||
}
|
|
||||||
params.headers['Content-Type'] = 'application/x-ndjson'
|
|
||||||
if (isStream(params.body) === false) {
|
|
||||||
params.headers['Content-Length'] = '' + Buffer.byteLength(params.body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serializes the querystring
|
||||||
|
params.querystring = this.serializer.qserialize(params.querystring)
|
||||||
|
// handles request timeout
|
||||||
|
params.timeout = toMs(params.requestTimeout || this.requestTimeout)
|
||||||
|
|
||||||
|
meta.request = params
|
||||||
|
this.emit('request', meta)
|
||||||
|
|
||||||
|
// perform the actual http request
|
||||||
|
return meta.connection.request(params, onResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.suggestCompression === true) {
|
const onResponse = (err, response) => {
|
||||||
params.headers['Accept-Encoding'] = 'gzip,deflate'
|
if (err !== null) {
|
||||||
}
|
|
||||||
|
|
||||||
// serializes the querystring
|
|
||||||
params.querystring = this.serializer.qserialize(params.querystring)
|
|
||||||
// handles request timeout
|
|
||||||
params.timeout = toMs(params.requestTimeout || this.requestTimeout)
|
|
||||||
|
|
||||||
this.emit('request', connection, params)
|
|
||||||
|
|
||||||
// perform the actual http request
|
|
||||||
const request = connection.request(params, (err, response) => {
|
|
||||||
if (err != null) {
|
|
||||||
// if there is an error in the connection
|
// if there is an error in the connection
|
||||||
// let's mark the connection as dead
|
// let's mark the connection as dead
|
||||||
this.connectionPool.markDead(connection)
|
this.connectionPool.markDead(meta.connection)
|
||||||
|
|
||||||
if (this.sniffOnConnectionFault === true) {
|
if (this.sniffOnConnectionFault === true) {
|
||||||
this.sniff()
|
this.sniff()
|
||||||
}
|
}
|
||||||
|
|
||||||
// retry logic
|
// retry logic
|
||||||
if (attempts > 0) {
|
if (meta.attempts < maxRetries) {
|
||||||
debug(`Retrying request, there are still ${attempts} attempts`, params)
|
meta.attempts++
|
||||||
params[kRemainingAttempts] = attempts - 1
|
debug(`Retrying request, there are still ${maxRetries - meta.attempts} attempts`, params)
|
||||||
return this.request(params, callback)
|
request = makeRequest(params, callback)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = err instanceof TimeoutError
|
const error = err instanceof TimeoutError
|
||||||
? err
|
? err
|
||||||
: new ConnectionError(err.message, params)
|
: new ConnectionError(err.message, params)
|
||||||
|
|
||||||
this.emit('error', error, connection, params)
|
this.emit('error', error, meta)
|
||||||
return callback(error, result)
|
return callback(error, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +141,8 @@ class Transport {
|
|||||||
|
|
||||||
if (params.asStream === true) {
|
if (params.asStream === true) {
|
||||||
result.body = response
|
result.body = response
|
||||||
this.emit('response', connection, params, result)
|
meta.response = result
|
||||||
|
this.emit('response', meta)
|
||||||
callback(null, result)
|
callback(null, result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -145,7 +166,7 @@ class Transport {
|
|||||||
try {
|
try {
|
||||||
result.body = this.serializer.deserialize(payload)
|
result.body = this.serializer.deserialize(payload)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.emit('error', err, connection, params)
|
this.emit('error', err, meta)
|
||||||
return callback(err, result)
|
return callback(err, result)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -162,19 +183,22 @@ class Transport {
|
|||||||
(statusCode === 502 || statusCode === 503 || statusCode === 504)) {
|
(statusCode === 502 || statusCode === 503 || statusCode === 504)) {
|
||||||
// if the statusCode is 502/3/4 we should run our retry strategy
|
// if the statusCode is 502/3/4 we should run our retry strategy
|
||||||
// and mark the connection as dead
|
// and mark the connection as dead
|
||||||
this.connectionPool.markDead(connection)
|
this.connectionPool.markDead(meta.connection)
|
||||||
if (attempts > 0) {
|
// retry logic
|
||||||
debug(`Retrying request, there are still ${attempts} attempts`, params)
|
if (meta.attempts < maxRetries) {
|
||||||
params[kRemainingAttempts] = attempts - 1
|
meta.attempts++
|
||||||
return this.request(params, callback)
|
debug(`Retrying request, there are still ${maxRetries - meta.attempts} attempts`, params)
|
||||||
|
request = makeRequest(params, callback)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// everything has worked as expected, let's mark
|
// everything has worked as expected, let's mark
|
||||||
// the connection as alive (or confirm it)
|
// the connection as alive (or confirm it)
|
||||||
this.connectionPool.markAlive(connection)
|
this.connectionPool.markAlive(meta.connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('response', connection, params, result)
|
meta.response = result
|
||||||
|
this.emit('response', meta)
|
||||||
if (ignoreStatusCode === false && statusCode >= 400) {
|
if (ignoreStatusCode === false && statusCode >= 400) {
|
||||||
callback(new ResponseError(result), result)
|
callback(new ResponseError(result), result)
|
||||||
} else {
|
} else {
|
||||||
@ -185,12 +209,15 @@ class Transport {
|
|||||||
callback(null, result)
|
callback(null, result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
request = makeRequest()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abort: () => {
|
abort: () => {
|
||||||
|
meta.aborted = true
|
||||||
request.abort()
|
request.abort()
|
||||||
debug('Request aborted', params)
|
debug('Aborting request', params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +231,8 @@ class Transport {
|
|||||||
return this.connectionPool.getConnection()
|
return this.connectionPool.getConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: add sniff reason
|
||||||
|
// 'connection-fault', 'interval', 'start', ...
|
||||||
sniff (callback = noop) {
|
sniff (callback = noop) {
|
||||||
if (this._isSniffing === true) return
|
if (this._isSniffing === true) return
|
||||||
this._isSniffing = true
|
this._isSniffing = true
|
||||||
@ -221,8 +250,8 @@ class Transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
this.emit('error', err, null, request)
|
|
||||||
debug('Sniffing errored', err)
|
debug('Sniffing errored', err)
|
||||||
|
this.emit('sniff', err, null)
|
||||||
return callback(err)
|
return callback(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +259,7 @@ class Transport {
|
|||||||
const hosts = this.connectionPool.nodesToHost(result.body.nodes)
|
const hosts = this.connectionPool.nodesToHost(result.body.nodes)
|
||||||
this.connectionPool.update(hosts)
|
this.connectionPool.update(hosts)
|
||||||
|
|
||||||
|
this.emit('sniff', null, hosts)
|
||||||
callback(null, hosts)
|
callback(null, hosts)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user