GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — test: fix flaky test-http2-client-upload by Flarna · Pull Request #29889 · nodejs/node Conversation
Member
Wait for close event on server stream before shuting down server and Actually I'm not sure if this just hides a bug, see #20750 (comment) for more details. Refs: #20750 (comment) Checklist
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
addaleax
reviewed
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactionsUh oh!
There was an error while loading. Please reload this page.
1 of 7 tasks
Contributor
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
|
There was an error while loading. Please reload this page.
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Tried the change proposed by @ronag but it doesn't help. The |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
@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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@ronag tried it but it doesn't help. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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.
There was an error while loading. Please reload this page.
|
@nodejs/http2 The code in #29889 (comment) is from lib/http/core.js. Thoughts on @ronag's comment there? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@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.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I will try that in a free slot during the next days. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'm a little confused as to why that goaway call is needed there since |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
@Trott That test no longer fails for me after #29889 (comment) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I can reproduce it even if I add |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@addaleax Replacing the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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, |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@nodejs/libuv |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Both protocols use TCP and
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Ah, I missed the following condition. Lines 326 to 328 in 1447a79
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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.
There was an error while loading. Please reload this page.
I guess this is because of |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.