Skip to content

Commit 51a6b19

Browse files
committed
Generalize apollo-server graceful shutdown to all integrations
Previously, the batteries-included `apollo-server` package had a special override of `stop()` which drains the HTTP server before letting the actual Apollo Server `stop()` machinery begin. This meant that `apollo-server` followed this nice shutdown lifecycle: - Stop listening for new connections - Close all idle connections and start closing connections as they go idle - Wait a grace period for all connections to close and force-close any remaining ones - Transition ApolloServer to the stopping state, where no operations will run - Run stop hooks (eg send final usage report) This was great... but only `apollo-server` worked this way, because only `apollo-server` has full knowledge and control over its HTTP server. This PR adds a server draining step to the ApolloServer lifecycle and plugin interface, and provides a built-in plugin which drains a Node `http.Server` using the logic of the first three steps above. `apollo-server`'s behavior is now just to automatically install the plugin. Specifically: - Add a new 'phase' called `draining` that fits between `started` and `stopping`. Like `started`, operations can still execute during `draining`. Like `stopping`, any concurrent call to `stop()` will just block until the first `stop()` call finishes rather than starting a second shutdown process. - Add a new `drainServer` plugin hook (on the object returned by `serverWillStart`). Invoke all `drainServer` hooks in parallel during the `draining` phase. - Make calling `stop()` when `start()` has not yet completed successfully into an error. That behavior was previously undefined. Note that as of #5639, the automatic `stop()` call from signal handlers can't happen before `start()` succeeds. - Add `ApolloServerPluginDrainHttpServer` to `apollo-server-core`. This plugin implements `drainServer` using the `Stopper` class that was previously in the `apollo-server` package. The default grace period is 10 seconds. - Clean up integration tests to just use `stop()` with the plugin instead of separately stopping the HTTP server. Note that for Fastify specifically we also call `app.close` although there is some weirdness here around both `app.close` and our Stopper closing the same server. A comment describes the weirdness; perhaps Fastify experts can improve this later. - The Hapi web framework has built in logic that is similar to our Stopper, so `apollo-server-hapi` exports `ApolloServerPluginStopHapiServer` which should be used instead of the other plugin with Hapi. - Fix some test issues (eg, have FakeTimers only mock out Date.now instead of setImmediate, drop an erroneous `const` which made an `app` not get cleaned up, etc). Fixes #5074.
1 parent 9b3467c commit 51a6b19

File tree

30 files changed

+746
-298
lines changed

30 files changed

+746
-298
lines changed

CHANGELOG.md

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

1010
## vNEXT
1111

12+
- `apollo-server-core`: Previously, only the batteries-included `apollo-server` package supported a graceful shutdown. Now the integrations support it as well, if you tell your `ApolloServer` which HTTP server to drain with the new `ApolloServerPluginDrainHttpServer` plugin. This plugin implements a new `drainServer` plugin hook. For `apollo-server-hapi` you can use `ApolloServerPluginStopHapiServer` instead. [PR #5635](https://github.com/apollographql/apollo-server/pull/5635)
1213
- `apollo-server-core`: Fix `experimental_approximateDocumentStoreMiB` option, which seems to have never worked before. [PR #5629](https://github.com/apollographql/apollo-server/pull/5629)
1314
- `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 #5639](https://github.com/apollographql/apollo-server/pull/5639)
1415

docs/gatsby-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ module.exports = {
7777
'api/plugin/usage-reporting',
7878
'api/plugin/schema-reporting',
7979
'api/plugin/inline-trace',
80+
'api/plugin/drain-http-server',
8081
'api/plugin/cache-control',
8182
'api/plugin/landing-pages',
8283
],

docs/source/api/apollo-server.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -517,22 +517,22 @@ The `start` method triggers the following actions:
517517

518518
## `stop`
519519

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).
520+
`ApolloServer.stop()` is an async method that tells all of Apollo Server's background tasks to complete. Specifically, it:
521521

522-
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.
522+
- Calls and awaits all [`drainServer` plugin handlers](../integrations/plugins-event-reference/#drainserver). These should generally:
523+
* Stop listening for new connections
524+
* Closes idle connections (i.e., connections with no current HTTP request)
525+
* Closes active connections whenever they become idle
526+
* Waits for all connections to be closed
527+
* After a grace period, if any connections remain active, forcefully close them.
528+
If you're using the batteries-included `apollo-server` package, it does this by default. (You can configure the grace period with the [`stopGracePeriodMillis` constructor option](#stopgraceperiodmillis).) Otherwise, you can use the [drain HTTP server plugin](./plugin/drain-http-server/) to drain your HTTP server.
529+
- Transitions the server to a state where it will not start executing more GraphQL operations.
530+
- 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).
531+
- 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.
523532

524-
In some circumstances, Apollo Server calls `stop` automatically when the process receives a `SIGINT` or `SIGTERM` signal. See the [`stopOnTerminationSignals` constructor option](#stoponterminationsignals) for details.
525-
526-
If you're using the `apollo-server` package (which handles setting up an HTTP server for you), this method first stops the HTTP server. Specifically, it:
527-
528-
* Stops listening for new connections
529-
* Closes idle connections (i.e., connections with no current HTTP request)
530-
* Closes active connections whenever they become idle
531-
* Waits for all connections to be closed
532-
533-
If any connections remain active after a grace period (10 seconds by default), Apollo Server forcefully closes those connections. You can configure this grace period with the [`stopGracePeriodMillis` constructor option](#stopgraceperiodmillis).
533+
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).
534534

535-
If you're using a [middleware package](../integrations/middleware/) instead of `apollo-server`, you should stop your HTTP server before calling `ApolloServer.stop()`.
535+
In some circumstances, Apollo Server calls `stop` automatically when the process receives a `SIGINT` or `SIGTERM` signal. See the [`stopOnTerminationSignals` constructor option](#stoponterminationsignals) for details.
536536

537537
## Framework-specific middleware function
538538

@@ -547,23 +547,25 @@ These functions take an `options` object as a parameter. Some supported fields o
547547
```js
548548
const express = require('express');
549549
const { ApolloServer } = require('apollo-server-express');
550+
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
550551
const { typeDefs, resolvers } = require('./schema');
551552

552553
async function startApolloServer() {
554+
const app = express();
555+
const httpServer = http.createServer(app);
553556
const server = new ApolloServer({
554557
typeDefs,
555558
resolvers,
559+
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
556560
});
557561
await server.start();
558562

559-
const app = express();
560-
561563
// Additional middleware can be mounted at this point to run before Apollo.
562564
app.use('*', jwtCheck, requireAuth, checkScope);
563565

564566
// Mount Apollo middleware here.
565567
server.applyMiddleware({ app, path: '/specialUrl' });
566-
await new Promise(resolve => app.listen({ port: 4000 }, resolve));
568+
await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
567569
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
568570
return { server, app };
569571
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
title: "API Reference: Drain HTTP server plugin"
3+
sidebar_title: Drain HTTP server
4+
api_reference: true
5+
---
6+
7+
## Using the plugin
8+
9+
This API reference documents the `ApolloServerPluginDrainHttpServer` plugin.
10+
11+
This plugin is designed for use with [`apollo-server-express` and other framework-specific packages](../../integrations/middleware/#all-supported-packages) which are built on top of [Node `http.Server`s](https://nodejs.org/api/http.html#http_class_http_server). It is highly recommended that you use this plugin with packages like `apollo-server-express` if you want your server to shut down gracefully.
12+
13+
You do not need to explicitly use this plugin with the batteries-included `apollo-server` package: that package automatically uses this plugin internally.
14+
15+
When you use this plugin, Apollo Server will drain your HTTP server when you call the `stop()` method (which is also called for you when the `SIGTERM` and `SIGINT` signals are received, unless disabled with the [`stopOnTerminationSignals` constructor option](./apollo-server/#stoponterminationsignals)). Specifically, it will:
16+
17+
* Stop listening for new connections
18+
* Close idle connections (i.e., connections with no current HTTP request)
19+
* Close active connections whenever they become idle
20+
* Wait for all connections to be closed
21+
* After a grace period, if any connections remain active, forcefully close them.
22+
23+
This plugin is exported from the `apollo-server-core` package. It is tested with `apollo-server-express`, `apollo-server-koa`, and `apollo-server-fastify`. (If you're using Hapi, you should instead use the `ApolloServerPluginStopHapiServer` plugin exported from the `apollo-server-hapi` package.)
24+
25+
Here's a basic example of how to use it with Express. See the [framework integrations docs](../../integrations/middleware/) for examples of how to use it with other frameworks.
26+
27+
```js
28+
const express = require('express');
29+
const { ApolloServer } = require('apollo-server-express');
30+
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
31+
const { typeDefs, resolvers } = require('./schema');
32+
33+
async function startApolloServer() {
34+
const app = express();
35+
const httpServer = http.createServer(app);
36+
const server = new ApolloServer({
37+
typeDefs,
38+
resolvers,
39+
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
40+
});
41+
await server.start();
42+
43+
// Mount Apollo middleware here.
44+
server.applyMiddleware({ app });
45+
await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
46+
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
47+
return { server, app };
48+
}
49+
```
50+
51+
## Options
52+
53+
<table class="field-table">
54+
<thead>
55+
<tr>
56+
<th>Name /<br/>Type</th>
57+
<th>Description</th>
58+
</tr>
59+
</thead>
60+
61+
<tbody>
62+
63+
<tr>
64+
<td>
65+
66+
###### `httpServer`
67+
68+
[`http.Server`](https://nodejs.org/api/http.html#http_class_http_server)
69+
</td>
70+
<td>
71+
72+
The server to drain; required.
73+
</td>
74+
</tr>
75+
76+
<tr>
77+
<td>
78+
79+
###### `stopGracePeriodMillis`
80+
81+
`number`
82+
</td>
83+
<td>
84+
85+
How long to wait before forcefully closing non-idle connections. Defaults to `10_000` (ten seconds).
86+
</td>
87+
</tr>
88+
89+
</tbody>
90+
</table>

docs/source/data/subscriptions.mdx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,26 @@ To run both an Express app _and_ a separate subscription server, we'll create an
9999
// This `server` is the instance returned from `new ApolloServer`.
100100
path: server.graphqlPath,
101101
});
102+
```
102103

103-
// Shut down in the case of interrupt and termination signals
104-
// We expect to handle this more cleanly in the future. See (#5074)[https://github.com/apollographql/apollo-server/issues/5074] for reference.
105-
['SIGINT', 'SIGTERM'].forEach(signal => {
106-
process.on(signal, () => subscriptionServer.close());
107-
});
104+
6. Add a plugin to your `ApolloServer` constructor to close the `SubscriptionServer`.
108105

106+
```javascript:title=index.js
107+
const server = new ApolloServer({
108+
schema,
109+
plugins: [{
110+
async serverWillStart() {
111+
return {
112+
async drainServer() {
113+
subscriptionServer.close();
114+
}
115+
};
116+
}
117+
}],
118+
});
109119
```
110120

111-
6. Finally, we need to adjust our existing `listen` call.
121+
7. Finally, we need to adjust our existing `listen` call.
112122

113123
Most Express applications call `app.listen(...)`. **Change this to `httpServer.listen(...)`** with the same arguments. This way, the server starts listening on the HTTP and WebSocket transports simultaneously.
114124

@@ -136,17 +146,26 @@ import typeDefs from "./typeDefs";
136146
resolvers,
137147
});
138148

149+
const subscriptionServer = SubscriptionServer.create(
150+
{ schema, execute, subscribe },
151+
{ server: httpServer, path: server.graphqlPath }
152+
);
153+
139154
const server = new ApolloServer({
140155
schema,
156+
plugins: [{
157+
async serverWillStart() {
158+
return {
159+
async drainServer() {
160+
subscriptionServer.close();
161+
}
162+
};
163+
}
164+
}],
141165
});
142166
await server.start();
143167
server.applyMiddleware({ app });
144168

145-
SubscriptionServer.create(
146-
{ schema, execute, subscribe },
147-
{ server: httpServer, path: server.graphqlPath }
148-
);
149-
150169
const PORT = 4000;
151170
httpServer.listen(PORT, () =>
152171
console.log(`Server is now running on http://localhost:${PORT}/graphql`)

0 commit comments

Comments
 (0)