-
Notifications
You must be signed in to change notification settings - Fork 269
Add support to automatically cancel query plan request if planning takes longer than a pre-configured amount of time. #3222
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
178d77f
f207d43
566751e
d525d96
819cf1c
f6d45b5
3f15f2f
51c3c17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@apollo/query-planner": minor | ||
| "@apollo/gateway": minor | ||
| --- | ||
|
|
||
| Query planner now has support to automatically abort query plan requests that take longer than a configured amount of time. Default value is 2 minutes. Value is set by `maxQueryPlanningTime` value in `QueryPlannerConfig` options. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,7 @@ | |
| import { enforceQueryPlannerConfigDefaults, QueryPlannerConfig, validateQueryPlannerConfig } from "./config"; | ||
| import { generateAllPlansAndFindBest } from "./generateAllPlans"; | ||
| import { QueryPlan, ResponsePath, SequenceNode, PlanNode, ParallelNode, FetchNode, SubscriptionNode, trimSelectionNodes } from "./QueryPlan"; | ||
| import { performance } from 'perf_hooks'; | ||
|
|
||
|
|
||
| const debug = newDebugLogger('plan'); | ||
|
|
@@ -394,6 +395,9 @@ | |
| readonly costFunction: CostFunction, | ||
| initialContext: PathContext, | ||
| typeConditionedFetching: boolean, | ||
|
|
||
| // time (calculated relative from performance.now) after which query plan calculation may be aborted | ||
| readonly deadline: number, | ||
| excludedDestinations: ExcludedDestinations = [], | ||
| excludedConditions: ExcludedConditions = [], | ||
| ) { | ||
|
|
@@ -445,6 +449,9 @@ | |
| } | ||
|
|
||
| private handleOpenBranch(selection: Selection, options: SimultaneousPathsWithLazyIndirectPaths<RV>[]) { | ||
| if (performance.now() > this.deadline) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We ended up adding deadline checks to some other parts in Rust QP (see this commit), can you also add those deadline checks to the corresponding parts in gateway's QP? |
||
| throw new Error('Query plan took too long to calculate'); | ||
| } | ||
| const operation = selection.element; | ||
| debug.group(() => `Handling open branch: ${operation}`); | ||
| let newOptions: SimultaneousPathsWithLazyIndirectPaths<RV>[] = []; | ||
|
|
@@ -771,6 +778,7 @@ | |
| this.costFunction, | ||
| context, | ||
| this.typeConditionedFetching, | ||
| this.deadline, | ||
| excludedDestinations, | ||
| addConditionExclusion(excludedConditions, edge.conditions), | ||
| ).findBestPlan(); | ||
|
|
@@ -3531,10 +3539,10 @@ | |
| let main: PlanNode | undefined = undefined; | ||
| let primarySelection: MutableSelectionSet | undefined = undefined; | ||
| let deferred: DeferredNode[] = []; | ||
|
|
||
| const { operation, processor } = parameters; | ||
| const { operation, processor, config } = parameters; | ||
| const deadline = performance.now() + config.maxQueryPlanningTime; | ||
clenfest marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (operation.rootKind === 'mutation') { | ||
| const dependencyGraphs = computeRootSerialDependencyGraph(parameters, hasDefers); | ||
| const dependencyGraphs = computeRootSerialDependencyGraph(parameters, hasDefers, deadline); | ||
| for (const dependencyGraph of dependencyGraphs) { | ||
| const { main: localMain, deferred: localDeferred } = dependencyGraph.process(processor, operation.rootKind); | ||
| // Note that `reduceSequence` "flatten" sequence if needs be. | ||
|
|
@@ -3554,6 +3562,7 @@ | |
| parameters, | ||
| 0, | ||
| hasDefers, | ||
| deadline, | ||
| ); | ||
| ({ main, deferred } = dependencyGraph.process(processor, operation.rootKind)); | ||
| primarySelection = dependencyGraph.deferTracking.primarySelection; | ||
|
|
@@ -3675,12 +3684,14 @@ | |
| parameters: PlanningParameters<RootVertex>, | ||
| startFetchIdGen: number, | ||
| hasDefer: boolean, | ||
| deadline: number, | ||
| ): FetchDependencyGraph { | ||
| return computeRootParallelBestPlan( | ||
| parameters, | ||
| parameters.operation.selectionSet, | ||
| startFetchIdGen, | ||
| hasDefer, | ||
| deadline, | ||
| )[0]; | ||
| } | ||
|
|
||
|
|
@@ -3689,6 +3700,7 @@ | |
| selection: SelectionSet, | ||
| startFetchIdGen: number, | ||
| hasDefers: boolean, | ||
| deadline: number, | ||
| ): [FetchDependencyGraph, OpPathTree<RootVertex>, number] { | ||
| const planningTraversal = new QueryPlanningTraversal( | ||
| parameters, | ||
|
|
@@ -3699,6 +3711,7 @@ | |
| defaultCostFunction, | ||
| emptyContext, | ||
| parameters.config.typeConditionedFetching, | ||
| deadline, | ||
| ); | ||
| const plan = planningTraversal.findBestPlan(); | ||
| // Getting no plan means the query is essentially unsatisfiable (it's a valid query, but we can prove it will never return a result), | ||
|
|
@@ -3726,17 +3739,18 @@ | |
| function computeRootSerialDependencyGraph( | ||
| parameters: PlanningParameters<RootVertex>, | ||
| hasDefers: boolean, | ||
| deadline: number, | ||
| ): FetchDependencyGraph[] { | ||
| const { supergraphSchema, federatedQueryGraph, operation, root } = parameters; | ||
| const rootType = hasDefers ? supergraphSchema.schemaDefinition.rootType(root.rootKind) : undefined; | ||
| // We have to serially compute a plan for each top-level selection. | ||
| const splittedRoots = splitTopLevelFields(operation.selectionSet); | ||
| const graphs: FetchDependencyGraph[] = []; | ||
| let startingFetchId = 0; | ||
| let [prevDepGraph, prevPaths] = computeRootParallelBestPlan(parameters, splittedRoots[0], startingFetchId, hasDefers); | ||
| let [prevDepGraph, prevPaths] = computeRootParallelBestPlan(parameters, splittedRoots[0], startingFetchId, hasDefers, deadline); | ||
| let prevSubgraph = onlyRootSubgraph(prevDepGraph); | ||
| for (let i = 1; i < splittedRoots.length; i++) { | ||
| const [newDepGraph, newPaths] = computeRootParallelBestPlan(parameters, splittedRoots[i], prevDepGraph.nextFetchId(), hasDefers); | ||
| const [newDepGraph, newPaths] = computeRootParallelBestPlan(parameters, splittedRoots[i], prevDepGraph.nextFetchId(), hasDefers, deadline); | ||
|
Check warning on line 3753 in query-planner-js/src/buildPlan.ts
|
||
| const newSubgraph = onlyRootSubgraph(newDepGraph); | ||
| if (prevSubgraph === newSubgraph) { | ||
| // The new operation (think 'mutation' operation) is on the same subgraph than the previous one, so we can concat them in a single fetch | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.