-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
Conversation
Currently reset timers on both session & stream when write starts and when it ends.
131b1a1
to
80c834d
Compare
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.)
80c834d
to
03d7e3f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
CI: https://ci.nodejs.org/job/node-test-pull-request/11005/ (edit: lone failure is unrelated) |
@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...) |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
lib/internal/http2/core.js
Outdated
if (this[kState].writeQueueSize > 0) { | ||
const handle = this[kHandle]; | ||
if (handle !== undefined && | ||
handle.chunksSentSinceLastWrite !== handle.updateChunksSent()) { |
There was a problem hiding this comment.
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? 😄
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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_
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, makes sense!
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. |
merging it right now actually |
doing one last local |
Awesome, thanks @jasnell! |
* 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 <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
Landed in ca82e30 |
* 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 <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
* 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 <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
* 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 <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
* 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 <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
* 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 <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
This resolves a couple of issues around timeouts in http2.
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).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:
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), orvcbuild test
(Windows) passesAffected core subsystem(s)
http2, test