Skip to content

Commit ee77696

Browse files
committed
apollo-server-core: register signal handlers later and not on serverless
`stop()` is not designed to work properly if `start()` has not previously succeeded (there's an XXX comment about this), and #5635 is going to make early `stop()` calls into a hard error. This change ensures that the signal handler won't cause that error to show up. Also, serverless integrations don't use the same sort of process-based shutdown as other environments (and you don't call `start` or `listen` yourself), so registering these signal handlers isn't a great default. (They start "listening" before the ApolloServer is started, so it would be weird if after this change the signal handling depended on whether or not a request had been processed or not.) Make stopOnTerminationSignals default to false for serverless integrations.
1 parent 50c993b commit ee77696

File tree

3 files changed

+53
-43
lines changed

3 files changed

+53
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The version headers in this history reflect the versions of Apollo Server itself
1010
## vNEXT
1111

1212
- `apollo-server-core`: Fix `experimental_approximateDocumentStoreMiB` option, which seems to have never worked before. [PR #5629](https://github.com/apollographql/apollo-server/pull/5629)
13+
- `apollo-server-core`: Only register `SIGINT` and `SIGTERM` handlers once the server successfully starts up; trying to call `stop` on a server that hasn't successfully started had undefined behavior. By default, don't register the handlers in serverless integrations, which don't have the same lifecycle as non-serverless integrations (eg, there's no explicit `start` call); you can still explicitly set `stopOnTerminationSignals` to override this default. [PR #FIXME](https://github.com/apollographql/apollo-server/pull/FIXME)
1314

1415
## v3.1.2
1516

docs/source/api/apollo-server.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,12 @@ In certain cases, Apollo Server installs some of its built-in plugins automatica
289289
</td>
290290
<td>
291291

292-
By default (when running in Node and when the `NODE_ENV` environment variable does not equal `test`), whenever Apollo Server receives a `SIGINT` or `SIGTERM` signal, it calls `await this.stop()` on itself. (While this call to `this.stop()` is running, it ignores all `SIGINT` and `SIGTERM` signals.) It then sends that same signal to itself to continue process shutdown.
292+
By default (when running in Node when the `NODE_ENV` environment variable does not equal `test` and not using a [serverless-specific package](../integrations/middleware/#all-supported-packages)), whenever Apollo Server receives a `SIGINT` or `SIGTERM` signal, it calls `await this.stop()` on itself. (While this call to `this.stop()` is running, it ignores all `SIGINT` and `SIGTERM` signals.) It then sends that same signal to itself to continue process shutdown.
293293

294294
Set this option to `false` to disable this default behavior, or to `true` to enable the behavior even when `NODE_ENV` _does_ equal `test`.
295295

296+
The signal handler is installed after [`start()`](#start) returns successfully.
297+
296298
You can also manually call `stop()` in other contexts. Note that `stop()` is asynchronous, so it isn't useful in a `process.on('exit')` handler.
297299

298300
</td>
@@ -515,7 +517,7 @@ The `start` method triggers the following actions:
515517

516518
## `stop`
517519

518-
`ApolloServer.stop()` is an async method that tells all of Apollo Server's background tasks to complete. It calls and awaits all [`serverWillStop` plugin handlers](../integrations/plugins-event-reference/#serverwillstop) (including the [usage reporting plugin](./plugin/usage-reporting/)'s handler, which sends a final usage report to Apollo Studio). This method takes no arguments.
520+
`ApolloServer.stop()` is an async method that tells all of Apollo Server's background tasks to complete. It calls and awaits all [`serverWillStop` plugin handlers](../integrations/plugins-event-reference/#serverwillstop) (including the [usage reporting plugin](./plugin/usage-reporting/)'s handler, which sends a final usage report to Apollo Studio). This method takes no arguments. You should only call it after [`start()`](#start) returns successfully (or [`listen()`](#listen) if you're using the batteries-included `apollo-server` package).
519521

520522
If your server is a [federated gateway](https://www.apollographql.com/docs/federation/gateway/), `stop` also stops gateway-specific background activities, such as polling for updated service configuration.
521523

packages/apollo-server-core/src/ApolloServer.ts

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export class ApolloServerBase<
122122
private toDispose = new Set<() => Promise<void>>();
123123
private toDisposeLast = new Set<() => Promise<void>>();
124124
private experimental_approximateDocumentStoreMiB: Config['experimental_approximateDocumentStoreMiB'];
125+
private stopOnTerminationSignals: boolean;
125126
private landingPage: LandingPage | null = null;
126127

127128
// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
@@ -191,6 +192,15 @@ export class ApolloServerBase<
191192
: process.env.NODE_ENV;
192193
const isDev = nodeEnv !== 'production';
193194

195+
// We handle signals if it was explicitly requested, or if we're in Node,
196+
// not in a test, not in a serverless framework, and it wasn't explicitly
197+
// turned off. (We only actually register the signal handlers once we've
198+
// successfully started up, because there's nothing to stop otherwise.)
199+
this.stopOnTerminationSignals =
200+
typeof stopOnTerminationSignals === 'boolean'
201+
? stopOnTerminationSignals
202+
: isNodeLike && nodeEnv !== 'test' && !this.serverlessFramework();
203+
194204
// if this is local dev, introspection should turned on
195205
// in production, we can manually turn introspection on by passing {
196206
// introspection: true } to the constructor of ApolloServer
@@ -227,47 +237,6 @@ export class ApolloServerBase<
227237
// is populated accordingly.
228238
this.ensurePluginInstantiation(plugins, isDev);
229239

230-
// We handle signals if it was explicitly requested, or if we're in Node,
231-
// not in a test, and it wasn't explicitly turned off.
232-
if (
233-
typeof stopOnTerminationSignals === 'boolean'
234-
? stopOnTerminationSignals
235-
: isNodeLike && nodeEnv !== 'test'
236-
) {
237-
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
238-
let receivedSignal = false;
239-
signals.forEach((signal) => {
240-
// Note: Node only started sending signal names to signal events with
241-
// Node v10 so we can't use that feature here.
242-
const handler: NodeJS.SignalsListener = async () => {
243-
if (receivedSignal) {
244-
// If we receive another SIGINT or SIGTERM while we're waiting
245-
// for the server to stop, just ignore it.
246-
return;
247-
}
248-
receivedSignal = true;
249-
try {
250-
await this.stop();
251-
} catch (e) {
252-
this.logger.error(`stop() threw during ${signal} shutdown`);
253-
this.logger.error(e);
254-
// Can't rely on the signal handlers being removed.
255-
process.exit(1);
256-
}
257-
// Note: this.stop will call the toDisposeLast handlers below, so at
258-
// this point this handler will have been removed and we can re-kill
259-
// ourself to die with the appropriate signal exit status. this.stop
260-
// takes care to call toDisposeLast last, so the signal handler isn't
261-
// removed until after the rest of shutdown happens.
262-
process.kill(process.pid, signal);
263-
};
264-
process.on(signal, handler);
265-
this.toDisposeLast.add(async () => {
266-
process.removeListener(signal, handler);
267-
});
268-
});
269-
}
270-
271240
if (gateway) {
272241
// ApolloServer has been initialized but we have not yet tried to load the
273242
// schema from the gateway. That will wait until the user calls
@@ -483,6 +452,7 @@ export class ApolloServerBase<
483452
phase: 'started',
484453
schemaManager,
485454
};
455+
this.maybeRegisterTerminationSignalHandlers(['SIGINT', 'SIGTERM']);
486456
} catch (error) {
487457
this.state = { phase: 'failed to start', error };
488458
throw error;
@@ -491,6 +461,43 @@ export class ApolloServerBase<
491461
}
492462
}
493463

464+
private maybeRegisterTerminationSignalHandlers(signals: NodeJS.Signals[]) {
465+
if (!this.stopOnTerminationSignals) {
466+
return;
467+
}
468+
469+
let receivedSignal = false;
470+
const signalHandler: NodeJS.SignalsListener = async (signal) => {
471+
if (receivedSignal) {
472+
// If we receive another SIGINT or SIGTERM while we're waiting
473+
// for the server to stop, just ignore it.
474+
return;
475+
}
476+
receivedSignal = true;
477+
try {
478+
await this.stop();
479+
} catch (e) {
480+
this.logger.error(`stop() threw during ${signal} shutdown`);
481+
this.logger.error(e);
482+
// Can't rely on the signal handlers being removed.
483+
process.exit(1);
484+
}
485+
// Note: this.stop will call the toDisposeLast handlers below, so at
486+
// this point this handler will have been removed and we can re-kill
487+
// ourself to die with the appropriate signal exit status. this.stop
488+
// takes care to call toDisposeLast last, so the signal handler isn't
489+
// removed until after the rest of shutdown happens.
490+
process.kill(process.pid, signal);
491+
};
492+
493+
signals.forEach((signal) => {
494+
process.on(signal, signalHandler);
495+
this.toDisposeLast.add(async () => {
496+
process.removeListener(signal, signalHandler);
497+
});
498+
});
499+
}
500+
494501
// This method is called at the beginning of each GraphQL request by
495502
// `graphQLServerOptions`. Most of its logic is only helpful for serverless
496503
// frameworks: unless you're in a serverless framework, you should have called

0 commit comments

Comments
 (0)