diff --git a/.changesets/feat_caroline_demand_control_by_subgraph.md b/.changesets/feat_caroline_demand_control_by_subgraph.md new file mode 100644 index 0000000000..c9e4ecff52 --- /dev/null +++ b/.changesets/feat_caroline_demand_control_by_subgraph.md @@ -0,0 +1,61 @@ +### Support subgraph-level demand control ([PR #8829](https://github.com/apollographql/router/pull/8829)) + +Subgraph-level demand control lets you enforce per-subgraph query cost limits in Apollo Router, in addition to the +existing global cost limit for the whole supergraph. This helps you protect specific backend services that have +different +capacity or cost profiles from being overwhelmed by expensive operations. + +When a subgraph‑specific cost limit is exceeded, the router: + +* Still runs the rest of the operation, including other subgraphs whose cost is within limits. +* Skips calls to only the over‑budget subgraph, and composes the response as if that subgraph had returned null, instead + of rejecting the entire query. + +Per‑subgraph limits apply to the total work for that subgraph in a single operation. For each request, the router tracks +the aggregate estimated cost per subgraph across the entire query plan. If the same subgraph is fetched multiple times +(for example, through entity lookups, nested fetches, or conditional branches), those costs are summed together and the +subgraph’s limit is enforced against that total. + +#### Configuration + +```yaml +demand_control: + enabled: true + mode: enforce + strategy: + static_estimated: + max: 10 + list_size: 10 + actual_cost_mode: by_subgraph + subgraphs: # <---- everything from here down is new (all fields optional) + all: + max: 8 + list_size: 10 + subgraphs: + products: + max: 6 + # list_size omitted, 10 implied because of all.list_size + reviews: + list_size: 50 + # max omitted, 8 implied because of all.max +``` + +#### Example + +Consider a topProducts query, which fetches a list of products from a products subgraph and then performs an entity +lookup for each product in a reviews subgraph. Assume that the products cost is 10 and the reviews cost is 5, leading to +a total estimated cost of 15 (10 + 5). + +Previously, you would only be able to restrict that query via `demand_control.static_estimated.max`: + +* If you set it <= 15, the query would execute +* If you set it >15, the query would be rejected + +This feature allows much more granular control. In addition to `demand_control.static_estimated.max`, which operates as +before, there are also per subgraph maxes. + +For example, if you set `max = 20` and `reviews.max = 2`, the query will 'pass' the aggregate check (15 < 20) and will +execute on the products subgraph (no limit specified), but will not execute against the reviews subgraph (5 > 2). The +result will be composed as if the reviews subgraph had returned null. + +By [@carodewig](https://github.com/carodewig) in https://github.com/apollographql/router/pull/8829 diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 7798c1acce..c53e67fd17 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -9170,6 +9170,21 @@ expression: "&schema" "description": "The maximum cost of a query", "format": "double", "type": "number" + }, + "subgraph": { + "allOf": [ + { + "$ref": "#/definitions/SubgraphSubgraphStrategyConfigConfiguration" + } + ], + "default": { + "all": { + "list_size": null, + "max": null + }, + "subgraphs": {} + }, + "description": "Cost control by subgraph" } }, "required": [ @@ -10441,6 +10456,28 @@ expression: "&schema" }, "type": "object" }, + "SubgraphStrategyConfig": { + "properties": { + "list_size": { + "description": "The assumed length of lists returned by the operation for this subgraph.", + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "max": { + "description": "The maximum query cost routed to this subgraph.", + "format": "double", + "type": [ + "number", + "null" + ] + } + }, + "type": "object" + }, "SubgraphSubgraphApqConfiguration": { "description": "Configuration options pertaining to the subgraph server component.", "properties": { @@ -10524,6 +10561,32 @@ expression: "&schema" }, "type": "object" }, + "SubgraphSubgraphStrategyConfigConfiguration": { + "description": "Configuration options pertaining to the subgraph server component.", + "properties": { + "all": { + "allOf": [ + { + "$ref": "#/definitions/SubgraphStrategyConfig" + } + ], + "default": { + "list_size": null, + "max": null + }, + "description": "options applying to all subgraphs" + }, + "subgraphs": { + "additionalProperties": { + "$ref": "#/definitions/SubgraphStrategyConfig" + }, + "default": {}, + "description": "per subgraph options", + "type": "object" + } + }, + "type": "object" + }, "SubgraphTlsClientConfiguration": { "description": "Configuration options pertaining to the subgraph server component.", "properties": { diff --git a/apollo-router/src/configuration/subgraph.rs b/apollo-router/src/configuration/subgraph.rs index a371f4ebe2..0761f8c256 100644 --- a/apollo-router/src/configuration/subgraph.rs +++ b/apollo-router/src/configuration/subgraph.rs @@ -96,6 +96,21 @@ where pub(crate) fn get(&self, subgraph_name: &str) -> &T { self.subgraphs.get(subgraph_name).unwrap_or(&self.all) } + + // Create a new `SubgraphConfiguration` by extracting a value `V` from `&T` + pub(crate) fn extract( + &self, + extract_fn: fn(&T) -> V, + ) -> SubgraphConfiguration { + SubgraphConfiguration { + all: extract_fn(&self.all), + subgraphs: self + .subgraphs + .iter() + .map(|(k, v)| (k.clone(), extract_fn(v))) + .collect(), + } + } } impl Debug for SubgraphConfiguration diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs b/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs index 1573157bc2..99c55c3f8c 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs @@ -3,12 +3,17 @@ pub(in crate::plugins::demand_control) mod schema; pub(crate) mod static_cost; use std::collections::HashMap; +use std::ops::AddAssign; use crate::plugins::demand_control::DemandControlError; #[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)] pub(crate) struct CostBySubgraph(HashMap); impl CostBySubgraph { + pub(crate) fn new(subgraph: &str, value: f64) -> Self { + Self(HashMap::from([(subgraph.to_string(), value)])) + } + pub(crate) fn add_or_insert(&mut self, subgraph: &str, value: f64) { if let Some(subgraph_cost) = self.0.get_mut(subgraph) { *subgraph_cost += value; @@ -17,7 +22,58 @@ impl CostBySubgraph { } } + pub(crate) fn get(&self, subgraph: &str) -> Option { + self.0.get(subgraph).copied() + } + pub(crate) fn total(&self) -> f64 { self.0.values().sum() } + + /// Creates a new `CostBySubgraph` where each value in the map is the maximum of its value + /// in the two input `CostBySubgraph`s. + /// + /// ```rust + /// let cost1 = CostBySubgraph::new("hello", 1.0); + /// let mut cost2 = CostBySubgraph::new("hello", 2.0); + /// cost2.add_or_insert("world", 1.0); + /// + /// let max = CostBySubgraph::maximum(cost1, cost2); + /// assert_eq!(max.0.get("hello"), Some(2.0)); + /// assert_eq!(max.0.get("world"), Some(1.0)); + /// ``` + pub(crate) fn maximum(mut cost1: Self, cost2: Self) -> Self { + for (subgraph, value) in cost2.0.into_iter() { + if let Some(subgraph_cost) = cost1.0.get_mut(&subgraph) { + *subgraph_cost = subgraph_cost.max(value); + } else { + cost1.0.insert(subgraph, value); + } + } + + cost1 + } +} + +impl AddAssign for CostBySubgraph { + fn add_assign(&mut self, rhs: Self) { + for (subgraph, value) in rhs.0.into_iter() { + if let Some(subgraph_cost) = self.0.get_mut(&subgraph) { + *subgraph_cost += value; + } else { + self.0.insert(subgraph, value); + } + } + } +} + +#[cfg(test)] +impl From<&[(&str, f64)]> for CostBySubgraph { + fn from(values: &[(&str, f64)]) -> Self { + let mut cost = Self(HashMap::default()); + for (subgraph, value) in values { + cost.add_or_insert(subgraph, *value); + } + cost + } } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 41d01f27e8..5589e03b71 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -14,11 +14,13 @@ use apollo_compiler::schema::ExtendedType; use apollo_federation::query_plan::serializable_document::SerializableDocument; use serde_json_bytes::Value; +use super::CostBySubgraph; use super::DemandControlError; use super::directives::IncludeDirective; use super::directives::SkipDirective; use super::schema::DemandControlledSchema; use super::schema::InputDefinition; +use crate::configuration::subgraph::SubgraphConfiguration; use crate::graphql::Response; use crate::graphql::ResponseVisitor; use crate::json_ext::Object; @@ -31,6 +33,7 @@ use crate::spec::TYPENAME; pub(crate) struct StaticCostCalculator { list_size: u32, + subgraph_list_sizes: Arc>>, supergraph_schema: Arc, subgraph_schemas: Arc>, } @@ -148,15 +151,21 @@ impl StaticCostCalculator { pub(crate) fn new( supergraph_schema: Arc, subgraph_schemas: Arc>, + subgraph_list_sizes: Arc>>, list_size: u32, ) -> Self { Self { list_size, + subgraph_list_sizes, supergraph_schema, subgraph_schemas, } } + fn subgraph_list_size(&self, subgraph_name: &str) -> Option { + *self.subgraph_list_sizes.get(subgraph_name) + } + /// Scores a field within a GraphQL operation, handling some expected cases where /// directives change how the query is fetched. In the case of the federation /// directive `@requires`, the cost of the required selection is added to the @@ -181,6 +190,7 @@ impl StaticCostCalculator { field: &Field, parent_type: &NamedType, list_size_from_upstream: Option, + subgraph: &str, ) -> Result { // When we pre-process the schema, __typename isn't included. So, we short-circuit here to avoid failed lookups. if field.name == TYPENAME { @@ -213,6 +223,8 @@ impl StaticCostCalculator { .and_then(|dir| dir.expected_size) { expected_size + } else if let Some(subgraph_list_size) = self.subgraph_list_size(subgraph) { + subgraph_list_size as i32 } else { self.list_size as i32 }; @@ -234,6 +246,7 @@ impl StaticCostCalculator { &field.selection_set, field.ty().inner_named_type(), list_size_directive.as_ref(), + subgraph, )?; let mut arguments_cost = 0.0; @@ -265,6 +278,7 @@ impl StaticCostCalculator { selection_set, parent_type, list_size_directive.as_ref(), + subgraph, )?; } } @@ -288,6 +302,7 @@ impl StaticCostCalculator { ctx: &ScoringContext, fragment_spread: &FragmentSpread, list_size_directive: Option<&ListSizeDirective>, + subgraph: &str, ) -> Result { let fragment = fragment_spread.fragment_def(ctx.query).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( @@ -300,6 +315,7 @@ impl StaticCostCalculator { &fragment.selection_set, fragment.type_condition(), list_size_directive, + subgraph, ) } @@ -309,6 +325,7 @@ impl StaticCostCalculator { inline_fragment: &InlineFragment, parent_type: &NamedType, list_size_directive: Option<&ListSizeDirective>, + subgraph: &str, ) -> Result { self.score_selection_set( ctx, @@ -318,6 +335,7 @@ impl StaticCostCalculator { .as_ref() .unwrap_or(parent_type), list_size_directive, + subgraph, ) } @@ -325,6 +343,7 @@ impl StaticCostCalculator { &self, operation: &Operation, ctx: &ScoringContext, + subgraph: &str, ) -> Result { let mut cost = if operation.is_mutation() { 10.0 } else { 0.0 }; @@ -335,7 +354,13 @@ impl StaticCostCalculator { ))); }; - cost += self.score_selection_set(ctx, &operation.selection_set, root_type_name, None)?; + cost += self.score_selection_set( + ctx, + &operation.selection_set, + root_type_name, + None, + subgraph, + )?; Ok(cost) } @@ -346,6 +371,7 @@ impl StaticCostCalculator { selection: &Selection, parent_type: &NamedType, list_size_directive: Option<&ListSizeDirective>, + subgraph: &str, ) -> Result { match selection { Selection::Field(f) => self.score_field( @@ -353,10 +379,13 @@ impl StaticCostCalculator { f, parent_type, list_size_directive.and_then(|dir| dir.size_of(f)), + subgraph, ), - Selection::FragmentSpread(s) => self.score_fragment_spread(ctx, s, list_size_directive), + Selection::FragmentSpread(s) => { + self.score_fragment_spread(ctx, s, list_size_directive, subgraph) + } Selection::InlineFragment(i) => { - self.score_inline_fragment(ctx, i, parent_type, list_size_directive) + self.score_inline_fragment(ctx, i, parent_type, list_size_directive, subgraph) } } } @@ -367,10 +396,17 @@ impl StaticCostCalculator { selection_set: &SelectionSet, parent_type_name: &NamedType, list_size_directive: Option<&ListSizeDirective>, + subgraph: &str, ) -> Result { let mut cost = 0.0; for selection in selection_set.selections.iter() { - cost += self.score_selection(ctx, selection, parent_type_name, list_size_directive)?; + cost += self.score_selection( + ctx, + selection, + parent_type_name, + list_size_directive, + subgraph, + )?; } Ok(cost) } @@ -393,7 +429,7 @@ impl StaticCostCalculator { &self, plan_node: &PlanNode, variables: &Object, - ) -> Result { + ) -> Result { match plan_node { PlanNode::Sequence { nodes } => self.summed_score_of_nodes(nodes, variables), PlanNode::Parallel { nodes } => self.summed_score_of_nodes(nodes, variables), @@ -424,7 +460,7 @@ impl StaticCostCalculator { subgraph: &str, operation: &SerializableDocument, variables: &Object, - ) -> Result { + ) -> Result { tracing::debug!("On subgraph {}, scoring operation: {}", subgraph, operation); let schema = self.subgraph_schemas.get(subgraph).ok_or_else(|| { @@ -436,7 +472,8 @@ impl StaticCostCalculator { let operation = operation .as_parsed() .map_err(DemandControlError::SubgraphOperationNotInitialized)?; - self.estimated(operation, schema, variables, false) + let cost = self.estimated(operation, schema, variables, false, subgraph)?; + Ok(CostBySubgraph::new(subgraph, cost)) } fn max_score_of_nodes( @@ -444,15 +481,15 @@ impl StaticCostCalculator { left: &Option>, right: &Option>, variables: &Object, - ) -> Result { + ) -> Result { match (left, right) { - (None, None) => Ok(0.0), + (None, None) => Ok(CostBySubgraph::default()), (None, Some(right)) => self.score_plan_node(right, variables), (Some(left), None) => self.score_plan_node(left, variables), (Some(left), Some(right)) => { let left_score = self.score_plan_node(left, variables)?; let right_score = self.score_plan_node(right, variables)?; - Ok(left_score.max(right_score)) + Ok(CostBySubgraph::maximum(left_score, right_score)) } } } @@ -462,8 +499,8 @@ impl StaticCostCalculator { primary: &Primary, deferred: &Vec, variables: &Object, - ) -> Result { - let mut score = 0.0; + ) -> Result { + let mut score = CostBySubgraph::default(); if let Some(node) = &primary.node { score += self.score_plan_node(node, variables)?; } @@ -479,20 +516,22 @@ impl StaticCostCalculator { &self, nodes: &Vec, variables: &Object, - ) -> Result { - let mut sum = 0.0; + ) -> Result { + let mut sum = CostBySubgraph::default(); for node in nodes { sum += self.score_plan_node(node, variables)?; } Ok(sum) } + /// Determine cost for a single-subgraph operation. pub(crate) fn estimated( &self, query: &ExecutableDocument, schema: &DemandControlledSchema, variables: &Object, should_estimate_requires: bool, + subgraph: &str, ) -> Result { let mut cost = 0.0; let ctx = ScoringContext { @@ -502,19 +541,20 @@ impl StaticCostCalculator { should_estimate_requires, }; if let Some(op) = &query.operations.anonymous { - cost += self.score_operation(op, &ctx)?; + cost += self.score_operation(op, &ctx, subgraph)?; } for (_name, op) in query.operations.named.iter() { - cost += self.score_operation(op, &ctx)?; + cost += self.score_operation(op, &ctx, subgraph)?; } Ok(cost) } + /// Determine cost for an operation which may span multiple subgraphs. pub(crate) fn planned( &self, query_plan: &QueryPlan, variables: &Object, - ) -> Result { + ) -> Result { self.score_plan_node(&query_plan.root, variables) } @@ -664,7 +704,7 @@ mod tests { variables: &Object, ) -> Result { let js_planner_node: PlanNode = query_plan.node.as_ref().unwrap().into(); - self.score_plan_node(&js_planner_node, variables) + Ok(self.score_plan_node(&js_planner_node, variables)?.total()) } } @@ -679,6 +719,9 @@ mod tests { } /// Estimate cost of an operation executed on a supergraph. + /// + /// Does not consider cost-by-subgraph or make use of `StaticCostCalculator::subgraph_list_size`, + /// which are only available when estimating the cost against a query plan. fn estimated_cost(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); @@ -689,7 +732,12 @@ mod tests { .unwrap_or_default(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); - let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); + let calculator = StaticCostCalculator::new( + Arc::new(schema), + Default::default(), + Default::default(), + 100, + ); calculator .estimated( @@ -697,11 +745,15 @@ mod tests { &calculator.supergraph_schema, &variables, true, + "", ) .unwrap() } /// Estimate cost of an operation on a plain, non-federated schema. + /// + /// Does not consider cost-by-subgraph or make use of `StaticCostCalculator::subgraph_list_size`, + /// which are only available when estimating the cost against a query plan. fn basic_estimated_cost(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let schema = apollo_compiler::Schema::parse_and_validate(schema_str, "schema.graphqls").unwrap(); @@ -717,10 +769,15 @@ mod tests { .cloned() .unwrap_or_default(); let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); - let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); + let calculator = StaticCostCalculator::new( + Arc::new(schema), + Default::default(), + Default::default(), + 100, + ); calculator - .estimated(&query, &calculator.supergraph_schema, &variables, true) + .estimated(&query, &calculator.supergraph_schema, &variables, true, "") .unwrap() } @@ -771,10 +828,11 @@ mod tests { let calculator = StaticCostCalculator::new( Arc::new(schema), Arc::new(demand_controlled_subgraph_schemas), + Default::default(), 100, ); - calculator.planned(&query_plan, &variables).unwrap() + calculator.planned(&query_plan, &variables).unwrap().total() } fn planned_cost_rust(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { @@ -806,6 +864,7 @@ mod tests { let calculator = StaticCostCalculator::new( Arc::new(schema), Arc::new(demand_controlled_subgraph_schemas), + Default::default(), 100, ); @@ -828,9 +887,14 @@ mod tests { let response = Response::from_bytes(Bytes::from(response_bytes)).unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); - StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) - .actual(&query.executable, &response, &variables) - .unwrap() + StaticCostCalculator::new( + Arc::new(schema), + Default::default(), + Default::default(), + 100, + ) + .actual(&query.executable, &response, &variables) + .unwrap() } /// Actual cost of an operation on a plain, non-federated schema. @@ -856,9 +920,14 @@ mod tests { let response = Response::from_bytes(Bytes::from(response_bytes)).unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); - StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) - .actual(&query, &response, &variables) - .unwrap() + StaticCostCalculator::new( + Arc::new(schema), + Default::default(), + Default::default(), + 100, + ) + .actual(&query, &response, &variables) + .unwrap() } #[test] @@ -1048,6 +1117,8 @@ mod tests { #[test(tokio::test)] async fn federated_query_with_adjustable_list_cost() { + // NB: does not consider cost-by-subgraph or make use of `StaticCostCalculator::subgraph_list_size`, + // which are only available when estimating the cost against a query plan. let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_deferred_query.graphql"); let (schema, query) = parse_schema_and_operation(schema, query, &Default::default()); @@ -1055,23 +1126,27 @@ mod tests { DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(), ); - let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 100); + let calculator = + StaticCostCalculator::new(schema.clone(), Default::default(), Default::default(), 100); let conservative_estimate = calculator .estimated( &query.executable, &calculator.supergraph_schema, &Default::default(), true, + "", ) .unwrap(); - let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 5); + let calculator = + StaticCostCalculator::new(schema.clone(), Default::default(), Default::default(), 5); let narrow_estimate = calculator .estimated( &query.executable, &calculator.supergraph_schema, &Default::default(), true, + "", ) .unwrap(); diff --git a/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_inheritance.yaml b/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_inheritance.yaml new file mode 100644 index 0000000000..505195ba0e --- /dev/null +++ b/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_inheritance.yaml @@ -0,0 +1,16 @@ +demand_control: + enabled: true + mode: enforce + strategy: + static_estimated: + list_size: 1 + max: 10 + subgraph: + all: + list_size: 3 + max: 5 + subgraphs: + products: + list_size: 5 + reviews: + max: 2 diff --git a/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_invalid.yaml b/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_invalid.yaml new file mode 100644 index 0000000000..c068685180 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_invalid.yaml @@ -0,0 +1,13 @@ +demand_control: + enabled: true + mode: enforce + strategy: + static_estimated: + list_size: 1 + max: 10 + subgraph: + all: + list_size: 3 + subgraphs: + products: + max: -1 diff --git a/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_no_inheritance.yaml b/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_no_inheritance.yaml new file mode 100644 index 0000000000..66725ff701 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/fixtures/per_subgraph_no_inheritance.yaml @@ -0,0 +1,15 @@ +demand_control: + enabled: true + mode: enforce + strategy: + static_estimated: + list_size: 1 + max: 10 + subgraph: + all: + list_size: 3 + subgraphs: + products: + list_size: 5 + reviews: + max: 2 diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index 1fb5501ae2..04a13238a4 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -1,6 +1,8 @@ //! Demand control plugin. //! This plugin will use the cost calculation algorithm to determine if a query should be allowed to execute. //! On the request path it will use estimated + +use std::collections::HashSet; use std::future; use std::ops::ControlFlow; use std::sync::Arc; @@ -27,6 +29,7 @@ use tower::ServiceBuilder; use tower::ServiceExt; use crate::Context; +use crate::configuration::subgraph::SubgraphConfiguration; use crate::error::Error; use crate::graphql; use crate::graphql::IntoGraphQLErrors; @@ -54,6 +57,9 @@ pub(crate) const COST_STRATEGY_KEY: &str = "apollo::demand_control::strategy"; pub(crate) const COST_BY_SUBGRAPH_ACTUAL_KEY: &str = "apollo::demand_control::actual_cost_by_subgraph"; +pub(crate) const COST_BY_SUBGRAPH_ESTIMATED_KEY: &str = + "apollo::demand_control::estimated_cost_by_subgraph"; +pub(crate) const COST_BY_SUBGRAPH_RESULT_KEY: &str = "apollo::demand_control::result_by_subgraph"; /// Algorithm for calculating the cost of an incoming query. #[derive(Clone, Debug, Deserialize, JsonSchema)] @@ -87,6 +93,10 @@ pub(crate) enum StrategyConfig { /// make it to the composed response. #[serde(default)] actual_cost_mode: ActualCostMode, + + /// Cost control by subgraph + #[serde(default)] + subgraph: SubgraphConfiguration, }, #[cfg(test)] @@ -110,12 +120,23 @@ pub(crate) enum ActualCostMode { ByResponseShape, } +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +pub(crate) struct SubgraphStrategyConfig { + /// The assumed length of lists returned by the operation for this subgraph. + list_size: Option, + + /// The maximum query cost routed to this subgraph. + max: Option, +} + impl StrategyConfig { - fn validate(&self) -> Result<(), BoxError> { - let actual_cost_mode = match self { + fn validate(&self, subgraph_names: HashSet<&String>) -> Result<(), BoxError> { + let (actual_cost_mode, subgraphs) = match self { StrategyConfig::StaticEstimated { - actual_cost_mode, .. - } => actual_cost_mode, + actual_cost_mode, + subgraph, + .. + } => (actual_cost_mode, subgraph), #[cfg(test)] StrategyConfig::Test { .. } => return Ok(()), }; @@ -127,6 +148,26 @@ impl StrategyConfig { ); } + if subgraphs.all.max.is_some_and(|s| s < 0.0) { + return Err("Maximum per-subgraph query cost for `all` is negative".into()); + } + + for (subgraph_name, subgraph_config) in subgraphs.subgraphs.iter() { + if !subgraph_names.contains(subgraph_name) { + tracing::warn!( + "Subgraph `{subgraph_name}` missing from schema but was specified in per-subgraph demand cost; it will be ignored" + ); + continue; + } + + if subgraph_config.max.is_some_and(|s| s < 0.0) { + return Err(format!( + "Maximum per-subgraph query cost for `{subgraph_name}` is negative" + ) + .into()); + } + } + Ok(()) } } @@ -161,7 +202,16 @@ pub(crate) enum DemandControlError { /// The maximum cost of the query max_cost: f64, }, - /// auery actual cost {actual_cost} exceeded configured maximum {max_cost} + /// query estimated cost {estimated_cost} exceeded configured maximum {max_cost} for subgraph {subgraph} + EstimatedSubgraphCostTooExpensive { + /// The name of the subgraph + subgraph: String, + /// The estimated total cost of the subgraph queries + estimated_cost: f64, + /// The maximum total cost of the subgraph queries + max_cost: f64, + }, + /// Query actual cost {actual_cost} exceeded configured maximum {max_cost} #[allow(dead_code)] ActualCostTooExpensive { /// The actual cost of the query @@ -197,6 +247,23 @@ impl IntoGraphQLErrors for DemandControlError { .build(), ]) } + DemandControlError::EstimatedSubgraphCostTooExpensive { + ref subgraph, + estimated_cost, + max_cost, + } => { + let mut extensions = Object::new(); + extensions.insert("cost.subgraph", subgraph.as_str().into()); + extensions.insert("cost.subgraph.estimated", estimated_cost.into()); + extensions.insert("cost.subgraph.max", max_cost.into()); + Ok(vec![ + graphql::Error::builder() + .extension_code(self.code()) + .extensions(extensions) + .message(self.to_string()) + .build(), + ]) + } DemandControlError::ActualCostTooExpensive { actual_cost, max_cost, @@ -244,6 +311,9 @@ impl DemandControlError { fn code(&self) -> &'static str { match self { DemandControlError::EstimatedCostTooExpensive { .. } => "COST_ESTIMATED_TOO_EXPENSIVE", + DemandControlError::EstimatedSubgraphCostTooExpensive { .. } => { + "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + } DemandControlError::ActualCostTooExpensive { .. } => "COST_ACTUAL_TOO_EXPENSIVE", DemandControlError::QueryParseFailure(_) => "COST_QUERY_PARSE_FAILURE", DemandControlError::SubgraphOperationNotInitialized(_) => { @@ -317,6 +387,22 @@ impl Context { Ok(estimated.zip(actual).map(|(est, act)| est - act)) } + pub(crate) fn insert_estimated_cost_by_subgraph( + &self, + cost: CostBySubgraph, + ) -> Result<(), DemandControlError> { + self.insert(COST_BY_SUBGRAPH_ESTIMATED_KEY, cost) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_estimated_cost_by_subgraph( + &self, + ) -> Result, DemandControlError> { + self.get::<&str, CostBySubgraph>(COST_BY_SUBGRAPH_ESTIMATED_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + pub(crate) fn get_actual_cost_by_subgraph( &self, ) -> Result, DemandControlError> { @@ -341,6 +427,23 @@ impl Context { Ok(()) } + #[cfg(test)] + pub(crate) fn insert_actual_cost_by_subgraph( + &self, + cost: CostBySubgraph, + ) -> Result<(), DemandControlError> { + // combine this cost with the cost that already exists in the context + self.upsert( + COST_BY_SUBGRAPH_ACTUAL_KEY, + |mut existing_cost: CostBySubgraph| { + existing_cost += cost; + existing_cost + }, + ) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + pub(crate) fn insert_cost_result(&self, result: String) -> Result<(), DemandControlError> { self.insert(COST_RESULT_KEY, result) .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; @@ -352,6 +455,22 @@ impl Context { .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) } + pub(crate) fn insert_cost_by_subgraph_result( + &self, + subgraph: String, + result: String, + ) -> Result<(), DemandControlError> { + self.upsert::<_, HashMap>( + COST_BY_SUBGRAPH_RESULT_KEY, + |mut current_results| { + current_results.insert(subgraph, result); + current_results + }, + ) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + pub(crate) fn insert_cost_strategy(&self, strategy: String) -> Result<(), DemandControlError> { self.insert(COST_STRATEGY_KEY, strategy) .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; @@ -421,7 +540,8 @@ impl Plugin for DemandControl { .insert(subgraph_name.clone(), demand_controlled_subgraph_schema); } - init.config.strategy.validate()?; + let subgraph_names = init.subgraph_schemas.keys().collect(); + init.config.strategy.validate(subgraph_names)?; Ok(DemandControl { strategy_factory: StrategyFactory::new( @@ -610,6 +730,7 @@ mod test { use futures::StreamExt; use schemars::JsonSchema; use serde::Deserialize; + use tokio::task::JoinSet; use crate::Context; use crate::graphql; @@ -838,4 +959,95 @@ mod test { } } } + + // sanity check that our actuals calculation is always accumulative and based on safe data + // structures (like its current implementation, using DashMap, which functions similarly to a + // RwLock) -- this test looks at the same subgraph updates + #[tokio::test] + async fn test_concurrent_actual_cost_updates_to_same_subgraph() { + let ctx = Arc::new(Context::new()); + let num_tasks = 100; + let cost_per_update = 1.5; + + // multiple updates to the SAME subgraph + let mut join_set = JoinSet::new(); + for _ in 0..num_tasks { + let ctx = ctx.clone(); + join_set.spawn(async move { + ctx.update_actual_cost_by_subgraph("products", cost_per_update) + .expect("update should succeed"); + }); + } + + while let Some(result) = join_set.join_next().await { + result.expect("painicked waiting to join tasks"); + } + + let cost_by_subgraph = ctx + .get_actual_cost_by_subgraph() + .expect("should deserialize") + .expect("should have value"); + + let products_cost = cost_by_subgraph + .get("products") + .expect("should have products"); + let expected_cost = num_tasks as f64 * cost_per_update; + + assert_eq!( + products_cost, expected_cost, + "Expected products cost {expected_cost}, got {products_cost}" + ); + } + + // sanity check that our actuals calculation is always accumulative and based on safe data + // structures (like its current implementation, using DashMap, which functions similarly to a + // RwLock) -- this test looks at different subgraph updates + #[tokio::test] + async fn test_concurrent_actual_cost_multiple_subgraphs() { + // multiple updates to DIFFERENT subgraphs + let ctx = Arc::new(Context::new()); + let subgraphs = ["users", "products", "reviews", "inventory", "pricing"]; + let updates_per_subgraph = 20; + let cost_per_update = 1.5; + + let mut join_set = JoinSet::new(); + for subgraph in subgraphs.iter() { + for _ in 0..updates_per_subgraph { + let ctx = ctx.clone(); + let subgraph = subgraph.to_string(); + join_set.spawn(async move { + ctx.update_actual_cost_by_subgraph(&subgraph, cost_per_update) + .expect("update should succeed"); + }); + } + } + + while let Some(result) = join_set.join_next().await { + result.expect("task should not panic"); + } + + let cost_by_subgraph = ctx + .get_actual_cost_by_subgraph() + .expect("should deserialize") + .expect("should have value"); + + for subgraph in subgraphs.iter() { + let cost = cost_by_subgraph + .get(subgraph) + .expect("should have subgraph"); + let expected = updates_per_subgraph as f64 * cost_per_update; + + assert_eq!( + cost, expected, + "Expected {subgraph} cost {expected}, got {cost}" + ); + } + + let total = cost_by_subgraph.total(); + let expected_total = subgraphs.len() as f64 * updates_per_subgraph as f64 * cost_per_update; + assert_eq!( + total, expected_total, + "Expected total cost {expected_total}, got {total}" + ); + } } diff --git a/apollo-router/src/plugins/demand_control/strategy/mod.rs b/apollo-router/src/plugins/demand_control/strategy/mod.rs index 1cb5a992f4..f7f334f42e 100644 --- a/apollo-router/src/plugins/demand_control/strategy/mod.rs +++ b/apollo-router/src/plugins/demand_control/strategy/mod.rs @@ -4,11 +4,14 @@ use ahash::HashMap; use apollo_compiler::ExecutableDocument; use crate::Context; +use crate::configuration::subgraph::SubgraphConfiguration; use crate::graphql; +use crate::plugins::demand_control::ActualCostMode; use crate::plugins::demand_control::DemandControlConfig; use crate::plugins::demand_control::DemandControlError; use crate::plugins::demand_control::Mode; use crate::plugins::demand_control::StrategyConfig; +use crate::plugins::demand_control::SubgraphStrategyConfig; use crate::plugins::demand_control::cost_calculator::schema::DemandControlledSchema; use crate::plugins::demand_control::cost_calculator::static_cost::StaticCostCalculator; use crate::plugins::demand_control::strategy::static_estimated::StaticEstimated; @@ -95,21 +98,41 @@ impl StrategyFactory { } } + pub(crate) fn create_static_estimated_strategy( + &self, + list_size: u32, + max: f64, + actual_cost_mode: ActualCostMode, + subgraphs: &SubgraphConfiguration, + ) -> StaticEstimated { + let subgraph_maxes = subgraphs.extract(|config| config.max); + let subgraph_list_sizes = subgraphs.extract(|config| config.list_size); + StaticEstimated { + max, + subgraph_maxes, + actual_cost_mode, + cost_calculator: StaticCostCalculator::new( + self.supergraph_schema.clone(), + self.subgraph_schemas.clone(), + Arc::new(subgraph_list_sizes), + list_size, + ), + } + } + pub(crate) fn create(&self) -> Strategy { let strategy: Arc = match &self.config.strategy { StrategyConfig::StaticEstimated { list_size, max, actual_cost_mode, - } => Arc::new(StaticEstimated { - max: *max, - actual_cost_mode: *actual_cost_mode, - cost_calculator: StaticCostCalculator::new( - self.supergraph_schema.clone(), - self.subgraph_schemas.clone(), - *list_size, - ), - }), + subgraph, + } => Arc::new(self.create_static_estimated_strategy( + *list_size, + *max, + *actual_cost_mode, + subgraph, + )), #[cfg(test)] StrategyConfig::Test { stage, error } => Arc::new(test::Test { stage: stage.clone(), diff --git a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs index 4042478c27..ccb5f449d1 100644 --- a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs +++ b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs @@ -1,5 +1,6 @@ use apollo_compiler::ExecutableDocument; +use crate::configuration::subgraph::SubgraphConfiguration; use crate::graphql; use crate::plugins::demand_control::ActualCostMode; use crate::plugins::demand_control::DemandControlError; @@ -12,10 +13,17 @@ use crate::services::subgraph; pub(crate) struct StaticEstimated { // The estimated value of the demand pub(crate) max: f64, + pub(crate) subgraph_maxes: SubgraphConfiguration>, pub(crate) actual_cost_mode: ActualCostMode, pub(crate) cost_calculator: StaticCostCalculator, } +impl StaticEstimated { + fn subgraph_max(&self, subgraph_name: &str) -> Option { + *self.subgraph_maxes.get(subgraph_name) + } +} + impl StrategyImpl for StaticEstimated { fn on_execution_request(&self, request: &execution::Request) -> Result<(), DemandControlError> { self.cost_calculator @@ -23,12 +31,15 @@ impl StrategyImpl for StaticEstimated { &request.query_plan, &request.supergraph_request.body().variables, ) - .and_then(|cost| { + .and_then(|cost_by_subgraph| { + let cost = cost_by_subgraph.total(); request .context .insert_cost_strategy("static_estimated".to_string())?; - request.context.insert_cost_result("COST_OK".to_string())?; request.context.insert_estimated_cost(cost)?; + request + .context + .insert_estimated_cost_by_subgraph(cost_by_subgraph)?; if cost > self.max { let error = DemandControlError::EstimatedCostTooExpensive { @@ -40,13 +51,37 @@ impl StrategyImpl for StaticEstimated { .insert_cost_result(error.code().to_string())?; Err(error) } else { + request.context.insert_cost_result("COST_OK".to_string())?; Ok(()) } }) } - fn on_subgraph_request(&self, _request: &subgraph::Request) -> Result<(), DemandControlError> { - Ok(()) + fn on_subgraph_request(&self, request: &subgraph::Request) -> Result<(), DemandControlError> { + let cost_by_subgraph = request.context.get_estimated_cost_by_subgraph()?; + + let subgraph = request.subgraph_name.clone(); + + if let Some(cost) = cost_by_subgraph.and_then(|c| c.get(&subgraph)) + && let Some(max) = self.subgraph_max(&subgraph) + && cost > max + { + // reject subgraph request when the total subgraph cost exceeds the subgraph max + let error = DemandControlError::EstimatedSubgraphCostTooExpensive { + subgraph: subgraph.clone(), + estimated_cost: cost, + max_cost: max, + }; + request + .context + .insert_cost_by_subgraph_result(subgraph, error.code().to_string())?; + Err(error) + } else { + request + .context + .insert_cost_by_subgraph_result(subgraph, "COST_OK".to_string())?; + Ok(()) + } } fn on_subgraph_response( @@ -105,3 +140,80 @@ impl StrategyImpl for StaticEstimated { Ok(()) } } + +#[cfg(test)] +mod tests { + use tower::BoxError; + + use super::StaticEstimated; + use crate::plugins::demand_control::DemandControl; + use crate::plugins::demand_control::StrategyConfig; + use crate::plugins::test::PluginTestHarness; + + async fn load_config_and_extract_strategy( + config: &'static str, + ) -> Result { + let schema_str = + include_str!("../cost_calculator/fixtures/basic_supergraph_schema.graphql"); + let plugin = PluginTestHarness::::builder() + .config(config) + .schema(schema_str) + .build() + .await?; + + let StrategyConfig::StaticEstimated { + list_size, + max, + actual_cost_mode, + ref subgraph, + } = plugin.config.strategy + else { + panic!("must provide static_estimated config"); + }; + + let strategy = plugin.strategy_factory.create_static_estimated_strategy( + list_size, + max, + actual_cost_mode, + subgraph, + ); + Ok(strategy) + } + + #[tokio::test] + async fn test_per_subgraph_configuration_inheritance() { + let config = include_str!("../fixtures/per_subgraph_inheritance.yaml"); + + let strategy = load_config_and_extract_strategy(config).await.unwrap(); + assert_eq!(strategy.subgraph_max("reviews").unwrap(), 2.0); + assert_eq!(strategy.subgraph_max("products").unwrap(), 5.0); + assert_eq!(strategy.subgraph_max("users").unwrap(), 5.0); + } + + #[tokio::test] + async fn test_per_subgraph_configuration_no_inheritance() { + let config = include_str!("../fixtures/per_subgraph_no_inheritance.yaml"); + + let strategy = load_config_and_extract_strategy(config).await.unwrap(); + assert_eq!(strategy.subgraph_max("reviews").unwrap(), 2.0); + assert!(strategy.subgraph_max("products").is_none()); + assert!(strategy.subgraph_max("users").is_none()); + } + + #[tokio::test] + async fn test_invalid_per_subgraph_configuration() { + let config = include_str!("../fixtures/per_subgraph_invalid.yaml"); + let strategy_result = load_config_and_extract_strategy(config).await; + + match strategy_result { + Ok(strategy) => { + eprintln!("{:?}", strategy.subgraph_maxes); + panic!("Expected error") + } + Err(err) => assert_eq!( + &err.to_string(), + "Maximum per-subgraph query cost for `products` is negative" + ), + }; + } +} diff --git a/apollo-router/src/plugins/rhai/engine/mod.rs b/apollo-router/src/plugins/rhai/engine/mod.rs index f97474ea53..ca89585345 100644 --- a/apollo-router/src/plugins/rhai/engine/mod.rs +++ b/apollo-router/src/plugins/rhai/engine/mod.rs @@ -45,6 +45,9 @@ use crate::http_ext; use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::plugins::cache::entity::CONTEXT_CACHE_KEY; use crate::plugins::demand_control::COST_ACTUAL_KEY; +use crate::plugins::demand_control::COST_BY_SUBGRAPH_ACTUAL_KEY; +use crate::plugins::demand_control::COST_BY_SUBGRAPH_ESTIMATED_KEY; +use crate::plugins::demand_control::COST_BY_SUBGRAPH_RESULT_KEY; use crate::plugins::demand_control::COST_ESTIMATED_KEY; use crate::plugins::demand_control::COST_RESULT_KEY; use crate::plugins::demand_control::COST_STRATEGY_KEY; @@ -1463,6 +1466,18 @@ impl Rhai { global_variables.insert("APOLLO_COST_ACTUAL_KEY".into(), COST_ACTUAL_KEY.into()); global_variables.insert("APOLLO_COST_STRATEGY_KEY".into(), COST_STRATEGY_KEY.into()); global_variables.insert("APOLLO_COST_RESULT_KEY".into(), COST_RESULT_KEY.into()); + global_variables.insert( + "APOLLO_COST_BY_SUBGRAPH_ESTIMATED_KEY".into(), + COST_BY_SUBGRAPH_ESTIMATED_KEY.into(), + ); + global_variables.insert( + "APOLLO_COST_BY_SUBGRAPH_ACTUAL_KEY".into(), + COST_BY_SUBGRAPH_ACTUAL_KEY.into(), + ); + global_variables.insert( + "APOLLO_COST_BY_SUBGRAPH_RESULT_KEY".into(), + COST_BY_SUBGRAPH_RESULT_KEY.into(), + ); let shared_globals = Arc::new(global_variables); diff --git a/apollo-router/src/plugins/rhai/tests.rs b/apollo-router/src/plugins/rhai/tests.rs index 3b848e7193..4e015cd0b7 100644 --- a/apollo-router/src/plugins/rhai/tests.rs +++ b/apollo-router/src/plugins/rhai/tests.rs @@ -36,6 +36,7 @@ use crate::plugin::test::MockExecutionService; use crate::plugin::test::MockRouterService; use crate::plugin::test::MockSubgraphService; use crate::plugin::test::MockSupergraphService; +use crate::plugins::demand_control::cost_calculator::CostBySubgraph; use crate::plugins::rhai::engine::RhaiExecutionDeferredResponse; use crate::plugins::rhai::engine::RhaiExecutionResponse; use crate::plugins::rhai::engine::RhaiRouterChunkedResponse; @@ -825,31 +826,42 @@ async fn it_can_access_demand_control_context() -> Result<(), BoxError> { .insert_cost_strategy("test_strategy".to_string()) .unwrap(); context.insert_cost_result("COST_OK".to_string()).unwrap(); + let estimated_cost_by_subgraph = + CostBySubgraph::from(&[("products", 40.0), ("users", 10.0)][..]); + context.insert_estimated_cost_by_subgraph(estimated_cost_by_subgraph)?; + let actual_cost_by_subgraph = CostBySubgraph::from(&[("products", 29.0), ("users", 6.0)][..]); + context.insert_actual_cost_by_subgraph(actual_cost_by_subgraph)?; + context.insert_cost_by_subgraph_result("products".to_string(), "COST_OK".to_string())?; + context.insert_cost_by_subgraph_result("users".to_string(), "COST_OK".to_string())?; + let supergraph_req = SupergraphRequest::fake_builder().context(context).build()?; let service_response = router_service.ready().await?.call(supergraph_req).await?; assert_eq!(StatusCode::OK, service_response.response.status()); - let headers = service_response.response.headers().clone(); - let demand_control_header = headers - .get("demand-control-estimate") - .map(|h| h.to_str().unwrap()); - assert_eq!(demand_control_header, Some("50.0")); - - let demand_control_header = headers - .get("demand-control-actual") - .map(|h| h.to_str().unwrap()); - assert_eq!(demand_control_header, Some("35.0")); - - let demand_control_header = headers - .get("demand-control-strategy") - .map(|h| h.to_str().unwrap()); - assert_eq!(demand_control_header, Some("test_strategy")); - - let demand_control_header = headers - .get("demand-control-result") - .map(|h| h.to_str().unwrap()); - assert_eq!(demand_control_header, Some("COST_OK")); + let headers = service_response.response.headers(); + for (key, expected_value) in [ + ("demand-control-estimate", "50.0"), + ("demand-control-actual", "35.0"), + ("demand-control-strategy", "test_strategy"), + ("demand-control-result", "COST_OK"), + ("demand-control-estimate-subgraph-products", "40.0"), + ("demand-control-estimate-subgraph-users", "10.0"), + ("demand-control-estimate-subgraphs", "2"), + ("demand-control-actual-subgraph-products", "29.0"), + ("demand-control-actual-subgraph-users", "6.0"), + ("demand-control-actual-subgraphs", "2"), + ("demand-control-result-subgraph-products", "COST_OK"), + ("demand-control-result-subgraph-users", "COST_OK"), + ("demand-control-result-subgraphs", "2"), + ] { + let header_value = headers + .get(key) + .unwrap_or_else(|| panic!("headers should have value for key `{key}`")) + .to_str() + .unwrap_or_else(|_| panic!("value for key `{key}` should be a string")); + assert_eq!(header_value, expected_value, "key = `{key}`"); + } Ok(()) } diff --git a/apollo-router/tests/fixtures/demand_control.rhai b/apollo-router/tests/fixtures/demand_control.rhai index 8232b1ad28..36ab51d148 100644 --- a/apollo-router/tests/fixtures/demand_control.rhai +++ b/apollo-router/tests/fixtures/demand_control.rhai @@ -15,4 +15,19 @@ fn process_response(response) { const result = response.context[Router.APOLLO_COST_RESULT_KEY]; response.headers["demand-control-result"] = result; + + const estimates_by_subgraph = response.context[Router.APOLLO_COST_BY_SUBGRAPH_ESTIMATED_KEY]; + response.headers["demand-control-estimate-subgraph-products"] = to_string(estimates_by_subgraph["products"]); + response.headers["demand-control-estimate-subgraph-users"] = to_string(estimates_by_subgraph["users"]); + response.headers["demand-control-estimate-subgraphs"] = to_string(estimates_by_subgraph.len()); + + const actuals_by_subgraph = response.context[Router.APOLLO_COST_BY_SUBGRAPH_ACTUAL_KEY]; + response.headers["demand-control-actual-subgraph-products"] = to_string(actuals_by_subgraph["products"]); + response.headers["demand-control-actual-subgraph-users"] = to_string(actuals_by_subgraph["users"]); + response.headers["demand-control-actual-subgraphs"] = to_string(actuals_by_subgraph.len()); + + const results_by_subgraph = response.context[Router.APOLLO_COST_BY_SUBGRAPH_RESULT_KEY]; + response.headers["demand-control-result-subgraph-products"] = to_string(results_by_subgraph["products"]); + response.headers["demand-control-result-subgraph-users"] = to_string(results_by_subgraph["users"]); + response.headers["demand-control-result-subgraphs"] = to_string(actuals_by_subgraph.len()); } \ No newline at end of file diff --git a/apollo-router/tests/integration/demand_control.rs b/apollo-router/tests/integration/demand_control.rs index c2ca962b47..686db4a19d 100644 --- a/apollo-router/tests/integration/demand_control.rs +++ b/apollo-router/tests/integration/demand_control.rs @@ -3,6 +3,10 @@ use apollo_router::services::supergraph; use tokio_stream::StreamExt; use tower::BoxError; use tower::ServiceExt; +use wiremock::ResponseTemplate; + +// Reasonable default max that should not be exceeded by any of these tests. +const MAX_COST: f64 = 10_000_000.0; macro_rules! set_snapshot_suffix { ($($expr:expr),*) => { @@ -84,6 +88,9 @@ fn federated_ships_required() -> TestSetupParameters { {"__typename": "Ship", "id": 1, "owner": {"addresses": [{"zipCode": 18263}]}, "registrationFee": 129.2}, {"__typename": "Ship", "id": 2, "owner": {"addresses": [{"zipCode": 61027}]}, "registrationFee": 14.0}, {"__typename": "Ship", "id": 3, "owner": {"addresses": [{"zipCode": 86204}]}, "registrationFee": 97.15}, + {"__typename": "Ship", "id": 1, "owner": null, "registrationFee": null}, + {"__typename": "Ship", "id": 2, "owner": null, "registrationFee": null}, + {"__typename": "Ship", "id": 3, "owner": null, "registrationFee": null}, ] }, "users": { @@ -196,7 +203,9 @@ async fn parse_result_for_snapshot(response: supergraph::Response) -> serde_json "apollo::demand_control::actual_cost", "apollo::demand_control::actual_cost_by_subgraph", "apollo::demand_control::estimated_cost", + "apollo::demand_control::estimated_cost_by_subgraph", "apollo::demand_control::result", + "apollo::demand_control::result_by_subgraph", "apollo::demand_control::strategy", "apollo::experimental_mock_subgraphs::subgraph_call_count", ] { @@ -298,7 +307,7 @@ async fn actual_cost_can_vary_based_on_mode( "static_estimated": { "list_size": 10, "actual_cost_mode": mode, - "max": 10000000 + "max": MAX_COST } } }); @@ -308,3 +317,275 @@ async fn actual_cost_can_vary_based_on_mode( Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +#[rstest::rstest] +async fn requests_exceeding_max_are_rejected_regardless_of_subgraph_config( + #[values( + basic_fragments(), + basic_mutation(), + federated_ships_required(), + federated_ships_fragment(), + custom_costs() + )] + test_parameters: TestSetupParameters, +) -> Result<(), BoxError> { + set_snapshot_suffix!("{}", test_parameters.name); + + let demand_control = serde_json::json!({ + "enabled": true, + "mode": "enforce", + "strategy": { + "static_estimated": { + "list_size": 10, + "max": 1.0, + "subgraph": { + "all": { + "max": MAX_COST + } + } + } + } + }); + + let response = query_supergraph_service(test_parameters, demand_control).await?; + insta::assert_json_snapshot!(parse_result_for_snapshot(response).await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[rstest::rstest] +#[case(basic_fragments(), "products")] +#[case(basic_mutation(), "products")] +#[case(federated_ships_required(), "users")] +#[case(federated_ships_fragment(), "vehicles")] +#[case(custom_costs(), "subgraphWithListSize")] +async fn requests_exceeding_one_subgraph_cost_are_accepted( + #[case] test_parameters: TestSetupParameters, + #[case] subgraph: &str, +) -> Result<(), BoxError> { + set_snapshot_suffix!("{}", test_parameters.name); + + let demand_control = serde_json::json!({ + "enabled": true, + "mode": "enforce", + "strategy": { + "static_estimated": { + "list_size": 10, + "max": MAX_COST, + "subgraph": { + "subgraphs": { + subgraph: { + "max": 1.0 + } + } + } + } + } + }); + + let response = query_supergraph_service(test_parameters, demand_control).await?; + insta::assert_json_snapshot!(parse_result_for_snapshot(response).await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[rstest::rstest] +async fn requests_exceeding_max_are_not_rejected_in_measure_mode( + #[values( + basic_fragments(), + basic_mutation(), + federated_ships_required(), + federated_ships_fragment(), + custom_costs() + )] + test_parameters: TestSetupParameters, +) -> Result<(), BoxError> { + set_snapshot_suffix!("{}", test_parameters.name); + + let demand_control = serde_json::json!({ + "enabled": true, + "mode": "measure", + "strategy": { + "static_estimated": { + "list_size": 100, + "max": 1.0, + "subgraph": { + "all": { + "max": 1.0 + } + } + } + } + }); + + let response = query_supergraph_service(test_parameters, demand_control).await?; + insta::assert_json_snapshot!(parse_result_for_snapshot(response).await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[rstest::rstest] +#[case(basic_fragments(), "products")] +#[case(federated_ships_fragment(), "vehicles")] +async fn list_size_subgraph_inheritance_changes_estimates( + #[case] test_parameters: TestSetupParameters, + #[case] subgraph_name: &str, + #[values(1, 10)] list_size: u64, + #[values(None, Some(2))] all_list_size: Option, + #[values(None, Some(3))] subgraph_list_size: Option, +) -> Result<(), BoxError> { + // Tests various permutations of list_sizes (both specified and null) to ensure that those + // list size defaults are being properly accounted for. + set_snapshot_suffix!( + "{}_{}_{}_{}", + test_parameters.name, + list_size, + all_list_size.map_or_else(|| "null".to_string(), |s| s.to_string()), + subgraph_list_size.map_or_else(|| "null".to_string(), |s| s.to_string()) + ); + + let mut demand_control = serde_json::json!({ + "enabled": true, + "mode": "enforce", + "strategy": { + "static_estimated": { + "list_size": list_size, + "max": MAX_COST, + "subgraph": { + "all": {}, + "subgraphs": { + subgraph_name: {} + } + } + } + } + }); + + if let Some(list_size) = all_list_size { + let path = "/strategy/static_estimated/subgraph/all"; + *demand_control.pointer_mut(path).unwrap() = serde_json::json!({"list_size": list_size}); + } + + if let Some(list_size) = subgraph_list_size { + let path = format!("/strategy/static_estimated/subgraph/subgraphs/{subgraph_name}"); + *demand_control.pointer_mut(&path).unwrap() = serde_json::json!({"list_size": list_size}); + } + + let response = query_supergraph_service(test_parameters, demand_control).await?; + insta::assert_json_snapshot!(parse_result_for_snapshot(response).await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn coprocessor_can_access_and_mutate_costs() -> Result<(), BoxError> { + let test_parameters = federated_ships_required(); + set_snapshot_suffix!("{}", test_parameters.name); + + // for this query and a configured list_size = 5: + // estimated_cost: 45.0 + // estimated_cost_by_subgraph: + // users: 30.0 + // vehicles: 15.0 + // the strategy is configured to reject the `users` queries (by setting subgraph.all.max = 20) + // but the execution-stage request coprocessor will modify the estimated cost to allow the query to be fully + // executed. + // the actual values are then checked at the execution-stage response coprocessor. + // + // NB: I don't think real coprocessors _should_ mutate costs, but it's a useful way to test things. + + let mock_server = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .and(wiremock::matchers::path("/")) + .respond_with(move |req: &wiremock::Request| { + let request = req.body_json::().expect("body"); + let stage = request.get("stage").and_then(|s| s.as_str()).unwrap_or(""); + + let mut response = request.clone(); + match stage { + "ExecutionRequest" => { + // read value of estimated_cost_by_subgraph for users, then set it to a different value. + let path = + "/context/entries/apollo::demand_control::estimated_cost_by_subgraph/users"; + + let users_cost = response.pointer_mut(path).unwrap(); + assert_eq!(users_cost.as_f64(), Some(30.0)); + *users_cost = 15.0.into(); + } + "ExecutionResponse" => { + // should see actual costs and results for both subgraphs + let path = "/context/entries/apollo::demand_control::actual_cost_by_subgraph"; + let actual_costs = request.pointer(path).unwrap(); + assert_eq!(actual_costs["users"].as_f64(), Some(6.0)); + assert_eq!(actual_costs["vehicles"].as_f64(), Some(9.0)); + + let path = "/context/entries/apollo::demand_control::result_by_subgraph"; + let results = request.pointer(path).unwrap(); + assert_eq!(results["users"].as_str(), Some("COST_OK")); + assert_eq!(results["vehicles"].as_str(), Some("COST_OK")); + } + _ => panic!("unexpected stage `{stage}`"), + } + ResponseTemplate::new(200).set_body_json(response) + }) + .expect(2) + .mount(&mock_server) + .await; + + let demand_control = serde_json::json!({ + "enabled": true, + "mode": "enforce", + "strategy": { + "static_estimated": { + "list_size": 5, + "max": MAX_COST, + "subgraph": { + "all": { + "max": 20.0 + } + } + } + } + }); + + let coprocessor = serde_json::json!({ + "url": mock_server.uri(), + "execution": { + "request": { + "context": { + "selective": ["apollo::demand_control::estimated_cost_by_subgraph"], + }, + }, + "response": { + "context": { + "selective": [ + "apollo::demand_control::actual_cost_by_subgraph", + "apollo::demand_control::result_by_subgraph" + ], + }, + }, + }, + }); + + let service = TestHarness::builder() + .schema(test_parameters.schema) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": {"all": true}, + "coprocessor": coprocessor, + "demand_control": demand_control, + "experimental_mock_subgraphs": test_parameters.subgraphs, + }))? + .build_supergraph() + .await?; + + let request = supergraph::Request::fake_builder() + .query(test_parameters.query) + .build()?; + let response = service.oneshot(request).await?; + insta::assert_json_snapshot!(parse_result_for_snapshot(response).await); + Ok(()) +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_response_shape.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_response_shape.snap index 136db53cd1..ecd00c48c8 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_response_shape.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_response_shape.snap @@ -6,7 +6,13 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": 2.0, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_subgraph.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_subgraph.snap index 35d7f14511..65e2436a3a 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_subgraph.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_fragments_by_subgraph.snap @@ -8,7 +8,13 @@ expression: parse_result_for_snapshot(response).await "products": 2.0 }, "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_response_shape.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_response_shape.snap index 218da6bdfa..e9e192da0d 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_response_shape.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_response_shape.snap @@ -6,7 +6,13 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": 0.0, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_subgraph.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_subgraph.snap index db91554d35..f54b8d51b6 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_subgraph.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@basic_mutation_by_subgraph.snap @@ -8,7 +8,13 @@ expression: parse_result_for_snapshot(response).await "products": 0.0 }, "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_response_shape.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_response_shape.snap index 651ae33052..ce876d204d 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_response_shape.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_response_shape.snap @@ -6,7 +6,15 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": 124.0, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "subgraphWithCost": "COST_OK", + "subgraphWithListSize": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "subgraphWithCost": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_subgraph.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_subgraph.snap index 64570a71eb..4ea7b93733 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_subgraph.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@custom_costs_by_subgraph.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "subgraphWithListSize": 3.0 }, "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "subgraphWithCost": "COST_OK", + "subgraphWithListSize": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "subgraphWithCost": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_response_shape.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_response_shape.snap index d24c2454fd..ee163c2849 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_response_shape.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_response_shape.snap @@ -6,7 +6,15 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": 12.0, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 2, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_subgraph.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_subgraph.snap index 62634dc166..2905772e5a 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_subgraph.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_fragment_by_subgraph.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "vehicles": 10.0 }, "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 2, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_response_shape.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_response_shape.snap index 150c70351a..76c0b8b5de 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_response_shape.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_response_shape.snap @@ -6,7 +6,15 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": 3.0, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 140.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 110.0, + "vehicles": 30.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_subgraph.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_subgraph.snap index c39644343a..f6cd01b34c 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_subgraph.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__actual_cost_can_vary_based_on_mode@federated_ships_required_by_subgraph.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "vehicles": 9.0 }, "apollo::demand_control::estimated_cost": 140.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 110.0, + "vehicles": 30.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__coprocessor_can_access_and_mutate_costs@federated_ships_required.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__coprocessor_can_access_and_mutate_costs@federated_ships_required.snap new file mode 100644 index 0000000000..273e377771 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__coprocessor_can_access_and_mutate_costs@federated_ships_required.snap @@ -0,0 +1,44 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 6.0, + "vehicles": 9.0 + }, + "apollo::demand_control::estimated_cost": 45.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 15.0, + "vehicles": 15.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 1, + "vehicles": 2 + }, + "body": { + "data": { + "ships": [ + { + "name": "Ship1", + "registrationFee": 129.2 + }, + { + "name": "Ship2", + "registrationFee": 14.0 + }, + { + "name": "Ship3", + "registrationFee": 97.15 + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_2_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_2_3.snap new file mode 100644 index 0000000000..805674f6d3 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_2_3.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 5.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 5.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_2_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_2_null.snap new file mode 100644 index 0000000000..63673da94f --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_2_null.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 4.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 4.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_null_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_null_3.snap new file mode 100644 index 0000000000..805674f6d3 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_null_3.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 5.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 5.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_null_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_null_null.snap new file mode 100644 index 0000000000..65e2436a3a --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_10_null_null.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_2_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_2_3.snap new file mode 100644 index 0000000000..805674f6d3 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_2_3.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 5.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 5.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_2_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_2_null.snap new file mode 100644 index 0000000000..63673da94f --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_2_null.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 4.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 4.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_null_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_null_3.snap new file mode 100644 index 0000000000..805674f6d3 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_null_3.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 5.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 5.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_null_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_null_null.snap new file mode 100644 index 0000000000..5d8b311a35 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@basic_fragments_1_null_null.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 3.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 3.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_2_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_2_3.snap new file mode 100644 index 0000000000..f0502985d7 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_2_3.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 4.0, + "vehicles": 6.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_2_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_2_null.snap new file mode 100644 index 0000000000..d7fba5d2cb --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_2_null.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 8.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 4.0, + "vehicles": 4.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_null_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_null_3.snap new file mode 100644 index 0000000000..7c68cbed5d --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_null_3.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 26.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 6.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_null_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_null_null.snap new file mode 100644 index 0000000000..2905772e5a --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_10_null_null.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_2_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_2_3.snap new file mode 100644 index 0000000000..f0502985d7 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_2_3.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 4.0, + "vehicles": 6.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_2_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_2_null.snap new file mode 100644 index 0000000000..d7fba5d2cb --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_2_null.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 8.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 4.0, + "vehicles": 4.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_null_3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_null_3.snap new file mode 100644 index 0000000000..7b8d0ad8a8 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_null_3.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 8.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 2.0, + "vehicles": 6.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_null_null.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_null_null.snap new file mode 100644 index 0000000000..83439e2f0e --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__list_size_subgraph_inheritance_changes_estimates@federated_ships_fragment_1_null_null.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 4.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 2.0, + "vehicles": 2.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@basic_fragments.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@basic_fragments.snap new file mode 100644 index 0000000000..bbefda1aaf --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@basic_fragments.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 2.0 + }, + "apollo::demand_control::estimated_cost": 102.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 102.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": { + "products": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "interfaceInstance1": { + "field1": null, + "field2": "hello" + }, + "someUnion": { + "innerList": [] + } + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@basic_mutation.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@basic_mutation.snap new file mode 100644 index 0000000000..4bd5df3c5a --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@basic_mutation.snap @@ -0,0 +1,27 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 0.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "products": 0.0 + }, + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": { + "products": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "products": 1 + }, + "body": { + "data": { + "doSomething": 6 + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@custom_costs.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@custom_costs.snap new file mode 100644 index 0000000000..33c33e7517 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@custom_costs.snap @@ -0,0 +1,55 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 124.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 3.0 + }, + "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": { + "subgraphWithCost": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "subgraphWithListSize": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "subgraphWithCost": 1, + "subgraphWithListSize": 1 + }, + "body": { + "data": { + "argWithCost": 30, + "enumWithCost": "A", + "fieldWithCost": 2, + "fieldWithDynamicListSize": { + "items": [ + { + "id": 7 + }, + { + "id": 9 + } + ] + }, + "fieldWithListSize": [ + "hello", + "world", + "and", + "nearby", + "planets" + ], + "inputWithCost": 5, + "objectWithCost": { + "id": 9 + }, + "scalarWithCost": 6172364 + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@federated_ships_fragment.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@federated_ships_fragment.snap new file mode 100644 index 0000000000..c9bc8c3609 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@federated_ships_fragment.snap @@ -0,0 +1,72 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 5.0, + "vehicles": 10.0 + }, + "apollo::demand_control::estimated_cost": 400.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 200.0, + "vehicles": 200.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": { + "users": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "vehicles": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 2, + "vehicles": 1 + }, + "body": { + "data": { + "ships": [ + { + "owner": { + "licenseNumber": 100, + "name": "User100" + } + }, + { + "owner": { + "licenseNumber": 110, + "name": "User110" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + }, + { + "owner": { + "licenseNumber": 120, + "name": "User120" + } + } + ], + "users": [ + { + "licenseNumber": 10, + "name": "User10" + }, + { + "licenseNumber": 11, + "name": "User11" + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@federated_ships_required.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@federated_ships_required.snap new file mode 100644 index 0000000000..b78095ac2c --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_not_rejected_in_measure_mode@federated_ships_required.snap @@ -0,0 +1,44 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 15.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 6.0, + "vehicles": 9.0 + }, + "apollo::demand_control::estimated_cost": 10400.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 10100.0, + "vehicles": 300.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": { + "users": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "vehicles": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 1, + "vehicles": 2 + }, + "body": { + "data": { + "ships": [ + { + "name": "Ship1", + "registrationFee": 129.2 + }, + { + "name": "Ship2", + "registrationFee": 14.0 + }, + { + "name": "Ship3", + "registrationFee": 97.15 + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_fragments.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_fragments.snap index 7227d45e5a..7fa55461c8 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_fragments.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_fragments.snap @@ -6,7 +6,11 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": null, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 102.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 102.0 + }, "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": null, "body": { diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_mutation.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_mutation.snap index c84d3952cc..0aa7d5d2ac 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_mutation.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@basic_mutation.snap @@ -6,7 +6,11 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": null, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": null, "body": { diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@custom_costs.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@custom_costs.snap index c70d3f6fad..de01ce816b 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@custom_costs.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@custom_costs.snap @@ -6,7 +6,12 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": null, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": null, "body": { diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_fragment.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_fragment.snap index 7c968904d1..9c20d9d19b 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_fragment.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_fragment.snap @@ -6,7 +6,12 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": null, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 400.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 200.0, + "vehicles": 200.0 + }, "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": null, "body": { diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_required.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_required.snap index 2209234666..dca956f4fc 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_required.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected@federated_ships_required.snap @@ -6,7 +6,12 @@ expression: parse_result_for_snapshot(response).await "apollo::demand_control::actual_cost": null, "apollo::demand_control::actual_cost_by_subgraph": null, "apollo::demand_control::estimated_cost": 10400.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 10100.0, + "vehicles": 300.0 + }, "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": null, "body": { diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@basic_fragments.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@basic_fragments.snap new file mode 100644 index 0000000000..e2e609743e --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@basic_fragments.snap @@ -0,0 +1,28 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": null, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "errors": [ + { + "extensions": { + "code": "COST_ESTIMATED_TOO_EXPENSIVE", + "cost.estimated": 12.0, + "cost.max": 1.0 + }, + "message": "query estimated cost 12 exceeded configured maximum 1" + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@basic_mutation.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@basic_mutation.snap new file mode 100644 index 0000000000..0aa7d5d2ac --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@basic_mutation.snap @@ -0,0 +1,28 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": null, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "errors": [ + { + "extensions": { + "code": "COST_ESTIMATED_TOO_EXPENSIVE", + "cost.estimated": 10.0, + "cost.max": 1.0 + }, + "message": "query estimated cost 10 exceeded configured maximum 1" + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@custom_costs.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@custom_costs.snap new file mode 100644 index 0000000000..de01ce816b --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@custom_costs.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": null, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "errors": [ + { + "extensions": { + "code": "COST_ESTIMATED_TOO_EXPENSIVE", + "cost.estimated": 127.0, + "cost.max": 1.0 + }, + "message": "query estimated cost 127 exceeded configured maximum 1" + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@federated_ships_fragment.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@federated_ships_fragment.snap new file mode 100644 index 0000000000..c10a31405c --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@federated_ships_fragment.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": null, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "errors": [ + { + "extensions": { + "code": "COST_ESTIMATED_TOO_EXPENSIVE", + "cost.estimated": 40.0, + "cost.max": 1.0 + }, + "message": "query estimated cost 40 exceeded configured maximum 1" + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@federated_ships_required.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@federated_ships_required.snap new file mode 100644 index 0000000000..9ab2e13246 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_max_are_rejected_regardless_of_subgraph_config@federated_ships_required.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": null, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 140.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 110.0, + "vehicles": 30.0 + }, + "apollo::demand_control::result": "COST_ESTIMATED_TOO_EXPENSIVE", + "apollo::demand_control::result_by_subgraph": null, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "errors": [ + { + "extensions": { + "code": "COST_ESTIMATED_TOO_EXPENSIVE", + "cost.estimated": 140.0, + "cost.max": 1.0 + }, + "message": "query estimated cost 140 exceeded configured maximum 1" + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@basic_fragments.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@basic_fragments.snap new file mode 100644 index 0000000000..91d645ed97 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@basic_fragments.snap @@ -0,0 +1,34 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 0.0, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "data": null, + "errors": [ + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "products", + "cost.subgraph.estimated": 12.0, + "cost.subgraph.max": 1.0, + "service": "products" + }, + "message": "query estimated cost 12 exceeded configured maximum 1 for subgraph products", + "path": [] + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@basic_mutation.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@basic_mutation.snap new file mode 100644 index 0000000000..d4cf2de9d2 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@basic_mutation.snap @@ -0,0 +1,34 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 0.0, + "apollo::demand_control::actual_cost_by_subgraph": null, + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": null, + "body": { + "data": null, + "errors": [ + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "products", + "cost.subgraph.estimated": 10.0, + "cost.subgraph.max": 1.0, + "service": "products" + }, + "message": "query estimated cost 10 exceeded configured maximum 1 for subgraph products", + "path": [] + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@custom_costs.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@custom_costs.snap new file mode 100644 index 0000000000..952dea548d --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@custom_costs.snap @@ -0,0 +1,51 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 121.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "subgraphWithCost": 121.0 + }, + "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "subgraphWithCost": "COST_OK", + "subgraphWithListSize": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "subgraphWithCost": 1 + }, + "body": { + "data": { + "argWithCost": 30, + "enumWithCost": "A", + "fieldWithCost": 2, + "fieldWithDynamicListSize": null, + "fieldWithListSize": null, + "inputWithCost": 5, + "objectWithCost": { + "id": 9 + }, + "scalarWithCost": 6172364 + }, + "errors": [ + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "subgraphWithListSize", + "cost.subgraph.estimated": 6.0, + "cost.subgraph.max": 1.0, + "service": "subgraphWithListSize" + }, + "message": "query estimated cost 6 exceeded configured maximum 1 for subgraph subgraphWithListSize", + "path": [] + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@federated_ships_fragment.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@federated_ships_fragment.snap new file mode 100644 index 0000000000..cab1cb523d --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@federated_ships_fragment.snap @@ -0,0 +1,48 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 2.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "users": 2.0 + }, + "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "users": 1 + }, + "body": { + "data": null, + "errors": [ + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "vehicles", + "cost.subgraph.estimated": 20.0, + "cost.subgraph.max": 1.0, + "service": "vehicles" + }, + "message": "query estimated cost 20 exceeded configured maximum 1 for subgraph vehicles", + "path": [] + } + ], + "extensions": { + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field Query.ships", + "path": [] + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@federated_ships_required.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@federated_ships_required.snap new file mode 100644 index 0000000000..15fb908bea --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_exceeding_one_subgraph_cost_are_accepted@federated_ships_required.snap @@ -0,0 +1,86 @@ +--- +source: apollo-router/tests/integration/demand_control.rs +expression: parse_result_for_snapshot(response).await +--- +{ + "apollo::demand_control::actual_cost": 9.0, + "apollo::demand_control::actual_cost_by_subgraph": { + "vehicles": 9.0 + }, + "apollo::demand_control::estimated_cost": 140.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 110.0, + "vehicles": 30.0 + }, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "vehicles": "COST_OK" + }, + "apollo::demand_control::strategy": "static_estimated", + "apollo::experimental_mock_subgraphs::subgraph_call_count": { + "vehicles": 2 + }, + "body": { + "data": { + "ships": [ + { + "name": "Ship1", + "registrationFee": null + }, + { + "name": "Ship2", + "registrationFee": null + }, + { + "name": "Ship3", + "registrationFee": null + } + ] + }, + "errors": [ + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "users", + "cost.subgraph.estimated": 110.0, + "cost.subgraph.max": 1.0, + "service": "users" + }, + "message": "query estimated cost 110 exceeded configured maximum 1 for subgraph users", + "path": [ + "ships", + 0 + ] + }, + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "users", + "cost.subgraph.estimated": 110.0, + "cost.subgraph.max": 1.0, + "service": "users" + }, + "message": "query estimated cost 110 exceeded configured maximum 1 for subgraph users", + "path": [ + "ships", + 1 + ] + }, + { + "extensions": { + "code": "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE", + "cost.subgraph": "users", + "cost.subgraph.estimated": 110.0, + "cost.subgraph.max": 1.0, + "service": "users" + }, + "message": "query estimated cost 110 exceeded configured maximum 1 for subgraph users", + "path": [ + "ships", + 2 + ] + } + ] + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_12.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_12.snap index 35d7f14511..65e2436a3a 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_12.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_12.snap @@ -8,7 +8,13 @@ expression: parse_result_for_snapshot(response).await "products": 2.0 }, "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_15.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_15.snap index 35d7f14511..65e2436a3a 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_15.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_fragments_15.snap @@ -8,7 +8,13 @@ expression: parse_result_for_snapshot(response).await "products": 2.0 }, "apollo::demand_control::estimated_cost": 12.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 12.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_10.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_10.snap index db91554d35..f54b8d51b6 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_10.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_10.snap @@ -8,7 +8,13 @@ expression: parse_result_for_snapshot(response).await "products": 0.0 }, "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_15.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_15.snap index db91554d35..f54b8d51b6 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_15.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@basic_mutation_15.snap @@ -8,7 +8,13 @@ expression: parse_result_for_snapshot(response).await "products": 0.0 }, "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "products": 10.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "products": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "products": 1 diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_127.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_127.snap index 64570a71eb..4ea7b93733 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_127.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_127.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "subgraphWithListSize": 3.0 }, "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "subgraphWithCost": "COST_OK", + "subgraphWithListSize": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "subgraphWithCost": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_130.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_130.snap index 64570a71eb..4ea7b93733 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_130.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@custom_costs_130.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "subgraphWithListSize": 3.0 }, "apollo::demand_control::estimated_cost": 127.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "subgraphWithCost": 121.0, + "subgraphWithListSize": 6.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "subgraphWithCost": "COST_OK", + "subgraphWithListSize": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "subgraphWithCost": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_40.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_40.snap index 62634dc166..2905772e5a 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_40.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_40.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "vehicles": 10.0 }, "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 2, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_50.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_50.snap index 62634dc166..2905772e5a 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_50.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_fragment_50.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "vehicles": 10.0 }, "apollo::demand_control::estimated_cost": 40.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 20.0, + "vehicles": 20.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 2, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_140.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_140.snap index c39644343a..f6cd01b34c 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_140.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_140.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "vehicles": 9.0 }, "apollo::demand_control::estimated_cost": 140.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 110.0, + "vehicles": 30.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 1, diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_150.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_150.snap index c39644343a..f6cd01b34c 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_150.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__demand_control__requests_within_max_are_accepted@federated_ships_required_150.snap @@ -9,7 +9,15 @@ expression: parse_result_for_snapshot(response).await "vehicles": 9.0 }, "apollo::demand_control::estimated_cost": 140.0, + "apollo::demand_control::estimated_cost_by_subgraph": { + "users": 110.0, + "vehicles": 30.0 + }, "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::result_by_subgraph": { + "users": "COST_OK", + "vehicles": "COST_OK" + }, "apollo::demand_control::strategy": "static_estimated", "apollo::experimental_mock_subgraphs::subgraph_call_count": { "users": 1, diff --git a/docs/shared/config/demand_control.mdx b/docs/shared/config/demand_control.mdx index d07e07d712..2e9804289c 100644 --- a/docs/shared/config/demand_control.mdx +++ b/docs/shared/config/demand_control.mdx @@ -9,4 +9,9 @@ demand_control: list_size: 0 max: 0.0 actual_cost_mode: by_subgraph + subgraph: + all: + list_size: 0 + max: 0.0 + subgraphs: {} ``` diff --git a/docs/source/routing/customization/coprocessor/reference.mdx b/docs/source/routing/customization/coprocessor/reference.mdx index 251e0d670c..1102e291a9 100644 --- a/docs/source/routing/customization/coprocessor/reference.mdx +++ b/docs/source/routing/customization/coprocessor/reference.mdx @@ -389,6 +389,9 @@ The following keys are populated by the demand control plugin: - `apollo::demand_control::estimated_cost` - Estimated cost of the requests to be sent to the subgraphs - `apollo::demand_control::actual_cost` - Actual calculated cost of the responses returned by the subgraphs - `apollo::demand_control::result` - `COST_OK` if allowed, and `COST_TOO_EXPENSIVE` if rejected due to cost limits +- `apollo::demand_control::estimated_cost_by_subgraph` - Map of estimated cost of the requests to be sent to the subgraphs, by subgraph +- `apollo::demand_control::actual_cost_by_subgraph` - Map of actual calculated cost of the responses returned by the subgraphs, by subgraph +- `apollo::demand_control::result_by_subgraph` - Map of result by subgraph, `COST_OK` if allowed, and `COST_TOO_EXPENSIVE` if rejected due to cost limits - `apollo::demand_control::strategy` - Name of the cost calculation strategy used ### Caching diff --git a/docs/source/routing/customization/rhai/reference.mdx b/docs/source/routing/customization/rhai/reference.mdx index 36aa4e40b5..19a71d2039 100644 --- a/docs/source/routing/customization/rhai/reference.mdx +++ b/docs/source/routing/customization/rhai/reference.mdx @@ -401,6 +401,9 @@ Router.APOLLO_COST_ESTIMATED_KEY // Context key to get the estimated cost of an Router.APOLLO_COST_ACTUAL_KEY // Context key to get the actual cost of an operation Router.APOLLO_COST_STRATEGY_KEY // Context key to get the strategy used to calculate cost Router.APOLLO_COST_RESULT_KEY // Context key to get the cost result of an operation +Router.APOLLO_COST_BY_SUBGRAPH_ESTIMATED_KEY // Context key to get the estimated cost of an operation against each subgraph +Router.APOLLO_COST_BY_SUBGRAPH_ACTUAL_KEY // Context key to get the actual cost of an operation against each subgraph +Router.APOLLO_COST_BY_SUBGRAPH_RESULT_KEY // Context key to get the cost result of an operation against each subgraph ``` ## `Request` interface diff --git a/docs/source/routing/errors.mdx b/docs/source/routing/errors.mdx index 1da326d2ee..9c71facd24 100644 --- a/docs/source/routing/errors.mdx +++ b/docs/source/routing/errors.mdx @@ -124,6 +124,11 @@ There was an error fetching data from a connector service. The estimated cost of the query was greater than the configured maximum cost. + + + +The estimated cost of the query directed at a specific subgraph exceeds the configured maximum cost. + diff --git a/docs/source/routing/security/demand-control.mdx b/docs/source/routing/security/demand-control.mdx index 9e87410833..6d08ade292 100644 --- a/docs/source/routing/security/demand-control.mdx +++ b/docs/source/routing/security/demand-control.mdx @@ -125,9 +125,9 @@ The difference between estimated and actual operation cost calculations is due o -There are two ways to indicate the expected list sizes to the router: +There are three ways to indicate the expected list sizes to the router: * Set the global maximum in your router configuration file (see [Configuring demand control](#configuring-demand-control)). - +* Set a subgraph-specific list size in your router configuration file (see [Configuring demand control](#configuring-demand-control); requires Router v2.12.0 and later) * Use the Apollo Federation [@listSize directive](/federation/federated-schemas/federated-directives/#listsize). The `@listSize` directive supports field-level granularity in setting list size. By using its `assumedSize` argument, you can set a statically defined list size for a field. If you are using paging parameters which control the size of the list, use the `slicingArguments` argument. @@ -207,9 +207,27 @@ When requesting 3 books: 1 Query (0) + 3 book objects (3 * (1 book object (1) + 1 author object (1) + 1 publisher object (1) + 1 address object (5))) = 24 total cost When requesting 7 books: -1 Query (0) + 3 book objects (7 * (1 book object (1) + 1 author object (1) + 1 publisher object (1) + 1 address object (5))) = 56 total cost +1 Query (0) + 7 book objects (7 * (1 book object (1) + 1 author object (1) + 1 publisher object (1) + 1 address object (5))) = 56 total cost ``` +## Subgraph-level demand control + +Requires Router v2.12.0 or later. + +Subgraph-level demand control lets you enforce per-subgraph query cost limits in Apollo Router, in addition to the +existing global cost limit for the whole supergraph. This helps you protect specific backend services that have different +capacity or cost profiles from being overwhelmed by expensive operations. + +When a subgraph‑specific cost limit is exceeded, the router: +- Still runs the rest of the operation, including other subgraphs whose cost is within limits. +- Skips calls to only the over‑budget subgraph, and composes the response as if that subgraph had returned null, instead + of rejecting the entire query. + +Per‑subgraph limits apply to the total work for that subgraph in a single operation. For each request, the router tracks +the aggregate estimated cost per subgraph across the entire query plan. If the same subgraph is fetched multiple times +(for example, through entity lookups, nested fetches, or conditional branches), those costs are summed together and the +subgraph’s limit is enforced against that total. + ## Configuring demand control To enable demand control in the router, configure the `demand_control` option in `router.yaml`: @@ -222,20 +240,34 @@ demand_control: static_estimated: list_size: 10 max: 1000 + subgraph: + all: + list_size: 10 + max: 800 # any subgraph can receive operations totaling at most 800 + subgraphs: + products: + list_size: 20 # overrides list_size = 10 from `subgraph.all` + users: + max: 200 # overrides max = 800 from `subgraph.all` ``` When `demand_control` is enabled, the router measures the cost of each operation and can enforce operation cost limits, based on additional configuration. Customize `demand_control` with the following settings: -| Option | Valid values | Default value | Description | -| ----------------------------------- | ---------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| `enabled` | boolean | `false` | Set to `true` to measure operation costs or enforce operation cost limits. | -| `mode` | `measure`, `enforce` | -- | - `measure` collects information about the cost of operations.
- `enforce` rejects operations exceeding configured cost limits. | -| `strategy` | `static_estimated` | -- | `static_estimated` estimates the cost of an operation before it is sent to a subgraph | -| `static_estimated.actual_cost_mode` | `by_subgraph`, `by_response_shape` | `by_subgraph` | - `by_subgraph` calculates the cost of an operation as the sum of the cost of each subgraph response.
- `by_response_shape` calculates the cost based on only the final shape of the response. | -| `static_estimated.list_size` | integer | -- | The assumed maximum size of a list for fields that return lists. | -| `static_estimated.max` | integer | -- | The maximum cost of an accepted operation. An operation with a higher cost than this is rejected. | +| Option | Valid values | Default value | Description | +| ------------------------------------------------- | ---------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | boolean | `false` | Set to `true` to measure operation costs or enforce operation cost limits. | +| `mode` | `measure`, `enforce` | -- | - `measure` collects information about the cost of operations.
- `enforce` rejects operations exceeding configured cost limits. | +| `strategy` | `static_estimated` | -- | `static_estimated` estimates the cost of an operation before it is sent to a subgraph | +| `static_estimated.actual_cost_mode` | `by_subgraph`, `by_response_shape` | `by_subgraph` | - `by_subgraph` calculates the cost of an operation as the sum of the cost of each subgraph response.
- `by_response_shape` calculates the cost based on only the final shape of the response. | +| `static_estimated.list_size` | integer | -- | The assumed maximum size of a list for fields that return lists. | +| `static_estimated.max` | integer | -- | The maximum cost of an accepted operation. An operation with a higher cost than this is rejected. | +| `static_estimated.subgraph` | integer (optional) | -- | Subgraph-level demand control (requires router >v2.12.0). | +| `static_estimated.subgraph.all.list_size` | integer (optional) | -- | The assumed maximum size of a list for fields that return lists. | +| `static_estimated.subgraph.all.max` | float (optional) | -- | The maximum cost accepted by a subgraph. | +| `static_estimated.subgraph.subgraphs.*.list_size` | integer (optional) | -- | The assumed maximum size of a list for fields that return lists, for this subgraph. | +| `static_estimated.subgraph.subgraphs.*.max` | float (optional) | -- | The maximum cost accepted by this subgraph. | When enabling `demand_control` for the first time, set it to `measure` mode. This will allow you to observe the cost of your operations before setting your maximum cost.