| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
There was a problem hiding this comment.
I’m not sure if this is helpful feedback, but I agree that this looks more like a real bug in the HTTP/2 code.
Sorry, something went wrong.
|
@Flarna thank you for taking on this work. @Trott, @jasnell, what do you think? we could perhaps add a TODO comment, referencing someone familiar with the http2 library, to dig further into this issue. But land this work to get Windows tests more stable? |
Sorry, something went wrong.
If anyone on @nodejs/http2 chimes in, I'll be likely to defer to them, but if not, my opinion would be: If we have code that causes the bug to happen reliably, we could add that code as a test to the known_issues directory and have this version in parallel. |
Sorry, something went wrong.
|
Back in July when I looked into this test I failed to created a 100% reproducer. Most likely because I haven't fully understood the actual root cause. |
Sorry, something went wrong.
|
Haven't looked much into http2 apart from the http2 compat parts but here is my take on this: Following stream semantics the existing test is not entirely correct. We cannot destroy the client before 'close' and expect that no further errors to be sent. 'finish'/'end' does not mean that the client is done, just that it is done with sending/receiving data from/to the user. There might still be relevant teardown logic that needs to run for a graceful completion. Therefore, we should not kill the client/server before the client has emitted 'close'. I think the test should be updated with: @@ -35,7 +35,7 @@ fs.readFile(loc, common.mustCall((err, data) => {
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
- const countdown = new Countdown(2, () => {
+ const countdown = new Countdown(3, () => {
server.close();
client.close();
});
@@ -47,6 +47,7 @@ fs.readFile(loc, common.mustCall((err, data) => {
req.on('end', common.mustCall());
req.on('finish', () => countdown.dec());
+ req.on('close', () => countdown.dec());
const str = fs.createReadStream(loc);
str.on('end', common.mustCall());Would be interesting if it is still flaky after that. i.e. this PR is on the right track but on the wrong side. |
Sorry, something went wrong.
|
Tried the change proposed by @ronag but it doesn't help. The close event on req is always emitted before close on file stream therefore no change regarding the shutdown time of client/server. |
Sorry, something went wrong.
|
@Flarna: What about something like: const countdown = new Countdown(3, () => {
client.on('close', () => server.close()); // I'm not sure if this event exists :/
client.close();
});The client maybe shuts down before the server causing a ECONNRESET. |
Sorry, something went wrong.
|
@ronag tried it but it doesn't help. |
Sorry, something went wrong.
|
Then maybe the problem lies in close and kMaybeDestroy: close(callback) {
if (this.closed || this.destroyed)
return;
debugSessionObj(this, 'marking session closed');
this[kState].flags |= SESSION_FLAGS_CLOSED;
if (typeof callback === 'function')
this.once('close', callback);
this.goaway();
this[kMaybeDestroy]();
}
// Destroy the session if:
// * error is not undefined/null
// * session is closed and there are no more pending or open streams
[kMaybeDestroy](error) {
if (error == null) {
const state = this[kState];
// Do not destroy if we're not closed and there are pending/open streams
if (!this.closed ||
state.streams.size > 0 ||
state.pendingStreams.size > 0) {
return;
}
}
this.destroy(error);
}Based on my interpretation of @Flarna's debugging it seems that the return condition in kMaybeDestroy does not take into account whether the go away frame has been sent/flushed or not. Also, (possibly unrelated) [kMaybeDestroy] does not seem to take into account whether the session has connected or not. Not quite sure of the implications of that. |
Sorry, something went wrong.
|
@nodejs/http2 The code in #29889 (comment) is from lib/http/core.js. Thoughts on @ronag's comment there? |
Sorry, something went wrong.
|
@Flarna My last guess. But maybe? Will try and see if I can sort out a Windows workstation to reproduce. --- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -2576,6 +2576,7 @@ void Http2Session::Goaway(uint32_t code,
Debug(this, "submitting goaway");
nghttp2_submit_goaway(session_, NGHTTP2_FLAG_NONE,
lastStreamID, code, data, len);
+ SendPendingData();
} |
Sorry, something went wrong.
If that is the case, we should probably wait for all outgoing writes to finish. There’s also this bit: node/lib/internal/http2/core.js Lines 1333 to 1339 in 179f423 Which seems invalid – there is no reason to believe that setImmediate() is giving us the right timing in terms of waiting for output data to be sent. So, yes, this looks like it’s definitely a bug, be it the one making the test flaky or not. |
Sorry, something went wrong.
|
I will try that in a free slot during the next days. |
Sorry, something went wrong.
|
I'm a little confused as to why that goaway call is needed there since HttpSession::Close does nghttp2_session_terminate_session. |
Sorry, something went wrong.
|
I could 100% reproduce the same error by making session.destroy method just call socket.destroy and read data slowly. node/lib/internal/http2/core.js Lines 1329 to 1331 in b798f64 I guess that Http2Session::SendPendingData can fail and it makes tests flaky. The codes to reproduce. 'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const http2 = require('http2');
const fs = require('fs');
const net = require('net');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const loc = fixtures.path('person-large.jpg');
const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
let sum = 0;
stream.pause();
const slowRead = ()=>{
setTimeout(()=>{
const data = stream.read(stream._readableState.highWaterMark/10);
sum += data ? data.length: 0;
console.log('read:' + sum + ' soc:' + socket.bytesWritten);
slowRead();
}, 10)
};
slowRead();
stream.respond();
stream.end();
}));
let socket;
server.listen(0, common.mustCall(() => {
const options = {
createConnection: (authority, options) => {
socket = net.connect(server.address().port, 'localhost');
return socket;
}
}
const client = http2.connect(`http://localhost:${server.address().port}`, options);
const req = client.request({ ':method': 'POST' });
req.on('response', common.mustCall());
req.resume();
req.on('close', common.mustCall(() => {
server.close();
client.close();
}));
const origin_destroy = client.destroy;
client.destroy = (err)=>{
console.log('session destroy called');
setImmediate(()=>{
socket.destroy(); // this cause a serverside error because this destroy a socket before goaway frame send.
//origin_destroy.call(client, err);
});
}
const str = fs.createReadStream(loc);
str.on('end', common.mustCall());
str.pipe(req);
}));Edit: Fix fixtures path as #29889 (comment) Thanks @Trott |
Sorry, something went wrong.
If anyone else wants to copy/paste the replication above, change this line to use 'person-large.jpg' rather than 'large.pdf' if you don't have a large pdf file lying around to put in the fixtures directory. The replication code works (as in it shows the problem) on macOS. So if it's a proper replication of the issue, it demonstrates that the problem is cross-platform. |
Sorry, something went wrong.
|
@Trott That test no longer fails for me after #29889 (comment) |
Sorry, something went wrong.
Since "goaway" is kind of a meta message I don't think it's considered as a "pending" write in the stream. We've got SendPendingData in the cpp side of things but I don't see any way to get a callback that ensures that the "goaway" frame has been sent from the js side. Side note. I know very little about http2 core so please take everything I write with a grain of salt. |
Sorry, something went wrong.
|
I can reproduce it even if I add SendPendingData() in Http2Session::Goaway(). |
Sorry, something went wrong.
|
@addaleax Replacing the setImmediate above with a setTimeout(2000) avoids the problem; most likely not a fix for production 😁 |
Sorry, something went wrong.
|
Maybe, I found the difference between TCP with windows and TCP with Linux that causes the error on windows, when calls the sendPendingData().
So, on windows, SendPendingData() could fail when 1 or 2 error happens. however on Linux, SendPendingData() retries writing data till success if there is no critical error. |
Sorry, something went wrong.
|
@nodejs/libuv |
Sorry, something went wrong.
Both protocols use TCP and uv_try_write() + uv_write().
net.Socket.write() does not call uv_fs_write(), at least not on Node’s side. |
Sorry, something went wrong.
Ah, I missed the following condition. Lines 326 to 328 in 1447a79 |
Sorry, something went wrong.
|
I tried the approach laid out above, waiting for writes to finish, but it actually makes the test much more flaky: Diff in the folddiff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 95ce8bcdb4ed..23b40565b665 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1327,16 +1327,12 @@ class Http2Session extends EventEmitter {
const handle = this[kHandle];
// Destroy the handle if it exists at this point
- if (handle !== undefined)
+ if (handle !== undefined) {
+ handle.ondone = finishSessionDestroy.bind(null, this, error);
handle.destroy(code, socket.destroyed);
-
- // If the socket is alive, use setImmediate to destroy the session on the
- // next iteration of the event loop in order to give data time to transmit.
- // Otherwise, destroy immediately.
- if (!socket.destroyed)
- setImmediate(finishSessionDestroy, this, error);
- else
+ } else {
finishSessionDestroy(this, error);
+ }
}
// Closing the session will:
diff --git a/src/node_http2.cc b/src/node_http2.cc
index f3ef8363e4eb..1b347478cf47 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -764,6 +764,10 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
flags_ |= SESSION_STATE_CLOSED;
+ if (!(flags_ & SESSION_STATE_WRITE_IN_PROGRESS)) {
+ MakeCallback(env()->ondone_string(), 0, nullptr);
+ }
+
// If there are outstanding pings, those will need to be canceled, do
// so on the next iteration of the event loop to avoid calling out into
// javascript since this may be called during garbage collection.
@@ -1554,6 +1558,11 @@ void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
// Inform all pending writes about their completion.
ClearOutgoing(status);
+ if (flags_ & SESSION_STATE_CLOSED) {
+ MakeCallback(env()->ondone_string(), 0, nullptr);
+ return;
+ }
+
if ((flags_ & SESSION_STATE_READING_STOPPED) &&
!(flags_ & SESSION_STATE_WRITE_IN_PROGRESS) &&
nghttp2_session_want_read(session_)) {While I think that is the right thing to do, it makes me wonder…
|
Sorry, something went wrong.
I guess this is because of OnStreamAfterWrite() is called by asynchronous write which is queued before sending goaway frame. Are we supposed to check there is no pending data before callback? @@ -1554,6 +1558,11 @@ void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
// Inform all pending writes about their completion.
ClearOutgoing(status);
+ if (flags_ & SESSION_STATE_CLOSED) {
+ MakeCallback(env()->ondone_string(), 0, nullptr);
+ return;
+ }
+
|
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Wait for close event on server stream before shuting down server and
client to avoid races seen on windows CI.
Actually I'm not sure if this just hides a bug, see #20750 (comment) for more details.
I still haven't transformed into a http2/nghttp/net expert and noone else took this since I last commented so I decided to start a PR to at least help CI.
Refs: #20750 (comment)
Refs: #29852
Checklist