-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add serverWillStop lifecycle hook; call stop() on signals by default #4450
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -31,6 +31,7 @@ import { | |||||||||||||||||||||
import { | ||||||||||||||||||||||
ApolloServerPlugin, | ||||||||||||||||||||||
GraphQLServiceContext, | ||||||||||||||||||||||
GraphQLServerListener, | ||||||||||||||||||||||
} from 'apollo-server-plugin-base'; | ||||||||||||||||||||||
import runtimeSupportsUploads from './utils/runtimeSupportsUploads'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -76,13 +77,14 @@ import { | |||||||||||||||||||||
import { Headers } from 'apollo-server-env'; | ||||||||||||||||||||||
import { buildServiceDefinition } from '@apollographql/apollo-tools'; | ||||||||||||||||||||||
import { plugin as pluginTracing } from "apollo-tracing"; | ||||||||||||||||||||||
import { Logger, SchemaHash } from "apollo-server-types"; | ||||||||||||||||||||||
import { Logger, SchemaHash, ValueOrPromise } from "apollo-server-types"; | ||||||||||||||||||||||
import { | ||||||||||||||||||||||
plugin as pluginCacheControl, | ||||||||||||||||||||||
CacheControlExtensionOptions, | ||||||||||||||||||||||
} from 'apollo-cache-control'; | ||||||||||||||||||||||
import { getEngineApiKey, getEngineGraphVariant } from "apollo-engine-reporting/dist/agent"; | ||||||||||||||||||||||
import { cloneObject } from "./runHttpQuery"; | ||||||||||||||||||||||
import isNodeLike from './utils/isNodeLike'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
const NoIntrospection = (context: ValidationContext) => ({ | ||||||||||||||||||||||
Field(node: FieldDefinitionNode) { | ||||||||||||||||||||||
|
@@ -149,7 +151,7 @@ export class ApolloServerBase { | |||||||||||||||||||||
private config: Config; | ||||||||||||||||||||||
/** @deprecated: This is undefined for servers operating as gateways, and will be removed in a future release **/ | ||||||||||||||||||||||
protected schema?: GraphQLSchema; | ||||||||||||||||||||||
private toDispose = new Set<() => void>(); | ||||||||||||||||||||||
private toDispose = new Set<() => ValueOrPromise<void>>(); | ||||||||||||||||||||||
private experimental_approximateDocumentStoreMiB: | ||||||||||||||||||||||
Config['experimental_approximateDocumentStoreMiB']; | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -177,6 +179,7 @@ export class ApolloServerBase { | |||||||||||||||||||||
gateway, | ||||||||||||||||||||||
cacheControl, | ||||||||||||||||||||||
experimental_approximateDocumentStoreMiB, | ||||||||||||||||||||||
stopOnTerminationSignals, | ||||||||||||||||||||||
...requestOptions | ||||||||||||||||||||||
} = config; | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -385,6 +388,32 @@ export class ApolloServerBase { | |||||||||||||||||||||
// is populated accordingly. | ||||||||||||||||||||||
this.ensurePluginInstantiation(plugins); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// We handle signals if it was explicitly requested, or if we're in Node, | ||||||||||||||||||||||
// not in a test, and it wasn't explicitly turned off. (For backwards | ||||||||||||||||||||||
// compatibility, we check both 'stopOnTerminationSignals' and | ||||||||||||||||||||||
// 'engine.handleSignals'.) | ||||||||||||||||||||||
if ( | ||||||||||||||||||||||
typeof stopOnTerminationSignals === 'boolean' | ||||||||||||||||||||||
? stopOnTerminationSignals | ||||||||||||||||||||||
: typeof this.config.engine === 'object' && | ||||||||||||||||||||||
typeof this.config.engine.handleSignals === 'boolean' | ||||||||||||||||||||||
? this.config.engine.handleSignals | ||||||||||||||||||||||
: isNodeLike && process.env.NODE_ENV !== 'test' | ||||||||||||||||||||||
) { | ||||||||||||||||||||||
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']; | ||||||||||||||||||||||
signals.forEach((signal) => { | ||||||||||||||||||||||
// Note: Node only started sending signal names to signal events with | ||||||||||||||||||||||
// Node v10 so we can't use that feature here. | ||||||||||||||||||||||
const handler: NodeJS.SignalsListener = async () => { | ||||||||||||||||||||||
await this.stop(); | ||||||||||||||||||||||
process.kill(process.pid, signal); | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realize this originated in the code from before, but I don't understand why we need send the same There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The theory is that we are not trying to prevent the process from dying when asked to, but just let it do some work before dying as requested. It's not really a primitive that composes well, but I don't know of a better alternative. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't that still happen if we didn't call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may be confused, but I'm pretty sure that handling a signal means that the signal is handled and the default behavior of the process exiting doesn't occur. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I didn't actually believe that to be the case, but I haven't double clicked on that idea in a while. |
||||||||||||||||||||||
}; | ||||||||||||||||||||||
process.once(signal, handler); | ||||||||||||||||||||||
this.toDispose.add(() => { | ||||||||||||||||||||||
process.removeListener(signal, handler); | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// used by integrations to synchronize the path with subscriptions, some | ||||||||||||||||||||||
|
@@ -585,24 +614,33 @@ export class ApolloServerBase { | |||||||||||||||||||||
if (this.requestOptions.persistedQueries?.cache) { | ||||||||||||||||||||||
service.persistedQueries = { | ||||||||||||||||||||||
cache: this.requestOptions.persistedQueries.cache, | ||||||||||||||||||||||
} | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
await Promise.all( | ||||||||||||||||||||||
this.plugins.map( | ||||||||||||||||||||||
plugin => | ||||||||||||||||||||||
plugin.serverWillStart && | ||||||||||||||||||||||
plugin.serverWillStart(service), | ||||||||||||||||||||||
), | ||||||||||||||||||||||
const serverListeners = ( | ||||||||||||||||||||||
await Promise.all( | ||||||||||||||||||||||
this.plugins.map( | ||||||||||||||||||||||
(plugin) => plugin.serverWillStart && plugin.serverWillStart(service), | ||||||||||||||||||||||
), | ||||||||||||||||||||||
) | ||||||||||||||||||||||
).filter( | ||||||||||||||||||||||
(maybeServerListener): maybeServerListener is GraphQLServerListener => | ||||||||||||||||||||||
typeof maybeServerListener === 'object' && | ||||||||||||||||||||||
!!maybeServerListener.serverWillStop, | ||||||||||||||||||||||
); | ||||||||||||||||||||||
this.toDispose.add(async () => { | ||||||||||||||||||||||
await Promise.all( | ||||||||||||||||||||||
serverListeners.map(({ serverWillStop }) => serverWillStop?.()), | ||||||||||||||||||||||
); | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
Comment on lines
+631
to
+635
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the additional
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think harmless is probably a true assessment, particularly since this is not a hot-spot in the code from a performance standpoint, but I think it's worth noting that it does create an additional Promise which needs to be resolved and await always yields to the event loop so it would be processed on the next tick. (Might even add an entry to the stack?) If we were doing it often, there might be memory implications. While some runtimes might (eventually) optimize it out via an optimization known as tail call optimization, that is not an optimization that exists in V8 today and it may never land in many runtimes (See link). Since this is a shutdown mechanism, we might actually be counting down the ticks until we can terminate, though I don't think anything here would be anything more than a microtask so I don't believe we're actually putting anything on the next full turn of the event loop. I might say that returning the Promise directly is probably preferred. However, to be very clear, this is just me trying to shed light on Promise execution dynamics, not me asking for a change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your suggestion here doesn't typecheck because toDispose is supposed to return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right. |
||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
public async stop() { | ||||||||||||||||||||||
this.toDispose.forEach(dispose => dispose()); | ||||||||||||||||||||||
await Promise.all([...this.toDispose].map(dispose => dispose())); | ||||||||||||||||||||||
if (this.subscriptionServer) await this.subscriptionServer.close(); | ||||||||||||||||||||||
if (this.engineReportingAgent) { | ||||||||||||||||||||||
this.engineReportingAgent.stop(); | ||||||||||||||||||||||
await this.engineReportingAgent.sendAllReports(); | ||||||||||||||||||||||
await this.engineReportingAgent.sendAllReportsAndReportErrors(); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import { | |
import { | ||
ApolloServerPlugin, | ||
GraphQLRequestExecutionListener, | ||
GraphQLServerListener, | ||
} from 'apollo-server-plugin-base'; | ||
import { InMemoryLRUCache } from 'apollo-server-caching'; | ||
import { Dispatcher } from './dispatcher'; | ||
|
@@ -98,16 +99,19 @@ export default async function pluginTestHarness<TContext>({ | |
} | ||
|
||
const schemaHash = generateSchemaHash(schema); | ||
let serverListener: GraphQLServerListener | undefined; | ||
if (typeof pluginInstance.serverWillStart === 'function') { | ||
pluginInstance.serverWillStart({ | ||
const maybeServerListener = await pluginInstance.serverWillStart({ | ||
logger: logger || console, | ||
schema, | ||
schemaHash, | ||
engine: {}, | ||
}); | ||
if (maybeServerListener && maybeServerListener.serverWillStop) { | ||
serverListener = maybeServerListener; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, but what are your thoughts of calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes to maybeServerListener which matches the type better |
||
} | ||
|
||
|
||
const requestContext: GraphQLRequestContext<TContext> = { | ||
logger: logger || console, | ||
schema, | ||
|
@@ -188,5 +192,7 @@ export default async function pluginTestHarness<TContext>({ | |
requestContext as GraphQLRequestContextWillSendResponse<TContext>, | ||
); | ||
|
||
await serverListener?.serverWillStop?.(); | ||
|
||
return requestContext as GraphQLRequestContextWillSendResponse<TContext>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,7 +62,7 @@ describe('apollo-server-express', () => { | |
serverOptions: ApolloServerExpressConfig, | ||
options: Partial<ServerRegistration> = {}, | ||
) { | ||
server = new ApolloServer(serverOptions); | ||
server = new ApolloServer({stopOnTerminationSignals: false, ...serverOptions}); | ||
app = express(); | ||
|
||
server.applyMiddleware({ ...options, app }); | ||
|
@@ -184,13 +184,12 @@ describe('apollo-server-express', () => { | |
}); | ||
|
||
it('renders GraphQL playground using request original url', async () => { | ||
const nodeEnv = process.env.NODE_ENV; | ||
delete process.env.NODE_ENV; | ||
const samplePath = '/innerSamplePath'; | ||
|
||
const rewiredServer = new ApolloServer({ | ||
typeDefs, | ||
resolvers, | ||
playground: true, | ||
Comment on lines
-187
to
+192
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this just a hacky workaround to make playground render in a testing env? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking about this as the opposite — setting NODE_ENV to mean "render playground" is a hacky workaround, whereas given that this is a test of playground functionality, asking for what you need makes sense. I left in some tests which set NODE_ENV which are explicitly saying "make sure playground is on by default in production" though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're on the same page. This was a hacky workaround, but I prefer the change you made here. Just confirming my understanding of the change. |
||
}); | ||
const innerApp = express(); | ||
rewiredServer.applyMiddleware({ app: innerApp }); | ||
|
@@ -218,7 +217,6 @@ describe('apollo-server-express', () => { | |
}, | ||
}, | ||
(error, response, body) => { | ||
process.env.NODE_ENV = nodeEnv; | ||
if (error) { | ||
reject(error); | ||
} else { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a deprecation note similar to the ones immediately above? Most importantly, I'd just like to see the actual word "deprecated" here somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do (next week)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I'd rather this not merge until #4453 merges too, at which point this entire EngineReportingOptions section will be deprecated.