source: node_modules/agentkeepalive/lib/agent.js@ 81bc7da

finki-main main
Last change on this file since 81bc7da was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

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