diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index f13ec59757..555d1a4339 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -29,6 +29,18 @@ impl From for String { } } +#[derive(Clone, Debug, strum_macros::Display, PartialEq, Eq)] +pub enum UnsupportedFeatureKind { + #[strum(to_string = "progressive overrides")] + ProgressiveOverrides, + #[strum(to_string = "defer")] + Defer, + #[strum(to_string = "context")] + Context, + #[strum(to_string = "alias")] + Alias, +} + #[derive(Debug, Clone, thiserror::Error)] pub enum SingleFederationError { #[error( @@ -185,7 +197,10 @@ pub enum SingleFederationError { #[error("{message}")] OverrideOnInterface { message: String }, #[error("{message}")] - UnsupportedFeature { message: String }, + UnsupportedFeature { + message: String, + kind: UnsupportedFeatureKind, + }, #[error("{message}")] InvalidFederationSupergraph { message: String }, #[error("{message}")] diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index 84f392340e..5670c685d9 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; +use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; @@ -46,6 +47,10 @@ use crate::utils::logging::snapshot; use crate::ApiSchemaOptions; use crate::Supergraph; +pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel"; +pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; +pub(crate) const JOIN_FIELD: &str = "join__field"; + #[derive(Debug, Clone, Hash)] pub struct QueryPlannerConfig { /// Whether the query planner should try to reused the named fragments of the planned query in @@ -208,6 +213,7 @@ impl QueryPlanner { config: QueryPlannerConfig, ) -> Result { config.assert_valid(); + Self::check_unsupported_features(supergraph)?; let supergraph_schema = supergraph.schema.clone(); let api_schema = supergraph.to_api_schema(ApiSchemaOptions { @@ -533,6 +539,89 @@ impl QueryPlanner { pub fn api_schema(&self) -> &ValidFederationSchema { &self.api_schema } + + fn check_unsupported_features(supergraph: &Supergraph) -> Result<(), FederationError> { + // We have a *progressive* override when `join__field` has a + // non-null value for `overrideLabel` field. + // + // This looks at object types' fields and their directive + // applications, looking specifically for `@join__field` + // arguments list. + let has_progressive_overrides = supergraph + .schema + .schema() + .types + .values() + .filter_map(|extended_type| { + // The override label args can be only on ObjectTypes + if let ExtendedType::Object(object_type) = extended_type { + Some(object_type) + } else { + None + } + }) + .flat_map(|object_type| &object_type.fields) + .flat_map(|(_, field)| { + field + .directives + .iter() + .filter(|d| d.name.as_str() == JOIN_FIELD) + }) + .any(|join_directive| { + if let Some(override_label_arg) = + join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) + { + // Any argument value for `overrideLabel` that's not + // null can be considered as progressive override usage + if !override_label_arg.is_null() { + return true; + } + return false; + } + false + }); + if has_progressive_overrides { + let message = "\ + `experimental_query_planner_mode: new` or `both` cannot yet \ + be used with progressive overrides. \ + Remove uses of progressive overrides to try the experimental query planner, \ + otherwise switch back to `legacy` or `both_best_effort`.\ + "; + return Err(SingleFederationError::UnsupportedFeature { + message: message.to_owned(), + kind: crate::error::UnsupportedFeatureKind::ProgressiveOverrides, + } + .into()); + } + + // We will only check for `@context` direcive, since + // `@fromContext` can only be used if `@context` is already + // applied, and we assume a correctly composed supergraph. + // + // `@context` can only be applied on Object Types, Interface + // Types and Unions. For simplicity of this function, we just + // check all 'extended_type` directives. + let has_set_context = supergraph + .schema + .schema() + .types + .values() + .any(|extended_type| extended_type.directives().has(CONTEXT_DIRECTIVE)); + if has_set_context { + let message = "\ + `experimental_query_planner_mode: new` or `both` cannot yet \ + be used with `@context`. \ + Remove uses of `@context` to try the experimental query planner, \ + otherwise switch back to `legacy` or `both_best_effort`.\ + "; + return Err(SingleFederationError::UnsupportedFeature { + message: message.to_owned(), + kind: crate::error::UnsupportedFeatureKind::Context, + } + .into()); + } + Ok(()) + } } fn compute_root_serial_dependency_graph( @@ -767,8 +856,9 @@ fn compute_plan_for_defer_conditionals( _parameters: &mut QueryPlanningParameters, _defer_conditions: IndexMap>, ) -> Result, FederationError> { - Err(SingleFederationError::Internal { + Err(SingleFederationError::UnsupportedFeature { message: String::from("@defer is currently not supported"), + kind: crate::error::UnsupportedFeatureKind::Defer, } .into()) } diff --git a/apollo-federation/src/schema/field_set.rs b/apollo-federation/src/schema/field_set.rs index 6aee222a35..442162efd9 100644 --- a/apollo-federation/src/schema/field_set.rs +++ b/apollo-federation/src/schema/field_set.rs @@ -41,7 +41,8 @@ fn check_absence_of_aliases(selection_set: &SelectionSet) -> Result<(), Federati errors.push(SingleFederationError::UnsupportedFeature { // PORT_NOTE: The JS version also quotes the directive name in the error message. // For example, "aliases are not currently supported in @requires". - message: format!(r#"Cannot use alias "{alias}" in "{}": aliases are not currently supported in the used directive"#, field.field) + message: format!(r#"Cannot use alias "{alias}" in "{}": aliases are not currently supported in the used directive"#, field.field), + kind: crate::error::UnsupportedFeatureKind::Alias }.into()); } if let Some(selection_set) = &field.selection_set { diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index 2ebf66bd4d..80b075a915 100644 --- a/apollo-router/src/error.rs +++ b/apollo-router/src/error.rs @@ -233,8 +233,8 @@ pub(crate) enum ServiceBuildError { /// couldn't build Query Planner Service: {0} QueryPlannerError(QueryPlannerError), - /// The supergraph schema failed to produce a valid API schema: {0} - ApiSchemaError(FederationError), + /// failed to initialize the query planner: {0} + QpInitError(FederationError), /// schema error: {0} Schema(SchemaError), @@ -249,12 +249,6 @@ impl From for ServiceBuildError { } } -impl From for ServiceBuildError { - fn from(err: FederationError) -> Self { - ServiceBuildError::ApiSchemaError(err) - } -} - impl From> for ServiceBuildError { fn from(errors: Vec) -> Self { ServiceBuildError::QueryPlannerError(errors.into()) diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index cb6335afb0..fc4ccce41d 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -10,6 +10,7 @@ use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_federation::error::FederationError; +use apollo_federation::error::SingleFederationError; use apollo_federation::query_plan::query_planner::QueryPlanner; use futures::future::BoxFuture; use opentelemetry_api::metrics::MeterProvider as _; @@ -64,6 +65,10 @@ use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; const JS_QP_MODE: &str = "js"; +const UNSUPPORTED_CONTEXT: &str = "context"; +const UNSUPPORTED_OVERRIDES: &str = "overrides"; +const UNSUPPORTED_FED1: &str = "fed1"; +const INTERNAL_INIT_ERROR: &str = "internal"; #[derive(Clone)] /// A query planner that calls out to the nodejs router-bridge query planner. @@ -162,10 +167,7 @@ impl PlannerMode { QueryPlannerMode::BothBestEffort => match Self::rust(schema, configuration) { Ok(planner) => Ok(Some(planner)), Err(error) => { - tracing::warn!( - "Failed to initialize the new query planner, \ - falling back to legacy: {error}" - ); + tracing::info!("Falling back to the legacy query planner: {error}"); Ok(None) } }, @@ -189,10 +191,34 @@ impl PlannerMode { }, debug: Default::default(), }; - Ok(Arc::new(QueryPlanner::new( - schema.federation_supergraph(), - config, - )?)) + let result = QueryPlanner::new(schema.federation_supergraph(), config); + + match &result { + Err(FederationError::SingleFederationError { + inner: error, + trace: _, + }) => match error { + SingleFederationError::UnsupportedFederationVersion { .. } => { + metric_rust_qp_init(Some(UNSUPPORTED_FED1)); + } + SingleFederationError::UnsupportedFeature { message: _, kind } => match kind { + apollo_federation::error::UnsupportedFeatureKind::ProgressiveOverrides => { + metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)) + } + apollo_federation::error::UnsupportedFeatureKind::Context => { + metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)) + } + _ => metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)), + }, + _ => { + metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)); + } + }, + Err(_) => metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)), + Ok(_) => metric_rust_qp_init(None), + } + + Ok(Arc::new(result.map_err(ServiceBuildError::QpInitError)?)) } async fn js( @@ -975,6 +1001,25 @@ pub(crate) fn metric_query_planning_plan_duration(planner: &'static str, start: ); } +pub(crate) fn metric_rust_qp_init(init_error_kind: Option<&'static str>) { + if let Some(init_error_kind) = init_error_kind { + u64_counter!( + "apollo.router.lifecycle.query_planner.init", + "Rust query planner initialization", + 1, + "init.error_kind" = init_error_kind, + "init.is_success" = false + ); + } else { + u64_counter!( + "apollo.router.lifecycle.query_planner.init", + "Rust query planner initialization", + 1, + "init.is_success" = true + ); + } +} + #[cfg(test)] mod tests { use std::fs; @@ -1617,4 +1662,42 @@ mod tests { "planner" = "js" ); } + + #[test] + fn test_metric_rust_qp_initialization() { + metric_rust_qp_init(None); + assert_counter!( + "apollo.router.lifecycle.query_planner.init", + 1, + "init.is_success" = true + ); + metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)); + assert_counter!( + "apollo.router.lifecycle.query_planner.init", + 1, + "init.error_kind" = "context", + "init.is_success" = false + ); + metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)); + assert_counter!( + "apollo.router.lifecycle.query_planner.init", + 1, + "init.error_kind" = "overrides", + "init.is_success" = false + ); + metric_rust_qp_init(Some(UNSUPPORTED_FED1)); + assert_counter!( + "apollo.router.lifecycle.query_planner.init", + 1, + "init.error_kind" = "fed1", + "init.is_success" = false + ); + metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)); + assert_counter!( + "apollo.router.lifecycle.query_planner.init", + 1, + "init.error_kind" = "internal", + "init.is_success" = false + ); + } } diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index fa2d8aab66..e110726712 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::io; use std::sync::Arc; -use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use axum::response::IntoResponse; use http::StatusCode; @@ -52,9 +51,6 @@ use crate::spec::Schema; use crate::ListenAddr; pub(crate) const STARTING_SPAN_NAME: &str = "starting"; -pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel"; -pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; -pub(crate) const JOIN_FIELD: &str = "join__field"; #[derive(Clone)] /// A path and a handler to be exposed as a web_endpoint for plugins @@ -238,13 +234,6 @@ impl YamlRouterFactory { ) .await?; - // Don't let the router start in experimental_query_planner_mode and - // unimplemented Rust QP features. - can_use_with_experimental_query_planner( - configuration.clone(), - supergraph_creator.schema(), - )?; - // Instantiate the parser here so we can use it to warm up the planner below let query_analysis_layer = QueryAnalysisLayer::new(supergraph_creator.schema(), Arc::clone(&configuration)).await; @@ -770,108 +759,6 @@ fn inject_schema_id(schema_id: Option<&str>, configuration: &mut Value) { } } -// The Rust QP has not yet implemented setContext -// (`@context` directives), progressive overrides, and it -// doesn't support fed v1 *supergraphs*. -// -// If users are using the Rust QP as standalone (`new`) or in comparison mode (`both`), -// fail to start up the router emitting an error. -fn can_use_with_experimental_query_planner( - configuration: Arc, - schema: Arc, -) -> Result<(), ConfigurationError> { - match configuration.experimental_query_planner_mode { - crate::configuration::QueryPlannerMode::New - | crate::configuration::QueryPlannerMode::Both => { - // We have a *progressive* override when `join__directive` has a - // non-null value for `overrideLabel` field. - // - // This looks at object types' fields and their directive - // applications, looking specifically for `@join__direcitve` - // arguments list. - let has_progressive_overrides = schema - .supergraph_schema() - .types - .values() - .filter_map(|extended_type| { - // The override label args can be only on ObjectTypes - if let ExtendedType::Object(object_type) = extended_type { - Some(object_type) - } else { - None - } - }) - .flat_map(|object_type| &object_type.fields) - .filter_map(|(_, field)| { - let join_field_directives = field - .directives - .iter() - .filter(|d| d.name.as_str() == JOIN_FIELD) - .collect::>(); - if !join_field_directives.is_empty() { - Some(join_field_directives) - } else { - None - } - }) - .flatten() - .any(|join_directive| { - if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) - { - // Any argument value for `overrideLabel` that's not - // null can be considered as progressive override usage - if !override_label_arg.is_null() { - return true; - } - return false; - } - false - }); - if has_progressive_overrides { - return Err(ConfigurationError::InvalidConfiguration { - message: "`experimental_query_planner_mode` cannot be used with progressive overrides", - error: "remove uses of progressive overrides to try the experimental_query_planner_mode in `both` or `new`, otherwise switch back to `legacy`.".to_string(), - }); - } - - // We will only check for `@context` direcive, since - // `@fromContext` can only be used if `@context` is already - // applied, and we assume a correctly composed supergraph. - // - // `@context` can only be applied on Object Types, Interface - // Types and Unions. For simplicity of this function, we just - // check all 'extended_type` directives. - let has_set_context = schema - .supergraph_schema() - .types - .values() - .any(|extended_type| extended_type.directives().has(CONTEXT_DIRECTIVE)); - if has_set_context { - return Err(ConfigurationError::InvalidConfiguration { - message: "`experimental_query_planner_mode` cannot be used with `@context`", - error: "remove uses of `@context` to try the experimental_query_planner_mode in `both` or `new`, otherwise switch back to `legacy`.".to_string(), - }); - } - - // Fed1 supergraphs will not work with the rust query planner. - let is_fed1_supergraph = match schema.federation_version() { - Some(v) => v == 1, - None => false, - }; - if is_fed1_supergraph { - return Err(ConfigurationError::InvalidConfiguration { - message: "`experimental_query_planner_mode` cannot be used with fed1 supergraph", - error: "switch back to `experimental_query_planner_mode: legacy` to use the router with fed1 supergraph".to_string(), - }); - } - - Ok(()) - } - crate::configuration::QueryPlannerMode::Legacy - | crate::configuration::QueryPlannerMode::BothBestEffort => Ok(()), - } -} #[cfg(test)] mod test { use std::sync::Arc; @@ -882,11 +769,9 @@ mod test { use tower_http::BoxError; use crate::configuration::Configuration; - use crate::configuration::QueryPlannerMode; use crate::plugin::Plugin; use crate::plugin::PluginInit; use crate::register_plugin; - use crate::router_factory::can_use_with_experimental_query_planner; use crate::router_factory::inject_schema_id; use crate::router_factory::RouterSuperServiceFactory; use crate::router_factory::YamlRouterFactory; @@ -1019,125 +904,4 @@ mod test { "8e2021d131b23684671c3b85f82dfca836908c6a541bbd5c3772c66e7f8429d8" ); } - - #[test] - fn test_cannot_use_context_with_experimental_query_planner() { - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Both, - ..Default::default() - }; - let schema = include_str!("testdata/supergraph_with_context.graphql"); - let schema = Arc::new(Schema::parse(schema, &config).unwrap()); - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_err(), - "experimental_query_planner_mode: both cannot be used with @context" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::New, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_err(), - "experimental_query_planner_mode: new cannot be used with @context" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Legacy, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_ok(), - "experimental_query_planner_mode: legacy should be able to be used with @context" - ); - } - - #[test] - fn test_cannot_use_progressive_overrides_with_experimental_query_planner() { - // PROGRESSIVE OVERRIDES - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Both, - ..Default::default() - }; - let schema = include_str!("testdata/supergraph_with_override_label.graphql"); - let schema = Arc::new(Schema::parse(schema, &config).unwrap()); - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_err(), - "experimental_query_planner_mode: both cannot be used with progressive overrides" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::New, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_err(), - "experimental_query_planner_mode: new cannot be used with progressive overrides" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Legacy, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_ok(), - "experimental_query_planner_mode: legacy should be able to be used with progressive overrides" - ); - } - - #[test] - fn test_cannot_use_fed1_supergraphs_with_experimental_query_planner() { - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Both, - ..Default::default() - }; - let schema = include_str!("testdata/supergraph.graphql"); - let schema = Arc::new(Schema::parse(schema, &config).unwrap()); - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_err(), - "experimental_query_planner_mode: both cannot be used with fed1 supergraph" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::New, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_err(), - "experimental_query_planner_mode: new cannot be used with fed1 supergraph" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Legacy, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_ok(), - "experimental_query_planner_mode: legacy should be able to be used with fed1 supergraph" - ); - } - - #[test] - fn test_can_use_fed2_supergraphs_with_experimental_query_planner() { - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Both, - ..Default::default() - }; - let schema = include_str!("testdata/minimal_fed2_supergraph.graphql"); - let schema = Arc::new(Schema::parse(schema, &config).unwrap()); - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_ok(), - "experimental_query_planner_mode: both can be used" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::New, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_ok(), - "experimental_query_planner_mode: new can be used" - ); - let config = Configuration { - experimental_query_planner_mode: QueryPlannerMode::Legacy, - ..Default::default() - }; - assert!( - can_use_with_experimental_query_planner(Arc::new(config), schema.clone()).is_ok(), - "experimental_query_planner_mode: legacy can be used" - ); - } } diff --git a/apollo-router/tests/fixtures/broken-supergraph.graphql b/apollo-router/tests/fixtures/broken-supergraph.graphql new file mode 100644 index 0000000000..eafc474b2b --- /dev/null +++ b/apollo-router/tests/fixtures/broken-supergraph.graphql @@ -0,0 +1,127 @@ +schema + # this is missing a link directive spec definition + # @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph! + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +scalar join__FieldSet + +scalar federation__Scope + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY + @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +} +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + price: Int @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + upc: String! + @join__field(graph: PRODUCTS) + @join__field(graph: INVENTORY, external: true) + @join__field(graph: REVIEWS, external: true) + weight: Int @join__field(graph: PRODUCTS) +} + +type Query @join__type(graph: ACCOUNTS) @join__type(graph: PRODUCTS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__owner(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") { + author: User @join__field(graph: REVIEWS) + body: String @join__field(graph: REVIEWS) + id: ID! + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + + reviews: [Review] @join__field(graph: REVIEWS) + username: String @join__field(graph: ACCOUNTS) +} diff --git a/apollo-router/tests/fixtures/valid-supergraph.graphql b/apollo-router/tests/fixtures/valid-supergraph.graphql new file mode 100644 index 0000000000..fe43cc6964 --- /dev/null +++ b/apollo-router/tests/fixtures/valid-supergraph.graphql @@ -0,0 +1,126 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph! + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +scalar join__FieldSet + +scalar federation__Scope + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY + @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +} +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + price: Int @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + upc: String! + @join__field(graph: PRODUCTS) + @join__field(graph: INVENTORY, external: true) + @join__field(graph: REVIEWS, external: true) + weight: Int @join__field(graph: PRODUCTS) +} + +type Query @join__type(graph: ACCOUNTS) @join__type(graph: PRODUCTS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__owner(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") { + author: User @join__field(graph: REVIEWS) + body: String @join__field(graph: REVIEWS) + id: ID! + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + + reviews: [Review] @join__field(graph: REVIEWS) + username: String @join__field(graph: ACCOUNTS) +} diff --git a/apollo-router/tests/integration/lifecycle.rs b/apollo-router/tests/integration/lifecycle.rs index 2f7feea952..71af2dbcf8 100644 --- a/apollo-router/tests/integration/lifecycle.rs +++ b/apollo-router/tests/integration/lifecycle.rs @@ -460,73 +460,3 @@ fn test_plugin_ordering_push_trace(context: &Context, entry: String) { ) .unwrap(); } - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_legacy_qp() { - let mut router = IntegrationTest::builder() - .config("experimental_query_planner_mode: legacy") - .supergraph("../examples/graphql/local.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_new_qp() { - let mut router = IntegrationTest::builder() - .config("experimental_query_planner_mode: new") - .supergraph("../examples/graphql/local.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - The supergraph schema failed to produce a valid API schema: \ - Supergraphs composed with federation version 1 are not supported.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_both_qp() { - let mut router = IntegrationTest::builder() - .config("experimental_query_planner_mode: both") - .supergraph("../examples/graphql/local.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - The supergraph schema failed to produce a valid API schema: \ - Supergraphs composed with federation version 1 are not supported.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_both_best_effort_qp() { - let mut router = IntegrationTest::builder() - .config("experimental_query_planner_mode: both_best_effort") - .supergraph("../examples/graphql/local.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "Failed to initialize the new query planner, falling back to legacy: \ - The supergraph schema failed to produce a valid API schema: \ - Supergraphs composed with federation version 1 are not supported. \ - Please recompose your supergraph with federation version 2 or greater", - ) - .await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} diff --git a/apollo-router/tests/integration/mod.rs b/apollo-router/tests/integration/mod.rs index 7ab2f50d95..f4c840d9e4 100644 --- a/apollo-router/tests/integration/mod.rs +++ b/apollo-router/tests/integration/mod.rs @@ -8,6 +8,7 @@ mod docs; mod file_upload; mod lifecycle; mod operation_limits; +mod query_planner; mod subgraph_response; mod traffic_shaping; diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs new file mode 100644 index 0000000000..9c85c99690 --- /dev/null +++ b/apollo-router/tests/integration/query_planner.rs @@ -0,0 +1,466 @@ +use std::path::PathBuf; + +use crate::integration::common::graph_os_enabled; +use crate::integration::IntegrationTest; + +const PROMETHEUS_METRICS_CONFIG: &str = include_str!("telemetry/fixtures/prometheus.router.yaml"); +const LEGACY_QP: &str = "experimental_query_planner_mode: legacy"; +const NEW_QP: &str = "experimental_query_planner_mode: new"; +const BOTH_QP: &str = "experimental_query_planner_mode: both"; +const BOTH_BEST_EFFORT_QP: &str = "experimental_query_planner_mode: both_best_effort"; + +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_legacy_qp() { + let mut router = IntegrationTest::builder() + .config(LEGACY_QP) + .supergraph("../examples/graphql/local.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_new_qp() { + let mut router = IntegrationTest::builder() + .config(NEW_QP) + .supergraph("../examples/graphql/local.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + failed to initialize the query planner: \ + Supergraphs composed with federation version 1 are not supported.", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_both_qp() { + let mut router = IntegrationTest::builder() + .config(BOTH_QP) + .supergraph("../examples/graphql/local.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + failed to initialize the query planner: \ + Supergraphs composed with federation version 1 are not supported.", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_both_best_effort_qp() { + let mut router = IntegrationTest::builder() + .config(BOTH_BEST_EFFORT_QP) + .supergraph("../examples/graphql/local.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "Falling back to the legacy query planner: \ + failed to initialize the query planner: \ + Supergraphs composed with federation version 1 are not supported. \ + Please recompose your supergraph with federation version 2 or greater", + ) + .await; + router.assert_started().await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_legacy_qp_reload_to_new_keep_previous_config() { + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("../examples/graphql/local.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); + router.update_config(&config).await; + router + .assert_log_contains("error while reloading, continuing with previous configuration") + .await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="fed1",init_is_success="false",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("../examples/graphql/local.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); + router.update_config(&config).await; + router + .assert_log_contains( + "Falling back to the legacy query planner: \ + failed to initialize the query planner: \ + Supergraphs composed with federation version 1 are not supported. \ + Please recompose your supergraph with federation version 2 or greater", + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="fed1",init_is_success="false",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn fed2_schema_with_new_qp() { + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("../examples/graphql/supergraph-fed2.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_is_success="true",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn progressive_override_with_legacy_qp() { + if !graph_os_enabled() { + return; + } + let mut router = IntegrationTest::builder() + .config(LEGACY_QP) + .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn progressive_override_with_new_qp() { + if !graph_os_enabled() { + return; + } + let mut router = IntegrationTest::builder() + .config(NEW_QP) + .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + failed to initialize the query planner: \ + `experimental_query_planner_mode: new` or `both` cannot yet \ + be used with progressive overrides. \ + Remove uses of progressive overrides to try the experimental query planner, \ + otherwise switch back to `legacy` or `both_best_effort`.", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn progressive_override_with_legacy_qp_change_to_new_qp_keeps_old_config() { + if !graph_os_enabled() { + return; + } + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); + router.update_config(&config).await; + router + .assert_log_contains("error while reloading, continuing with previous configuration") + .await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn progressive_override_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { + if !graph_os_enabled() { + return; + } + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); + router.update_config(&config).await; + router + .assert_log_contains( + "Falling back to the legacy query planner: \ + failed to initialize the query planner: \ + `experimental_query_planner_mode: new` or `both` cannot yet \ + be used with progressive overrides. \ + Remove uses of progressive overrides to try the experimental query planner, \ + otherwise switch back to `legacy` or `both_best_effort`.", + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn context_with_legacy_qp() { + if !graph_os_enabled() { + return; + } + let mut router = IntegrationTest::builder() + .config(PROMETHEUS_METRICS_CONFIG) + .supergraph("tests/fixtures/set_context/supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn context_with_new_qp() { + if !graph_os_enabled() { + return; + } + let mut router = IntegrationTest::builder() + .config(NEW_QP) + .supergraph("tests/fixtures/set_context/supergraph.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + failed to initialize the query planner: \ + `experimental_query_planner_mode: new` or `both` cannot yet \ + be used with `@context`. \ + Remove uses of `@context` to try the experimental query planner, \ + otherwise switch back to `legacy` or `both_best_effort`.", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn context_with_legacy_qp_change_to_new_qp_keeps_old_config() { + if !graph_os_enabled() { + return; + } + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("tests/fixtures/set_context/supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); + router.update_config(&config).await; + router + .assert_log_contains("error while reloading, continuing with previous configuration") + .await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="context",init_is_success="false",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn context_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { + if !graph_os_enabled() { + return; + } + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("tests/fixtures/set_context/supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); + router.update_config(&config).await; + router + .assert_log_contains( + "Falling back to the legacy query planner: \ + failed to initialize the query planner: \ + `experimental_query_planner_mode: new` or `both` cannot yet \ + be used with `@context`. \ + Remove uses of `@context` to try the experimental query planner, \ + otherwise switch back to `legacy` or `both_best_effort`.", + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="context",init_is_success="false",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn invalid_schema_with_legacy_qp_fails_startup() { + let mut router = IntegrationTest::builder() + .config(LEGACY_QP) + .supergraph("tests/fixtures/broken-supergraph.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + Federation error: Invalid supergraph: must be a core schema", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn invalid_schema_with_new_qp_fails_startup() { + let mut router = IntegrationTest::builder() + .config(NEW_QP) + .supergraph("tests/fixtures/broken-supergraph.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + Federation error: Invalid supergraph: must be a core schema", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn invalid_schema_with_both_qp_fails_startup() { + let mut router = IntegrationTest::builder() + .config(BOTH_QP) + .supergraph("tests/fixtures/broken-supergraph.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + Federation error: Invalid supergraph: must be a core schema", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn invalid_schema_with_both_best_effort_qp_fails_startup() { + let mut router = IntegrationTest::builder() + .config(BOTH_BEST_EFFORT_QP) + .supergraph("tests/fixtures/broken-supergraph.graphql") + .build() + .await; + router.start().await; + router + .assert_log_contains( + "could not create router: \ + Federation error: Invalid supergraph: must be a core schema", + ) + .await; + router.assert_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn valid_schema_with_new_qp_change_to_broken_schema_keeps_old_config() { + let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); + let mut router = IntegrationTest::builder() + .config(config) + .supergraph("tests/fixtures/valid-supergraph.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + router + .assert_metrics_contains( + r#"apollo_router_lifecycle_query_planner_init_total{init_is_success="true",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router.execute_default_query().await; + router + .update_schema(&PathBuf::from("tests/fixtures/broken-supergraph.graphql")) + .await; + router + .assert_log_contains("error while reloading, continuing with previous configuration") + .await; + router.execute_default_query().await; + router.graceful_shutdown().await; +}