diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 6342b5e1..0d92881b 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -91,6 +91,7 @@ export class Server { string, UntypedHandler >(); + private sessions = new Set(); private started = false; constructor(options?: object) {} @@ -223,7 +224,22 @@ export class Server { } forceShutdown(): void { - throw new Error('Not yet implemented'); + // Close the server if it is still running. + if (this.http2Server && this.http2Server.listening) { + this.http2Server.close(); + } + + this.started = false; + + // Always destroy any available sessions. It's possible that one or more + // tryShutdown() calls are in progress. Don't wait on them to finish. + this.sessions.forEach(session => { + // Cast NGHTTP2_CANCEL to any because TypeScript doesn't seem to + // recognize destroy(code) as a valid signature. + // tslint:disable-next-line:no-any + session.destroy(http2.constants.NGHTTP2_CANCEL as any); + }); + this.sessions.clear(); } register( @@ -259,17 +275,34 @@ export class Server { } tryShutdown(callback: (error?: Error) => void): void { - callback = typeof callback === 'function' ? callback : noop; + let pendingChecks = 0; - if (this.http2Server === null) { - callback(new Error('server is not running')); - return; + function maybeCallback(): void { + pendingChecks--; + + if (pendingChecks === 0) { + callback(); + } } - this.http2Server.close((err?: Error) => { - this.started = false; - callback(err); + // Close the server if necessary. + this.started = false; + + if (this.http2Server && this.http2Server.listening) { + pendingChecks++; + this.http2Server.close(maybeCallback); + } + + // If any sessions are active, close them gracefully. + pendingChecks += this.sessions.size; + this.sessions.forEach(session => { + session.close(maybeCallback); }); + + // If the server is closed and there are no active sessions, just call back. + if (pendingChecks === 0) { + callback(); + } } addHttp2Port(): void { @@ -352,6 +385,8 @@ export class Server { session.destroy(); return; } + + this.sessions.add(session); }); } } diff --git a/packages/grpc-js/test/test-server-errors.ts b/packages/grpc-js/test/test-server-errors.ts index d429330d..ffabaac5 100644 --- a/packages/grpc-js/test/test-server-errors.ts +++ b/packages/grpc-js/test/test-server-errors.ts @@ -386,9 +386,9 @@ describe('Other conditions', () => { }); }); - after(done => { + after(() => { client.close(); - server.tryShutdown(done); + server.forceShutdown(); }); describe('Server receiving bad input', () => { diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index 09259481..3e02c3d5 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -127,17 +127,6 @@ describe('Server', () => { }); }); - describe('tryShutdown', () => { - it('calls back with an error if the server is not running', done => { - const server = new Server(); - - server.tryShutdown(err => { - assert(err !== undefined && err.message === 'server is not running'); - done(); - }); - }); - }); - describe('start', () => { let server: Server; @@ -237,10 +226,6 @@ describe('Server', () => { server.addProtoService(); }, /Not implemented. Use addService\(\) instead/); - assert.throws(() => { - server.forceShutdown(); - }, /Not yet implemented/); - assert.throws(() => { server.addHttp2Port(); }, /Not yet implemented/);