Skip to content

Bind context to async execution avoiding race-conditions #2521

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shiny-turkeys-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-modules': patch
---

Bind context to async execution avoiding race-conditions
46 changes: 25 additions & 21 deletions packages/graphql-modules/src/application/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { wrapSchema } from '@graphql-tools/wrap';
import { DocumentNode, execute, GraphQLSchema } from 'graphql';
import { uniqueId } from '../shared/utils';
import { InternalAppContext } from './application';
import { ExecutionContextBuilder } from './context';
import { ExecutionContextBuilder, ExecutionContextEnv } from './context';
import { Application } from './types';

const CONTEXT_ID = Symbol.for('context-id');
Expand Down Expand Up @@ -60,11 +60,12 @@ export function apolloSchemaCreator({
> = {};
const subscription = createSubscription();

function getSession(ctx: any) {
function getSession(
ctx: any,
{ context, ɵdestroy: destroy }: ExecutionContextEnv
) {
if (!ctx[CONTEXT_ID]) {
ctx[CONTEXT_ID] = uniqueId((id) => !sessions[id]);
const { context, ɵdestroy: destroy } = contextBuilder(ctx);

sessions[ctx[CONTEXT_ID]] = {
count: 0,
session: {
Expand Down Expand Up @@ -99,24 +100,27 @@ export function apolloSchemaCreator({
operationName: input.operationName,
});
}
// Create an execution context
const { context, destroy } = getSession(input.context!);

// It's important to wrap the executeFn within a promise
// so we can easily control the end of execution (with finally)
return Promise.resolve()
.then(
() =>
execute({
schema,
document: input.document,
contextValue: context,
variableValues: input.variables as any,
rootValue: input.rootValue,
operationName: input.operationName,
}) as any
)
.finally(destroy);
// Create an execution context and run within it
return contextBuilder(input.context!).runWithContext((env) => {
const { context, destroy } = getSession(input.context!, env);

// It's important to wrap the executeFn within a promise
// so we can easily control the end of execution (with finally)
return Promise.resolve()
.then(
() =>
execute({
schema,
document: input.document,
contextValue: context,
variableValues: input.variables as any,
rootValue: input.rootValue,
operationName: input.operationName,
}) as any
)
.finally(destroy);
});
},
});
};
Expand Down
40 changes: 36 additions & 4 deletions packages/graphql-modules/src/application/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AsyncLocalStorage } from 'async_hooks';
import { Injector, ReflectiveInjector } from '../di';
import { ResolvedProvider } from '../di/resolution';
import { ID } from '../shared/types';
Expand All @@ -6,11 +7,22 @@ import type { InternalAppContext, ModulesMap } from './application';
import { attachGlobalProvidersMap } from './di';
import { CONTEXT } from './tokens';

const alc = new AsyncLocalStorage<{
getApplicationContext(): GraphQLModules.AppContext;
getModuleContext(moduleId: string): GraphQLModules.ModuleContext;
}>();

export type ExecutionContextBuilder<
TContext extends {
[key: string]: any;
} = {},
> = (context: TContext) => {
> = (context: TContext) => ExecutionContextEnv & {
runWithContext<TReturn = any>(
cb: (env: ExecutionContextEnv) => TReturn
): TReturn;
};

export type ExecutionContextEnv = {
context: InternalAppContext;
ɵdestroy(): void;
ɵinjector: Injector;
Expand Down Expand Up @@ -67,12 +79,15 @@ export function createContextBuilder({
});

appInjector.setExecutionContextGetter(function executionContextGetter() {
return appContext;
return alc.getStore()?.getApplicationContext() || appContext;
} as any);

function createModuleExecutionContextGetter(moduleId: string) {
return function moduleExecutionContextGetter() {
return getModuleContext(moduleId, context);
return (
alc.getStore()?.getModuleContext(moduleId) ||
getModuleContext(moduleId, context)
);
};
}

Expand Down Expand Up @@ -164,7 +179,7 @@ export function createContextBuilder({
},
});

return {
const env: ExecutionContextEnv = {
ɵdestroy: once(() => {
providersToDestroy.forEach(([injector, keyId]) => {
// If provider was instantiated
Expand All @@ -178,6 +193,23 @@ export function createContextBuilder({
ɵinjector: operationAppInjector,
context: sharedContext,
};

return {
...env,
runWithContext(cb) {
return alc.run(
{
getApplicationContext() {
return appContext;
},
getModuleContext(moduleId) {
return getModuleContext(moduleId, context);
},
},
() => cb(env)
);
},
};
};

return contextBuilder;
Expand Down
70 changes: 40 additions & 30 deletions packages/graphql-modules/src/application/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Application } from './types';
import { ExecutionContextBuilder } from './context';
import { Maybe } from '../shared/types';
import { isNotSchema } from '../shared/utils';
import { InternalAppContext } from './application';

export function executionCreator({
contextBuilder,
Expand All @@ -30,38 +31,47 @@ export function executionCreator({
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>
) => {
// Create an execution context
const { context, ɵdestroy: destroy } =
options?.controller ??
contextBuilder(
isNotSchema<ExecutionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
);
function perform({
context,
ɵdestroy: destroy,
}: {
context: InternalAppContext;
ɵdestroy: () => void;
}) {
const executionArgs: ExecutionArgs = isNotSchema<ExecutionArgs>(
argsOrSchema
)
? {
...argsOrSchema,
contextValue: context,
}
: {
schema: argsOrSchema,
document: document!,
rootValue,
contextValue: context,
variableValues,
operationName,
fieldResolver,
typeResolver,
};

const executionArgs: ExecutionArgs = isNotSchema<ExecutionArgs>(
argsOrSchema
)
? {
...argsOrSchema,
contextValue: context,
}
: {
schema: argsOrSchema,
document: document!,
rootValue,
contextValue: context,
variableValues,
operationName,
fieldResolver,
typeResolver,
};
// It's important to wrap the executeFn within a promise
// so we can easily control the end of execution (with finally)
return Promise.resolve()
.then(() => executeFn(executionArgs))
.finally(destroy);
}

// It's important to wrap the executeFn within a promise
// so we can easily control the end of execution (with finally)
return Promise.resolve()
.then(() => executeFn(executionArgs))
.finally(destroy);
if (options?.controller) {
return perform(options.controller);
}

return contextBuilder(
isNotSchema<ExecutionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
).runWithContext(perform);
};
};

Expand Down
91 changes: 50 additions & 41 deletions packages/graphql-modules/src/application/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../shared/utils';
import { ExecutionContextBuilder } from './context';
import { Application } from './types';
import { InternalAppContext } from './application';

export function subscriptionCreator({
contextBuilder,
Expand All @@ -33,51 +34,59 @@ export function subscriptionCreator({
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>
) => {
// Create an subscription context
const { context, ɵdestroy: destroy } =
options?.controller ??
contextBuilder(
function perform({
context,
ɵdestroy: destroy,
}: {
context: InternalAppContext;
ɵdestroy: () => void;
}) {
const subscriptionArgs: SubscriptionArgs =
isNotSchema<SubscriptionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
);
? {
...argsOrSchema,
contextValue: context,
}
: {
schema: argsOrSchema,
document: document!,
rootValue,
contextValue: context,
variableValues,
operationName,
fieldResolver,
subscribeFieldResolver,
};

const subscriptionArgs: SubscriptionArgs = isNotSchema<SubscriptionArgs>(
argsOrSchema
)
? {
...argsOrSchema,
contextValue: context,
}
: {
schema: argsOrSchema,
document: document!,
rootValue,
contextValue: context,
variableValues,
operationName,
fieldResolver,
subscribeFieldResolver,
};
let isIterable = false;

let isIterable = false;
// It's important to wrap the subscribeFn within a promise
// so we can easily control the end of subscription (with finally)
return Promise.resolve()
.then(() => subscribeFn(subscriptionArgs))
.then((sub) => {
if (isAsyncIterable(sub)) {
isIterable = true;
return tapAsyncIterator(sub, destroy);
}
return sub;
})
.finally(() => {
if (!isIterable) {
destroy();
}
});
}

// It's important to wrap the subscribeFn within a promise
// so we can easily control the end of subscription (with finally)
return Promise.resolve()
.then(() => subscribeFn(subscriptionArgs))
.then((sub) => {
if (isAsyncIterable(sub)) {
isIterable = true;
return tapAsyncIterator(sub, destroy);
}
return sub;
})
.finally(() => {
if (!isIterable) {
destroy();
}
});
if (options?.controller) {
return perform(options.controller);
}

return contextBuilder(
isNotSchema<SubscriptionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
).runWithContext(perform);
};
};

Expand Down
Loading
Loading