Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use AsyncLocalStorage for execution context if available (#2395)
Co-authored-by: Tom Klaver <tomklav@gmail.com> Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
- Loading branch information
1 parent
b6bcac0
commit d9e91d0
Showing
6 changed files
with
133 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"graphql-modules": minor | ||
--- | ||
|
||
Use AsyncLocalStorage for execution context if available |
29 changes: 29 additions & 0 deletions
29
packages/graphql-modules/src/application/execution-context-async-local-storage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { AsyncLocalStorage } from 'async_hooks'; | ||
import { type ExecutionContextPicker } from './execution-context.interface'; | ||
|
||
const executionContextStore = AsyncLocalStorage | ||
? new AsyncLocalStorage<ExecutionContextPicker>() | ||
: undefined; | ||
|
||
export const executionContext: { | ||
create(picker: ExecutionContextPicker): () => void; | ||
getModuleContext: ExecutionContextPicker['getModuleContext']; | ||
getApplicationContext: ExecutionContextPicker['getApplicationContext']; | ||
} = { | ||
create(picker) { | ||
executionContextStore!.enterWith(picker); | ||
return function destroyContext() {}; | ||
}, | ||
getModuleContext(moduleId) { | ||
return executionContextStore!.getStore()!.getModuleContext(moduleId); | ||
}, | ||
getApplicationContext() { | ||
return executionContextStore!.getStore()!.getApplicationContext(); | ||
}, | ||
}; | ||
|
||
export function enableExecutionContext() {} | ||
|
||
export function getExecutionContextStore() { | ||
return executionContextStore; | ||
} |
79 changes: 79 additions & 0 deletions
79
packages/graphql-modules/src/application/execution-context-hooks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { createHook, executionAsyncId } from 'async_hooks'; | ||
import { type ExecutionContextPicker } from './execution-context.interface'; | ||
|
||
const executionContextStore = new Map<number, ExecutionContextPicker>(); | ||
const executionContextDependencyStore = new Map<number, Set<number>>(); | ||
|
||
const executionContextHook = createHook({ | ||
init(asyncId, _, triggerAsyncId) { | ||
// Store same context data for child async resources | ||
const ctx = executionContextStore.get(triggerAsyncId); | ||
if (ctx) { | ||
const dependencies = | ||
executionContextDependencyStore.get(triggerAsyncId) ?? | ||
executionContextDependencyStore | ||
.set(triggerAsyncId, new Set()) | ||
.get(triggerAsyncId)!; | ||
dependencies.add(asyncId); | ||
executionContextStore.set(asyncId, ctx); | ||
} | ||
}, | ||
destroy(asyncId) { | ||
if (executionContextStore.has(asyncId)) { | ||
executionContextStore.delete(asyncId); | ||
} | ||
}, | ||
}); | ||
|
||
function destroyContextAndItsChildren(id: number) { | ||
if (executionContextStore.has(id)) { | ||
executionContextStore.delete(id); | ||
} | ||
|
||
const deps = executionContextDependencyStore.get(id); | ||
|
||
if (deps) { | ||
for (const dep of deps) { | ||
destroyContextAndItsChildren(dep); | ||
} | ||
executionContextDependencyStore.delete(id); | ||
} | ||
} | ||
|
||
export const executionContext: { | ||
create(picker: ExecutionContextPicker): () => void; | ||
getModuleContext: ExecutionContextPicker['getModuleContext']; | ||
getApplicationContext: ExecutionContextPicker['getApplicationContext']; | ||
} = { | ||
create(picker) { | ||
const id = executionAsyncId(); | ||
executionContextStore.set(id, picker); | ||
return function destroyContext() { | ||
destroyContextAndItsChildren(id); | ||
}; | ||
}, | ||
getModuleContext(moduleId) { | ||
const picker = executionContextStore.get(executionAsyncId())!; | ||
return picker.getModuleContext(moduleId); | ||
}, | ||
getApplicationContext() { | ||
const picker = executionContextStore.get(executionAsyncId())!; | ||
return picker.getApplicationContext(); | ||
}, | ||
}; | ||
|
||
let executionContextEnabled = false; | ||
|
||
export function enableExecutionContext() { | ||
if (!executionContextEnabled) { | ||
executionContextHook.enable(); | ||
} | ||
} | ||
|
||
export function getExecutionContextStore() { | ||
return executionContextStore; | ||
} | ||
|
||
export function getExecutionContextDependencyStore() { | ||
return executionContextDependencyStore; | ||
} |
4 changes: 4 additions & 0 deletions
4
packages/graphql-modules/src/application/execution-context.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface ExecutionContextPicker { | ||
getModuleContext(moduleId: string): GraphQLModules.ModuleContext; | ||
getApplicationContext(): GraphQLModules.AppContext; | ||
} |
92 changes: 14 additions & 78 deletions
92
packages/graphql-modules/src/application/execution-context.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,19 @@ | ||
import { createHook, executionAsyncId } from 'async_hooks'; | ||
import { AsyncLocalStorage } from 'async_hooks'; | ||
|
||
export interface ExecutionContextPicker { | ||
getModuleContext(moduleId: string): GraphQLModules.ModuleContext; | ||
getApplicationContext(): GraphQLModules.AppContext; | ||
} | ||
/* | ||
Use AsyncLocalStorage if available (available sync Node 14). | ||
Otherwise, fall back to using async_hooks.createHook | ||
*/ | ||
|
||
const executionContextStore = new Map<number, ExecutionContextPicker>(); | ||
const executionContextDependencyStore = new Map<number, Set<number>>(); | ||
import * as Hooks from './execution-context-hooks'; | ||
import * as Async from './execution-context-async-local-storage'; | ||
|
||
const executionContextHook = createHook({ | ||
init(asyncId, _, triggerAsyncId) { | ||
// Store same context data for child async resources | ||
const ctx = executionContextStore.get(triggerAsyncId); | ||
if (ctx) { | ||
const dependencies = | ||
executionContextDependencyStore.get(triggerAsyncId) ?? | ||
executionContextDependencyStore | ||
.set(triggerAsyncId, new Set()) | ||
.get(triggerAsyncId)!; | ||
dependencies.add(asyncId); | ||
executionContextStore.set(asyncId, ctx); | ||
} | ||
}, | ||
destroy(asyncId) { | ||
if (executionContextStore.has(asyncId)) { | ||
executionContextStore.delete(asyncId); | ||
} | ||
}, | ||
}); | ||
export type { ExecutionContextPicker } from './execution-context.interface'; | ||
|
||
function destroyContextAndItsChildren(id: number) { | ||
if (executionContextStore.has(id)) { | ||
executionContextStore.delete(id); | ||
} | ||
export const executionContext = AsyncLocalStorage | ||
? Async.executionContext | ||
: Hooks.executionContext; | ||
|
||
const deps = executionContextDependencyStore.get(id); | ||
|
||
if (deps) { | ||
for (const dep of deps) { | ||
destroyContextAndItsChildren(dep); | ||
} | ||
executionContextDependencyStore.delete(id); | ||
} | ||
} | ||
|
||
export const executionContext: { | ||
create(picker: ExecutionContextPicker): () => void; | ||
getModuleContext: ExecutionContextPicker['getModuleContext']; | ||
getApplicationContext: ExecutionContextPicker['getApplicationContext']; | ||
} = { | ||
create(picker) { | ||
const id = executionAsyncId(); | ||
executionContextStore.set(id, picker); | ||
return function destroyContext() { | ||
destroyContextAndItsChildren(id); | ||
}; | ||
}, | ||
getModuleContext(moduleId) { | ||
const picker = executionContextStore.get(executionAsyncId())!; | ||
return picker.getModuleContext(moduleId); | ||
}, | ||
getApplicationContext() { | ||
const picker = executionContextStore.get(executionAsyncId())!; | ||
return picker.getApplicationContext(); | ||
}, | ||
}; | ||
|
||
let executionContextEnabled = false; | ||
|
||
export function enableExecutionContext() { | ||
if (!executionContextEnabled) { | ||
executionContextHook.enable(); | ||
} | ||
} | ||
|
||
export function getExecutionContextStore() { | ||
return executionContextStore; | ||
} | ||
|
||
export function getExecutionContextDependencyStore() { | ||
return executionContextDependencyStore; | ||
} | ||
export const enableExecutionContext = AsyncLocalStorage | ||
? () => undefined | ||
: Hooks.enableExecutionContext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters