-
Notifications
You must be signed in to change notification settings - Fork 30.7k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
test: fix flaky test-http2-client-upload #29889
test: fix flaky test-http2-client-upload #29889
Conversation
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.
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.
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 |
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. |
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 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. |
Tried the change proposed by @ronag but it doesn't help. The |
@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 |
@ronag tried it but it doesn't help. |
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. |
@nodejs/http2 The code in #29889 (comment) is from lib/http/core.js. Thoughts on @ronag's comment there? |
@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();
} |
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 |
I will try that in a free slot during the next days. |
I'm a little confused as to why that goaway call is needed there since |
I could 100% reproduce the same error by making node/lib/internal/http2/core.js Lines 1329 to 1331 in b798f64
I guess that 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 |
If anyone else wants to copy/paste the replication above, change this line to use 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. |
@Trott That test no longer fails for me after #29889 (comment) |
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 Side note. I know very little about http2 core so please take everything I write with a grain of salt. |
I can reproduce it even if I add |
@addaleax Replacing the |
Maybe, I found the difference between TCP with windows and TCP with Linux that causes the error on windows, when calls the
So, on windows, |
@nodejs/libuv |
This comment has been minimized.
This comment has been minimized.
Both protocols use TCP and
|
Ah, I missed the following condition. Lines 326 to 328 in 1447a79
|
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…
|
I guess this is because of
|
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
make -j4 test
(UNIX), orvcbuild test
(Windows) passes