- Version: v13.11.0+
- Platform:
- Subsystem: http
We have a desire to limit the overall lifetime of a socket used for http keep-alive, we've done this with a simple (possibly non-ideal) agent implementation that looks similar to below:
Expand to see HttpAgent implementation
'use strict';const_HttpsAgent=require('https').Agent;constMAX_SOCKET_LIFETIME_MS=10*60*1000;constMAX_KEEPALIVE_IDLE_MS=50*1000;// By default timeouts on a socket don't really do anything. As such, after// setting a timeout when a socket is kept for keep-alive, it will still get// re-used again after the timeout period unless it is closed by other means.// Listening for the timeout event lets use close the socket and remove it from// the pool. This listener is added when sockets get kept, and removed when// they get re-used, so should have no risk of effecting external behaviour.functiononTimeout(){this.destroy();}functionsetSocketCreationTime(err,socket){if(err)return;socket._creation=Date.now();}functionsocketIsExpired(agent,socket){if(!socket._creation){// shouldn't happen, but covering my basessetSocketCreationTime(null,socket);returnfalse;}returnDate.now()-socket._creation>agent.maxSocketLifetime;}functionsetSocketTimeout(agent,socket){constttl=socket._creation-Date.now()+agent.maxSocketLifetime;consttimeout=Math.min(ttl,agent.maxIdleTimeout);socket.setTimeout(timeout,onTimeout);}functionclearSocketTimeout(socket){socket.setTimeout(0,onTimeout);}classHttpsAgentextends_HttpsAgent{constructor(opts){opts=opts||{};opts.keepAlive=opts.keepAlive!==false;super(opts);this.maxSocketLifetime=MAX_SOCKET_LIFETIME_MS;this.maxIdleTimeout=MAX_KEEPALIVE_IDLE_MS;}createSocket(req,options,cb){returnsuper.createSocket(req,options,(err,socket)=>{setSocketCreationTime(err,socket);returncb(err,socket);});}keepSocketAlive(socket){// while we want keep-alive for latency/performance, we don't want to// keep sockets around forever so that if a backend moves (without// necessarily closing connections) we will eventually hit the new// addressesif(socketIsExpired(this,socket)){// returning false means the agent will destroy the socket instead// of keeping it for re-usereturnfalse;}// by default sockets will never time out// set a timeout for our remaining desired lifetime of the socket, so// if it doesn't get re-used before then, it will be destroyed for// being too old while sitting idlesetSocketTimeout(this,socket);returnsuper.keepSocketAlive(socket);}reuseSocket(socket,req){// we set a timeout when this socket last went back into the pool so// that it didnt sit idle past its desired lifetime; however that could// have been as little as say, 100ms. We don't want to cause// inconsistent behaviour, having the socket time out during a "slow"// request to the backend, so reset the timeout back to infinity.// this can still be set by request.timeout() afterward, as normalclearSocketTimeout(socket);returnsuper.reuseSocket(socket,req);}}The overall approach:
- Track when the socket was created
- Check if its passed its lifetime when entering the freelist and reject it
- Set a timeout for its remaining lifetime (or a "max free time") when entering the freelist
Which has served us fairly well, and I believe similar mechanisms exist in more featureful "keep-alive agent" node modules.
The built-in agent started trying to handle some of this behaviour in 13.11 (8700d89#diff-5f7fb0850412c6be189faeddea6c5359#32000) by setting its own timeout listener (removing the need for our agent's onTimeout), but also by resetting the socket's timeout just before re-inserting it into the freelist. Unfortunately this setTimeout proceeds the call to keepSocketAlive, overriding the behaviour we've implemented.
} else if (this.keepSocketAlive(socket)) {
freeSockets = freeSockets || [];
this.freeSockets[name] = freeSockets;
socket[async_id_symbol] = -1;
socket._httpMessage = null;
this.removeSocket(socket, options);
+ const agentTimeout = this.options.timeout || 0;+ if (socket.timeout !== agentTimeout) {+ socket.setTimeout(agentTimeout);+ }+
freeSockets.push(socket);The documentation on the agent's overridable hooks doesn't necessarily describe what may happen before or after them, but this is something we'd like to be able to maintain.
I may be able to follow up with a PR in the next few days. My first thought would be to move the agent's built-in timeout reset be moved into the default keepSocketAlive function.
Thanks!
We have a desire to limit the overall lifetime of a socket used for http keep-alive, we've done this with a simple (possibly non-ideal) agent implementation that looks similar to below:
Expand to see HttpAgent implementation
The overall approach:
Which has served us fairly well, and I believe similar mechanisms exist in more featureful "keep-alive agent" node modules.
The built-in agent started trying to handle some of this behaviour in 13.11 (8700d89#diff-5f7fb0850412c6be189faeddea6c5359#32000) by setting its own timeout listener (removing the need for our agent's
onTimeout), but also by resetting the socket's timeout just before re-inserting it into the freelist. Unfortunately thissetTimeoutproceeds the call tokeepSocketAlive, overriding the behaviour we've implemented.} else if (this.keepSocketAlive(socket)) { freeSockets = freeSockets || []; this.freeSockets[name] = freeSockets; socket[async_id_symbol] = -1; socket._httpMessage = null; this.removeSocket(socket, options); + const agentTimeout = this.options.timeout || 0;+ if (socket.timeout !== agentTimeout) {+ socket.setTimeout(agentTimeout);+ }+ freeSockets.push(socket);The documentation on the agent's overridable hooks doesn't necessarily describe what may happen before or after them, but this is something we'd like to be able to maintain.
I may be able to follow up with a PR in the next few days. My first thought would be to move the agent's built-in timeout reset be moved into the default
keepSocketAlivefunction.Thanks!