Skip to content

Commit 723d1af

Browse files
jasnelladdaleax
authored andcommitted
http2: fix flakiness in timeout
Backport-PR-URL: #14813 Backport-Reviewed-By: Anna Henningsen <anna@addaleax.net> Backport-Reviewed-By: Timothy Gu <timothygu99@gmail.com> PR-URL: #14239 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 6a30448 commit 723d1af

File tree

4 files changed

+47
-29
lines changed

4 files changed

+47
-29
lines changed

lib/internal/http2/core.js

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -673,15 +673,23 @@ function submitShutdown(options) {
673673
}
674674

675675
function finishSessionDestroy(socket) {
676+
const state = this[kState];
677+
if (state.destroyed)
678+
return;
679+
676680
if (!socket.destroyed)
677681
socket.destroy();
678682

683+
state.destroying = false;
684+
state.destroyed = true;
685+
679686
// Destroy the handle
680687
const handle = this[kHandle];
681688
if (handle !== undefined) {
682-
handle.destroy();
689+
handle.destroy(state.skipUnconsume);
683690
debug(`[${sessionName(this[kType])}] nghttp2session handle destroyed`);
684691
}
692+
this[kHandle] = undefined;
685693

686694
process.nextTick(emit.bind(this, 'close'));
687695
debug(`[${sessionName(this[kType])}] nghttp2session destroyed`);
@@ -825,7 +833,7 @@ class Http2Session extends EventEmitter {
825833

826834
// Submits a SETTINGS frame to be sent to the remote peer.
827835
settings(settings) {
828-
if (this[kState].destroyed)
836+
if (this[kState].destroyed || this[kState].destroying)
829837
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
830838

831839
// Validate the input first
@@ -871,7 +879,7 @@ class Http2Session extends EventEmitter {
871879

872880
// Submits a PRIORITY frame to be sent to the remote peer.
873881
priority(stream, options) {
874-
if (this[kState].destroyed)
882+
if (this[kState].destroyed || this[kState].destroying)
875883
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
876884

877885
if (!(stream instanceof Http2Stream)) {
@@ -905,6 +913,8 @@ class Http2Session extends EventEmitter {
905913
// Submits an RST-STREAM frame to be sent to the remote peer. This will
906914
// cause the stream to be closed.
907915
rstStream(stream, code = NGHTTP2_NO_ERROR) {
916+
// Do not check destroying here, as the rstStream may be sent while
917+
// the session is in the middle of being destroyed.
908918
if (this[kState].destroyed)
909919
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
910920

@@ -946,7 +956,6 @@ class Http2Session extends EventEmitter {
946956
const state = this[kState];
947957
if (state.destroyed || state.destroying)
948958
return;
949-
950959
debug(`[${sessionName(this[kType])}] destroying nghttp2session`);
951960
state.destroying = true;
952961

@@ -963,8 +972,8 @@ class Http2Session extends EventEmitter {
963972
delete this[kSocket];
964973
delete this[kServer];
965974

966-
state.destroyed = true;
967-
state.destroying = false;
975+
state.destroyed = false;
976+
state.destroying = true;
968977

969978
if (this[kHandle] !== undefined)
970979
this[kHandle].destroying();
@@ -975,7 +984,7 @@ class Http2Session extends EventEmitter {
975984
// Graceful or immediate shutdown of the Http2Session. Graceful shutdown
976985
// is only supported on the server-side
977986
shutdown(options, callback) {
978-
if (this[kState].destroyed)
987+
if (this[kState].destroyed || this[kState].destroying)
979988
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
980989

981990
if (this[kState].shutdown || this[kState].shuttingDown)
@@ -1037,7 +1046,7 @@ class Http2Session extends EventEmitter {
10371046
}
10381047

10391048
_onTimeout() {
1040-
this.emit('timeout');
1049+
process.nextTick(emit.bind(this, 'timeout'));
10411050
}
10421051
}
10431052

@@ -1061,7 +1070,7 @@ class ClientHttp2Session extends Http2Session {
10611070
// Submits a new HTTP2 request to the connected peer. Returns the
10621071
// associated Http2Stream instance.
10631072
request(headers, options) {
1064-
if (this[kState].destroyed)
1073+
if (this[kState].destroyed || this[kState].destroying)
10651074
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
10661075
debug(`[${sessionName(this[kType])}] initiating request`);
10671076
_unrefActive(this);
@@ -1317,7 +1326,7 @@ class Http2Stream extends Duplex {
13171326
}
13181327

13191328
_onTimeout() {
1320-
this.emit('timeout');
1329+
process.nextTick(emit.bind(this, 'timeout'));
13211330
}
13221331

13231332
// true if the Http2Stream was aborted abornomally.
@@ -2104,7 +2113,7 @@ const onTimeout = {
21042113
configurable: false,
21052114
enumerable: false,
21062115
value: function() {
2107-
this.emit('timeout');
2116+
process.nextTick(emit.bind(this, 'timeout'));
21082117
}
21092118
};
21102119

@@ -2191,20 +2200,22 @@ function socketOnError(error) {
21912200
// of the session.
21922201
function socketOnTimeout() {
21932202
debug('socket timeout');
2194-
const server = this[kServer];
2195-
const session = this[kSession];
2196-
// If server or session are undefined, then we're already in the process of
2197-
// shutting down, do nothing.
2198-
if (server === undefined || session === undefined)
2199-
return;
2200-
if (!server.emit('timeout', session, this)) {
2201-
session.shutdown(
2202-
{
2203-
graceful: true,
2204-
errorCode: NGHTTP2_NO_ERROR
2205-
},
2206-
this.destroy.bind(this));
2207-
}
2203+
process.nextTick(() => {
2204+
const server = this[kServer];
2205+
const session = this[kSession];
2206+
// If server or session are undefined, then we're already in the process of
2207+
// shutting down, do nothing.
2208+
if (server === undefined || session === undefined)
2209+
return;
2210+
if (!server.emit('timeout', session, this)) {
2211+
session.shutdown(
2212+
{
2213+
graceful: true,
2214+
errorCode: NGHTTP2_NO_ERROR
2215+
},
2216+
this.destroy.bind(this));
2217+
}
2218+
});
22082219
}
22092220

22102221
// Handles the on('stream') event for a session and forwards
@@ -2346,6 +2357,8 @@ function setupCompat(ev) {
23462357
function socketOnClose(hadError) {
23472358
const session = this[kSession];
23482359
if (session !== undefined && !session.destroyed) {
2360+
// Skip unconsume because the socket is destroyed.
2361+
session[kState].skipUnconsume = true;
23492362
session.destroy();
23502363
}
23512364
}

src/node_http2.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,13 @@ void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
407407
Http2Session* session;
408408
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
409409
DEBUG_HTTP2("Http2Session: destroying session %d\n", session->type());
410-
session->Unconsume();
410+
Environment* env = Environment::GetCurrent(args);
411+
Local<Context> context = env->context();
412+
413+
bool skipUnconsume = args[0]->BooleanValue(context).ToChecked();
414+
415+
if (!skipUnconsume)
416+
session->Unconsume();
411417
session->Free();
412418
}
413419

src/node_http2_core.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ void Nghttp2Session::GetTrailers(nghttp2_session* session,
176176
handle->OnTrailers(stream, &trailers);
177177
if (trailers.length() > 0) {
178178
DEBUG_HTTP2("Nghttp2Session %d: sending trailers for stream %d, "
179-
"count: %d\n", handle->session_type_, id,
179+
"count: %d\n", handle->session_type_, stream->id(),
180180
trailers.length());
181181
*flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
182182
nghttp2_submit_trailer(session,

test/parallel/test-http2-server-timeout.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ server.setTimeout(common.platformTimeout(1));
99

1010
const onServerTimeout = common.mustCall((session) => {
1111
session.destroy();
12-
server.removeListener('timeout', onServerTimeout);
1312
});
1413

1514
server.on('stream', common.mustNotCall());
16-
server.on('timeout', onServerTimeout);
15+
server.once('timeout', onServerTimeout);
1716

1817
server.listen(0, common.mustCall(() => {
1918
const url = `http://localhost:${server.address().port}`;

0 commit comments

Comments
 (0)