diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 634bcf3d23..4c6a86948c 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -135,6 +135,29 @@ describe('ParseGraphQLServer', () => { expect(server).toBe(firstServer); }); }); + + it('does not leak process signal listeners across schema rebuilds (#9813)', async () => { + const originalNodeEnv = process.env.NODE_ENV; + // Apollo registers SIGINT/SIGTERM handlers only when NODE_ENV !== 'test'; force the + // production path so the (pre-fix) leak is exercised deterministically. + process.env.NODE_ENV = 'production'; + try { + parseGraphQLServer.server = undefined; + await parseGraphQLServer._getServer(); + const before = process.listenerCount('SIGTERM') + process.listenerCount('SIGINT'); + // Force several real schema rebuilds (each new class changes the schema). + for (let i = 0; i < 5; i++) { + await new Parse.Object(`LeakClass${i}`).save(); + await parseGraphQLServer._getServer(); + } + const after = process.listenerCount('SIGTERM') + process.listenerCount('SIGINT'); + // Fix: discarded ApolloServers register no process listeners, so the count is stable. + // Baseline: each rebuild leaks a SIGINT + SIGTERM listener (grows by 2 per rebuild). + expect(after).toBe(before); + } finally { + process.env.NODE_ENV = originalNodeEnv; + } + }); }); describe('_getGraphQLOptions', () => { diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 9d18f87d94..2dc0fd04ed 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -180,6 +180,13 @@ class ParseGraphQLServer { // We need always true introspection because apollo server have changing behavior based on the NODE_ENV variable // we delegate the introspection control to the IntrospectionControlPlugin introspection: true, + // A new ApolloServer is created every time the GraphQL schema changes, and Parse Server + // already manages its own SIGTERM/SIGINT graceful shutdown (see configureListeners in + // ParseServer). Left to its default, apollo.start() registers a SIGINT and a SIGTERM + // listener on the global `process` per build and only removes them on apollo.stop() — + // which is never called on the discarded server — pinning every old ApolloServer and its + // entire GraphQL schema graph in memory forever. (#9813) + stopOnTerminationSignals: false, plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), SchemaSuggestionsControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)], schema, });