Skip to content

Commit b471188

Browse files
jasnellcjihrig
authored andcommitted
quic: implement QuicSocket Promise API, part 2
PR-URL: #34283 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 766320b commit b471188

File tree

4 files changed

+82
-101
lines changed

4 files changed

+82
-101
lines changed

doc/api/quic.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -1584,16 +1584,17 @@ with this `QuicSocket`.
15841584

15851585
Read-only.
15861586

1587-
#### quicsocket.close(\[callback\])
1587+
#### quicsocket.close()
15881588
<!-- YAML
15891589
added: REPLACEME
15901590
-->
15911591

1592-
* `callback` {Function}
1592+
* Returns: {Promise}
15931593

15941594
Gracefully closes the `QuicSocket`. Existing `QuicSession` instances will be
15951595
permitted to close naturally. New `QuicClientSession` and `QuicServerSession`
1596-
instances will not be allowed.
1596+
instances will not be allowed. The returns `Promise` will be resolved once
1597+
the `QuicSocket` is destroyed.
15971598

15981599
#### quicsocket.connect(\[options\])
15991600
<!-- YAML

lib/internal/quic/core.js

+64-87
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,7 @@ const kRejections = Symbol.for('nodejs.rejection');
252252
const kSocketUnbound = 0;
253253
const kSocketPending = 1;
254254
const kSocketBound = 2;
255-
const kSocketClosing = 3;
256-
const kSocketDestroyed = 4;
255+
const kSocketDestroyed = 3;
257256

258257
let diagnosticPacketLossWarned = false;
259258
let warnedVerifyHostnameIdentity = false;
@@ -939,6 +938,9 @@ class QuicSocket extends EventEmitter {
939938
alpn: undefined,
940939
bindPromise: undefined,
941940
client: undefined,
941+
closePromise: undefined,
942+
closePromiseResolve: undefined,
943+
closePromiseReject: undefined,
942944
defaultEncoding: undefined,
943945
endpoints: new Set(),
944946
highWaterMark: undefined,
@@ -1089,8 +1091,10 @@ class QuicSocket extends EventEmitter {
10891091
}
10901092

10911093
[kRemoveSession](session) {
1092-
this[kInternalState].sessions.delete(session);
1093-
this[kMaybeDestroy]();
1094+
const state = this[kInternalState];
1095+
state.sessions.delete(session);
1096+
if (this.closing && state.sessions.size === 0)
1097+
this.destroy();
10941098
}
10951099

10961100
[kMaybeBind](options) {
@@ -1191,37 +1195,6 @@ class QuicSocket extends EventEmitter {
11911195
});
11921196
}
11931197

1194-
[kEndpointClose](endpoint, error) {
1195-
const state = this[kInternalState];
1196-
state.endpoints.delete(endpoint);
1197-
process.nextTick(() => {
1198-
try {
1199-
this.emit('endpointClose', endpoint, error);
1200-
} catch (error) {
1201-
this.destroy(error);
1202-
}
1203-
});
1204-
1205-
// If there aren't any more endpoints, the QuicSession
1206-
// is no longer usable and needs to be destroyed.
1207-
if (state.endpoints.size === 0) {
1208-
if (!this.destroyed)
1209-
return this.destroy(error);
1210-
this[kDestroy](error);
1211-
}
1212-
}
1213-
1214-
// kMaybeDestroy is called one or more times after the close() method
1215-
// is called. The QuicSocket will be destroyed if there are no remaining
1216-
// open sessions.
1217-
[kMaybeDestroy]() {
1218-
if (this.closing && this[kInternalState].sessions.size === 0) {
1219-
this.destroy();
1220-
return true;
1221-
}
1222-
return false;
1223-
}
1224-
12251198
// Called by the C++ internals to notify when server busy status is toggled.
12261199
[kServerBusy]() {
12271200
const busy = this.serverBusy;
@@ -1419,6 +1392,26 @@ class QuicSocket extends EventEmitter {
14191392
return session;
14201393
}
14211394

1395+
[kEndpointClose](endpoint, error) {
1396+
const state = this[kInternalState];
1397+
state.endpoints.delete(endpoint);
1398+
process.nextTick(() => {
1399+
try {
1400+
this.emit('endpointClose', endpoint, error);
1401+
} catch (error) {
1402+
this.destroy(error);
1403+
}
1404+
});
1405+
1406+
// If there aren't any more endpoints, the QuicSession
1407+
// is no longer usable and needs to be destroyed.
1408+
if (state.endpoints.size === 0) {
1409+
if (!this.destroyed)
1410+
return this.destroy(error);
1411+
this[kDestroy](error);
1412+
}
1413+
}
1414+
14221415
// Initiate a Graceful Close of the QuicSocket.
14231416
// Existing QuicClientSession and QuicServerSession instances will be
14241417
// permitted to close naturally and gracefully on their own.
@@ -1427,80 +1420,57 @@ class QuicSocket extends EventEmitter {
14271420
// QuicClientSession or QuicServerSession instances, the QuicSocket
14281421
// will be immediately closed.
14291422
//
1430-
// If specified, the callback will be registered for once('close').
1423+
// Returns a Promise that will be resolved once the QuicSocket is
1424+
// destroyed.
14311425
//
14321426
// No additional QuicServerSession instances will be accepted from
14331427
// remote peers, and calls to connect() to create QuicClientSession
14341428
// instances will fail. The QuicSocket will be otherwise usable in
14351429
// every other way.
14361430
//
1437-
// Subsequent calls to close(callback) will register the close callback
1438-
// if one is defined but will otherwise do nothing.
1439-
//
14401431
// Once initiated, a graceful close cannot be canceled. The graceful
14411432
// close can be interupted, however, by abruptly destroying the
14421433
// QuicSocket using the destroy() method.
14431434
//
14441435
// If close() is called before the QuicSocket has been bound (before
14451436
// either connect() or listen() have been called, or the QuicSocket
1446-
// is still in the pending state, the callback is registered for the
1447-
// once('close') event (if specified) and the QuicSocket is destroyed
1437+
// is still in the pending state, the QuicSocket is destroyed
14481438
// immediately.
1449-
close(callback) {
1450-
const state = this[kInternalState];
1451-
if (this.destroyed)
1452-
throw new ERR_INVALID_STATE('QuicSocket is already destroyed');
1453-
1454-
// If a callback function is specified, it is registered as a
1455-
// handler for the once('close') event. If the close occurs
1456-
// immediately, the close event will be emitted as soon as the
1457-
// process.nextTick queue is processed. Otherwise, the close
1458-
// event will occur at some unspecified time in the near future.
1459-
if (callback) {
1460-
if (typeof callback !== 'function')
1461-
throw new ERR_INVALID_CALLBACK();
1462-
this.once('close', callback);
1463-
}
1464-
1465-
// If we are already closing, do nothing else and wait
1466-
// for the close event to be invoked.
1467-
if (state.state === kSocketClosing)
1468-
return;
1439+
close() {
1440+
return this[kInternalState].closePromise || this[kClose]();
1441+
}
14691442

1470-
// If the QuicSocket is otherwise not bound to the local
1471-
// port, destroy the QuicSocket immediately.
1472-
if (state.state !== kSocketBound) {
1473-
this.destroy();
1443+
[kClose]() {
1444+
if (this.destroyed) {
1445+
return Promise.reject(
1446+
new ERR_INVALID_STATE('QuicSocket is already destroyed'));
14741447
}
1475-
1476-
// Mark the QuicSocket as closing to prevent re-entry
1477-
state.state = kSocketClosing;
1478-
1479-
// Otherwise, gracefully close each QuicSession, with
1480-
// [kMaybeDestroy]() being called after each closes.
1481-
const maybeDestroy = this[kMaybeDestroy].bind(this);
1448+
const state = this[kInternalState];
1449+
const promise = deferredClosePromise(state);
14821450

14831451
// Tell the underlying QuicSocket C++ object to stop
14841452
// listening for new QuicServerSession connections.
14851453
// New initial connection packets for currently unknown
14861454
// DCID's will be ignored.
14871455
if (this[kHandle])
1488-
this[kInternalState].sharedState.serverListening = false;
1456+
state.sharedState.serverListening = false;
14891457

1490-
// If there are no sessions, calling maybeDestroy
1491-
// will immediately and synchronously destroy the
1492-
// QuicSocket.
1493-
if (maybeDestroy())
1494-
return;
1458+
// If the QuicSocket is otherwise not bound to the local
1459+
// port, or there are not active sessions, destroy the
1460+
// QuicSocket immediately and we're done.
1461+
if (state.state !== kSocketBound || state.sessions.size === 0) {
1462+
this.destroy();
1463+
return promise;
1464+
}
14951465

1496-
// If we got this far, there a QuicClientSession and
1497-
// QuicServerSession instances still, we need to trigger
1498-
// a graceful close for each of them. As each closes,
1499-
// they will call the kMaybeDestroy function. When there
1500-
// are no remaining session instances, the QuicSocket
1501-
// will be closed and destroyed.
1466+
// Otherwise, loop through each of the known sessions
1467+
// and close them.
1468+
// TODO(@jasnell): These will be promises soon, but we
1469+
// do not want to await them.
15021470
for (const session of state.sessions)
1503-
session.close(maybeDestroy);
1471+
session.close();
1472+
1473+
return promise;
15041474
}
15051475

15061476
// Initiate an abrupt close and destruction of the QuicSocket.
@@ -1546,7 +1516,14 @@ class QuicSocket extends EventEmitter {
15461516
}
15471517

15481518
[kDestroy](error) {
1549-
if (error) process.nextTick(emit.bind(this, 'error', error));
1519+
const state = this[kInternalState];
1520+
if (error) {
1521+
if (typeof state.closePromiseReject === 'function')
1522+
state.closePromiseReject(error);
1523+
process.nextTick(emit.bind(this, 'error', error));
1524+
} else if (typeof state.closePromiseResolve === 'function') {
1525+
state.closePromiseResolve();
1526+
}
15501527
process.nextTick(emit.bind(this, 'close'));
15511528
}
15521529

@@ -1587,7 +1564,7 @@ class QuicSocket extends EventEmitter {
15871564

15881565
// True if graceful close has been initiated by calling close()
15891566
get closing() {
1590-
return this[kInternalState].state === kSocketClosing;
1567+
return this[kInternalState].closePromise !== undefined;
15911568
}
15921569

15931570
// True if the QuicSocket has been destroyed and is no longer usable

test/parallel/test-quic-quicsession-server-destroy-early.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,17 @@ const server = createQuicSocket({ server: options });
2323

2424
(async function() {
2525
server.on('session', common.mustCall((session) => {
26-
session.on('close', common.mustCall(() => {
27-
client.close();
28-
server.close();
29-
assert.throws(() => server.close(), {
26+
session.on('stream', common.mustNotCall());
27+
session.on('close', common.mustCall(async () => {
28+
await Promise.all([
29+
client.close(),
30+
server.close()
31+
]);
32+
assert.rejects(server.close(), {
3033
code: 'ERR_INVALID_STATE',
3134
name: 'Error'
3235
});
3336
}));
34-
session.on('stream', common.mustNotCall());
3537
session.destroy();
3638
}));
3739

test/parallel/test-quic-quicsocket-close.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ if (!common.hasQuic)
88
const assert = require('assert');
99
const { createQuicSocket } = require('net');
1010

11-
{
12-
const socket = createQuicSocket();
13-
socket.close(common.mustCall());
14-
socket.on('close', common.mustCall());
15-
assert.throws(() => socket.close(), {
11+
const socket = createQuicSocket();
12+
socket.on('close', common.mustCall());
13+
14+
(async function() {
15+
await socket.close();
16+
assert.rejects(() => socket.close(), {
1617
code: 'ERR_INVALID_STATE'
1718
});
18-
}
19+
})().then(common.mustCall());

0 commit comments

Comments
 (0)