Skip to content

Commit 93a4cf6

Browse files
apapirovskiMylesBorins
authored andcommitted
http2: use session not socket timeout, tests
Change default timeout to be tracked on the session instead of the socket, as nghttp2 manages the socket and we would need to maintain two sets of timeouts for similar purpose. Also fixes session setTimeout to work as it wasn't getting _unrefActive correctly (was called on the handle). Fixes: #15158 PR-URL: #15188 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 764213c commit 93a4cf6

File tree

2 files changed

+82
-41
lines changed

2 files changed

+82
-41
lines changed

lib/internal/http2/core.js

+41-41
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ function emit() {
150150
// event. If the stream is not new, emit the 'headers' event to pass
151151
// the block of headers on.
152152
function onSessionHeaders(id, cat, flags, headers) {
153-
_unrefActive(this);
154153
const owner = this[kOwner];
154+
_unrefActive(owner);
155155
debug(`[${sessionName(owner[kType])}] headers were received on ` +
156156
`stream ${id}: ${cat}`);
157157
const streams = owner[kState].streams;
@@ -265,7 +265,7 @@ function onSessionStreamClose(id, code) {
265265
const stream = owner[kState].streams.get(id);
266266
if (stream === undefined)
267267
return;
268-
_unrefActive(this);
268+
_unrefActive(owner);
269269
// Set the rst state for the stream
270270
abort(stream);
271271
const state = stream[kState];
@@ -287,14 +287,16 @@ function afterFDClose(err) {
287287

288288
// Called when an error event needs to be triggered
289289
function onSessionError(error) {
290-
_unrefActive(this);
291-
process.nextTick(() => this[kOwner].emit('error', error));
290+
const owner = this[kOwner];
291+
_unrefActive(owner);
292+
process.nextTick(() => owner.emit('error', error));
292293
}
293294

294295
// Receives a chunk of data for a given stream and forwards it on
295296
// to the Http2Stream Duplex for processing.
296297
function onSessionRead(nread, buf, handle) {
297-
const streams = this[kOwner][kState].streams;
298+
const owner = this[kOwner];
299+
const streams = owner[kState].streams;
298300
const id = handle.id;
299301
const stream = streams.get(id);
300302
// It should not be possible for the stream to not exist at this point.
@@ -303,7 +305,7 @@ function onSessionRead(nread, buf, handle) {
303305
'Internal HTTP/2 Failure. Stream does not exist. Please ' +
304306
'report this as a bug in Node.js');
305307
const state = stream[kState];
306-
_unrefActive(this); // Reset the session timeout timer
308+
_unrefActive(owner); // Reset the session timeout timer
307309
_unrefActive(stream); // Reset the stream timeout timer
308310

309311
if (nread >= 0 && !stream.destroyed) {
@@ -322,7 +324,7 @@ function onSessionRead(nread, buf, handle) {
322324
function onSettings(ack) {
323325
const owner = this[kOwner];
324326
debug(`[${sessionName(owner[kType])}] new settings received`);
325-
_unrefActive(this);
327+
_unrefActive(owner);
326328
let event = 'remoteSettings';
327329
if (ack) {
328330
if (owner[kState].pendingAck > 0)
@@ -348,7 +350,7 @@ function onPriority(id, parent, weight, exclusive) {
348350
debug(`[${sessionName(owner[kType])}] priority advisement for stream ` +
349351
`${id}: \n parent: ${parent},\n weight: ${weight},\n` +
350352
` exclusive: ${exclusive}`);
351-
_unrefActive(this);
353+
_unrefActive(owner);
352354
const streams = owner[kState].streams;
353355
const stream = streams.get(id);
354356
const emitter = stream === undefined ? owner : stream;
@@ -370,7 +372,7 @@ function onFrameError(id, type, code) {
370372
const owner = this[kOwner];
371373
debug(`[${sessionName(owner[kType])}] error sending frame type ` +
372374
`${type} on stream ${id}, code: ${code}`);
373-
_unrefActive(this);
375+
_unrefActive(owner);
374376
const streams = owner[kState].streams;
375377
const stream = streams.get(id);
376378
const emitter = stream !== undefined ? stream : owner;
@@ -975,7 +977,7 @@ class Http2Session extends EventEmitter {
975977
state.destroying = true;
976978

977979
// Unenroll the timer
978-
this.setTimeout(0);
980+
this.setTimeout(0, sessionOnTimeout);
979981

980982
// Shut down any still open streams
981983
const streams = state.streams;
@@ -2185,7 +2187,6 @@ function socketDestroy(error) {
21852187
const type = this[kSession][kType];
21862188
debug(`[${sessionName(type)}] socket destroy called`);
21872189
delete this[kServer];
2188-
this.setTimeout(0);
21892190
// destroy the session first so that it will stop trying to
21902191
// send data while we close the socket.
21912192
this[kSession].destroy();
@@ -2247,31 +2248,6 @@ function socketOnError(error) {
22472248
this.destroy(error);
22482249
}
22492250

2250-
// When the socket times out on the server, attempt a graceful shutdown
2251-
// of the session.
2252-
function socketOnTimeout() {
2253-
debug('socket timeout');
2254-
process.nextTick(() => {
2255-
const server = this[kServer];
2256-
const session = this[kSession];
2257-
// If server or session are undefined, or session.destroyed is true
2258-
// then we're already in the process of shutting down, do nothing.
2259-
if (server === undefined || session === undefined)
2260-
return;
2261-
const state = session[kState];
2262-
if (state.destroyed || state.destroying)
2263-
return;
2264-
if (!server.emit('timeout', session, this)) {
2265-
session.shutdown(
2266-
{
2267-
graceful: true,
2268-
errorCode: NGHTTP2_NO_ERROR
2269-
},
2270-
this.destroy.bind(this));
2271-
}
2272-
});
2273-
}
2274-
22752251
// Handles the on('stream') event for a session and forwards
22762252
// it on to the server object.
22772253
function sessionOnStream(stream, headers, flags, rawHeaders) {
@@ -2289,15 +2265,34 @@ function sessionOnSocketError(error, socket) {
22892265
this[kServer].emit('socketError', error, socket, this);
22902266
}
22912267

2268+
// When the session times out on the server, attempt a graceful shutdown
2269+
function sessionOnTimeout() {
2270+
debug('session timeout');
2271+
process.nextTick(() => {
2272+
// if destroyed or destryoing, do nothing
2273+
if (this[kState].destroyed || this[kState].destroying)
2274+
return;
2275+
const server = this[kServer];
2276+
const socket = this[kSocket];
2277+
// If server or socket are undefined, then we're already in the process of
2278+
// shutting down, do nothing.
2279+
if (server === undefined || socket === undefined)
2280+
return;
2281+
if (!server.emit('timeout', this)) {
2282+
this.shutdown(
2283+
{
2284+
graceful: true,
2285+
errorCode: NGHTTP2_NO_ERROR
2286+
},
2287+
socket.destroy.bind(socket));
2288+
}
2289+
});
2290+
}
2291+
22922292
function connectionListener(socket) {
22932293
debug('[server] received a connection');
22942294
const options = this[kOptions] || {};
22952295

2296-
if (this.timeout) {
2297-
socket.setTimeout(this.timeout);
2298-
socket.on('timeout', socketOnTimeout);
2299-
}
2300-
23012296
if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
23022297
if (options.allowHTTP1 === true) {
23032298
// Fallback to HTTP/1.1
@@ -2325,6 +2320,11 @@ function connectionListener(socket) {
23252320
session.on('priority', sessionOnPriority);
23262321
session.on('socketError', sessionOnSocketError);
23272322

2323+
if (this.timeout) {
2324+
session.setTimeout(this.timeout);
2325+
session.on('timeout', sessionOnTimeout);
2326+
}
2327+
23282328
socket[kServer] = this;
23292329

23302330
process.nextTick(emit.bind(this, 'session', session));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Flags: --expose-http2
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
const h2 = require('http2');
8+
9+
const serverTimeout = common.platformTimeout(200);
10+
const callTimeout = common.platformTimeout(10);
11+
12+
const server = h2.createServer();
13+
server.timeout = serverTimeout;
14+
15+
server.on('request', (req, res) => res.end());
16+
server.on('timeout', common.mustNotCall());
17+
18+
server.listen(0, common.mustCall(() => {
19+
const port = server.address().port;
20+
21+
const url = `http://localhost:${port}`;
22+
const client = h2.connect(url);
23+
makeReq(40);
24+
25+
function makeReq(attempts) {
26+
const request = client.request({
27+
':path': '/foobar',
28+
':method': 'GET',
29+
':scheme': 'http',
30+
':authority': `localhost:${port}`,
31+
});
32+
request.end();
33+
34+
if (attempts) {
35+
setTimeout(() => makeReq(attempts - 1), callTimeout);
36+
} else {
37+
server.close();
38+
client.destroy();
39+
}
40+
}
41+
}));

0 commit comments

Comments
 (0)