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

http2: fix several timeout related issues #16525

Closed
wants to merge 3 commits into from

Conversation

apapirovski
Copy link
Member

This resolves a couple of issues around timeouts in http2.

  1. Upon start and stop of write, _unrefActive should be called both on stream and on session. I can't really create a good test case for this because it's way too reliant on timers. One can observe this by having a server receive a request, have a timeout of 1000ms on the session and delay the stream response (which has to last longer than 500ms) by 500ms. The session will timeout 500ms after the stream write starts (instead of 1000ms).

  2. Long writes will always timeout because once the data is handed off to C++, JS loses all track of it. This is basically the same issue as the one recently patched in net & tls. Solution is a bit different because tracking writeQueueSize in C++ is not quite reliable due to framing headers and what not.

(Much like with tls/net, the solution isn't super straightforward because we don't want to completely tank the performance of requests that aren't likely to timeout.)

To test locally, you can use the following server:

'use strict';

const http2 = require('http2');

const server = http2.createServer((req, res) => {

  var content = Buffer.alloc(30000000, 0x44);

  res.writeHead(200, {
    'Content-Type': 'application/octet-stream',
    'Content-Length': content.length.toString(),
    'Vary': 'Accept-Encoding'
  });

  res.write(content);
  res.end();
});
server.setTimeout(1000);

server.listen(8000);

and run this in your terminal:

nghttp http://localhost:8000 | dd bs=1 of=/dev/null

A successful result would be 30000000+0 records in (and take around 40s) but it'll stop after only 1s with current versions of node.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

http2, test

Currently reset timers on both session & stream when write
starts and when it ends.
@apapirovski apapirovski added c++ Issues and PRs that require attention from people who are familiar with C++. http2 Issues or PRs related to the http2 subsystem. labels Oct 26, 2017
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. labels Oct 26, 2017
@apapirovski apapirovski force-pushed the patch-http2-timeouts branch 2 times, most recently from 131b1a1 to 80c834d Compare October 26, 2017 19:32
@apapirovski
Copy link
Member Author

Also, needless to say, any and all input on the C++ end is appreciated. Still trying to get a handle on all of it.

When writing a large chunk of data in http2, once the data is handed
off to C++, the JS session & stream lose all track of the write and
will timeout if the write doesn't complete within the timeout window

Fix this issue by tracking whether a write request is ongoing and
also tracking how many chunks have been sent since the most recent
write started. (Since each write call resets the timer.)
Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

LGTM

@apapirovski
Copy link
Member Author

apapirovski commented Oct 26, 2017

CI: https://ci.nodejs.org/job/node-test-pull-request/11005/ (edit: lone failure is unrelated)

@apapirovski
Copy link
Member Author

@addaleax if you happen to have some spare bandwidth in the next few days, would appreciate a review both for C++ reasons and because you reviewed the net/tls version of this. Thanks!

(The tests are probably the longest part of this and are identical to the net/tls version so you can probably skip them. They're pretty verbose...)

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

Nice work!

function trackWriteState(stream, bytes) {
const session = stream[kSession];
stream[kState].writeQueueSize += bytes;
session[kState].writeQueueSize += bytes;
Copy link
Member

Choose a reason for hiding this comment

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

Any chance we could rename the property to writeQueueBytes if it’s counting bytes? My initial thought was it was counting the # of write reqs… I realize this makes sense because if it were trakcing the # of reqs, it would probaly be Length rather than Size, but maybe being even more explicit could be helpful?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's currently the same name as net/tls but I can definitely rename if that's preferred.

Copy link
Member

Choose a reason for hiding this comment

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

Nah, if it’s for consistency then this is fine

if (this[kState].writeQueueSize > 0) {
const handle = this[kHandle];
if (handle !== undefined &&
handle.chunksSentSinceLastWrite !== handle.updateChunksSent()) {
Copy link
Member

Choose a reason for hiding this comment

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

The right side of this comparison modifies the left side, right? Could we maaaaybe store the left side in a variable? 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep.

@@ -811,6 +830,8 @@ int Http2Session::DoWrite(WriteWrap* req_wrap,
}
}

chunks_sent_since_last_write_ = 0;
Copy link
Member

Choose a reason for hiding this comment

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

Is my understanding correct that this and SubmitFile are the only parts of the HTTP2 source that can trigger Http2Session::Send to be called?

Copy link
Member Author

@apapirovski apapirovski Oct 28, 2017

Choose a reason for hiding this comment

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

As far as I could tell, yes. misleading

Copy link
Member Author

@apapirovski apapirovski Oct 28, 2017

Choose a reason for hiding this comment

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

Sorry, to be clear: it doesn't quite matter if they're the only parts that can trigger it because any trigger of Send means that the connection is still active (in some form). The more important part of this is that only DoWrite and SubmitFile are triggered when a write occurs and hence the only ones that should reset chunks_sent_since_last_write_.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, okay. My question wasn’t relevant to this particular PR – it just was kind of my hope that this meant the # of places that could schedule a write would be overseeable so that it would be easy to turn the uv_prepare_t we use on/off in those places

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. I think that loop is a bit more complicated since nghttp2 manages the flow of data. The only place that can call Send is SendPendingData which requests it from nghttp2_session_mem_send. I think there are like 2-3 places that call SendPendingData.

Send itself is responsible for all the data going to the socket, which includes all the frames that nghttp2 itself manages. I think @jasnell might have more insight, I've only started figuring out nghttp2 in the last few weeks.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah – basically, the question was if these were the only places that would make nghttp2 want to write something to the socket ;) I guess I can answer that for myself with a ”no”, in the end.

Copy link
Member

Choose a reason for hiding this comment

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

There are lots of things that could trigger Ng too want to write. Responding to ping frames, for instance, is handled entirely by Ng without any intervention from our code. There are also automatic error handing situations that do not bubble up to our code.

Copy link
Member

Choose a reason for hiding this comment

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

Responding to ping frames, for instance, is handled entirely by Ng without any intervention from our code.

How do we know we need to write to the socket in those cases?

Copy link
Member

Choose a reason for hiding this comment

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

That's currently why we attempt to send on every event loop tick. There is a want_write api, however that can be checked. It won't tell you how much data is pending to write, but it will answer true so long as there is data pending in the queue.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, makes sense!

@apapirovski
Copy link
Member Author

Thanks for the review @addaleax & @mcollina! Implemented the tiny change request as per above.

@apapirovski
Copy link
Member Author

I'll be merging this within the next hour or so (so it can make 9.0.0), barring any objections. It has 3 reviews + sat around for 3 days.

@jasnell
Copy link
Member

jasnell commented Oct 29, 2017

merging it right now actually

@jasnell
Copy link
Member

jasnell commented Oct 29, 2017

doing one last local make test then will be pushing

@apapirovski
Copy link
Member Author

Awesome, thanks @jasnell!

jasnell pushed a commit that referenced this pull request Oct 29, 2017
* correctly reset write timers: currently reset timers on
  both session & stream when write starts and when it ends.
* prevent large writes from timing out: when writing a large
  chunk of data in http2, once the data is handed off to C++,
  the JS session & stream lose all track of the write and will
  timeout if the write doesn't complete within the timeout window
  Fix this issue by tracking whether a write request is ongoing and
  also tracking how many chunks have been sent since the most recent
  write started. (Since each write call resets the timer.)

PR-URL: #16525
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
@jasnell
Copy link
Member

jasnell commented Oct 29, 2017

Landed in ca82e30

@jasnell jasnell closed this Oct 29, 2017
@apapirovski apapirovski deleted the patch-http2-timeouts branch October 29, 2017 17:03
gibfahn pushed a commit that referenced this pull request Oct 30, 2017
* correctly reset write timers: currently reset timers on
  both session & stream when write starts and when it ends.
* prevent large writes from timing out: when writing a large
  chunk of data in http2, once the data is handed off to C++,
  the JS session & stream lose all track of the write and will
  timeout if the write doesn't complete within the timeout window
  Fix this issue by tracking whether a write request is ongoing and
  also tracking how many chunks have been sent since the most recent
  write started. (Since each write call resets the timer.)

PR-URL: #16525
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
gibfahn pushed a commit that referenced this pull request Oct 30, 2017
* correctly reset write timers: currently reset timers on
  both session & stream when write starts and when it ends.
* prevent large writes from timing out: when writing a large
  chunk of data in http2, once the data is handed off to C++,
  the JS session & stream lose all track of the write and will
  timeout if the write doesn't complete within the timeout window
  Fix this issue by tracking whether a write request is ongoing and
  also tracking how many chunks have been sent since the most recent
  write started. (Since each write call resets the timer.)

PR-URL: #16525
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
gibfahn pushed a commit that referenced this pull request Oct 31, 2017
* correctly reset write timers: currently reset timers on
  both session & stream when write starts and when it ends.
* prevent large writes from timing out: when writing a large
  chunk of data in http2, once the data is handed off to C++,
  the JS session & stream lose all track of the write and will
  timeout if the write doesn't complete within the timeout window
  Fix this issue by tracking whether a write request is ongoing and
  also tracking how many chunks have been sent since the most recent
  write started. (Since each write call resets the timer.)

PR-URL: #16525
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
@gibfahn gibfahn mentioned this pull request Oct 31, 2017
Qard pushed a commit to ayojs/ayo that referenced this pull request Nov 2, 2017
* correctly reset write timers: currently reset timers on
  both session & stream when write starts and when it ends.
* prevent large writes from timing out: when writing a large
  chunk of data in http2, once the data is handed off to C++,
  the JS session & stream lose all track of the write and will
  timeout if the write doesn't complete within the timeout window
  Fix this issue by tracking whether a write request is ongoing and
  also tracking how many chunks have been sent since the most recent
  write started. (Since each write call resets the timer.)

PR-URL: nodejs/node#16525
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
addaleax pushed a commit to ayojs/ayo that referenced this pull request Dec 7, 2017
* correctly reset write timers: currently reset timers on
  both session & stream when write starts and when it ends.
* prevent large writes from timing out: when writing a large
  chunk of data in http2, once the data is handed off to C++,
  the JS session & stream lose all track of the write and will
  timeout if the write doesn't complete within the timeout window
  Fix this issue by tracking whether a write request is ongoing and
  also tracking how many chunks have been sent since the most recent
  write started. (Since each write call resets the timer.)

PR-URL: nodejs/node#16525
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. http2 Issues or PRs related to the http2 subsystem. lib / src Issues and PRs related to general changes in the lib or src directory.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants