1 | 'use strict';
|
---|
2 |
|
---|
3 | const OriginalAgent = require('http').Agent;
|
---|
4 | const ms = require('humanize-ms');
|
---|
5 | const debug = require('debug')('agentkeepalive');
|
---|
6 | const deprecate = require('depd')('agentkeepalive');
|
---|
7 | const {
|
---|
8 | INIT_SOCKET,
|
---|
9 | CURRENT_ID,
|
---|
10 | CREATE_ID,
|
---|
11 | SOCKET_CREATED_TIME,
|
---|
12 | SOCKET_NAME,
|
---|
13 | SOCKET_REQUEST_COUNT,
|
---|
14 | SOCKET_REQUEST_FINISHED_COUNT,
|
---|
15 | } = require('./constants');
|
---|
16 |
|
---|
17 | // OriginalAgent come from
|
---|
18 | // - https://github.com/nodejs/node/blob/v8.12.0/lib/_http_agent.js
|
---|
19 | // - https://github.com/nodejs/node/blob/v10.12.0/lib/_http_agent.js
|
---|
20 |
|
---|
21 | // node <= 10
|
---|
22 | let defaultTimeoutListenerCount = 1;
|
---|
23 | const majorVersion = parseInt(process.version.split('.', 1)[0].substring(1));
|
---|
24 | if (majorVersion >= 11 && majorVersion <= 12) {
|
---|
25 | defaultTimeoutListenerCount = 2;
|
---|
26 | } else if (majorVersion >= 13) {
|
---|
27 | defaultTimeoutListenerCount = 3;
|
---|
28 | }
|
---|
29 |
|
---|
30 | class Agent extends OriginalAgent {
|
---|
31 | constructor(options) {
|
---|
32 | options = options || {};
|
---|
33 | options.keepAlive = options.keepAlive !== false;
|
---|
34 | // default is keep-alive and 15s free socket timeout
|
---|
35 | if (options.freeSocketTimeout === undefined) {
|
---|
36 | options.freeSocketTimeout = 15000;
|
---|
37 | }
|
---|
38 | // Legacy API: keepAliveTimeout should be rename to `freeSocketTimeout`
|
---|
39 | if (options.keepAliveTimeout) {
|
---|
40 | deprecate('options.keepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
|
---|
41 | options.freeSocketTimeout = options.keepAliveTimeout;
|
---|
42 | delete options.keepAliveTimeout;
|
---|
43 | }
|
---|
44 | // Legacy API: freeSocketKeepAliveTimeout should be rename to `freeSocketTimeout`
|
---|
45 | if (options.freeSocketKeepAliveTimeout) {
|
---|
46 | deprecate('options.freeSocketKeepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
|
---|
47 | options.freeSocketTimeout = options.freeSocketKeepAliveTimeout;
|
---|
48 | delete options.freeSocketKeepAliveTimeout;
|
---|
49 | }
|
---|
50 |
|
---|
51 | // Sets the socket to timeout after timeout milliseconds of inactivity on the socket.
|
---|
52 | // By default is double free socket timeout.
|
---|
53 | if (options.timeout === undefined) {
|
---|
54 | // make sure socket default inactivity timeout >= 30s
|
---|
55 | options.timeout = Math.max(options.freeSocketTimeout * 2, 30000);
|
---|
56 | }
|
---|
57 |
|
---|
58 | // support humanize format
|
---|
59 | options.timeout = ms(options.timeout);
|
---|
60 | options.freeSocketTimeout = ms(options.freeSocketTimeout);
|
---|
61 | options.socketActiveTTL = options.socketActiveTTL ? ms(options.socketActiveTTL) : 0;
|
---|
62 |
|
---|
63 | super(options);
|
---|
64 |
|
---|
65 | this[CURRENT_ID] = 0;
|
---|
66 |
|
---|
67 | // create socket success counter
|
---|
68 | this.createSocketCount = 0;
|
---|
69 | this.createSocketCountLastCheck = 0;
|
---|
70 |
|
---|
71 | this.createSocketErrorCount = 0;
|
---|
72 | this.createSocketErrorCountLastCheck = 0;
|
---|
73 |
|
---|
74 | this.closeSocketCount = 0;
|
---|
75 | this.closeSocketCountLastCheck = 0;
|
---|
76 |
|
---|
77 | // socket error event count
|
---|
78 | this.errorSocketCount = 0;
|
---|
79 | this.errorSocketCountLastCheck = 0;
|
---|
80 |
|
---|
81 | // request finished counter
|
---|
82 | this.requestCount = 0;
|
---|
83 | this.requestCountLastCheck = 0;
|
---|
84 |
|
---|
85 | // including free socket timeout counter
|
---|
86 | this.timeoutSocketCount = 0;
|
---|
87 | this.timeoutSocketCountLastCheck = 0;
|
---|
88 |
|
---|
89 | this.on('free', socket => {
|
---|
90 | // https://github.com/nodejs/node/pull/32000
|
---|
91 | // Node.js native agent will check socket timeout eqs agent.options.timeout.
|
---|
92 | // Use the ttl or freeSocketTimeout to overwrite.
|
---|
93 | const timeout = this.calcSocketTimeout(socket);
|
---|
94 | if (timeout > 0 && socket.timeout !== timeout) {
|
---|
95 | socket.setTimeout(timeout);
|
---|
96 | }
|
---|
97 | });
|
---|
98 | }
|
---|
99 |
|
---|
100 | get freeSocketKeepAliveTimeout() {
|
---|
101 | deprecate('agent.freeSocketKeepAliveTimeout is deprecated, please use agent.options.freeSocketTimeout instead');
|
---|
102 | return this.options.freeSocketTimeout;
|
---|
103 | }
|
---|
104 |
|
---|
105 | get timeout() {
|
---|
106 | deprecate('agent.timeout is deprecated, please use agent.options.timeout instead');
|
---|
107 | return this.options.timeout;
|
---|
108 | }
|
---|
109 |
|
---|
110 | get socketActiveTTL() {
|
---|
111 | deprecate('agent.socketActiveTTL is deprecated, please use agent.options.socketActiveTTL instead');
|
---|
112 | return this.options.socketActiveTTL;
|
---|
113 | }
|
---|
114 |
|
---|
115 | calcSocketTimeout(socket) {
|
---|
116 | /**
|
---|
117 | * return <= 0: should free socket
|
---|
118 | * return > 0: should update socket timeout
|
---|
119 | * return undefined: not find custom timeout
|
---|
120 | */
|
---|
121 | let freeSocketTimeout = this.options.freeSocketTimeout;
|
---|
122 | const socketActiveTTL = this.options.socketActiveTTL;
|
---|
123 | if (socketActiveTTL) {
|
---|
124 | // check socketActiveTTL
|
---|
125 | const aliveTime = Date.now() - socket[SOCKET_CREATED_TIME];
|
---|
126 | const diff = socketActiveTTL - aliveTime;
|
---|
127 | if (diff <= 0) {
|
---|
128 | return diff;
|
---|
129 | }
|
---|
130 | if (freeSocketTimeout && diff < freeSocketTimeout) {
|
---|
131 | freeSocketTimeout = diff;
|
---|
132 | }
|
---|
133 | }
|
---|
134 | // set freeSocketTimeout
|
---|
135 | if (freeSocketTimeout) {
|
---|
136 | // set free keepalive timer
|
---|
137 | // try to use socket custom freeSocketTimeout first, support headers['keep-alive']
|
---|
138 | // https://github.com/node-modules/urllib/blob/b76053020923f4d99a1c93cf2e16e0c5ba10bacf/lib/urllib.js#L498
|
---|
139 | const customFreeSocketTimeout = socket.freeSocketTimeout || socket.freeSocketKeepAliveTimeout;
|
---|
140 | return customFreeSocketTimeout || freeSocketTimeout;
|
---|
141 | }
|
---|
142 | }
|
---|
143 |
|
---|
144 | keepSocketAlive(socket) {
|
---|
145 | const result = super.keepSocketAlive(socket);
|
---|
146 | // should not keepAlive, do nothing
|
---|
147 | if (!result) return result;
|
---|
148 |
|
---|
149 | const customTimeout = this.calcSocketTimeout(socket);
|
---|
150 | if (typeof customTimeout === 'undefined') {
|
---|
151 | return true;
|
---|
152 | }
|
---|
153 | if (customTimeout <= 0) {
|
---|
154 | debug('%s(requests: %s, finished: %s) free but need to destroy by TTL, request count %s, diff is %s',
|
---|
155 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], customTimeout);
|
---|
156 | return false;
|
---|
157 | }
|
---|
158 | if (socket.timeout !== customTimeout) {
|
---|
159 | socket.setTimeout(customTimeout);
|
---|
160 | }
|
---|
161 | return true;
|
---|
162 | }
|
---|
163 |
|
---|
164 | // only call on addRequest
|
---|
165 | reuseSocket(...args) {
|
---|
166 | // reuseSocket(socket, req)
|
---|
167 | super.reuseSocket(...args);
|
---|
168 | const socket = args[0];
|
---|
169 | const req = args[1];
|
---|
170 | req.reusedSocket = true;
|
---|
171 | const agentTimeout = this.options.timeout;
|
---|
172 | if (getSocketTimeout(socket) !== agentTimeout) {
|
---|
173 | // reset timeout before use
|
---|
174 | socket.setTimeout(agentTimeout);
|
---|
175 | debug('%s reset timeout to %sms', socket[SOCKET_NAME], agentTimeout);
|
---|
176 | }
|
---|
177 | socket[SOCKET_REQUEST_COUNT]++;
|
---|
178 | debug('%s(requests: %s, finished: %s) reuse on addRequest, timeout %sms',
|
---|
179 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
|
---|
180 | getSocketTimeout(socket));
|
---|
181 | }
|
---|
182 |
|
---|
183 | [CREATE_ID]() {
|
---|
184 | const id = this[CURRENT_ID]++;
|
---|
185 | if (this[CURRENT_ID] === Number.MAX_SAFE_INTEGER) this[CURRENT_ID] = 0;
|
---|
186 | return id;
|
---|
187 | }
|
---|
188 |
|
---|
189 | [INIT_SOCKET](socket, options) {
|
---|
190 | // bugfix here.
|
---|
191 | // https on node 8, 10 won't set agent.options.timeout by default
|
---|
192 | // TODO: need to fix on node itself
|
---|
193 | if (options.timeout) {
|
---|
194 | const timeout = getSocketTimeout(socket);
|
---|
195 | if (!timeout) {
|
---|
196 | socket.setTimeout(options.timeout);
|
---|
197 | }
|
---|
198 | }
|
---|
199 |
|
---|
200 | if (this.options.keepAlive) {
|
---|
201 | // Disable Nagle's algorithm: http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
|
---|
202 | // https://fengmk2.com/benchmark/nagle-algorithm-delayed-ack-mock.html
|
---|
203 | socket.setNoDelay(true);
|
---|
204 | }
|
---|
205 | this.createSocketCount++;
|
---|
206 | if (this.options.socketActiveTTL) {
|
---|
207 | socket[SOCKET_CREATED_TIME] = Date.now();
|
---|
208 | }
|
---|
209 | // don't show the hole '-----BEGIN CERTIFICATE----' key string
|
---|
210 | socket[SOCKET_NAME] = `sock[${this[CREATE_ID]()}#${options._agentKey}]`.split('-----BEGIN', 1)[0];
|
---|
211 | socket[SOCKET_REQUEST_COUNT] = 1;
|
---|
212 | socket[SOCKET_REQUEST_FINISHED_COUNT] = 0;
|
---|
213 | installListeners(this, socket, options);
|
---|
214 | }
|
---|
215 |
|
---|
216 | createConnection(options, oncreate) {
|
---|
217 | let called = false;
|
---|
218 | const onNewCreate = (err, socket) => {
|
---|
219 | if (called) return;
|
---|
220 | called = true;
|
---|
221 |
|
---|
222 | if (err) {
|
---|
223 | this.createSocketErrorCount++;
|
---|
224 | return oncreate(err);
|
---|
225 | }
|
---|
226 | this[INIT_SOCKET](socket, options);
|
---|
227 | oncreate(err, socket);
|
---|
228 | };
|
---|
229 |
|
---|
230 | const newSocket = super.createConnection(options, onNewCreate);
|
---|
231 | if (newSocket) onNewCreate(null, newSocket);
|
---|
232 | }
|
---|
233 |
|
---|
234 | get statusChanged() {
|
---|
235 | const changed = this.createSocketCount !== this.createSocketCountLastCheck ||
|
---|
236 | this.createSocketErrorCount !== this.createSocketErrorCountLastCheck ||
|
---|
237 | this.closeSocketCount !== this.closeSocketCountLastCheck ||
|
---|
238 | this.errorSocketCount !== this.errorSocketCountLastCheck ||
|
---|
239 | this.timeoutSocketCount !== this.timeoutSocketCountLastCheck ||
|
---|
240 | this.requestCount !== this.requestCountLastCheck;
|
---|
241 | if (changed) {
|
---|
242 | this.createSocketCountLastCheck = this.createSocketCount;
|
---|
243 | this.createSocketErrorCountLastCheck = this.createSocketErrorCount;
|
---|
244 | this.closeSocketCountLastCheck = this.closeSocketCount;
|
---|
245 | this.errorSocketCountLastCheck = this.errorSocketCount;
|
---|
246 | this.timeoutSocketCountLastCheck = this.timeoutSocketCount;
|
---|
247 | this.requestCountLastCheck = this.requestCount;
|
---|
248 | }
|
---|
249 | return changed;
|
---|
250 | }
|
---|
251 |
|
---|
252 | getCurrentStatus() {
|
---|
253 | return {
|
---|
254 | createSocketCount: this.createSocketCount,
|
---|
255 | createSocketErrorCount: this.createSocketErrorCount,
|
---|
256 | closeSocketCount: this.closeSocketCount,
|
---|
257 | errorSocketCount: this.errorSocketCount,
|
---|
258 | timeoutSocketCount: this.timeoutSocketCount,
|
---|
259 | requestCount: this.requestCount,
|
---|
260 | freeSockets: inspect(this.freeSockets),
|
---|
261 | sockets: inspect(this.sockets),
|
---|
262 | requests: inspect(this.requests),
|
---|
263 | };
|
---|
264 | }
|
---|
265 | }
|
---|
266 |
|
---|
267 | // node 8 don't has timeout attribute on socket
|
---|
268 | // https://github.com/nodejs/node/pull/21204/files#diff-e6ef024c3775d787c38487a6309e491dR408
|
---|
269 | function getSocketTimeout(socket) {
|
---|
270 | return socket.timeout || socket._idleTimeout;
|
---|
271 | }
|
---|
272 |
|
---|
273 | function installListeners(agent, socket, options) {
|
---|
274 | debug('%s create, timeout %sms', socket[SOCKET_NAME], getSocketTimeout(socket));
|
---|
275 |
|
---|
276 | // listener socket events: close, timeout, error, free
|
---|
277 | function onFree() {
|
---|
278 | // create and socket.emit('free') logic
|
---|
279 | // https://github.com/nodejs/node/blob/master/lib/_http_agent.js#L311
|
---|
280 | // no req on the socket, it should be the new socket
|
---|
281 | if (!socket._httpMessage && socket[SOCKET_REQUEST_COUNT] === 1) return;
|
---|
282 |
|
---|
283 | socket[SOCKET_REQUEST_FINISHED_COUNT]++;
|
---|
284 | agent.requestCount++;
|
---|
285 | debug('%s(requests: %s, finished: %s) free',
|
---|
286 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
|
---|
287 |
|
---|
288 | // should reuse on pedding requests?
|
---|
289 | const name = agent.getName(options);
|
---|
290 | if (socket.writable && agent.requests[name] && agent.requests[name].length) {
|
---|
291 | // will be reuse on agent free listener
|
---|
292 | socket[SOCKET_REQUEST_COUNT]++;
|
---|
293 | debug('%s(requests: %s, finished: %s) will be reuse on agent free event',
|
---|
294 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
|
---|
295 | }
|
---|
296 | }
|
---|
297 | socket.on('free', onFree);
|
---|
298 |
|
---|
299 | function onClose(isError) {
|
---|
300 | debug('%s(requests: %s, finished: %s) close, isError: %s',
|
---|
301 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], isError);
|
---|
302 | agent.closeSocketCount++;
|
---|
303 | }
|
---|
304 | socket.on('close', onClose);
|
---|
305 |
|
---|
306 | // start socket timeout handler
|
---|
307 | function onTimeout() {
|
---|
308 | // onTimeout and emitRequestTimeout(_http_client.js)
|
---|
309 | // https://github.com/nodejs/node/blob/v12.x/lib/_http_client.js#L711
|
---|
310 | const listenerCount = socket.listeners('timeout').length;
|
---|
311 | // node <= 10, default listenerCount is 1, onTimeout
|
---|
312 | // 11 < node <= 12, default listenerCount is 2, onTimeout and emitRequestTimeout
|
---|
313 | // node >= 13, default listenerCount is 3, onTimeout,
|
---|
314 | // onTimeout(https://github.com/nodejs/node/pull/32000/files#diff-5f7fb0850412c6be189faeddea6c5359R333)
|
---|
315 | // and emitRequestTimeout
|
---|
316 | const timeout = getSocketTimeout(socket);
|
---|
317 | const req = socket._httpMessage;
|
---|
318 | const reqTimeoutListenerCount = req && req.listeners('timeout').length || 0;
|
---|
319 | debug('%s(requests: %s, finished: %s) timeout after %sms, listeners %s, defaultTimeoutListenerCount %s, hasHttpRequest %s, HttpRequest timeoutListenerCount %s',
|
---|
320 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
|
---|
321 | timeout, listenerCount, defaultTimeoutListenerCount, !!req, reqTimeoutListenerCount);
|
---|
322 | if (debug.enabled) {
|
---|
323 | debug('timeout listeners: %s', socket.listeners('timeout').map(f => f.name).join(', '));
|
---|
324 | }
|
---|
325 | agent.timeoutSocketCount++;
|
---|
326 | const name = agent.getName(options);
|
---|
327 | if (agent.freeSockets[name] && agent.freeSockets[name].indexOf(socket) !== -1) {
|
---|
328 | // free socket timeout, destroy quietly
|
---|
329 | socket.destroy();
|
---|
330 | // Remove it from freeSockets list immediately to prevent new requests
|
---|
331 | // from being sent through this socket.
|
---|
332 | agent.removeSocket(socket, options);
|
---|
333 | debug('%s is free, destroy quietly', socket[SOCKET_NAME]);
|
---|
334 | } else {
|
---|
335 | // if there is no any request socket timeout handler,
|
---|
336 | // agent need to handle socket timeout itself.
|
---|
337 | //
|
---|
338 | // custom request socket timeout handle logic must follow these rules:
|
---|
339 | // 1. Destroy socket first
|
---|
340 | // 2. Must emit socket 'agentRemove' event tell agent remove socket
|
---|
341 | // from freeSockets list immediately.
|
---|
342 | // Otherise you may be get 'socket hang up' error when reuse
|
---|
343 | // free socket and timeout happen in the same time.
|
---|
344 | if (reqTimeoutListenerCount === 0) {
|
---|
345 | const error = new Error('Socket timeout');
|
---|
346 | error.code = 'ERR_SOCKET_TIMEOUT';
|
---|
347 | error.timeout = timeout;
|
---|
348 | // must manually call socket.end() or socket.destroy() to end the connection.
|
---|
349 | // https://nodejs.org/dist/latest-v10.x/docs/api/net.html#net_socket_settimeout_timeout_callback
|
---|
350 | socket.destroy(error);
|
---|
351 | agent.removeSocket(socket, options);
|
---|
352 | debug('%s destroy with timeout error', socket[SOCKET_NAME]);
|
---|
353 | }
|
---|
354 | }
|
---|
355 | }
|
---|
356 | socket.on('timeout', onTimeout);
|
---|
357 |
|
---|
358 | function onError(err) {
|
---|
359 | const listenerCount = socket.listeners('error').length;
|
---|
360 | debug('%s(requests: %s, finished: %s) error: %s, listenerCount: %s',
|
---|
361 | socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
|
---|
362 | err, listenerCount);
|
---|
363 | agent.errorSocketCount++;
|
---|
364 | if (listenerCount === 1) {
|
---|
365 | // if socket don't contain error event handler, don't catch it, emit it again
|
---|
366 | debug('%s emit uncaught error event', socket[SOCKET_NAME]);
|
---|
367 | socket.removeListener('error', onError);
|
---|
368 | socket.emit('error', err);
|
---|
369 | }
|
---|
370 | }
|
---|
371 | socket.on('error', onError);
|
---|
372 |
|
---|
373 | function onRemove() {
|
---|
374 | debug('%s(requests: %s, finished: %s) agentRemove',
|
---|
375 | socket[SOCKET_NAME],
|
---|
376 | socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
|
---|
377 | // We need this function for cases like HTTP 'upgrade'
|
---|
378 | // (defined by WebSockets) where we need to remove a socket from the
|
---|
379 | // pool because it'll be locked up indefinitely
|
---|
380 | socket.removeListener('close', onClose);
|
---|
381 | socket.removeListener('error', onError);
|
---|
382 | socket.removeListener('free', onFree);
|
---|
383 | socket.removeListener('timeout', onTimeout);
|
---|
384 | socket.removeListener('agentRemove', onRemove);
|
---|
385 | }
|
---|
386 | socket.on('agentRemove', onRemove);
|
---|
387 | }
|
---|
388 |
|
---|
389 | module.exports = Agent;
|
---|
390 |
|
---|
391 | function inspect(obj) {
|
---|
392 | const res = {};
|
---|
393 | for (const key in obj) {
|
---|
394 | res[key] = obj[key].length;
|
---|
395 | }
|
---|
396 | return res;
|
---|
397 | }
|
---|