Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize apollo-server graceful shutdown to all integrations #5635

Merged
merged 1 commit into from
Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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