Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: reduce likelihood of race conditions on keep-alive timeout #54863

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
anonrig marked this conversation as resolved.
Show resolved Hide resolved
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();
}));
Loading