Skip to content

Commit 61696f1

Browse files
jasnelladdaleax
authored andcommitted
http2: fix socketOnTimeout and a segfault
Fixes: nodejs/http2#179 Was fixing issue #179 and encountered a segault that was happening somewhat randomly on session destruction. Both should be fixed Backport-PR-URL: #14813 Backport-Reviewed-By: Anna Henningsen <[email protected]> Backport-Reviewed-By: Timothy Gu <[email protected]> PR-URL: #14239 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 2620769 commit 61696f1

File tree

6 files changed

+83
-9
lines changed

6 files changed

+83
-9
lines changed

lib/internal/http2/core.js

+23-6
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ function finishSessionDestroy(socket) {
670670
debug(`[${sessionName(this[kType])}] nghttp2session handle destroyed`);
671671
}
672672

673-
this.emit('close');
673+
process.nextTick(emit.bind(this, 'close'));
674674
debug(`[${sessionName(this[kType])}] nghttp2session destroyed`);
675675
}
676676

@@ -953,6 +953,9 @@ class Http2Session extends EventEmitter {
953953
state.destroyed = true;
954954
state.destroying = false;
955955

956+
if (this[kHandle] !== undefined)
957+
this[kHandle].destroying();
958+
956959
setImmediate(finishSessionDestroy.bind(this, socket));
957960
}
958961

@@ -1985,6 +1988,7 @@ function socketDestroy(error) {
19851988
const type = this[kSession][kType];
19861989
debug(`[${sessionName(type)}] socket destroy called`);
19871990
delete this[kServer];
1991+
this.removeListener('timeout', socketOnTimeout);
19881992
// destroy the session first so that it will stop trying to
19891993
// send data while we close the socket.
19901994
this[kSession].destroy();
@@ -2046,14 +2050,18 @@ function socketOnError(error) {
20462050
this.destroy(error);
20472051
}
20482052

2049-
// When the socket times out, attempt a graceful shutdown
2050-
// of the session
2053+
// When the socket times out on the server, attempt a graceful shutdown
2054+
// of the session.
20512055
function socketOnTimeout() {
20522056
debug('socket timeout');
20532057
const server = this[kServer];
2054-
// server can be null if the socket is a client
2055-
if (server === undefined || !server.emit('timeout', this)) {
2056-
this[kSession].shutdown(
2058+
const session = this[kSession];
2059+
// If server or session are undefined, then we're already in the process of
2060+
// shutting down, do nothing.
2061+
if (server === undefined || session === undefined)
2062+
return;
2063+
if (!server.emit('timeout', session, this)) {
2064+
session.shutdown(
20572065
{
20582066
graceful: true,
20592067
errorCode: NGHTTP2_NO_ERROR
@@ -2105,6 +2113,7 @@ function connectionListener(socket) {
21052113
socket.on('resume', socketOnResume);
21062114
socket.on('pause', socketOnPause);
21072115
socket.on('drain', socketOnDrain);
2116+
socket.on('close', socketOnClose);
21082117

21092118
// Set up the Session
21102119
const session = new ServerHttp2Session(options, socket, this);
@@ -2197,6 +2206,13 @@ function setupCompat(ev) {
21972206
}
21982207
}
21992208

2209+
function socketOnClose(hadError) {
2210+
const session = this[kSession];
2211+
if (session !== undefined && !session.destroyed) {
2212+
session.destroy();
2213+
}
2214+
}
2215+
22002216
// If the session emits an error, forward it to the socket as a sessionError;
22012217
// failing that, destroy the session, remove the listener and re-emit the error
22022218
function clientSessionOnError(error) {
@@ -2244,6 +2260,7 @@ function connect(authority, options, listener) {
22442260
socket.on('resume', socketOnResume);
22452261
socket.on('pause', socketOnPause);
22462262
socket.on('drain', socketOnDrain);
2263+
socket.on('close', socketOnClose);
22472264

22482265
const session = new ClientHttp2Session(options, socket);
22492266

src/node_http2.cc

+12-2
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,21 @@ void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) {
401401
}
402402

403403
void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
404-
DEBUG_HTTP2("Http2Session: destroying session\n");
405404
Http2Session* session;
406405
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
406+
DEBUG_HTTP2("Http2Session: destroying session %d\n", session->type());
407407
session->Unconsume();
408408
session->Free();
409409
}
410410

411+
void Http2Session::Destroying(const FunctionCallbackInfo<Value>& args) {
412+
Http2Session* session;
413+
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
414+
DEBUG_HTTP2("Http2Session: preparing to destroy session %d\n",
415+
session->type());
416+
session->MarkDestroying();
417+
}
418+
411419
void Http2Session::SubmitPriority(const FunctionCallbackInfo<Value>& args) {
412420
Environment* env = Environment::GetCurrent(args);
413421
Http2Session* session;
@@ -816,11 +824,11 @@ void Http2Session::AllocateSend(size_t recommended, uv_buf_t* buf) {
816824
}
817825

818826
void Http2Session::Send(uv_buf_t* buf, size_t length) {
827+
DEBUG_HTTP2("Http2Session: Attempting to send data\n");
819828
if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) {
820829
return;
821830
}
822831
HandleScope scope(env()->isolate());
823-
824832
auto AfterWrite = [](WriteWrap* req_wrap, int status) {
825833
req_wrap->Dispose();
826834
};
@@ -1191,6 +1199,8 @@ void Initialize(Local<Object> target,
11911199
Http2Session::Consume);
11921200
env->SetProtoMethod(session, "destroy",
11931201
Http2Session::Destroy);
1202+
env->SetProtoMethod(session, "destroying",
1203+
Http2Session::Destroying);
11941204
env->SetProtoMethod(session, "sendHeaders",
11951205
Http2Session::SendHeaders);
11961206
env->SetProtoMethod(session, "submitShutdownNotice",

src/node_http2.h

+1
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ class Http2Session : public AsyncWrap,
429429
static void New(const FunctionCallbackInfo<Value>& args);
430430
static void Consume(const FunctionCallbackInfo<Value>& args);
431431
static void Unconsume(const FunctionCallbackInfo<Value>& args);
432+
static void Destroying(const FunctionCallbackInfo<Value>& args);
432433
static void Destroy(const FunctionCallbackInfo<Value>& args);
433434
static void SubmitSettings(const FunctionCallbackInfo<Value>& args);
434435
static void SubmitRstStream(const FunctionCallbackInfo<Value>& args);

src/node_http2_core-inl.h

+11-1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
136136

137137
// Prompts nghttp2 to flush the queue of pending data frames
138138
inline void Nghttp2Session::SendPendingData() {
139+
DEBUG_HTTP2("Nghttp2Session %d: Sending pending data\n", session_type_);
140+
// Do not attempt to send data on the socket if the destroying flag has
141+
// been set. That means everything is shutting down and the socket
142+
// will not be usable.
143+
if (IsDestroying())
144+
return;
139145
const uint8_t* data;
140146
ssize_t len = 0;
141147
size_t ncopy = 0;
@@ -167,6 +173,7 @@ inline int Nghttp2Session::Init(uv_loop_t* loop,
167173
DEBUG_HTTP2("Nghttp2Session %d: initializing session\n", type);
168174
loop_ = loop;
169175
session_type_ = type;
176+
destroying_ = false;
170177
int ret = 0;
171178

172179
nghttp2_session_callbacks* callbacks
@@ -211,6 +218,9 @@ inline int Nghttp2Session::Init(uv_loop_t* loop,
211218
return ret;
212219
}
213220

221+
inline void Nghttp2Session::MarkDestroying() {
222+
destroying_ = true;
223+
}
214224

215225
inline int Nghttp2Session::Free() {
216226
assert(session_ != nullptr);
@@ -224,11 +234,11 @@ inline int Nghttp2Session::Free() {
224234
session->OnFreeSession();
225235
};
226236
uv_close(reinterpret_cast<uv_handle_t*>(&prep_), PrepClose);
227-
228237
nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR);
229238
nghttp2_session_del(session_);
230239
session_ = nullptr;
231240
loop_ = nullptr;
241+
DEBUG_HTTP2("Nghttp2Session %d: session freed\n", session_type_);
232242
return 1;
233243
}
234244

src/node_http2_core.h

+9
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ class Nghttp2Session {
100100

101101
// Frees this session instance
102102
inline int Free();
103+
inline void MarkDestroying();
104+
bool IsDestroying() {
105+
return destroying_;
106+
}
103107

104108
// Returns the pointer to the identified stream, or nullptr if
105109
// the stream does not exist
@@ -128,6 +132,10 @@ class Nghttp2Session {
128132
// Returns the nghttp2 library session
129133
inline nghttp2_session* session() { return session_; }
130134

135+
nghttp2_session_type type() {
136+
return session_type_;
137+
}
138+
131139
protected:
132140
// Adds a stream instance to this session
133141
inline void AddStream(Nghttp2Stream* stream);
@@ -240,6 +248,7 @@ class Nghttp2Session {
240248
uv_prepare_t prep_;
241249
nghttp2_session_type session_type_;
242250
std::unordered_map<int32_t, Nghttp2Stream*> streams_;
251+
bool destroying_ = false;
243252

244253
friend class Nghttp2Stream;
245254
};
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Flags: --expose-http2
2+
'use strict';
3+
4+
const common = require('../common');
5+
const http2 = require('http2');
6+
7+
const server = http2.createServer();
8+
server.setTimeout(common.platformTimeout(1));
9+
10+
const onServerTimeout = common.mustCall((session) => {
11+
session.destroy();
12+
server.removeListener('timeout', onServerTimeout);
13+
});
14+
15+
server.on('stream', common.mustNotCall());
16+
server.on('timeout', onServerTimeout);
17+
18+
server.listen(0, common.mustCall(() => {
19+
const url = `http://localhost:${server.address().port}`;
20+
const client = http2.connect(url);
21+
client.on('close', common.mustCall(() => {
22+
23+
const client2 = http2.connect(url);
24+
client2.on('close', common.mustCall(() => server.close()));
25+
26+
}));
27+
}));

0 commit comments

Comments
 (0)