Skip to content

Reduce likelihood of race conditions on keep-alive timeout calculatio… - #52653

Closed
zanettea wants to merge 1 commit into
nodejs:mainfrom
zanettea:main
Closed

Reduce likelihood of race conditions on keep-alive timeout calculatio…#52653
zanettea wants to merge 1 commit into
nodejs:mainfrom
zanettea:main

Conversation

@zanettea

Copy link
Copy Markdown
Contributor

Added 1 seconds threshold in keepalive timeout client-side
Added 1 second threshold in keepalive timeout server-side (expire the socket timeout 1 sec after the announced timeout)

Probably better to use a configurable threshold like in undici keepAliveTimeoutThreshold (nodejs/undici#291)

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/http
  • @nodejs/net

@nodejs-github-botnodejs-github-bot added http Issues or PRs related to the http subsystem. needs-ci PRs that need a full CI run. labels Apr 23, 2024

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Apr 23, 2024
@mcollina

Copy link
Copy Markdown
Member

ping @mweberxyz

@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Apr 23, 2024
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/_http_agent.js Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional needs to be fixed - agentTimeout defaults to 0, so the serverHintTimeout is never being set.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the socket shouldn't be reused at all. Is setting socket.setTimeout(0) the right way to do it?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look at socket docs - socket.setTimeout(0) doesn't mean immediate, it means never.

@zanetteazanetteaApr 23, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I have limited understanding of the network internals. In undici ( https://github.com/nodejs/undici/pull/291/files ) if the keepAliveTimeout goes down to 0 they flag the connection as reset:

if (!keepAliveTimeout || keepAliveTimeout < 1e3) {
client[kReset] = true
} 

I don't know how to achieve the same result here. Maybe:

socket.setKeepAlive(false);

?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also have a limited understanding of network internals, but I think:

if (!agentTimeout || serverHintTimeout < agentTimeout) {

is what you want.

Comment threadlib/_http_server.js Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this server change is necessary.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be the cause of all the test failures.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix protects clients that are using the hint timeout as is (current node impl) without adjusting it for network (or cpu load) delays

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If server.keepAliveTimeout is set to 0 (never time out) this will change it to 1 second.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If server.keepAliveTimeout == 0 then the if condition line1012 is false. The timeout on the socket is set only if server.keepAliveTimeout != 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct, sorry I missed that.

Comment threadlib/_http_agent.js Outdated

@mweberxyzmweberxyzApr 23, 2024

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case the server responds with a 1 second keepalive, this will set the disable the timeout because serverHintTimeout will be 0 -- maybe go with - 500 in place of - 1000 ?

Comment threadlib/_http_agent.js Outdated

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the PR to just skip keep alive if the timeout is too short. In my local build the test now pass.

Comment threadlib/_http_agent.js Outdated
@meyfa

This comment has been minimized.

@zanetteazanettea changed the title Reduce likeliwood of race conditions on keep-alive timeout calculatio…Reduce likelihood of race conditions on keep-alive timeout calculatio…Apr 24, 2024
@zanettea
zanetteaforce-pushed the main branch 2 times, most recently from 8407fd7 to bc34680CompareApril 24, 2024 10:12
@mweberxyz

Copy link
Copy Markdown

Found the same issue in dotnet -- they went with a 1 second offset as well, so that was a good choice. 👍

@zanettea
zanetteaforce-pushed the main branch 3 times, most recently from ba0d895 to 2124b55CompareApril 24, 2024 15:30

@mweberxyzmweberxyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! I verified that the test cases in #52649 pass without error.

I don't have Approve permission - lgtm @mcollina

@ShogunPandaShogunPanda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

reduce likelihood of race conditions on keep-alive timeout
calculation between http1.1 servers and clients and honor server
keep-alive timeout when agentTimeout is not set
Fixes: nodejs#47130Fixes: nodejs#52649

@fahrradfluchtfahrradflucht left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it looks like this should be a testable behavior change.

I had a short look and I guess if test-http-client-keep-alive-hint.js would be a better test it should actually fail now, because the case it tests now behaves quite different (there is no keep-alive because the hint is just 1 second).

I think a test somewhat along these lines would pass on main and fail on this branch:

'use strict';constcommon=require('../common');constassert=require('assert');consthttp=require('http');constserver=http.createServer({keepAliveTimeout: common.platformTimeout(60000)},function(req,res){req.resume();res.writeHead(200,{'Connection': 'keep-alive','Keep-Alive': 'timeout=1'});res.end('FOO');});server.listen(0,common.mustCall(()=>{letshouldStillBeAlive=true;setTimeout(()=>{shouldStillBeAlive=false;},common.platformTimeout(500));// 500ms bufferconstreq=http.get({port: server.address().port},(res)=>{assert.strictEqual(res.statusCode,200);res.resume();});req.on('socket',(socket)=>{socket.on('close',common.mustCall(()=>{if(shouldStillBeAlive){assert.fail('socket prematurely closed');}server.close();}));});}));// This timer should never go off as the agent will parse the hint and terminate earliersetTimeout(common.mustNotCall(),common.platformTimeout(3000)).unref();

I guess it would be good to write a similar test (hopefully a version that has to rely less on timers) that verifies that we are actually latency adjusting (currently hard-coded to 1s) the hint based keep alive?

Comment threadlib/_http_server.js
} 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 likeliwood of ECONNRESET errors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Increase the internal timeout wrt the advertised value to reduce likeliwood of ECONNRESET errors
// Increase the internal timeout wrt the advertised value to reduce likelihood of ECONNRESET errors

Comment threadlib/_http_agent.js
const serverHintTimeout = NumberParseInt(hint) * 1000;

if (serverHintTimeout < agentTimeout) {
// Let the timer expires before the announced timeout to reduce

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Let the timer expires before the announced timeout to reduce
// Let the timer expire before the announced timeout to reduce

@jazelly

Copy link
Copy Markdown
Member

Hey, this valuable PR fixes many similar issues for keep-alive. Is it possible to push this through 👀

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2024
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2024
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jakecastellijakecastelli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a test?

@jazelly

jazelly commented Sep 7, 2024

Copy link
Copy Markdown
Member

Hi, I'd like to help this land to resovle similar issues. Can I continue the work and add @zanettea as a co-author? To me, there are just test cases to be added and some commits to be rebased to dodge the flakiness on CI.

@nikwen

Copy link
Copy Markdown
Contributor

The new PR was just merged. This one can be closed now. (Thanks to @jazelly, @zanettea and everyone else who worked on it!)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

httpIssues or PRs related to the http subsystem.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

12 participants

@zanettea@nodejs-github-bot@mcollina@meyfa@mweberxyz@jazelly@nikwen@ShogunPanda@jasnell@meixg@fahrradflucht@jakecastelli