Skip to content

Commit e3bb358

Browse files
committed
quic: cleanly tear down QUIC after worker.terminate()
Signed-off-by: Tim Perry <pimterry@gmail.com>
1 parent fa95cd5 commit e3bb358

3 files changed

Lines changed: 58 additions & 0 deletions

File tree

src/quic/bindingdata.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,14 @@ BindingData::BindingData(Realm* realm, Local<Object> object)
353353
MakeWeak();
354354
// Unref so the check handle doesn't keep the event loop alive on its own.
355355
flush_check_.Unref();
356+
// Ensure Clean() below is called before the tearing anything down.
357+
env()->cleanable_queue()->PushFront(this);
358+
}
359+
360+
void BindingData::Clean() {
361+
// Make sure sessions are always properly destroyed. This does nothing in
362+
// a clean shutdown, but is required for cases like worker.terminate().
363+
if (session_manager_) session_manager_->DestroyAllSessions();
356364
}
357365

358366
SessionManager& BindingData::session_manager() {

src/quic/bindingdata.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ class CheckWrapHandle : public MemoryRetainer {
261261
// TODO(@jasnell): Make this snapshotable?
262262
class BindingData final
263263
: public BaseObject,
264+
public Cleanable,
264265
public mem::NgLibMemoryManager<BindingData, ngtcp2_mem> {
265266
public:
266267
SET_BINDING_ID(quic_binding_data)
@@ -392,6 +393,9 @@ class BindingData final
392393
bool flush_check_started_ = false;
393394

394395
void OnFlushCheck();
396+
397+
private:
398+
void Clean() override;
395399
};
396400

397401
JS_METHOD_IMPL(IllegalConstructor);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Flags: --experimental-quic --no-warnings
2+
3+
// Test: terminating a worker thread that still holds live QUIC sessions.
4+
//
5+
// worker.terminate() tears the environment down without running any of the
6+
// JavaScript close paths, so the sessions are still open when the QUIC
7+
// binding is cleaned up. Sessions must be properly destroyed before reaching
8+
// ~Session.
9+
10+
import { hasQuic, skip, mustCall } from '../common/index.mjs';
11+
import assert from 'node:assert';
12+
import { Worker, isMainThread, parentPort } from 'node:worker_threads';
13+
14+
if (!hasQuic) {
15+
skip('QUIC is not enabled');
16+
}
17+
18+
const { listen, connect } = await import('../common/quic.mjs');
19+
20+
// Launch a client and server in a worker thread, then kill it:
21+
if (!isMainThread) {
22+
// A client and a server session, both with an open stream, and neither
23+
// closed. The worker then parks forever waiting to be terminated.
24+
const serverEndpoint = await listen((session) => {
25+
session.closed.catch(() => {});
26+
session.onstream = (stream) => { stream.closed.catch(() => {}); };
27+
});
28+
29+
const clientSession = await connect(serverEndpoint.address);
30+
clientSession.closed.catch(() => {});
31+
await clientSession.opened;
32+
const stream = await clientSession.createBidirectionalStream({
33+
body: new Uint8Array(1),
34+
});
35+
stream.closed.catch(() => {});
36+
37+
parentPort.postMessage('ready');
38+
await new Promise(() => {});
39+
} else {
40+
const worker = new Worker(new URL(import.meta.url));
41+
worker.on('error', (err) => { assert.fail(err); });
42+
worker.on('message', mustCall(async (message) => {
43+
assert.strictEqual(message, 'ready');
44+
assert.strictEqual(await worker.terminate(), 1);
45+
}));
46+
}

0 commit comments

Comments
 (0)