diff --git a/Cargo.lock b/Cargo.lock index c1126f45..168bfb9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arraydeque" version = "0.5.1" @@ -758,6 +764,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -1204,6 +1221,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" name = "hive-router" version = "0.0.9" dependencies = [ + "arc-swap", "async-trait", "futures", "graphql-parser", @@ -1219,6 +1237,7 @@ dependencies = [ "moka", "ntex", "rand", + "reqwest", "serde", "sonic-rs", "thiserror 2.0.16", @@ -1381,6 +1400,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1403,6 +1438,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1410,12 +1446,16 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1442,12 +1482,119 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1492,6 +1639,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1562,6 +1725,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -2286,6 +2455,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2498,6 +2676,60 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.8.11" @@ -2583,6 +2815,39 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2966,6 +3231,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions_next" version = "1.1.2" @@ -3012,6 +3283,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -3039,6 +3316,41 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "tagptr" @@ -3147,6 +3459,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3213,6 +3535,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3314,6 +3646,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3477,12 +3827,36 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.18.1" @@ -3816,6 +4190,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -3967,6 +4352,12 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -3984,6 +4375,30 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -4003,3 +4418,63 @@ dependencies = [ "quote", "syn 2.0.106", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/Cargo.toml b/Cargo.toml index 23d11c65..83bba319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,4 @@ xxhash-rust = { version = "0.8.15", features = ["xxh3"] } tokio = { version = "1.47.1", features = ["full"] } tokio-util = { version = "0.7.16" } rand = "0.9.2" +reqwest = "0.12.23" diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml index f9e8c5b0..fbb27b1a 100644 --- a/bin/router/Cargo.toml +++ b/bin/router/Cargo.toml @@ -25,6 +25,7 @@ futures = { workspace = true } graphql-parser = { workspace = true } graphql-tools = { workspace = true } serde = { workspace = true } +reqwest = { workspace = true } sonic-rs = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } @@ -42,3 +43,4 @@ mimalloc = { version = "0.1.47", features = ["override"] } moka = { version = "0.12.10", features = ["future"] } ulid = "1.2.1" ntex = { version = "2", features = ["tokio"] } +arc-swap = "1.7.1" diff --git a/bin/router/src/lib.rs b/bin/router/src/lib.rs index fc484629..27347dba 100644 --- a/bin/router/src/lib.rs +++ b/bin/router/src/lib.rs @@ -2,6 +2,8 @@ mod http_utils; mod logger; mod pipeline; mod shared_state; +mod supergraph; +mod supergraph_mgr; use std::sync::Arc; @@ -10,6 +12,7 @@ use crate::{ logger::configure_logging, pipeline::graphql_request_handler, shared_state::RouterSharedState, + supergraph_mgr::SupergraphManager, }; use hive_router_config::load_config; @@ -18,14 +21,15 @@ use ntex::{ web::{self, HttpRequest}, }; -use hive_router_query_planner::utils::parsing::parse_schema; - async fn graphql_endpoint_handler( mut request: HttpRequest, body_bytes: Bytes, + supergraph_manager: web::types::State>, app_state: web::types::State>, ) -> impl web::Responder { - graphql_request_handler(&mut request, body_bytes, app_state.get_ref()).await + let supergraph = supergraph_manager.current(); + + graphql_request_handler(&mut request, body_bytes, &supergraph, app_state.get_ref()).await } pub async fn router_entrypoint() -> Result<(), Box> { @@ -33,14 +37,15 @@ pub async fn router_entrypoint() -> Result<(), Box> { let router_config = load_config(config_path)?; configure_logging(&router_config.log); - let supergraph_sdl = router_config.supergraph.load().await?; - let parsed_schema = parse_schema(&supergraph_sdl); let addr = router_config.http.address(); - let shared_state = RouterSharedState::new(parsed_schema, router_config); + + let supergraph_manager = Arc::new(SupergraphManager::new_from_config(&router_config).await?); + let shared_state = Arc::new(RouterSharedState::new(router_config)); web::HttpServer::new(move || { web::App::new() .state(shared_state.clone()) + .state(supergraph_manager.clone()) .route("/graphql", web::to(graphql_endpoint_handler)) .route("/health", web::to(health_check_handler)) .default_service(web::to(landing_page_handler)) diff --git a/bin/router/src/pipeline/coerce_variables.rs b/bin/router/src/pipeline/coerce_variables.rs index c1dc0b6b..4d80199f 100644 --- a/bin/router/src/pipeline/coerce_variables.rs +++ b/bin/router/src/pipeline/coerce_variables.rs @@ -11,7 +11,7 @@ use tracing::{error, trace, warn}; use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::execution_request::ExecutionRequest; use crate::pipeline::normalize::GraphQLNormalizationPayload; -use crate::shared_state::RouterSharedState; +use crate::supergraph_mgr::SupergraphData; #[derive(Clone, Debug)] pub struct CoerceVariablesPayload { @@ -21,7 +21,7 @@ pub struct CoerceVariablesPayload { #[inline] pub fn coerce_request_variables( req: &HttpRequest, - app_state: &Arc, + supergraph: &Arc, execution_params: ExecutionRequest, normalized_operation: &Arc, ) -> Result { @@ -38,7 +38,7 @@ pub fn coerce_request_variables( match collect_variables( &normalized_operation.operation_for_plan, execution_params.variables, - &app_state.schema_metadata, + &supergraph.metadata, ) { Ok(values) => { trace!( diff --git a/bin/router/src/pipeline/execution.rs b/bin/router/src/pipeline/execution.rs index 8e126228..4a32d778 100644 --- a/bin/router/src/pipeline/execution.rs +++ b/bin/router/src/pipeline/execution.rs @@ -5,6 +5,7 @@ use crate::pipeline::coerce_variables::CoerceVariablesPayload; use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::normalize::GraphQLNormalizationPayload; use crate::shared_state::RouterSharedState; +use crate::supergraph_mgr::SupergraphData; use hive_router_plan_executor::execute_query_plan; use hive_router_plan_executor::execution::plan::QueryPlanExecutionContext; use hive_router_plan_executor::introspection::resolve::IntrospectionContext; @@ -25,6 +26,7 @@ enum ExposeQueryPlanMode { #[inline] pub async fn execute_plan( req: &mut HttpRequest, + supergraph: &Arc, app_state: &Arc, normalized_payload: &Arc, query_plan_payload: &Arc, @@ -57,8 +59,8 @@ pub async fn execute_plan( let introspection_context = IntrospectionContext { query: normalized_payload.operation_for_introspection.as_ref(), - schema: &app_state.planner.consumer_schema.document, - metadata: &app_state.schema_metadata, + schema: &supergraph.planner.consumer_schema.document, + metadata: &supergraph.metadata, }; execute_query_plan(QueryPlanExecutionContext { @@ -68,7 +70,7 @@ pub async fn execute_plan( extensions, introspection_context: &introspection_context, operation_type_name: normalized_payload.root_type_name, - executors: &app_state.subgraph_executor_map, + executors: &supergraph.subgraph_executor_map, }) .await .map(Bytes::from) diff --git a/bin/router/src/pipeline/mod.rs b/bin/router/src/pipeline/mod.rs index f234f9cc..9a601415 100644 --- a/bin/router/src/pipeline/mod.rs +++ b/bin/router/src/pipeline/mod.rs @@ -24,6 +24,7 @@ use crate::{ validation::validate_operation_with_cache, }, shared_state::RouterSharedState, + supergraph_mgr::SupergraphData, }; pub mod coerce_variables; @@ -43,6 +44,7 @@ static GRAPHIQL_HTML: &str = include_str!("../../static/graphiql.html"); pub async fn graphql_request_handler( req: &mut HttpRequest, body_bytes: Bytes, + supergraph: &Arc, state: &Arc, ) -> impl web::Responder { if req.method() == Method::GET && req.accepts_content_type(*TEXT_HTML_CONTENT_TYPE) { @@ -51,7 +53,7 @@ pub async fn graphql_request_handler( .body(GRAPHIQL_HTML); } - match execute_pipeline(req, body_bytes, state).await { + match execute_pipeline(req, body_bytes, supergraph, state).await { Ok(response_bytes) => { let response_content_type: &'static HeaderValue = if req.accepts_content_type(*APPLICATION_GRAPHQL_RESPONSE_JSON_STR) { @@ -72,23 +74,26 @@ pub async fn graphql_request_handler( pub async fn execute_pipeline( req: &mut HttpRequest, body_bytes: Bytes, + supergraph: &Arc, state: &Arc, ) -> Result { let execution_request = get_execution_request(req, body_bytes).await?; let parser_payload = parse_operation_with_cache(req, state, &execution_request).await?; - validate_operation_with_cache(req, state, &parser_payload).await?; + validate_operation_with_cache(req, supergraph, state, &parser_payload).await?; let progressive_override_ctx = request_override_context()?; let normalize_payload = - normalize_request_with_cache(req, state, &execution_request, &parser_payload).await?; + normalize_request_with_cache(req, supergraph, state, &execution_request, &parser_payload) + .await?; let variable_payload = - coerce_request_variables(req, state, execution_request, &normalize_payload)?; + coerce_request_variables(req, supergraph, execution_request, &normalize_payload)?; let query_plan_cancellation_token = CancellationToken::with_timeout(state.router_config.query_planner.timeout); let query_plan_payload = plan_operation_with_cache( req, + supergraph, state, &normalize_payload, &progressive_override_ctx, @@ -98,6 +103,7 @@ pub async fn execute_pipeline( let execution_result = execute_plan( req, + supergraph, state, &normalize_payload, &query_plan_payload, diff --git a/bin/router/src/pipeline/normalize.rs b/bin/router/src/pipeline/normalize.rs index 2d99f3a2..4d0f0cba 100644 --- a/bin/router/src/pipeline/normalize.rs +++ b/bin/router/src/pipeline/normalize.rs @@ -12,6 +12,7 @@ use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, Pipel use crate::pipeline::execution_request::ExecutionRequest; use crate::pipeline::parser::GraphQLParserPayload; use crate::shared_state::RouterSharedState; +use crate::supergraph_mgr::SupergraphData; use tracing::{error, trace}; #[derive(Debug)] @@ -26,6 +27,7 @@ pub struct GraphQLNormalizationPayload { #[inline] pub async fn normalize_request_with_cache( req: &HttpRequest, + supergraph: &Arc, app_state: &Arc, execution_params: &ExecutionRequest, parser_payload: &GraphQLParserPayload, @@ -51,7 +53,7 @@ pub async fn normalize_request_with_cache( Ok(payload) } None => match normalize_operation( - &app_state.planner.supergraph, + &supergraph.planner.supergraph, &parser_payload.parsed_operation, execution_params.operation_name.as_deref(), ) { @@ -64,7 +66,7 @@ pub async fn normalize_request_with_cache( let operation = doc.operation; let (root_type_name, projection_plan) = - FieldProjectionPlan::from_operation(&operation, &app_state.schema_metadata); + FieldProjectionPlan::from_operation(&operation, &supergraph.metadata); let partitioned_operation = partition_operation(operation); let payload = GraphQLNormalizationPayload { diff --git a/bin/router/src/pipeline/query_plan.rs b/bin/router/src/pipeline/query_plan.rs index 58b0475e..cf7a619e 100644 --- a/bin/router/src/pipeline/query_plan.rs +++ b/bin/router/src/pipeline/query_plan.rs @@ -5,6 +5,7 @@ use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, Pipel use crate::pipeline::normalize::GraphQLNormalizationPayload; use crate::pipeline::progressive_override::{RequestOverrideContext, StableOverrideContext}; use crate::shared_state::RouterSharedState; +use crate::supergraph_mgr::SupergraphData; use hive_router_query_planner::planner::plan_nodes::QueryPlan; use hive_router_query_planner::utils::cancellation::CancellationToken; use ntex::web::HttpRequest; @@ -13,13 +14,14 @@ use xxhash_rust::xxh3::Xxh3; #[inline] pub async fn plan_operation_with_cache( req: &HttpRequest, + supergraph: &Arc, app_state: &Arc, normalized_operation: &Arc, request_override_context: &RequestOverrideContext, cancellation_token: &CancellationToken, ) -> Result, PipelineError> { let stable_override_context = - StableOverrideContext::new(&app_state.planner.supergraph, request_override_context); + StableOverrideContext::new(&supergraph.planner.supergraph, request_override_context); let filtered_operation_for_plan = &normalized_operation.operation_for_plan; let plan_cache_key = @@ -37,7 +39,7 @@ pub async fn plan_operation_with_cache( })); } - app_state + supergraph .planner .plan_from_normalized_operation( filtered_operation_for_plan, diff --git a/bin/router/src/pipeline/validation.rs b/bin/router/src/pipeline/validation.rs index 48a1093f..1515d603 100644 --- a/bin/router/src/pipeline/validation.rs +++ b/bin/router/src/pipeline/validation.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::parser::GraphQLParserPayload; use crate::shared_state::RouterSharedState; +use crate::supergraph_mgr::SupergraphData; use graphql_tools::validation::validate::validate; use ntex::web::HttpRequest; use tracing::{error, trace}; @@ -10,10 +11,11 @@ use tracing::{error, trace}; #[inline] pub async fn validate_operation_with_cache( req: &HttpRequest, + supergraph: &Arc, app_state: &Arc, parser_payload: &GraphQLParserPayload, ) -> Result<(), PipelineError> { - let consumer_schema_ast = &app_state.planner.consumer_schema.document; + let consumer_schema_ast = &supergraph.planner.consumer_schema.document; let validation_result = match app_state .validate_cache diff --git a/bin/router/src/shared_state.rs b/bin/router/src/shared_state.rs index a5e6fa1e..c25dcfea 100644 --- a/bin/router/src/shared_state.rs +++ b/bin/router/src/shared_state.rs @@ -1,25 +1,14 @@ use std::sync::Arc; -use graphql_parser::schema::Document; use graphql_tools::validation::{utils::ValidationError, validate::ValidationPlan}; use hive_router_config::HiveRouterConfig; -use hive_router_plan_executor::{ - introspection::schema::{SchemaMetadata, SchemaWithMetadata}, - SubgraphExecutorMap, -}; -use hive_router_query_planner::{ - planner::{plan_nodes::QueryPlan, Planner}, - state::supergraph_state::SupergraphState, -}; +use hive_router_query_planner::planner::plan_nodes::QueryPlan; use moka::future::Cache; use crate::pipeline::normalize::GraphQLNormalizationPayload; pub struct RouterSharedState { - pub schema_metadata: SchemaMetadata, - pub planner: Planner, pub validation_plan: ValidationPlan, - pub subgraph_executor_map: SubgraphExecutorMap, pub plan_cache: Cache>, pub validate_cache: Cache>>, pub parse_cache: Cache>>, @@ -28,31 +17,14 @@ pub struct RouterSharedState { } impl RouterSharedState { - pub fn new( - parsed_supergraph_sdl: Document<'static, String>, - router_config: HiveRouterConfig, - ) -> Arc { - let supergraph_state = SupergraphState::new(&parsed_supergraph_sdl); - let planner = - Planner::new_from_supergraph(&parsed_supergraph_sdl).expect("failed to create planner"); - let schema_metadata = planner.consumer_schema.schema_metadata(); - - let subgraph_executor_map = SubgraphExecutorMap::from_http_endpoint_map( - supergraph_state.subgraph_endpoint_map, - router_config.traffic_shaping.clone(), - ) - .expect("Failed to create subgraph executor map"); - - Arc::new(Self { - schema_metadata, - planner, + pub fn new(router_config: HiveRouterConfig) -> Self { + Self { validation_plan: graphql_tools::validation::rules::default_rules_validation_plan(), - subgraph_executor_map, plan_cache: moka::future::Cache::new(1000), validate_cache: moka::future::Cache::new(1000), parse_cache: moka::future::Cache::new(1000), normalize_cache: moka::future::Cache::new(1000), router_config, - }) + } } } diff --git a/bin/router/src/supergraph/base.rs b/bin/router/src/supergraph/base.rs new file mode 100644 index 00000000..e1da1605 --- /dev/null +++ b/bin/router/src/supergraph/base.rs @@ -0,0 +1,15 @@ +use async_trait::async_trait; + +#[derive(Debug, thiserror::Error)] +pub enum LoadSupergraphError { + #[error("Failed to read supergraph file: {0}")] + ReadFileError(#[from] std::io::Error), + #[error("Failed to read supergraph from network: {0}")] + NetworkError(#[from] reqwest::Error), +} + +#[async_trait] +pub trait SupergraphLoader { + async fn reload(&mut self) -> Result<(), LoadSupergraphError>; + fn current(&self) -> Option<&str>; +} diff --git a/bin/router/src/supergraph/file.rs b/bin/router/src/supergraph/file.rs new file mode 100644 index 00000000..ff8689c0 --- /dev/null +++ b/bin/router/src/supergraph/file.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use hive_router_config::primitives::file_path::FilePath; +use tracing::{debug, trace}; + +use crate::supergraph::base::{LoadSupergraphError, SupergraphLoader}; + +pub struct SupergraphFileLoader { + file_path: FilePath, + current: Option, +} + +#[async_trait] +impl SupergraphLoader for SupergraphFileLoader { + async fn reload(&mut self) -> Result<(), LoadSupergraphError> { + debug!( + "Reloading supergraph from file path: '{}'", + self.file_path.0 + ); + let content = tokio::fs::read_to_string(&self.file_path.0).await?; + trace!( + "Supergraph loaded from file path: '{}', content: {}", + self.file_path.0, + content + ); + + self.current = Some(content); + Ok(()) + } + + fn current(&self) -> Option<&str> { + self.current.as_deref() + } +} + +impl SupergraphFileLoader { + pub async fn new(file_path: &FilePath) -> Result, LoadSupergraphError> { + debug!( + "Creating supergraph source from file path: '{}'", + file_path.0 + ); + + Ok(Box::new(Self { + file_path: file_path.clone(), + current: None, + })) + } +} diff --git a/bin/router/src/supergraph/hive.rs b/bin/router/src/supergraph/hive.rs new file mode 100644 index 00000000..3a7dc785 --- /dev/null +++ b/bin/router/src/supergraph/hive.rs @@ -0,0 +1,64 @@ +use async_trait::async_trait; +use http::header::USER_AGENT; +use tracing::debug; + +use crate::supergraph::base::{LoadSupergraphError, SupergraphLoader}; + +static USER_AGENT_VALUE: &str = "hive-router"; +static TIMEOUT: u64 = 10; +static AUTH_HEADER_NAME: &str = "X-Hive-CDN-Key"; + +pub struct SupergraphHiveConsoleLoader { + endpoint: String, + key: String, + current: Option, + http_client: reqwest::Client, +} + +#[async_trait] +impl SupergraphLoader for SupergraphHiveConsoleLoader { + async fn reload(&mut self) -> Result<(), LoadSupergraphError> { + debug!( + "Fetching supergraph from Hive Console CDN: '{}'", + self.endpoint + ); + + let response = self + .http_client + .get(&self.endpoint) + .header(AUTH_HEADER_NAME, &self.key) + .header(USER_AGENT, USER_AGENT_VALUE) + .timeout(std::time::Duration::from_secs(TIMEOUT)) + .send() + .await? + .error_for_status()?; + + let content = response + .text() + .await + .map_err(LoadSupergraphError::NetworkError)?; + + self.current = Some(content); + Ok(()) + } + + fn current(&self) -> Option<&str> { + self.current.as_deref() + } +} + +impl SupergraphHiveConsoleLoader { + pub async fn new(endpoint: &str, key: &str) -> Result, LoadSupergraphError> { + debug!( + "Creating supergraph source from Hive Console CDN: '{}'", + endpoint + ); + + Ok(Box::new(Self { + endpoint: endpoint.to_string(), + key: key.to_string(), + current: None, + http_client: reqwest::Client::new(), + })) + } +} diff --git a/bin/router/src/supergraph/mod.rs b/bin/router/src/supergraph/mod.rs new file mode 100644 index 00000000..4fbb66bc --- /dev/null +++ b/bin/router/src/supergraph/mod.rs @@ -0,0 +1,25 @@ +use hive_router_config::supergraph::SupergraphSource; + +use crate::supergraph::{ + base::{LoadSupergraphError, SupergraphLoader}, + file::SupergraphFileLoader, + hive::SupergraphHiveConsoleLoader, +}; +use tracing::debug; + +pub mod base; +pub mod file; +pub mod hive; + +pub async fn resolve_from_config( + config: &SupergraphSource, +) -> Result, LoadSupergraphError> { + debug!("Resolving supergraph from config: {:?}", config); + + match config { + SupergraphSource::File { path } => Ok(SupergraphFileLoader::new(path).await?), + SupergraphSource::HiveConsole { endpoint, key } => { + Ok(SupergraphHiveConsoleLoader::new(endpoint, key).await?) + } + } +} diff --git a/bin/router/src/supergraph_mgr.rs b/bin/router/src/supergraph_mgr.rs new file mode 100644 index 00000000..08ed598e --- /dev/null +++ b/bin/router/src/supergraph_mgr.rs @@ -0,0 +1,86 @@ +use std::sync::Arc; + +use arc_swap::{ArcSwap, Guard}; +use hive_router_config::HiveRouterConfig; +use hive_router_plan_executor::{ + executors::error::SubgraphExecutorError, + introspection::schema::{SchemaMetadata, SchemaWithMetadata}, + SubgraphExecutorMap, +}; +use hive_router_query_planner::{ + planner::{Planner, PlannerError}, + state::supergraph_state::SupergraphState, + utils::parsing::parse_schema, +}; + +use crate::supergraph::{ + base::{LoadSupergraphError, SupergraphLoader}, + resolve_from_config, +}; + +pub struct SupergraphManager { + current: ArcSwap, + #[allow(dead_code)] + loader: Box, +} + +pub struct SupergraphData { + pub metadata: SchemaMetadata, + pub planner: Planner, + pub subgraph_executor_map: SubgraphExecutorMap, +} + +#[derive(Debug, thiserror::Error)] +pub enum SupergraphManagerError { + #[error("Failed to load supergraph: {0}")] + LoadSupergraphError(#[from] LoadSupergraphError), + #[error("Failed to build planner: {0}")] + PlannerBuilderError(#[from] PlannerError), + #[error("Failed to init executor: {0}")] + ExecutorInitError(#[from] SubgraphExecutorError), + #[error("Unexpected: failed to load initial supergraph")] + FailedToLoadInitialSupergraph, +} + +impl SupergraphManager { + pub async fn new_from_config( + router_config: &HiveRouterConfig, + ) -> Result { + let mut loader = resolve_from_config(&router_config.supergraph).await?; + loader.reload().await?; + let supergraph_sdl = loader + .current() + .ok_or_else(|| SupergraphManagerError::FailedToLoadInitialSupergraph)?; + let current_data = Self::build_data(router_config, supergraph_sdl)?; + let swappable_data = ArcSwap::from(Arc::new(current_data)); + + Ok(Self { + current: swappable_data, + loader, + }) + } + + fn build_data( + router_config: &HiveRouterConfig, + supergraph_sdl: &str, + ) -> Result { + let parsed_supergraph_sdl = parse_schema(supergraph_sdl); + let supergraph_state = SupergraphState::new(&parsed_supergraph_sdl); + let planner = Planner::new_from_supergraph(&parsed_supergraph_sdl)?; + let metadata = planner.consumer_schema.schema_metadata(); + let subgraph_executor_map = SubgraphExecutorMap::from_http_endpoint_map( + supergraph_state.subgraph_endpoint_map, + router_config.traffic_shaping.clone(), + )?; + + Ok(SupergraphData { + metadata, + planner, + subgraph_executor_map, + }) + } + + pub fn current(&self) -> Guard> { + self.current.load() + } +} diff --git a/docs/README.md b/docs/README.md index c2f2f0c8..23741e7f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -124,6 +124,20 @@ The path can be either absolute or relative to the router's working directory. |**source**|`string`|Constant Value: `"file"`
|yes| +  +**Option 2 (alternative):** +Loads a supergraph from Hive Console CDN. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**endpoint**|`string`||yes| +|**key**|`string`||yes| +|**source**|`string`|Constant Value: `"hive"`
|yes| + + **Example** ```yaml diff --git a/lib/router-config/src/supergraph.rs b/lib/router-config/src/supergraph.rs index 6b36778c..0b1882f9 100644 --- a/lib/router-config/src/supergraph.rs +++ b/lib/router-config/src/supergraph.rs @@ -3,13 +3,17 @@ use serde::{Deserialize, Serialize}; use crate::primitives::file_path::FilePath; -#[derive(Deserialize, Serialize, JsonSchema)] +#[derive(Deserialize, Serialize, JsonSchema, Debug)] #[serde(tag = "source")] pub enum SupergraphSource { /// Loads a supergraph from the filesystem. /// The path can be either absolute or relative to the router's working directory. #[serde(rename = "file")] File { path: FilePath }, + /// Loads a supergraph from Hive Console CDN. + /// + #[serde(rename = "hive")] + HiveConsole { endpoint: String, key: String }, } impl Default for SupergraphSource { @@ -19,19 +23,3 @@ impl Default for SupergraphSource { } } } - -impl SupergraphSource { - pub async fn load(&self) -> Result> { - match self { - SupergraphSource::File { path } => { - let supergraph_sdl = std::fs::read_to_string(&path.0).map_err(|e| { - std::io::Error::new( - e.kind(), - format!("Failed to read supergraph file '{}': {}", path.0, e), - ) - })?; - Ok(supergraph_sdl) - } - } - } -}