Skip to content

Commit

Permalink
http: reduce likelihood of race conditions on keep-alive timeout
Browse files Browse the repository at this point in the history
Fixes: #52649
Refs: #54293

Co-authored-by: Arrigo Zanette <[email protected]>
  • Loading branch information
jazelly and zanettea committed Sep 10, 2024
1 parent 79a33a7 commit 5f6ab97
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 5 deletions.
18 changes: 14 additions & 4 deletions lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const {
const kOnKeylog = Symbol('onkeylog');
const kRequestOptions = Symbol('requestOptions');
const kRequestAsyncResource = Symbol('requestAsyncResource');

// TODO(jazelly): make this configurable
const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;
// New Agent code.

// The largest departure from the previous implementation is that
Expand Down Expand Up @@ -473,6 +476,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.unref();

let agentTimeout = this.options.timeout || 0;
let canKeepSocketAlive = true;

if (socket._httpMessage?.res) {
const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
Expand All @@ -481,9 +485,15 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];

if (hint) {
const serverHintTimeout = NumberParseInt(hint) * 1000;

if (serverHintTimeout < agentTimeout) {
// Let the timer expire before the announced timeout to reduce
// the likelihood of ECONNRESET errors
let serverHintTimeout = (NumberParseInt(hint) * 1000) - HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER;
serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
if (serverHintTimeout === 0) {
// Cannot safely reuse the socket because the server timeout is
// too short
canKeepSocketAlive = false;
} else if (serverHintTimeout < agentTimeout) {
agentTimeout = serverHintTimeout;
}
}
Expand All @@ -494,7 +504,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setTimeout(agentTimeout);
}

return true;
return canKeepSocketAlive;
};

Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
Expand Down
6 changes: 5 additions & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ const kConnections = Symbol('http.server.connections');
const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval');

const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request';
// TODO(jazelly): make this configurable
const HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;

class HTTPServerAsyncResource {
constructor(type, socket) {
Expand Down Expand Up @@ -1007,7 +1009,9 @@ function resOnFinish(req, res, socket, state, server) {
}
} else if (state.outgoing.length === 0) {
if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') {
socket.setTimeout(server.keepAliveTimeout);
// Increase the internal timeout wrt the advertised value to reduce
// the likelihood of ECONNRESET errors.
socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER);
state.keepAliveTimeoutSet = true;
}
} else {
Expand Down
38 changes: 38 additions & 0 deletions test/parallel/test-http-keep-alive-timeout-race-condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

const common = require('../common');
const http = require('http');

const makeRequest = (port, agent) =>
new Promise((resolve, reject) => {
const req = http.get(
{ path: '/', port, agent },
(res) => {
res.resume();
res.on('end', () => resolve());
},
);
req.on('error', (e) => reject(e));
req.end();
});

const server = http.createServer(
{ keepAliveTimeout: common.platformTimeout(2000), keepAlive: true },
common.mustCall((req, res) => {
const body = 'hello world\n';
res.writeHead(200, { 'Content-Length': body.length });
res.write(body);
res.end();
}, 2)
);

const agent = new http.Agent({ maxSockets: 5, keepAlive: true });

server.listen(0, common.mustCall(async function() {
await makeRequest(this.address().port, agent);
// Block the event loop for 2 seconds
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
await makeRequest(this.address().port, agent);
server.close();
agent.destroy();
}));

0 comments on commit 5f6ab97

Please sign in to comment.