Skip to content

Commit

Permalink
Add a limit to the number of options for a selection (#2880)
Browse files Browse the repository at this point in the history
In some cases, the number of options generated for a selection can grow
to a huge number, which results in a lot of time spent pruning options
and creating plans. The new `pathsLimit` option will put a limit on that
number of options

---------

Co-authored-by: Sachin D. Shinde <[email protected]>
  • Loading branch information
Geal and sachindshinde authored Dec 11, 2023
1 parent 972097c commit ffe67df
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/fast-cherries-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/query-planner": patch
---

Add a limit to the number of options for a selection. In some cases, we will generate a lot of possible paths to access a field. There is a process to remove redundant paths, but when the list is too large, that process gets very expensive. To prevent that, we introduce an optional limit that will reject the query if too many paths are generated
8 changes: 7 additions & 1 deletion query-planner-js/src/buildPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ class QueryPlanningTraversal<RV extends Vertex> {

private stack: [Selection, SimultaneousPathsWithLazyIndirectPaths<RV>[]][];
private readonly closedBranches: ClosedBranch<RV>[] = [];
private readonly optionsLimit: number | null;

constructor(
readonly parameters: PlanningParameters<RV>,
Expand All @@ -391,6 +392,7 @@ class QueryPlanningTraversal<RV extends Vertex> {
) {
const { root, federatedQueryGraph } = parameters;
this.isTopLevel = isRootVertex(root);
this.optionsLimit = parameters.config.debug?.pathsLimit;
this.conditionResolver = cachingConditionResolver(
federatedQueryGraph,
(edge, context, excludedEdges, excludedConditions) => this.resolveConditionPlan(edge, context, excludedEdges, excludedConditions),
Expand Down Expand Up @@ -477,6 +479,10 @@ class QueryPlanningTraversal<RV extends Vertex> {
return;
}
newOptions = newOptions.concat(followupForOption);

if (this.optionsLimit && newOptions.length > this.optionsLimit) {
throw new Error(`Too many options generated for ${selection}, reached the limit of ${this.optionsLimit}`);
}
}

if (newOptions.length === 0) {
Expand Down Expand Up @@ -787,7 +793,7 @@ class QueryPlanningTraversal<RV extends Vertex> {
this.costFunction,
context,
excludedDestinations,
addConditionExclusion(excludedConditions, edge.conditions)
addConditionExclusion(excludedConditions, edge.conditions),
).findBestPlan();
// Note that we want to return 'null', not 'undefined', because it's the latter that means "I cannot resolve that
// condition" within `advanceSimultaneousPathsWithOperation`.
Expand Down
17 changes: 17 additions & 0 deletions query-planner-js/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ export type QueryPlannerConfig = {
* query plans).
*/
maxEvaluatedPlans?: number,

/**
* Before creating query plans, for each path of fields in the query we compute all the
* possible options to traverse that path via the subgraphs. Multiple options can arise because
* fields in the path can be provided by multiple subgraphs, and abstract types (i.e. unions
* and interfaces) returned by fields sometimes require the query planner to traverse through
* each constituent object type. The number of options generated in this computation can grow
* large if the schema or query are sufficiently complex, and that will increase the time spent
* planning.
*
* This config allows specifying a per-path limit to the number of options considered. If any
* path's options exceeds this limit, query planning will abort and the operation will fail.
*
* The default value is null, which specifies no limit.
*/
pathsLimit?: number | null
},
}

Expand All @@ -104,6 +120,7 @@ export function enforceQueryPlannerConfigDefaults(
// don't take more than a handful of seconds. It might be worth running a bit more experiments on more environment
// to see if it's such a good default.
maxEvaluatedPlans: 10000,
pathsLimit: null,
...config?.debug,
},
};
Expand Down

0 comments on commit ffe67df

Please sign in to comment.