summaryrefslogtreecommitdiff
path: root/node_modules/agentkeepalive/lib/agent.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/agentkeepalive/lib/agent.js')
-rw-r--r--node_modules/agentkeepalive/lib/agent.js398
1 files changed, 398 insertions, 0 deletions
diff --git a/node_modules/agentkeepalive/lib/agent.js b/node_modules/agentkeepalive/lib/agent.js
new file mode 100644
index 0000000..a7065b5
--- /dev/null
+++ b/node_modules/agentkeepalive/lib/agent.js
@@ -0,0 +1,398 @@
+'use strict';
+
+const OriginalAgent = require('http').Agent;
+const ms = require('humanize-ms');
+const debug = require('debug')('agentkeepalive');
+const deprecate = require('depd')('agentkeepalive');
+const {
+ INIT_SOCKET,
+ CURRENT_ID,
+ CREATE_ID,
+ SOCKET_CREATED_TIME,
+ SOCKET_NAME,
+ SOCKET_REQUEST_COUNT,
+ SOCKET_REQUEST_FINISHED_COUNT,
+} = require('./constants');
+
+// OriginalAgent come from
+// - https://github.com/nodejs/node/blob/v8.12.0/lib/_http_agent.js
+// - https://github.com/nodejs/node/blob/v10.12.0/lib/_http_agent.js
+
+// node <= 10
+let defaultTimeoutListenerCount = 1;
+const majorVersion = parseInt(process.version.split('.', 1)[0].substring(1));
+if (majorVersion >= 11 && majorVersion <= 12) {
+ defaultTimeoutListenerCount = 2;
+} else if (majorVersion >= 13) {
+ defaultTimeoutListenerCount = 3;
+}
+
+class Agent extends OriginalAgent {
+ constructor(options) {
+ options = options || {};
+ options.keepAlive = options.keepAlive !== false;
+ // default is keep-alive and 4s free socket timeout
+ // see https://medium.com/ssense-tech/reduce-networking-errors-in-nodejs-23b4eb9f2d83
+ if (options.freeSocketTimeout === undefined) {
+ options.freeSocketTimeout = 4000;
+ }
+ // Legacy API: keepAliveTimeout should be rename to `freeSocketTimeout`
+ if (options.keepAliveTimeout) {
+ deprecate('options.keepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
+ options.freeSocketTimeout = options.keepAliveTimeout;
+ delete options.keepAliveTimeout;
+ }
+ // Legacy API: freeSocketKeepAliveTimeout should be rename to `freeSocketTimeout`
+ if (options.freeSocketKeepAliveTimeout) {
+ deprecate('options.freeSocketKeepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
+ options.freeSocketTimeout = options.freeSocketKeepAliveTimeout;
+ delete options.freeSocketKeepAliveTimeout;
+ }
+
+ // Sets the socket to timeout after timeout milliseconds of inactivity on the socket.
+ // By default is double free socket timeout.
+ if (options.timeout === undefined) {
+ // make sure socket default inactivity timeout >= 8s
+ options.timeout = Math.max(options.freeSocketTimeout * 2, 8000);
+ }
+
+ // support humanize format
+ options.timeout = ms(options.timeout);
+ options.freeSocketTimeout = ms(options.freeSocketTimeout);
+ options.socketActiveTTL = options.socketActiveTTL ? ms(options.socketActiveTTL) : 0;
+
+ super(options);
+
+ this[CURRENT_ID] = 0;
+
+ // create socket success counter
+ this.createSocketCount = 0;
+ this.createSocketCountLastCheck = 0;
+
+ this.createSocketErrorCount = 0;
+ this.createSocketErrorCountLastCheck = 0;
+
+ this.closeSocketCount = 0;
+ this.closeSocketCountLastCheck = 0;
+
+ // socket error event count
+ this.errorSocketCount = 0;
+ this.errorSocketCountLastCheck = 0;
+
+ // request finished counter
+ this.requestCount = 0;
+ this.requestCountLastCheck = 0;
+
+ // including free socket timeout counter
+ this.timeoutSocketCount = 0;
+ this.timeoutSocketCountLastCheck = 0;
+
+ this.on('free', socket => {
+ // https://github.com/nodejs/node/pull/32000
+ // Node.js native agent will check socket timeout eqs agent.options.timeout.
+ // Use the ttl or freeSocketTimeout to overwrite.
+ const timeout = this.calcSocketTimeout(socket);
+ if (timeout > 0 && socket.timeout !== timeout) {
+ socket.setTimeout(timeout);
+ }
+ });
+ }
+
+ get freeSocketKeepAliveTimeout() {
+ deprecate('agent.freeSocketKeepAliveTimeout is deprecated, please use agent.options.freeSocketTimeout instead');
+ return this.options.freeSocketTimeout;
+ }
+
+ get timeout() {
+ deprecate('agent.timeout is deprecated, please use agent.options.timeout instead');
+ return this.options.timeout;
+ }
+
+ get socketActiveTTL() {
+ deprecate('agent.socketActiveTTL is deprecated, please use agent.options.socketActiveTTL instead');
+ return this.options.socketActiveTTL;
+ }
+
+ calcSocketTimeout(socket) {
+ /**
+ * return <= 0: should free socket
+ * return > 0: should update socket timeout
+ * return undefined: not find custom timeout
+ */
+ let freeSocketTimeout = this.options.freeSocketTimeout;
+ const socketActiveTTL = this.options.socketActiveTTL;
+ if (socketActiveTTL) {
+ // check socketActiveTTL
+ const aliveTime = Date.now() - socket[SOCKET_CREATED_TIME];
+ const diff = socketActiveTTL - aliveTime;
+ if (diff <= 0) {
+ return diff;
+ }
+ if (freeSocketTimeout && diff < freeSocketTimeout) {
+ freeSocketTimeout = diff;
+ }
+ }
+ // set freeSocketTimeout
+ if (freeSocketTimeout) {
+ // set free keepalive timer
+ // try to use socket custom freeSocketTimeout first, support headers['keep-alive']
+ // https://github.com/node-modules/urllib/blob/b76053020923f4d99a1c93cf2e16e0c5ba10bacf/lib/urllib.js#L498
+ const customFreeSocketTimeout = socket.freeSocketTimeout || socket.freeSocketKeepAliveTimeout;
+ return customFreeSocketTimeout || freeSocketTimeout;
+ }
+ }
+
+ keepSocketAlive(socket) {
+ const result = super.keepSocketAlive(socket);
+ // should not keepAlive, do nothing
+ if (!result) return result;
+
+ const customTimeout = this.calcSocketTimeout(socket);
+ if (typeof customTimeout === 'undefined') {
+ return true;
+ }
+ if (customTimeout <= 0) {
+ debug('%s(requests: %s, finished: %s) free but need to destroy by TTL, request count %s, diff is %s',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], customTimeout);
+ return false;
+ }
+ if (socket.timeout !== customTimeout) {
+ socket.setTimeout(customTimeout);
+ }
+ return true;
+ }
+
+ // only call on addRequest
+ reuseSocket(...args) {
+ // reuseSocket(socket, req)
+ super.reuseSocket(...args);
+ const socket = args[0];
+ const req = args[1];
+ req.reusedSocket = true;
+ const agentTimeout = this.options.timeout;
+ if (getSocketTimeout(socket) !== agentTimeout) {
+ // reset timeout before use
+ socket.setTimeout(agentTimeout);
+ debug('%s reset timeout to %sms', socket[SOCKET_NAME], agentTimeout);
+ }
+ socket[SOCKET_REQUEST_COUNT]++;
+ debug('%s(requests: %s, finished: %s) reuse on addRequest, timeout %sms',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
+ getSocketTimeout(socket));
+ }
+
+ [CREATE_ID]() {
+ const id = this[CURRENT_ID]++;
+ if (this[CURRENT_ID] === Number.MAX_SAFE_INTEGER) this[CURRENT_ID] = 0;
+ return id;
+ }
+
+ [INIT_SOCKET](socket, options) {
+ // bugfix here.
+ // https on node 8, 10 won't set agent.options.timeout by default
+ // TODO: need to fix on node itself
+ if (options.timeout) {
+ const timeout = getSocketTimeout(socket);
+ if (!timeout) {
+ socket.setTimeout(options.timeout);
+ }
+ }
+
+ if (this.options.keepAlive) {
+ // Disable Nagle's algorithm: http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
+ // https://fengmk2.com/benchmark/nagle-algorithm-delayed-ack-mock.html
+ socket.setNoDelay(true);
+ }
+ this.createSocketCount++;
+ if (this.options.socketActiveTTL) {
+ socket[SOCKET_CREATED_TIME] = Date.now();
+ }
+ // don't show the hole '-----BEGIN CERTIFICATE----' key string
+ socket[SOCKET_NAME] = `sock[${this[CREATE_ID]()}#${options._agentKey}]`.split('-----BEGIN', 1)[0];
+ socket[SOCKET_REQUEST_COUNT] = 1;
+ socket[SOCKET_REQUEST_FINISHED_COUNT] = 0;
+ installListeners(this, socket, options);
+ }
+
+ createConnection(options, oncreate) {
+ let called = false;
+ const onNewCreate = (err, socket) => {
+ if (called) return;
+ called = true;
+
+ if (err) {
+ this.createSocketErrorCount++;
+ return oncreate(err);
+ }
+ this[INIT_SOCKET](socket, options);
+ oncreate(err, socket);
+ };
+
+ const newSocket = super.createConnection(options, onNewCreate);
+ if (newSocket) onNewCreate(null, newSocket);
+ }
+
+ get statusChanged() {
+ const changed = this.createSocketCount !== this.createSocketCountLastCheck ||
+ this.createSocketErrorCount !== this.createSocketErrorCountLastCheck ||
+ this.closeSocketCount !== this.closeSocketCountLastCheck ||
+ this.errorSocketCount !== this.errorSocketCountLastCheck ||
+ this.timeoutSocketCount !== this.timeoutSocketCountLastCheck ||
+ this.requestCount !== this.requestCountLastCheck;
+ if (changed) {
+ this.createSocketCountLastCheck = this.createSocketCount;
+ this.createSocketErrorCountLastCheck = this.createSocketErrorCount;
+ this.closeSocketCountLastCheck = this.closeSocketCount;
+ this.errorSocketCountLastCheck = this.errorSocketCount;
+ this.timeoutSocketCountLastCheck = this.timeoutSocketCount;
+ this.requestCountLastCheck = this.requestCount;
+ }
+ return changed;
+ }
+
+ getCurrentStatus() {
+ return {
+ createSocketCount: this.createSocketCount,
+ createSocketErrorCount: this.createSocketErrorCount,
+ closeSocketCount: this.closeSocketCount,
+ errorSocketCount: this.errorSocketCount,
+ timeoutSocketCount: this.timeoutSocketCount,
+ requestCount: this.requestCount,
+ freeSockets: inspect(this.freeSockets),
+ sockets: inspect(this.sockets),
+ requests: inspect(this.requests),
+ };
+ }
+}
+
+// node 8 don't has timeout attribute on socket
+// https://github.com/nodejs/node/pull/21204/files#diff-e6ef024c3775d787c38487a6309e491dR408
+function getSocketTimeout(socket) {
+ return socket.timeout || socket._idleTimeout;
+}
+
+function installListeners(agent, socket, options) {
+ debug('%s create, timeout %sms', socket[SOCKET_NAME], getSocketTimeout(socket));
+
+ // listener socket events: close, timeout, error, free
+ function onFree() {
+ // create and socket.emit('free') logic
+ // https://github.com/nodejs/node/blob/master/lib/_http_agent.js#L311
+ // no req on the socket, it should be the new socket
+ if (!socket._httpMessage && socket[SOCKET_REQUEST_COUNT] === 1) return;
+
+ socket[SOCKET_REQUEST_FINISHED_COUNT]++;
+ agent.requestCount++;
+ debug('%s(requests: %s, finished: %s) free',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
+
+ // should reuse on pedding requests?
+ const name = agent.getName(options);
+ if (socket.writable && agent.requests[name] && agent.requests[name].length) {
+ // will be reuse on agent free listener
+ socket[SOCKET_REQUEST_COUNT]++;
+ debug('%s(requests: %s, finished: %s) will be reuse on agent free event',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
+ }
+ }
+ socket.on('free', onFree);
+
+ function onClose(isError) {
+ debug('%s(requests: %s, finished: %s) close, isError: %s',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], isError);
+ agent.closeSocketCount++;
+ }
+ socket.on('close', onClose);
+
+ // start socket timeout handler
+ function onTimeout() {
+ // onTimeout and emitRequestTimeout(_http_client.js)
+ // https://github.com/nodejs/node/blob/v12.x/lib/_http_client.js#L711
+ const listenerCount = socket.listeners('timeout').length;
+ // node <= 10, default listenerCount is 1, onTimeout
+ // 11 < node <= 12, default listenerCount is 2, onTimeout and emitRequestTimeout
+ // node >= 13, default listenerCount is 3, onTimeout,
+ // onTimeout(https://github.com/nodejs/node/pull/32000/files#diff-5f7fb0850412c6be189faeddea6c5359R333)
+ // and emitRequestTimeout
+ const timeout = getSocketTimeout(socket);
+ const req = socket._httpMessage;
+ const reqTimeoutListenerCount = req && req.listeners('timeout').length || 0;
+ debug('%s(requests: %s, finished: %s) timeout after %sms, listeners %s, defaultTimeoutListenerCount %s, hasHttpRequest %s, HttpRequest timeoutListenerCount %s',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
+ timeout, listenerCount, defaultTimeoutListenerCount, !!req, reqTimeoutListenerCount);
+ if (debug.enabled) {
+ debug('timeout listeners: %s', socket.listeners('timeout').map(f => f.name).join(', '));
+ }
+ agent.timeoutSocketCount++;
+ const name = agent.getName(options);
+ if (agent.freeSockets[name] && agent.freeSockets[name].indexOf(socket) !== -1) {
+ // free socket timeout, destroy quietly
+ socket.destroy();
+ // Remove it from freeSockets list immediately to prevent new requests
+ // from being sent through this socket.
+ agent.removeSocket(socket, options);
+ debug('%s is free, destroy quietly', socket[SOCKET_NAME]);
+ } else {
+ // if there is no any request socket timeout handler,
+ // agent need to handle socket timeout itself.
+ //
+ // custom request socket timeout handle logic must follow these rules:
+ // 1. Destroy socket first
+ // 2. Must emit socket 'agentRemove' event tell agent remove socket
+ // from freeSockets list immediately.
+ // Otherise you may be get 'socket hang up' error when reuse
+ // free socket and timeout happen in the same time.
+ if (reqTimeoutListenerCount === 0) {
+ const error = new Error('Socket timeout');
+ error.code = 'ERR_SOCKET_TIMEOUT';
+ error.timeout = timeout;
+ // must manually call socket.end() or socket.destroy() to end the connection.
+ // https://nodejs.org/dist/latest-v10.x/docs/api/net.html#net_socket_settimeout_timeout_callback
+ socket.destroy(error);
+ agent.removeSocket(socket, options);
+ debug('%s destroy with timeout error', socket[SOCKET_NAME]);
+ }
+ }
+ }
+ socket.on('timeout', onTimeout);
+
+ function onError(err) {
+ const listenerCount = socket.listeners('error').length;
+ debug('%s(requests: %s, finished: %s) error: %s, listenerCount: %s',
+ socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
+ err, listenerCount);
+ agent.errorSocketCount++;
+ if (listenerCount === 1) {
+ // if socket don't contain error event handler, don't catch it, emit it again
+ debug('%s emit uncaught error event', socket[SOCKET_NAME]);
+ socket.removeListener('error', onError);
+ socket.emit('error', err);
+ }
+ }
+ socket.on('error', onError);
+
+ function onRemove() {
+ debug('%s(requests: %s, finished: %s) agentRemove',
+ socket[SOCKET_NAME],
+ socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
+ // We need this function for cases like HTTP 'upgrade'
+ // (defined by WebSockets) where we need to remove a socket from the
+ // pool because it'll be locked up indefinitely
+ socket.removeListener('close', onClose);
+ socket.removeListener('error', onError);
+ socket.removeListener('free', onFree);
+ socket.removeListener('timeout', onTimeout);
+ socket.removeListener('agentRemove', onRemove);
+ }
+ socket.on('agentRemove', onRemove);
+}
+
+module.exports = Agent;
+
+function inspect(obj) {
+ const res = {};
+ for (const key in obj) {
+ res[key] = obj[key].length;
+ }
+ return res;
+}