fix: Race condition can cause multiple Apollo server initializations under load (#9929)

This commit is contained in:
Antoine Cormouls
2025-11-17 16:18:39 +01:00
committed by GitHub
parent 306c5fd830
commit 7d5e9fcf3c
2 changed files with 46 additions and 15 deletions

View File

@@ -118,6 +118,20 @@ describe('ParseGraphQLServer', () => {
expect(server3).not.toBe(server2); expect(server3).not.toBe(server2);
expect(server3).toBe(server4); 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', () => { describe('_getGraphQLOptions', () => {

View File

@@ -97,21 +97,38 @@ class ParseGraphQLServer {
if (schemaRef === newSchemaRef && this._server) { if (schemaRef === newSchemaRef && this._server) {
return this._server; return this._server;
} }
const { schema, context } = await this._getGraphQLOptions(); // It means a parallel _getServer call is already in progress
const apollo = new ApolloServer({ if (this._schemaRefMutex === newSchemaRef) {
csrfPrevention: { return this._server;
// See https://www.apollographql.com/docs/router/configuration/csrf/ }
// needed since we use graphql upload // Update the schema ref mutex to avoid parallel _getServer calls
requestHeaders: ['X-Parse-Application-Id'], this._schemaRefMutex = newSchemaRef;
}, const createServer = async () => {
introspection: this.config.graphQLPublicIntrospection, try {
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], const { schema, context } = await this._getGraphQLOptions();
schema, const apollo = new ApolloServer({
}); csrfPrevention: {
await apollo.start(); // See https://www.apollographql.com/docs/router/configuration/csrf/
this._server = expressMiddleware(apollo, { // needed since we use graphql upload
context, 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; return this._server;
} }