From ac775ae8f255de1d22cec35c2a1bf780dd4d9d55 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 18 Apr 2024 21:30:00 +0800 Subject: [PATCH] Drain httpServer when apollo server stops --- packages/api/src/apollo.ts | 37 ++++++++++++++++++++++++++----------- packages/api/src/server.ts | 20 +++++--------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index 26d255e5d..68e1c3645 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -4,16 +4,24 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/require-await */ +import { createPrometheusExporterPlugin } from '@bmatei/apollo-prometheus-exporter' import { makeExecutableSchema } from '@graphql-tools/schema' import * as Sentry from '@sentry/node' -import { ContextFunction, PluginDefinition } from 'apollo-server-core' -import { Express } from 'express' +import { + ApolloServerPluginDrainHttpServer, + ContextFunction, + PluginDefinition, +} from 'apollo-server-core' import { ApolloServer } from 'apollo-server-express' import { ExpressContext } from 'apollo-server-express/dist/ApolloServer' +import { ApolloServerPlugin } from 'apollo-server-plugin-base' +import { Express } from 'express' import * as httpContext from 'express-http-context2' +import type http from 'http' import * as jwt from 'jsonwebtoken' import { EntityManager } from 'typeorm' import { promisify } from 'util' +import { ReadingProgressDataSource } from './datasources/reading_progress_data_source' import { appDataSource } from './data_source' import { sanitizeDirectiveTransformer } from './directives' import { env } from './env' @@ -22,17 +30,14 @@ import { functionResolvers } from './resolvers/function_resolvers' import { ClaimsToSet, RequestContext, ResolverContext } from './resolvers/types' import ScalarResolvers from './scalars' import typeDefs from './schema' -import { tracer } from './tracing' -import { getClaimsByToken, setAuthInCookie } from './utils/auth' -import { SetClaimsRole } from './utils/dictionary' -import { logger } from './utils/logger' -import { ReadingProgressDataSource } from './datasources/reading_progress_data_source' -import { createPrometheusExporterPlugin } from '@bmatei/apollo-prometheus-exporter' -import { ApolloServerPlugin } from 'apollo-server-plugin-base' import { countDailyServiceUsage, createServiceUsage, } from './services/service_usage' +import { tracer } from './tracing' +import { getClaimsByToken, setAuthInCookie } from './utils/auth' +import { SetClaimsRole } from './utils/dictionary' +import { logger } from './utils/logger' const signToken = promisify(jwt.sign) const pubsub = createPubSubClient() @@ -100,7 +105,10 @@ const contextFunc: ContextFunction = async ({ return ctx } -export function makeApolloServer(app: Express): ApolloServer { +export function makeApolloServer( + app: Express, + httpServer: http.Server +): ApolloServer { let schema = makeExecutableSchema({ resolvers, typeDefs, @@ -169,7 +177,14 @@ export function makeApolloServer(app: Express): ApolloServer { const apollo = new ApolloServer({ schema: schema, context: contextFunc, - plugins: [promExporter, usageLimitPlugin], + plugins: [ + // Our httpServer handles incoming requests to our Express app. + // Below, we tell Apollo Server to "drain" this httpServer, + // enabling our servers to shut down gracefully. + ApolloServerPluginDrainHttpServer({ httpServer }), + promExporter, + usageLimitPlugin, + ], formatError: (err) => { logger.info('server error', err) Sentry.captureException(err) diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index e1603a6cb..58cdafbda 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -4,13 +4,12 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ import * as lw from '@google-cloud/logging-winston' import * as Sentry from '@sentry/node' -import { ApolloServer } from 'apollo-server-express' import { json, urlencoded } from 'body-parser' import cookieParser from 'cookie-parser' import express, { Express } from 'express' import * as httpContext from 'express-http-context2' import promBundle from 'express-prom-bundle' -import { createServer, Server } from 'http' +import { createServer } from 'http' import * as prom from 'prom-client' import { config, loggers } from 'winston' import { makeApolloServer } from './apollo' @@ -150,7 +149,8 @@ const main = async (): Promise => { } const app = createApp() - const apollo = makeApolloServer(app) + const httpServer = createServer(app) + const apollo = makeApolloServer(app, httpServer) await apollo.start() apollo.applyMiddleware({ app, path: '/api/graphql', cors: corsConfig }) @@ -159,7 +159,7 @@ const main = async (): Promise => { const mw = await lw.express.makeMiddleware(mwLogger, transport) app.use(mw) - const listener = app.listen({ port: PORT }, async () => { + const listener = httpServer.listen({ port: PORT }, async () => { const logger = buildLogger('app.dispatch') logger.notice(`🚀 Server ready at ${apollo.graphqlPath}`) }) @@ -176,22 +176,12 @@ const main = async (): Promise => { const gracefulShutdown = async (signal: string) => { console.log(`[api]: Received ${signal}, closing server...`) await apollo.stop() - console.log('[api]: Apollo server stopped') + console.log('[api]: Express server stopped') console.log('[posthog]: flushing events') await analytics.shutdownAsync() console.log('[posthog]: events flushed') - await new Promise((resolve) => { - listener.close((err) => { - console.log('[api]: Express listener closed') - if (err) { - console.log('[api]: error stopping listener', { err }) - } - resolve() - }) - }) - // Shutdown redis before DB because the quit sequence can // cause appDataSource to get reloaded in the callback await redisDataSource.shutdown()