From bc4b25d9522b2ebb318937d6823059df15f690c2 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Mon, 10 Nov 2025 18:24:07 +0100 Subject: [PATCH 1/2] fix: apollo parallel server init under load --- spec/ParseGraphQLServer.spec.js | 14 +++++++++++ src/GraphQL/ParseGraphQLServer.js | 40 +++++++++++++++++++------------ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index aee3575079..5031a9cdff 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -118,6 +118,20 @@ describe('ParseGraphQLServer', () => { expect(server3).not.toBe(server2); expect(server3).toBe(server4); }); + + it('should return same server reference when called 100 times in parallel', async () => { + parseGraphQLServer.server = undefined; + + // Call _getServer 100 times in parallel + const promises = Array.from({ length: 100 }, () => parseGraphQLServer._getServer()); + const servers = await Promise.all(promises); + + // All resolved servers should be the same reference + const firstServer = servers[0]; + servers.forEach((server, index) => { + expect(server).toBe(firstServer); + }); + }); }); describe('_getGraphQLOptions', () => { diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index bf7e14f7e2..f6bfb1ea62 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -97,21 +97,31 @@ class ParseGraphQLServer { if (schemaRef === newSchemaRef && this._server) { return this._server; } - const { schema, context } = await this._getGraphQLOptions(); - const apollo = new ApolloServer({ - csrfPrevention: { - // See https://www.apollographql.com/docs/router/configuration/csrf/ - // needed since we use graphql upload - requestHeaders: ['X-Parse-Application-Id'], - }, - introspection: this.config.graphQLPublicIntrospection, - plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], - schema, - }); - await apollo.start(); - this._server = expressMiddleware(apollo, { - context, - }); + // It means a parallel _getServer call is already in progress + if (this._schemaRefMutex === newSchemaRef) { + return this._server; + } + // Update the schema ref mutex to avoid parallel _getServer calls + this._schemaRefMutex = newSchemaRef; + const createServer = async () => { + const { schema, context } = await this._getGraphQLOptions(); + const apollo = new ApolloServer({ + csrfPrevention: { + // See https://www.apollographql.com/docs/router/configuration/csrf/ + // needed since we use graphql upload + requestHeaders: ['X-Parse-Application-Id'], + }, + introspection: this.config.graphQLPublicIntrospection, + plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], + schema, + }); + await apollo.start(); + return expressMiddleware(apollo, { + context, + }); + } + // Do not await so parallel request will wait the same promise ref + this._server = createServer(); return this._server; } From 41457d63b38445d85b2df292f4945d1a1377e4a7 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Mon, 10 Nov 2025 18:37:10 +0100 Subject: [PATCH 2/2] fix: mutex in case of init error --- src/GraphQL/ParseGraphQLServer.js | 37 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index f6bfb1ea62..231e44f5ef 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -104,21 +104,28 @@ class ParseGraphQLServer { // Update the schema ref mutex to avoid parallel _getServer calls this._schemaRefMutex = newSchemaRef; const createServer = async () => { - const { schema, context } = await this._getGraphQLOptions(); - const apollo = new ApolloServer({ - csrfPrevention: { - // See https://www.apollographql.com/docs/router/configuration/csrf/ - // needed since we use graphql upload - requestHeaders: ['X-Parse-Application-Id'], - }, - introspection: this.config.graphQLPublicIntrospection, - plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], - schema, - }); - await apollo.start(); - return expressMiddleware(apollo, { - context, - }); + try { + const { schema, context } = await this._getGraphQLOptions(); + const apollo = new ApolloServer({ + csrfPrevention: { + // See https://www.apollographql.com/docs/router/configuration/csrf/ + // needed since we use graphql upload + requestHeaders: ['X-Parse-Application-Id'], + }, + introspection: this.config.graphQLPublicIntrospection, + plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], + schema, + }); + await apollo.start(); + return expressMiddleware(apollo, { + context, + }); + } catch (e) { + // Reset all mutexes and forward the error + this._server = null; + this._schemaRefMutex = null; + throw e; + } } // Do not await so parallel request will wait the same promise ref this._server = createServer();