Skip to content
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
11 changes: 11 additions & 0 deletions .changesets/feat_lrlna_router_2150_max_non_local_selections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
### Add environment variable for MAX_NON_LOCAL_SELECTIONS

Adds a environment variable for MAX_NON_LOCAL_SELECTIONS const used in query

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This release note exposes an internal constant and advertises an “undocumented” setting without explaining the user benefit. Replace the heading and first paragraph with “Allow support-guided adjustment of query-planning protection” and “Support can adjust a query-planning safety limit for legitimate operations that exceed the default.” If the histogram remains, replace the second paragraph with “Adds apollo.router.query_planning.plan.non_local_selections, a histogram of estimated selections requiring query-planning exploration for successful, newly generated plans.”

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I don't know about your wording 😅 . I feel that mine is clearer.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the mention of the env var and just kept the changelog around the metric.

planning traversal. This is an undocumented env var, as it's intended for
internal use.

This additional adds a metric for tracking current number of non-local
selections, which is a histogram available under
`apollo.router.query_planning.plan.non_local_selections`.

By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/10349
8 changes: 7 additions & 1 deletion apollo-federation/src/query_plan/query_planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ pub struct QueryPlanningStatistics {
/// `best_plan_cost` can be NaN, if the cost is not computed or irrelevant.
#[serde(deserialize_with = "deserialize_f64_nullable")]
pub best_plan_cost: f64,
/// `non_local_selections_count` can be `None`, if
/// `QueryPlanOptions::non_local_selections_limit_enabled` is `false`
pub non_local_selections_count: Option<u64>,
}

/// Deserialize helper for f64 that treats null as NaN.
Expand Down Expand Up @@ -563,6 +566,7 @@ impl QueryPlanner {
node: root_node,
statistics: QueryPlanningStatistics {
best_plan_cost: cost,
non_local_selections_count: non_local_selection_state.as_ref().map(|s| s.count),
..statistics
},
};
Expand Down Expand Up @@ -1458,13 +1462,15 @@ type User
evaluated_plan_count: Cell::new(10),
evaluated_plan_paths: Cell::new(20),
best_plan_cost: f64::NAN,
non_local_selections_count: Some(30),
};
let serialized = serde_json::to_string_pretty(&stats).expect("Serializing");
insta::assert_snapshot!(serialized, @r###"
{
"evaluated_plan_count": 10,
"evaluated_plan_paths": 20,
"best_plan_cost": null
"best_plan_cost": null,
"non_local_selections_count": 30
}
"###);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
return Err(SingleFederationError::QueryPlanComplexityExceeded {
message: format!(
"Number of non-local selections exceeds limit of {}",
Self::MAX_NON_LOCAL_SELECTIONS,
Self::max_non_local_selections(),
),
}
.into());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::OnceLock;

use apollo_compiler::Name;
use apollo_compiler::collections::IndexMap;
use apollo_compiler::collections::IndexSet;
Expand All @@ -22,7 +24,16 @@ use crate::schema::position::INTROSPECTION_TYPENAME_FIELD_NAME;
use crate::schema::position::ObjectTypeDefinitionPosition;

impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
pub(super) const MAX_NON_LOCAL_SELECTIONS: u64 = 100_000;
pub(super) fn max_non_local_selections() -> u64 {
static MAX_NON_LOCAL_SELECTIONS: OnceLock<u64> = OnceLock::new();
*MAX_NON_LOCAL_SELECTIONS.get_or_init(|| {
// This environment variable is intentionally undocumented.
std::env::var("APOLLO_ROUTER_MAX_NON_LOCAL_SELECTIONS")
Comment thread
lrlna marked this conversation as resolved.
Outdated
.ok()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(100_000)
Comment thread
lrlna marked this conversation as resolved.
Outdated
})
}

/// This calls `check_non_local_selections_limit_exceeded()` for each of the selections in the
/// open branches stack; see that function's doc comment for more information.
Expand Down Expand Up @@ -91,8 +102,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
/// set that wouldn't be avoided by such an optimization (i.e. the "non-local" selections), and
/// adds it to the given count in the state. Note that the count for a given selection set is
/// scaled by an approximate upper bound on the possible number of tail nodes for paths ending
/// at that selection set. If at any point, the count exceeds `Self::MAX_NON_LOCAL_SELECTIONS`,
/// then this function will return `true`.
/// at that selection set. If at any point, the count exceeds
/// `Self::max_non_local_selections()`, then this function will return `true`.
///
/// This function's code is closely related to `selection_set_is_fully_local_from_all_nodes()`
/// (which implements the aforementioned optimization). However, when it comes to traversing the
Expand Down Expand Up @@ -235,7 +246,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
}

/// Updates the non-local selection set count in the state, returning true if this causes the
/// count to exceed `Self::MAX_NON_LOCAL_SELECTIONS`.
/// count to exceed `Self::max_non_local_selections()`.
fn update_count(num_selections: usize, num_parent_nodes: usize, state: &mut State) -> bool {
let Ok(num_selections) = u64::try_from(num_selections) else {
return true;
Expand All @@ -249,7 +260,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
if let Some(new_count) = state
.count
.checked_add(additional_count)
.take_if(|v| *v <= Self::MAX_NON_LOCAL_SELECTIONS)
.take_if(|v| *v <= Self::max_non_local_selections())
{
state.count = new_count;
} else {
Expand Down
36 changes: 36 additions & 0 deletions apollo-router/src/query_planner/query_planner_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ impl QueryPlannerService {
query_plan_root_node: root_node.map(Arc::new),
evaluated_plan_count: plan.statistics.evaluated_plan_count.clone().into_inner() as u64,
evaluated_plan_paths: plan.statistics.evaluated_plan_paths.clone().into_inner() as u64,
non_local_selections_count: plan.statistics.non_local_selections_count,
})
}

Expand Down Expand Up @@ -331,6 +332,7 @@ impl QueryPlannerService {
formatted_query_plan,
evaluated_plan_count,
evaluated_plan_paths,
non_local_selections_count,
} = plan_result;

// If the query is filtered, we want to generate the signature using the original query and generate the
Expand Down Expand Up @@ -366,6 +368,9 @@ impl QueryPlannerService {
"Number of paths (including intermediate ones) considered to plan a query before starting to generate a plan",
evaluated_plan_paths
);
if let Some(non_local_selections_count) = non_local_selections_count {
metric_query_planning_non_local_selections(non_local_selections_count);
}

Ok(QueryPlannerContent::Plan {
plan: Arc::new(super::QueryPlan {
Expand Down Expand Up @@ -614,6 +619,7 @@ pub(crate) struct QueryPlanResult {
pub(super) query_plan_root_node: Option<Arc<PlanNode>>,
pub(super) evaluated_plan_count: u64,
pub(super) evaluated_plan_paths: u64,
pub(super) non_local_selections_count: Option<u64>,
}

/// The outcome of a query-planning attempt. Shared across query-planning metrics (e.g.
Expand Down Expand Up @@ -673,6 +679,15 @@ pub(crate) fn metric_query_planning_plan_duration(
);
}

pub(crate) fn metric_query_planning_non_local_selections(count: u64) {
u64_histogram_with_unit!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This histogram inherits the default bucket boundaries, which stop at 10, so estimates near the 100,000 limit all fall into the +Inf bucket. Provide count-appropriate boundaries (including the region around the limit) and test bucket placement; otherwise drop it from this change.

"apollo.router.query_planning.plan.non_local_selections",
"Number of non-local selections estimated during query planning traversal, used for optimising plan option exploration",
Comment thread
lrlna marked this conversation as resolved.
Outdated
"{selection}",
count
);
}

pub(crate) fn metric_rust_qp_init(init_error_kind: Option<&'static str>) {
if let Some(init_error_kind) = init_error_kind {
u64_counter!(
Expand Down Expand Up @@ -1340,6 +1355,27 @@ mod tests {
.await;
}

#[test(tokio::test)]
async fn test_non_local_selections_histogram() {
async {
let _ = plan(
EXAMPLE_SCHEMA,
include_str!("testdata/query.graphql"),
include_str!("testdata/query.graphql"),
None,
PlanOptions::default(),
)
.await
.unwrap();

assert_histogram_exists!("apollo.router.query_planning.plan.non_local_selections", u64);
assert_histogram_count!("apollo.router.query_planning.plan.non_local_selections", 1 as u64);
assert_histogram_sum!("apollo.router.query_planning.plan.non_local_selections", 7 as u64);
}
.with_metrics()
.await;
}

async fn plan_unauthorized_operation(compute_job_type: ComputeJobType) -> QueryPlannerContent {
let configuration: Arc<Configuration> = Arc::default();
let schema = Schema::parse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
- `apollo.router.query_planning.total.duration` - Histogram of plan durations including queue time.
- `apollo.router.query_planning.plan.evaluated_plans` - Histogram of the number of evaluated query plans.
- `apollo.router.query_planning.plan.evaluated_paths` - Histogram of the number of paths (including intermediate ones) the planner considers before generating a plan. High values often correlate with long planning times on complex schemas or queries. Tune the limits as described in [Tuning query planner limits](/graphos/routing/query-planning/query-planning-best-practices#tuning-query-planner-limits).
- `apollo.router.query_planning.plan.non_local_selections` - Histogram of the number non-local selections estimated during query planning traversal. This is used to optimise option exploration when planning an operation.

Check warning on line 198 in docs/source/routing/observability/router-telemetry-otel/enabling-telemetry/standard-instruments.mdx

View check run for this annotation

Apollo Librarian / AI Style Review

docs/source/routing/observability/router-telemetry-otel/enabling-telemetry/standard-instruments.mdx#L198

**Framing Apollo Products**: Use simpler language like 'improve' instead of 'optimise'. **Framing**: Use the imperative 'Use' instead of the passive 'This is used to' for more direct instructions. **Language**: Use American English spelling ('optimize' instead of 'optimise'). **Products and Features**: The word 'optimise' uses British spelling; use the American spelling 'optimize' for consistency. **Structural Elements**: Omit ending punctuation for list items that are fragments. **Text Formatting**: Remove italics from 'optimise' as italics should not be used for general emphasis. **Verb Tense and Voice**: Use present tense instead of 'is used to'. **Word and Symbol Usage**: Use 'optimize' instead of 'optimise' for standard spelling. ```suggestion - `apollo.router.query_planning.plan.non_local_selections` - Histogram of the number non-local selections estimated during query planning traversal. This is used to improve option exploration when planning an operation. ```
Comment thread
lrlna marked this conversation as resolved.
Outdated
- `apollo.router.query_planner.memory` - Histogram of memory allocated during query planning, in bytes. Tracks memory allocation patterns specifically for query planning operations executed in the compute job thread pool. Attributes:
- `allocation.type`: The type of memory operation (`allocated`, `deallocated`, `zeroed`, `reallocated`)
- `context`: The context name where the allocation occurred (e.g., `query_planning`)
Expand Down
Loading