Skip to content

Commit

Permalink
Generalize apollo-server graceful shutdown to all integrations
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
glasser committed Aug 23, 2021
1 parent 9b3467c commit 51a6b19
Show file tree
Hide file tree
Showing 30 changed files with 746 additions and 298 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The version headers in this history reflect the versions of Apollo Server itself

## vNEXT

- `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)
- `apollo-server-core`: Fix `experimental_approximateDocumentStoreMiB` option, which seems to have never worked before. [PR #5629](https://github.com/apollographql/apollo-server/pull/5629)
- `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)

Expand Down
1 change: 1 addition & 0 deletions docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ module.exports = {
'api/plugin/usage-reporting',
'api/plugin/schema-reporting',
'api/plugin/inline-trace',
'api/plugin/drain-http-server',
'api/plugin/cache-control',
'api/plugin/landing-pages',
],
Expand Down
34 changes: 18 additions & 16 deletions docs/source/api/apollo-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,22 +517,22 @@ The `start` method triggers the following actions:

## `stop`

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

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.
- Calls and awaits all [`drainServer` plugin handlers](../integrations/plugins-event-reference/#drainserver). These should generally:
* Stop listening for new connections
* Closes idle connections (i.e., connections with no current HTTP request)
* Closes active connections whenever they become idle
* Waits for all connections to be closed
* After a grace period, if any connections remain active, forcefully close them.
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.
- Transitions the server to a state where it will not start executing more GraphQL operations.
- 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).
- 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.

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.

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:

* Stops listening for new connections
* Closes idle connections (i.e., connections with no current HTTP request)
* Closes active connections whenever they become idle
* Waits for all connections to be closed

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).
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).

If you're using a [middleware package](../integrations/middleware/) instead of `apollo-server`, you should stop your HTTP server before calling `ApolloServer.stop()`.
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.

## Framework-specific middleware function

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

async function startApolloServer() {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();

const app = express();

// Additional middleware can be mounted at this point to run before Apollo.
app.use('*', jwtCheck, requireAuth, checkScope);

// Mount Apollo middleware here.
server.applyMiddleware({ app, path: '/specialUrl' });
await new Promise(resolve => app.listen({ port: 4000 }, resolve));
await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
return { server, app };
}
Expand Down
90 changes: 90 additions & 0 deletions docs/source/api/plugin/drain-http-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: "API Reference: Drain HTTP server plugin"
sidebar_title: Drain HTTP server
api_reference: true
---

## Using the plugin

This API reference documents the `ApolloServerPluginDrainHttpServer` plugin.

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.

You do not need to explicitly use this plugin with the batteries-included `apollo-server` package: that package automatically uses this plugin internally.

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:

* Stop listening for new connections
* Close idle connections (i.e., connections with no current HTTP request)
* Close active connections whenever they become idle
* Wait for all connections to be closed
* After a grace period, if any connections remain active, forcefully close them.

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.)

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.

```js
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
const { typeDefs, resolvers } = require('./schema');

async function startApolloServer() {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();

// Mount Apollo middleware here.
server.applyMiddleware({ app });
await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
return { server, app };
}
```

## Options

<table class="field-table">
<thead>
<tr>
<th>Name /<br/>Type</th>
<th>Description</th>
</tr>
</thead>

<tbody>

<tr>
<td>

###### `httpServer`

[`http.Server`](https://nodejs.org/api/http.html#http_class_http_server)
</td>
<td>

The server to drain; required.
</td>
</tr>

<tr>
<td>

###### `stopGracePeriodMillis`

`number`
</td>
<td>

How long to wait before forcefully closing non-idle connections. Defaults to `10_000` (ten seconds).
</td>
</tr>

</tbody>
</table>
41 changes: 30 additions & 11 deletions docs/source/data/subscriptions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,26 @@ To run both an Express app _and_ a separate subscription server, we'll create an
// This `server` is the instance returned from `new ApolloServer`.
path: server.graphqlPath,
});
```

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

```javascript:title=index.js
const server = new ApolloServer({
schema,
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServer.close();
}
};
}
}],
});
```

6. Finally, we need to adjust our existing `listen` call.
7. Finally, we need to adjust our existing `listen` call.

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.

Expand Down Expand Up @@ -136,17 +146,26 @@ import typeDefs from "./typeDefs";
resolvers,
});

const subscriptionServer = SubscriptionServer.create(
{ schema, execute, subscribe },
{ server: httpServer, path: server.graphqlPath }
);

const server = new ApolloServer({
schema,
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServer.close();
}
};
}
}],
});
await server.start();
server.applyMiddleware({ app });

SubscriptionServer.create(
{ schema, execute, subscribe },
{ server: httpServer, path: server.graphqlPath }
);

const PORT = 4000;
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}/graphql`)
Expand Down
Loading

0 comments on commit 51a6b19

Please sign in to comment.