diff --git a/bin/router/src/pipeline/query_plan.rs b/bin/router/src/pipeline/query_plan.rs index 58b0475e..c131f9cb 100644 --- a/bin/router/src/pipeline/query_plan.rs +++ b/bin/router/src/pipeline/query_plan.rs @@ -6,8 +6,10 @@ use crate::pipeline::normalize::GraphQLNormalizationPayload; use crate::pipeline::progressive_override::{RequestOverrideContext, StableOverrideContext}; use crate::shared_state::RouterSharedState; use hive_router_query_planner::planner::plan_nodes::QueryPlan; +use hive_router_query_planner::planner::PlannerError; use hive_router_query_planner::utils::cancellation::CancellationToken; use ntex::web::HttpRequest; +use tracing::debug; use xxhash_rust::xxh3::Xxh3; #[inline] @@ -22,36 +24,36 @@ pub async fn plan_operation_with_cache( StableOverrideContext::new(&app_state.planner.supergraph, request_override_context); let filtered_operation_for_plan = &normalized_operation.operation_for_plan; - let plan_cache_key = - calculate_cache_key(filtered_operation_for_plan.hash(), &stable_override_context); - let is_pure_introspection = filtered_operation_for_plan.selection_set.is_empty() - && normalized_operation.operation_for_introspection.is_some(); - let plan_result = app_state - .plan_cache - .try_get_with(plan_cache_key, async move { - if is_pure_introspection { - return Ok(Arc::new(QueryPlan { - kind: "QueryPlan".to_string(), - node: None, - })); - } - - app_state - .planner - .plan_from_normalized_operation( + if !app_state.router_config.query_planner.cache.enabled { + get_plan( + app_state, + filtered_operation_for_plan, + normalized_operation, + request_override_context, + cancellation_token, + ) + .map(Arc::new) + .map_err(Arc::new) + } else { + let plan_cache_key = + calculate_cache_key(filtered_operation_for_plan.hash(), &stable_override_context); + + app_state + .plan_cache + .try_get_with(plan_cache_key, async move { + get_plan( + app_state, filtered_operation_for_plan, - (&request_override_context.clone()).into(), + normalized_operation, + request_override_context, cancellation_token, ) .map(Arc::new) - }) - .await; - - match plan_result { - Ok(plan) => Ok(plan), - Err(e) => Err(req.new_pipeline_error(PipelineErrorVariant::PlannerError(e.clone()))), + }) + .await } + .map_err(|err| req.new_pipeline_error(PipelineErrorVariant::PlannerError(err))) } #[inline] @@ -61,3 +63,28 @@ fn calculate_cache_key(operation_hash: u64, context: &StableOverrideContext) -> context.hash(&mut hasher); hasher.finish() } + +#[inline] +fn get_plan( + app_state: &Arc, + filtered_operation_for_plan: &hive_router_query_planner::ast::operation::OperationDefinition, + normalized_operation: &Arc, + request_override_context: &RequestOverrideContext, + cancellation_token: &CancellationToken, +) -> Result { + let is_pure_introspection = filtered_operation_for_plan.selection_set.is_empty() + && normalized_operation.operation_for_introspection.is_some(); + if is_pure_introspection { + debug!("No need for a plan, as the incoming query only involves introspection fields"); + return Ok(QueryPlan { + kind: "QueryPlan".to_string(), + node: None, + }); + } + + app_state.planner.plan_from_normalized_operation( + filtered_operation_for_plan, + (&request_override_context.clone()).into(), + cancellation_token, + ) +} diff --git a/docs/README.md b/docs/README.md index c2f2f0c8..9e47d422 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ |----|----|-----------|--------| |[**http**](#http)|`object`|Configuration for the HTTP server/listener.
Default: `{"host":"0.0.0.0","port":4000}`
|| |[**log**](#log)|`object`|The router logger configuration.
Default: `{"filter":null,"format":"json","level":"info"}`
|| -|[**query\_planner**](#query_planner)|`object`|Query planning configuration.
Default: `{"allow_expose":false,"timeout":"10s"}`
|| +|[**query\_planner**](#query_planner)|`object`|Query planning configuration.
Default: `{"allow_expose":false,"cache":{"enabled":true},"timeout":"10s"}`
|yes| |[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`).
Default: `{"path":"supergraph.graphql","source":"file"}`
|| |[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs.
Default: `{"dedupe_enabled":true,"dedupe_fingerprint_headers":["authorization"],"max_connections_per_host":100,"pool_idle_timeout_seconds":50}`
|| @@ -23,6 +23,8 @@ log: level: info query_planner: allow_expose: false + cache: + enabled: true timeout: 10s supergraph: path: supergraph.graphql @@ -92,17 +94,29 @@ Query planning configuration. |Name|Type|Description|Required| |----|----|-----------|--------| -|**allow\_expose**|`boolean`|A flag to allow exposing the query plan in the response.
When set to `true` and an incoming request has a `hive-expose-query-plan: true` header, the query plan will be exposed in the response, as part of `extensions`.
Default: `false`
|| -|**timeout**|`string`|The maximum time for the query planner to create an execution plan.
This acts as a safeguard against overly complex or malicious queries that could degrade server performance.
When the timeout is reached, the planning process is cancelled.

Default: 10s.
Default: `"10s"`
|| +|**allow\_expose**|`boolean`|A flag to allow exposing the query plan in the response.
When set to `true` and an incoming request has a `hive-expose-query-plan: true` header, the query plan will be exposed in the response, as part of `extensions`.
Default: `false`
|no| +|[**cache**](#query_plannercache)|`object`||yes| +|**timeout**|`string`|The maximum time for the query planner to create an execution plan.
This acts as a safeguard against overly complex or malicious queries that could degrade server performance.
When the timeout is reached, the planning process is cancelled.

Default: 10s.
Default: `"10s"`
|no| **Example** ```yaml allow_expose: false +cache: + enabled: true timeout: 10s ``` + +### query\_planner\.cache: object + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**enabled**|`boolean`||yes| + ## supergraph: object diff --git a/lib/router-config/src/query_planner.rs b/lib/router-config/src/query_planner.rs index aa42c532..55eddb3f 100644 --- a/lib/router-config/src/query_planner.rs +++ b/lib/router-config/src/query_planner.rs @@ -21,6 +21,7 @@ pub struct QueryPlannerConfig { )] #[schemars(with = "String")] pub timeout: Duration, + pub cache: QueryPlannerCacheConfig, } impl Default for QueryPlannerConfig { @@ -28,6 +29,7 @@ impl Default for QueryPlannerConfig { Self { allow_expose: default_query_planning_allow_expose(), timeout: default_query_planning_timeout(), + cache: QueryPlannerCacheConfig::default(), } } } @@ -39,3 +41,14 @@ fn default_query_planning_allow_expose() -> bool { fn default_query_planning_timeout() -> Duration { Duration::from_secs(10) } + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct QueryPlannerCacheConfig { + pub enabled: bool, +} + +impl Default for QueryPlannerCacheConfig { + fn default() -> Self { + Self { enabled: true } + } +}