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..231e44f5ef 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -97,21 +97,38 @@ 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 () => { + 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(); return this._server; }