quic: implement QuicSocket Promise API, part 2

PR-URL: https://github.com/nodejs/node/pull/34283
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
James M Snell 2020-07-10 12:38:04 -07:00
parent 79c0e892dd
commit 6665dda9f6
4 changed files with 83 additions and 102 deletions

View File

@ -1584,16 +1584,17 @@ with this `QuicSocket`.
Read-only.
#### quicsocket.close(\[callback\])
#### quicsocket.close()
<!-- YAML
added: REPLACEME
-->
* `callback` {Function}
* Returns: {Promise}
Gracefully closes the `QuicSocket`. Existing `QuicSession` instances will be
permitted to close naturally. New `QuicClientSession` and `QuicServerSession`
instances will not be allowed.
instances will not be allowed. The returns `Promise` will be resolved once
the `QuicSocket` is destroyed.
#### quicsocket.connect(\[options\])
<!-- YAML

View File

@ -252,8 +252,7 @@ const kRejections = Symbol.for('nodejs.rejection');
const kSocketUnbound = 0;
const kSocketPending = 1;
const kSocketBound = 2;
const kSocketClosing = 3;
const kSocketDestroyed = 4;
const kSocketDestroyed = 3;
let diagnosticPacketLossWarned = false;
let warnedVerifyHostnameIdentity = false;
@ -939,6 +938,9 @@ class QuicSocket extends EventEmitter {
alpn: undefined,
bindPromise: undefined,
client: undefined,
closePromise: undefined,
closePromiseResolve: undefined,
closePromiseReject: undefined,
defaultEncoding: undefined,
endpoints: new Set(),
highWaterMark: undefined,
@ -1089,8 +1091,10 @@ class QuicSocket extends EventEmitter {
}
[kRemoveSession](session) {
this[kInternalState].sessions.delete(session);
this[kMaybeDestroy]();
const state = this[kInternalState];
state.sessions.delete(session);
if (this.closing && state.sessions.size === 0)
this.destroy();
}
[kMaybeBind](options) {
@ -1191,37 +1195,6 @@ class QuicSocket extends EventEmitter {
});
}
[kEndpointClose](endpoint, error) {
const state = this[kInternalState];
state.endpoints.delete(endpoint);
process.nextTick(() => {
try {
this.emit('endpointClose', endpoint, error);
} catch (error) {
this.destroy(error);
}
});
// If there aren't any more endpoints, the QuicSession
// is no longer usable and needs to be destroyed.
if (state.endpoints.size === 0) {
if (!this.destroyed)
return this.destroy(error);
this[kDestroy](error);
}
}
// kMaybeDestroy is called one or more times after the close() method
// is called. The QuicSocket will be destroyed if there are no remaining
// open sessions.
[kMaybeDestroy]() {
if (this.closing && this[kInternalState].sessions.size === 0) {
this.destroy();
return true;
}
return false;
}
// Called by the C++ internals to notify when server busy status is toggled.
[kServerBusy]() {
const busy = this.serverBusy;
@ -1419,6 +1392,26 @@ class QuicSocket extends EventEmitter {
return session;
}
[kEndpointClose](endpoint, error) {
const state = this[kInternalState];
state.endpoints.delete(endpoint);
process.nextTick(() => {
try {
this.emit('endpointClose', endpoint, error);
} catch (error) {
this.destroy(error);
}
});
// If there aren't any more endpoints, the QuicSession
// is no longer usable and needs to be destroyed.
if (state.endpoints.size === 0) {
if (!this.destroyed)
return this.destroy(error);
this[kDestroy](error);
}
}
// Initiate a Graceful Close of the QuicSocket.
// Existing QuicClientSession and QuicServerSession instances will be
// permitted to close naturally and gracefully on their own.
@ -1427,80 +1420,57 @@ class QuicSocket extends EventEmitter {
// QuicClientSession or QuicServerSession instances, the QuicSocket
// will be immediately closed.
//
// If specified, the callback will be registered for once('close').
// Returns a Promise that will be resolved once the QuicSocket is
// destroyed.
//
// No additional QuicServerSession instances will be accepted from
// remote peers, and calls to connect() to create QuicClientSession
// instances will fail. The QuicSocket will be otherwise usable in
// every other way.
//
// Subsequent calls to close(callback) will register the close callback
// if one is defined but will otherwise do nothing.
//
// Once initiated, a graceful close cannot be canceled. The graceful
// close can be interupted, however, by abruptly destroying the
// QuicSocket using the destroy() method.
//
// If close() is called before the QuicSocket has been bound (before
// either connect() or listen() have been called, or the QuicSocket
// is still in the pending state, the callback is registered for the
// once('close') event (if specified) and the QuicSocket is destroyed
// is still in the pending state, the QuicSocket is destroyed
// immediately.
close(callback) {
close() {
return this[kInternalState].closePromise || this[kClose]();
}
[kClose]() {
if (this.destroyed) {
return Promise.reject(
new ERR_INVALID_STATE('QuicSocket is already destroyed'));
}
const state = this[kInternalState];
if (this.destroyed)
throw new ERR_INVALID_STATE('QuicSocket is already destroyed');
// If a callback function is specified, it is registered as a
// handler for the once('close') event. If the close occurs
// immediately, the close event will be emitted as soon as the
// process.nextTick queue is processed. Otherwise, the close
// event will occur at some unspecified time in the near future.
if (callback) {
if (typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();
this.once('close', callback);
}
// If we are already closing, do nothing else and wait
// for the close event to be invoked.
if (state.state === kSocketClosing)
return;
// If the QuicSocket is otherwise not bound to the local
// port, destroy the QuicSocket immediately.
if (state.state !== kSocketBound) {
this.destroy();
}
// Mark the QuicSocket as closing to prevent re-entry
state.state = kSocketClosing;
// Otherwise, gracefully close each QuicSession, with
// [kMaybeDestroy]() being called after each closes.
const maybeDestroy = this[kMaybeDestroy].bind(this);
const promise = deferredClosePromise(state);
// Tell the underlying QuicSocket C++ object to stop
// listening for new QuicServerSession connections.
// New initial connection packets for currently unknown
// DCID's will be ignored.
if (this[kHandle])
this[kInternalState].sharedState.serverListening = false;
state.sharedState.serverListening = false;
// If there are no sessions, calling maybeDestroy
// will immediately and synchronously destroy the
// QuicSocket.
if (maybeDestroy())
return;
// If the QuicSocket is otherwise not bound to the local
// port, or there are not active sessions, destroy the
// QuicSocket immediately and we're done.
if (state.state !== kSocketBound || state.sessions.size === 0) {
this.destroy();
return promise;
}
// If we got this far, there a QuicClientSession and
// QuicServerSession instances still, we need to trigger
// a graceful close for each of them. As each closes,
// they will call the kMaybeDestroy function. When there
// are no remaining session instances, the QuicSocket
// will be closed and destroyed.
// Otherwise, loop through each of the known sessions
// and close them.
// TODO(@jasnell): These will be promises soon, but we
// do not want to await them.
for (const session of state.sessions)
session.close(maybeDestroy);
session.close();
return promise;
}
// Initiate an abrupt close and destruction of the QuicSocket.
@ -1546,7 +1516,14 @@ class QuicSocket extends EventEmitter {
}
[kDestroy](error) {
if (error) process.nextTick(emit.bind(this, 'error', error));
const state = this[kInternalState];
if (error) {
if (typeof state.closePromiseReject === 'function')
state.closePromiseReject(error);
process.nextTick(emit.bind(this, 'error', error));
} else if (typeof state.closePromiseResolve === 'function') {
state.closePromiseResolve();
}
process.nextTick(emit.bind(this, 'close'));
}
@ -1587,7 +1564,7 @@ class QuicSocket extends EventEmitter {
// True if graceful close has been initiated by calling close()
get closing() {
return this[kInternalState].state === kSocketClosing;
return this[kInternalState].closePromise !== undefined;
}
// True if the QuicSocket has been destroyed and is no longer usable

View File

@ -23,15 +23,17 @@ const server = createQuicSocket({ server: options });
(async function() {
server.on('session', common.mustCall((session) => {
session.on('close', common.mustCall(() => {
client.close();
server.close();
assert.throws(() => server.close(), {
session.on('stream', common.mustNotCall());
session.on('close', common.mustCall(async () => {
await Promise.all([
client.close(),
server.close()
]);
assert.rejects(server.close(), {
code: 'ERR_INVALID_STATE',
name: 'Error'
});
}));
session.on('stream', common.mustNotCall());
session.destroy();
}));

View File

@ -8,11 +8,12 @@ if (!common.hasQuic)
const assert = require('assert');
const { createQuicSocket } = require('net');
{
const socket = createQuicSocket();
socket.close(common.mustCall());
socket.on('close', common.mustCall());
assert.throws(() => socket.close(), {
const socket = createQuicSocket();
socket.on('close', common.mustCall());
(async function() {
await socket.close();
assert.rejects(() => socket.close(), {
code: 'ERR_INVALID_STATE'
});
}
})().then(common.mustCall());