Drain httpServer when apollo server stops

This commit is contained in:
Hongbo Wu 2024-04-18 21:30:00 +08:00
parent 273ff9d4be
commit ac775ae8f2
2 changed files with 31 additions and 26 deletions

View file

@ -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<ExpressContext, ResolverContext> = 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)

View file

@ -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<void> => {
}
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<void> => {
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<void> => {
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<void>((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()