Skip to content
Open
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
75 changes: 51 additions & 24 deletions bin/router/src/pipeline/query_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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<RouterSharedState>,
filtered_operation_for_plan: &hive_router_query_planner::ast::operation::OperationDefinition,
normalized_operation: &Arc<GraphQLNormalizationPayload>,
request_override_context: &RequestOverrideContext,
cancellation_token: &CancellationToken,
) -> Result<QueryPlan, PlannerError> {
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,
)
}
20 changes: 17 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
|----|----|-----------|--------|
|[**http**](#http)|`object`|Configuration for the HTTP server/listener.<br/>Default: `{"host":"0.0.0.0","port":4000}`<br/>||
|[**log**](#log)|`object`|The router logger configuration.<br/>Default: `{"filter":null,"format":"json","level":"info"}`<br/>||
|[**query\_planner**](#query_planner)|`object`|Query planning configuration.<br/>Default: `{"allow_expose":false,"timeout":"10s"}`<br/>||
|[**query\_planner**](#query_planner)|`object`|Query planning configuration.<br/>Default: `{"allow_expose":false,"cache":{"enabled":true},"timeout":"10s"}`<br/>|yes|
|[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`).<br/>Default: `{"path":"supergraph.graphql","source":"file"}`<br/>||
|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs.<br/>Default: `{"dedupe_enabled":true,"dedupe_fingerprint_headers":["authorization"],"max_connections_per_host":100,"pool_idle_timeout_seconds":50}`<br/>||

Expand All @@ -23,6 +23,8 @@ log:
level: info
query_planner:
allow_expose: false
cache:
enabled: true
timeout: 10s
supergraph:
path: supergraph.graphql
Expand Down Expand Up @@ -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.<br/>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`.<br/>Default: `false`<br/>||
|**timeout**|`string`|The maximum time for the query planner to create an execution plan.<br/>This acts as a safeguard against overly complex or malicious queries that could degrade server performance.<br/>When the timeout is reached, the planning process is cancelled.<br/><br/>Default: 10s.<br/>Default: `"10s"`<br/>||
|**allow\_expose**|`boolean`|A flag to allow exposing the query plan in the response.<br/>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`.<br/>Default: `false`<br/>|no|
|[**cache**](#query_plannercache)|`object`||yes|
|**timeout**|`string`|The maximum time for the query planner to create an execution plan.<br/>This acts as a safeguard against overly complex or malicious queries that could degrade server performance.<br/>When the timeout is reached, the planning process is cancelled.<br/><br/>Default: 10s.<br/>Default: `"10s"`<br/>|no|

**Example**

```yaml
allow_expose: false
cache:
enabled: true
timeout: 10s

```

<a name="query_plannercache"></a>
### query\_planner\.cache: object

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**enabled**|`boolean`||yes|

<a name="supergraph"></a>
## supergraph: object

Expand Down
13 changes: 13 additions & 0 deletions lib/router-config/src/query_planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ pub struct QueryPlannerConfig {
)]
#[schemars(with = "String")]
pub timeout: Duration,
pub cache: QueryPlannerCacheConfig,
}

impl Default for QueryPlannerConfig {
fn default() -> Self {
Self {
allow_expose: default_query_planning_allow_expose(),
timeout: default_query_planning_timeout(),
cache: QueryPlannerCacheConfig::default(),
}
}
}
Expand All @@ -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 }
}
}
Loading