diff --git a/.changesets/feat_geal_v8_heap_statistics.md b/.changesets/feat_geal_v8_heap_statistics.md new file mode 100644 index 0000000000..c091b108a8 --- /dev/null +++ b/.changesets/feat_geal_v8_heap_statistics.md @@ -0,0 +1,7 @@ +### Add V8 heap usage metrics ([PR #5781](https://github.com/apollographql/router/pull/5781)) + +The router supports new gauge metrics for tracking heap memory usage of the V8 Javascript engine: +- `apollo.router.v8.heap.used`: heap memory used by V8, in bytes +- `apollo.router.v8.heap.total`: total heap allocated by V8, in bytes + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5781 \ No newline at end of file diff --git a/.changesets/feat_tninesling_cost_directives.md b/.changesets/feat_tninesling_cost_directives.md new file mode 100644 index 0000000000..e7994edc84 --- /dev/null +++ b/.changesets/feat_tninesling_cost_directives.md @@ -0,0 +1,28 @@ +### Account for demand control directives when scoring operations ([PR #5777](https://github.com/apollographql/router/pull/5777)) + +When scoring operations in the demand control plugin, utilize applications of `@cost` and `@listSize` from the supergraph schema to make better cost estimates. + +For expensive resolvers, the `@cost` directive can override the default weights in the cost calculation. + +```graphql +type Product { + id: ID! + name: String + expensiveField: Int @cost(weight: 20) +} +``` + +Additionally, if a list field's length differs significantly from the globally-configured list size, the `@listSize` directive can provide a tighter size estimate. + +```graphql +type Magazine { + # This is assumed to always return 5 items + headlines: [Article] @listSize(assumedSize: 5) + + # This is estimated to return as many items as are requested by the parameter named "first" + getPage(first: Int!, after: ID!): [Article] + @listSize(slicingArguments: ["first"]) +} +``` + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5777 diff --git a/.changesets/feat_update_federation.md b/.changesets/feat_update_federation.md new file mode 100644 index 0000000000..b3c0670daa --- /dev/null +++ b/.changesets/feat_update_federation.md @@ -0,0 +1,8 @@ +### Update federation to 2.8.3 ([PR #5781](https://github.com/apollographql/router/pull/5781)) + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +This updates the router from federation version 2.8.1 to 2.8.3, with a [fix for fragment generation](https://github.com/apollographql/federation/pull/3043). + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5781 diff --git a/.changesets/fix_geal_subgraph_error_path.md b/.changesets/fix_geal_subgraph_error_path.md new file mode 100644 index 0000000000..21c32032eb --- /dev/null +++ b/.changesets/fix_geal_subgraph_error_path.md @@ -0,0 +1,5 @@ +### set the subgraph error path if not present ([PR #5773](https://github.com/apollographql/router/pull/5773)) + +This fixes subgraph response conversion to set the error path in all cases. For some network level errors, the subgraph service was not setting the path + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5773 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a501ca6fcc..173028082a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,242 +24,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.6.0", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.6.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash", - "base64 0.22.1", - "bitflags 2.6.0", - "brotli 6.0.0", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.71", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if 1.0.0", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.7", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash", - "bytes", - "bytestring", - "cfg-if 1.0.0", - "cookie 0.16.2", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.7", - "time", - "url", -] - -[[package]] -name = "actix-web-actors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420b001bb709d8510c3e2659dae046e54509ff9528018d09c78381e765a1f9fa" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.71", -] - -[[package]] -name = "actix_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.71", -] - [[package]] name = "add-timestamp-header" version = "0.1.0" @@ -499,7 +263,7 @@ dependencies = [ "clap", "console", "console-subscriber", - "cookie 0.18.1", + "cookie", "crossbeam-channel", "dashmap", "derivative", @@ -875,9 +639,9 @@ dependencies = [ [[package]] name = "async-graphql" -version = "5.0.10" +version = "6.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35ef8f9be23ee30fe1eb1cf175c689bc33517c6c6d0fd0669dade611e5ced7f" +checksum = "298a5d587d6e6fdb271bf56af2dc325a80eb291fd0fc979146584b9a05494a8c" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -891,7 +655,7 @@ dependencies = [ "futures-util", "handlebars 4.5.0", "http 0.2.12", - "indexmap 1.9.3", + "indexmap 2.2.6", "mime", "multer", "num-traits", @@ -907,28 +671,28 @@ dependencies = [ ] [[package]] -name = "async-graphql-actix-web" -version = "5.0.10" +name = "async-graphql-axum" +version = "6.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e3d335639e722213bdd120f77a66f531bde8bbcff1b19ab8e542f82aed7f48" +checksum = "01a1c20a2059bffbc95130715b23435a05168c518fba9709c81fa2a38eed990c" dependencies = [ - "actix", - "actix-http", - "actix-web", - "actix-web-actors", - "async-channel 1.9.0", "async-graphql", - "futures-channel", + "async-trait", + "axum", + "bytes", "futures-util", "serde_json", - "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", ] [[package]] name = "async-graphql-derive" -version = "5.0.10" +version = "6.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0f6ceed3640b4825424da70a5107e79d48d9b2bc6318dfc666b2fc4777f8c4" +checksum = "c7f329c7eb9b646a72f70c9c4b516c70867d356ec46cb00dcac8ad343fd006b0" dependencies = [ "Inflector", "async-graphql-parser", @@ -936,15 +700,16 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 1.0.109", + "strum 0.25.0", + "syn 2.0.71", "thiserror", ] [[package]] name = "async-graphql-parser" -version = "5.0.10" +version = "6.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc308cd3bc611ee86c9cf19182d2b5ee583da40761970e41207f088be3db18f" +checksum = "6139181845757fd6a73fbb8839f3d036d7150b798db0e9bb3c6e83cdd65bd53b" dependencies = [ "async-graphql-value", "pest", @@ -954,12 +719,12 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "5.0.10" +version = "6.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d461325bfb04058070712296601dfe5e5bd6cdff84780a0a8c569ffb15c87eb3" +checksum = "323a5143f5bdd2030f45e3f2e0c821c9b1d36e79cf382129c64299c50a7f3750" dependencies = [ "bytes", - "indexmap 1.9.3", + "indexmap 2.2.6", "serde", "serde_json", ] @@ -1130,17 +895,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "auth-git2" version = "0.5.4" @@ -1753,15 +1507,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytestring" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" -dependencies = [ - "bytes", -] - [[package]] name = "cache-control" version = "0.1.0" @@ -1912,7 +1657,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -2084,17 +1829,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "cookie" version = "0.18.1" @@ -2329,9 +2063,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -2339,27 +2073,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.14.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 1.0.109", + "strsim", + "syn 2.0.71", ] [[package]] name = "darling_macro" -version = "0.14.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 1.0.109", + "syn 2.0.71", ] [[package]] @@ -2807,19 +2541,6 @@ dependencies = [ "syn 2.0.71", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.10.2" @@ -2925,10 +2646,10 @@ dependencies = [ name = "everything-subgraph" version = "0.1.0" dependencies = [ - "actix-web", "async-graphql", - "async-graphql-actix-web", - "env_logger 0.9.3", + "async-graphql-axum", + "axum", + "env_logger", "futures", "lazy_static", "log", @@ -2936,6 +2657,7 @@ dependencies = [ "rand 0.8.5", "serde_json", "tokio", + "tower", ] [[package]] @@ -3617,15 +3339,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.9" @@ -3911,7 +3624,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -4220,12 +3932,6 @@ dependencies = [ "log", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy-regex" version = "2.5.0" @@ -4391,23 +4097,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.12" @@ -6137,9 +5826,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.5.27+v2.8.1" +version = "0.5.30+v2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "288fa40fc4e0a76fb911410e05d4525e8bf7558622bd02403f89f871c4d0785b" +checksum = "9b2b67ccfc13842df12e473cbb93fe306a8dc3d120cfa2be57e3537c71bf0e63" dependencies = [ "anyhow", "async-channel 1.9.0", @@ -6169,7 +5858,7 @@ dependencies = [ "apollo-router", "apollo-smith", "async-trait", - "env_logger 0.10.2", + "env_logger", "http 0.2.12", "libfuzzer-sys", "log", @@ -6912,12 +6601,6 @@ dependencies = [ "regex", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -7410,6 +7093,7 @@ checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "slab", diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index 7e707434e2..ab42f16151 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -248,11 +248,12 @@ fn cmd_plan( ) -> Result<(), FederationError> { let query = read_input(query_path); let supergraph = load_supergraph(schema_paths)?; - let query_doc = - ExecutableDocument::parse_and_validate(supergraph.schema.schema(), query, query_path)?; - let config = QueryPlannerConfig::from(planner); + let config = QueryPlannerConfig::from(planner); let planner = QueryPlanner::new(&supergraph, config)?; + + let query_doc = + ExecutableDocument::parse_and_validate(planner.api_schema().schema(), query, query_path)?; print!("{}", planner.build_query_plan(&query_doc, None)?); Ok(()) } 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/lib.rs b/apollo-federation/src/lib.rs index 92420d2c03..ed94b65f24 100644 --- a/apollo-federation/src/lib.rs +++ b/apollo-federation/src/lib.rs @@ -30,6 +30,7 @@ pub mod query_plan; pub mod schema; pub mod sources; pub mod subgraph; +pub(crate) mod supergraph; pub(crate) mod utils; use apollo_compiler::ast::NamedType; @@ -47,10 +48,10 @@ use crate::link::spec::Identity; use crate::link::spec_definition::SpecDefinitions; use crate::merge::merge_subgraphs; use crate::merge::MergeFailure; -pub use crate::query_graph::extract_subgraphs_from_supergraph::ValidFederationSubgraph; -pub use crate::query_graph::extract_subgraphs_from_supergraph::ValidFederationSubgraphs; use crate::schema::ValidFederationSchema; use crate::subgraph::ValidSubgraph; +pub use crate::supergraph::ValidFederationSubgraph; +pub use crate::supergraph::ValidFederationSubgraphs; pub(crate) type SupergraphSpecs = (&'static LinkSpecDefinition, &'static JoinSpecDefinition); @@ -129,10 +130,7 @@ impl Supergraph { } pub fn extract_subgraphs(&self) -> Result { - crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph( - &self.schema, - None, - ) + supergraph::extract_subgraphs_from_supergraph(&self.schema, None) } } diff --git a/apollo-federation/src/operation/contains.rs b/apollo-federation/src/operation/contains.rs index 9b306504a2..e69f978b3f 100644 --- a/apollo-federation/src/operation/contains.rs +++ b/apollo-federation/src/operation/contains.rs @@ -1,7 +1,4 @@ -use apollo_compiler::collections::IndexMap; use apollo_compiler::executable; -use apollo_compiler::Name; -use apollo_compiler::Node; use super::FieldSelection; use super::FragmentSpreadSelection; @@ -10,202 +7,6 @@ use super::InlineFragmentSelection; use super::Selection; use super::SelectionSet; -/// Compare two input values, with two special cases for objects: assuming no duplicate keys, -/// and order-independence. -/// -/// This comes from apollo-rs: https://github.com/apollographql/apollo-rs/blob/6825be88fe13cd0d67b83b0e4eb6e03c8ab2555e/crates/apollo-compiler/src/validation/selection.rs#L160-L188 -/// Hopefully we can do this more easily in the future! -fn same_value(left: &executable::Value, right: &executable::Value) -> bool { - use apollo_compiler::executable::Value; - match (left, right) { - (Value::Null, Value::Null) => true, - (Value::Enum(left), Value::Enum(right)) => left == right, - (Value::Variable(left), Value::Variable(right)) => left == right, - (Value::String(left), Value::String(right)) => left == right, - (Value::Float(left), Value::Float(right)) => left == right, - (Value::Int(left), Value::Int(right)) => left == right, - (Value::Boolean(left), Value::Boolean(right)) => left == right, - (Value::List(left), Value::List(right)) if left.len() == right.len() => left - .iter() - .zip(right.iter()) - .all(|(left, right)| same_value(left, right)), - (Value::Object(left), Value::Object(right)) if left.len() == right.len() => { - left.iter().all(|(key, value)| { - right - .iter() - .find(|(other_key, _)| key == other_key) - .is_some_and(|(_, other_value)| same_value(value, other_value)) - }) - } - _ => false, - } -} - -/// Sort an input value, which means specifically sorting their object values by keys (assuming no -/// duplicates). This is used for hashing input values in a way consistent with [same_value()]. -fn sort_value(value: &mut executable::Value) { - use apollo_compiler::executable::Value; - match value { - Value::List(elems) => { - elems - .iter_mut() - .for_each(|value| sort_value(value.make_mut())); - } - Value::Object(pairs) => { - pairs - .iter_mut() - .for_each(|(_, value)| sort_value(value.make_mut())); - pairs.sort_by(|left, right| left.0.cmp(&right.0)); - } - _ => {} - } -} - -/// Compare sorted input values, which means specifically establishing an order between the variants -/// of input values, and comparing values for the same variants accordingly. This is used for -/// hashing directives in a way consistent with [same_directives()]. -/// -/// Note that Floats and Ints are compared textually and not parsed numerically. This is fine for -/// the purposes of hashing. For object comparison semantics, see [compare_sorted_object_pairs()]. -fn compare_sorted_value(left: &executable::Value, right: &executable::Value) -> std::cmp::Ordering { - use apollo_compiler::executable::Value; - fn discriminant(value: &Value) -> u8 { - match value { - Value::Null => 0, - Value::Enum(_) => 1, - Value::Variable(_) => 2, - Value::String(_) => 3, - Value::Float(_) => 4, - Value::Int(_) => 5, - Value::Boolean(_) => 6, - Value::List(_) => 7, - Value::Object(_) => 8, - } - } - match (left, right) { - (Value::Null, Value::Null) => std::cmp::Ordering::Equal, - (Value::Enum(left), Value::Enum(right)) => left.cmp(right), - (Value::Variable(left), Value::Variable(right)) => left.cmp(right), - (Value::String(left), Value::String(right)) => left.cmp(right), - (Value::Float(left), Value::Float(right)) => left.as_str().cmp(right.as_str()), - (Value::Int(left), Value::Int(right)) => left.as_str().cmp(right.as_str()), - (Value::Boolean(left), Value::Boolean(right)) => left.cmp(right), - (Value::List(left), Value::List(right)) => left.len().cmp(&right.len()).then_with(|| { - left.iter() - .zip(right) - .map(|(left, right)| compare_sorted_value(left, right)) - .find(|o| o.is_ne()) - .unwrap_or(std::cmp::Ordering::Equal) - }), - (Value::Object(left), Value::Object(right)) => compare_sorted_name_value_pairs( - left.iter().map(|pair| &pair.0), - left.iter().map(|pair| &pair.1), - right.iter().map(|pair| &pair.0), - right.iter().map(|pair| &pair.1), - ), - _ => discriminant(left).cmp(&discriminant(right)), - } -} - -/// Compare the (name, value) pair iterators, which are assumed to be sorted by name and have sorted -/// values. This is used for hashing objects/arguments in a way consistent with [same_directives()]. -/// -/// Note that pair iterators are compared by length, then lexicographically by name, then finally -/// recursively by value. This is intended to compute an ordering quickly for hashing. -fn compare_sorted_name_value_pairs<'doc>( - left_names: impl ExactSizeIterator, - left_values: impl ExactSizeIterator>, - right_names: impl ExactSizeIterator, - right_values: impl ExactSizeIterator>, -) -> std::cmp::Ordering { - left_names - .len() - .cmp(&right_names.len()) - .then_with(|| left_names.cmp(right_names)) - .then_with(|| { - left_values - .zip(right_values) - .map(|(left, right)| compare_sorted_value(left, right)) - .find(|o| o.is_ne()) - .unwrap_or(std::cmp::Ordering::Equal) - }) -} - -/// Returns true if two argument lists are equivalent. -/// -/// The arguments and values must be the same, independent of order. -fn same_arguments( - left: &[Node], - right: &[Node], -) -> bool { - if left.len() != right.len() { - return false; - } - - let right = right - .iter() - .map(|arg| (&arg.name, arg)) - .collect::>(); - - left.iter().all(|arg| { - right - .get(&arg.name) - .is_some_and(|right_arg| same_value(&arg.value, &right_arg.value)) - }) -} - -/// Sort arguments, which means specifically sorting arguments by names and object values by keys -/// (assuming no duplicates). This is used for hashing arguments in a way consistent with -/// [same_arguments()]. -pub(super) fn sort_arguments(arguments: &mut [Node]) { - arguments - .iter_mut() - .for_each(|arg| sort_value(arg.make_mut().value.make_mut())); - arguments.sort_by(|left, right| left.name.cmp(&right.name)); -} - -/// Compare sorted arguments; see [compare_sorted_name_value_pairs()] for semantics. This is used -/// for hashing directives in a way consistent with [same_directives()]. -fn compare_sorted_arguments( - left: &[Node], - right: &[Node], -) -> std::cmp::Ordering { - compare_sorted_name_value_pairs( - left.iter().map(|arg| &arg.name), - left.iter().map(|arg| &arg.value), - right.iter().map(|arg| &arg.name), - right.iter().map(|arg| &arg.value), - ) -} - -/// Returns true if two directive lists are equivalent, independent of order. -fn same_directives(left: &executable::DirectiveList, right: &executable::DirectiveList) -> bool { - if left.len() != right.len() { - return false; - } - - left.iter().all(|left_directive| { - right.iter().any(|right_directive| { - left_directive.name == right_directive.name - && same_arguments(&left_directive.arguments, &right_directive.arguments) - }) - }) -} - -/// Sort directives, which means specifically sorting their arguments, sorting the directives by -/// name, and then breaking directive-name ties by comparing sorted arguments. This is used for -/// hashing arguments in a way consistent with [same_directives()]. -pub(super) fn sort_directives(directives: &mut executable::DirectiveList) { - directives - .iter_mut() - .for_each(|directive| sort_arguments(&mut directive.make_mut().arguments)); - directives.sort_by(|left, right| { - left.name - .cmp(&right.name) - .then_with(|| compare_sorted_arguments(&left.arguments, &right.arguments)) - }); -} - pub(super) fn is_deferred_selection(directives: &executable::DirectiveList) -> bool { directives.has("defer") } @@ -277,8 +78,8 @@ impl FieldSelection { pub fn containment(&self, other: &FieldSelection, options: ContainmentOptions) -> Containment { if self.field.name() != other.field.name() || self.field.alias != other.field.alias - || !same_arguments(&self.field.arguments, &other.field.arguments) - || !same_directives(&self.field.directives, &other.field.directives) + || self.field.arguments != other.field.arguments + || self.field.directives != other.field.directives { return Containment::NotContained; } diff --git a/apollo-federation/src/operation/directive_list.rs b/apollo-federation/src/operation/directive_list.rs new file mode 100644 index 0000000000..913a1184e6 --- /dev/null +++ b/apollo-federation/src/operation/directive_list.rs @@ -0,0 +1,410 @@ +use std::fmt; +use std::fmt::Display; +use std::hash::BuildHasher; +use std::hash::Hash; +use std::hash::Hasher; +use std::ops::Deref; +use std::sync::Arc; +use std::sync::OnceLock; + +use apollo_compiler::executable; +use apollo_compiler::Name; +use apollo_compiler::Node; + +use super::sort_arguments; + +/// Compare sorted input values, which means specifically establishing an order between the variants +/// of input values, and comparing values for the same variants accordingly. +/// +/// Note that Floats and Ints are compared textually and not parsed numerically. This is fine for +/// the purposes of hashing. +fn compare_sorted_value(left: &executable::Value, right: &executable::Value) -> std::cmp::Ordering { + use apollo_compiler::executable::Value; + /// Returns an arbitrary index for each value type so values of different types are sorted consistently. + fn discriminant(value: &Value) -> u8 { + match value { + Value::Null => 0, + Value::Enum(_) => 1, + Value::Variable(_) => 2, + Value::String(_) => 3, + Value::Float(_) => 4, + Value::Int(_) => 5, + Value::Boolean(_) => 6, + Value::List(_) => 7, + Value::Object(_) => 8, + } + } + match (left, right) { + (Value::Null, Value::Null) => std::cmp::Ordering::Equal, + (Value::Enum(left), Value::Enum(right)) => left.cmp(right), + (Value::Variable(left), Value::Variable(right)) => left.cmp(right), + (Value::String(left), Value::String(right)) => left.cmp(right), + (Value::Float(left), Value::Float(right)) => left.as_str().cmp(right.as_str()), + (Value::Int(left), Value::Int(right)) => left.as_str().cmp(right.as_str()), + (Value::Boolean(left), Value::Boolean(right)) => left.cmp(right), + (Value::List(left), Value::List(right)) => left.len().cmp(&right.len()).then_with(|| { + left.iter() + .zip(right) + .map(|(left, right)| compare_sorted_value(left, right)) + .find(|o| o.is_ne()) + .unwrap_or(std::cmp::Ordering::Equal) + }), + (Value::Object(left), Value::Object(right)) => compare_sorted_name_value_pairs( + left.iter().map(|pair| &pair.0), + left.iter().map(|pair| &pair.1), + right.iter().map(|pair| &pair.0), + right.iter().map(|pair| &pair.1), + ), + _ => discriminant(left).cmp(&discriminant(right)), + } +} + +/// Compare the (name, value) pair iterators, which are assumed to be sorted by name and have sorted +/// values. This is used for hashing objects/arguments in a way consistent with [same_directives()]. +/// +/// Note that pair iterators are compared by length, then lexicographically by name, then finally +/// recursively by value. This is intended to compute an ordering quickly for hashing. +fn compare_sorted_name_value_pairs<'doc>( + left_names: impl ExactSizeIterator, + left_values: impl ExactSizeIterator>, + right_names: impl ExactSizeIterator, + right_values: impl ExactSizeIterator>, +) -> std::cmp::Ordering { + left_names + .len() + .cmp(&right_names.len()) + .then_with(|| left_names.cmp(right_names)) + .then_with(|| { + left_values + .zip(right_values) + .map(|(left, right)| compare_sorted_value(left, right)) + .find(|o| o.is_ne()) + .unwrap_or(std::cmp::Ordering::Equal) + }) +} + +/// Compare sorted arguments; see [compare_sorted_name_value_pairs()] for semantics. This is used +/// for hashing directives in a way consistent with [same_directives()]. +fn compare_sorted_arguments( + left: &[Node], + right: &[Node], +) -> std::cmp::Ordering { + compare_sorted_name_value_pairs( + left.iter().map(|arg| &arg.name), + left.iter().map(|arg| &arg.value), + right.iter().map(|arg| &arg.name), + right.iter().map(|arg| &arg.value), + ) +} + +/// An empty apollo-compiler directive list that we can return a reference to when a +/// [`DirectiveList`] is in the empty state. +static EMPTY_DIRECTIVE_LIST: executable::DirectiveList = executable::DirectiveList(vec![]); + +/// Contents for a non-empty directive list. +#[derive(Debug, Clone)] +struct DirectiveListInner { + // Cached hash: hashing may be expensive with deeply nested values or very many directives, + // so we only want to do it once. + // The hash is eagerly precomputed because we expect to, most of the time, hash a DirectiveList + // at least once (when inserting its selection into a selection map). + hash: u64, + // Mutable access to the underlying directive list should not be handed out because `sort_order` + // may get out of sync. + directives: executable::DirectiveList, + sort_order: Vec, +} + +impl PartialEq for DirectiveListInner { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + && self + .iter_sorted() + .zip(other.iter_sorted()) + .all(|(left, right)| { + // We can just use `Eq` because the arguments are sorted recursively + left.name == right.name && left.arguments == right.arguments + }) + } +} + +impl Eq for DirectiveListInner {} + +impl DirectiveListInner { + fn rehash(&mut self) { + static SHARED_RANDOM: OnceLock = OnceLock::new(); + + let mut state = SHARED_RANDOM.get_or_init(Default::default).build_hasher(); + self.len().hash(&mut state); + // Hash in sorted order + for d in self.iter_sorted() { + d.hash(&mut state); + } + self.hash = state.finish(); + } + + fn len(&self) -> usize { + self.directives.len() + } + + fn iter_sorted(&self) -> DirectiveIterSorted<'_> { + DirectiveIterSorted { + directives: &self.directives.0, + inner: self.sort_order.iter(), + } + } +} + +/// A list of directives, with order-independent hashing and equality. +/// +/// Original order of directive applications is stored but is not part of hashing, +/// so it may not be maintained exactly when round-tripping several directive lists +/// through a HashSet for example. +/// +/// Arguments and input object values provided to directives are all sorted and the +/// original order is not tracked. +/// +/// This list is cheaply cloneable, but not intended for frequent mutations. +/// When the list is empty, it does not require an allocation. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct DirectiveList { + inner: Option>, +} + +impl Deref for DirectiveList { + type Target = executable::DirectiveList; + fn deref(&self) -> &Self::Target { + self.inner + .as_ref() + .map_or(&EMPTY_DIRECTIVE_LIST, |inner| &inner.directives) + } +} + +impl Hash for DirectiveList { + fn hash(&self, state: &mut H) { + state.write_u64(self.inner.as_ref().map_or(0, |inner| inner.hash)) + } +} + +impl Display for DirectiveList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(inner) = &self.inner { + inner.directives.fmt(f) + } else { + Ok(()) + } + } +} + +impl From for DirectiveList { + fn from(mut directives: executable::DirectiveList) -> Self { + if directives.is_empty() { + return Self::new(); + } + + // Sort directives, which means specifically sorting their arguments, sorting the directives by + // name, and then breaking directive-name ties by comparing sorted arguments. This is used for + // hashing arguments in a way consistent with [same_directives()]. + + for directive in directives.iter_mut() { + sort_arguments(&mut directive.make_mut().arguments); + } + + let mut sort_order = (0usize..directives.len()).collect::>(); + sort_order.sort_by(|left, right| { + let left = &directives[*left]; + let right = &directives[*right]; + left.name + .cmp(&right.name) + .then_with(|| compare_sorted_arguments(&left.arguments, &right.arguments)) + }); + + let mut partially_initialized = DirectiveListInner { + hash: 0, + directives, + sort_order, + }; + partially_initialized.rehash(); + Self { + inner: Some(Arc::new(partially_initialized)), + } + } +} + +impl FromIterator> for DirectiveList { + fn from_iter>>(iter: T) -> Self { + Self::from(executable::DirectiveList::from_iter(iter)) + } +} + +impl FromIterator for DirectiveList { + fn from_iter>(iter: T) -> Self { + Self::from(executable::DirectiveList::from_iter(iter)) + } +} + +impl DirectiveList { + /// Create an empty directive list. + pub(crate) const fn new() -> Self { + Self { inner: None } + } + + /// Create a directive list with a single directive. + /// + /// This sorts arguments and input object values provided to the directive. + pub(crate) fn one(directive: impl Into>) -> Self { + std::iter::once(directive.into()).collect() + } + + #[cfg(test)] + pub(crate) fn parse(input: &str) -> Self { + use apollo_compiler::ast; + let input = format!( + r#"query {{ field +# Directive input: +{input} +# +}}"# + ); + let mut parser = apollo_compiler::parser::Parser::new(); + let document = parser + .parse_ast(&input, "DirectiveList::parse.graphql") + .unwrap(); + let Some(ast::Definition::OperationDefinition(operation)) = document.definitions.first() + else { + unreachable!(); + }; + let Some(ast::Selection::Field(field)) = operation.selection_set.first() else { + unreachable!(); + }; + field.directives.clone().into() + } + + /// Iterate the directives in their original order. + pub(crate) fn iter(&self) -> impl ExactSizeIterator> { + self.inner + .as_ref() + .map_or(&EMPTY_DIRECTIVE_LIST, |inner| &inner.directives) + .iter() + } + + /// Iterate the directives in a consistent sort order. + pub(crate) fn iter_sorted(&self) -> DirectiveIterSorted<'_> { + self.inner + .as_ref() + .map_or_else(DirectiveIterSorted::empty, |inner| inner.iter_sorted()) + } + + /// Remove one directive application by name. + /// + /// To remove a repeatable directive, you may need to call this multiple times. + pub(crate) fn remove_one(&mut self, name: &str) -> Option> { + let Some(inner) = self.inner.as_mut() else { + // Nothing to do on an empty list + return None; + }; + let Some(index) = inner.directives.iter().position(|dir| dir.name == name) else { + return None; + }; + + // The directive exists and is the only directive: switch to the empty representation + if inner.len() == 1 { + // The index is guaranteed to exist so we can safely use the panicky [] syntax. + let item = inner.directives[index].clone(); + self.inner = None; + return Some(item); + } + + // The directive exists: clone the inner structure if necessary. + let inner = Arc::make_mut(inner); + let sort_index = inner + .sort_order + .iter() + .position(|sorted| *sorted == index) + .expect("index must exist in sort order"); + let item = inner.directives.remove(index); + inner.sort_order.remove(sort_index); + + for order in &mut inner.sort_order { + if *order > index { + *order -= 1; + } + } + inner.rehash(); + Some(item) + } +} + +/// Iterate over a [`DirectiveList`] in a consistent sort order. +pub(crate) struct DirectiveIterSorted<'a> { + directives: &'a [Node], + inner: std::slice::Iter<'a, usize>, +} +impl<'a> Iterator for DirectiveIterSorted<'a> { + type Item = &'a Node; + + fn next(&mut self) -> Option { + self.inner.next().map(|index| &self.directives[*index]) + } +} + +impl ExactSizeIterator for DirectiveIterSorted<'_> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl DirectiveIterSorted<'_> { + fn empty() -> Self { + Self { + directives: &[], + inner: [].iter(), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn consistent_hash() { + let mut set = HashSet::new(); + + assert!(set.insert(DirectiveList::new())); + assert!(!set.insert(DirectiveList::new())); + + assert!(set.insert(DirectiveList::parse("@a @b"))); + assert!(!set.insert(DirectiveList::parse("@b @a"))); + } + + #[test] + fn order_independent_equality() { + assert_eq!(DirectiveList::new(), DirectiveList::new()); + assert_eq!( + DirectiveList::parse("@a @b"), + DirectiveList::parse("@b @a"), + "equality should be order independent" + ); + + assert_eq!( + DirectiveList::parse("@a(arg1: true, arg2: false) @b(arg2: false, arg1: true)"), + DirectiveList::parse("@b(arg1: true, arg2: false) @a(arg1: true, arg2: false)"), + "arguments should be order independent" + ); + + assert_eq!( + DirectiveList::parse("@nested(object: { a: 1, b: 2, c: 3 })"), + DirectiveList::parse("@nested(object: { b: 2, c: 3, a: 1 })"), + "input objects should be order independent" + ); + + assert_eq!( + DirectiveList::parse("@nested(object: [true, { a: 1, b: 2, c: { a: 3 } }])"), + DirectiveList::parse("@nested(object: [true, { b: 2, c: { a: 3 }, a: 1 }])"), + "input objects should be order independent" + ); + } +} diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index a6ed20d3f6..71b4aece8b 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -19,7 +19,6 @@ use std::hash::Hash; use std::ops::Deref; use std::sync::atomic; use std::sync::Arc; -use std::sync::OnceLock; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; @@ -48,6 +47,7 @@ use crate::schema::position::SchemaRootDefinitionKind; use crate::schema::ValidFederationSchema; mod contains; +mod directive_list; mod merging; mod optimize; mod rebase; @@ -56,6 +56,7 @@ mod simplify; mod tests; pub(crate) use contains::*; +pub(crate) use directive_list::DirectiveList; pub(crate) use merging::*; pub(crate) use rebase::*; @@ -81,6 +82,101 @@ impl SelectionId { } } +/// A list of arguments to a field or directive. +/// +/// All arguments and input object values are sorted in a consistent order. +/// +/// This type is immutable and cheaply cloneable. +#[derive(Clone, PartialEq, Eq, Default)] +pub(crate) struct ArgumentList { + /// The inner list *must* be sorted with `sort_arguments`. + inner: Option]>>, +} + +impl std::fmt::Debug for ArgumentList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // Print the slice representation. + self.deref().fmt(f) + } +} + +/// Sort an input value, which means specifically sorting their object values by keys (assuming no +/// duplicates). +/// +/// After sorting, hashing and plain-Rust equality have the expected result for values that are +/// spec-equivalent. +fn sort_value(value: &mut executable::Value) { + use apollo_compiler::executable::Value; + match value { + Value::List(elems) => { + elems + .iter_mut() + .for_each(|value| sort_value(value.make_mut())); + } + Value::Object(pairs) => { + pairs + .iter_mut() + .for_each(|(_, value)| sort_value(value.make_mut())); + pairs.sort_by(|left, right| left.0.cmp(&right.0)); + } + _ => {} + } +} + +/// Sort arguments, which means specifically sorting arguments by names and object values by keys +/// (assuming no duplicates). +/// +/// After sorting, hashing and plain-Rust equality have the expected result for lists that are +/// spec-equivalent. +fn sort_arguments(arguments: &mut [Node]) { + arguments + .iter_mut() + .for_each(|arg| sort_value(arg.make_mut().value.make_mut())); + arguments.sort_by(|left, right| left.name.cmp(&right.name)); +} + +impl From>> for ArgumentList { + fn from(mut arguments: Vec>) -> Self { + if arguments.is_empty() { + return Self::new(); + } + + sort_arguments(&mut arguments); + + Self { + inner: Some(Arc::from(arguments)), + } + } +} + +impl FromIterator> for ArgumentList { + fn from_iter>>(iter: T) -> Self { + Self::from(Vec::from_iter(iter)) + } +} + +impl Deref for ArgumentList { + type Target = [Node]; + + fn deref(&self) -> &Self::Target { + self.inner.as_deref().unwrap_or_default() + } +} + +impl ArgumentList { + /// Create an empty argument list. + pub(crate) const fn new() -> Self { + Self { inner: None } + } + + /// Create a argument list with a single argument. + /// + /// This sorts any input object values provided to the argument. + pub(crate) fn one(argument: impl Into>) -> Self { + Self::from(vec![argument.into()]) + } +} + /// An analogue of the apollo-compiler type `Operation` with these changes: /// - Stores the schema that the operation is queried against. /// - Swaps `operation_type` with `root_kind` (using the analogous apollo-federation type). @@ -93,7 +189,7 @@ pub struct Operation { pub(crate) root_kind: SchemaRootDefinitionKind, pub(crate) name: Option, pub(crate) variables: Arc>>, - pub(crate) directives: Arc, + pub(crate) directives: DirectiveList, pub(crate) selection_set: SelectionSet, pub(crate) named_fragments: NamedFragments, } @@ -138,7 +234,7 @@ impl Operation { root_kind: operation.operation_type.into(), name: operation.name.clone(), variables: Arc::new(operation.variables.clone()), - directives: Arc::new(operation.directives.clone()), + directives: operation.directives.clone().into(), selection_set, named_fragments, }) @@ -215,7 +311,6 @@ mod selection_map { use std::sync::Arc; use apollo_compiler::collections::IndexMap; - use apollo_compiler::executable; use serde::Serialize; use crate::error::FederationError; @@ -223,6 +318,7 @@ mod selection_map { use crate::operation::field_selection::FieldSelection; use crate::operation::fragment_spread_selection::FragmentSpreadSelection; use crate::operation::inline_fragment_selection::InlineFragmentSelection; + use crate::operation::DirectiveList; use crate::operation::HasSelectionKey; use crate::operation::Selection; use crate::operation::SelectionKey; @@ -434,7 +530,7 @@ mod selection_map { } } - pub(super) fn get_directives_mut(&mut self) -> &mut Arc { + pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { match self { Self::Field(field) => field.get_directives_mut(), Self::FragmentSpread(spread) => spread.get_directives_mut(), @@ -467,7 +563,7 @@ mod selection_map { Arc::make_mut(self.0).field.sibling_typename_mut() } - pub(super) fn get_directives_mut(&mut self) -> &mut Arc { + pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { Arc::make_mut(self.0).field.directives_mut() } @@ -484,7 +580,7 @@ mod selection_map { Self(fragment_spread_selection) } - pub(super) fn get_directives_mut(&mut self) -> &mut Arc { + pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { Arc::make_mut(self.0).spread.directives_mut() } @@ -509,7 +605,7 @@ mod selection_map { self.0 } - pub(super) fn get_directives_mut(&mut self) -> &mut Arc { + pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { Arc::make_mut(self.0).inline_fragment.directives_mut() } @@ -617,21 +713,21 @@ pub(crate) enum SelectionKey { response_name: Name, /// directives applied on the field #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - directives: Arc, + directives: DirectiveList, }, FragmentSpread { /// The name of the fragment. fragment_name: Name, /// Directives applied on the fragment spread (does not contain @defer). #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - directives: Arc, + directives: DirectiveList, }, InlineFragment { /// The optional type condition of the fragment. type_condition: Option, /// Directives applied on the fragment spread (does not contain @defer). #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - directives: Arc, + directives: DirectiveList, }, Defer { /// Unique selection ID used to distinguish deferred fragment spreads that cannot be merged. @@ -746,7 +842,7 @@ impl Selection { } } - fn directives(&self) -> &Arc { + fn directives(&self) -> &DirectiveList { match self { Selection::Field(field_selection) => &field_selection.field.directives, Selection::FragmentSpread(fragment_spread_selection) => { @@ -876,7 +972,7 @@ impl Selection { pub(crate) fn with_updated_directives( &self, - directives: executable::DirectiveList, + directives: impl Into, ) -> Result { match self { Selection::Field(field) => Ok(Selection::Field(Arc::new( @@ -967,7 +1063,7 @@ pub(crate) struct Fragment { pub(crate) schema: ValidFederationSchema, pub(crate) name: Name, pub(crate) type_condition_position: CompositeTypeDefinitionPosition, - pub(crate) directives: Arc, + pub(crate) directives: DirectiveList, pub(crate) selection_set: SelectionSet, } @@ -983,7 +1079,7 @@ impl Fragment { type_condition_position: schema .get_type(fragment.type_condition().clone())? .try_into()?, - directives: Arc::new(fragment.directives.clone()), + directives: fragment.directives.clone().into(), selection_set: SelectionSet::from_selection_set( &fragment.selection_set, named_fragments, @@ -1001,17 +1097,14 @@ mod field_selection { use std::hash::Hash; use std::hash::Hasher; use std::ops::Deref; - use std::sync::Arc; use apollo_compiler::ast; - use apollo_compiler::executable; use apollo_compiler::Name; - use apollo_compiler::Node; use serde::Serialize; use crate::error::FederationError; - use crate::operation::sort_arguments; - use crate::operation::sort_directives; + use crate::operation::ArgumentList; + use crate::operation::DirectiveList; use crate::operation::HasSelectionKey; use crate::operation::SelectionKey; use crate::operation::SelectionSet; @@ -1054,10 +1147,7 @@ mod field_selection { } } - pub(crate) fn with_updated_directives( - &self, - directives: executable::DirectiveList, - ) -> Self { + pub(crate) fn with_updated_directives(&self, directives: impl Into) -> Self { Self { field: self.field.with_updated_directives(directives), selection_set: self.selection_set.clone(), @@ -1081,8 +1171,6 @@ mod field_selection { pub(crate) struct Field { data: FieldData, key: SelectionKey, - #[serde(serialize_with = "crate::display_helpers::serialize_as_debug_string")] - sorted_arguments: Arc>>, } impl std::fmt::Debug for Field { @@ -1095,7 +1183,7 @@ mod field_selection { fn eq(&self, other: &Self) -> bool { self.data.field_position.field_name() == other.data.field_position.field_name() && self.key == other.key - && self.sorted_arguments == other.sorted_arguments + && self.data.arguments == other.data.arguments } } @@ -1105,7 +1193,7 @@ mod field_selection { fn hash(&self, state: &mut H) { self.data.field_position.field_name().hash(state); self.key.hash(state); - self.sorted_arguments.hash(state); + self.data.arguments.hash(state); } } @@ -1119,11 +1207,8 @@ mod field_selection { impl Field { pub(crate) fn new(data: FieldData) -> Self { - let mut arguments = data.arguments.as_ref().clone(); - sort_arguments(&mut arguments); Self { key: data.key(), - sorted_arguments: Arc::new(arguments), data, } } @@ -1203,7 +1288,7 @@ mod field_selection { &self.data } - pub(super) fn directives_mut(&mut self) -> &mut Arc { + pub(super) fn directives_mut(&mut self) -> &mut DirectiveList { &mut self.data.directives } @@ -1217,10 +1302,10 @@ mod field_selection { pub(crate) fn with_updated_directives( &self, - directives: executable::DirectiveList, + directives: impl Into, ) -> Field { let mut data = self.data.clone(); - data.directives = Arc::new(directives); + data.directives = directives.into(); Self::new(data) } @@ -1260,9 +1345,9 @@ mod field_selection { pub(crate) field_position: FieldDefinitionPosition, pub(crate) alias: Option, #[serde(serialize_with = "crate::display_helpers::serialize_as_debug_string")] - pub(crate) arguments: Arc>>, + pub(crate) arguments: ArgumentList, #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - pub(crate) directives: Arc, + pub(crate) directives: DirectiveList, pub(crate) sibling_typename: Option, } @@ -1311,11 +1396,9 @@ mod field_selection { impl HasSelectionKey for FieldData { fn key(&self) -> SelectionKey { - let mut directives = self.directives.as_ref().clone(); - sort_directives(&mut directives); SelectionKey::Field { response_name: self.response_name(), - directives: Arc::new(directives), + directives: self.directives.clone(), } } } @@ -1328,14 +1411,12 @@ pub(crate) use field_selection::SiblingTypename; mod fragment_spread_selection { use std::ops::Deref; - use std::sync::Arc; - use apollo_compiler::executable; use apollo_compiler::Name; use serde::Serialize; use crate::operation::is_deferred_selection; - use crate::operation::sort_directives; + use crate::operation::DirectiveList; use crate::operation::HasSelectionKey; use crate::operation::SelectionId; use crate::operation::SelectionKey; @@ -1398,7 +1479,7 @@ mod fragment_spread_selection { &self.data } - pub(super) fn directives_mut(&mut self) -> &mut Arc { + pub(super) fn directives_mut(&mut self) -> &mut DirectiveList { &mut self.data.directives } } @@ -1417,14 +1498,14 @@ mod fragment_spread_selection { pub(crate) type_condition_position: CompositeTypeDefinitionPosition, // directives applied on the fragment spread selection #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - pub(crate) directives: Arc, + pub(crate) directives: DirectiveList, // directives applied within the fragment definition // // PORT_NOTE: The JS codebase combined the fragment spread's directives with the fragment // definition's directives. This was invalid GraphQL as those directives may not be applicable // on different locations. While we now keep track of those references, they are currently ignored. #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - pub(crate) fragment_directives: Arc, + pub(crate) fragment_directives: DirectiveList, #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] pub(crate) selection_id: SelectionId, } @@ -1436,11 +1517,9 @@ mod fragment_spread_selection { deferred_id: self.selection_id.clone(), } } else { - let mut directives = self.directives.as_ref().clone(); - sort_directives(&mut directives); SelectionKey::FragmentSpread { fragment_name: self.fragment_name.clone(), - directives: Arc::new(directives), + directives: self.directives.clone(), } } } @@ -1538,7 +1617,7 @@ impl FragmentSpreadData { schema: fragment.schema.clone(), fragment_name: fragment.name.clone(), type_condition_position: fragment.type_condition_position.clone(), - directives: Arc::new(spread_directives.clone()), + directives: spread_directives.clone().into(), fragment_directives: fragment.directives.clone(), selection_id: SelectionId::new(), } @@ -1549,16 +1628,14 @@ mod inline_fragment_selection { use std::hash::Hash; use std::hash::Hasher; use std::ops::Deref; - use std::sync::Arc; - use apollo_compiler::executable; use serde::Serialize; use crate::error::FederationError; use crate::link::graphql_definition::defer_directive_arguments; use crate::link::graphql_definition::DeferDirectiveArguments; use crate::operation::is_deferred_selection; - use crate::operation::sort_directives; + use crate::operation::DirectiveList; use crate::operation::HasSelectionKey; use crate::operation::SelectionId; use crate::operation::SelectionKey; @@ -1589,10 +1666,7 @@ mod inline_fragment_selection { } } - pub(crate) fn with_updated_directives( - &self, - directives: executable::DirectiveList, - ) -> Self { + pub(crate) fn with_updated_directives(&self, directives: impl Into) -> Self { Self { inline_fragment: self.inline_fragment.with_updated_directives(directives), selection_set: self.selection_set.clone(), @@ -1658,7 +1732,7 @@ mod inline_fragment_selection { &self.data } - pub(super) fn directives_mut(&mut self) -> &mut Arc { + pub(super) fn directives_mut(&mut self) -> &mut DirectiveList { &mut self.data.directives } @@ -1672,10 +1746,10 @@ mod inline_fragment_selection { } pub(crate) fn with_updated_directives( &self, - directives: executable::DirectiveList, + directives: impl Into, ) -> InlineFragment { let mut data = self.data().clone(); - data.directives = Arc::new(directives); + data.directives = directives.into(); Self::new(data) } @@ -1701,7 +1775,7 @@ mod inline_fragment_selection { pub(crate) parent_type_position: CompositeTypeDefinitionPosition, pub(crate) type_condition_position: Option, #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - pub(crate) directives: Arc, + pub(crate) directives: DirectiveList, #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] pub(crate) selection_id: SelectionId, } @@ -1731,14 +1805,12 @@ mod inline_fragment_selection { deferred_id: self.selection_id.clone(), } } else { - let mut directives = self.directives.as_ref().clone(); - sort_directives(&mut directives); SelectionKey::InlineFragment { type_condition: self .type_condition_position .as_ref() .map(|pos| pos.type_name().clone()), - directives: Arc::new(directives), + directives: self.directives.clone(), } } } @@ -2438,6 +2510,8 @@ impl SelectionSet { ) -> Result { let mut selection_map = SelectionMap::new(); if let Some(parent) = parent_type_if_abstract { + // XXX(@goto-bus-stop): if the selection set has an *alias* named __typename for some + // other field, this doesn't work right. is that allowed? if !self.has_top_level_typename_field() { let typename_selection = Selection::from_field( Field::new_introspection_typename(&self.schema, &parent.into(), None), @@ -2472,16 +2546,12 @@ impl SelectionSet { } fn has_top_level_typename_field(&self) -> bool { - // Needs to be behind a OnceLock because `Arc::new` is non-const. - // XXX(@goto-bus-stop): Note this does *not* count `__typename @include(if: true)`. - // This seems wrong? But it's what JS does, too. - static TYPENAME_KEY: OnceLock = OnceLock::new(); - let key = TYPENAME_KEY.get_or_init(|| SelectionKey::Field { + const TYPENAME_KEY: SelectionKey = SelectionKey::Field { response_name: TYPENAME_FIELD, - directives: Arc::new(Default::default()), - }); + directives: DirectiveList::new(), + }; - self.selections.contains_key(key) + self.selections.contains_key(&TYPENAME_KEY) } /// Adds a path, and optional some selections following that path, to this selection map. @@ -2595,7 +2665,9 @@ impl SelectionSet { /// Removes the @defer directive from all selections without removing that selection. fn without_defer(&mut self) { for (_key, mut selection) in Arc::make_mut(&mut self.selections).iter_mut() { - Arc::make_mut(selection.get_directives_mut()).retain(|dir| dir.name != name!("defer")); + // TODO(@goto-bus-stop): doing this changes the key of the selection! + // We have to rebuild the selection map. + selection.get_directives_mut().remove_one("defer"); if let Some(set) = selection.get_selection_set_mut() { set.without_defer(); } @@ -3121,8 +3193,8 @@ impl FieldSelection { schema: schema.clone(), field_position, alias: field.alias.clone(), - arguments: Arc::new(field.arguments.clone()), - directives: Arc::new(field.directives.clone()), + arguments: field.arguments.clone().into(), + directives: field.directives.clone().into(), sibling_typename: None, }), selection_set: if is_composite { @@ -3237,7 +3309,7 @@ impl InlineFragmentSelection { schema: schema.clone(), parent_type_position: parent_type_position.clone(), type_condition_position, - directives: Arc::new(inline_fragment.directives.clone()), + directives: inline_fragment.directives.clone().into(), selection_id: SelectionId::new(), }); Ok(InlineFragmentSelection::new( @@ -3276,7 +3348,7 @@ impl InlineFragmentSelection { pub(crate) fn from_selection_set( parent_type_position: CompositeTypeDefinitionPosition, selection_set: SelectionSet, - directives: Arc, + directives: DirectiveList, ) -> Self { let inline_fragment_data = InlineFragmentData { schema: selection_set.schema.clone(), @@ -3690,7 +3762,7 @@ impl TryFrom<&Operation> for executable::Operation { operation_type, name: normalized_operation.name.clone(), variables: normalized_operation.variables.deref().clone(), - directives: normalized_operation.directives.deref().clone(), + directives: normalized_operation.directives.iter().cloned().collect(), selection_set: (&normalized_operation.selection_set).try_into()?, }) } @@ -3702,7 +3774,7 @@ impl TryFrom<&Fragment> for executable::Fragment { fn try_from(normalized_fragment: &Fragment) -> Result { Ok(Self { name: normalized_fragment.name.clone(), - directives: normalized_fragment.directives.deref().clone(), + directives: normalized_fragment.directives.iter().cloned().collect(), selection_set: (&normalized_fragment.selection_set).try_into()?, }) } @@ -3773,7 +3845,7 @@ impl TryFrom<&Field> for executable::Field { alias: normalized_field.alias.to_owned(), name: normalized_field.name().to_owned(), arguments: normalized_field.arguments.deref().to_owned(), - directives: normalized_field.directives.deref().to_owned(), + directives: normalized_field.directives.iter().cloned().collect(), selection_set, }) } @@ -3807,7 +3879,11 @@ impl TryFrom<&InlineFragment> for executable::InlineFragment { }); Ok(Self { type_condition, - directives: normalized_inline_fragment.directives.deref().to_owned(), + directives: normalized_inline_fragment + .directives + .iter() + .cloned() + .collect(), selection_set: executable::SelectionSet { ty, selections: Vec::new(), @@ -3832,7 +3908,11 @@ impl From<&FragmentSpreadSelection> for executable::FragmentSpread { let normalized_fragment_spread = &val.spread; Self { fragment_name: normalized_fragment_spread.fragment_name.to_owned(), - directives: normalized_fragment_spread.directives.deref().to_owned(), + directives: normalized_fragment_spread + .directives + .iter() + .cloned() + .collect(), } } } @@ -4016,7 +4096,7 @@ pub(crate) fn normalize_operation( root_kind: operation.operation_type.into(), name: operation.name.clone(), variables: Arc::new(operation.variables.clone()), - directives: Arc::new(operation.directives.clone()), + directives: operation.directives.clone().into(), selection_set: normalized_selection_set, named_fragments, }; diff --git a/apollo-federation/src/operation/optimize.rs b/apollo-federation/src/operation/optimize.rs index 68f8178783..2bd4262e88 100644 --- a/apollo-federation/src/operation/optimize.rs +++ b/apollo-federation/src/operation/optimize.rs @@ -46,6 +46,7 @@ use apollo_compiler::Node; use super::Containment; use super::ContainmentOptions; +use super::DirectiveList; use super::Field; use super::FieldSelection; use super::Fragment; @@ -54,7 +55,6 @@ use super::InlineFragmentSelection; use super::NamedFragments; use super::Operation; use super::Selection; -use super::SelectionKey; use super::SelectionMapperReturn; use super::SelectionOrSet; use super::SelectionSet; @@ -749,10 +749,10 @@ impl Fragment { return false; } - self.selection_set.selections.iter().any(|(selection_key, _)| { + self.selection_set.selections.iter().any(|(_, selection)| { matches!( - selection_key, - SelectionKey::FragmentSpread {fragment_name, directives: _} if fragment_name == other_fragment_name, + selection, + Selection::FragmentSpread(fragment) if fragment.spread.fragment_name == *other_fragment_name ) }) } @@ -763,7 +763,7 @@ enum FullMatchingFragmentCondition<'a> { ForInlineFragmentSelection { // the type condition and directives on an inline fragment selection. type_condition_position: &'a CompositeTypeDefinitionPosition, - directives: &'a Arc, + directives: &'a DirectiveList, }, } @@ -3206,8 +3206,8 @@ mod tests { /// #[test] - #[should_panic(expected = "directive cannot be used on FRAGMENT_DEFINITION")] - // TODO: Investigate this restriction on query document in Rust version. + #[should_panic(expected = "directive is not supported for FRAGMENT_DEFINITION")] + // XXX(@goto-bus-stop): this test does not make sense, we should remove this feature fn reuse_fragments_with_same_directive_on_the_fragment() { let schema_doc = r#" type Query { @@ -3506,6 +3506,7 @@ mod tests { use apollo_compiler::name; use super::*; + use crate::operation::SelectionKey; const TEST_SCHEMA_FOR_EMPTY_BRANCH_REMOVAL: &str = r#" type Query { diff --git a/apollo-federation/src/operation/simplify.rs b/apollo-federation/src/operation/simplify.rs index 89fb42f110..8555b241a2 100644 --- a/apollo-federation/src/operation/simplify.rs +++ b/apollo-federation/src/operation/simplify.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use apollo_compiler::executable; use apollo_compiler::name; -use apollo_compiler::Node; use super::runtime_types_intersect; +use super::DirectiveList; use super::Field; use super::FieldData; use super::FieldSelection; @@ -83,22 +83,18 @@ impl FieldSelection { // sub-selection is empty. Which suggest something may be wrong with this part of the query // intent, but the query was valid while keeping an empty sub-selection isn't. So in that // case, we just add some "non-included" __typename field just to keep the query valid. - let directives = - executable::DirectiveList(vec![Node::new(executable::Directive { - name: name!("include"), - arguments: vec![Node::new(executable::Argument { - name: name!("if"), - value: Node::new(executable::Value::Boolean(false)), - })], - })]); + let directives = DirectiveList::one(executable::Directive { + name: name!("include"), + arguments: vec![(name!("if"), false).into()], + }); let non_included_typename = Selection::from_field( Field::new(FieldData { schema: schema.clone(), field_position: field_composite_type_position .introspection_typename_field(), alias: None, - arguments: Arc::new(vec![]), - directives: Arc::new(directives), + arguments: Default::default(), + directives, sibling_typename: None, }), None, @@ -224,14 +220,10 @@ impl InlineFragmentSelection { // We should be able to rebase, or there is a bug, so error if that is the case. // If we rebased successfully then we add "non-included" __typename field selection // just to keep the query valid. - let directives = - executable::DirectiveList(vec![Node::new(executable::Directive { - name: name!("include"), - arguments: vec![Node::new(executable::Argument { - name: name!("if"), - value: Node::new(executable::Value::Boolean(false)), - })], - })]); + let directives = DirectiveList::one(executable::Directive { + name: name!("include"), + arguments: vec![(name!("if"), false).into()], + }); let parent_typename_field = if let Some(condition) = this_condition { condition.introspection_typename_field() } else { @@ -242,8 +234,8 @@ impl InlineFragmentSelection { schema: schema.clone(), field_position: parent_typename_field, alias: None, - arguments: Arc::new(vec![]), - directives: Arc::new(directives), + arguments: Default::default(), + directives, sibling_typename: None, }), None, diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index d90760d341..ed69e54d71 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use apollo_compiler::collections::IndexSet; use apollo_compiler::name; use apollo_compiler::schema::Schema; @@ -40,27 +38,7 @@ pub(super) fn parse_schema(schema_doc: &str) -> ValidFederationSchema { } pub(super) fn parse_operation(schema: &ValidFederationSchema, query: &str) -> Operation { - let executable_document = apollo_compiler::ExecutableDocument::parse_and_validate( - schema.schema(), - query, - "query.graphql", - ) - .unwrap(); - let operation = executable_document.operations.get(None).unwrap(); - let named_fragments = NamedFragments::new(&executable_document.fragments, schema); - let selection_set = - SelectionSet::from_selection_set(&operation.selection_set, &named_fragments, schema) - .unwrap(); - - Operation { - schema: schema.clone(), - root_kind: operation.operation_type.into(), - name: operation.name.clone(), - variables: Arc::new(operation.variables.clone()), - directives: Arc::new(operation.directives.clone()), - selection_set, - named_fragments, - } + Operation::parse(schema.clone(), query, "query.graphql", None).unwrap() } /// Parse and validate the query similarly to `parse_operation`, but does not construct the diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 8aca65e9e0..3dd7abbcd6 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -21,7 +21,6 @@ use crate::link::federation_spec_definition::KeyDirectiveArguments; use crate::operation::merge_selection_sets; use crate::operation::Selection; use crate::operation::SelectionSet; -use crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdge; use crate::query_graph::QueryGraphEdgeTransition; @@ -41,6 +40,7 @@ use crate::schema::position::SchemaRootDefinitionPosition; use crate::schema::position::TypeDefinitionPosition; use crate::schema::position::UnionTypeDefinitionPosition; use crate::schema::ValidFederationSchema; +use crate::supergraph::extract_subgraphs_from_supergraph; /// Builds a "federated" query graph based on the provided supergraph and API schema. /// diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 3c75c66b78..b9486f8434 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -11,7 +11,6 @@ use std::sync::Arc; use apollo_compiler::ast::Value; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; -use apollo_compiler::executable::DirectiveList; use itertools::Itertools; use petgraph::graph::EdgeIndex; use petgraph::graph::NodeIndex; @@ -30,6 +29,7 @@ use crate::link::graphql_definition::BooleanOrVariable; use crate::link::graphql_definition::DeferDirectiveArguments; use crate::link::graphql_definition::OperationConditional; use crate::link::graphql_definition::OperationConditionalKind; +use crate::operation::DirectiveList; use crate::operation::Field; use crate::operation::FieldData; use crate::operation::HasSelectionKey; @@ -310,7 +310,7 @@ impl HasSelectionKey for OpPathElement { } impl OpPathElement { - pub(crate) fn directives(&self) -> &Arc { + pub(crate) fn directives(&self) -> &DirectiveList { match self { OpPathElement::Field(field) => &field.directives, OpPathElement::InlineFragment(inline_fragment) => &inline_fragment.directives, @@ -427,6 +427,7 @@ impl OpPathElement { match self { Self::Field(_) => Some(self.clone()), // unchanged Self::InlineFragment(inline_fragment) => { + // TODO(@goto-bus-stop): is this not exactly the wrong way around? let updated_directives: DirectiveList = inline_fragment .directives .get_all("defer") @@ -3677,18 +3678,16 @@ impl OpPath { } pub(crate) fn conditional_directives(&self) -> DirectiveList { - DirectiveList( - self.0 - .iter() - .flat_map(|path_element| { - path_element - .directives() - .iter() - .filter(|d| d.name == "include" || d.name == "skip") - }) - .cloned() - .collect(), - ) + self.0 + .iter() + .flat_map(|path_element| { + path_element + .directives() + .iter() + .filter(|d| d.name == "include" || d.name == "skip") + }) + .cloned() + .collect() } /// Filter any fragment element in the provided path whose type condition does not exist in the provided schema. @@ -3837,7 +3836,6 @@ fn is_useless_followup_element( mod tests { use std::sync::Arc; - use apollo_compiler::executable::DirectiveList; use apollo_compiler::Name; use apollo_compiler::Schema; use petgraph::stable_graph::EdgeIndex; @@ -3850,7 +3848,6 @@ mod tests { use crate::query_graph::graph_path::OpGraphPath; use crate::query_graph::graph_path::OpGraphPathTrigger; use crate::query_graph::graph_path::OpPathElement; - use crate::schema::position::FieldDefinitionPosition; use crate::schema::position::ObjectFieldDefinitionPosition; use crate::schema::ValidFederationSchema; @@ -3881,14 +3878,7 @@ mod tests { type_name: Name::new("T").unwrap(), field_name: Name::new("t").unwrap(), }; - let data = FieldData { - schema: schema.clone(), - field_position: FieldDefinitionPosition::Object(pos), - alias: None, - arguments: Arc::new(Vec::new()), - directives: Arc::new(DirectiveList::new()), - sibling_typename: None, - }; + let data = FieldData::from_position(&schema, pos.into()); let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(Field::new(data))); let path = path .add( @@ -3906,14 +3896,7 @@ mod tests { type_name: Name::new("ID").unwrap(), field_name: Name::new("id").unwrap(), }; - let data = FieldData { - schema, - field_position: FieldDefinitionPosition::Object(pos), - alias: None, - arguments: Arc::new(Vec::new()), - directives: Arc::new(DirectiveList::new()), - sibling_typename: None, - }; + let data = FieldData::from_position(&schema, pos.into()); let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(Field::new(data))); let path = path .add( diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index e77d191efa..15e83f49f9 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -30,7 +30,6 @@ use crate::schema::ValidFederationSchema; pub mod build_query_graph; pub(crate) mod condition_resolver; -pub(crate) mod extract_subgraphs_from_supergraph; pub(crate) mod graph_path; pub mod output; pub(crate) mod path_tree; diff --git a/apollo-federation/src/query_graph/path_tree.rs b/apollo-federation/src/query_graph/path_tree.rs index 3411458f89..02812159a3 100644 --- a/apollo-federation/src/query_graph/path_tree.rs +++ b/apollo-federation/src/query_graph/path_tree.rs @@ -464,7 +464,6 @@ where mod tests { use std::sync::Arc; - use apollo_compiler::executable::DirectiveList; use apollo_compiler::ExecutableDocument; use petgraph::stable_graph::NodeIndex; use petgraph::visit::EdgeRef; @@ -542,8 +541,8 @@ mod tests { schema: query_graph.schema().unwrap().clone(), field_position: field_def.clone(), alias: None, - arguments: Arc::new(Vec::new()), - directives: Arc::new(DirectiveList::new()), + arguments: Default::default(), + directives: Default::default(), sibling_typename: None, }; let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(Field::new(data))); diff --git a/apollo-federation/src/query_plan/conditions.rs b/apollo-federation/src/query_plan/conditions.rs index ab84d2d8ca..d4a84b9f49 100644 --- a/apollo-federation/src/query_plan/conditions.rs +++ b/apollo-federation/src/query_plan/conditions.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use apollo_compiler::ast::Directive; use apollo_compiler::collections::IndexMap; -use apollo_compiler::executable::DirectiveList; use apollo_compiler::executable::Value; use apollo_compiler::Name; use apollo_compiler::Node; @@ -10,6 +9,7 @@ use indexmap::map::Entry; use serde::Serialize; use crate::error::FederationError; +use crate::operation::DirectiveList; use crate::operation::Selection; use crate::operation::SelectionMap; use crate::operation::SelectionSet; @@ -93,7 +93,7 @@ impl Conditions { pub(crate) fn from_directives(directives: &DirectiveList) -> Result { let mut variables = IndexMap::default(); - for directive in directives { + for directive in directives.iter_sorted() { let negated = match directive.name.as_str() { "include" => false, "skip" => true, @@ -285,8 +285,8 @@ pub(crate) fn remove_unneeded_top_level_fragment_directives( } // We can skip some of the fragment directives directive. - let final_selection = - inline_fragment.with_updated_directives(DirectiveList(needed_directives)); + let final_selection = inline_fragment + .with_updated_directives(DirectiveList::from_iter(needed_directives)); selection_map.insert(Selection::InlineFragment(Arc::new(final_selection))); } } @@ -308,19 +308,17 @@ fn remove_conditions_of_element( element: OpPathElement, conditions: &VariableConditions, ) -> OpPathElement { - let updated_directives: DirectiveList = DirectiveList( - element - .directives() - .iter() - .filter(|d| { - !matches_condition_for_kind(d, conditions, ConditionKind::Include) - && !matches_condition_for_kind(d, conditions, ConditionKind::Skip) - }) - .cloned() - .collect(), - ); + let updated_directives: DirectiveList = element + .directives() + .iter() + .filter(|d| { + !matches_condition_for_kind(d, conditions, ConditionKind::Include) + && !matches_condition_for_kind(d, conditions, ConditionKind::Skip) + }) + .cloned() + .collect(); - if updated_directives.0.len() == element.directives().len() { + if updated_directives.len() == element.directives().len() { element } else { element.with_updated_directives(updated_directives) diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index d14e57bad1..dfc862e23b 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -12,7 +12,6 @@ use apollo_compiler::ast::Type; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; -use apollo_compiler::executable::DirectiveList; use apollo_compiler::executable::VariableDefinition; use apollo_compiler::name; use apollo_compiler::schema; @@ -31,7 +30,9 @@ use super::query_planner::SubgraphOperationCompression; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::graphql_definition::DeferDirectiveArguments; +use crate::operation::ArgumentList; use crate::operation::ContainmentOptions; +use crate::operation::DirectiveList; use crate::operation::Field; use crate::operation::FieldData; use crate::operation::InlineFragment; @@ -44,8 +45,6 @@ use crate::operation::SelectionMap; use crate::operation::SelectionSet; use crate::operation::VariableCollector; use crate::operation::TYPENAME_FIELD; -use crate::query_graph::extract_subgraphs_from_supergraph::FEDERATION_REPRESENTATIONS_ARGUMENTS_NAME; -use crate::query_graph::extract_subgraphs_from_supergraph::FEDERATION_REPRESENTATIONS_VAR_NAME; use crate::query_graph::graph_path::concat_op_paths; use crate::query_graph::graph_path::concat_paths_in_parents; use crate::query_graph::graph_path::OpGraphPathContext; @@ -74,6 +73,8 @@ use crate::schema::position::TypeDefinitionPosition; use crate::schema::ValidFederationSchema; use crate::subgraph::spec::ANY_SCALAR_NAME; use crate::subgraph::spec::ENTITIES_QUERY; +use crate::supergraph::FEDERATION_REPRESENTATIONS_ARGUMENTS_NAME; +use crate::supergraph::FEDERATION_REPRESENTATIONS_VAR_NAME; use crate::utils::logging::snapshot; /// Represents the value of a `@defer(label:)` argument. @@ -2407,7 +2408,7 @@ impl FetchDependencyGraphNode { query_graph: &QueryGraph, handled_conditions: &Conditions, variable_definitions: &[Node], - operation_directives: &Arc, + operation_directives: &DirectiveList, operation_compression: &mut SubgraphOperationCompression, operation_name: Option, ) -> Result, FederationError> { @@ -2708,7 +2709,7 @@ fn operation_for_entities_fetch( subgraph_schema: &ValidFederationSchema, selection_set: SelectionSet, mut variable_definitions: Vec>, - operation_directives: &Arc, + operation_directives: &DirectiveList, operation_name: &Option, ) -> Result { variable_definitions.insert(0, representations_variable_definition(subgraph_schema)?); @@ -2746,11 +2747,10 @@ fn operation_for_entities_fetch( schema: subgraph_schema.clone(), field_position: entities, alias: None, - arguments: Arc::new(vec![executable::Argument { - name: FEDERATION_REPRESENTATIONS_ARGUMENTS_NAME, - value: executable::Value::Variable(FEDERATION_REPRESENTATIONS_VAR_NAME).into(), - } - .into()]), + arguments: ArgumentList::one(( + FEDERATION_REPRESENTATIONS_ARGUMENTS_NAME, + executable::Value::Variable(FEDERATION_REPRESENTATIONS_VAR_NAME), + )), directives: Default::default(), sibling_typename: None, })), @@ -2775,7 +2775,7 @@ fn operation_for_entities_fetch( root_kind: SchemaRootDefinitionKind::Query, name: operation_name.clone(), variables: Arc::new(variable_definitions), - directives: Arc::clone(operation_directives), + directives: operation_directives.clone(), selection_set, named_fragments: Default::default(), }) @@ -2786,7 +2786,7 @@ fn operation_for_query_fetch( root_kind: SchemaRootDefinitionKind, selection_set: SelectionSet, variable_definitions: Vec>, - operation_directives: &Arc, + operation_directives: &DirectiveList, operation_name: &Option, ) -> Result { Ok(Operation { @@ -2794,7 +2794,7 @@ fn operation_for_query_fetch( root_kind, name: operation_name.clone(), variables: Arc::new(variable_definitions), - directives: Arc::clone(operation_directives), + directives: operation_directives.clone(), selection_set, named_fragments: Default::default(), }) @@ -3715,7 +3715,7 @@ fn wrap_selection_with_type_and_conditions( schema: supergraph_schema.clone(), parent_type_position: wrapping_type.clone(), type_condition_position: Some(type_condition.clone()), - directives: Arc::new([directive].into_iter().collect()), + directives: [directive].into_iter().collect(), selection_id: SelectionId::new(), }), acc, diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs b/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs index 75f945e2b2..ab126dbcc6 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs @@ -1,13 +1,13 @@ use std::sync::Arc; use apollo_compiler::collections::IndexSet; -use apollo_compiler::executable::DirectiveList; use apollo_compiler::executable::VariableDefinition; use apollo_compiler::Name; use apollo_compiler::Node; use super::query_planner::SubgraphOperationCompression; use crate::error::FederationError; +use crate::operation::DirectiveList; use crate::operation::SelectionSet; use crate::query_graph::QueryGraph; use crate::query_plan::conditions::Conditions; @@ -47,7 +47,7 @@ const PIPELINING_COST: QueryPlanCost = 100.0; pub(crate) struct FetchDependencyGraphToQueryPlanProcessor { variable_definitions: Arc>>, - operation_directives: Arc, + operation_directives: DirectiveList, operation_compression: SubgraphOperationCompression, operation_name: Option, assigned_defer_labels: Option>, @@ -245,7 +245,7 @@ fn sequence_cost(values: impl IntoIterator) -> QueryPlanCo impl FetchDependencyGraphToQueryPlanProcessor { pub(crate) fn new( variable_definitions: Arc>>, - operation_directives: Arc, + operation_directives: DirectiveList, operation_compression: SubgraphOperationCompression, operation_name: Option, assigned_defer_labels: Option>, 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-federation/src/sources/connect/expand/carryover.rs b/apollo-federation/src/sources/connect/expand/carryover.rs index 980a9af879..53e594bc04 100644 --- a/apollo-federation/src/sources/connect/expand/carryover.rs +++ b/apollo-federation/src/sources/connect/expand/carryover.rs @@ -443,8 +443,8 @@ mod tests { use super::carryover_directives; use crate::merge::merge_federation_subgraphs; - use crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph; use crate::schema::FederationSchema; + use crate::supergraph::extract_subgraphs_from_supergraph; #[test] fn test_carryover() { diff --git a/apollo-federation/src/sources/connect/expand/mod.rs b/apollo-federation/src/sources/connect/expand/mod.rs index 4dbc182c23..80331bb770 100644 --- a/apollo-federation/src/sources/connect/expand/mod.rs +++ b/apollo-federation/src/sources/connect/expand/mod.rs @@ -198,7 +198,6 @@ mod helpers { use crate::error::FederationError; use crate::link::spec::Identity; use crate::link::Link; - use crate::query_graph::extract_subgraphs_from_supergraph::new_empty_fed_2_subgraph_schema; use crate::schema::position::ObjectFieldDefinitionPosition; use crate::schema::position::ObjectOrInterfaceTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; @@ -217,6 +216,7 @@ mod helpers { use crate::subgraph::spec::EXTERNAL_DIRECTIVE_NAME; use crate::subgraph::spec::KEY_DIRECTIVE_NAME; use crate::subgraph::spec::REQUIRES_DIRECTIVE_NAME; + use crate::supergraph::new_empty_fed_2_subgraph_schema; use crate::ValidFederationSubgraph; /// A helper struct for expanding a subgraph into one per connect directive. diff --git a/apollo-federation/src/sources/connect/models.rs b/apollo-federation/src/sources/connect/models.rs index 422be9ce90..b97a83b95a 100644 --- a/apollo-federation/src/sources/connect/models.rs +++ b/apollo-federation/src/sources/connect/models.rs @@ -231,8 +231,8 @@ mod tests { use insta::assert_debug_snapshot; use super::*; - use crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph; use crate::schema::FederationSchema; + use crate::supergraph::extract_subgraphs_from_supergraph; use crate::ValidFederationSubgraphs; static SIMPLE_SUPERGRAPH: &str = include_str!("./tests/schemas/simple.graphql"); diff --git a/apollo-federation/src/sources/connect/spec/directives.rs b/apollo-federation/src/sources/connect/spec/directives.rs index d33b052902..db1e04da4e 100644 --- a/apollo-federation/src/sources/connect/spec/directives.rs +++ b/apollo-federation/src/sources/connect/spec/directives.rs @@ -389,11 +389,11 @@ mod tests { use apollo_compiler::name; use apollo_compiler::Schema; - use crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph; use crate::schema::FederationSchema; use crate::sources::connect::spec::schema::SourceDirectiveArguments; use crate::sources::connect::spec::schema::CONNECT_DIRECTIVE_NAME_IN_SPEC; use crate::sources::connect::spec::schema::SOURCE_DIRECTIVE_NAME_IN_SPEC; + use crate::supergraph::extract_subgraphs_from_supergraph; use crate::ValidFederationSubgraphs; static SIMPLE_SUPERGRAPH: &str = include_str!("../tests/schemas/simple.graphql"); diff --git a/apollo-federation/src/query_graph/extract_subgraphs_from_supergraph.rs b/apollo-federation/src/supergraph/mod.rs similarity index 94% rename from apollo-federation/src/query_graph/extract_subgraphs_from_supergraph.rs rename to apollo-federation/src/supergraph/mod.rs index 2512c076c1..16628f3cbb 100644 --- a/apollo-federation/src/query_graph/extract_subgraphs_from_supergraph.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -1,5 +1,6 @@ -use std::collections::BTreeMap; -use std::fmt; +mod schema; +mod subgraph; + use std::fmt::Write; use std::ops::Deref; use std::sync::Arc; @@ -27,7 +28,6 @@ use apollo_compiler::schema::InterfaceType; use apollo_compiler::schema::NamedType; use apollo_compiler::schema::ObjectType; use apollo_compiler::schema::ScalarType; -use apollo_compiler::schema::SchemaBuilder; use apollo_compiler::schema::Type; use apollo_compiler::schema::UnionType; use apollo_compiler::validation::Valid; @@ -37,6 +37,12 @@ use itertools::Itertools; use lazy_static::lazy_static; use time::OffsetDateTime; +use self::schema::get_apollo_directive_names; +pub(crate) use self::schema::new_empty_fed_2_subgraph_schema; +use self::subgraph::FederationSubgraph; +use self::subgraph::FederationSubgraphs; +pub use self::subgraph::ValidFederationSubgraph; +pub use self::subgraph::ValidFederationSubgraphs; use crate::error::FederationError; use crate::error::MultipleFederationErrors; use crate::error::SingleFederationError; @@ -49,9 +55,7 @@ use crate::link::join_spec_definition::JoinSpecDefinition; use crate::link::join_spec_definition::TypeDirectiveArguments; use crate::link::spec::Identity; use crate::link::spec::Version; -use crate::link::spec::APOLLO_SPEC_DOMAIN; use crate::link::spec_definition::SpecDefinition; -use crate::link::Link; use crate::link::DEFAULT_LINK_NAME; use crate::schema::field_set::parse_field_set_without_normalization; use crate::schema::position::is_graphql_reserved_name; @@ -76,7 +80,6 @@ use crate::schema::type_and_directive_specification::ScalarTypeSpecification; use crate::schema::type_and_directive_specification::TypeAndDirectiveSpecification; use crate::schema::type_and_directive_specification::UnionTypeSpecification; use crate::schema::FederationSchema; -use crate::schema::ValidFederationSchema; use crate::sources::connect::ConnectSpecDefinition; /// Assumes the given schema has been validated. @@ -231,70 +234,6 @@ fn collect_empty_subgraphs( )) } -/// TODO: Use the JS/programmatic approach instead of hard-coding definitions. -pub(crate) fn new_empty_fed_2_subgraph_schema() -> Result { - let builder = SchemaBuilder::new().adopt_orphan_extensions(); - let builder = builder.parse( - r#" - extend schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.9") - - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - - 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 - } - - directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE - - directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION - - directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION - - directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION - - directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA - - directive @federation__extends on OBJECT | INTERFACE - - directive @federation__shareable on OBJECT | FIELD_DEFINITION - - directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION - - directive @federation__override(from: String!, label: String) on FIELD_DEFINITION - - directive @federation__composeDirective(name: String) repeatable on SCHEMA - - directive @federation__interfaceObject on OBJECT - - directive @federation__authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM - - directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM - - directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR - - directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION - - scalar federation__FieldSet - - scalar federation__Scope - "#, - "subgraph.graphql", - ); - FederationSchema::new(builder.build()?) -} - struct TypeInfo { name: NamedType, // IndexMap @@ -309,43 +248,6 @@ struct TypeInfos { input_object_types: Vec, } -/// Builds a map of original name to new name for Apollo feature directives. This is -/// used to handle cases where a directive is renamed via an import statement. For -/// example, importing a directive with a custom name like -/// ```graphql -/// @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }]) -/// ``` -/// results in a map entry of `cost -> renamedCost` with the `@` prefix removed. -/// -/// If the directive is imported under its default name, that also results in an entry. So, -/// ```graphql -/// @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) -/// ``` -/// results in a map entry of `cost -> cost`. This duals as a way to check if a directive -/// is included in the supergraph schema. -/// -/// **Important:** This map does _not_ include directives imported from identities other -/// than `specs.apollo.dev`. This helps us avoid extracting directives to subgraphs -/// when a custom directive's name conflicts with that of a default one. -fn get_apollo_directive_names( - supergraph_schema: &FederationSchema, -) -> Result, FederationError> { - let mut hm: IndexMap = IndexMap::default(); - for directive in &supergraph_schema.schema().schema_definition.directives { - if directive.name.as_str() == "link" { - if let Ok(link) = Link::from_directive_application(directive) { - if link.url.identity.domain != APOLLO_SPEC_DOMAIN { - continue; - } - for import in link.imports { - hm.insert(import.element.clone(), import.imported_name().clone()); - } - } - } - } - Ok(hm) -} - fn extract_subgraphs_from_fed_2_supergraph( supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, @@ -1655,105 +1557,6 @@ fn get_subgraph<'subgraph>( }) } -struct FederationSubgraph { - name: String, - url: String, - schema: FederationSchema, -} - -struct FederationSubgraphs { - subgraphs: BTreeMap, -} - -impl FederationSubgraphs { - fn new() -> Self { - FederationSubgraphs { - subgraphs: BTreeMap::new(), - } - } - - fn add(&mut self, subgraph: FederationSubgraph) -> Result<(), FederationError> { - if self.subgraphs.contains_key(&subgraph.name) { - return Err(SingleFederationError::InvalidFederationSupergraph { - message: format!("A subgraph named \"{}\" already exists", subgraph.name), - } - .into()); - } - self.subgraphs.insert(subgraph.name.clone(), subgraph); - Ok(()) - } - - fn get(&self, name: &str) -> Option<&FederationSubgraph> { - self.subgraphs.get(name) - } - - fn get_mut(&mut self, name: &str) -> Option<&mut FederationSubgraph> { - self.subgraphs.get_mut(name) - } -} - -impl IntoIterator for FederationSubgraphs { - type Item = as IntoIterator>::Item; - type IntoIter = as IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.subgraphs.into_iter() - } -} - -// TODO(@goto-bus-stop): consider an appropriate name for this in the public API -// TODO(@goto-bus-stop): should this exist separately from the `crate::subgraph::Subgraph` type? -#[derive(Debug, Clone)] -pub struct ValidFederationSubgraph { - pub name: String, - pub url: String, - pub schema: ValidFederationSchema, -} - -pub struct ValidFederationSubgraphs { - subgraphs: BTreeMap, ValidFederationSubgraph>, -} - -impl fmt::Debug for ValidFederationSubgraphs { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("ValidFederationSubgraphs ")?; - f.debug_map().entries(self.subgraphs.iter()).finish() - } -} - -impl ValidFederationSubgraphs { - pub(crate) fn new() -> Self { - ValidFederationSubgraphs { - subgraphs: BTreeMap::new(), - } - } - - pub(crate) fn add(&mut self, subgraph: ValidFederationSubgraph) -> Result<(), FederationError> { - if self.subgraphs.contains_key(subgraph.name.as_str()) { - return Err(SingleFederationError::InvalidFederationSupergraph { - message: format!("A subgraph named \"{}\" already exists", subgraph.name), - } - .into()); - } - self.subgraphs - .insert(subgraph.name.as_str().into(), subgraph); - Ok(()) - } - - pub fn get(&self, name: &str) -> Option<&ValidFederationSubgraph> { - self.subgraphs.get(name) - } -} - -impl IntoIterator for ValidFederationSubgraphs { - type Item = , ValidFederationSubgraph> as IntoIterator>::Item; - type IntoIter = , ValidFederationSubgraph> as IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.subgraphs.into_iter() - } -} - lazy_static! { static ref EXECUTABLE_DIRECTIVE_LOCATIONS: IndexSet = { [ diff --git a/apollo-federation/src/supergraph/schema.rs b/apollo-federation/src/supergraph/schema.rs new file mode 100644 index 0000000000..589131f633 --- /dev/null +++ b/apollo-federation/src/supergraph/schema.rs @@ -0,0 +1,109 @@ +use apollo_compiler::collections::IndexMap; +use apollo_compiler::schema::SchemaBuilder; +use apollo_compiler::Name; + +use crate::error::FederationError; +use crate::link::spec::APOLLO_SPEC_DOMAIN; +use crate::link::Link; +use crate::schema::FederationSchema; + +/// Builds a map of original name to new name for Apollo feature directives. This is +/// used to handle cases where a directive is renamed via an import statement. For +/// example, importing a directive with a custom name like +/// ```graphql +/// @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }]) +/// ``` +/// results in a map entry of `cost -> renamedCost` with the `@` prefix removed. +/// +/// If the directive is imported under its default name, that also results in an entry. So, +/// ```graphql +/// @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) +/// ``` +/// results in a map entry of `cost -> cost`. This duals as a way to check if a directive +/// is included in the supergraph schema. +/// +/// **Important:** This map does _not_ include directives imported from identities other +/// than `specs.apollo.dev`. This helps us avoid extracting directives to subgraphs +/// when a custom directive's name conflicts with that of a default one. +pub(super) fn get_apollo_directive_names( + supergraph_schema: &FederationSchema, +) -> Result, FederationError> { + let mut hm: IndexMap = IndexMap::default(); + for directive in &supergraph_schema.schema().schema_definition.directives { + if directive.name.as_str() == "link" { + if let Ok(link) = Link::from_directive_application(directive) { + if link.url.identity.domain != APOLLO_SPEC_DOMAIN { + continue; + } + for import in link.imports { + hm.insert(import.element.clone(), import.imported_name().clone()); + } + } + } + } + Ok(hm) +} + +/// TODO: Use the JS/programmatic approach instead of hard-coding definitions. +pub(crate) fn new_empty_fed_2_subgraph_schema() -> Result { + let builder = SchemaBuilder::new().adopt_orphan_extensions(); + let builder = builder.parse( + r#" + extend schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/federation/v2.9") + + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + + 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 + } + + directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + + directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION + + directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION + + directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION + + directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + + directive @federation__extends on OBJECT | INTERFACE + + directive @federation__shareable on OBJECT | FIELD_DEFINITION + + directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + + directive @federation__override(from: String!, label: String) on FIELD_DEFINITION + + directive @federation__composeDirective(name: String) repeatable on SCHEMA + + directive @federation__interfaceObject on OBJECT + + directive @federation__authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + + directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + + directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR + + directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION + + scalar federation__FieldSet + + scalar federation__Scope + "#, + "subgraph.graphql", + ); + FederationSchema::new(builder.build()?) +} diff --git a/apollo-federation/src/supergraph/subgraph.rs b/apollo-federation/src/supergraph/subgraph.rs new file mode 100644 index 0000000000..7697d3b569 --- /dev/null +++ b/apollo-federation/src/supergraph/subgraph.rs @@ -0,0 +1,107 @@ +use std::collections::BTreeMap; +use std::fmt; +use std::sync::Arc; + +use crate::error::FederationError; +use crate::error::SingleFederationError; +use crate::schema::FederationSchema; +use crate::schema::ValidFederationSchema; + +pub(super) struct FederationSubgraph { + pub(super) name: String, + pub(super) url: String, + pub(super) schema: FederationSchema, +} + +pub(super) struct FederationSubgraphs { + pub(super) subgraphs: BTreeMap, +} + +impl FederationSubgraphs { + pub(super) fn new() -> Self { + FederationSubgraphs { + subgraphs: BTreeMap::new(), + } + } + + pub(super) fn add(&mut self, subgraph: FederationSubgraph) -> Result<(), FederationError> { + if self.subgraphs.contains_key(&subgraph.name) { + return Err(SingleFederationError::InvalidFederationSupergraph { + message: format!("A subgraph named \"{}\" already exists", subgraph.name), + } + .into()); + } + self.subgraphs.insert(subgraph.name.clone(), subgraph); + Ok(()) + } + + fn get(&self, name: &str) -> Option<&FederationSubgraph> { + self.subgraphs.get(name) + } + + pub(super) fn get_mut(&mut self, name: &str) -> Option<&mut FederationSubgraph> { + self.subgraphs.get_mut(name) + } +} + +impl IntoIterator for FederationSubgraphs { + type Item = as IntoIterator>::Item; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.subgraphs.into_iter() + } +} + +// TODO(@goto-bus-stop): consider an appropriate name for this in the public API +// TODO(@goto-bus-stop): should this exist separately from the `crate::subgraph::Subgraph` type? +#[derive(Debug, Clone)] +pub struct ValidFederationSubgraph { + pub name: String, + pub url: String, + pub schema: ValidFederationSchema, +} + +pub struct ValidFederationSubgraphs { + pub(super) subgraphs: BTreeMap, ValidFederationSubgraph>, +} + +impl fmt::Debug for ValidFederationSubgraphs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ValidFederationSubgraphs ")?; + f.debug_map().entries(self.subgraphs.iter()).finish() + } +} + +impl ValidFederationSubgraphs { + pub(crate) fn new() -> Self { + ValidFederationSubgraphs { + subgraphs: BTreeMap::new(), + } + } + + pub(crate) fn add(&mut self, subgraph: ValidFederationSubgraph) -> Result<(), FederationError> { + if self.subgraphs.contains_key(subgraph.name.as_str()) { + return Err(SingleFederationError::InvalidFederationSupergraph { + message: format!("A subgraph named \"{}\" already exists", subgraph.name), + } + .into()); + } + self.subgraphs + .insert(subgraph.name.as_str().into(), subgraph); + Ok(()) + } + + pub fn get(&self, name: &str) -> Option<&ValidFederationSubgraph> { + self.subgraphs.get(name) + } +} + +impl IntoIterator for ValidFederationSubgraphs { + type Item = , ValidFederationSubgraph> as IntoIterator>::Item; + type IntoIter = , ValidFederationSubgraph> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.subgraphs.into_iter() + } +} diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 1d301f10dd..a57461c068 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -198,7 +198,7 @@ regex = "1.10.5" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.27+v2.8.1" +router-bridge = "=0.5.30+v2.8.3" rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" diff --git a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap index e60d87a783..6d6e785101 100644 --- a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap +++ b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap @@ -21,6 +21,10 @@ expression: parts "errors": [ { "message": "couldn't find mock for query {\"query\":\"query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{__typename id product{__typename upc}}}}}\",\"operationName\":\"TopProducts__reviews__1\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", + "path": [ + "topProducts", + "@" + ], "extensions": { "code": "FETCH_ERROR" } diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index 1334b015ab..64fef38b36 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/plugins/connectors/tests.rs b/apollo-router/src/plugins/connectors/tests.rs index 2a09b8f564..b1f4e64b14 100644 --- a/apollo-router/src/plugins/connectors/tests.rs +++ b/apollo-router/src/plugins/connectors/tests.rs @@ -276,6 +276,10 @@ async fn max_requests() { "errors": [ { "message": "Request limit exceeded", + "path": [ + "users", + "@" + ], "extensions": { "service": "connectors.json http: GET /users/{$args.id!}", "code": "REQUEST_LIMIT_EXCEEDED" @@ -342,6 +346,10 @@ async fn source_max_requests() { "errors": [ { "message": "Request limit exceeded", + "path": [ + "users", + "@" + ], "extensions": { "service": "connectors.json http: GET /users/{$args.id!}", "code": "REQUEST_LIMIT_EXCEEDED" @@ -539,6 +547,7 @@ async fn basic_errors() { "errors": [ { "message": "HTTP fetch failed from 'connectors.json http: GET /users': 404: Not Found", + "path": [], "extensions": { "code": "SUBREQUEST_HTTP_ERROR", "service": "connectors.json http: GET /users", diff --git a/apollo-router/src/plugins/csrf.rs b/apollo-router/src/plugins/csrf.rs index 6e0f08e118..5c76c57116 100644 --- a/apollo-router/src/plugins/csrf.rs +++ b/apollo-router/src/plugins/csrf.rs @@ -1,5 +1,6 @@ //! Cross Site Request Forgery (CSRF) plugin. use std::ops::ControlFlow; +use std::sync::Arc; use http::header; use http::HeaderMap; @@ -35,14 +36,14 @@ pub(crate) struct CSRFConfig { /// - did not set any `allow_headers` list (so it defaults to `mirror_request`) /// - added your required headers to the allow_headers list, as shown in the /// `examples/cors-and-csrf/custom-headers.router.yaml` files. - required_headers: Vec, + required_headers: Arc>, } -fn apollo_custom_preflight_headers() -> Vec { - vec![ +fn apollo_custom_preflight_headers() -> Arc> { + Arc::new(vec![ "x-apollo-operation-name".to_string(), "apollo-require-preflight".to_string(), - ] + ]) } impl Default for CSRFConfig { diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs index b3f3afe372..c4dcc36b00 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs @@ -1,13 +1,112 @@ +use ahash::HashMap; +use ahash::HashMapExt; +use ahash::HashSet; +use apollo_compiler::ast::DirectiveList; +use apollo_compiler::ast::FieldDefinition; +use apollo_compiler::ast::InputValueDefinition; use apollo_compiler::ast::NamedType; use apollo_compiler::executable::Field; use apollo_compiler::executable::SelectionSet; +use apollo_compiler::name; use apollo_compiler::parser::Parser; +use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; +use apollo_compiler::Name; use apollo_compiler::Schema; +use apollo_federation::link::spec::APOLLO_SPEC_DOMAIN; +use apollo_federation::link::Link; use tower::BoxError; use super::DemandControlError; +const COST_DIRECTIVE_NAME: Name = name!("cost"); +const COST_DIRECTIVE_DEFAULT_NAME: Name = name!("federation__cost"); +const COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME: Name = name!("weight"); + +const LIST_SIZE_DIRECTIVE_NAME: Name = name!("listSize"); +const LIST_SIZE_DIRECTIVE_DEFAULT_NAME: Name = name!("federation__listSize"); +const LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME: Name = name!("assumedSize"); +const LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME: Name = name!("slicingArguments"); +const LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME: Name = name!("sizedFields"); +const LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME: Name = + name!("requireOneSlicingArgument"); + +pub(in crate::plugins::demand_control) fn get_apollo_directive_names( + schema: &Schema, +) -> HashMap { + let mut hm: HashMap = HashMap::new(); + for directive in &schema.schema_definition.directives { + if directive.name.as_str() == "link" { + if let Ok(link) = Link::from_directive_application(directive) { + if link.url.identity.domain != APOLLO_SPEC_DOMAIN { + continue; + } + for import in link.imports { + hm.insert(import.element.clone(), import.imported_name().clone()); + } + } + } + } + hm +} + +pub(in crate::plugins::demand_control) struct CostDirective { + weight: i32, +} + +impl CostDirective { + pub(in crate::plugins::demand_control) fn weight(&self) -> f64 { + self.weight as f64 + } + + pub(in crate::plugins::demand_control) fn from_argument( + directive_name_map: &HashMap, + argument: &InputValueDefinition, + ) -> Option { + Self::from_directives(directive_name_map, &argument.directives) + } + + pub(in crate::plugins::demand_control) fn from_field( + directive_name_map: &HashMap, + field: &FieldDefinition, + ) -> Option { + Self::from_directives(directive_name_map, &field.directives) + } + + pub(in crate::plugins::demand_control) fn from_type( + directive_name_map: &HashMap, + ty: &ExtendedType, + ) -> Option { + Self::from_schema_directives(directive_name_map, ty.directives()) + } + + fn from_directives( + directive_name_map: &HashMap, + directives: &DirectiveList, + ) -> Option { + directive_name_map + .get(&COST_DIRECTIVE_NAME) + .and_then(|name| directives.get(name)) + .or(directives.get(&COST_DIRECTIVE_DEFAULT_NAME)) + .and_then(|cost| cost.argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) + .and_then(|weight| weight.to_i32()) + .map(|weight| Self { weight }) + } + + pub(in crate::plugins::demand_control) fn from_schema_directives( + directive_name_map: &HashMap, + directives: &apollo_compiler::schema::DirectiveList, + ) -> Option { + directive_name_map + .get(&COST_DIRECTIVE_NAME) + .and_then(|name| directives.get(name)) + .or(directives.get(&COST_DIRECTIVE_DEFAULT_NAME)) + .and_then(|cost| cost.argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) + .and_then(|weight| weight.to_i32()) + .map(|weight| Self { weight }) + } +} + pub(in crate::plugins::demand_control) struct IncludeDirective { pub(in crate::plugins::demand_control) is_included: bool, } @@ -27,31 +126,142 @@ impl IncludeDirective { } } +pub(in crate::plugins::demand_control) struct ListSizeDirective<'schema> { + pub(in crate::plugins::demand_control) expected_size: Option, + pub(in crate::plugins::demand_control) sized_fields: Option>, +} + +impl<'schema> ListSizeDirective<'schema> { + pub(in crate::plugins::demand_control) fn size_of(&self, field: &Field) -> Option { + if self + .sized_fields + .as_ref() + .is_some_and(|sf| sf.contains(field.name.as_str())) + { + self.expected_size + } else { + None + } + } +} + +/// The `@listSize` directive from a field definition, which can be converted to +/// `ListSizeDirective` with a concrete field from a request. +pub(in crate::plugins::demand_control) struct DefinitionListSizeDirective { + assumed_size: Option, + slicing_argument_names: Option>, + sized_fields: Option>, + require_one_slicing_argument: bool, +} + +impl DefinitionListSizeDirective { + pub(in crate::plugins::demand_control) fn from_field_definition( + directive_name_map: &HashMap, + definition: &FieldDefinition, + ) -> Result, DemandControlError> { + let directive = directive_name_map + .get(&LIST_SIZE_DIRECTIVE_NAME) + .and_then(|name| definition.directives.get(name)) + .or(definition.directives.get(&LIST_SIZE_DIRECTIVE_DEFAULT_NAME)); + if let Some(directive) = directive { + let assumed_size = directive + .argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME) + .and_then(|arg| arg.to_i32()); + let slicing_argument_names = directive + .argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME) + .and_then(|arg| arg.as_list()) + .map(|arg_list| { + arg_list + .iter() + .flat_map(|arg| arg.as_str()) + .map(String::from) + .collect() + }); + let sized_fields = directive + .argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME) + .and_then(|arg| arg.as_list()) + .map(|arg_list| { + arg_list + .iter() + .flat_map(|arg| arg.as_str()) + .map(String::from) + .collect() + }); + let require_one_slicing_argument = directive + .argument_by_name(&LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME) + .and_then(|arg| arg.to_bool()) + .unwrap_or(true); + + Ok(Some(Self { + assumed_size, + slicing_argument_names, + sized_fields, + require_one_slicing_argument, + })) + } else { + Ok(None) + } + } + + pub(in crate::plugins::demand_control) fn with_field( + &self, + field: &Field, + ) -> Result { + let mut slicing_arguments: HashMap<&str, i32> = HashMap::new(); + if let Some(slicing_argument_names) = self.slicing_argument_names.as_ref() { + // First, collect the default values for each argument + for argument in &field.definition.arguments { + if slicing_argument_names.contains(argument.name.as_str()) { + if let Some(numeric_value) = + argument.default_value.as_ref().and_then(|v| v.to_i32()) + { + slicing_arguments.insert(&argument.name, numeric_value); + } + } + } + // Then, overwrite any default values with the actual values passed in the query + for argument in &field.arguments { + if slicing_argument_names.contains(argument.name.as_str()) { + if let Some(numeric_value) = argument.value.to_i32() { + slicing_arguments.insert(&argument.name, numeric_value); + } + } + } + + if self.require_one_slicing_argument && slicing_arguments.len() != 1 { + return Err(DemandControlError::QueryParseFailure(format!( + "Exactly one slicing argument is required, but found {}", + slicing_arguments.len() + ))); + } + } + + let expected_size = slicing_arguments + .values() + .max() + .cloned() + .or(self.assumed_size); + + Ok(ListSizeDirective { + expected_size, + sized_fields: self + .sized_fields + .as_ref() + .map(|set| set.iter().map(|s| s.as_str()).collect()), + }) + } +} + pub(in crate::plugins::demand_control) struct RequiresDirective { pub(in crate::plugins::demand_control) fields: SelectionSet, } impl RequiresDirective { - pub(in crate::plugins::demand_control) fn from_field( - field: &Field, + pub(in crate::plugins::demand_control) fn from_field_definition( + definition: &FieldDefinition, parent_type_name: &NamedType, schema: &Valid, ) -> Result, DemandControlError> { - // When a user marks a subgraph schema field with `@requires`, the composition process - // replaces `@requires(field: "")` with `@join__field(requires: "")`. - // - // Note we cannot use `field.definition` in this case: The operation executes against the - // API schema, so its definition pointers point into the API schema. To find the - // `@join__field()` directive, we must instead look up the field on the type with the same - // name in the supergraph. - let definition = schema - .type_field(parent_type_name, &field.name) - .map_err(|_err| { - DemandControlError::QueryParseFailure(format!( - "Could not find the API schema type {}.{} in the supergraph. This looks like a bug", - parent_type_name, &field.name - )) - })?; let requires_arg = definition .directives .get("join__field") diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query.graphql index 86a01356e7..c8494f9697 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query.graphql +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query.graphql @@ -1,3 +1,5 @@ query BasicInputObjectQuery { - getScalarByObject(args: { inner: { id: 1 } }) + getScalarByObject( + args: { inner: { id: 1 }, listOfInner: [{ id: 2 }, { id: 3 }] } + ) } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query_2.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query_2.graphql new file mode 100644 index 0000000000..26a1a06623 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_query_2.graphql @@ -0,0 +1,8 @@ +query BasicInputObjectQuery2 { + getObjectsByObject( + args: { inner: { id: 1 }, listOfInner: [{ id: 2 }, { id: 3 }] } + ) { + field1 + field2 + } +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_response.json b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_response.json new file mode 100644 index 0000000000..092377bf7f --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_input_object_response.json @@ -0,0 +1,9 @@ +{ + "data": { + "getObjectsByObject": [ + { "field1": 1, "field2": "one" }, + { "field1": 2, "field2": "two" }, + { "field1": 3, "field2": "three" } + ] + } +} \ No newline at end of file diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_schema.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_schema.graphql index d613012b0d..17f3046414 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_schema.graphql +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/basic_schema.graphql @@ -7,6 +7,7 @@ type Query { someUnion: UnionOfObjectTypes someObjects: [FirstObjectType] intList: [Int] + getObjectsByObject(args: OuterInput): [SecondObjectType] } type Mutation { @@ -35,4 +36,6 @@ input InnerInput { input OuterInput { inner: InnerInput + inner2: InnerInput + listOfInner: [InnerInput!] } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query.graphql new file mode 100644 index 0000000000..751c8a005e --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query.graphql @@ -0,0 +1,20 @@ +fragment Items on SizedField { + items { + id + } +} + +{ + fieldWithCost + argWithCost(arg: 3) + enumWithCost + inputWithCost(someInput: { somethingWithCost: 10 }) + scalarWithCost + objectWithCost { + id + } + fieldWithListSize + fieldWithDynamicListSize(first: 5) { + ...Items + } +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_default_slicing_argument.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_default_slicing_argument.graphql new file mode 100644 index 0000000000..fb50e08fef --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_default_slicing_argument.graphql @@ -0,0 +1,20 @@ +fragment Items on SizedField { + items { + id + } +} + +{ + fieldWithCost + argWithCost(arg: 3) + enumWithCost + inputWithCost(someInput: { somethingWithCost: 10 }) + scalarWithCost + objectWithCost { + id + } + fieldWithListSize + fieldWithDynamicListSize { + ...Items + } +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_response.json b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_response.json new file mode 100644 index 0000000000..664a2684e6 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_response.json @@ -0,0 +1,24 @@ +{ + "data": { + "fieldWithCost": 1, + "argWithCost": 2, + "enumWithCost": "A", + "inputWithCost": 3, + "scalarWithCost": 4, + "objectWithCost": { + "id": 5 + }, + "fieldWithListSize": [ + "first", + "second", + "third" + ], + "fieldWithDynamicListSize": { + "items": [ + { "id": 6 }, + { "id": 7 }, + { "id": 8 } + ] + } + } +} \ No newline at end of file diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_schema.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_schema.graphql new file mode 100644 index 0000000000..d966512be1 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_schema.graphql @@ -0,0 +1,154 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link( + url: "https://specs.apollo.dev/cost/v0.1" + import: ["@cost", "@listSize"] + ) { + query: Query +} + +directive @cost( + weight: Int! +) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR + +directive @cost__listSize( + assumedSize: Int + slicingArguments: [String!] + sizedFields: [String!] + requireOneSlicingArgument: Boolean = true +) on FIELD_DEFINITION + +directive @join__directive( + graphs: [join__Graph!] + name: String! + args: join__DirectiveArguments +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + overrideLabel: String + contextArguments: [join__ContextArgument!] +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @listSize( + assumedSize: Int + slicingArguments: [String!] + sizedFields: [String!] + requireOneSlicingArgument: Boolean = true +) on FIELD_DEFINITION + +type A @join__type(graph: SUBGRAPHWITHLISTSIZE) { + id: ID +} + +enum AorB @join__type(graph: SUBGRAPHWITHCOST) @cost(weight: 15) { + A @join__enumValue(graph: SUBGRAPHWITHCOST) + B @join__enumValue(graph: SUBGRAPHWITHCOST) +} + +scalar ExpensiveInt @join__type(graph: SUBGRAPHWITHCOST) @cost(weight: 30) + +type ExpensiveObject @join__type(graph: SUBGRAPHWITHCOST) @cost(weight: 40) { + id: ID +} + +input InputTypeWithCost @join__type(graph: SUBGRAPHWITHCOST) { + somethingWithCost: Int @cost(weight: 20) +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPHWITHCOST + @join__graph(name: "subgraphWithCost", url: "http://localhost:4001") + SUBGRAPHWITHLISTSIZE + @join__graph(name: "subgraphWithListSize", url: "http://localhost:4002") +} + +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 +} + +type Query + @join__type(graph: SUBGRAPHWITHCOST) + @join__type(graph: SUBGRAPHWITHLISTSIZE) { + fieldWithCost: Int @join__field(graph: SUBGRAPHWITHCOST) @cost(weight: 5) + argWithCost(arg: Int @cost(weight: 10)): Int + @join__field(graph: SUBGRAPHWITHCOST) + enumWithCost: AorB @join__field(graph: SUBGRAPHWITHCOST) + inputWithCost(someInput: InputTypeWithCost): Int + @join__field(graph: SUBGRAPHWITHCOST) + scalarWithCost: ExpensiveInt @join__field(graph: SUBGRAPHWITHCOST) + objectWithCost: ExpensiveObject @join__field(graph: SUBGRAPHWITHCOST) + fieldWithListSize: [String!] + @join__field(graph: SUBGRAPHWITHLISTSIZE) + @listSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int = 10): SizedField + @join__field(graph: SUBGRAPHWITHLISTSIZE) + @listSize( + slicingArguments: ["first"] + sizedFields: ["items"] + requireOneSlicingArgument: true + ) +} + +type SizedField @join__type(graph: SUBGRAPHWITHLISTSIZE) { + items: [A] +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_schema_with_renamed_directives.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_schema_with_renamed_directives.graphql new file mode 100644 index 0000000000..1d1f17263d --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_schema_with_renamed_directives.graphql @@ -0,0 +1,163 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link( + url: "https://specs.apollo.dev/cost/v0.1" + import: [ + { name: "@cost", as: "@renamedCost" } + { name: "@listSize", as: "@renamedListSize" } + ] + ) { + query: Query +} + +directive @cost__listSize( + assumedSize: Int + slicingArguments: [String!] + sizedFields: [String!] + requireOneSlicingArgument: Boolean = true +) on FIELD_DEFINITION + +directive @join__directive( + graphs: [join__Graph!] + name: String! + args: join__DirectiveArguments +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + overrideLabel: String + contextArguments: [join__ContextArgument!] +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @renamedCost( + weight: Int! +) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR + +directive @renamedListSize( + assumedSize: Int + slicingArguments: [String!] + sizedFields: [String!] + requireOneSlicingArgument: Boolean = true +) on FIELD_DEFINITION + +type A @join__type(graph: SUBGRAPHWITHLISTSIZE) { + id: ID +} + +enum AorB @join__type(graph: SUBGRAPHWITHCOST) @renamedCost(weight: 15) { + A @join__enumValue(graph: SUBGRAPHWITHCOST) + B @join__enumValue(graph: SUBGRAPHWITHCOST) +} + +scalar ExpensiveInt + @join__type(graph: SUBGRAPHWITHCOST) + @renamedCost(weight: 30) + +type ExpensiveObject + @join__type(graph: SUBGRAPHWITHCOST) + @renamedCost(weight: 40) { + id: ID +} + +input InputTypeWithCost @join__type(graph: SUBGRAPHWITHCOST) { + somethingWithCost: Int @renamedCost(weight: 20) +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPHWITHCOST + @join__graph(name: "subgraphWithCost", url: "http://localhost:4001") + SUBGRAPHWITHLISTSIZE + @join__graph(name: "subgraphWithListSize", url: "http://localhost:4002") +} + +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 +} + +type Query + @join__type(graph: SUBGRAPHWITHCOST) + @join__type(graph: SUBGRAPHWITHLISTSIZE) { + fieldWithCost: Int + @join__field(graph: SUBGRAPHWITHCOST) + @renamedCost(weight: 5) + argWithCost(arg: Int @renamedCost(weight: 10)): Int + @join__field(graph: SUBGRAPHWITHCOST) + enumWithCost: AorB @join__field(graph: SUBGRAPHWITHCOST) + inputWithCost(someInput: InputTypeWithCost): Int + @join__field(graph: SUBGRAPHWITHCOST) + scalarWithCost: ExpensiveInt @join__field(graph: SUBGRAPHWITHCOST) + objectWithCost: ExpensiveObject @join__field(graph: SUBGRAPHWITHCOST) + fieldWithListSize: [String!] + @join__field(graph: SUBGRAPHWITHLISTSIZE) + @renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int = 10): SizedField + @join__field(graph: SUBGRAPHWITHLISTSIZE) + @renamedListSize( + slicingArguments: ["first"] + sizedFields: ["items"] + requireOneSlicingArgument: true + ) +} + +type SizedField @join__type(graph: SUBGRAPHWITHLISTSIZE) { + items: [A] +} 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 290ce4dbe4..a534f91a94 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs @@ -1,4 +1,5 @@ mod directives; +pub(in crate::plugins::demand_control) mod schema; pub(crate) mod static_cost; use crate::plugins::demand_control::DemandControlError; diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs b/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs new file mode 100644 index 0000000000..6a46ee9fe9 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs @@ -0,0 +1,180 @@ +use std::ops::Deref; +use std::sync::Arc; + +use ahash::HashMap; +use ahash::HashMapExt; +use apollo_compiler::schema::ExtendedType; +use apollo_compiler::validation::Valid; +use apollo_compiler::Name; +use apollo_compiler::Schema; + +use super::directives::get_apollo_directive_names; +use super::directives::CostDirective; +use super::directives::DefinitionListSizeDirective as ListSizeDirective; +use super::directives::RequiresDirective; +use crate::plugins::demand_control::DemandControlError; + +pub(crate) struct DemandControlledSchema { + directive_name_map: HashMap, + inner: Arc>, + type_field_cost_directives: HashMap>, + type_field_list_size_directives: HashMap>, + type_field_requires_directives: HashMap>, +} + +impl DemandControlledSchema { + pub(crate) fn new(schema: Arc>) -> Result { + let directive_name_map = get_apollo_directive_names(&schema); + + let mut type_field_cost_directives: HashMap> = + HashMap::new(); + let mut type_field_list_size_directives: HashMap> = + HashMap::new(); + let mut type_field_requires_directives: HashMap> = + HashMap::new(); + + for (type_name, type_) in &schema.types { + let field_cost_directives = type_field_cost_directives + .entry(type_name.clone()) + .or_default(); + let field_list_size_directives = type_field_list_size_directives + .entry(type_name.clone()) + .or_default(); + let field_requires_directives = type_field_requires_directives + .entry(type_name.clone()) + .or_default(); + + match type_ { + ExtendedType::Interface(ty) => { + for field_name in ty.fields.keys() { + let field_definition = schema.type_field(type_name, field_name)?; + let field_type = schema.types.get(field_definition.ty.inner_named_type()).ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Field {} was found in query, but its type is missing from the schema.", + field_name + )) + })?; + + if let Some(cost_directive) = + CostDirective::from_field(&directive_name_map, field_definition) + .or(CostDirective::from_type(&directive_name_map, field_type)) + { + field_cost_directives.insert(field_name.clone(), cost_directive); + } + + if let Some(list_size_directive) = ListSizeDirective::from_field_definition( + &directive_name_map, + field_definition, + )? { + field_list_size_directives + .insert(field_name.clone(), list_size_directive); + } + + if let Some(requires_directive) = RequiresDirective::from_field_definition( + field_definition, + type_name, + &schema, + )? { + field_requires_directives + .insert(field_name.clone(), requires_directive); + } + } + } + ExtendedType::Object(ty) => { + for field_name in ty.fields.keys() { + let field_definition = schema.type_field(type_name, field_name)?; + let field_type = schema.types.get(field_definition.ty.inner_named_type()).ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Field {} was found in query, but its type is missing from the schema.", + field_name + )) + })?; + + if let Some(cost_directive) = + CostDirective::from_field(&directive_name_map, field_definition) + .or(CostDirective::from_type(&directive_name_map, field_type)) + { + field_cost_directives.insert(field_name.clone(), cost_directive); + } + + if let Some(list_size_directive) = ListSizeDirective::from_field_definition( + &directive_name_map, + field_definition, + )? { + field_list_size_directives + .insert(field_name.clone(), list_size_directive); + } + + if let Some(requires_directive) = RequiresDirective::from_field_definition( + field_definition, + type_name, + &schema, + )? { + field_requires_directives + .insert(field_name.clone(), requires_directive); + } + } + } + _ => { + // Other types don't have fields + } + } + } + + Ok(Self { + directive_name_map, + inner: schema, + type_field_cost_directives, + type_field_list_size_directives, + type_field_requires_directives, + }) + } + + pub(in crate::plugins::demand_control) fn directive_name_map(&self) -> &HashMap { + &self.directive_name_map + } + + pub(in crate::plugins::demand_control) fn type_field_cost_directive( + &self, + type_name: &str, + field_name: &str, + ) -> Option<&CostDirective> { + self.type_field_cost_directives + .get(type_name)? + .get(field_name) + } + + pub(in crate::plugins::demand_control) fn type_field_list_size_directive( + &self, + type_name: &str, + field_name: &str, + ) -> Option<&ListSizeDirective> { + self.type_field_list_size_directives + .get(type_name)? + .get(field_name) + } + + pub(in crate::plugins::demand_control) fn type_field_requires_directive( + &self, + type_name: &str, + field_name: &str, + ) -> Option<&RequiresDirective> { + self.type_field_requires_directives + .get(type_name)? + .get(field_name) + } +} + +impl AsRef> for DemandControlledSchema { + fn as_ref(&self) -> &Valid { + &self.inner + } +} + +impl Deref for DemandControlledSchema { + type Target = Schema; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} 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 7601ba71e5..4f2e585db3 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 @@ -1,5 +1,7 @@ use std::sync::Arc; +use ahash::HashMap; +use apollo_compiler::ast; use apollo_compiler::ast::InputValueDefinition; use apollo_compiler::ast::NamedType; use apollo_compiler::executable::ExecutableDocument; @@ -9,18 +11,19 @@ use apollo_compiler::executable::InlineFragment; use apollo_compiler::executable::Operation; use apollo_compiler::executable::Selection; use apollo_compiler::executable::SelectionSet; -use apollo_compiler::validation::Valid; -use apollo_compiler::Schema; +use apollo_compiler::schema::ExtendedType; +use apollo_compiler::Node; use serde_json_bytes::Value; use super::directives::IncludeDirective; -use super::directives::RequiresDirective; use super::directives::SkipDirective; +use super::schema::DemandControlledSchema; use super::DemandControlError; use crate::graphql::Response; use crate::graphql::ResponseVisitor; +use crate::plugins::demand_control::cost_calculator::directives::CostDirective; +use crate::plugins::demand_control::cost_calculator::directives::ListSizeDirective; use crate::query_planner::fetch::SubgraphOperation; -use crate::query_planner::fetch::SubgraphSchemas; use crate::query_planner::DeferredNode; use crate::query_planner::PlanNode; use crate::query_planner::Primary; @@ -28,13 +31,74 @@ use crate::query_planner::QueryPlan; pub(crate) struct StaticCostCalculator { list_size: u32, - subgraph_schemas: Arc, + supergraph_schema: Arc, + subgraph_schemas: Arc>, +} + +fn score_argument( + argument: &apollo_compiler::ast::Value, + argument_definition: &Node, + schema: &DemandControlledSchema, +) -> Result { + let cost_directive = + CostDirective::from_argument(schema.directive_name_map(), argument_definition); + let ty = schema + .types + .get(argument_definition.ty.inner_named_type()) + .ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Argument {} was found in query, but its type ({}) was not found in the schema", + argument_definition.name, + argument_definition.ty.inner_named_type() + )) + })?; + + match (argument, ty) { + (_, ExtendedType::Interface(_)) + | (_, ExtendedType::Object(_)) + | (_, ExtendedType::Union(_)) => Err(DemandControlError::QueryParseFailure( + format!( + "Argument {} has type {}, but objects, interfaces, and unions are disallowed in this position", + argument_definition.name, + argument_definition.ty.inner_named_type() + ) + )), + + (ast::Value::Object(inner_args), ExtendedType::InputObject(inner_arg_defs)) => { + let mut cost = cost_directive.map_or(1.0, |cost| cost.weight()); + for (arg_name, arg_val) in inner_args { + let arg_def = inner_arg_defs.fields.get(arg_name).ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Argument {} was found in query, but its type ({}) was not found in the schema", + argument_definition.name, + argument_definition.ty.inner_named_type() + )) + })?; + cost += score_argument(arg_val, arg_def, schema)?; + } + Ok(cost) + } + (ast::Value::List(inner_args), _) => { + let mut cost = cost_directive.map_or(0.0, |cost| cost.weight()); + for arg_val in inner_args { + cost += score_argument(arg_val, argument_definition, schema)?; + } + Ok(cost) + } + (ast::Value::Null, _) => Ok(0.0), + _ => Ok(cost_directive.map_or(0.0, |cost| cost.weight())) + } } impl StaticCostCalculator { - pub(crate) fn new(subgraph_schemas: Arc, list_size: u32) -> Self { + pub(crate) fn new( + supergraph_schema: Arc, + subgraph_schemas: Arc>, + list_size: u32, + ) -> Self { Self { list_size, + supergraph_schema, subgraph_schemas, } } @@ -61,14 +125,18 @@ impl StaticCostCalculator { &self, field: &Field, parent_type: &NamedType, - schema: &Valid, + schema: &DemandControlledSchema, executable: &ExecutableDocument, should_estimate_requires: bool, + list_size_from_upstream: Option, ) -> Result { if StaticCostCalculator::skipped_by_directives(field) { return Ok(0.0); } + // We need to look up the `FieldDefinition` from the supergraph schema instead of using `field.definition` + // because `field.definition` was generated from the API schema, which strips off the directives we need. + let definition = schema.type_field(parent_type, &field.name)?; let ty = field.inner_type_def(schema).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( "Field {} was found in query, but its type is missing from the schema.", @@ -76,17 +144,32 @@ impl StaticCostCalculator { )) })?; - // Determine how many instances we're scoring. If there's no user-provided - // information, assume lists have 100 items. - let instance_count = if field.ty().is_list() { - self.list_size as f64 + let list_size_directive = + match schema.type_field_list_size_directive(parent_type, &field.name) { + Some(dir) => dir.with_field(field).map(Some), + None => Ok(None), + }?; + let instance_count = if !field.ty().is_list() { + 1 + } else if let Some(value) = list_size_from_upstream { + // This is a sized field whose length is defined by the `@listSize` directive on the parent field + value + } else if let Some(expected_size) = list_size_directive + .as_ref() + .and_then(|dir| dir.expected_size) + { + expected_size } else { - 1.0 + self.list_size as i32 }; // Determine the cost for this particular field. Scalars are free, non-scalars are not. // For fields with selections, add in the cost of the selections as well. - let mut type_cost = if ty.is_interface() || ty.is_object() || ty.is_union() { + let mut type_cost = if let Some(cost_directive) = + schema.type_field_cost_directive(parent_type, &field.name) + { + cost_directive.weight() + } else if ty.is_interface() || ty.is_object() || ty.is_union() { 1.0 } else { 0.0 @@ -97,10 +180,19 @@ impl StaticCostCalculator { schema, executable, should_estimate_requires, + list_size_directive.as_ref(), )?; - for argument in &field.definition.arguments { - type_cost += Self::score_argument(argument, schema)?; + let mut arguments_cost = 0.0; + for argument in &field.arguments { + let argument_definition = + definition.argument_by_name(&argument.name).ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Argument {} of field {} is missing a definition in the schema", + argument.name, field.name + )) + })?; + arguments_cost += score_argument(&argument.value, argument_definition, schema)?; } let mut requirements_cost = 0.0; @@ -108,25 +200,28 @@ impl StaticCostCalculator { // If the field is marked with `@requires`, the required selection may not be included // in the query's selection. Adding that requirement's cost to the field ensures it's // accounted for. - let requirements = - RequiresDirective::from_field(field, parent_type, schema)?.map(|d| d.fields); + let requirements = schema + .type_field_requires_directive(parent_type, &field.name) + .map(|d| &d.fields); if let Some(selection_set) = requirements { requirements_cost = self.score_selection_set( - &selection_set, + selection_set, parent_type, schema, executable, should_estimate_requires, + list_size_directive.as_ref(), )?; } } - let cost = instance_count * type_cost + requirements_cost; + let cost = (instance_count as f64) * type_cost + arguments_cost + requirements_cost; tracing::debug!( - "Field {} cost breakdown: (count) {} * (type cost) {} + (requirements) {} = {}", + "Field {} cost breakdown: (count) {} * (type cost) {} + (arguments) {} + (requirements) {} = {}", field.name, instance_count, type_cost, + arguments_cost, requirements_cost, cost ); @@ -134,47 +229,14 @@ impl StaticCostCalculator { Ok(cost) } - fn score_argument( - argument: &InputValueDefinition, - schema: &Valid, - ) -> Result { - if let Some(ty) = schema.types.get(argument.ty.inner_named_type().as_str()) { - match ty { - apollo_compiler::schema::ExtendedType::InputObject(inner_arguments) => { - let mut cost = 1.0; - for inner_argument in inner_arguments.fields.values() { - cost += Self::score_argument(inner_argument, schema)?; - } - Ok(cost) - } - - apollo_compiler::schema::ExtendedType::Scalar(_) - | apollo_compiler::schema::ExtendedType::Enum(_) => Ok(0.0), - - apollo_compiler::schema::ExtendedType::Object(_) - | apollo_compiler::schema::ExtendedType::Interface(_) - | apollo_compiler::schema::ExtendedType::Union(_) => { - Err(DemandControlError::QueryParseFailure( - format!("Argument {} has type {}, but objects, interfaces, and unions are disallowed in this position", argument.name, argument.ty.inner_named_type()) - )) - } - } - } else { - Err(DemandControlError::QueryParseFailure(format!( - "Argument {} was found in query, but its type ({}) was not found in the schema", - argument.name, - argument.ty.inner_named_type() - ))) - } - } - fn score_fragment_spread( &self, fragment_spread: &FragmentSpread, parent_type: &NamedType, - schema: &Valid, + schema: &DemandControlledSchema, executable: &ExecutableDocument, should_estimate_requires: bool, + list_size_directive: Option<&ListSizeDirective>, ) -> Result { let fragment = fragment_spread.fragment_def(executable).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( @@ -188,6 +250,7 @@ impl StaticCostCalculator { schema, executable, should_estimate_requires, + list_size_directive, ) } @@ -195,9 +258,10 @@ impl StaticCostCalculator { &self, inline_fragment: &InlineFragment, parent_type: &NamedType, - schema: &Valid, + schema: &DemandControlledSchema, executable: &ExecutableDocument, should_estimate_requires: bool, + list_size_directive: Option<&ListSizeDirective>, ) -> Result { self.score_selection_set( &inline_fragment.selection_set, @@ -205,13 +269,14 @@ impl StaticCostCalculator { schema, executable, should_estimate_requires, + list_size_directive, ) } fn score_operation( &self, operation: &Operation, - schema: &Valid, + schema: &DemandControlledSchema, executable: &ExecutableDocument, should_estimate_requires: bool, ) -> Result { @@ -230,6 +295,7 @@ impl StaticCostCalculator { schema, executable, should_estimate_requires, + None, )?; Ok(cost) @@ -239,20 +305,27 @@ impl StaticCostCalculator { &self, selection: &Selection, parent_type: &NamedType, - schema: &Valid, + schema: &DemandControlledSchema, executable: &ExecutableDocument, should_estimate_requires: bool, + list_size_directive: Option<&ListSizeDirective>, ) -> Result { match selection { - Selection::Field(f) => { - self.score_field(f, parent_type, schema, executable, should_estimate_requires) - } + Selection::Field(f) => self.score_field( + f, + parent_type, + schema, + executable, + should_estimate_requires, + list_size_directive.and_then(|dir| dir.size_of(f)), + ), Selection::FragmentSpread(s) => self.score_fragment_spread( s, parent_type, schema, executable, should_estimate_requires, + list_size_directive, ), Selection::InlineFragment(i) => self.score_inline_fragment( i, @@ -260,6 +333,7 @@ impl StaticCostCalculator { schema, executable, should_estimate_requires, + list_size_directive, ), } } @@ -268,9 +342,10 @@ impl StaticCostCalculator { &self, selection_set: &SelectionSet, parent_type_name: &NamedType, - schema: &Valid, + schema: &DemandControlledSchema, executable: &ExecutableDocument, should_estimate_requires: bool, + list_size_directive: Option<&ListSizeDirective>, ) -> Result { let mut cost = 0.0; for selection in selection_set.selections.iter() { @@ -280,6 +355,7 @@ impl StaticCostCalculator { schema, executable, should_estimate_requires, + list_size_directive, )?; } Ok(cost) @@ -386,7 +462,7 @@ impl StaticCostCalculator { pub(crate) fn estimated( &self, query: &ExecutableDocument, - schema: &Valid, + schema: &DemandControlledSchema, should_estimate_requires: bool, ) -> Result { let mut cost = 0.0; @@ -408,39 +484,75 @@ impl StaticCostCalculator { request: &ExecutableDocument, response: &Response, ) -> Result { - let mut visitor = ResponseCostCalculator::new(); + let mut visitor = ResponseCostCalculator::new(&self.supergraph_schema); visitor.visit(request, response); Ok(visitor.cost) } } -pub(crate) struct ResponseCostCalculator { +pub(crate) struct ResponseCostCalculator<'a> { pub(crate) cost: f64, + schema: &'a DemandControlledSchema, } -impl ResponseCostCalculator { - pub(crate) fn new() -> Self { - Self { cost: 0.0 } +impl<'schema> ResponseCostCalculator<'schema> { + pub(crate) fn new(schema: &'schema DemandControlledSchema) -> Self { + Self { cost: 0.0, schema } } } -impl ResponseVisitor for ResponseCostCalculator { +impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { fn visit_field( &mut self, request: &ExecutableDocument, - _ty: &NamedType, + parent_ty: &NamedType, field: &Field, value: &Value, ) { + self.visit_list_item(request, parent_ty, field, value); + + let definition = self.schema.type_field(parent_ty, &field.name); + for argument in &field.arguments { + if let Ok(Some(argument_definition)) = definition + .as_ref() + .map(|def| def.argument_by_name(&argument.name)) + { + if let Ok(score) = score_argument(&argument.value, argument_definition, self.schema) + { + self.cost += score; + } + } else { + tracing::warn!( + "Failed to get schema definition for argument {} of field {}. The resulting actual cost will be a partial result.", + argument.name, + field.name + ) + } + } + } + + fn visit_list_item( + &mut self, + request: &apollo_compiler::ExecutableDocument, + parent_ty: &apollo_compiler::executable::NamedType, + field: &apollo_compiler::executable::Field, + value: &Value, + ) { + let cost_directive = self + .schema + .type_field_cost_directive(parent_ty, &field.name); + match value { - Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { + self.cost += cost_directive.map_or(0.0, |cost| cost.weight()); + } Value::Array(items) => { for item in items { - self.visit_field(request, field.ty().inner_named_type(), field, item); + self.visit_list_item(request, parent_ty, field, item); } } Value::Object(children) => { - self.cost += 1.0; + self.cost += cost_directive.map_or(1.0, |cost| cost.weight()); self.visit_selections(request, &field.selection_set, children); } } @@ -451,19 +563,26 @@ impl ResponseVisitor for ResponseCostCalculator { mod tests { use std::sync::Arc; + use ahash::HashMapExt; + use apollo_federation::query_plan::query_planner::QueryPlanner; use bytes::Bytes; use test_log::test; - use tower::Service; use super::*; - use crate::query_planner::BridgeQueryPlanner; use crate::services::layers::query_analysis::ParsedDocument; - use crate::services::QueryPlannerContent; - use crate::services::QueryPlannerRequest; use crate::spec; use crate::spec::Query; use crate::Configuration; - use crate::Context; + + impl StaticCostCalculator { + fn rust_planned( + &self, + query_plan: &apollo_federation::query_plan::QueryPlan, + ) -> Result { + let js_planner_node: PlanNode = query_plan.node.as_ref().unwrap().into(); + self.score_plan_node(&js_planner_node) + } + } fn parse_schema_and_operation( schema_str: &str, @@ -479,8 +598,12 @@ mod tests { fn estimated_cost(schema_str: &str, query_str: &str) -> f64 { let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); - StaticCostCalculator::new(Default::default(), 100) - .estimated(&query.executable, schema.supergraph_schema(), true) + let schema = + DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); + let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); + + calculator + .estimated(&query.executable, &calculator.supergraph_schema, true) .unwrap() } @@ -494,8 +617,11 @@ mod tests { "query.graphql", ) .unwrap(); - StaticCostCalculator::new(Default::default(), 100) - .estimated(&query, &schema, true) + let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); + let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); + + calculator + .estimated(&query, &calculator.supergraph_schema, true) .unwrap() } @@ -503,40 +629,59 @@ mod tests { let config: Arc = Arc::new(Default::default()); let (schema, query) = parse_schema_and_operation(schema_str, query_str, &config); - let mut planner = BridgeQueryPlanner::new(schema.into(), config.clone(), None, None) - .await - .unwrap(); + let planner = + QueryPlanner::new(schema.federation_supergraph(), Default::default()).unwrap(); - let ctx = Context::new(); - ctx.extensions() - .with_lock(|mut lock| lock.insert::(query)); + let query_plan = planner.build_query_plan(&query.executable, None).unwrap(); - let planner_res = planner - .call(QueryPlannerRequest::new(query_str.to_string(), None, ctx)) - .await - .unwrap(); - let query_plan = match planner_res.content.unwrap() { - QueryPlannerContent::Plan { plan } => plan, - _ => panic!("Query planner returned unexpected non-plan content"), - }; + let schema = + DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); + let mut demand_controlled_subgraph_schemas = HashMap::new(); + for (subgraph_name, subgraph_schema) in planner.subgraph_schemas().iter() { + let demand_controlled_subgraph_schema = + DemandControlledSchema::new(Arc::new(subgraph_schema.schema().clone())).unwrap(); + demand_controlled_subgraph_schemas + .insert(subgraph_name.to_string(), demand_controlled_subgraph_schema); + } - let calculator = StaticCostCalculator { - subgraph_schemas: planner.subgraph_schemas(), - list_size: 100, - }; + let calculator = StaticCostCalculator::new( + Arc::new(schema), + Arc::new(demand_controlled_subgraph_schemas), + 100, + ); - calculator.planned(&query_plan).unwrap() + calculator.rust_planned(&query_plan).unwrap() } fn actual_cost(schema_str: &str, query_str: &str, response_bytes: &'static [u8]) -> f64 { - let (_schema, query) = + let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); let response = Response::from_bytes("test", Bytes::from(response_bytes)).unwrap(); - StaticCostCalculator::new(Default::default(), 100) + let schema = + DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); + StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) .actual(&query.executable, &response) .unwrap() } + /// Actual cost of an operation on a plain, non-federated schema. + fn basic_actual_cost(schema_str: &str, query_str: &str, response_bytes: &'static [u8]) -> f64 { + let schema = + apollo_compiler::Schema::parse_and_validate(schema_str, "schema.graphqls").unwrap(); + let query = apollo_compiler::ExecutableDocument::parse_and_validate( + &schema, + query_str, + "query.graphql", + ) + .unwrap(); + let response = Response::from_bytes("test", Bytes::from(response_bytes)).unwrap(); + + let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); + StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) + .actual(&query, &response) + .unwrap() + } + #[test] fn query_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); @@ -606,7 +751,18 @@ mod tests { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_input_object_query.graphql"); - assert_eq!(basic_estimated_cost(schema, query), 2.0) + assert_eq!(basic_estimated_cost(schema, query), 4.0) + } + + #[test] + fn input_object_cost_with_returned_objects() { + let schema = include_str!("./fixtures/basic_schema.graphql"); + let query = include_str!("./fixtures/basic_input_object_query_2.graphql"); + let response = include_bytes!("./fixtures/basic_input_object_response.json"); + + assert_eq!(basic_estimated_cost(schema, query), 104.0); + // The cost of the arguments from the query should be included when scoring the response + assert_eq!(basic_actual_cost(schema, query, response), 7.0); } #[test] @@ -684,15 +840,55 @@ mod tests { 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()); + let schema = Arc::new( + DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(), + ); - let conservative_estimate = StaticCostCalculator::new(Default::default(), 100) - .estimated(&query.executable, schema.supergraph_schema(), true) + let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 100); + let conservative_estimate = calculator + .estimated(&query.executable, &calculator.supergraph_schema, true) .unwrap(); - let narrow_estimate = StaticCostCalculator::new(Default::default(), 5) - .estimated(&query.executable, schema.supergraph_schema(), true) + + let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 5); + let narrow_estimate = calculator + .estimated(&query.executable, &calculator.supergraph_schema, true) .unwrap(); assert_eq!(conservative_estimate, 10200.0); assert_eq!(narrow_estimate, 35.0); } + + #[test(tokio::test)] + async fn custom_cost_query() { + let schema = include_str!("./fixtures/custom_cost_schema.graphql"); + let query = include_str!("./fixtures/custom_cost_query.graphql"); + let response = include_bytes!("./fixtures/custom_cost_response.json"); + + assert_eq!(estimated_cost(schema, query), 127.0); + assert_eq!(planned_cost(schema, query).await, 127.0); + assert_eq!(actual_cost(schema, query, response), 125.0); + } + + #[test(tokio::test)] + async fn custom_cost_query_with_renamed_directives() { + let schema = include_str!("./fixtures/custom_cost_schema_with_renamed_directives.graphql"); + let query = include_str!("./fixtures/custom_cost_query.graphql"); + let response = include_bytes!("./fixtures/custom_cost_response.json"); + + assert_eq!(estimated_cost(schema, query), 127.0); + assert_eq!(planned_cost(schema, query).await, 127.0); + assert_eq!(actual_cost(schema, query, response), 125.0); + } + + #[test(tokio::test)] + async fn custom_cost_query_with_default_slicing_argument() { + let schema = include_str!("./fixtures/custom_cost_schema.graphql"); + let query = + include_str!("./fixtures/custom_cost_query_with_default_slicing_argument.graphql"); + let response = include_bytes!("./fixtures/custom_cost_response.json"); + + assert_eq!(estimated_cost(schema, query), 132.0); + assert_eq!(planned_cost(schema, query).await, 132.0); + assert_eq!(actual_cost(schema, query, response), 125.0); + } } diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index 476deeb737..bf0cdf5f26 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -5,6 +5,9 @@ use std::future; use std::ops::ControlFlow; use std::sync::Arc; +use ahash::HashMap; +use ahash::HashMapExt; +use apollo_compiler::schema::FieldLookupError; use apollo_compiler::validation::Valid; use apollo_compiler::validation::WithErrors; use apollo_compiler::ExecutableDocument; @@ -27,6 +30,7 @@ use crate::json_ext::Object; use crate::layers::ServiceBuilderExt; use crate::plugin::Plugin; use crate::plugin::PluginInit; +use crate::plugins::demand_control::cost_calculator::schema::DemandControlledSchema; use crate::plugins::demand_control::strategy::Strategy; use crate::plugins::demand_control::strategy::StrategyFactory; use crate::register_plugin; @@ -199,6 +203,22 @@ impl From> for DemandControlError { } } +impl<'a> From> for DemandControlError { + fn from(value: FieldLookupError) -> Self { + match value { + FieldLookupError::NoSuchType => DemandControlError::QueryParseFailure( + "Attempted to look up a type which does not exist in the schema".to_string(), + ), + FieldLookupError::NoSuchField(type_name, _) => { + DemandControlError::QueryParseFailure(format!( + "Attempted to look up a field on type {}, but the field does not exist", + type_name + )) + } + } + } +} + pub(crate) struct DemandControl { config: DemandControlConfig, strategy_factory: StrategyFactory, @@ -223,11 +243,21 @@ impl Plugin for DemandControl { type Config = DemandControlConfig; async fn new(init: PluginInit) -> Result { + let demand_controlled_supergraph_schema = + DemandControlledSchema::new(init.supergraph_schema.clone())?; + let mut demand_controlled_subgraph_schemas = HashMap::new(); + for (subgraph_name, subgraph_schema) in init.subgraph_schemas.iter() { + let demand_controlled_subgraph_schema = + DemandControlledSchema::new(subgraph_schema.clone())?; + demand_controlled_subgraph_schemas + .insert(subgraph_name.clone(), demand_controlled_subgraph_schema); + } + Ok(DemandControl { strategy_factory: StrategyFactory::new( init.config.clone(), - init.supergraph_schema.clone(), - init.subgraph_schemas.clone(), + Arc::new(demand_controlled_supergraph_schema), + Arc::new(demand_controlled_subgraph_schemas), ), config: init.config, }) diff --git a/apollo-router/src/plugins/demand_control/strategy/mod.rs b/apollo-router/src/plugins/demand_control/strategy/mod.rs index 5defca64d5..6bae126694 100644 --- a/apollo-router/src/plugins/demand_control/strategy/mod.rs +++ b/apollo-router/src/plugins/demand_control/strategy/mod.rs @@ -1,11 +1,10 @@ -use std::collections::HashMap; use std::sync::Arc; -use apollo_compiler::validation::Valid; +use ahash::HashMap; use apollo_compiler::ExecutableDocument; -use apollo_compiler::Schema; use crate::graphql; +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; use crate::plugins::demand_control::DemandControlConfig; @@ -75,15 +74,15 @@ impl Strategy { pub(crate) struct StrategyFactory { config: DemandControlConfig, #[allow(dead_code)] - supergraph_schema: Arc>, - subgraph_schemas: Arc>>>, + supergraph_schema: Arc, + subgraph_schemas: Arc>, } impl StrategyFactory { pub(crate) fn new( config: DemandControlConfig, - supergraph_schema: Arc>, - subgraph_schemas: Arc>>>, + supergraph_schema: Arc, + subgraph_schemas: Arc>, ) -> Self { Self { config, @@ -97,6 +96,7 @@ impl StrategyFactory { StrategyConfig::StaticEstimated { list_size, max } => Arc::new(StaticEstimated { max: *max, cost_calculator: StaticCostCalculator::new( + self.supergraph_schema.clone(), self.subgraph_schemas.clone(), *list_size, ), diff --git a/apollo-router/src/plugins/headers.rs b/apollo-router/src/plugins/headers.rs index 2f19a965ff..1e52cd444c 100644 --- a/apollo-router/src/plugins/headers.rs +++ b/apollo-router/src/plugins/headers.rs @@ -193,6 +193,7 @@ struct Config { struct Headers { all_operations: Arc>, subgraph_operations: HashMap>>, + reserved_headers: Arc>, } #[async_trait::async_trait] @@ -220,6 +221,7 @@ impl Plugin for Headers { Ok(Headers { all_operations: Arc::new(operations), subgraph_operations, + reserved_headers: Arc::new(RESERVED_HEADERS.iter().collect()), }) } @@ -230,6 +232,7 @@ impl Plugin for Headers { .get(name) .cloned() .unwrap_or_else(|| self.all_operations.clone()), + self.reserved_headers.clone(), )) .service(service) .boxed() @@ -242,10 +245,13 @@ struct HeadersLayer { } impl HeadersLayer { - fn new(operations: Arc>) -> Self { + fn new( + operations: Arc>, + reserved_headers: Arc>, + ) -> Self { Self { operations, - reserved_headers: Arc::new(RESERVED_HEADERS.iter().collect()), + reserved_headers, } } } @@ -583,12 +589,13 @@ mod test { }) .returning(example_response); - let mut service = HeadersLayer::new(Arc::new(vec![Operation::Insert(Insert::Static( - InsertStatic { + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Insert(Insert::Static(InsertStatic { name: "c".try_into()?, value: "d".try_into()?, - }, - ))])) + }))]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) .layer(mock); service.ready().await?.call(example_request()).await?; @@ -610,12 +617,15 @@ mod test { }) .returning(example_response); - let mut service = HeadersLayer::new(Arc::new(vec![Operation::Insert( - Insert::FromContext(InsertFromContext { - name: "header_from_context".try_into()?, - from_context: "my_key".to_string(), - }), - )])) + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Insert(Insert::FromContext( + InsertFromContext { + name: "header_from_context".try_into()?, + from_context: "my_key".to_string(), + }, + ))]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) .layer(mock); service.ready().await?.call(example_request()).await?; @@ -637,13 +647,14 @@ mod test { }) .returning(example_response); - let mut service = HeadersLayer::new(Arc::new(vec![Operation::Insert(Insert::FromBody( - InsertFromBody { + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Insert(Insert::FromBody(InsertFromBody { name: "header_from_request".try_into()?, path: JSONQuery::parse(".operationName")?, default: None, - }, - ))])) + }))]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) .layer(mock); service.ready().await?.call(example_request()).await?; @@ -658,9 +669,10 @@ mod test { .withf(|request| request.assert_headers(vec![("ac", "vac"), ("ab", "vab")])) .returning(example_response); - let mut service = HeadersLayer::new(Arc::new(vec![Operation::Remove(Remove::Named( - "aa".try_into()?, - ))])) + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Remove(Remove::Named("aa".try_into()?))]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) .layer(mock); service.ready().await?.call(example_request()).await?; @@ -675,9 +687,12 @@ mod test { .withf(|request| request.assert_headers(vec![("ac", "vac")])) .returning(example_response); - let mut service = HeadersLayer::new(Arc::new(vec![Operation::Remove(Remove::Matching( - Regex::from_str("a[ab]")?, - ))])) + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Remove(Remove::Matching(Regex::from_str( + "a[ab]", + )?))]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) .layer(mock); service.ready().await?.call(example_request()).await?; @@ -701,11 +716,13 @@ mod test { }) .returning(example_response); - let mut service = - HeadersLayer::new(Arc::new(vec![Operation::Propagate(Propagate::Matching { + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Propagate(Propagate::Matching { matching: Regex::from_str("d[ab]")?, - })])) - .layer(mock); + })]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) + .layer(mock); service.ready().await?.call(example_request()).await?; Ok(()) @@ -726,13 +743,15 @@ mod test { }) .returning(example_response); - let mut service = - HeadersLayer::new(Arc::new(vec![Operation::Propagate(Propagate::Named { + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Propagate(Propagate::Named { named: "da".try_into()?, rename: None, default: None, - })])) - .layer(mock); + })]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) + .layer(mock); service.ready().await?.call(example_request()).await?; Ok(()) @@ -753,13 +772,15 @@ mod test { }) .returning(example_response); - let mut service = - HeadersLayer::new(Arc::new(vec![Operation::Propagate(Propagate::Named { + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Propagate(Propagate::Named { named: "da".try_into()?, rename: Some("ea".try_into()?), default: None, - })])) - .layer(mock); + })]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) + .layer(mock); service.ready().await?.call(example_request()).await?; Ok(()) @@ -780,13 +801,15 @@ mod test { }) .returning(example_response); - let mut service = - HeadersLayer::new(Arc::new(vec![Operation::Propagate(Propagate::Named { + let mut service = HeadersLayer::new( + Arc::new(vec![Operation::Propagate(Propagate::Named { named: "ea".try_into()?, rename: None, default: Some("defaulted".try_into()?), - })])) - .layer(mock); + })]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) + .layer(mock); service.ready().await?.call(example_request()).await?; Ok(()) diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 56aae8045c..4f558ced82 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -104,19 +104,20 @@ mod test { use crate::Configuration; static UNREDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { - Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query ErrorTopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}\",\"operationName\":\"ErrorTopProducts__products__0\",\"variables\":{\"first\":2}}","extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) + Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query ErrorTopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}\",\"operationName\":\"ErrorTopProducts__products__0\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) }); static REDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { Bytes::from_static( - r#"{"data":{"topProducts":null},"errors":[{"message":"Subgraph errors redacted"}]}"# + r#"{"data":{"topProducts":null},"errors":[{"message":"Subgraph errors redacted","path":[]}]}"# .as_bytes(), ) }); static REDACTED_ACCOUNT_RESPONSE: Lazy = Lazy::new(|| { Bytes::from_static( - r#"{"data":null,"errors":[{"message":"Subgraph errors redacted"}]}"#.as_bytes(), + r#"{"data":null,"errors":[{"message":"Subgraph errors redacted","path":[]}]}"# + .as_bytes(), ) }); 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/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index a306f19b6b..bb75124df1 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; use std::num::NonZeroUsize; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Instant; @@ -8,6 +10,9 @@ use async_channel::bounded; use async_channel::Sender; use futures::future::BoxFuture; use opentelemetry::metrics::MeterProvider; +use opentelemetry::metrics::ObservableGauge; +use opentelemetry::metrics::Unit; +use opentelemetry_api::metrics::Meter; use router_bridge::planner::Planner; use tokio::sync::oneshot; use tokio::task::JoinSet; @@ -37,6 +42,10 @@ pub(crate) struct BridgeQueryPlannerPool { schema: Arc, subgraph_schemas: Arc>>>, _pool_size_gauge: opentelemetry::metrics::ObservableGauge, + v8_heap_used: Arc, + _v8_heap_used_gauge: ObservableGauge, + v8_heap_total: Arc, + _v8_heap_total_gauge: ObservableGauge, } impl BridgeQueryPlannerPool { @@ -93,7 +102,7 @@ impl BridgeQueryPlannerPool { })? .subgraph_schemas(); - let planners = bridge_query_planners + let planners: Vec<_> = bridge_query_planners .iter() .map(|p| p.planner().clone()) .collect(); @@ -119,21 +128,68 @@ impl BridgeQueryPlannerPool { }); } let sender_for_gauge = sender.clone(); - let pool_size_gauge = meter_provider() - .meter("apollo/router") + let meter = meter_provider().meter("apollo/router"); + let pool_size_gauge = meter .u64_observable_gauge("apollo.router.query_planning.queued") + .with_description("Number of queries waiting to be planned") + .with_unit(Unit::new("query")) .with_callback(move |m| m.observe(sender_for_gauge.len() as u64, &[])) .init(); + let (v8_heap_used, _v8_heap_used_gauge) = Self::create_heap_used_gauge(&meter); + let (v8_heap_total, _v8_heap_total_gauge) = Self::create_heap_total_gauge(&meter); + + // initialize v8 metrics + if let Some(bridge_query_planner) = planners.first().cloned() { + Self::get_v8_metrics( + bridge_query_planner, + v8_heap_used.clone(), + v8_heap_total.clone(), + ) + .await; + } + Ok(Self { js_planners: planners, sender, schema, subgraph_schemas, _pool_size_gauge: pool_size_gauge, + v8_heap_used, + _v8_heap_used_gauge, + v8_heap_total, + _v8_heap_total_gauge, }) } + fn create_heap_used_gauge(meter: &Meter) -> (Arc, ObservableGauge) { + let current_heap_used = Arc::new(AtomicU64::new(0)); + let current_heap_used_for_gauge = current_heap_used.clone(); + let heap_used_gauge = meter + .u64_observable_gauge("apollo.router.v8.heap.used") + .with_description("V8 heap used, in bytes") + .with_unit(Unit::new("By")) + .with_callback(move |i| { + i.observe(current_heap_used_for_gauge.load(Ordering::SeqCst), &[]) + }) + .init(); + (current_heap_used, heap_used_gauge) + } + + fn create_heap_total_gauge(meter: &Meter) -> (Arc, ObservableGauge) { + let current_heap_total = Arc::new(AtomicU64::new(0)); + let current_heap_total_for_gauge = current_heap_total.clone(); + let heap_total_gauge = meter + .u64_observable_gauge("apollo.router.v8.heap.total") + .with_description("V8 heap total, in bytes") + .with_unit(Unit::new("By")) + .with_callback(move |i| { + i.observe(current_heap_total_for_gauge.load(Ordering::SeqCst), &[]) + }) + .init(); + (current_heap_total, heap_total_gauge) + } + pub(crate) fn planners(&self) -> Vec>> { self.js_planners.clone() } @@ -147,6 +203,18 @@ impl BridgeQueryPlannerPool { ) -> Arc>>> { self.subgraph_schemas.clone() } + + async fn get_v8_metrics( + planner: Arc>, + v8_heap_used: Arc, + v8_heap_total: Arc, + ) { + let metrics = planner.get_heap_statistics().await; + if let Ok(metrics) = metrics { + v8_heap_used.store(metrics.heap_used, Ordering::SeqCst); + v8_heap_total.store(metrics.heap_total, Ordering::SeqCst); + } + } } impl tower::Service for BridgeQueryPlannerPool { @@ -173,6 +241,20 @@ impl tower::Service for BridgeQueryPlannerPool { let (response_sender, response_receiver) = oneshot::channel(); let sender = self.sender.clone(); + let get_metrics_future = + if let Some(bridge_query_planner) = self.js_planners.first().cloned() { + let v8_heap_used = self.v8_heap_used.clone(); + let v8_heap_total = self.v8_heap_total.clone(); + + Some(Self::get_v8_metrics( + bridge_query_planner, + v8_heap_used, + v8_heap_total, + )) + } else { + None + }; + Box::pin(async move { let start = Instant::now(); let _ = sender.send((req, response_sender)).await; @@ -187,7 +269,73 @@ impl tower::Service for BridgeQueryPlannerPool { start.elapsed().as_secs_f64() ); + if let Some(f) = get_metrics_future { + // execute in a separate task to avoid blocking the request + tokio::task::spawn(f); + } + res }) } } + +#[cfg(test)] + +mod tests { + use opentelemetry_sdk::metrics::data::Gauge; + + use super::*; + use crate::metrics::FutureMetricsExt; + use crate::spec::Query; + use crate::Context; + + #[tokio::test] + async fn test_v8_metrics() { + let sdl = include_str!("../testdata/supergraph.graphql"); + let config = Arc::default(); + let schema = Arc::new(Schema::parse(sdl, &config).unwrap()); + + async move { + let mut pool = BridgeQueryPlannerPool::new( + schema.clone(), + config.clone(), + NonZeroUsize::new(2).unwrap(), + ) + .await + .unwrap(); + let query = "query { me { name } }".to_string(); + + let doc = Query::parse_document(&query, None, &schema, &config).unwrap(); + let context = Context::new(); + context.extensions().with_lock(|mut lock| lock.insert(doc)); + + pool.call(QueryPlannerRequest::new(query, None, context)) + .await + .unwrap(); + + let metrics = crate::metrics::collect_metrics(); + let heap_used = metrics.find("apollo.router.v8.heap.used").unwrap(); + let heap_total = metrics.find("apollo.router.v8.heap.total").unwrap(); + + println!( + "got heap_used: {:?}, heap_total: {:?}", + heap_used + .data + .as_any() + .downcast_ref::>() + .unwrap() + .data_points[0] + .value, + heap_total + .data + .as_any() + .downcast_ref::>() + .unwrap() + .data_points[0] + .value + ); + } + .with_metrics() + .await; + } +} diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 0ef5bde512..6a880cf538 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -140,17 +140,20 @@ impl BothModeComparisonJob { (Ok(js_plan), Ok(rust_plan)) => { let js_root_node = &js_plan.query_plan.node; let rust_root_node = convert_root_query_plan_node(rust_plan); - is_matched = opt_plan_node_matches(js_root_node, &rust_root_node); - if is_matched { - tracing::debug!("JS and Rust query plans match{operation_desc}! 🎉"); - } else { - tracing::debug!("JS v.s. Rust query plan mismatch{operation_desc}"); - tracing::debug!( - "Diff of formatted plans:\n{}", - diff_plan(js_plan, rust_plan) - ); - tracing::trace!("JS query plan Debug: {js_root_node:#?}"); - tracing::trace!("Rust query plan Debug: {rust_root_node:#?}"); + let match_result = opt_plan_node_matches(js_root_node, &rust_root_node); + is_matched = match_result.is_ok(); + match match_result { + Ok(_) => tracing::debug!("JS and Rust query plans match{operation_desc}! 🎉"), + Err(err) => { + tracing::debug!("JS v.s. Rust query plan mismatch{operation_desc}"); + tracing::debug!("{}", err.full_description()); + tracing::debug!( + "Diff of formatted plans:\n{}", + diff_plan(js_plan, rust_plan) + ); + tracing::trace!("JS query plan Debug: {js_root_node:#?}"); + tracing::trace!("Rust query plan Debug: {rust_root_node:#?}"); + } } } } @@ -168,7 +171,62 @@ impl BothModeComparisonJob { // Specific comparison functions -fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> bool { +pub struct MatchFailure { + description: String, + backtrace: std::backtrace::Backtrace, +} + +impl MatchFailure { + pub fn description(&self) -> String { + self.description.clone() + } + + pub fn full_description(&self) -> String { + format!("{}\n\nBacktrace:\n{}", self.description, self.backtrace) + } + + fn new(description: String) -> MatchFailure { + MatchFailure { + description, + backtrace: std::backtrace::Backtrace::force_capture(), + } + } + + fn add_description(self: MatchFailure, description: &str) -> MatchFailure { + MatchFailure { + description: format!("{}\n{}", self.description, description), + backtrace: self.backtrace, + } + } +} + +macro_rules! check_match { + ($pred:expr) => { + if !$pred { + return Err(MatchFailure::new(format!( + "mismatch at {}", + stringify!($pred) + ))); + } + }; +} + +macro_rules! check_match_eq { + ($a:expr, $b:expr) => { + if $a != $b { + let message = format!( + "mismatch between {} and {}:\nleft: {:?}\nright: {:?}", + stringify!($a), + stringify!($b), + $a, + $b + ); + return Err(MatchFailure::new(message)); + } + }; +} + +fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> Result<(), MatchFailure> { let FetchNode { service_name, requires, @@ -183,16 +241,18 @@ fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> bool { schema_aware_hash: _, // ignored authorization, } = this; - *service_name == other.service_name - && same_selection_set_sorted(requires, &other.requires) - && vec_matches_sorted(variable_usages, &other.variable_usages) - && *operation_kind == other.operation_kind - && *id == other.id - && same_rewrites(input_rewrites, &other.input_rewrites) - && same_rewrites(output_rewrites, &other.output_rewrites) - && same_rewrites(context_rewrites, &other.context_rewrites) - && *authorization == other.authorization - && operation_matches(operation, &other.operation) + + check_match_eq!(*service_name, other.service_name); + check_match_eq!(*operation_kind, other.operation_kind); + check_match_eq!(*id, other.id); + check_match_eq!(*authorization, other.authorization); + check_match!(same_selection_set_sorted(requires, &other.requires)); + check_match!(vec_matches_sorted(variable_usages, &other.variable_usages)); + check_match!(same_rewrites(input_rewrites, &other.input_rewrites)); + check_match!(same_rewrites(output_rewrites, &other.output_rewrites)); + check_match!(same_rewrites(context_rewrites, &other.context_rewrites)); + operation_matches(operation, &other.operation)?; + Ok(()) } fn subscription_primary_matches(this: &SubscriptionNode, other: &SubscriptionNode) -> bool { @@ -211,22 +271,27 @@ fn subscription_primary_matches(this: &SubscriptionNode, other: &SubscriptionNod && *operation_kind == other.operation_kind && *input_rewrites == other.input_rewrites && *output_rewrites == other.output_rewrites - && operation_matches(operation, &other.operation) + && operation_matches(operation, &other.operation).is_ok() } -fn operation_matches(this: &SubgraphOperation, other: &SubgraphOperation) -> bool { +fn operation_matches( + this: &SubgraphOperation, + other: &SubgraphOperation, +) -> Result<(), MatchFailure> { let this_ast = match ast::Document::parse(this.as_serialized(), "this_operation.graphql") { Ok(document) => document, Err(_) => { - // TODO: log error - return false; + return Err(MatchFailure::new( + "Failed to parse this operation".to_string(), + )); } }; let other_ast = match ast::Document::parse(other.as_serialized(), "other_operation.graphql") { Ok(document) => document, Err(_) => { - // TODO: log error - return false; + return Err(MatchFailure::new( + "Failed to parse other operation".to_string(), + )); } }; same_ast_document(&this_ast, &other_ast) @@ -236,7 +301,7 @@ fn operation_matches(this: &SubgraphOperation, other: &SubgraphOperation) -> boo // but otherwise behave just like `PartialEq`: // Note: Reexported under `apollo_router::_private` -pub fn plan_matches(js_plan: &QueryPlanResult, rust_plan: &QueryPlan) -> bool { +pub fn plan_matches(js_plan: &QueryPlanResult, rust_plan: &QueryPlan) -> Result<(), MatchFailure> { let js_root_node = &js_plan.query_plan.node; let rust_root_node = convert_root_query_plan_node(rust_plan); opt_plan_node_matches(js_root_node, &rust_root_node) @@ -270,10 +335,14 @@ pub fn diff_plan(js_plan: &QueryPlanResult, rust_plan: &QueryPlan) -> String { fn opt_plan_node_matches( this: &Option>, other: &Option>, -) -> bool { +) -> Result<(), MatchFailure> { match (this, other) { - (None, None) => true, - (None, Some(_)) | (Some(_), None) => false, + (None, None) => Ok(()), + (None, Some(_)) | (Some(_), None) => Err(MatchFailure::new(format!( + "mismatch at opt_plan_node_matches\nleft: {:?}\nright: {:?}", + this.is_some(), + other.is_some() + ))), (Some(this), Some(other)) => plan_node_matches(this.borrow(), other.borrow()), } } @@ -283,6 +352,22 @@ fn vec_matches(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool && std::iter::zip(this, other).all(|(this, other)| item_matches(this, other)) } +fn vec_matches_result( + this: &[T], + other: &[T], + item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, +) -> Result<(), MatchFailure> { + check_match_eq!(this.len(), other.len()); + std::iter::zip(this, other) + .enumerate() + .try_fold((), |_acc, (index, (this, other))| { + item_matches(this, other) + .map_err(|err| err.add_description(&format!("under item[{}]", index))) + })?; + assert!(vec_matches(this, other, |a, b| item_matches(a, b).is_ok())); + Ok(()) +} + fn vec_matches_sorted(this: &[T], other: &[T]) -> bool { let mut this_sorted = this.to_owned(); let mut other_sorted = other.to_owned(); @@ -318,16 +403,65 @@ fn vec_matches_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) }) } -fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> bool { +fn vec_matches_result_as_set( + this: &[T], + other: &[T], + item_matches: impl Fn(&T, &T) -> bool, +) -> Result<(), MatchFailure> { + // Set-inclusion test in both directions + check_match_eq!(this.len(), other.len()); + for (index, this_node) in this.iter().enumerate() { + if !other + .iter() + .any(|other_node| item_matches(this_node, other_node)) + { + return Err(MatchFailure::new(format!( + "mismatched set: missing item[{}]", + index + ))); + } + } + for other_node in other.iter() { + if !this + .iter() + .any(|this_node| item_matches(this_node, other_node)) + { + return Err(MatchFailure::new( + "mismatched set: extra item found".to_string(), + )); + } + } + assert!(vec_matches_as_set(this, other, item_matches)); + Ok(()) +} + +fn option_to_string(name: Option) -> String { + name.map_or_else(|| "".to_string(), |name| name.to_string()) +} + +fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> Result<(), MatchFailure> { match (this, other) { (PlanNode::Sequence { nodes: this }, PlanNode::Sequence { nodes: other }) => { - vec_matches(this, other, plan_node_matches) + vec_matches_result(this, other, plan_node_matches) + .map_err(|err| err.add_description("under Sequence node"))?; } (PlanNode::Parallel { nodes: this }, PlanNode::Parallel { nodes: other }) => { - vec_matches_as_set(this, other, plan_node_matches) + vec_matches_result_as_set(this, other, |a, b| plan_node_matches(a, b).is_ok()) + .map_err(|err| err.add_description("under Parallel node"))?; + } + (PlanNode::Fetch(this), PlanNode::Fetch(other)) => { + fetch_node_matches(this, other).map_err(|err| { + err.add_description(&format!( + "under Fetch node (operation name: {})", + option_to_string(this.operation_name.as_ref()) + )) + })?; + } + (PlanNode::Flatten(this), PlanNode::Flatten(other)) => { + flatten_node_matches(this, other).map_err(|err| { + err.add_description(&format!("under Flatten node (path: {})", this.path)) + })?; } - (PlanNode::Fetch(this), PlanNode::Fetch(other)) => fetch_node_matches(this, other), - (PlanNode::Flatten(this), PlanNode::Flatten(other)) => flatten_node_matches(this, other), ( PlanNode::Defer { primary, deferred }, PlanNode::Defer { @@ -335,8 +469,8 @@ fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> bool { deferred: other_deferred, }, ) => { - defer_primary_node_matches(primary, other_primary) - && vec_matches(deferred, other_deferred, deferred_node_matches) + check_match!(defer_primary_node_matches(primary, other_primary)); + check_match!(vec_matches(deferred, other_deferred, deferred_node_matches)); } ( PlanNode::Subscription { primary, rest }, @@ -345,8 +479,9 @@ fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> bool { rest: other_rest, }, ) => { - subscription_primary_matches(primary, other_primary) - && opt_plan_node_matches(rest, other_rest) + check_match!(subscription_primary_matches(primary, other_primary)); + opt_plan_node_matches(rest, other_rest) + .map_err(|err| err.add_description("under Subscription"))?; } ( PlanNode::Condition { @@ -360,17 +495,25 @@ fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> bool { else_clause: other_else_clause, }, ) => { - condition == other_condition - && opt_plan_node_matches(if_clause, other_if_clause) - && opt_plan_node_matches(else_clause, other_else_clause) + check_match_eq!(condition, other_condition); + opt_plan_node_matches(if_clause, other_if_clause) + .map_err(|err| err.add_description("under Condition node (if_clause)"))?; + opt_plan_node_matches(else_clause, other_else_clause) + .map_err(|err| err.add_description("under Condition node (else_clause)"))?; } - _ => false, - } + _ => { + return Err(MatchFailure::new(format!( + "mismatched plan node types\nleft: {:?}\nright: {:?}", + this, other + ))) + } + }; + Ok(()) } fn defer_primary_node_matches(this: &Primary, other: &Primary) -> bool { let Primary { subselection, node } = this; - *subselection == other.subselection && opt_plan_node_matches(node, &other.node) + *subselection == other.subselection && opt_plan_node_matches(node, &other.node).is_ok() } fn deferred_node_matches(this: &DeferredNode, other: &DeferredNode) -> bool { @@ -385,12 +528,13 @@ fn deferred_node_matches(this: &DeferredNode, other: &DeferredNode) -> bool { && *label == other.label && *query_path == other.query_path && *subselection == other.subselection - && opt_plan_node_matches(node, &other.node) + && opt_plan_node_matches(node, &other.node).is_ok() } -fn flatten_node_matches(this: &FlattenNode, other: &FlattenNode) -> bool { +fn flatten_node_matches(this: &FlattenNode, other: &FlattenNode) -> Result<(), MatchFailure> { let FlattenNode { path, node } = this; - *path == other.path && plan_node_matches(node, &other.node) + check_match_eq!(*path, other.path); + plan_node_matches(node, &other.node) } // Copied and modified from `apollo_federation::operation::SelectionKey` @@ -478,7 +622,7 @@ fn same_rewrites(x: &Option>, y: &Option>) -> //================================================================================================== // AST comparison functions -fn same_ast_document(x: &ast::Document, y: &ast::Document) -> bool { +fn same_ast_document(x: &ast::Document, y: &ast::Document) -> Result<(), MatchFailure> { fn split_definitions( doc: &ast::Document, ) -> ( @@ -510,34 +654,54 @@ fn same_ast_document(x: &ast::Document, y: &ast::Document) -> bool { "Different number of operation definitions" ); - x_ops.len() == y_ops.len() - && x_ops - .iter() - .zip(y_ops.iter()) - .all(|(x_op, y_op)| same_ast_operation_definition(x_op, y_op)) - && x_frags.len() == y_frags.len() - && x_frags - .iter() - .zip(y_frags.iter()) - .all(|(x_frag, y_frag)| same_ast_fragment_definition(x_frag, y_frag)) + check_match_eq!(x_ops.len(), y_ops.len()); + x_ops + .iter() + .zip(y_ops.iter()) + .try_fold((), |_, (x_op, y_op)| { + same_ast_operation_definition(x_op, y_op) + .map_err(|err| err.add_description("under operation definition")) + })?; + check_match_eq!(x_frags.len(), y_frags.len()); + x_frags + .iter() + .zip(y_frags.iter()) + .try_fold((), |_, (x_frag, y_frag)| { + same_ast_fragment_definition(x_frag, y_frag) + .map_err(|err| err.add_description("under fragment definition")) + })?; + Ok(()) } fn same_ast_operation_definition( x: &ast::OperationDefinition, y: &ast::OperationDefinition, -) -> bool { +) -> Result<(), MatchFailure> { // Note: Operation names are ignored, since parallel fetches may have different names. - x.operation_type == y.operation_type - && vec_matches_sorted_by(&x.variables, &y.variables, |x, y| x.name.cmp(&y.name)) - && x.directives == y.directives - && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set) -} - -fn same_ast_fragment_definition(x: &ast::FragmentDefinition, y: &ast::FragmentDefinition) -> bool { - x.name == y.name - && x.type_condition == y.type_condition - && x.directives == y.directives - && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set) + check_match_eq!(x.operation_type, y.operation_type); + check_match!(vec_matches_sorted_by(&x.variables, &y.variables, |x, y| x + .name + .cmp(&y.name))); + check_match_eq!(x.directives, y.directives); + check_match!(same_ast_selection_set_sorted( + &x.selection_set, + &y.selection_set + )); + Ok(()) +} + +fn same_ast_fragment_definition( + x: &ast::FragmentDefinition, + y: &ast::FragmentDefinition, +) -> Result<(), MatchFailure> { + check_match_eq!(x.name, y.name); + check_match_eq!(x.type_condition, y.type_condition); + check_match_eq!(x.directives, y.directives); + check_match!(same_ast_selection_set_sorted( + &x.selection_set, + &y.selection_set + )); + Ok(()) } fn get_ast_selection_key(selection: &ast::Selection) -> SelectionKey { @@ -617,7 +781,7 @@ mod ast_comparison_tests { let op_y = r#"query($qv1: Int!, $qv2: String!) { x(arg1: $qv1, arg2: $qv2) }"#; let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y)); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } #[test] @@ -634,7 +798,7 @@ mod ast_comparison_tests { "#; let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y)); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } #[test] @@ -643,7 +807,7 @@ mod ast_comparison_tests { let op_y = r#"{ y x { z w } }"#; let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y)); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } #[test] @@ -652,6 +816,6 @@ mod ast_comparison_tests { let op_y = r#"{ q { ...f1 ...f2 } } fragment f2 on T { w z } fragment f1 on T { x y }"#; let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y)); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } } diff --git a/apollo-router/src/query_planner/fetch.rs b/apollo-router/src/query_planner/fetch.rs index a38063063e..79193e63e6 100644 --- a/apollo-router/src/query_planner/fetch.rs +++ b/apollo-router/src/query_planner/fetch.rs @@ -494,6 +494,7 @@ impl FetchNode { errors.push(error); } } else { + error.path = Some(current_dir.clone()); errors.push(error); } } @@ -551,13 +552,17 @@ impl FetchNode { .errors .into_iter() .map(|error| { - let path = error.path.as_ref().map(|path| { - Path::from_iter(current_slice.iter().chain(path.iter()).cloned()) - }); + let path = error + .path + .as_ref() + .map(|path| { + Path::from_iter(current_slice.iter().chain(path.iter()).cloned()) + }) + .unwrap_or_else(|| current_dir.clone()); Error { locations: error.locations, - path, + path: Some(path), message: error.message, extensions: error.extensions, } diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index 33efa8416e..75810e9b21 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; @@ -791,108 +780,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; @@ -903,11 +790,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; @@ -1040,125 +925,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/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap index 33f7508979..a4366f1d9a 100644 --- a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap @@ -1,5 +1,5 @@ --- -source: apollo-router/src/services/supergraph_service.rs +source: apollo-router/src/services/supergraph/tests.rs expression: stream.next_response().await.unwrap() --- { @@ -14,7 +14,11 @@ expression: stream.next_response().await.unwrap() }, "errors": [ { - "message": "error" + "message": "error", + "path": [ + "currentUser", + "activeOrganization" + ] } ] } 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/batching.rs b/apollo-router/tests/integration/batching.rs index c50c85054b..071ca5cb7a 100644 --- a/apollo-router/tests/integration/batching.rs +++ b/apollo-router/tests/integration/batching.rs @@ -140,19 +140,20 @@ async fn it_batches_with_errors_in_single_graph() -> Result<(), BoxError> { if test_is_enabled() { // Make sure that we got back what we wanted assert_yaml_snapshot!(responses, @r###" - --- - - data: - entryA: - index: 0 - - errors: - - message: expected error in A - - data: - entryA: - index: 2 - - data: - entryA: - index: 3 - "###); + --- + - data: + entryA: + index: 0 + - errors: + - message: expected error in A + path: [] + - data: + entryA: + index: 2 + - data: + entryA: + index: 3 + "###); } Ok(()) @@ -189,24 +190,26 @@ async fn it_batches_with_errors_in_multi_graph() -> Result<(), BoxError> { if test_is_enabled() { assert_yaml_snapshot!(responses, @r###" - --- - - data: - entryA: - index: 0 - - data: - entryB: - index: 0 - - errors: - - message: expected error in A - - errors: - - message: expected error in B - - data: - entryA: - index: 2 - - data: - entryB: - index: 2 - "###); + --- + - data: + entryA: + index: 0 + - data: + entryB: + index: 0 + - errors: + - message: expected error in A + path: [] + - errors: + - message: expected error in B + path: [] + - data: + entryA: + index: 2 + - data: + entryB: + index: 2 + "###); } Ok(()) @@ -250,6 +253,7 @@ async fn it_handles_short_timeouts() -> Result<(), BoxError> { index: 0 - errors: - message: Request timed out + path: [] extensions: code: REQUEST_TIMEOUT - data: @@ -257,6 +261,7 @@ async fn it_handles_short_timeouts() -> Result<(), BoxError> { index: 1 - errors: - message: Request timed out + path: [] extensions: code: REQUEST_TIMEOUT "###); @@ -323,14 +328,17 @@ async fn it_handles_indefinite_timeouts() -> Result<(), BoxError> { index: 2 - errors: - message: Request timed out + path: [] extensions: code: REQUEST_TIMEOUT - errors: - message: Request timed out + path: [] extensions: code: REQUEST_TIMEOUT - errors: - message: Request timed out + path: [] extensions: code: REQUEST_TIMEOUT "###); @@ -554,22 +562,24 @@ async fn it_handles_cancelled_by_coprocessor() -> Result<(), BoxError> { if test_is_enabled() { assert_yaml_snapshot!(responses, @r###" - --- - - errors: - - message: Subgraph A is not allowed - extensions: - code: ERR_NOT_ALLOWED - - data: - entryB: - index: 0 - - errors: - - message: Subgraph A is not allowed - extensions: - code: ERR_NOT_ALLOWED - - data: - entryB: - index: 1 - "###); + --- + - errors: + - message: Subgraph A is not allowed + path: [] + extensions: + code: ERR_NOT_ALLOWED + - data: + entryB: + index: 0 + - errors: + - message: Subgraph A is not allowed + path: [] + extensions: + code: ERR_NOT_ALLOWED + - data: + entryB: + index: 1 + "###); } Ok(()) @@ -697,33 +707,34 @@ async fn it_handles_single_request_cancelled_by_coprocessor() -> Result<(), BoxE if test_is_enabled() { assert_yaml_snapshot!(responses, @r###" - --- - - data: - entryA: - index: 0 - - data: - entryB: - index: 0 - - data: - entryA: - index: 1 - - data: - entryB: - index: 1 - - errors: - - message: Subgraph A index 2 is not allowed - extensions: - code: ERR_NOT_ALLOWED - - data: - entryB: - index: 2 - - data: - entryA: - index: 3 - - data: - entryB: - index: 3 - "###); + --- + - data: + entryA: + index: 0 + - data: + entryB: + index: 0 + - data: + entryA: + index: 1 + - data: + entryB: + index: 1 + - errors: + - message: Subgraph A index 2 is not allowed + path: [] + extensions: + code: ERR_NOT_ALLOWED + - data: + entryB: + index: 2 + - data: + entryA: + index: 3 + - data: + entryB: + index: 3 + "###); } Ok(()) 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; +} diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index cb8b79959e..6b0ff6b404 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -26,7 +26,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { // 2. run `docker compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. // 3. Run the `redis-cli` command from the shell and start the redis `monitor` command. // 4. Run this test and yank the updated cache key from the redis logs. - let known_cache_key = "plan:0:v2.8.1:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3106dfc3339d8c3f3020434024bff0f566a8be5995199954db5a7525a7d7e67a"; + let known_cache_key = "plan:0:v2.8.3:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3106dfc3339d8c3f3020434024bff0f566a8be5995199954db5a7525a7d7e67a"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -921,7 +921,7 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9054d19854e1d9e282ac7645c612bc70b8a7143d43b73d44dade4a5ec43938b4", + "plan:0:v2.8.3:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9054d19854e1d9e282ac7645c612bc70b8a7143d43b73d44dade4a5ec43938b4", ) .await; } @@ -940,7 +940,7 @@ async fn query_planner_redis_update_planner_mode() { async fn query_planner_redis_update_introspection() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:04b3051125b5994fba6b0a22b2d8b4246cadc145be030c491a3431655d2ba07a", + "plan:0:v2.8.3:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:04b3051125b5994fba6b0a22b2d8b4246cadc145be030c491a3431655d2ba07a", ) .await; } @@ -949,7 +949,7 @@ async fn query_planner_redis_update_introspection() { async fn query_planner_redis_update_defer() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3b7241b0db2cd878b79c0810121953ba544543f3cb2692aaf1a59184470747b0", + "plan:0:v2.8.3:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3b7241b0db2cd878b79c0810121953ba544543f3cb2692aaf1a59184470747b0", ) .await; } @@ -960,7 +960,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:0ca695a8c4c448b65fa04229c663f44150af53b184ebdcbb0ad6862290efed76", + "plan:0:v2.8.3:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:0ca695a8c4c448b65fa04229c663f44150af53b184ebdcbb0ad6862290efed76", ) .await; } @@ -971,7 +971,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:f7c04319556397ec4b550aa5aaa96c73689cee09026b661b6a9fc20b49e6fa77", + "plan:0:v2.8.3:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:f7c04319556397ec4b550aa5aaa96c73689cee09026b661b6a9fc20b49e6fa77", ) .await; } @@ -994,7 +994,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.assert_started().await; router.clear_redis_cache().await; - let starting_key = "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4a5827854a6d2efc85045f0d5bede402e15958390f1073d2e77df56188338e5a"; + let starting_key = "plan:0:v2.8.3:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4a5827854a6d2efc85045f0d5bede402e15958390f1073d2e77df56188338e5a"; router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap index 584b125252..07df294289 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap @@ -2,4 +2,4 @@ source: apollo-router/tests/integration/traffic_shaping.rs expression: response --- -"{\"data\":null,\"errors\":[{\"message\":\"Your request has been rate limited\",\"extensions\":{\"code\":\"REQUEST_RATE_LIMITED\"}}]}" +"{\"data\":null,\"errors\":[{\"message\":\"Your request has been rate limited\",\"path\":[],\"extensions\":{\"code\":\"REQUEST_RATE_LIMITED\"}}]}" diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap index 671e207784..407674dfff 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap @@ -1,5 +1,5 @@ --- source: apollo-router/tests/integration/traffic_shaping.rs -expression: response.text().await? +expression: response --- -"{\"data\":null,\"errors\":[{\"message\":\"Request timed out\",\"extensions\":{\"code\":\"REQUEST_TIMEOUT\"}}]}" +"{\"data\":null,\"errors\":[{\"message\":\"Request timed out\",\"path\":[],\"extensions\":{\"code\":\"REQUEST_TIMEOUT\"}}]}" diff --git a/apollo-router/tests/integration/subgraph_response.rs b/apollo-router/tests/integration/subgraph_response.rs index 5e6e831d3c..2dd8fc68d6 100644 --- a/apollo-router/tests/integration/subgraph_response.rs +++ b/apollo-router/tests/integration/subgraph_response.rs @@ -118,6 +118,7 @@ async fn test_invalid_error_locations() -> Result<(), BoxError> { "data": null, "errors": [{ "message":"service 'products' response was malformed: invalid `locations` within error: invalid type: boolean `true`, expected u32", + "path": [], "extensions": { "service": "products", "reason": "invalid `locations` within error: invalid type: boolean `true`, expected u32", diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/skipped.json b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json similarity index 96% rename from apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/skipped.json rename to apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json index c08682fff8..1bb1bc0210 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/skipped.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json @@ -172,15 +172,23 @@ }, "expected_response":{ "data":{ - "topProducts":[{"reviews":null},{"reviews":null}] + "topProducts":[ + {"reviews": [{ + "body": "A" + },{ + "body": "B" + }]}, + {"reviews":null}] }, "errors":[ { "message":"HTTP fetch failed from 'invalidation-entity-key-reviews': 500: Internal Server Error", + "path": ["topProducts", 1], "extensions":{"code":"SUBREQUEST_HTTP_ERROR","service":"invalidation-entity-key-reviews","reason":"500: Internal Server Error","http":{"status":500}} }, { "message":"service 'invalidation-entity-key-reviews' response was malformed: {}", + "path": ["topProducts", 1], "extensions":{"service":"invalidation-entity-key-reviews","reason":"{}","code":"SUBREQUEST_MALFORMED_RESPONSE"} } ] diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index fe7d87ed71..e3fd0d5264 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -472,7 +472,7 @@ impl TestExecution { writeln!(out, "assertion `left == right` failed").unwrap(); writeln!( out, - " expected: {}", + "expected: {}", serde_json::to_string(&expected_response).unwrap() ) .unwrap(); diff --git a/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx b/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx index 37c63e8b57..d29cbf1fca 100644 --- a/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx +++ b/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx @@ -66,6 +66,8 @@ The coprocessor operations metric has the following attributes: - `apollo.router.query_planning.plan.duration` - Histogram of plan durations isolated to query planning time only. - `apollo.router.query_planning.total.duration` - Histogram of plan durations including queue time. - `apollo.router.query_planning.queued` - A gauge of the number of queued plans requests. +- `apollo.router.v8.heap.used` - heap memory used by V8, in bytes. +- `apollo.router.v8.heap.total` - total heap allocated by V8, in bytes. ### Uplink diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 451ea09375..781b40cc11 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,7 +20,7 @@ reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.27+v2.8.1" +router-bridge = "=0.5.30+v2.8.3" [dev-dependencies] anyhow = "1" diff --git a/fuzz/subgraph/Cargo.toml b/fuzz/subgraph/Cargo.toml index a6be7c3c72..e04f35066e 100644 --- a/fuzz/subgraph/Cargo.toml +++ b/fuzz/subgraph/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -actix-web = { version = "4", features = ["default"] } -async-graphql = "5" -async-graphql-actix-web = "5" -env_logger = "0.9.0" +axum = "0.6.20" +async-graphql = "6" +async-graphql-axum = "6" +env_logger = "0.10" futures = "0.3.17" lazy_static = "1.4.0" log = "0.4.16" @@ -15,3 +15,4 @@ moka = { version = "0.8.5", features = ["future"] } rand = { version = "0.8.5", features = ["std_rng"] } serde_json = "1.0.79" tokio = { version = "1.22.0", features = ["time", "full"] } +tower = "0.4.0" diff --git a/fuzz/subgraph/src/main.rs b/fuzz/subgraph/src/main.rs index d6e497a9a6..0be9550c7c 100644 --- a/fuzz/subgraph/src/main.rs +++ b/fuzz/subgraph/src/main.rs @@ -1,66 +1,37 @@ -use std::time::Duration; - -use actix_web::get; -use actix_web::post; -use actix_web::web; -use actix_web::web::Data; -use actix_web::App; -use actix_web::HttpResponse; -use actix_web::HttpServer; -use actix_web::Result; -use async_graphql::http::playground_source; -use async_graphql::http::GraphQLPlaygroundConfig; use async_graphql::EmptySubscription; -use async_graphql::Schema; -use async_graphql_actix_web::GraphQLRequest; +use async_graphql_axum::GraphQLRequest; +use async_graphql_axum::GraphQLResponse; +use axum::routing::post; +use axum::Extension; +use axum::Router; +use tower::ServiceBuilder; use crate::model::Mutation; use crate::model::Query; mod model; -#[post("/")] -async fn index( - schema: web::Data>, - mut req: GraphQLRequest, -) -> HttpResponse { +type Schema = async_graphql::Schema; + +async fn graphql_handler(schema: Extension, mut req: GraphQLRequest) -> GraphQLResponse { //Zero out the random variable req.0.variables.remove(&async_graphql::Name::new("random")); println!("query: {}", req.0.query); - - let response = schema.execute(req.into_inner()).await; - let response_json = serde_json::to_string(&response).unwrap(); - - HttpResponse::Ok() - .content_type("application/json") - .body(response_json) -} - -#[get("*")] -async fn index_playground() -> Result { - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(playground_source( - GraphQLPlaygroundConfig::new("/").subscription_endpoint("/"), - ))) + schema.execute(req.into_inner()).await.into() } #[tokio::main] -async fn main() -> std::io::Result<()> { +async fn main() { env_logger::init(); println!("about to listen to http://localhost:4005"); - HttpServer::new(move || { - let schema = Schema::build(Query, Mutation, EmptySubscription).finish(); - App::new() - .app_data(Data::new(schema)) - //.wrap(EnsureKeepAlive) - //.wrap(DelayFor::default()) - .service(index) - .service(index_playground) - }) - .keep_alive(Duration::from_secs(75)) - .bind("0.0.0.0:4005")? - .run() - .await + let schema = Schema::build(Query, Mutation, EmptySubscription).finish(); + let router = Router::new() + .route("/", post(graphql_handler)) + .layer(ServiceBuilder::new().layer(Extension(schema))); + + axum::Server::bind(&"0.0.0.0:4005".parse().expect("Fixed address is valid")) + .serve(router.into_make_service()) + .await + .expect("Server failed to start") }