-
Notifications
You must be signed in to change notification settings - Fork 30k
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
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