diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91fe9f95803..9c8c3668f7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
index 9b6b6ef377e..75ff8a39da5 100644
--- a/docs/gatsby-config.js
+++ b/docs/gatsby-config.js
@@ -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',
],
diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md
index d687dbbb747..ff96e1b52ea 100644
--- a/docs/source/api/apollo-server.md
+++ b/docs/source/api/apollo-server.md
@@ -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
@@ -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 };
}
diff --git a/docs/source/api/plugin/drain-http-server.md b/docs/source/api/plugin/drain-http-server.md
new file mode 100644
index 00000000000..fd3a05dbf94
--- /dev/null
+++ b/docs/source/api/plugin/drain-http-server.md
@@ -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
+
+
+
+
+ Name / Type |
+ Description |
+
+
+
+
+
+
+
+
+###### `httpServer`
+
+[`http.Server`](https://nodejs.org/api/http.html#http_class_http_server)
+ |
+
+
+The server to drain; required.
+ |
+
+
+
+
+
+###### `stopGracePeriodMillis`
+
+`number`
+ |
+
+
+How long to wait before forcefully closing non-idle connections. Defaults to `10_000` (ten seconds).
+ |
+
+
+
+
diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx
index efe3c28065d..9ab6862a3b6 100644
--- a/docs/source/data/subscriptions.mdx
+++ b/docs/source/data/subscriptions.mdx
@@ -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.
@@ -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`)
diff --git a/docs/source/integrations/middleware.md b/docs/source/integrations/middleware.md
index 9913d51c658..5398eebaf75 100644
--- a/docs/source/integrations/middleware.md
+++ b/docs/source/integrations/middleware.md
@@ -138,7 +138,7 @@ async function startApolloServer(typeDefs, resolvers) {
To swap this out for `apollo-server-express`, we first install the following required packages:
```bash
-npm install apollo-server-express express graphql
+npm install apollo-server-express apollo-server-core express graphql
```
We can (and should) also _uninstall_ `apollo-server`.
@@ -147,16 +147,24 @@ Next, we can modify our code to match the following:
```javascript:title=index.js
import { ApolloServer } from 'apollo-server-express';
+import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import express from 'express';
+import http from 'http';
async function startApolloServer(typeDefs, resolvers) {
+ // Required logic for integrating with Express
+ const app = express();
+ const httpServer = http.createServer(app);
- // Same ApolloServer initialization as before
- const server = new ApolloServer({ typeDefs, resolvers });
+ // Same ApolloServer initialization as before, plus the drain plugin.
+ const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
+ });
- // Required logic for integrating with Express
+ // More required logic for integrating with Express
await server.start();
- const app = express();
server.applyMiddleware({
app,
@@ -167,20 +175,12 @@ async function startApolloServer(typeDefs, resolvers) {
});
// Modified server startup
- 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}`);
}
```
-### Handling server shutdown
-
-The `apollo-server` package does provide one feature that isn't straightforward to implement with other packages: because `apollo-server` handles making its HTTP server listen for requests, its [`stop`](../api/apollo-server/#stop) method handles _stopping_ the HTTP server. This means it can make sure to stop accepting new requests _before_ it begins to shut down the machinery that processes GraphQL operations.
-
-If you want this behavior in another package, you need to make sure to stop your web server *before* calling `stop()` on your `ApolloServer` instance. This can be challenging if `stop` is being called due to the [signal handlers](../api/apollo-server/#stoponterminationsignals) that Apollo Server installs by default.
-
-We intend to improve this behavior discrepancy, as described in [this GitHub issue](https://github.com/apollographql/apollo-server/issues/5074).
-
## Package conventions
@@ -237,19 +237,26 @@ If you want to do something with your server that isn't supported by `apollo-ser
The following example is roughly equivalent to the [`apollo-server` example](#apollo-server) above.
```bash
-npm install apollo-server-express express graphql
+npm install apollo-server-express apollo-server-core express graphql
```
```javascript:title=index.js
import { ApolloServer } from 'apollo-server-express';
+import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import express from 'express';
+import http from 'http';
async function startApolloServer(typeDefs, resolvers) {
- const server = new ApolloServer({ typeDefs, resolvers });
- await server.start();
const app = express();
+ const httpServer = http.createServer(app);
+ const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
+ });
+ await server.start();
server.applyMiddleware({ app });
- 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}`);
}
```
@@ -273,17 +280,37 @@ You can call `server.getMiddleware` instead of `server.applyMiddleware` if you w
The following example is roughly equivalent to the [`apollo-server` example](#apollo-server) above.
```
-$ npm install apollo-server-fastify fastify graphql
+$ npm install apollo-server-fastify apollo-server-core fastify graphql
```
```javascript
import { ApolloServer } from 'apollo-server-fastify';
+import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import fastify from 'fastify';
+function fastifyAppClosePlugin(app: FastifyInstance): ApolloServerPlugin {
+ return {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ await app.close();
+ },
+ };
+ },
+ };
+}
+
async function startApolloServer(typeDefs, resolvers) {
- const server = new ApolloServer({ typeDefs, resolvers });
- await server.start();
const app = fastify();
+ const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ fastifyAppClosePlugin(app),
+ ApolloServerPluginDrainHttpServer({ httpServer: app.server }),
+ ],
+ });
+ await server.start();
app.register(server.createHandler());
await app.listen(4000);
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
@@ -299,6 +326,8 @@ You _must_ `await server.start()` before calling `server.createHandler`. You can
* `onHealthCheck`
* `disableHealthCheck`
+> See [this issue](https://github.com/apollographql/apollo-server/issues/5642) for details about draining Fastify servers. The pattern suggested here isn't perfect; we look forward to suggestions from Fastify users.
+
### `apollo-server-hapi`
`apollo-server-hapi` is the GraphQL server for [hapi](https://hapi.dev/), a Node.js web framework. Apollo Server 3 is only tested with `@hapi/hapi` v20.1.2 and later (the minimum version that supports Node.js 16).
@@ -310,18 +339,20 @@ $ npm install apollo-server-hapi @hapi/hapi graphql
```
```javascript
-import { ApolloServer } from 'apollo-server-hapi';
+import { ApolloServer, ApolloServerPluginStopHapiServer } from 'apollo-server-hapi';
import Hapi from '@hapi/hapi';
async function startApolloServer(typeDefs, resolvers) {
- const server = new ApolloServer({ typeDefs, resolvers });
- await server.start();
- const app = new Hapi.server({
- port: 4000
- });
- await server.applyMiddleware({
- app,
+ const app = Hapi.server({ port: 4000 });
+ const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ ApolloServerPluginStopHapiServer({ hapiServer: app }),
+ ],
});
+ await server.start();
+ await server.applyMiddleware({ app });
await app.start();
}
```
@@ -344,19 +375,27 @@ You _must_ `await server.start()` before calling `server.applyMiddleware`. You c
The following example is roughly equivalent to the [`apollo-server` example](#apollo-server) above.
```
-$ npm install apollo-server-koa koa graphql
+$ npm install apollo-server-koa apollo-server-core koa graphql
```
```javascript
import { ApolloServer } from 'apollo-server-koa';
+import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import Koa from 'koa';
+import http from 'http';
async function startApolloServer(typeDefs, resolvers) {
- const server = new ApolloServer({ typeDefs, resolvers });
+ const httpServer = http.createServer();
+ const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
+ });
await server.start();
const app = new Koa();
server.applyMiddleware({ app });
- await new Promise(resolve => app.listen({ port: 4000 }, resolve));
+ httpServer.on('request', app.callback());
+ await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
return { server, app };
}
@@ -403,6 +442,8 @@ Then run the web server with `npx micro`.
Note that `apollo-server-micro` does _not_ have a built-in way of setting CORS headers.
+> We do not have a recommended `drainServer` implementation for `apollo-server-micro`. If you use `apollo-server-micro`, feel free to [contribute one](https://github.com/apollographql/apollo-server/pulls)!
+
### `apollo-server-lambda`
diff --git a/docs/source/integrations/plugins-event-reference.md b/docs/source/integrations/plugins-event-reference.md
index 533ee480caf..310186e8579 100644
--- a/docs/source/integrations/plugins-event-reference.md
+++ b/docs/source/integrations/plugins-event-reference.md
@@ -44,12 +44,40 @@ const server = new ApolloServer({
})
```
+### `drainServer`
+
+The `drainServer` event fires when Apollo Server is starting to shut down because [`ApolloServer.stop()`](../api/apollo-server/#stop) has been invoked (either explicitly by your code, or by one of the [termination signal handlers](../api/apollo-server/#stoponterminationsignals)). While `drainServer` handlers run, GraphQL operations can still execute successfully. This hook is designed to allow you to stop accepting new connections and close existing connections. Apollo Server has a [built-in plugin](../api/plugins/drain-http-server) which uses this event to drain a [Node `http.Server`](https://nodejs.org/api/http.html#http_class_http_server).
+
+You define your `drainServer` handler in the object returned by your [`serverWillStart`](#serverwillstart) handler, because the two handlers usually interact with the same data. Currently, `drainServer` handlers do not take arguments (this might change in the future).
+
+#### Example
+
+```js
+const server = new ApolloServer({
+ /* ... other necessary configuration ... */
+
+ plugins: [
+ {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ await myCustomServer.drain();
+ }
+ }
+ }
+ }
+ ]
+})
+```
+
### `serverWillStop`
The `serverWillStop` event fires when Apollo Server is starting to shut down because [`ApolloServer.stop()`](../api/apollo-server/#stop) has been invoked (either explicitly by your code, or by one of the [termination signal handlers](../api/apollo-server/#stoponterminationsignals)). If your plugin is running any background tasks, this is a good place to shut them down.
You define your `serverWillStop` handler in the object returned by your [`serverWillStart`](#serverwillstart) handler, because the two handlers usually interact with the same data. Currently, `serverWillStop` handlers do not take arguments (this might change in the future).
+When your `serverWillStart` handler is called, Apollo Server is in a state where it will no longer start to execute new GraphQL operations, so it's a good place to flush observability data. If you are looking for a hook that runs while operations can still execute, try [`drainServer`](#drainserver).
+
#### Example
```js
diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx
index 201120c63b9..e6bb763c77a 100644
--- a/docs/source/migration.mdx
+++ b/docs/source/migration.mdx
@@ -120,11 +120,22 @@ Then, complete the following:
// This `server` is the instance returned from `new ApolloServer`.
path: server.graphqlPath,
});
+ ```
+
+1. Add a plugin to your `ApolloServer` constructor to close the `SubscriptionServer`.
- // 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());
+ ```javascript:title=index.js-10
+ const server = new ApolloServer({
+ schema,
+ plugins: [{
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ subscriptionServer.close();
+ }
+ };
+ }
+ }],
});
```
@@ -155,16 +166,6 @@ import typeDefs from "./typeDefs";
resolvers,
});
- const server = new ApolloServer({
- schema,
- context() {
- // lookup userId by token, etc.
- return { userId };
- }
- });
- await server.start();
- server.applyMiddleware({ app });
-
const subscriptionServer = SubscriptionServer.create({
schema,
execute,
@@ -178,11 +179,24 @@ import typeDefs from "./typeDefs";
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());
+ const server = new ApolloServer({
+ schema,
+ context() {
+ // lookup userId by token, etc.
+ return { userId };
+ },
+ plugins: [{
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ subscriptionServer.close();
+ }
+ };
+ }
+ }],
});
+ await server.start();
+ server.applyMiddleware({ app });
const PORT = 4000;
httpServer.listen(PORT, () =>
diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts
index 2978154b5a3..c63ec1e9c4c 100644
--- a/packages/apollo-server-core/src/ApolloServer.ts
+++ b/packages/apollo-server-core/src/ApolloServer.ts
@@ -80,19 +80,36 @@ export type SchemaDerivedData = {
};
type ServerState =
- | { phase: 'initialized'; schemaManager: SchemaManager }
+ | {
+ phase: 'initialized';
+ schemaManager: SchemaManager;
+ }
| {
phase: 'starting';
barrier: Resolvable;
schemaManager: SchemaManager;
}
- | { phase: 'failed to start'; error: Error }
+ | {
+ phase: 'failed to start';
+ error: Error;
+ }
| {
phase: 'started';
schemaManager: SchemaManager;
}
- | { phase: 'stopping'; barrier: Resolvable }
- | { phase: 'stopped'; stopError: Error | null };
+ | {
+ phase: 'draining';
+ schemaManager: SchemaManager;
+ barrier: Resolvable;
+ }
+ | {
+ phase: 'stopping';
+ barrier: Resolvable;
+ }
+ | {
+ phase: 'stopped';
+ stopError: Error | null;
+ };
// Throw this in places that should be unreachable (because all other cases have
// been handled, reducing the type of the argument to `never`). TypeScript will
@@ -121,6 +138,7 @@ export class ApolloServerBase<
private state: ServerState;
private toDispose = new Set<() => Promise>();
private toDisposeLast = new Set<() => Promise>();
+ private drainServers: (() => Promise) | null = null;
private experimental_approximateDocumentStoreMiB: Config['experimental_approximateDocumentStoreMiB'];
private stopOnTerminationSignals: boolean;
private landingPage: LandingPage | null = null;
@@ -137,13 +155,15 @@ export class ApolloServerBase<
typeDefs,
parseOptions = {},
introspection,
- mocks,
- mockEntireSchema,
plugins,
gateway,
- experimental_approximateDocumentStoreMiB,
- stopOnTerminationSignals,
apollo,
+ stopOnTerminationSignals,
+ // These next options aren't used in this function but they don't belong in
+ // requestOptions.
+ mocks,
+ mockEntireSchema,
+ experimental_approximateDocumentStoreMiB,
...requestOptions
} = config;
@@ -425,6 +445,17 @@ export class ApolloServerBase<
});
}
+ const drainServerCallbacks = taggedServerListeners.flatMap((l) =>
+ l.serverListener.drainServer ? [l.serverListener.drainServer] : [],
+ );
+ if (drainServerCallbacks.length) {
+ this.drainServers = async () => {
+ await Promise.all(
+ drainServerCallbacks.map((drainServer) => drainServer()),
+ );
+ };
+ }
+
// Find the renderLandingPage callback, if one is provided. If the user
// installed ApolloServerPluginLandingPageDisabled then there may be none
// found. On the other hand, if the user installed a landingPage plugin,
@@ -537,6 +568,7 @@ export class ApolloServerBase<
'This data graph is missing a valid configuration. More details may be available in the server logs.',
);
case 'started':
+ case 'draining': // We continue to run operations while draining.
return this.state.schemaManager.getSchemaDerivedData();
case 'stopping':
throw new Error(
@@ -559,7 +591,7 @@ export class ApolloServerBase<
}
protected assertStarted(methodName: string) {
- if (this.state.phase !== 'started') {
+ if (this.state.phase !== 'started' && this.state.phase !== 'draining') {
throw new Error(
'You must `await server.start()` before calling `server.' +
methodName +
@@ -648,45 +680,67 @@ export class ApolloServerBase<
};
}
- /**
- * XXX: Note that stop() was designed to be called after start() has finished,
- * and should not be called concurrently with start() or before start(), or
- * else unexpected behavior may occur (e.g. some dependencies may not be
- * stopped).
- */
public async stop() {
- // Calling stop more than once should have the same result as the first time.
- if (this.state.phase === 'stopped') {
- if (this.state.stopError) {
- throw this.state.stopError;
- }
- return;
- }
+ switch (this.state.phase) {
+ case 'initialized':
+ case 'starting':
+ case 'failed to start':
+ throw Error(
+ 'apolloServer.stop() should only be called after `await apolloServer.start()` has succeeded',
+ );
- // Two parallel calls to stop; just wait for the other one to finish and
- // do whatever it did.
- if (this.state.phase === 'stopping') {
- await this.state.barrier;
- // The cast here is because TS doesn't understand that this.state can
- // change during the await
- // (https://github.com/microsoft/TypeScript/issues/9998).
- const state = this.state as ServerState;
- if (state.phase !== 'stopped') {
- throw Error(`Surprising post-stopping state ${state.phase}`);
- }
- if (state.stopError) {
- throw state.stopError;
+ // Calling stop more than once should have the same result as the first time.
+ case 'stopped':
+ if (this.state.stopError) {
+ throw this.state.stopError;
+ }
+ return;
+
+ // Two parallel calls to stop; just wait for the other one to finish and
+ // do whatever it did.
+ case 'stopping':
+ case 'draining': {
+ await this.state.barrier;
+ // The cast here is because TS doesn't understand that this.state can
+ // change during the await
+ // (https://github.com/microsoft/TypeScript/issues/9998).
+ const state = this.state as ServerState;
+ if (state.phase !== 'stopped') {
+ throw Error(`Surprising post-stopping state ${state.phase}`);
+ }
+ if (state.stopError) {
+ throw state.stopError;
+ }
+ return;
}
- return;
+
+ case 'started':
+ // This is handled by the rest of the function.
+ break;
+
+ default:
+ throw new UnreachableCaseError(this.state);
}
- // Commit to stopping, actually stop, and update the phase.
+ const barrier = resolvable();
+
+ // Commit to stopping and start draining servers.
+ this.state = {
+ phase: 'draining',
+ schemaManager: this.state.schemaManager,
+ barrier,
+ };
+
+ await this.drainServers?.();
+
+ // Servers are drained. Prevent further operations from starting and call
+ // stop handlers.
this.state = { phase: 'stopping', barrier: resolvable() };
try {
// We run shutdown handlers in two phases because we don't want to turn
- // off our signal listeners until we've done the important parts of shutdown
- // like running serverWillStop handlers. (We can make this more generic later
- // if it's helpful.)
+ // off our signal listeners (ie, allow signals to kill the process) until
+ // we've done the important parts of shutdown like running serverWillStop
+ // handlers. (We can make this more generic later if it's helpful.)
await Promise.all([...this.toDispose].map((dispose) => dispose()));
await Promise.all([...this.toDisposeLast].map((dispose) => dispose()));
} catch (stopError) {
diff --git a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts
index 44f2bf2eba5..a092d553649 100644
--- a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts
+++ b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts
@@ -49,16 +49,17 @@ describe('ApolloServerBase construction', () => {
warn,
error: jest.fn(),
};
- expect(() =>
- new ApolloServerBase({
- typeDefs,
- resolvers,
- apollo: {
- graphVariant: 'foo',
- key: 'service:real:key',
- },
- logger,
- }).stop(),
+ expect(
+ () =>
+ new ApolloServerBase({
+ typeDefs,
+ resolvers,
+ apollo: {
+ graphVariant: 'foo',
+ key: 'service:real:key',
+ },
+ logger,
+ }),
).not.toThrow();
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toMatch(
diff --git a/packages/apollo-server/src/__tests__/stoppable.test.ts b/packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable.test.ts
similarity index 100%
rename from packages/apollo-server/src/__tests__/stoppable.test.ts
rename to packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable.test.ts
diff --git a/packages/apollo-server/src/__tests__/stoppable/fixture.cert b/packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/fixture.cert
similarity index 100%
rename from packages/apollo-server/src/__tests__/stoppable/fixture.cert
rename to packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/fixture.cert
diff --git a/packages/apollo-server/src/__tests__/stoppable/fixture.key b/packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/fixture.key
similarity index 100%
rename from packages/apollo-server/src/__tests__/stoppable/fixture.key
rename to packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/fixture.key
diff --git a/packages/apollo-server/src/__tests__/stoppable/server.js b/packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/server.js
similarity index 81%
rename from packages/apollo-server/src/__tests__/stoppable/server.js
rename to packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/server.js
index eac7e44db41..872620cb841 100644
--- a/packages/apollo-server/src/__tests__/stoppable/server.js
+++ b/packages/apollo-server-core/src/plugin/drainHttpServer/__tests__/stoppable/server.js
@@ -1,5 +1,7 @@
const http = require('http');
-const { Stopper } = require('../../../dist/stoppable.js');
+const {
+ Stopper,
+} = require('../../../../../dist/plugin/drainHttpServer/stoppable.js');
const grace = Number(process.argv[2] || Infinity);
let stopper;
diff --git a/packages/apollo-server-core/src/plugin/drainHttpServer/index.ts b/packages/apollo-server-core/src/plugin/drainHttpServer/index.ts
new file mode 100644
index 00000000000..29ad4eb78bd
--- /dev/null
+++ b/packages/apollo-server-core/src/plugin/drainHttpServer/index.ts
@@ -0,0 +1,39 @@
+import type http from 'http';
+import type { ApolloServerPlugin } from 'apollo-server-plugin-base';
+import { Stopper } from './stoppable';
+
+/**
+ * Options for ApolloServerPluginDrainHttpServer.
+ */
+export interface ApolloServerPluginDrainHttpServerOptions {
+ /**
+ * The http.Server (or https.Server, etc) to drain. Required.
+ */
+ httpServer: http.Server;
+ /**
+ * How long to wait before forcefully closing non-idle connections.
+ * Defaults to 10_000 (ten seconds).
+ */
+ stopGracePeriodMillis?: number;
+}
+
+/**
+ * This plugin is used with apollo-server-express and other framework
+ * integrations to drain your HTTP server on shutdown.
+ * See https://www.apollographql.com/docs/apollo-server/api/plugin/drain-http-server/
+ * for details.
+ */
+export function ApolloServerPluginDrainHttpServer(
+ options: ApolloServerPluginDrainHttpServerOptions,
+): ApolloServerPlugin {
+ const stopper = new Stopper(options.httpServer);
+ return {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ await stopper.stop(options.stopGracePeriodMillis ?? 10_000);
+ },
+ };
+ },
+ };
+}
diff --git a/packages/apollo-server/src/stoppable.ts b/packages/apollo-server-core/src/plugin/drainHttpServer/stoppable.ts
similarity index 100%
rename from packages/apollo-server/src/stoppable.ts
rename to packages/apollo-server-core/src/plugin/drainHttpServer/stoppable.ts
diff --git a/packages/apollo-server-core/src/plugin/index.ts b/packages/apollo-server-core/src/plugin/index.ts
index 585fdf27f9c..b2432282ee2 100644
--- a/packages/apollo-server-core/src/plugin/index.ts
+++ b/packages/apollo-server-core/src/plugin/index.ts
@@ -80,6 +80,18 @@ export function ApolloServerPluginCacheControlDisabled(): ApolloServerPlugin {
}
//#endregion
+//#region Drain HTTP server
+import type { ApolloServerPluginDrainHttpServerOptions } from './drainHttpServer';
+export type { ApolloServerPluginDrainHttpServerOptions } from './drainHttpServer';
+export function ApolloServerPluginDrainHttpServer(
+ options: ApolloServerPluginDrainHttpServerOptions,
+): ApolloServerPlugin {
+ return require('./drainHttpServer').ApolloServerPluginDrainHttpServer(
+ options,
+ );
+}
+//#endregion
+
//#region LandingPage
import type { InternalApolloServerPlugin } from '../internalPlugin';
export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin {
diff --git a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts
index daaabb5474c..d554a6616e6 100644
--- a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts
+++ b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts
@@ -8,6 +8,7 @@ import {
gql,
AuthenticationError,
ApolloServerPluginCacheControlDisabled,
+ ApolloServerPluginDrainHttpServer,
} from 'apollo-server-core';
import {
ApolloServer,
@@ -34,24 +35,34 @@ const resolvers = {
};
describe('apollo-server-express', () => {
- let server: ApolloServer;
- let httpServer: http.Server;
+ let serverToCleanUp: ApolloServer | null = null;
testApolloServer(
async (config: ApolloServerExpressConfig, options) => {
- server = new ApolloServer(config);
+ serverToCleanUp = null;
+ const app = express();
+ const httpServer = http.createServer(app);
+ const server = new ApolloServer({
+ ...config,
+ plugins: [
+ ...(config.plugins ?? []),
+ ApolloServerPluginDrainHttpServer({
+ httpServer: httpServer,
+ }),
+ ],
+ });
if (!options?.suppressStartCall) {
await server.start();
+ serverToCleanUp = server;
}
- const app = express();
server.applyMiddleware({ app, path: options?.graphqlPath });
- httpServer = await new Promise((resolve) => {
- const s: http.Server = app.listen({ port: 0 }, () => resolve(s));
+ await new Promise((resolve) => {
+ httpServer.once('listening', resolve);
+ httpServer.listen({ port: 0 });
});
return createServerInfo(server, httpServer);
},
async () => {
- if (httpServer?.listening) await httpServer.close();
- if (server) await server.stop();
+ await serverToCleanUp?.stop();
},
);
});
diff --git a/packages/apollo-server-express/src/__tests__/expressApollo.test.ts b/packages/apollo-server-express/src/__tests__/expressApollo.test.ts
index b154509176d..812cb5d10dd 100644
--- a/packages/apollo-server-express/src/__tests__/expressApollo.test.ts
+++ b/packages/apollo-server-express/src/__tests__/expressApollo.test.ts
@@ -5,17 +5,6 @@ import testSuite, {
CreateAppOptions,
} from 'apollo-server-integration-testsuite';
-async function createApp(options: CreateAppOptions = {}) {
- const app = express();
-
- const server = new ApolloServer(
- (options.graphqlOptions as ApolloServerExpressConfig) || { schema: Schema },
- );
- await server.start();
- server.applyMiddleware({ app });
- return app;
-}
-
describe('expressApollo', () => {
it('throws error if called without schema', function () {
expect(() => new ApolloServer(undefined as any)).toThrow(
@@ -25,5 +14,23 @@ describe('expressApollo', () => {
});
describe('integration:Express', () => {
- testSuite({ createApp });
+ let serverToCleanUp: ApolloServer | null = null;
+ testSuite({
+ createApp: async function createApp(options: CreateAppOptions = {}) {
+ serverToCleanUp = null;
+ const app = express();
+ const server = new ApolloServer(
+ (options.graphqlOptions as ApolloServerExpressConfig) || {
+ schema: Schema,
+ },
+ );
+ await server.start();
+ serverToCleanUp = server;
+ server.applyMiddleware({ app });
+ return app;
+ },
+ destroyApp: async function () {
+ await serverToCleanUp?.stop();
+ },
+ });
});
diff --git a/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts
index 0255b123bd2..3b5dae5f8d0 100644
--- a/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts
+++ b/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts
@@ -1,8 +1,5 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import fastify from 'fastify';
-
-import http from 'http';
-
import request from 'supertest';
import {
@@ -10,6 +7,7 @@ import {
AuthenticationError,
Config,
ApolloServerPluginCacheControlDisabled,
+ ApolloServerPluginDrainHttpServer,
} from 'apollo-server-core';
import { ApolloServer, ServerRegistration } from '../ApolloServer';
@@ -18,6 +16,7 @@ import {
createServerInfo,
createApolloFetch,
} from 'apollo-server-integration-testsuite';
+import type { ApolloServerPlugin } from 'apollo-server-plugin-base';
const typeDefs = gql`
type Query {
@@ -33,34 +32,60 @@ const resolvers = {
const port = 9999;
+function fastifyAppClosePlugin(app: FastifyInstance): ApolloServerPlugin {
+ return {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ await app.close();
+ },
+ };
+ },
+ };
+}
+
describe('apollo-server-fastify', () => {
- let server: ApolloServer;
- let httpServer: http.Server;
- let app: FastifyInstance;
+ let serverToCleanUp: ApolloServer | null = null;
testApolloServer(
async (config: any, options) => {
- server = new ApolloServer(config);
+ serverToCleanUp = null;
+ const app = fastify();
+ const server = new ApolloServer({
+ ...config,
+ plugins: [
+ ...(config.plugins ?? []),
+ // I *think* racing these two plugins against each other works.
+ // They will both end up calling server.close, and one will
+ // get ERR_SERVER_NOT_RUNNING, but our stoppable implementation
+ // ignores errors from close. An alternative would be to use
+ // Fastify's serverFactory to return a server whose 'close' actually
+ // does the stoppable stuff. (The tests do seem to pass without this
+ // close call, but it seems like a good idea to invoke app.close in
+ // case the user registered any onClose hooks?)
+ // See https://github.com/apollographql/apollo-server/issues/5642
+ fastifyAppClosePlugin(app),
+ ApolloServerPluginDrainHttpServer({
+ httpServer: app.server,
+ }),
+ ],
+ });
if (!options?.suppressStartCall) {
await server.start();
+ serverToCleanUp = server;
}
- app = fastify();
app.register(server.createHandler({ path: options?.graphqlPath }));
await app.listen(port);
return createServerInfo(server, app.server);
},
async () => {
- if (server) await server.stop();
- if (app) await new Promise((resolve) => app.close(() => resolve()));
- if (httpServer?.listening) await httpServer.close();
+ await serverToCleanUp?.stop();
},
);
});
describe('apollo-server-fastify', () => {
let server: ApolloServer;
- let app: FastifyInstance;
- let httpServer: http.Server;
let replyDecorator: jest.Mock | undefined;
let requestDecorator: jest.Mock | undefined;
@@ -69,12 +94,19 @@ describe('apollo-server-fastify', () => {
options: Partial = {},
mockDecorators: boolean = false,
) {
+ const app = fastify();
server = new ApolloServer({
stopOnTerminationSignals: false,
...serverOptions,
+ plugins: [
+ ...(serverOptions.plugins ?? []),
+ fastifyAppClosePlugin(app),
+ ApolloServerPluginDrainHttpServer({
+ httpServer: app.server,
+ }),
+ ],
});
await server.start();
- app = fastify();
if (mockDecorators) {
replyDecorator = jest.fn();
@@ -91,9 +123,7 @@ describe('apollo-server-fastify', () => {
}
afterEach(async () => {
- if (server) await server.stop();
- if (app) await new Promise((resolve) => app.close(() => resolve()));
- if (httpServer) await httpServer.close();
+ await server?.stop();
});
describe('constructor', () => {
diff --git a/packages/apollo-server-fastify/src/__tests__/fastifyApollo.test.ts b/packages/apollo-server-fastify/src/__tests__/fastifyApollo.test.ts
index 7cb688dbeb5..751fa9e5baa 100644
--- a/packages/apollo-server-fastify/src/__tests__/fastifyApollo.test.ts
+++ b/packages/apollo-server-fastify/src/__tests__/fastifyApollo.test.ts
@@ -1,32 +1,11 @@
-import fastify from 'fastify';
-import { Server } from 'http';
+import fastify, { FastifyInstance } from 'fastify';
import { ApolloServer } from '../ApolloServer';
import testSuite, {
schema as Schema,
CreateAppOptions,
} from 'apollo-server-integration-testsuite';
import { Config } from 'apollo-server-core';
-
-async function createApp(options: CreateAppOptions = {}) {
- const app = fastify();
-
- const server = new ApolloServer(
- (options.graphqlOptions as Config) || { schema: Schema },
- );
- await server.start();
-
- app.register(server.createHandler());
- await app.listen(0);
-
- return app.server;
-}
-
-async function destroyApp(app: Server) {
- if (!app || !app.close) {
- return;
- }
- await new Promise((resolve) => app.close(resolve));
-}
+import type { ApolloServerPlugin } from 'apollo-server-plugin-base';
describe('fastifyApollo', () => {
it('throws error if called without schema', function () {
@@ -36,6 +15,40 @@ describe('fastifyApollo', () => {
});
});
+function fastifyAppClosePlugin(app: FastifyInstance): ApolloServerPlugin {
+ return {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ await app.close();
+ },
+ };
+ },
+ };
+}
+
describe('integration:Fastify', () => {
- testSuite({ createApp, destroyApp, integrationName: 'fastify' });
+ let serverToCleanUp: ApolloServer | null = null;
+ let app: FastifyInstance | null = null;
+ testSuite({
+ async createApp(options: CreateAppOptions = {}) {
+ serverToCleanUp = null;
+ app = fastify();
+ const config = (options.graphqlOptions as Config) || { schema: Schema };
+ const server = new ApolloServer({
+ ...config,
+ plugins: [...(config.plugins ?? []), fastifyAppClosePlugin(app)],
+ });
+ await server.start();
+ serverToCleanUp = server;
+ app.register(server.createHandler());
+ await app.listen(0);
+
+ return app.server;
+ },
+ async destroyApp() {
+ await serverToCleanUp?.stop();
+ },
+ integrationName: 'fastify',
+ });
});
diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts
index 6ee235b73f5..f41e91b9fb1 100644
--- a/packages/apollo-server-hapi/src/ApolloServer.ts
+++ b/packages/apollo-server-hapi/src/ApolloServer.ts
@@ -10,6 +10,7 @@ import {
runHttpQuery,
} from 'apollo-server-core';
import Boom from '@hapi/boom';
+import type { ApolloServerPlugin } from 'apollo-server-plugin-base';
export class ApolloServer extends ApolloServerBase {
// This translates the arguments from the middleware into graphQL options It
@@ -164,3 +165,20 @@ export interface ServerRegistration {
onHealthCheck?: (request: hapi.Request) => Promise;
disableHealthCheck?: boolean;
}
+
+// hapi's app.stop() works similarly to ApolloServerPluginDrainHttpServer by
+// default (as long as cleanStop has its default value of true) so we just use
+// it instead of our own HTTP-server-draining implementation.
+export function ApolloServerPluginStopHapiServer(options: {
+ hapiServer: hapi.Server;
+}): ApolloServerPlugin {
+ return {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ await options.hapiServer.stop();
+ },
+ };
+ },
+ };
+}
diff --git a/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts
index 2d9970ec984..4ab69bfa0e1 100644
--- a/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts
+++ b/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts
@@ -4,42 +4,60 @@ import {
createApolloFetch,
} from 'apollo-server-integration-testsuite';
-import http = require('http');
import request from 'supertest';
-import { Server } from '@hapi/hapi';
+import Hapi from '@hapi/hapi';
import { gql, AuthenticationError, Config } from 'apollo-server-core';
-import { ApolloServer, ServerRegistration } from '../ApolloServer';
+import {
+ ApolloServer,
+ ApolloServerPluginStopHapiServer,
+ ServerRegistration,
+} from '../ApolloServer';
const port = 0;
describe('apollo-server-hapi', () => {
- let server: ApolloServer;
-
- let app: Server;
- let httpServer: http.Server;
+ let serverToCleanUp: ApolloServer | null = null;
+
+ testApolloServer(
+ async (config: any, options) => {
+ serverToCleanUp = null;
+ const app = Hapi.server({ host: 'localhost', port });
+ const server = new ApolloServer({
+ ...config,
+ plugins: [
+ ...(config.plugins ?? []),
+ ApolloServerPluginStopHapiServer({
+ hapiServer: app,
+ }),
+ ],
+ });
+ if (!options?.suppressStartCall) {
+ await server.start();
+ serverToCleanUp = server;
+ }
+ await server.applyMiddleware({ app, path: options?.graphqlPath });
+ await app.start();
+ const httpServer = app.listener;
+ return createServerInfo(server, httpServer);
+ },
+ async () => {
+ await serverToCleanUp?.stop();
+ },
+ );
+});
- async function cleanup() {
- if (server) await server.stop();
- if (app) await app.stop();
- if (httpServer?.listening) httpServer.close();
- }
- afterEach(cleanup);
+describe('non-integration tests', () => {
+ let serverToCleanUp: ApolloServer | null = null;
- testApolloServer(async (config: any, options) => {
- server = new ApolloServer(config);
- app = new Server({ host: 'localhost', port });
- if (!options?.suppressStartCall) {
- await server.start();
- }
- await server.applyMiddleware({ app, path: options?.graphqlPath });
- await app.start();
- const httpServer = app.listener;
- return createServerInfo(server, httpServer);
- }, cleanup);
+ beforeEach(() => {
+ serverToCleanUp = null;
+ });
+ afterEach(async () => {
+ await serverToCleanUp?.stop();
+ });
- //Non-integration tests
const typeDefs = gql`
type Query {
hello: String
@@ -56,23 +74,25 @@ describe('apollo-server-hapi', () => {
serverOptions: Config,
options: Partial = {},
) {
- server = new ApolloServer({
+ const app = Hapi.server({ port: 0 });
+ const server = new ApolloServer({
stopOnTerminationSignals: false,
...serverOptions,
+ plugins: [
+ ...(serverOptions.plugins ?? []),
+ ApolloServerPluginStopHapiServer({
+ hapiServer: app,
+ }),
+ ],
});
await server.start();
- app = new Server({ port: 0 });
+ serverToCleanUp = server;
await server.applyMiddleware({ ...options, app });
await app.start();
return createServerInfo(server, app.listener);
}
- afterEach(async () => {
- if (server) await server.stop();
- if (httpServer) await httpServer.close();
- });
-
describe('constructor', () => {
it('accepts typeDefs and resolvers', async () => {
return createServer({ typeDefs, resolvers });
@@ -201,10 +221,6 @@ describe('apollo-server-hapi', () => {
});
describe('healthchecks', () => {
- afterEach(async () => {
- await server.stop();
- });
-
it('creates a healthcheck endpoint', async () => {
const { httpServer } = await createServer({
typeDefs,
diff --git a/packages/apollo-server-hapi/src/index.ts b/packages/apollo-server-hapi/src/index.ts
index 12f384f069d..843b247bab3 100644
--- a/packages/apollo-server-hapi/src/index.ts
+++ b/packages/apollo-server-hapi/src/index.ts
@@ -13,4 +13,8 @@ export {
} from 'apollo-server-core';
// ApolloServer integration.
-export { ApolloServer, ServerRegistration } from './ApolloServer';
+export {
+ ApolloServer,
+ ServerRegistration,
+ ApolloServerPluginStopHapiServer,
+} from './ApolloServer';
diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts
index 3667d5e8dc3..137ed8016a3 100644
--- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts
+++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts
@@ -176,7 +176,11 @@ export interface ServerInfo {
export interface CreateServerFunc {
(
config: Config,
- options?: { suppressStartCall?: boolean; graphqlPath?: string },
+ options?: {
+ suppressStartCall?: boolean;
+ graphqlPath?: string;
+ noRequestsMade?: boolean;
+ },
): Promise>;
}
@@ -2174,7 +2178,12 @@ export function testApolloServer(
describe('Response caching', () => {
let clock: FakeTimers.Clock;
beforeAll(() => {
- clock = FakeTimers.install();
+ // These tests use the default InMemoryLRUCache, which is backed by the
+ // lru-cache npm module, whose maxAge feature is based on `Date.now()`
+ // (no setTimeout or anything like that). So we want to use fake timers
+ // just for Date. (Faking all the timer methods messes up things like a
+ // setImmediate in ApolloServerPluginDrainHttpServer.)
+ clock = FakeTimers.install({ toFake: ['Date'] });
});
afterAll(() => {
@@ -2726,11 +2735,17 @@ export function testApolloServer(
const { gateway, triggers } = makeGatewayMock({ optionsSpy });
triggers.resolveLoad({ schema, executor: async () => ({}) });
- await createApolloServer({
- gateway,
- apollo: { key: 'service:tester:1234abc', graphRef: 'tester@staging' },
- logger: quietLogger,
- });
+ await createApolloServer(
+ {
+ gateway,
+ apollo: {
+ key: 'service:tester:1234abc',
+ graphRef: 'tester@staging',
+ },
+ logger: quietLogger,
+ },
+ { noRequestsMade: true },
+ );
expect(optionsSpy).toHaveBeenLastCalledWith({
apollo: {
diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts
index 330dc1a6976..2abd00c242a 100644
--- a/packages/apollo-server-integration-testsuite/src/index.ts
+++ b/packages/apollo-server-integration-testsuite/src/index.ts
@@ -1310,7 +1310,7 @@ export default ({
describe('status code', () => {
it('allows setting a custom status code', async () => {
- const app = await createApp({
+ app = await createApp({
graphqlOptions: {
schema,
plugins: [
diff --git a/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts
index 35b0033e3c6..300a5c093e7 100644
--- a/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts
+++ b/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts
@@ -1,5 +1,5 @@
import http from 'http';
-
+import Koa from 'koa';
import request from 'supertest';
import {
@@ -7,6 +7,7 @@ import {
AuthenticationError,
Config,
ApolloServerPluginCacheControlDisabled,
+ ApolloServerPluginDrainHttpServer,
} from 'apollo-server-core';
import {
@@ -30,25 +31,34 @@ const resolvers = {
describe('apollo-server-koa', () => {
const { ApolloServer } = require('../ApolloServer');
- const Koa = require('koa');
- let server: ApolloServer;
- let httpServer: http.Server;
+ let serverToCleanUp: ApolloServer | null = null;
testApolloServer(
async (config: any, options) => {
- server = new ApolloServer(config);
+ serverToCleanUp = null;
+ const httpServer = http.createServer();
+ const server = new ApolloServer({
+ ...config,
+ plugins: [
+ ...(config.plugins ?? []),
+ ApolloServerPluginDrainHttpServer({
+ httpServer: httpServer,
+ }),
+ ],
+ });
if (!options?.suppressStartCall) {
await server.start();
+ serverToCleanUp = server;
}
const app = new Koa();
server.applyMiddleware({ app, path: options?.graphqlPath });
- httpServer = await new Promise((resolve) => {
- const s = app.listen({ port: 0 }, () => resolve(s));
+ httpServer.on('request', app.callback());
+ await new Promise((resolve) => {
+ httpServer.listen({ port: 0 }, () => resolve());
});
return createServerInfo(server, httpServer);
},
async () => {
- if (server) await server.stop();
- if (httpServer?.listening) await httpServer.close();
+ await serverToCleanUp?.stop();
},
);
});
diff --git a/packages/apollo-server-lambda/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-lambda/src/__tests__/ApolloServer.test.ts
index cc9c8950227..d3b1afa5cb6 100644
--- a/packages/apollo-server-lambda/src/__tests__/ApolloServer.test.ts
+++ b/packages/apollo-server-lambda/src/__tests__/ApolloServer.test.ts
@@ -2,7 +2,11 @@ import http from 'http';
import request from 'supertest';
import express from 'express';
import { createMockServer } from './mockAPIGatewayServer';
-import { Config, gql } from 'apollo-server-core';
+import {
+ ApolloServerPluginDrainHttpServer,
+ Config,
+ gql,
+} from 'apollo-server-core';
import {
ApolloServer,
CreateHandlerOptions,
@@ -26,30 +30,49 @@ const resolvers = {
};
describe('apollo-server-lambda', () => {
- let server: ApolloServer;
- let httpServer: http.Server;
+ let serverToCleanUp: ApolloServer | null = null;
testApolloServer(
async (config: Config, options) => {
- server = new ApolloServer(config);
+ serverToCleanUp = null;
+ const httpServer = http.createServer();
+ const server = new ApolloServer({
+ ...config,
+ plugins: [
+ ...(config.plugins ?? []),
+ // You don't typically need to use this plugin in a real Lambda
+ // environment, but our test harness sets up an http.Server, so we use
+ // it here.
+ ApolloServerPluginDrainHttpServer({
+ httpServer: httpServer,
+ }),
+ ],
+ });
// Ignore suppressStartCall because serverless ApolloServers don't
- // get `start`ed.
+ // get manually `start`ed. However, if no requests will be made then
+ // it won't get `start`ed and that's the condition we need to use to
+ // decide whether or not to `stop`.
+ if (!options?.noRequestsMade) {
+ serverToCleanUp = server;
+ }
const lambdaHandler = server.createHandler({
expressGetMiddlewareOptions: { path: options?.graphqlPath },
});
- const httpHandler = createMockServer(lambdaHandler);
- httpServer = new http.Server(httpHandler);
+ httpServer.on('request', createMockServer(lambdaHandler));
await new Promise((resolve) => {
httpServer.listen({ port: 0 }, () => resolve());
});
- return createServerInfo(server, httpServer);
+ const serverInfo = createServerInfo(server, httpServer);
+ if (options?.noRequestsMade) {
+ // Since no requests will be made (and the server won't even start), we
+ // immediately close the HTTP server. (We made it at all so we can have
+ // a typesafe return value from this function, though the only test that
+ // uses this mode ignores the return value from this function.)
+ await new Promise((resolve) => httpServer.close(() => resolve()));
+ }
+ return serverInfo;
},
async () => {
- if (httpServer?.listening) {
- await new Promise((resolve) => {
- httpServer.close(() => resolve());
- });
- }
- if (server) await server.stop();
+ await serverToCleanUp?.stop();
},
{ serverlessFramework: true },
);
diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts
index 5c7774a5693..5b1df8ce8b3 100644
--- a/packages/apollo-server-plugin-base/src/index.ts
+++ b/packages/apollo-server-plugin-base/src/index.ts
@@ -60,6 +60,19 @@ export interface ApolloServerPlugin<
export interface GraphQLServerListener {
schemaDidLoadOrUpdate?(schemaContext: GraphQLSchemaContext): void;
+ // When your server is stopped (by calling `stop()` or via the
+ // `SIGINT`/`SIGTERM` handlers), Apollo Server first awaits all `drainServer`
+ // hooks in parallel. GraphQL operations can still execute while `drainServer`
+ // is in progress. A typical use is to stop listening for new connections and
+ // wait until all current connections are idle. The built-in
+ // ApolloServerPluginDrainHttpServer implements this method.
+ drainServer?(): Promise;
+
+ // When your server is stopped (by calling `stop()` or via the
+ // `SIGINT`/`SIGTERM` handlers) then (after the `drainServer` phase finishes)
+ // Apollo Server transitions into a state where no new operations will run and
+ // then awaits all `drainServer` hooks in parallel. A typical use is to flush
+ // outstanding observability data.
serverWillStop?(): Promise;
// At most one plugin's serverWillStart may return a GraphQLServerListener
diff --git a/packages/apollo-server/src/index.ts b/packages/apollo-server/src/index.ts
index a47e0683d12..f5717ec1769 100644
--- a/packages/apollo-server/src/index.ts
+++ b/packages/apollo-server/src/index.ts
@@ -12,7 +12,7 @@ import {
} from 'apollo-server-express';
import type { AddressInfo } from 'net';
import { format as urlFormat } from 'url';
-import { Stopper } from './stoppable';
+import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
export * from './exports';
@@ -25,10 +25,9 @@ export interface ServerInfo {
}
export class ApolloServer extends ApolloServerExpress {
- private stopper?: Stopper;
private cors?: CorsOptions | boolean;
private onHealthCheck?: (req: express.Request) => Promise;
- private stopGracePeriodMillis: number;
+ private httpServer: http.Server;
constructor(
config: ApolloServerExpressConfig & {
@@ -37,14 +36,25 @@ export class ApolloServer extends ApolloServerExpress {
stopGracePeriodMillis?: number;
},
) {
- super(config);
- this.cors = config?.cors;
- this.onHealthCheck = config?.onHealthCheck;
- this.stopGracePeriodMillis = config?.stopGracePeriodMillis ?? 10_000;
+ const httpServer = http.createServer();
+ super({
+ ...config,
+ plugins: [
+ ...(config.plugins ?? []),
+ ApolloServerPluginDrainHttpServer({
+ httpServer: httpServer,
+ stopGracePeriodMillis: config.stopGracePeriodMillis,
+ }),
+ ],
+ });
+
+ this.httpServer = httpServer;
+ this.cors = config.cors;
+ this.onHealthCheck = config.onHealthCheck;
}
- private createServerInfo(server: http.Server): ServerInfo {
- const addressInfo = server.address() as AddressInfo;
+ private createServerInfo(): ServerInfo {
+ const addressInfo = this.httpServer.address() as AddressInfo;
// Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
// corresponding loopback ip. If this heuristic is wrong for your use case,
@@ -64,7 +74,7 @@ export class ApolloServer extends ApolloServerExpress {
return {
...addressInfo,
- server,
+ server: this.httpServer,
url,
};
}
@@ -90,12 +100,13 @@ export class ApolloServer extends ApolloServerExpress {
// This class is the easy mode for people who don't create their own express
// object, so we have to create it.
const app = express();
+ this.httpServer.on('request', app);
app.disable('x-powered-by');
// provide generous values for the getting started experience
super.applyMiddleware({
- app,
+ app: app,
path: '/',
bodyParserConfig: { limit: '50mb' },
onHealthCheck: this.onHealthCheck,
@@ -107,41 +118,14 @@ export class ApolloServer extends ApolloServerExpress {
},
});
- const httpServer = http.createServer(app);
-
- this.stopper = new Stopper(httpServer);
-
await new Promise((resolve) => {
- httpServer.once('listening', resolve);
+ this.httpServer.once('listening', resolve);
// If the user passed a callback to listen, it'll get called in addition
// to our resolver. They won't have the ability to get the ServerInfo
// object unless they use our Promise, though.
- httpServer.listen(...(opts.length ? opts : [{ port: 4000 }]));
+ this.httpServer.listen(...(opts.length ? opts : [{ port: 4000 }]));
});
- return this.createServerInfo(httpServer);
- }
-
- public override async stop() {
- // First drain the HTTP server. (See #5074 for a plan to generalize this to
- // the web framework integrations.)
- //
- // `Stopper.stop` is an async function which:
- // - closes the server (ie, stops listening)
- // - closes all connections with no active requests
- // - continues to close connections when their active request count drops to
- // zero
- // - in 10 seconds (configurable), closes all remaining active connections
- // - returns (async) once there are no remaining active connections
- //
- // If you don't like this behavior, use apollo-server-express instead of
- // apollo-server.
- const { stopper } = this;
- if (stopper) {
- this.stopper = undefined;
- await stopper.stop(this.stopGracePeriodMillis);
- }
-
- await super.stop();
+ return this.createServerInfo();
}
}