Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/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