From 93050d46a81c7a34846a85a930bd6834cc65206b Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Tue, 15 Oct 2024 14:07:00 +0100 Subject: [PATCH 001/112] FLEET-19 Update filter and add new dependencies --- Cargo.lock | 77 +++++++++++++++++++++++++++++ apollo-router/Cargo.toml | 1 + apollo-router/src/metrics/filter.rs | 3 +- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 192ac8fc00..bda4735e66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,7 @@ dependencies = [ "static_assertions", "strum_macros 0.26.4", "sys-info", + "sysinfo", "tempfile", "test-log", "thiserror", @@ -4193,6 +4194,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -6373,6 +6383,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -7545,6 +7569,59 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index ced84ad29f..b69b9d7fc8 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -208,6 +208,7 @@ serde_yaml = "0.8.26" static_assertions = "1.1.0" strum_macros = "0.26.0" sys-info = "0.9.1" +sysinfo = { version = "0.32.0", features = ["windows"] } thiserror = "1.0.61" tokio.workspace = true tokio-stream = { version = "0.1.15", features = ["sync", "net"] } diff --git a/apollo-router/src/metrics/filter.rs b/apollo-router/src/metrics/filter.rs index fa6dad38df..529b25883a 100644 --- a/apollo-router/src/metrics/filter.rs +++ b/apollo-router/src/metrics/filter.rs @@ -94,7 +94,7 @@ impl FilterMeterProvider { .delegate(delegate) .allow( Regex::new( - r"apollo\.(graphos\.cloud|router\.(operations?|lifecycle|config|schema|query|query_planning|telemetry))(\..*|$)|apollo_router_uplink_fetch_count_total|apollo_router_uplink_fetch_duration_seconds", + r"apollo\.(graphos\.cloud|router\.(operations?|lifecycle|config|schema|query|query_planning|telemetry|instance))(\..*|$)|apollo_router_uplink_fetch_count_total|apollo_router_uplink_fetch_duration_seconds", ) .expect("regex should have been valid"), ) @@ -244,7 +244,6 @@ impl opentelemetry::metrics::MeterProvider for FilterMeterProvider { #[cfg(test)] mod test { - use opentelemetry::metrics::MeterProvider; use opentelemetry::metrics::Unit; use opentelemetry::runtime; From 107d37d843c3573b71b6248d148a5b243764a56c Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Tue, 15 Oct 2024 14:07:00 +0100 Subject: [PATCH 002/112] FLEET-19 Add initial take on Fleet Detection Plugin Adds an initial plugin, that loads at startup and emits metrics for three simple cases: cpus, cpu_freq and total_memory. --- apollo-router/src/plugins/fleet_detector.rs | 126 ++++++++++++++++++++ apollo-router/src/plugins/mod.rs | 1 + apollo-router/src/router_factory.rs | 1 + 3 files changed, 128 insertions(+) create mode 100644 apollo-router/src/plugins/fleet_detector.rs diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs new file mode 100644 index 0000000000..78b5d06c99 --- /dev/null +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -0,0 +1,126 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use sysinfo::System; +use tokio::task::JoinHandle; +use tower::BoxError; +use tracing::debug; +use tracing::info; + +use crate::plugin::Plugin; +use crate::plugin::PluginInit; + +#[derive(Debug)] +struct FleetDetector { + handle: JoinHandle<()>, +} + +#[derive(Debug, Default, Deserialize, JsonSchema)] +struct Conf {} + +#[async_trait::async_trait] +impl Plugin for FleetDetector { + type Config = Conf; + + async fn new(_: PluginInit) -> Result { + debug!("beginning environment detection"); + debug!("spawning continuous detector task"); + let handle = tokio::task::spawn(async { + let mut sys = System::new_all(); + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60)); + loop { + interval.tick().await; + sys.refresh_cpu_all(); + sys.refresh_memory(); + detect_cpu_values(&sys); + detect_memory_values(&sys); + } + }); + + Ok(FleetDetector { handle }) + } +} + +impl Drop for FleetDetector { + fn drop(&mut self) { + self.handle.abort(); + } +} + +fn detect_cpu_values(system: &System) { + let cpus = system.cpus(); + let cpu_count = detect_cpu_count(system); + let cpu_freq = cpus.iter().map(|cpu| cpu.frequency()).sum::() / cpus.len() as u64; + info!(value.apollo.router.instance.cpu_freq = cpu_freq); + info!(counter.apollo.router.instance.cpu_count = cpu_count); +} + +#[cfg(not(target_os = "linux"))] +fn detect_cpu_count(system: &System) -> u64 { + system.cpus().len() as u64 +} + +// Because Linux provides CGroups as a way of controlling the proportion of CPU time each +// process gets we can perform slightly more introspection here than simply appealing to the +// raw number of processors. Hence, the extra logic including below. +#[cfg(target_os = "linux")] +fn detect_cpu_count(system: &System) -> u64 { + use std::collections::HashSet; + use std::fs; + + let system_cpus = system.cpus().len() as u64; + // Grab the contents of /proc/filesystems + let fses: HashSet = match fs::read_to_string("/proc/filesystems") { + Ok(content) => content + .lines() + .map(|x| x.split_whitespace().next().unwrap_or("").to_string()) + .filter(|x| x.contains("cgroup")) + .collect(), + Err(_) => return system_cpus, + }; + + if fses.contains("cgroup2") { + // If we're looking at cgroup2 then we need to look in `cpu.max` + match fs::read_to_string("/sys/fs/cgroup/cpu.max") { + Ok(readings) => { + // The format of the file lists the quota first, followed by the period, + // but the quota could also be max which would mean there are no restrictions. + if readings.starts_with("max") { + system_cpus + } else { + // If it's not max then divide the two to get an integer answer + let (a, b) = readings.split_once(' ').unwrap(); + a.parse::().unwrap() / b.parse::().unwrap() + } + } + Err(_) => system_cpus, + } + } else if fses.contains("cgroup") { + // If we're in cgroup v1 then we need to read from two separate files + let quota = fs::read_to_string("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") + .map(|s| String::from(s.trim())) + .ok(); + let period = fs::read_to_string("/sys/fs/cgroup/cpu/cpu.cfs_period_us") + .map(|s| String::from(s.trim())) + .ok(); + match (quota, period) { + (Some(quota), Some(period)) => { + // In v1 quota being -1 indicates no restrictions so return the maximum (all + // system CPUs) otherwise divide the two. + if quota == "-1" { + system_cpus + } else { + quota.parse::().unwrap() / period.parse::().unwrap() + } + } + _ => system_cpus, + } + } else { + system_cpus + } +} + +fn detect_memory_values(system: &System) { + info!(counter.apollo.router.instance.total_memory = system.total_memory()) +} + +register_plugin!("apollo", "fleet_detector", FleetDetector); diff --git a/apollo-router/src/plugins/mod.rs b/apollo-router/src/plugins/mod.rs index beac8037b9..5684d0c3d3 100644 --- a/apollo-router/src/plugins/mod.rs +++ b/apollo-router/src/plugins/mod.rs @@ -28,6 +28,7 @@ pub(crate) mod csrf; mod demand_control; mod expose_query_plan; pub(crate) mod file_uploads; +mod fleet_detector; mod forbid_mutations; mod headers; mod include_subgraph_errors; diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index ca40bd0a86..b3a4bb27b6 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -678,6 +678,7 @@ pub(crate) async fn create_plugins( } add_mandatory_apollo_plugin!("limits"); add_mandatory_apollo_plugin!("traffic_shaping"); + add_mandatory_apollo_plugin!("fleet_detector"); add_optional_apollo_plugin!("forbid_mutations"); add_optional_apollo_plugin!("subscription"); add_optional_apollo_plugin!("override_subgraph_url"); From fefd8a47e420fbebc8c0c005bd9df2f5df1f5f8b Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Wed, 6 Nov 2024 14:48:25 +0000 Subject: [PATCH 003/112] FLEET-19 Fixing CI with new schema generation --- ...nfiguration__tests__schema_generation.snap | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 817d67517a..58e1840af0 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -1266,6 +1266,9 @@ expression: "&schema" "type": "object" }, "Conf5": { + "type": "object" + }, + "Conf6": { "anyOf": [ { "additionalProperties": { @@ -1277,7 +1280,7 @@ expression: "&schema" ], "description": "Subgraph URL mappings" }, - "Conf6": { + "Conf7": { "additionalProperties": false, "description": "Configuration for the Rhai Plugin", "properties": { @@ -1294,7 +1297,7 @@ expression: "&schema" }, "type": "object" }, - "Conf7": { + "Conf8": { "additionalProperties": false, "description": "Telemetry configuration", "properties": { @@ -8323,6 +8326,10 @@ expression: "&schema" "description": "Type conditioned fetching configuration.", "type": "boolean" }, + "fleet_detector": { + "$ref": "#/definitions/Conf5", + "description": "#/definitions/Conf5" + }, "forbid_mutations": { "$ref": "#/definitions/ForbidMutationsConfig", "description": "#/definitions/ForbidMutationsConfig" @@ -8348,8 +8355,8 @@ expression: "&schema" "description": "#/definitions/Config" }, "override_subgraph_url": { - "$ref": "#/definitions/Conf5", - "description": "#/definitions/Conf5" + "$ref": "#/definitions/Conf6", + "description": "#/definitions/Conf6" }, "persisted_queries": { "$ref": "#/definitions/PersistedQueries", @@ -8372,8 +8379,8 @@ expression: "&schema" "description": "#/definitions/Config8" }, "rhai": { - "$ref": "#/definitions/Conf6", - "description": "#/definitions/Conf6" + "$ref": "#/definitions/Conf7", + "description": "#/definitions/Conf7" }, "sandbox": { "$ref": "#/definitions/Sandbox", @@ -8388,8 +8395,8 @@ expression: "&schema" "description": "#/definitions/Supergraph" }, "telemetry": { - "$ref": "#/definitions/Conf7", - "description": "#/definitions/Conf7" + "$ref": "#/definitions/Conf8", + "description": "#/definitions/Conf8" }, "tls": { "$ref": "#/definitions/Tls", From f600f29290c99e2423008cee50f895fbd0bf5cf9 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Mon, 28 Oct 2024 08:33:34 +0000 Subject: [PATCH 004/112] FLEET-19 Use correct metrics form Also improve how we handle refreshing the System object, rather than doing it in either the callback or the async task, contain that within a struct and do it there. --- apollo-router/src/plugins/fleet_detector.rs | 118 ++++++++++++++++---- 1 file changed, 99 insertions(+), 19 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 78b5d06c99..55ab128bae 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -1,42 +1,105 @@ +use std::env::consts::ARCH; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Duration; +use std::time::Instant; + +use opentelemetry::metrics::MeterProvider; +use opentelemetry_api::metrics::ObservableGauge; +use opentelemetry_api::metrics::Unit; use schemars::JsonSchema; use serde::Deserialize; use sysinfo::System; use tokio::task::JoinHandle; use tower::BoxError; use tracing::debug; -use tracing::info; +use crate::metrics::meter_provider; use crate::plugin::Plugin; use crate::plugin::PluginInit; +const REFRESH_INTERVAL: Duration = Duration::from_secs(60); + +#[derive(Debug, Default, Deserialize, JsonSchema)] +struct Conf {} + +#[derive(Debug)] +struct SystemGetter { + system: System, + start: Instant, +} + +impl SystemGetter { + fn new() -> Self { + let mut system = System::new(); + system.refresh_all(); + Self { + system, + start: Instant::now(), + } + } + + fn get_system(&mut self) -> &System { + if self.start.elapsed() < REFRESH_INTERVAL { + &self.system + } else { + self.start = Instant::now(); + self.system.refresh_cpu_all(); + self.system.refresh_memory(); + &self.system + } + } +} + #[derive(Debug)] struct FleetDetector { handle: JoinHandle<()>, + #[allow(dead_code)] + // We have to store a reference to the gauge otherwise it will be dropped once the plugin is + // initialised, even though it still has data to emit + freq_gauge: ObservableGauge, } -#[derive(Debug, Default, Deserialize, JsonSchema)] -struct Conf {} - #[async_trait::async_trait] impl Plugin for FleetDetector { type Config = Conf; async fn new(_: PluginInit) -> Result { - debug!("beginning environment detection"); - debug!("spawning continuous detector task"); - let handle = tokio::task::spawn(async { - let mut sys = System::new_all(); - let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60)); + debug!("beginning environment detection, spawning gauges"); + let system_getter = Arc::new(Mutex::new(SystemGetter::new())); + let meter = meter_provider().meter("apollo/router"); + + let gauge_local_system_getter = system_getter.clone(); + let freq_gauge = meter + .u64_observable_gauge("apollo.router.instance.cpu_freq") + .with_description( + "The CPU frequency of the underlying instance the router is deployed to", + ) + .with_unit(Unit::new("Mhz")) + .with_callback(move |i| { + let mut system_getter = gauge_local_system_getter.lock().unwrap(); + let system = system_getter.get_system(); + let cpus = system.cpus(); + let cpu_freq = + cpus.iter().map(|cpu| cpu.frequency()).sum::() / cpus.len() as u64; + i.observe(cpu_freq, &[]) + }) + .init(); + + debug!("establishing metrics emission task"); + let counter_local_system_getter = system_getter.clone(); + let handle = tokio::task::spawn(async move { + let mut interval = tokio::time::interval(REFRESH_INTERVAL); loop { interval.tick().await; - sys.refresh_cpu_all(); - sys.refresh_memory(); - detect_cpu_values(&sys); - detect_memory_values(&sys); + let mut system_getter = counter_local_system_getter.lock().unwrap(); + let system = system_getter.get_system(); + detect_cpu_values(system); + detect_memory_values(system); } }); - Ok(FleetDetector { handle }) + Ok(FleetDetector { handle, freq_gauge }) } } @@ -47,11 +110,13 @@ impl Drop for FleetDetector { } fn detect_cpu_values(system: &System) { - let cpus = system.cpus(); let cpu_count = detect_cpu_count(system); - let cpu_freq = cpus.iter().map(|cpu| cpu.frequency()).sum::() / cpus.len() as u64; - info!(value.apollo.router.instance.cpu_freq = cpu_freq); - info!(counter.apollo.router.instance.cpu_count = cpu_count); + u64_counter!( + "apollo.router.instance.cpu_count", + "The number of CPUs reported by the instance the router is running on", + cpu_count, + host.arch = get_otel_arch() + ); } #[cfg(not(target_os = "linux"))] @@ -120,7 +185,22 @@ fn detect_cpu_count(system: &System) -> u64 { } fn detect_memory_values(system: &System) { - info!(counter.apollo.router.instance.total_memory = system.total_memory()) + u64_counter!( + "apollo.router.instance.total_memory", + "The amount of memory reported by the instance the router is running on", + system.total_memory() + ); +} + +fn get_otel_arch() -> &'static str { + match ARCH { + "x86_64" => "amd64", + "aarch64" => "arm64", + "arm" => "arm32", + "powerpc" => "ppc32", + "powerpc64" => "ppc64", + a => a, + } } register_plugin!("apollo", "fleet_detector", FleetDetector); From aaa789d20c1c2d409ce1c2a7bed9619182b36413 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Thu, 7 Nov 2024 12:48:37 +0000 Subject: [PATCH 005/112] FLEET-19 Implement new `activate` hook --- apollo-router/src/plugin/mod.rs | 10 ++++++++++ apollo-router/src/services/supergraph/service.rs | 2 ++ 2 files changed, 12 insertions(+) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index 3354868491..646ca06050 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -630,6 +630,9 @@ pub(crate) trait PluginPrivate: Send + Sync + 'static { fn web_endpoints(&self) -> MultiMap { MultiMap::new() } + + /// This is invoked once after the OTEL meter has been refreshed. + async fn activate(&self) {} } #[async_trait] @@ -733,6 +736,9 @@ pub(crate) trait DynPlugin: Send + Sync + 'static { /// Support downcasting #[cfg(test)] fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + + /// This is invoked once after the OTEL meter has been refreshed. + async fn activate(&self) {} } #[async_trait] @@ -783,6 +789,10 @@ where fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + async fn activate(&self) { + self.activate().await + } } /// Register a plugin with a group and a name diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index a217df0bd1..dd9c54d355 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -812,6 +812,8 @@ impl PluggableSupergraphServiceBuilder { for (_, plugin) in self.plugins.iter() { if let Some(telemetry) = plugin.as_any().downcast_ref::() { telemetry.activate(); + } else { + plugin.activate().await; } } From e3e5c2403205b6c56d0cef1b30e30ced7e01c6d4 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Thu, 7 Nov 2024 13:39:20 +0000 Subject: [PATCH 006/112] FLEET-19 Move to gauges Ensures that gauges will survive hot reload --- apollo-router/src/plugins/fleet_detector.rs | 158 ++++++++++++-------- 1 file changed, 95 insertions(+), 63 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 55ab128bae..011ed306ff 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -7,16 +7,17 @@ use std::time::Instant; use opentelemetry::metrics::MeterProvider; use opentelemetry_api::metrics::ObservableGauge; use opentelemetry_api::metrics::Unit; +use opentelemetry_api::KeyValue; use schemars::JsonSchema; use serde::Deserialize; use sysinfo::System; -use tokio::task::JoinHandle; +use tokio::sync::mpsc::Sender; use tower::BoxError; use tracing::debug; use crate::metrics::meter_provider; -use crate::plugin::Plugin; use crate::plugin::PluginInit; +use crate::plugin::PluginPrivate; const REFRESH_INTERVAL: Duration = Duration::from_secs(60); @@ -51,74 +52,113 @@ impl SystemGetter { } } +struct GaugeStore { + gauges: Vec>, +} + +impl GaugeStore { + fn new() -> Self { + GaugeStore { gauges: Vec::new() } + } + fn initialize_gauges(&mut self, system_getter: Arc>) { + let meter = meter_provider().meter("apollo/router"); + { + let system_getter = system_getter.clone(); + self.gauges.push( + meter + .u64_observable_gauge("apollo.router.instance.cpu_freq") + .with_description( + "The CPU frequency of the underlying instance the router is deployed to", + ) + .with_unit(Unit::new("Mhz")) + .with_callback(move |i| { + let local_system_getter = system_getter.clone(); + let mut system_getter = local_system_getter.lock().unwrap(); + let system = system_getter.get_system(); + let cpus = system.cpus(); + let cpu_freq = + cpus.iter().map(|cpu| cpu.frequency()).sum::() / cpus.len() as u64; + i.observe(cpu_freq, &[]) + }) + .init(), + ); + } + { + let system_getter = system_getter.clone(); + self.gauges.push( + meter + .u64_observable_gauge("apollo.router.instance.cpu_count") + .with_description( + "The number of CPUs reported by the instance the router is running on", + ) + .with_callback(move |i| { + let local_system_getter = system_getter.clone(); + let mut system_getter = local_system_getter.lock().unwrap(); + let system = system_getter.get_system(); + let cpu_count = detect_cpu_count(system); + i.observe(cpu_count, &[KeyValue::new("host.arch", get_otel_arch())]) + }) + .init(), + ); + } + { + let system_getter = system_getter.clone(); + self.gauges.push( + meter + .u64_observable_gauge("apollo.router.instance.total_memory") + .with_description( + "The amount of memory reported by the instance the router is running on", + ) + .with_callback(move |i| { + let local_system_getter = system_getter.clone(); + let mut system_getter = local_system_getter.lock().unwrap(); + let system = system_getter.get_system(); + i.observe( + system.total_memory(), + &[KeyValue::new("host.arch", get_otel_arch())], + ) + }) + .with_unit(Unit::new("bytes")) + .init(), + ); + } + } +} + #[derive(Debug)] struct FleetDetector { - handle: JoinHandle<()>, #[allow(dead_code)] - // We have to store a reference to the gauge otherwise it will be dropped once the plugin is - // initialised, even though it still has data to emit - freq_gauge: ObservableGauge, + gauge_initializer: Sender<()>, } - #[async_trait::async_trait] -impl Plugin for FleetDetector { +impl PluginPrivate for FleetDetector { type Config = Conf; async fn new(_: PluginInit) -> Result { - debug!("beginning environment detection, spawning gauges"); - let system_getter = Arc::new(Mutex::new(SystemGetter::new())); - let meter = meter_provider().meter("apollo/router"); - - let gauge_local_system_getter = system_getter.clone(); - let freq_gauge = meter - .u64_observable_gauge("apollo.router.instance.cpu_freq") - .with_description( - "The CPU frequency of the underlying instance the router is deployed to", - ) - .with_unit(Unit::new("Mhz")) - .with_callback(move |i| { - let mut system_getter = gauge_local_system_getter.lock().unwrap(); - let system = system_getter.get_system(); - let cpus = system.cpus(); - let cpu_freq = - cpus.iter().map(|cpu| cpu.frequency()).sum::() / cpus.len() as u64; - i.observe(cpu_freq, &[]) - }) - .init(); - - debug!("establishing metrics emission task"); - let counter_local_system_getter = system_getter.clone(); - let handle = tokio::task::spawn(async move { - let mut interval = tokio::time::interval(REFRESH_INTERVAL); - loop { - interval.tick().await; - let mut system_getter = counter_local_system_getter.lock().unwrap(); - let system = system_getter.get_system(); - detect_cpu_values(system); - detect_memory_values(system); + debug!("initialising fleet detection plugin"); + let (gauge_initializer, mut rx) = tokio::sync::mpsc::channel(1); + + debug!("spawning gauge initializer task"); + tokio::spawn(async move { + let mut gauge_store = GaugeStore::new(); + let system_getter = Arc::new(Mutex::new(SystemGetter::new())); + while rx.recv().await.is_some() { + let system_getter = system_getter.clone(); + gauge_store.initialize_gauges(system_getter); } }); - Ok(FleetDetector { handle, freq_gauge }) + Ok(FleetDetector { gauge_initializer }) } -} -impl Drop for FleetDetector { - fn drop(&mut self) { - self.handle.abort(); + async fn activate(&self) { + debug!("initializing gauges"); + if let Err(e) = self.gauge_initializer.send(()).await { + debug!("failed to activate fleet detector plugin: {:?}", e); + } } } -fn detect_cpu_values(system: &System) { - let cpu_count = detect_cpu_count(system); - u64_counter!( - "apollo.router.instance.cpu_count", - "The number of CPUs reported by the instance the router is running on", - cpu_count, - host.arch = get_otel_arch() - ); -} - #[cfg(not(target_os = "linux"))] fn detect_cpu_count(system: &System) -> u64 { system.cpus().len() as u64 @@ -184,14 +224,6 @@ fn detect_cpu_count(system: &System) -> u64 { } } -fn detect_memory_values(system: &System) { - u64_counter!( - "apollo.router.instance.total_memory", - "The amount of memory reported by the instance the router is running on", - system.total_memory() - ); -} - fn get_otel_arch() -> &'static str { match ARCH { "x86_64" => "amd64", @@ -203,4 +235,4 @@ fn get_otel_arch() -> &'static str { } } -register_plugin!("apollo", "fleet_detector", FleetDetector); +register_private_plugin!("apollo", "fleet_detector", FleetDetector); From 4bd425c8fd40cd3bf14153c237112808ecace0b3 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Fri, 8 Nov 2024 10:51:27 +0000 Subject: [PATCH 007/112] FLEET-19 Turn off Fleet Detection if APOLLO_TELEMETRY_DISABLED is detected --- apollo-router/src/executable.rs | 3 ++- apollo-router/src/plugins/fleet_detector.rs | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apollo-router/src/executable.rs b/apollo-router/src/executable.rs index 86bdee162f..a67e294a7f 100644 --- a/apollo-router/src/executable.rs +++ b/apollo-router/src/executable.rs @@ -62,6 +62,7 @@ pub(crate) static mut DHAT_HEAP_PROFILER: OnceCell = OnceCell::n pub(crate) static mut DHAT_AD_HOC_PROFILER: OnceCell = OnceCell::new(); pub(crate) const APOLLO_ROUTER_DEV_ENV: &str = "APOLLO_ROUTER_DEV"; +pub(crate) const APOLLO_TELEMETRY_DISABLED: &str = "APOLLO_TELEMETRY_DISABLED"; // Note: Constructor/Destructor functions may not play nicely with tracing, since they run after // main completes, so don't use tracing, use println!() and eprintln!().. @@ -240,7 +241,7 @@ pub struct Opt { apollo_uplink_poll_interval: Duration, /// Disable sending anonymous usage information to Apollo. - #[clap(long, env = "APOLLO_TELEMETRY_DISABLED", value_parser = FalseyValueParser::new())] + #[clap(long, env = APOLLO_TELEMETRY_DISABLED, value_parser = FalseyValueParser::new())] anonymous_telemetry_disabled: bool, /// The timeout for an http call to Apollo uplink. Defaults to 30s. diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 011ed306ff..bf36dabcf1 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -1,3 +1,4 @@ +use std::env; use std::env::consts::ARCH; use std::sync::Arc; use std::sync::Mutex; @@ -15,6 +16,7 @@ use tokio::sync::mpsc::Sender; use tower::BoxError; use tracing::debug; +use crate::executable::APOLLO_TELEMETRY_DISABLED; use crate::metrics::meter_provider; use crate::plugin::PluginInit; use crate::plugin::PluginPrivate; @@ -138,6 +140,12 @@ impl PluginPrivate for FleetDetector { debug!("initialising fleet detection plugin"); let (gauge_initializer, mut rx) = tokio::sync::mpsc::channel(1); + if env::var(APOLLO_TELEMETRY_DISABLED).is_ok() { + debug!("fleet detection disabled, no telemetry will be sent"); + rx.close(); + return Ok(FleetDetector { gauge_initializer }); + } + debug!("spawning gauge initializer task"); tokio::spawn(async move { let mut gauge_store = GaugeStore::new(); @@ -152,9 +160,11 @@ impl PluginPrivate for FleetDetector { } async fn activate(&self) { - debug!("initializing gauges"); - if let Err(e) = self.gauge_initializer.send(()).await { - debug!("failed to activate fleet detector plugin: {:?}", e); + if !self.gauge_initializer.is_closed() { + debug!("initializing gauges"); + if let Err(e) = self.gauge_initializer.send(()).await { + debug!("failed to activate fleet detector plugin: {:?}", e); + } } } } From 245aad664965bd270243b53966a41dbe67ceaba7 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Fri, 8 Nov 2024 10:59:27 +0000 Subject: [PATCH 008/112] FLEET-19 Add CHANGELOG Entry --- .changesets/feat_jr_add_fleet_awareness_plugin.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changesets/feat_jr_add_fleet_awareness_plugin.md diff --git a/.changesets/feat_jr_add_fleet_awareness_plugin.md b/.changesets/feat_jr_add_fleet_awareness_plugin.md new file mode 100644 index 0000000000..a39fbca968 --- /dev/null +++ b/.changesets/feat_jr_add_fleet_awareness_plugin.md @@ -0,0 +1,11 @@ +### Adds Fleet Awareness Plugin + +Adds a new plugin that reports telemetry to Apollo on the configuration and deployment of the Router. Initially only +reports, memory and CPU usage but will be expanded to cover other non-intrusive measures in future. 🚀 + +As part of the above PluginPrivate has been extended with a new `activate` hook which is guaranteed to be called once +the OTEL meter has been refreshed. This ensures that code, particularly that concerned with gauges, will survive a hot +reload of the router. + +By [@jonathanrainer](https://github.com/jonathanrainer), [@nmoutschen](https://github.com/nmoutschen), [@loshz](https://github.com/loshz) +in https://github.com/apollographql/router/pull/6151 From c1ec7c722708787fc941108f5b58a030dcf18b3d Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Tue, 12 Nov 2024 22:01:06 +0100 Subject: [PATCH 009/112] FLEET-19 Get rid of unwraps --- apollo-router/src/plugins/fleet_detector.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index bf36dabcf1..4f25931868 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -203,8 +203,12 @@ fn detect_cpu_count(system: &System) -> u64 { system_cpus } else { // If it's not max then divide the two to get an integer answer - let (a, b) = readings.split_once(' ').unwrap(); - a.parse::().unwrap() / b.parse::().unwrap() + match readings.split_once(' ') { + None => system_cpus, + Some((quota, period)) => { + calculate_cpu_count_with_default(system_cpus, quota, period) + } + } } } Err(_) => system_cpus, @@ -224,7 +228,7 @@ fn detect_cpu_count(system: &System) -> u64 { if quota == "-1" { system_cpus } else { - quota.parse::().unwrap() / period.parse::().unwrap() + calculate_cpu_count_with_default(system_cpus, "a, &period) } } _ => system_cpus, @@ -234,6 +238,15 @@ fn detect_cpu_count(system: &System) -> u64 { } } +#[cfg(target_os = "linux")] +fn calculate_cpu_count_with_default(default: u64, quota: &str, period: &str) -> u64 { + if let (Ok(q), Ok(p)) = (quota.parse::(), period.parse::()) { + q / p + } else { + default + } +} + fn get_otel_arch() -> &'static str { match ARCH { "x86_64" => "amd64", From 173050615aa4074e0f77c84e44181c3aed09cb5b Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 15 Nov 2024 14:46:18 +0000 Subject: [PATCH 010/112] This commit docs a few things 1. activate is made non-async. It must never fail and it must complete. async fns can halt execution before completion. 2. The spawned thread and channel for fleet detector is removed. There's no need for these. Gauges will be called periodically for export. 3. Telemetry is converted to PrivatePlugin to allow uniform calls to activate. --- apollo-router/src/plugin/mod.rs | 23 ++++++-- apollo-router/src/plugins/fleet_detector.rs | 57 ++++++++----------- .../plugins/telemetry/metrics/apollo/mod.rs | 4 +- apollo-router/src/plugins/telemetry/mod.rs | 13 ++--- apollo-router/src/plugins/test.rs | 4 +- .../src/services/supergraph/service.rs | 7 +-- apollo-router/src/test_harness.rs | 2 +- 7 files changed, 52 insertions(+), 58 deletions(-) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index 646ca06050..bfdaa631e3 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -631,8 +631,8 @@ pub(crate) trait PluginPrivate: Send + Sync + 'static { MultiMap::new() } - /// This is invoked once after the OTEL meter has been refreshed. - async fn activate(&self) {} + /// The point of no return this plugin is about to go live + fn activate(&self) {} } #[async_trait] @@ -680,6 +680,8 @@ where fn web_endpoints(&self) -> MultiMap { PluginUnstable::web_endpoints(self) } + + fn activate(&self) {} } fn get_type_of(_: &T) -> &'static str { @@ -737,8 +739,8 @@ pub(crate) trait DynPlugin: Send + Sync + 'static { #[cfg(test)] fn as_any_mut(&mut self) -> &mut dyn std::any::Any; - /// This is invoked once after the OTEL meter has been refreshed. - async fn activate(&self) {} + /// The point of no return, this plugin is about to go live + fn activate(&self) {} } #[async_trait] @@ -790,8 +792,17 @@ where self } - async fn activate(&self) { - self.activate().await + fn activate(&self) { + self.activate() + } +} + +impl From for Box +where + T: PluginPrivate, +{ + fn from(value: T) -> Self { + Box::new(value) } } diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 4f25931868..70786f1795 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -12,7 +12,6 @@ use opentelemetry_api::KeyValue; use schemars::JsonSchema; use serde::Deserialize; use sysinfo::System; -use tokio::sync::mpsc::Sender; use tower::BoxError; use tracing::debug; @@ -54,19 +53,22 @@ impl SystemGetter { } } -struct GaugeStore { - gauges: Vec>, +#[derive(Default)] +enum GaugeStore { + #[default] + Disabled, + Pending, + Active(Vec>), } impl GaugeStore { - fn new() -> Self { - GaugeStore { gauges: Vec::new() } - } - fn initialize_gauges(&mut self, system_getter: Arc>) { + fn active() -> GaugeStore { + let system_getter = Arc::new(Mutex::new(SystemGetter::new())); let meter = meter_provider().meter("apollo/router"); + let mut gauges = Vec::new(); { let system_getter = system_getter.clone(); - self.gauges.push( + gauges.push( meter .u64_observable_gauge("apollo.router.instance.cpu_freq") .with_description( @@ -87,7 +89,7 @@ impl GaugeStore { } { let system_getter = system_getter.clone(); - self.gauges.push( + gauges.push( meter .u64_observable_gauge("apollo.router.instance.cpu_count") .with_description( @@ -105,7 +107,7 @@ impl GaugeStore { } { let system_getter = system_getter.clone(); - self.gauges.push( + gauges.push( meter .u64_observable_gauge("apollo.router.instance.total_memory") .with_description( @@ -124,13 +126,13 @@ impl GaugeStore { .init(), ); } + GaugeStore::Active(gauges) } } -#[derive(Debug)] +#[derive(Default)] struct FleetDetector { - #[allow(dead_code)] - gauge_initializer: Sender<()>, + gauge_store: Mutex, } #[async_trait::async_trait] impl PluginPrivate for FleetDetector { @@ -138,33 +140,20 @@ impl PluginPrivate for FleetDetector { async fn new(_: PluginInit) -> Result { debug!("initialising fleet detection plugin"); - let (gauge_initializer, mut rx) = tokio::sync::mpsc::channel(1); - if env::var(APOLLO_TELEMETRY_DISABLED).is_ok() { debug!("fleet detection disabled, no telemetry will be sent"); - rx.close(); - return Ok(FleetDetector { gauge_initializer }); + return Ok(FleetDetector::default()); } - debug!("spawning gauge initializer task"); - tokio::spawn(async move { - let mut gauge_store = GaugeStore::new(); - let system_getter = Arc::new(Mutex::new(SystemGetter::new())); - while rx.recv().await.is_some() { - let system_getter = system_getter.clone(); - gauge_store.initialize_gauges(system_getter); - } - }); - - Ok(FleetDetector { gauge_initializer }) + Ok(FleetDetector { + gauge_store: Mutex::new(GaugeStore::Pending), + }) } - async fn activate(&self) { - if !self.gauge_initializer.is_closed() { - debug!("initializing gauges"); - if let Err(e) = self.gauge_initializer.send(()).await { - debug!("failed to activate fleet detector plugin: {:?}", e); - } + fn activate(&self) { + let mut store = self.gauge_store.lock().expect("lock poisoned"); + if matches!(*store, GaugeStore::Pending) { + *store = GaugeStore::active(); } } } diff --git a/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs b/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs index 47a13762d0..d721bad5a6 100644 --- a/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs +++ b/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs @@ -190,8 +190,8 @@ mod test { use super::studio::SingleStatsReport; use super::*; use crate::context::OPERATION_KIND; - use crate::plugin::Plugin; use crate::plugin::PluginInit; + use crate::plugin::PluginPrivate; use crate::plugins::subscription; use crate::plugins::telemetry::apollo; use crate::plugins::telemetry::apollo::default_buffer_size; @@ -364,7 +364,7 @@ mod test { request_builder.header("accept", "multipart/mixed;subscriptionSpec=1.0"); } TestHarness::builder() - .extra_plugin(plugin) + .extra_private_plugin(plugin) .extra_plugin(create_subscription_plugin().await?) .build_router() .await? diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index b6a0e7a1a2..bea99b93b7 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -93,8 +93,8 @@ use crate::layers::ServiceBuilderExt; use crate::metrics::aggregation::MeterProviderType; use crate::metrics::filter::FilterMeterProvider; use crate::metrics::meter_provider; -use crate::plugin::Plugin; use crate::plugin::PluginInit; +use crate::plugin::PluginPrivate; use crate::plugins::telemetry::apollo::ForwardHeaders; use crate::plugins::telemetry::apollo_exporter::proto::reports::trace::node::Id::ResponseName; use crate::plugins::telemetry::apollo_exporter::proto::reports::StatsContext; @@ -133,7 +133,6 @@ use crate::plugins::telemetry::tracing::apollo_telemetry::APOLLO_PRIVATE_OPERATI use crate::plugins::telemetry::tracing::TracingConfigurator; use crate::plugins::telemetry::utils::TracingUtils; use crate::query_planner::OperationKind; -use crate::register_plugin; use crate::router_factory::Endpoint; use crate::services::execution; use crate::services::router; @@ -280,7 +279,7 @@ fn create_builtin_instruments(config: &InstrumentsConfig) -> BuiltinInstruments } #[async_trait::async_trait] -impl Plugin for Telemetry { +impl PluginPrivate for Telemetry { type Config = config::Conf; async fn new(init: PluginInit) -> Result { @@ -853,10 +852,8 @@ impl Plugin for Telemetry { fn web_endpoints(&self) -> MultiMap { self.custom_endpoints.clone() } -} -impl Telemetry { - pub(crate) fn activate(&self) { + fn activate(&self) { let mut activation = self.activation.lock(); if activation.is_active { return; @@ -910,7 +907,9 @@ impl Telemetry { reload_fmt(create_fmt_layer(&self.config)); activation.is_active = true; } +} +impl Telemetry { fn create_propagator(config: &config::Conf) -> TextMapCompositePropagator { let propagation = &config.exporters.tracing.propagation; @@ -1979,7 +1978,7 @@ fn handle_error_internal>( } } -register_plugin!("apollo", "telemetry", Telemetry); +register_private_plugin!("apollo", "telemetry", Telemetry); fn request_ftv1(mut req: SubgraphRequest) -> SubgraphRequest { if req diff --git a/apollo-router/src/plugins/test.rs b/apollo-router/src/plugins/test.rs index ec1d9c509e..c8924f0ef0 100644 --- a/apollo-router/src/plugins/test.rs +++ b/apollo-router/src/plugins/test.rs @@ -65,12 +65,12 @@ use crate::Notify; /// You can pass in a configuration and a schema to the test harness. If you pass in a schema, the test harness will create a query planner and use the schema to extract subgraph schemas. /// /// -pub(crate) struct PluginTestHarness { +pub(crate) struct PluginTestHarness>> { plugin: Box, phantom: std::marker::PhantomData, } #[buildstructor::buildstructor] -impl PluginTestHarness { +impl> + 'static> PluginTestHarness { #[builder] pub(crate) async fn new<'a, 'b>(config: Option<&'a str>, schema: Option<&'b str>) -> Self { let factory = crate::plugin::plugins() diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index dd9c54d355..6a920f792b 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -40,7 +40,6 @@ use crate::plugins::telemetry::config_new::events::log_event; use crate::plugins::telemetry::config_new::events::SupergraphEventResponse; use crate::plugins::telemetry::consts::QUERY_PLANNING_SPAN_NAME; use crate::plugins::telemetry::tracing::apollo_telemetry::APOLLO_PRIVATE_DURATION_NS; -use crate::plugins::telemetry::Telemetry; use crate::plugins::telemetry::LOGGING_DISPLAY_BODY; use crate::plugins::traffic_shaping::TrafficShaping; use crate::plugins::traffic_shaping::APOLLO_TRAFFIC_SHAPING; @@ -810,11 +809,7 @@ impl PluggableSupergraphServiceBuilder { // Activate the telemetry plugin. // We must NOT fail to go live with the new router from this point as the telemetry plugin activate interacts with globals. for (_, plugin) in self.plugins.iter() { - if let Some(telemetry) = plugin.as_any().downcast_ref::() { - telemetry.activate(); - } else { - plugin.activate().await; - } + plugin.activate(); } // We need a non-fallible hook so that once we know we are going live with a pipeline we do final initialization. diff --git a/apollo-router/src/test_harness.rs b/apollo-router/src/test_harness.rs index 516048e3d7..2eb49be5f2 100644 --- a/apollo-router/src/test_harness.rs +++ b/apollo-router/src/test_harness.rs @@ -176,7 +176,7 @@ impl<'a> TestHarness<'a> { ), }; - self.extra_plugins.push((name, Box::new(plugin))); + self.extra_plugins.push((name, plugin.into())); self } From 533aae724adb0dac621f790a02867a722fccb737 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Tue, 19 Nov 2024 14:30:29 +0000 Subject: [PATCH 011/112] plugins/fleet_detector: add schema and persisted_queries gauges --- apollo-router/src/plugins/fleet_detector.rs | 36 ++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 70786f1795..87f4682e75 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -42,14 +42,12 @@ impl SystemGetter { } fn get_system(&mut self) -> &System { - if self.start.elapsed() < REFRESH_INTERVAL { - &self.system - } else { + if self.start.elapsed() >= REFRESH_INTERVAL { self.start = Instant::now(); self.system.refresh_cpu_all(); self.system.refresh_memory(); - &self.system } + &self.system } } @@ -126,6 +124,36 @@ impl GaugeStore { .init(), ); } + { + gauges.push( + meter + .u64_observable_gauge("apollo.router.schema") + .with_description("Details about the current in-use schema") + .with_callback(|i| { + // TODO: get launch_id & schema_hash. + i.observe( + 1, + &[ + KeyValue::new("launch_id", ""), + KeyValue::new("schema_hash", ""), + ], + ) + }) + .init(), + ) + } + { + gauges.push( + meter + .u64_observable_gauge("apollo.router.persisted_queries") + .with_description("Details about the current persisted queries") + .with_callback(|i| { + // TODO: get persisted_queries_version. + i.observe(1, &[KeyValue::new("persisted_queries_version", "")]) + }) + .init(), + ) + } GaugeStore::Active(gauges) } } From c3f75ce7c65c854980fd6a7f4bc143275f1c452d Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Tue, 19 Nov 2024 14:54:53 +0000 Subject: [PATCH 012/112] add comments --- apollo-router/src/plugins/fleet_detector.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 87f4682e75..6139104d96 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -131,6 +131,8 @@ impl GaugeStore { .with_description("Details about the current in-use schema") .with_callback(|i| { // TODO: get launch_id & schema_hash. + // NOTE: this is a fixed gauge. We only care about observing the included + // attributes. i.observe( 1, &[ @@ -149,6 +151,8 @@ impl GaugeStore { .with_description("Details about the current persisted queries") .with_callback(|i| { // TODO: get persisted_queries_version. + // NOTE: this is a fixed gauge. We only care about observing the included + // attributes. i.observe(1, &[KeyValue::new("persisted_queries_version", "")]) }) .init(), @@ -162,6 +166,7 @@ impl GaugeStore { struct FleetDetector { gauge_store: Mutex, } + #[async_trait::async_trait] impl PluginPrivate for FleetDetector { type Config = Conf; From 64adb53e7b70c5180110304fdd7dd20c78623aab Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Thu, 21 Nov 2024 10:51:39 +0000 Subject: [PATCH 013/112] add gauge store options --- apollo-router/src/plugins/fleet_detector.rs | 26 ++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 6139104d96..7826f99758 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -60,7 +60,7 @@ enum GaugeStore { } impl GaugeStore { - fn active() -> GaugeStore { + fn active(opts: &GaugeOptions) -> GaugeStore { let system_getter = Arc::new(Mutex::new(SystemGetter::new())); let meter = meter_provider().meter("apollo/router"); let mut gauges = Vec::new(); @@ -130,15 +130,12 @@ impl GaugeStore { .u64_observable_gauge("apollo.router.schema") .with_description("Details about the current in-use schema") .with_callback(|i| { - // TODO: get launch_id & schema_hash. + // TODO: get launch_id. // NOTE: this is a fixed gauge. We only care about observing the included // attributes. i.observe( 1, - &[ - KeyValue::new("launch_id", ""), - KeyValue::new("schema_hash", ""), - ], + &[KeyValue::new("launch_id", ""), KeyValue::new("schema_hash" opts.supergraph_schema_hash)], ) }) .init(), @@ -162,31 +159,44 @@ impl GaugeStore { } } +struct GaugeOptions { + // Router Supergraph Schema Hash (SHA256 of the SDL)) + supergraph_schema_hash: String, +} + #[derive(Default)] struct FleetDetector { gauge_store: Mutex, + + // Options passed to the gauge_store during activation. + gauge_options: GaugeOptions, } #[async_trait::async_trait] impl PluginPrivate for FleetDetector { type Config = Conf; - async fn new(_: PluginInit) -> Result { + async fn new(plugin: PluginInit) -> Result { debug!("initialising fleet detection plugin"); if env::var(APOLLO_TELEMETRY_DISABLED).is_ok() { debug!("fleet detection disabled, no telemetry will be sent"); return Ok(FleetDetector::default()); } + let gauge_options = GaugeOptions { + supergraph_schema_hash: (*plugin).supergraph_schema_id.to_string(), + }; + Ok(FleetDetector { gauge_store: Mutex::new(GaugeStore::Pending), + gauge_options, }) } fn activate(&self) { let mut store = self.gauge_store.lock().expect("lock poisoned"); if matches!(*store, GaugeStore::Pending) { - *store = GaugeStore::active(); + *store = GaugeStore::active(&self.gauge_options); } } } From 62b4a7556b4200b7b2232ed7754c5b13d5960f14 Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Thu, 21 Nov 2024 16:58:04 -0500 Subject: [PATCH 014/112] Context test rewriter assertion (#6319) --- .../build_query_plan_tests/context.rs | 427 +++++------------- 1 file changed, 111 insertions(+), 316 deletions(-) diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs index 4bdf9f41b1..624678dc7a 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs @@ -36,6 +36,48 @@ use apollo_federation::query_plan::FetchDataRewrite; use apollo_federation::query_plan::PlanNode; use apollo_federation::query_plan::TopLevelPlanNode; +fn parse_fetch_data_path_element(value: &str) -> FetchDataPathElement { + if value == ".." { + FetchDataPathElement::Parent + } else if let Some(("", ty)) = value.split_once("... on ") { + FetchDataPathElement::TypenameEquals(Name::new(ty).unwrap()) + } else { + FetchDataPathElement::Key(Name::new(value).unwrap(), Default::default()) + } +} + +macro_rules! node_assert { + ($plan: ident, $index: literal, $($rename_key_to: literal, $path: expr),+$(,)?) => { + let Some(TopLevelPlanNode::Sequence(node)) = $plan.node else { + panic!("failed to get sequence node"); + }; + let Some(PlanNode::Flatten(node)) = node.nodes.get($index) else { + panic!("failed to get fetch node"); + }; + let PlanNode::Fetch(node) = &*node.node else { + panic!("failed to get flatten node"); + }; + let expected_rewrites = &[ $( $rename_key_to ),+ ]; + let expected_paths = &[ $( $path.into_iter().map(parse_fetch_data_path_element).collect::>() ),+ ]; + assert_eq!(expected_rewrites.len(), expected_paths.len()); + assert_eq!(node.context_rewrites.len(), expected_rewrites.len()); + node + .context_rewrites + .iter() + .map(|rewriter| { + let FetchDataRewrite::KeyRenamer(renamer) = &**rewriter else { + panic!("Expected KeyRenamer"); + }; + renamer + }) + .zip(expected_rewrites.iter().zip(expected_paths)) + .for_each(|(actual, (rename_key_to, path))|{ + assert_eq!(&actual.rename_key_to.as_str(), rename_key_to); + assert_eq!(&actual.path, path); + }); + }; +} + #[test] fn set_context_test_variable_is_from_same_subgraph() { let planner = planner!( @@ -110,33 +152,12 @@ fn set_context_test_variable_is_from_same_subgraph() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["..", "... on T", "prop"] + ); } #[test] @@ -230,33 +251,12 @@ fn set_context_test_variable_is_from_different_subgraph() { } "###); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(2) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + node_assert!( + plan, + 2, + "contextualArgument_1_0", + ["..", "... on T", "prop"] + ); } #[test] @@ -337,33 +337,13 @@ fn set_context_test_variable_is_already_in_a_different_fetch_group() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["..", "... on T", "prop"] + ); } #[test] @@ -540,33 +520,13 @@ fn set_context_test_fetched_as_a_list() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["..", "... on T", "prop"] + ); } #[test] @@ -657,44 +617,15 @@ fn set_context_test_impacts_on_query_planning() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![ - Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("A").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - })), - Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("B").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - })), - ] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["..", "... on A", "prop"], + "contextualArgument_1_0", + ["..", "... on B", "prop"] + ); } #[test] @@ -806,44 +737,15 @@ fn set_context_test_with_type_conditions_for_union() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![ - Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("A").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - })), - Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("B").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - })), - ] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["..", "... on A", "prop"], + "contextualArgument_1_0", + ["..", "... on B", "prop"] + ); } #[test] @@ -921,36 +823,8 @@ fn set_context_test_accesses_a_different_top_level_query() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::Key( - Name::new("me").unwrap(), - Default::default() - ), - FetchDataPathElement::Key( - Name::new("locale").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!(plan, 1, "contextualArgument_1_0", ["..", "me", "locale"]); } #[test] @@ -1022,33 +896,13 @@ fn set_context_one_subgraph() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["..", "... on T", "prop"] + ); } #[test] @@ -1185,45 +1039,13 @@ fn set_context_required_field_is_several_levels_deep_going_back_and_forth_betwee } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(3) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![Arc::new(FetchDataRewrite::KeyRenamer( - FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Parent, - FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), - FetchDataPathElement::Key( - Name::new("a").unwrap(), - Default::default() - ), - FetchDataPathElement::Key( - Name::new("b").unwrap(), - Default::default() - ), - FetchDataPathElement::Key( - Name::new("c").unwrap(), - Default::default() - ), - FetchDataPathElement::Key( - Name::new("prop").unwrap(), - Default::default() - ), - ], - } - )),] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 3, + "contextualArgument_1_0", + ["..", "... on T", "a", "b", "c", "prop"] + ); } #[test] @@ -1452,40 +1274,13 @@ fn set_context_test_efficiently_merge_fetch_groups() { } "### ); - match plan.node { - Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { - Some(PlanNode::Flatten(node)) => match &*node.node { - PlanNode::Fetch(node) => { - assert_eq!( - node.context_rewrites, - vec![ - Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_0").unwrap(), - path: vec![ - FetchDataPathElement::Key( - Name::new_unchecked("identifiers"), - Default::default() - ), - FetchDataPathElement::Key( - Name::new_unchecked("id5"), - Default::default() - ), - ], - })), - Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { - rename_key_to: Name::new("contextualArgument_1_1").unwrap(), - path: vec![FetchDataPathElement::Key( - Name::new_unchecked("mid"), - Default::default() - ),], - })), - ] - ); - } - _ => panic!("failed to get fetch node"), - }, - _ => panic!("failed to get flatten node"), - }, - _ => panic!("failed to get sequence node"), - } + + node_assert!( + plan, + 1, + "contextualArgument_1_0", + ["identifiers", "id5"], + "contextualArgument_1_1", + ["mid"] + ); } From 78a3cfc51f0051afc44a09d8fc550f53e0bcad7b Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Fri, 22 Nov 2024 09:38:18 +0000 Subject: [PATCH 015/112] fix gauge attributes list --- apollo-router/src/plugins/fleet_detector.rs | 27 ++++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 7826f99758..2d4f131277 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -73,14 +73,14 @@ impl GaugeStore { "The CPU frequency of the underlying instance the router is deployed to", ) .with_unit(Unit::new("Mhz")) - .with_callback(move |i| { + .with_callback(move |gauge| { let local_system_getter = system_getter.clone(); let mut system_getter = local_system_getter.lock().unwrap(); let system = system_getter.get_system(); let cpus = system.cpus(); let cpu_freq = cpus.iter().map(|cpu| cpu.frequency()).sum::() / cpus.len() as u64; - i.observe(cpu_freq, &[]) + gauge.observe(cpu_freq, &[]) }) .init(), ); @@ -93,12 +93,12 @@ impl GaugeStore { .with_description( "The number of CPUs reported by the instance the router is running on", ) - .with_callback(move |i| { + .with_callback(move |gauge| { let local_system_getter = system_getter.clone(); let mut system_getter = local_system_getter.lock().unwrap(); let system = system_getter.get_system(); let cpu_count = detect_cpu_count(system); - i.observe(cpu_count, &[KeyValue::new("host.arch", get_otel_arch())]) + gauge.observe(cpu_count, &[KeyValue::new("host.arch", get_otel_arch())]) }) .init(), ); @@ -111,11 +111,11 @@ impl GaugeStore { .with_description( "The amount of memory reported by the instance the router is running on", ) - .with_callback(move |i| { + .with_callback(move |gauge| { let local_system_getter = system_getter.clone(); let mut system_getter = local_system_getter.lock().unwrap(); let system = system_getter.get_system(); - i.observe( + gauge.observe( system.total_memory(), &[KeyValue::new("host.arch", get_otel_arch())], ) @@ -129,13 +129,16 @@ impl GaugeStore { meter .u64_observable_gauge("apollo.router.schema") .with_description("Details about the current in-use schema") - .with_callback(|i| { + .with_callback(|gauge| { // TODO: get launch_id. // NOTE: this is a fixed gauge. We only care about observing the included // attributes. - i.observe( + gauge.observe( 1, - &[KeyValue::new("launch_id", ""), KeyValue::new("schema_hash" opts.supergraph_schema_hash)], + &[ + KeyValue::new("launch_id", ""), + KeyValue::new("schema_hash", opts.supergraph_schema_hash), + ], ) }) .init(), @@ -146,11 +149,11 @@ impl GaugeStore { meter .u64_observable_gauge("apollo.router.persisted_queries") .with_description("Details about the current persisted queries") - .with_callback(|i| { + .with_callback(|gauge| { // TODO: get persisted_queries_version. // NOTE: this is a fixed gauge. We only care about observing the included // attributes. - i.observe(1, &[KeyValue::new("persisted_queries_version", "")]) + gauge.observe(1, &[KeyValue::new("persisted_queries_version", "")]) }) .init(), ) @@ -160,7 +163,7 @@ impl GaugeStore { } struct GaugeOptions { - // Router Supergraph Schema Hash (SHA256 of the SDL)) + // Router Supergraph Schema Hash (SHA256 of the SDL) supergraph_schema_hash: String, } From 7be07037d2e9317268867248cfc48b980a8e62b7 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Fri, 22 Nov 2024 10:03:05 +0000 Subject: [PATCH 016/112] fix lint issues --- apollo-router/src/plugins/fleet_detector.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 2d4f131277..6dc81dc6a7 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -162,6 +162,7 @@ impl GaugeStore { } } +#[derive(Default)] struct GaugeOptions { // Router Supergraph Schema Hash (SHA256 of the SDL) supergraph_schema_hash: String, @@ -187,7 +188,7 @@ impl PluginPrivate for FleetDetector { } let gauge_options = GaugeOptions { - supergraph_schema_hash: (*plugin).supergraph_schema_id.to_string(), + supergraph_schema_hash: (*&plugin).supergraph_schema_id.clone(), }; Ok(FleetDetector { From dc0426c8f578126abcf0cbbeb7fbb5a51c2eb16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Fri, 22 Nov 2024 13:23:32 +0000 Subject: [PATCH 017/112] chore(federation): add unit tests and documentation for conditions handling (#6325) --- .../src/query_plan/conditions.rs | 270 ++++++++++++++++-- .../src/query_plan/fetch_dependency_graph.rs | 5 +- 2 files changed, 253 insertions(+), 22 deletions(-) diff --git a/apollo-federation/src/query_plan/conditions.rs b/apollo-federation/src/query_plan/conditions.rs index f202fd4058..9d61ab76ea 100644 --- a/apollo-federation/src/query_plan/conditions.rs +++ b/apollo-federation/src/query_plan/conditions.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::sync::Arc; use apollo_compiler::ast::Directive; @@ -35,17 +36,50 @@ impl ConditionKind { } } -/// This struct is meant for tracking whether a selection set in a `FetchDependencyGraphNode` needs +impl Display for ConditionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } +} + +/// Represents a combined set of conditions. +/// +/// This struct is meant for tracking whether a selection set in a [FetchDependencyGraphNode] needs /// to be queried, based on the `@skip`/`@include` applications on the selections within. -/// Accordingly, there is much logic around merging and short-circuiting; `OperationConditional` is +/// Accordingly, there is much logic around merging and short-circuiting; [OperationConditional] is /// the more appropriate struct when trying to record the original structure/intent of those /// `@skip`/`@include` applications. +/// +/// [FetchDependencyGraphNode]: crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNode +/// [OperationConditional]: crate::link::graphql_definition::OperationConditional #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) enum Conditions { Variables(VariableConditions), Boolean(bool), } +impl Display for Conditions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // This uses GraphQL directive syntax. + // Add brackets to distinguish it from a real directive list. + write!(f, "[")?; + + match self { + Conditions::Boolean(constant) => write!(f, "{constant:?}")?, + Conditions::Variables(variables) => { + for (index, (name, kind)) in variables.iter().enumerate() { + if index > 0 { + write!(f, " ")?; + } + write!(f, "@{kind}(if: ${name})")?; + } + } + } + + write!(f, "]") + } +} + /// A list of variable conditions, represented as a map from variable names to whether that variable /// is negated in the condition. We maintain the invariant that there's at least one condition (i.e. /// the map is non-empty), and that there's at most one condition per variable name. @@ -96,23 +130,28 @@ impl VariableConditions { } } -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct VariableCondition { - variable: Name, - kind: ConditionKind, -} - impl Conditions { - /// Create conditions from a map of variable conditions. If empty, instead returns a - /// condition that always evaluates to true. + /// Create conditions from a map of variable conditions. + /// + /// If empty, instead returns a condition that always evaluates to true. fn from_variables(map: IndexMap) -> Self { if map.is_empty() { - Self::Boolean(true) + Self::always() } else { Self::Variables(VariableConditions::new_unchecked(map)) } } + /// Create conditions that always evaluate to true. + pub(crate) const fn always() -> Self { + Self::Boolean(true) + } + + /// Create conditions that always evaluate to false. + pub(crate) const fn never() -> Self { + Self::Boolean(false) + } + /// Parse @skip and @include conditions from a directive list. /// /// # Errors @@ -127,7 +166,7 @@ impl Conditions { match value.as_ref() { // Constant @skip(if: true) can never match - Value::Boolean(true) => return Ok(Self::Boolean(false)), + Value::Boolean(true) => return Ok(Self::never()), // Constant @skip(if: false) always matches Value::Boolean(_) => {} Value::Variable(name) => { @@ -146,7 +185,7 @@ impl Conditions { match value.as_ref() { // Constant @include(if: false) can never match - Value::Boolean(false) => return Ok(Self::Boolean(false)), + Value::Boolean(false) => return Ok(Self::never()), // Constant @include(if: true) always matches Value::Boolean(true) => {} // If both @skip(if: $var) and @include(if: $var) exist, the condition can also @@ -155,7 +194,7 @@ impl Conditions { if variables.insert(name.clone(), ConditionKind::Include) == Some(ConditionKind::Skip) { - return Ok(Self::Boolean(false)); + return Ok(Self::never()); } } _ => { @@ -167,10 +206,34 @@ impl Conditions { Ok(Self::from_variables(variables)) } - // TODO(@goto-bus-stop): what exactly is the difference between this and `Self::merge`? - pub(crate) fn update_with(&self, new_conditions: &Self) -> Self { - match (new_conditions, self) { - (Conditions::Boolean(_), _) | (_, Conditions::Boolean(_)) => new_conditions.clone(), + /// Returns a new set of conditions that omits those conditions that are already handled by the + /// argument. + /// + /// For example, if we have a selection set like so: + /// ```graphql + /// { + /// a @skip(if: $a) { + /// b @skip(if: $a) @include(if: $b) { + /// c + /// } + /// } + /// } + /// ``` + /// Then we may call `b.conditions().update_with( a.conditions() )`, and get: + /// ```graphql + /// { + /// a @skip(if: $a) { + /// b @include(if: $b) { + /// c + /// } + /// } + /// } + /// ``` + /// because the `@skip(if: $a)` condition in `b` must always match, as implied by + /// being nested inside `a`. + pub(crate) fn update_with(&self, handled_conditions: &Self) -> Self { + match (self, handled_conditions) { + (Conditions::Boolean(_), _) | (_, Conditions::Boolean(_)) => self.clone(), (Conditions::Variables(new_conditions), Conditions::Variables(handled_conditions)) => { let mut filtered = IndexMap::default(); for (cond_name, &cond_kind) in new_conditions.0.iter() { @@ -179,7 +242,7 @@ impl Conditions { // If we've already handled that exact condition, we can skip it. // But if we've already handled the _negation_ of this condition, then this mean the overall conditions // are unreachable and we can just return `false` directly. - return Conditions::Boolean(false); + return Conditions::never(); } Some(_) => {} None => { @@ -198,7 +261,7 @@ impl Conditions { match (self, other) { // Absorbing element (Conditions::Boolean(false), _) | (_, Conditions::Boolean(false)) => { - Conditions::Boolean(false) + Conditions::never() } // Neutral element @@ -207,7 +270,7 @@ impl Conditions { (Conditions::Variables(self_vars), Conditions::Variables(other_vars)) => { match self_vars.merge(other_vars) { Some(vars) => Conditions::Variables(vars), - None => Conditions::Boolean(false), + None => Conditions::never(), } } } @@ -366,3 +429,168 @@ fn matches_condition_for_kind( None => false, } } + +#[cfg(test)] +mod tests { + use apollo_compiler::ExecutableDocument; + use apollo_compiler::Schema; + + use super::*; + + fn parse(directives: &str) -> Conditions { + let schema = + Schema::parse_and_validate("type Query { a: String }", "schema.graphql").unwrap(); + let doc = + ExecutableDocument::parse(&schema, format!("{{ a {directives} }}"), "query.graphql") + .unwrap(); + let operation = doc.operations.get(None).unwrap(); + let directives = operation.selection_set.selections[0].directives(); + Conditions::from_directives(&DirectiveList::from(directives.clone())).unwrap() + } + + #[test] + fn merge_conditions() { + assert_eq!( + parse("@skip(if: $a)") + .merge(parse("@include(if: $b)")) + .to_string(), + "[@skip(if: $a) @include(if: $b)]", + "combine skip/include" + ); + assert_eq!( + parse("@skip(if: $a)") + .merge(parse("@skip(if: $b)")) + .to_string(), + "[@skip(if: $a) @skip(if: $b)]", + "combine multiple skips" + ); + assert_eq!( + parse("@include(if: $a)") + .merge(parse("@include(if: $b)")) + .to_string(), + "[@include(if: $a) @include(if: $b)]", + "combine multiple includes" + ); + assert_eq!( + parse("@skip(if: $a)").merge(parse("@include(if: $a)")), + Conditions::never(), + "skip/include with same variable conflicts" + ); + assert_eq!( + parse("@skip(if: $a)").merge(Conditions::always()), + parse("@skip(if: $a)"), + "merge with `true` returns original" + ); + assert_eq!( + Conditions::always().merge(Conditions::always()), + Conditions::always(), + "merge with `true` returns original" + ); + assert_eq!( + parse("@skip(if: $a)").merge(Conditions::never()), + Conditions::never(), + "merge with `false` returns `false`" + ); + assert_eq!( + parse("@include(if: $a)").merge(Conditions::never()), + Conditions::never(), + "merge with `false` returns `false`" + ); + assert_eq!( + Conditions::always().merge(Conditions::never()), + Conditions::never(), + "merge with `false` returns `false`" + ); + assert_eq!( + parse("@skip(if: true)").merge(parse("@include(if: $a)")), + Conditions::never(), + "@skip with hardcoded if: true can never evaluate to true" + ); + assert_eq!( + parse("@skip(if: false)").merge(parse("@include(if: $a)")), + parse("@include(if: $a)"), + "@skip with hardcoded if: false returns other side" + ); + assert_eq!( + parse("@include(if: true)").merge(parse("@include(if: $a)")), + parse("@include(if: $a)"), + "@include with hardcoded if: true returns other side" + ); + assert_eq!( + parse("@include(if: false)").merge(parse("@include(if: $a)")), + Conditions::never(), + "@include with hardcoded if: false can never evaluate to true" + ); + } + + #[test] + fn update_conditions() { + assert_eq!( + parse("@skip(if: $a)") + .merge(parse("@include(if: $b)")) + .update_with(&parse("@include(if: $b)")), + parse("@skip(if: $a)"), + "trim @include(if:) condition" + ); + assert_eq!( + parse("@skip(if: $a)") + .merge(parse("@include(if: $b)")) + .update_with(&parse("@skip(if: $a)")), + parse("@include(if: $b)"), + "trim @skip(if:) condition" + ); + + let list = parse("@skip(if: $a)") + .merge(parse("@skip(if: $b)")) + .merge(parse("@skip(if: $c)")) + .merge(parse("@skip(if: $d)")) + .merge(parse("@skip(if: $e)")); + let handled = parse("@skip(if: $b)").merge(parse("@skip(if: $e)")); + assert_eq!( + list.update_with(&handled), + parse("@skip(if: $a)") + .merge(parse("@skip(if: $c)")) + .merge(parse("@skip(if: $d)")), + "trim multiple conditions" + ); + + let list = parse("@include(if: $a)") + .merge(parse("@include(if: $b)")) + .merge(parse("@include(if: $c)")) + .merge(parse("@include(if: $d)")) + .merge(parse("@include(if: $e)")); + let handled = parse("@include(if: $b)").merge(parse("@include(if: $e)")); + assert_eq!( + list.update_with(&handled), + parse("@include(if: $a)") + .merge(parse("@include(if: $c)")) + .merge(parse("@include(if: $d)")), + "trim multiple conditions" + ); + + let list = parse("@include(if: $a)") + .merge(parse("@include(if: $b)")) + .merge(parse("@include(if: $c)")) + .merge(parse("@include(if: $d)")) + .merge(parse("@include(if: $e)")); + // It may technically be correct to return `never()` here? + // But the result for query planning is the same either way, as these conditions will never + // be reached. + assert_eq!( + list.update_with(&Conditions::never()), + list, + "update with constant does not affect conditions" + ); + + let list = parse("@include(if: $a)") + .merge(parse("@include(if: $b)")) + .merge(parse("@include(if: $c)")) + .merge(parse("@include(if: $d)")) + .merge(parse("@include(if: $e)")); + assert_eq!( + list.update_with(&Conditions::always()), + list, + "update with constant does not affect conditions" + ); + } +} diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index 4a2da90e95..91482c2495 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -1779,7 +1779,10 @@ impl FetchDependencyGraph { .graph .node_weight_mut(node_index) .ok_or_else(|| FederationError::internal("Node unexpectedly missing"))?; - let conditions = handled_conditions.update_with(&node.selection_set.conditions); + let conditions = node + .selection_set + .conditions + .update_with(&handled_conditions); let new_handled_conditions = conditions.clone().merge(handled_conditions); let processed = processor.on_node( From 9a8c7340211595f694a17c83a9e3a58a2a2bee67 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Fri, 22 Nov 2024 14:49:31 +0000 Subject: [PATCH 018/112] add SchemaState --- apollo-router/src/plugins/fleet_detector.rs | 2 +- apollo-router/src/router/event/mod.rs | 3 +- apollo-router/src/router/event/schema.rs | 45 +++++++++++++++------ apollo-router/src/router/mod.rs | 27 ++++++++----- apollo-router/src/state_machine.rs | 40 +++++++++++++----- apollo-router/src/uplink/mod.rs | 2 + apollo-router/src/uplink/schema.rs | 5 +++ 7 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 apollo-router/src/uplink/schema.rs diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 6dc81dc6a7..50db884f14 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -188,7 +188,7 @@ impl PluginPrivate for FleetDetector { } let gauge_options = GaugeOptions { - supergraph_schema_hash: (*&plugin).supergraph_schema_id.clone(), + supergraph_schema_hash: (*&plugin).supergraph_schema_id.to_string(), }; Ok(FleetDetector { diff --git a/apollo-router/src/router/event/mod.rs b/apollo-router/src/router/event/mod.rs index 2645ad4755..aa98558b6b 100644 --- a/apollo-router/src/router/event/mod.rs +++ b/apollo-router/src/router/event/mod.rs @@ -22,6 +22,7 @@ use self::Event::UpdateConfiguration; use self::Event::UpdateLicense; use self::Event::UpdateSchema; use crate::uplink::license_enforcement::LicenseState; +use crate::uplink::schema::SchemaState; use crate::Configuration; /// Messages that are broadcast across the app. @@ -33,7 +34,7 @@ pub(crate) enum Event { NoMoreConfiguration, /// The schema was updated. - UpdateSchema(String), + UpdateSchema(SchemaState), /// There are no more updates to the schema NoMoreSchema, diff --git a/apollo-router/src/router/event/schema.rs b/apollo-router/src/router/event/schema.rs index 229992fa68..8c33366b80 100644 --- a/apollo-router/src/router/event/schema.rs +++ b/apollo-router/src/router/event/schema.rs @@ -11,6 +11,7 @@ use url::Url; use crate::router::Event; use crate::router::Event::NoMoreSchema; use crate::router::Event::UpdateSchema; +use crate::uplink::schema::SchemaState; use crate::uplink::schema_stream::SupergraphSdlQuery; use crate::uplink::stream_from_uplink; use crate::uplink::UplinkConfig; @@ -74,7 +75,11 @@ impl SchemaSource { pub(crate) fn into_stream(self) -> impl Stream { match self { SchemaSource::Static { schema_sdl: schema } => { - stream::once(future::ready(UpdateSchema(schema))).boxed() + let update_schema = UpdateSchema(SchemaState { + sdl: schema, + launch_id: None, + }); + stream::once(future::ready(update_schema)).boxed() } SchemaSource::Stream(stream) => stream.map(UpdateSchema).boxed(), #[allow(deprecated)] @@ -100,7 +105,10 @@ impl SchemaSource { let path = path.clone(); async move { match tokio::fs::read_to_string(&path).await { - Ok(schema) => Some(UpdateSchema(schema)), + Ok(schema) => { + let update_schema = UpdateSchema(SchemaState{sdl:schema,launch_id:None}); + Some(update_schema) + } Err(err) => { tracing::error!(reason = %err, "failed to read supergraph schema"); None @@ -110,7 +118,8 @@ impl SchemaSource { }) .boxed() } else { - stream::once(future::ready(UpdateSchema(schema))).boxed() + let update_schema = UpdateSchema(SchemaState{sdl:schema,launch_id:None}); + stream::once(future::ready(update_schema)).boxed() } } Err(err) => { @@ -124,7 +133,13 @@ impl SchemaSource { stream_from_uplink::(uplink_config) .filter_map(|res| { future::ready(match res { - Ok(schema) => Some(UpdateSchema(schema)), + Ok(schema) => { + let update_schema = UpdateSchema(SchemaState { + sdl: schema, + launch_id: None, + }); + Some(update_schema) + } Err(e) => { tracing::error!("{}", e); None @@ -222,7 +237,13 @@ impl Fetcher { .await { Ok(res) if res.status().is_success() => match res.text().await { - Ok(schema) => return Some(UpdateSchema(schema)), + Ok(schema) => { + let update_schema = UpdateSchema(SchemaState { + sdl: schema, + launch_id: None, + }); + return Some(update_schema); + } Err(err) => { tracing::warn!( url.full = %url, @@ -346,10 +367,10 @@ mod tests { .into_stream(); assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_1) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_1) ); assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_1) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_1) ); } .with_subscriber(assert_snapshot_subscriber!()) @@ -382,10 +403,10 @@ mod tests { .into_stream(); assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_2) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_2) ); assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_2) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_2) ); } .with_subscriber(assert_snapshot_subscriber!({ @@ -448,7 +469,7 @@ mod tests { .await; assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_1) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_1) ); drop(success); @@ -468,7 +489,7 @@ mod tests { .await; assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_1) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_1) ); } .with_subscriber(assert_snapshot_subscriber!({ @@ -497,7 +518,7 @@ mod tests { .into_stream(); assert!( - matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema == SCHEMA_1) + matches!(stream.next().await.unwrap(), UpdateSchema(schema) if schema.sdl == SCHEMA_1) ); assert!(matches!(stream.next().await.unwrap(), NoMoreSchema)); } diff --git a/apollo-router/src/router/mod.rs b/apollo-router/src/router/mod.rs index bd5461a046..030e9797eb 100644 --- a/apollo-router/src/router/mod.rs +++ b/apollo-router/src/router/mod.rs @@ -354,6 +354,7 @@ mod tests { use crate::router::Event::UpdateLicense; use crate::router::Event::UpdateSchema; use crate::uplink::license_enforcement::LicenseState; + use crate::uplink::schema::SchemaState; use crate::Configuration; fn init_with_server() -> RouterHttpServer { @@ -417,7 +418,10 @@ mod tests { .await .unwrap(); router_handle - .send_event(UpdateSchema(schema.to_string())) + .send_event(UpdateSchema(SchemaState { + sdl: schema.to_string(), + launch_id: None, + })) .await .unwrap(); router_handle @@ -460,9 +464,10 @@ mod tests { .await .unwrap(); router_handle - .send_event(UpdateSchema( - include_str!("../testdata/supergraph_missing_name.graphql").to_string(), - )) + .send_event(UpdateSchema(SchemaState { + sdl: include_str!("../testdata/supergraph_missing_name.graphql").to_string(), + launch_id: None, + })) .await .unwrap(); router_handle @@ -502,9 +507,10 @@ mod tests { // let's update the schema to add the field router_handle - .send_event(UpdateSchema( - include_str!("../testdata/supergraph.graphql").to_string(), - )) + .send_event(UpdateSchema(SchemaState { + sdl: include_str!("../testdata/supergraph.graphql").to_string(), + launch_id: None, + })) .await .unwrap(); @@ -528,9 +534,10 @@ mod tests { // let's go back and remove the field router_handle - .send_event(UpdateSchema( - include_str!("../testdata/supergraph_missing_name.graphql").to_string(), - )) + .send_event(UpdateSchema(SchemaState { + sdl: include_str!("../testdata/supergraph_missing_name.graphql").to_string(), + launch_id: None, + })) .await .unwrap(); diff --git a/apollo-router/src/state_machine.rs b/apollo-router/src/state_machine.rs index e3ce6c3a67..7d50b0ab39 100644 --- a/apollo-router/src/state_machine.rs +++ b/apollo-router/src/state_machine.rs @@ -539,7 +539,7 @@ where NoMoreConfiguration => state.no_more_configuration().await, UpdateSchema(schema) => { state - .update_inputs(&mut self, Some(Arc::new(schema)), None, None) + .update_inputs(&mut self, Some(Arc::new(schema.sdl)), None, None) .await } NoMoreSchema => state.no_more_schema().await, @@ -612,11 +612,15 @@ mod tests { use crate::services::new_service::ServiceFactory; use crate::services::router; use crate::services::RouterRequest; + use crate::uplink::schema::SchemaState; type SharedOneShotReceiver = Arc>>>; - fn example_schema() -> String { - include_str!("testdata/supergraph.graphql").to_owned() + fn example_schema() -> SchemaState { + SchemaState { + sdl: include_str!("testdata/supergraph.graphql").to_owned(), + launch_id: None, + } } macro_rules! assert_matches { @@ -870,7 +874,10 @@ mod tests { router_factory, stream::iter(vec![ UpdateConfiguration(Configuration::builder().build().unwrap()), - UpdateSchema(minimal_schema.to_owned()), + UpdateSchema(SchemaState { + sdl: minimal_schema.to_owned(), + launch_id: None + }), UpdateLicense(LicenseState::default()), UpdateSchema(example_schema()), Shutdown @@ -893,9 +900,15 @@ mod tests { router_factory, stream::iter(vec![ UpdateConfiguration(Configuration::builder().build().unwrap()), - UpdateSchema(minimal_schema.to_owned()), + UpdateSchema(SchemaState { + sdl: minimal_schema.to_owned(), + launch_id: None + }), UpdateLicense(LicenseState::default()), - UpdateSchema(minimal_schema.to_owned()), + UpdateSchema(SchemaState { + sdl: minimal_schema.to_owned(), + launch_id: None + }), Shutdown ]) ) @@ -916,7 +929,10 @@ mod tests { router_factory, stream::iter(vec![ UpdateConfiguration(Configuration::builder().build().unwrap()), - UpdateSchema(minimal_schema.to_owned()), + UpdateSchema(SchemaState { + sdl: minimal_schema.to_owned(), + launch_id: None + }), UpdateLicense(LicenseState::default()), UpdateLicense(LicenseState::Licensed), Shutdown @@ -1039,7 +1055,10 @@ mod tests { UpdateConfiguration(Configuration::builder().build().unwrap()), UpdateSchema(example_schema()), UpdateLicense(LicenseState::default()), - UpdateSchema(minimal_schema.to_owned()), + UpdateSchema(SchemaState { + sdl: minimal_schema.to_owned(), + launch_id: None + }), Shutdown ]) ) @@ -1097,7 +1116,10 @@ mod tests { .build() .unwrap() ), - UpdateSchema(minimal_schema.to_owned()), + UpdateSchema(SchemaState { + sdl: minimal_schema.to_owned(), + launch_id: None + }), Shutdown ]), ) diff --git a/apollo-router/src/uplink/mod.rs b/apollo-router/src/uplink/mod.rs index 6a8974699e..85d4a62bf7 100644 --- a/apollo-router/src/uplink/mod.rs +++ b/apollo-router/src/uplink/mod.rs @@ -19,6 +19,8 @@ pub(crate) mod license_stream; pub(crate) mod persisted_queries_manifest_stream; pub(crate) mod schema_stream; +pub mod schema; + const GCP_URL: &str = "https://uplink.api.apollographql.com"; const AWS_URL: &str = "https://aws.uplink.api.apollographql.com"; diff --git a/apollo-router/src/uplink/schema.rs b/apollo-router/src/uplink/schema.rs new file mode 100644 index 0000000000..dcfafb7698 --- /dev/null +++ b/apollo-router/src/uplink/schema.rs @@ -0,0 +1,5 @@ +/// Represents the new state of a schema after an update. +pub struct SchemaState { + pub sdl: String, + pub launch_id: Option, +} From 5c5a6043028b16a0f5b4578643fbe4f482a2eba2 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Fri, 22 Nov 2024 15:44:37 +0000 Subject: [PATCH 019/112] fix stream map closure --- apollo-router/src/plugins/fleet_detector.rs | 32 +++++++-------------- apollo-router/src/router/event/schema.rs | 19 ++++++++++-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 50db884f14..2d4244fb21 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -1,3 +1,4 @@ +use core::slice::SlicePattern; use std::env; use std::env::consts::ARCH; use std::sync::Arc; @@ -130,30 +131,14 @@ impl GaugeStore { .u64_observable_gauge("apollo.router.schema") .with_description("Details about the current in-use schema") .with_callback(|gauge| { - // TODO: get launch_id. // NOTE: this is a fixed gauge. We only care about observing the included // attributes. - gauge.observe( - 1, - &[ - KeyValue::new("launch_id", ""), - KeyValue::new("schema_hash", opts.supergraph_schema_hash), - ], - ) - }) - .init(), - ) - } - { - gauges.push( - meter - .u64_observable_gauge("apollo.router.persisted_queries") - .with_description("Details about the current persisted queries") - .with_callback(|gauge| { - // TODO: get persisted_queries_version. - // NOTE: this is a fixed gauge. We only care about observing the included - // attributes. - gauge.observe(1, &[KeyValue::new("persisted_queries_version", "")]) + let mut attributes: Vec = + vec![KeyValue::new("schema_hash", opts.supergraph_schema_hash)]; + if let Some(launch_id) = opts.launch_id { + attributes.push(KeyValue::new("launch_id", launch_id)); + } + gauge.observe(1, attributes.as_slice()) }) .init(), ) @@ -164,6 +149,8 @@ impl GaugeStore { #[derive(Default)] struct GaugeOptions { + launch_id: Option, + // Router Supergraph Schema Hash (SHA256 of the SDL) supergraph_schema_hash: String, } @@ -188,6 +175,7 @@ impl PluginPrivate for FleetDetector { } let gauge_options = GaugeOptions { + launch_id: None, supergraph_schema_hash: (*&plugin).supergraph_schema_id.to_string(), }; diff --git a/apollo-router/src/router/event/schema.rs b/apollo-router/src/router/event/schema.rs index 8c33366b80..b728444377 100644 --- a/apollo-router/src/router/event/schema.rs +++ b/apollo-router/src/router/event/schema.rs @@ -81,7 +81,14 @@ impl SchemaSource { }); stream::once(future::ready(update_schema)).boxed() } - SchemaSource::Stream(stream) => stream.map(UpdateSchema).boxed(), + SchemaSource::Stream(stream) => stream + .map(|sdl| { + UpdateSchema(SchemaState { + sdl, + launch_id: None, + }) + }) + .boxed(), #[allow(deprecated)] SchemaSource::File { path, @@ -106,7 +113,10 @@ impl SchemaSource { async move { match tokio::fs::read_to_string(&path).await { Ok(schema) => { - let update_schema = UpdateSchema(SchemaState{sdl:schema,launch_id:None}); + let update_schema = UpdateSchema(SchemaState { + sdl: schema, + launch_id: None, + }); Some(update_schema) } Err(err) => { @@ -118,7 +128,10 @@ impl SchemaSource { }) .boxed() } else { - let update_schema = UpdateSchema(SchemaState{sdl:schema,launch_id:None}); + let update_schema = UpdateSchema(SchemaState { + sdl: schema, + launch_id: None, + }); stream::once(future::ready(update_schema)).boxed() } } From 894e429abb27a9b2efd2de673703d66a06213948 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Fri, 22 Nov 2024 16:01:44 +0000 Subject: [PATCH 020/112] fix deref addrof --- apollo-router/src/plugins/fleet_detector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 2d4244fb21..8304e146dc 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -176,7 +176,7 @@ impl PluginPrivate for FleetDetector { let gauge_options = GaugeOptions { launch_id: None, - supergraph_schema_hash: (*&plugin).supergraph_schema_id.to_string(), + supergraph_schema_hash: plugin.supergraph_schema_id.to_string(), }; Ok(FleetDetector { From 214b2e4800f7b31700e79f10275326f628603b7d Mon Sep 17 00:00:00 2001 From: Edward Huang Date: Fri, 22 Nov 2024 09:00:01 -0800 Subject: [PATCH 021/112] docs: router setup for Datadog (#6008) --- .../images/router/datadog-apm-ops-example.png | Bin 0 -> 492109 bytes .../telemetry/metrics-exporters/datadog.mdx | 37 +++++++------- .../telemetry/trace-exporters/datadog.mdx | 46 ++++++++++++++++++ 3 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 docs/source/images/router/datadog-apm-ops-example.png diff --git a/docs/source/images/router/datadog-apm-ops-example.png b/docs/source/images/router/datadog-apm-ops-example.png new file mode 100644 index 0000000000000000000000000000000000000000..6d92a5e2d57134f305d3e655c58254b251259e36 GIT binary patch literal 492109 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 z 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%0Dju;8ylFF6}gcbLRI45B*_S{pk81V)k>N>uTJy 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ℑ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ʞVv3m35n2s|hF>)V+A%R%_NHUZbI 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

Third Party Licenses

Overview of licenses:

@@ -2319,6 +2323,18 @@ 

Used by:

Apache License 2.0

Used by:

+ +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +   Copyright [yyyy] [name of copyright owner]
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    +   
  • Apache License 2.0

    @@ -4676,6 +4901,215 @@

    Used by:

    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1.  Definitions.
    +
    +    "License" shall mean the terms and conditions for use, reproduction,
    +    and distribution as defined by Sections 1 through 9 of this document.
    +
    +    "Licensor" shall mean the copyright owner or entity authorized by
    +    the copyright owner that is granting the License.
    +
    +    "Legal Entity" shall mean the union of the acting entity and all
    +    other entities that control, are controlled by, or are under common
    +    control with that entity. For the purposes of this definition,
    +    "control" means (i) the power, direct or indirect, to cause the
    +    direction or management of such entity, whether by contract or
    +    otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +    outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +    "You" (or "Your") shall mean an individual or Legal Entity
    +    exercising permissions granted by this License.
    +
    +    "Source" form shall mean the preferred form for making modifications,
    +    including but not limited to software source code, documentation
    +    source, and configuration files.
    +
    +    "Object" form shall mean any form resulting from mechanical
    +    transformation or translation of a Source form, including but
    +    not limited to compiled object code, generated documentation,
    +    and conversions to other media types.
    +
    +    "Work" shall mean the work of authorship, whether in Source or
    +    Object form, made available under the License, as indicated by a
    +    copyright notice that is included in or attached to the work
    +    (an example is provided in the Appendix below).
    +
    +    "Derivative Works" shall mean any work, whether in Source or Object
    +    form, that is based on (or derived from) the Work and for which the
    +    editorial revisions, annotations, elaborations, or other modifications
    +    represent, as a whole, an original work of authorship. For the purposes
    +    of this License, Derivative Works shall not include works that remain
    +    separable from, or merely link (or bind by name) to the interfaces of,
    +    the Work and Derivative Works thereof.
    +
    +    "Contribution" shall mean any work of authorship, including
    +    the original version of the Work and any modifications or additions
    +    to that Work or Derivative Works thereof, that is intentionally
    +    submitted to Licensor for inclusion in the Work by the copyright owner
    +    or by an individual or Legal Entity authorized to submit on behalf of
    +    the copyright owner. For the purposes of this definition, "submitted"
    +    means any form of electronic, verbal, or written communication sent
    +    to the Licensor or its representatives, including but not limited to
    +    communication on electronic mailing lists, source code control systems,
    +    and issue tracking systems that are managed by, or on behalf of, the
    +    Licensor for the purpose of discussing and improving the Work, but
    +    excluding communication that is conspicuously marked or otherwise
    +    designated in writing by the copyright owner as "Not a Contribution."
    +
    +    "Contributor" shall mean Licensor and any individual or Legal Entity
    +    on behalf of whom a Contribution has been received by Licensor and
    +    subsequently incorporated within the Work.
    +
    +2.  Grant of Copyright License. Subject to the terms and conditions of
    +    this License, each Contributor hereby grants to You a perpetual,
    +    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +    copyright license to reproduce, prepare Derivative Works of,
    +    publicly display, publicly perform, sublicense, and distribute the
    +    Work and such Derivative Works in Source or Object form.
    +
    +3.  Grant of Patent License. Subject to the terms and conditions of
    +    this License, each Contributor hereby grants to You a perpetual,
    +    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +    (except as stated in this section) patent license to make, have made,
    +    use, offer to sell, sell, import, and otherwise transfer the Work,
    +    where such license applies only to those patent claims licensable
    +    by such Contributor that are necessarily infringed by their
    +    Contribution(s) alone or by combination of their Contribution(s)
    +    with the Work to which such Contribution(s) was submitted. If You
    +    institute patent litigation against any entity (including a
    +    cross-claim or counterclaim in a lawsuit) alleging that the Work
    +    or a Contribution incorporated within the Work constitutes direct
    +    or contributory patent infringement, then any patent licenses
    +    granted to You under this License for that Work shall terminate
    +    as of the date such litigation is filed.
    +
    +4.  Redistribution. You may reproduce and distribute copies of the
    +    Work or Derivative Works thereof in any medium, with or without
    +    modifications, and in Source or Object form, provided that You
    +    meet the following conditions:
    +
    +    (a) You must give any other recipients of the Work or
    +    Derivative Works a copy of this License; and
    +
    +    (b) You must cause any modified files to carry prominent notices
    +    stating that You changed the files; and
    +
    +    (c) You must retain, in the Source form of any Derivative Works
    +    that You distribute, all copyright, patent, trademark, and
    +    attribution notices from the Source form of the Work,
    +    excluding those notices that do not pertain to any part of
    +    the Derivative Works; and
    +
    +    (d) If the Work includes a "NOTICE" text file as part of its
    +    distribution, then any Derivative Works that You distribute must
    +    include a readable copy of the attribution notices contained
    +    within such NOTICE file, excluding those notices that do not
    +    pertain to any part of the Derivative Works, in at least one
    +    of the following places: within a NOTICE text file distributed
    +    as part of the Derivative Works; within the Source form or
    +    documentation, if provided along with the Derivative Works; or,
    +    within a display generated by the Derivative Works, if and
    +    wherever such third-party notices normally appear. The contents
    +    of the NOTICE file are for informational purposes only and
    +    do not modify the License. You may add Your own attribution
    +    notices within Derivative Works that You distribute, alongside
    +    or as an addendum to the NOTICE text from the Work, provided
    +    that such additional attribution notices cannot be construed
    +    as modifying the License.
    +
    +    You may add Your own copyright statement to Your modifications and
    +    may provide additional or different license terms and conditions
    +    for use, reproduction, or distribution of Your modifications, or
    +    for any such Derivative Works as a whole, provided Your use,
    +    reproduction, and distribution of the Work otherwise complies with
    +    the conditions stated in this License.
    +
    +5.  Submission of Contributions. Unless You explicitly state otherwise,
    +    any Contribution intentionally submitted for inclusion in the Work
    +    by You to the Licensor shall be under the terms and conditions of
    +    this License, without any additional terms or conditions.
    +    Notwithstanding the above, nothing herein shall supersede or modify
    +    the terms of any separate license agreement you may have executed
    +    with Licensor regarding such Contributions.
    +
    +6.  Trademarks. This License does not grant permission to use the trade
    +    names, trademarks, service marks, or product names of the Licensor,
    +    except as required for reasonable and customary use in describing the
    +    origin of the Work and reproducing the content of the NOTICE file.
    +
    +7.  Disclaimer of Warranty. Unless required by applicable law or
    +    agreed to in writing, Licensor provides the Work (and each
    +    Contributor provides its Contributions) on an "AS IS" BASIS,
    +    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +    implied, including, without limitation, any warranties or conditions
    +    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +    PARTICULAR PURPOSE. You are solely responsible for determining the
    +    appropriateness of using or redistributing the Work and assume any
    +    risks associated with Your exercise of permissions under this License.
    +
    +8.  Limitation of Liability. In no event and under no legal theory,
    +    whether in tort (including negligence), contract, or otherwise,
    +    unless required by applicable law (such as deliberate and grossly
    +    negligent acts) or agreed to in writing, shall any Contributor be
    +    liable to You for damages, including any direct, indirect, special,
    +    incidental, or consequential damages of any character arising as a
    +    result of this License or out of the use or inability to use the
    +    Work (including but not limited to damages for loss of goodwill,
    +    work stoppage, computer failure or malfunction, or any and all
    +    other commercial damages or losses), even if such Contributor
    +    has been advised of the possibility of such damages.
    +
    +9.  Accepting Warranty or Additional Liability. While redistributing
    +    the Work or Derivative Works thereof, You may choose to offer,
    +    and charge a fee for, acceptance of support, warranty, indemnity,
    +    or other liability obligations and/or rights consistent with this
    +    License. However, in accepting such obligations, You may act only
    +    on Your own behalf and on Your sole responsibility, not on behalf
    +    of any other Contributor, and only if You agree to indemnify,
    +    defend, and hold each Contributor harmless for any liability
    +    incurred by, or claims asserted against, such Contributor by reason
    +    of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +Copyright [yyyy] [name of copyright owner]
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
     
  • @@ -5688,7 +6122,6 @@

    Used by:

  • erased-serde
  • ghost
  • itoa
  • -
  • libc
  • linkme
  • paste
  • prettyplease
  • @@ -5705,7 +6138,6 @@

    Used by:

  • serde_path_to_error
  • serde_qs
  • serde_urlencoded
  • -
  • syn
  • thiserror
  • thiserror-impl
  • unicode-ident
  • @@ -5896,15 +6328,225 @@

    Used by:

    Apache License 2.0

    Used by:

    +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1. Definitions.
    +
    +   "License" shall mean the terms and conditions for use, reproduction,
    +   and distribution as defined by Sections 1 through 9 of this document.
    +
    +   "Licensor" shall mean the copyright owner or entity authorized by
    +   the copyright owner that is granting the License.
    +
    +   "Legal Entity" shall mean the union of the acting entity and all
    +   other entities that control, are controlled by, or are under common
    +   control with that entity. For the purposes of this definition,
    +   "control" means (i) the power, direct or indirect, to cause the
    +   direction or management of such entity, whether by contract or
    +   otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +   outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +   "You" (or "Your") shall mean an individual or Legal Entity
    +   exercising permissions granted by this License.
    +
    +   "Source" form shall mean the preferred form for making modifications,
    +   including but not limited to software source code, documentation
    +   source, and configuration files.
    +
    +   "Object" form shall mean any form resulting from mechanical
    +   transformation or translation of a Source form, including but
    +   not limited to compiled object code, generated documentation,
    +   and conversions to other media types.
    +
    +   "Work" shall mean the work of authorship, whether in Source or
    +   Object form, made available under the License, as indicated by a
    +   copyright notice that is included in or attached to the work
    +   (an example is provided in the Appendix below).
    +
    +   "Derivative Works" shall mean any work, whether in Source or Object
    +   form, that is based on (or derived from) the Work and for which the
    +   editorial revisions, annotations, elaborations, or other modifications
    +   represent, as a whole, an original work of authorship. For the purposes
    +   of this License, Derivative Works shall not include works that remain
    +   separable from, or merely link (or bind by name) to the interfaces of,
    +   the Work and Derivative Works thereof.
    +
    +   "Contribution" shall mean any work of authorship, including
    +   the original version of the Work and any modifications or additions
    +   to that Work or Derivative Works thereof, that is intentionally
    +   submitted to Licensor for inclusion in the Work by the copyright owner
    +   or by an individual or Legal Entity authorized to submit on behalf of
    +   the copyright owner. For the purposes of this definition, "submitted"
    +   means any form of electronic, verbal, or written communication sent
    +   to the Licensor or its representatives, including but not limited to
    +   communication on electronic mailing lists, source code control systems,
    +   and issue tracking systems that are managed by, or on behalf of, the
    +   Licensor for the purpose of discussing and improving the Work, but
    +   excluding communication that is conspicuously marked or otherwise
    +   designated in writing by the copyright owner as "Not a Contribution."
    +
    +   "Contributor" shall mean Licensor and any individual or Legal Entity
    +   on behalf of whom a Contribution has been received by Licensor and
    +   subsequently incorporated within the Work.
    +
    +2. Grant of Copyright License. Subject to the terms and conditions of
    +   this License, each Contributor hereby grants to You a perpetual,
    +   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +   copyright license to reproduce, prepare Derivative Works of,
    +   publicly display, publicly perform, sublicense, and distribute the
    +   Work and such Derivative Works in Source or Object form.
    +
    +3. Grant of Patent License. Subject to the terms and conditions of
    +   this License, each Contributor hereby grants to You a perpetual,
    +   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +   (except as stated in this section) patent license to make, have made,
    +   use, offer to sell, sell, import, and otherwise transfer the Work,
    +   where such license applies only to those patent claims licensable
    +   by such Contributor that are necessarily infringed by their
    +   Contribution(s) alone or by combination of their Contribution(s)
    +   with the Work to which such Contribution(s) was submitted. If You
    +   institute patent litigation against any entity (including a
    +   cross-claim or counterclaim in a lawsuit) alleging that the Work
    +   or a Contribution incorporated within the Work constitutes direct
    +   or contributory patent infringement, then any patent licenses
    +   granted to You under this License for that Work shall terminate
    +   as of the date such litigation is filed.
    +
    +4. Redistribution. You may reproduce and distribute copies of the
    +   Work or Derivative Works thereof in any medium, with or without
    +   modifications, and in Source or Object form, provided that You
    +   meet the following conditions:
    +
    +   (a) You must give any other recipients of the Work or
    +       Derivative Works a copy of this License; and
    +
    +   (b) You must cause any modified files to carry prominent notices
    +       stating that You changed the files; and
    +
    +   (c) You must retain, in the Source form of any Derivative Works
    +       that You distribute, all copyright, patent, trademark, and
    +       attribution notices from the Source form of the Work,
    +       excluding those notices that do not pertain to any part of
    +       the Derivative Works; and
    +
    +   (d) If the Work includes a "NOTICE" text file as part of its
    +       distribution, then any Derivative Works that You distribute must
    +       include a readable copy of the attribution notices contained
    +       within such NOTICE file, excluding those notices that do not
    +       pertain to any part of the Derivative Works, in at least one
    +       of the following places: within a NOTICE text file distributed
    +       as part of the Derivative Works; within the Source form or
    +       documentation, if provided along with the Derivative Works; or,
    +       within a display generated by the Derivative Works, if and
    +       wherever such third-party notices normally appear. The contents
    +       of the NOTICE file are for informational purposes only and
    +       do not modify the License. You may add Your own attribution
    +       notices within Derivative Works that You distribute, alongside
    +       or as an addendum to the NOTICE text from the Work, provided
    +       that such additional attribution notices cannot be construed
    +       as modifying the License.
    +
    +   You may add Your own copyright statement to Your modifications and
    +   may provide additional or different license terms and conditions
    +   for use, reproduction, or distribution of Your modifications, or
    +   for any such Derivative Works as a whole, provided Your use,
    +   reproduction, and distribution of the Work otherwise complies with
    +   the conditions stated in this License.
    +
    +5. Submission of Contributions. Unless You explicitly state otherwise,
    +   any Contribution intentionally submitted for inclusion in the Work
    +   by You to the Licensor shall be under the terms and conditions of
    +   this License, without any additional terms or conditions.
    +   Notwithstanding the above, nothing herein shall supersede or modify
    +   the terms of any separate license agreement you may have executed
    +   with Licensor regarding such Contributions.
    +
    +6. Trademarks. This License does not grant permission to use the trade
    +   names, trademarks, service marks, or product names of the Licensor,
    +   except as required for reasonable and customary use in describing the
    +   origin of the Work and reproducing the content of the NOTICE file.
    +
    +7. Disclaimer of Warranty. Unless required by applicable law or
    +   agreed to in writing, Licensor provides the Work (and each
    +   Contributor provides its Contributions) on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +   implied, including, without limitation, any warranties or conditions
    +   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +   PARTICULAR PURPOSE. You are solely responsible for determining the
    +   appropriateness of using or redistributing the Work and assume any
    +   risks associated with Your exercise of permissions under this License.
    +
    +8. Limitation of Liability. In no event and under no legal theory,
    +   whether in tort (including negligence), contract, or otherwise,
    +   unless required by applicable law (such as deliberate and grossly
    +   negligent acts) or agreed to in writing, shall any Contributor be
    +   liable to You for damages, including any direct, indirect, special,
    +   incidental, or consequential damages of any character arising as a
    +   result of this License or out of the use or inability to use the
    +   Work (including but not limited to damages for loss of goodwill,
    +   work stoppage, computer failure or malfunction, or any and all
    +   other commercial damages or losses), even if such Contributor
    +   has been advised of the possibility of such damages.
    +
    +9. Accepting Warranty or Additional Liability. While redistributing
    +   the Work or Derivative Works thereof, You may choose to offer,
    +   and charge a fee for, acceptance of support, warranty, indemnity,
    +   or other liability obligations and/or rights consistent with this
    +   License. However, in accepting such obligations, You may act only
    +   on Your own behalf and on Your sole responsibility, not on behalf
    +   of any other Contributor, and only if You agree to indemnify,
    +   defend, and hold each Contributor harmless for any liability
    +   incurred by, or claims asserted against, such Contributor by reason
    +   of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +APPENDIX: How to apply the Apache License to your work.
    +
    +   To apply the Apache License to your work, attach the following
    +   boilerplate notice, with the fields enclosed by brackets "[]"
    +   replaced with your own identifying information. (Don't include
    +   the brackets!)  The text should be enclosed in the appropriate
    +   comment syntax for the file format. We also recommend that a
    +   file or class name and description of purpose be included on the
    +   same "printed page" as the copyright notice for easier
    +   identification within third-party archives.
    +
    +Copyright (c) 2016 Alex Crichton
    +Copyright (c) 2017 The Tokio Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +
    + +
  • +

    Apache License 2.0

    +

    Used by:

    +
                                  Apache License
                             Version 2.0, January 2004
    @@ -6094,8 +6736,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright 2014 Paho Lurie-Gregg Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -6107,14 +6748,13 @@

    Used by:

    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. -
    +limitations under the License.
  • Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -6304,7 +6944,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2014 Paho Lurie-Gregg +Copyright 2016 Sean McArthur Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -6316,13 +6956,14 @@

    Used by:

    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License.
    +limitations under the License. +
  • Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -6512,7 +7153,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2016 Sean McArthur +Copyright 2017 Sergio Benitez Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -6531,7 +7172,7 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -6722,6 +7363,7 @@ 

    Used by:

    identification within third-party archives. Copyright 2017 Sergio Benitez +Copyright 2014 Alex Chricton Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -6740,7 +7382,8 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -6930,8 +7573,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 Sergio Benitez -Copyright 2014 Alex Chricton +Copyright 2017 http-rs authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -6950,8 +7592,7 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -7141,7 +7782,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 http-rs authors +Copyright 2017 quininer kel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -7160,7 +7801,7 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -7350,7 +7991,7 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 quininer kel +Copyright 2018 The pin-utils authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -7369,7 +8010,7 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -7559,13 +8200,13 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2018 The pin-utils authors +Copyright 2019 The CryptoCorrosion Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -7578,7 +8219,8 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -7768,13 +8410,13 @@ 

    Used by:

    same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2019 The CryptoCorrosion Contributors +Copyright 2020 Andrew Straw Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -8441,6 +9083,8 @@

    Used by:

  • core-foundation-sys
  • countme
  • crossbeam-channel
  • +
  • crossbeam-deque
  • +
  • crossbeam-epoch
  • crossbeam-utils
  • debugid
  • derivative
  • @@ -8477,6 +9121,7 @@

    Used by:

  • hyper-timeout
  • idna
  • idna
  • +
  • idna_adapter
  • if_chain
  • indexmap
  • indexmap
  • @@ -8531,6 +9176,8 @@

    Used by:

  • prost-types
  • prost-types
  • proteus
  • +
  • rayon
  • +
  • rayon-core
  • regex
  • regex-automata
  • regex-lite
  • @@ -10637,6 +11284,7 @@

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -11945,6 +12593,7 @@ 

    Used by:

  • async-graphql-derive
  • async-graphql-parser
  • async-graphql-value
  • +
  • chrono
  • deno-proc-macro-rules
  • deno-proc-macro-rules-macros
  • dunce
  • @@ -11954,6 +12603,7 @@

    Used by:

  • graphql_query_derive
  • http-serde
  • ident_case
  • +
  • libc
  • libssh2-sys
  • linkme-impl
  • md5
  • @@ -11961,6 +12611,7 @@

    Used by:

  • prost
  • rhai_codegen
  • siphasher
  • +
  • syn
  • system-configuration
  • system-configuration-sys
  • thrift
  • @@ -12042,6 +12693,27 @@

    Used by:

    http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Copyright 2016 Nicolas Silva
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    @@ -12901,6 +13573,107 @@ 

    Used by:

    **trademark** means trademarks, service marks, and similar rights. --------------------------------------------------------------------------------
    +
  • +
  • +

    Elastic License 2.0

    +

    Used by:

    + +
    Elastic License 2.0
    +
    +URL: https://www.elastic.co/licensing/elastic-license
    +
    +## Acceptance
    +
    +By using the software, you agree to all of the terms and conditions below.
    +
    +## Copyright License
    +
    +The licensor grants you a non-exclusive, royalty-free, worldwide,
    +non-sublicensable, non-transferable license to use, copy, distribute, make
    +available, and prepare derivative works of the software, in each case subject to
    +the limitations and conditions below.
    +
    +## Limitations
    +
    +You may not provide the software to third parties as a hosted or managed
    +service, where the service provides users with access to any substantial set of
    +the features or functionality of the software.
    +
    +You may not move, change, disable, or circumvent the license key functionality
    +in the software, and you may not remove or obscure any functionality in the
    +software that is protected by the license key.
    +
    +You may not alter, remove, or obscure any licensing, copyright, or other notices
    +of the licensor in the software. Any use of the licensor’s trademarks is subject
    +to applicable law.
    +
    +## Patents
    +
    +The licensor grants you a license, under any patent claims the licensor can
    +license, or becomes able to license, to make, have made, use, sell, offer for
    +sale, import and have imported the software, in each case subject to the
    +limitations and conditions in this license. This license does not cover any
    +patent claims that you cause to be infringed by modifications or additions to
    +the software. If you or your company make any written claim that the software
    +infringes or contributes to infringement of any patent, your patent license for
    +the software granted under these terms ends immediately. If your company makes
    +such a claim, your patent license ends immediately for work on behalf of your
    +company.
    +
    +## Notices
    +
    +You must ensure that anyone who gets a copy of any part of the software from you
    +also gets a copy of these terms.
    +
    +If you modify the software, you must include in any modified copies of the
    +software prominent notices stating that you have modified the software.
    +
    +## No Other Rights
    +
    +These terms do not imply any licenses other than those expressly granted in
    +these terms.
    +
    +## Termination
    +
    +If you use the software in violation of these terms, such use is not licensed,
    +and your licenses will automatically terminate. If the licensor provides you
    +with a notice of your violation, and you cease all violation of this license no
    +later than 30 days after you receive that notice, your licenses will be
    +reinstated retroactively. However, if you violate these terms after such
    +reinstatement, any additional violation of these terms will cause your licenses
    +to terminate automatically and permanently.
    +
    +## No Liability
    +
    +*As far as the law allows, the software comes as is, without any warranty or
    +condition, and the licensor will not be liable to you for any damages arising
    +out of these terms or the use or nature of the software, under any kind of
    +legal claim.*
    +
    +## Definitions
    +
    +The **licensor** is the entity offering these terms, and the **software** is the
    +software the licensor makes available under these terms, including any portion
    +of it.
    +
    +**you** refers to the individual or entity agreeing to these terms.
    +
    +**your company** is any legal entity, sole proprietorship, or other kind of
    +organization that you work for, plus all organizations that have control over,
    +are under the control of, or are under common control with that
    +organization. **control** means ownership of substantially all the assets of an
    +entity, or the power to direct its management and policies by vote, contract, or
    +otherwise. Control can be direct or indirect.
    +
    +**your licenses** are all the licenses granted to you for the software under
    +these terms.
    +
    +**use** means anything you do with the software requiring one of your licenses.
    +
    +**trademark** means trademarks, service marks, and similar rights.
    +
  • ISC License

    @@ -14153,6 +14926,21 @@

    Used by:

    OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright 2016 Nika Layzell
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     
  • @@ -15251,6 +16039,36 @@

    Used by:

    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2015 Guillaume Gomez
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    +
     
  • @@ -16918,6 +17736,71 @@

    Used by:

    * Hudson (tjh@cryptsoft.com). * */ +
  • +
  • +

    Unicode License v3

    +

    Used by:

    + +
    UNICODE LICENSE V3
    +
    +COPYRIGHT AND PERMISSION NOTICE
    +
    +Copyright © 1991-2023 Unicode, Inc.
    +
    +NOTICE TO USER: Carefully read the following legal agreement. BY
    +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
    +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
    +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
    +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
    +
    +Permission is hereby granted, free of charge, to any person obtaining a
    +copy of data files and any associated documentation (the "Data Files") or
    +software and any associated documentation (the "Software") to deal in the
    +Data Files or Software without restriction, including without limitation
    +the rights to use, copy, modify, merge, publish, distribute, and/or sell
    +copies of the Data Files or Software, and to permit persons to whom the
    +Data Files or Software are furnished to do so, provided that either (a)
    +this copyright and permission notice appear with all copies of the Data
    +Files or Software, or (b) this copyright and permission notice appear in
    +associated Documentation.
    +
    +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
    +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
    +THIRD PARTY RIGHTS.
    +
    +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
    +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
    +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
    +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
    +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
    +FILES OR SOFTWARE.
    +
    +Except as contained in this notice, the name of a copyright holder shall
    +not be used in advertising or otherwise to promote the sale, use or other
    +dealings in these Data Files or Software without prior written
    +authorization of the copyright holder.
    +
  • Unicode License Agreement - Data Files and Software (2016)

    From f6ede7f0e89e700b78b60aea2f16e1bb61309236 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Tue, 10 Dec 2024 15:05:44 +0100 Subject: [PATCH 097/112] tests: add unit tests to fleet_detector --- apollo-router/src/plugins/fleet_detector.rs | 267 ++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 659faba898..2e54b44877 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -469,3 +469,270 @@ fn get_deployment_type() -> &'static str { } register_private_plugin!("apollo", "fleet_detector", FleetDetector); + +#[cfg(test)] +mod tests { + use http::StatusCode; + use tower::Service as _; + + use super::*; + use crate::metrics::collect_metrics; + use crate::metrics::test_utils::MetricType; + use crate::metrics::FutureMetricsExt as _; + use crate::plugin::test::MockHttpClientService; + use crate::plugin::test::MockRouterService; + use crate::services::Body; + + #[tokio::test] + async fn test_disabled_router_service() { + async { + // WHEN the plugin is disabled + let plugin = FleetDetector::default(); + + // GIVEN a router service request + let mut mock_bad_request_service = MockRouterService::new(); + mock_bad_request_service + .expect_call() + .times(1) + .returning(|req: router::Request| { + Ok(router::Response { + context: req.context, + response: http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .header("content-type", "application/json") + // making sure the request body is consumed + .body(req.router_request.into_body()) + .unwrap(), + }) + }); + let mut bad_request_router_service = + plugin.router_service(mock_bad_request_service.boxed()); + let router_req = router::Request::fake_builder() + .body("request") + .build() + .unwrap(); + let _router_response = bad_request_router_service + .ready() + .await + .unwrap() + .call(router_req) + .await + .unwrap() + .next_response() + .await + .unwrap(); + + // THEN operation size metrics shouldn't exist + assert!(!collect_metrics().metric_exists::( + "apollo.router.operations.request_size", + MetricType::Counter, + &[], + )); + assert!(!collect_metrics().metric_exists::( + "apollo.router.operations.response_size", + MetricType::Counter, + &[], + )); + } + .with_metrics() + .await; + } + + #[tokio::test] + async fn test_enabled_router_service() { + async { + // WHEN the plugin is enabled + let plugin = FleetDetector { + enabled: true, + ..Default::default() + }; + + // GIVEN a router service request + let mut mock_bad_request_service = MockRouterService::new(); + mock_bad_request_service + .expect_call() + .times(1) + .returning(|req: router::Request| { + Ok(router::Response { + context: req.context, + response: http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .header("content-type", "application/json") + // making sure the request body is consumed + .body(req.router_request.into_body()) + .unwrap(), + }) + }); + let mut bad_request_router_service = + plugin.router_service(mock_bad_request_service.boxed()); + let router_req = router::Request::fake_builder() + .body(Body::wrap_stream(Body::from("request"))) + .build() + .unwrap(); + let _router_response = bad_request_router_service + .ready() + .await + .unwrap() + .call(router_req) + .await + .unwrap() + .next_response() + .await + .unwrap(); + + // THEN operation size metrics should exist + assert_counter!("apollo.router.operations.request_size", 7, &[]); + assert_counter!("apollo.router.operations.response_size", 7, &[]); + } + .with_metrics() + .await; + } + + #[tokio::test] + async fn test_disabled_http_client_service() { + async { + // WHEN the plugin is disabled + let plugin = FleetDetector::default(); + + // GIVEN an http client service request + let mut mock_bad_request_service = MockHttpClientService::new(); + mock_bad_request_service.expect_call().times(1).returning( + |req: http::Request| { + Box::pin(async { + let data = hyper::body::to_bytes(req.into_body()).await?; + Ok(http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .header("content-type", "application/json") + // making sure the request body is consumed + .body(Body::from(data)) + .unwrap()) + }) + }, + ); + let mut bad_request_http_client_service = plugin.http_client_service( + "subgraph", + mock_bad_request_service + .map_request(|req: HttpRequest| req.http_request.map(|body| body.into_inner())) + .map_response(|res: http::Response| HttpResponse { + http_response: res.map(RouterBody::from), + context: Default::default(), + }) + .boxed(), + ); + let http_client_req = HttpRequest { + http_request: http::Request::builder() + .body(RouterBody::from("request")) + .unwrap(), + context: Default::default(), + }; + let http_client_response = bad_request_http_client_service + .ready() + .await + .unwrap() + .call(http_client_req) + .await + .unwrap(); + // making sure the response body is consumed + let _data = hyper::body::to_bytes(http_client_response.http_response.into_body()) + .await + .unwrap(); + + // THEN fetch metrics shouldn't exist + assert!(!collect_metrics().metric_exists::( + "apollo.router.operations.fetch", + MetricType::Counter, + &[KeyValue::new("subgraph.service.name", "subgraph"),], + )); + assert!(!collect_metrics().metric_exists::( + "apollo.router.operations.fetch.request_size", + MetricType::Counter, + &[KeyValue::new("subgraph.service.name", "subgraph"),], + )); + assert!(!collect_metrics().metric_exists::( + "apollo.router.operations.fetch.response_size", + MetricType::Counter, + &[KeyValue::new("subgraph.service.name", "subgraph"),], + )); + } + .with_metrics() + .await; + } + + #[tokio::test] + async fn test_enabled_http_client_service() { + async { + // WHEN the plugin is enabled + let plugin = FleetDetector { + enabled: true, + ..Default::default() + }; + + // GIVEN an http client service request + let mut mock_bad_request_service = MockHttpClientService::new(); + mock_bad_request_service.expect_call().times(1).returning( + |req: http::Request| { + Box::pin(async { + let data = hyper::body::to_bytes(req.into_body()).await?; + Ok(http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .header("content-type", "application/json") + // making sure the request body is consumed + .body(Body::from(data)) + .unwrap()) + }) + }, + ); + let mut bad_request_http_client_service = plugin.http_client_service( + "subgraph", + mock_bad_request_service + .map_request(|req: HttpRequest| req.http_request.map(|body| body.into_inner())) + .map_response(|res: http::Response| HttpResponse { + http_response: res.map(RouterBody::from), + context: Default::default(), + }) + .boxed(), + ); + let http_client_req = HttpRequest { + http_request: http::Request::builder() + .body(RouterBody::from("request")) + .unwrap(), + context: Default::default(), + }; + let http_client_response = bad_request_http_client_service + .ready() + .await + .unwrap() + .call(http_client_req) + .await + .unwrap(); + + // making sure the response body is consumed + let _data = hyper::body::to_bytes(http_client_response.http_response.into_body()) + .await + .unwrap(); + + // THEN fetch metrics should exist + assert_counter!( + "apollo.router.operations.fetch", + 1, + &[ + KeyValue::new("subgraph.service.name", "subgraph"), + KeyValue::new("http.response.status_code", 400), + KeyValue::new("client_error", false) + ] + ); + assert_counter!( + "apollo.router.operations.fetch.request_size", + 7, + &[KeyValue::new("subgraph.service.name", "subgraph"),] + ); + assert_counter!( + "apollo.router.operations.fetch.response_size", + 7, + &[KeyValue::new("subgraph.service.name", "subgraph"),] + ); + } + .with_metrics() + .await; + } +} From 666ccf767b5a865430b43ec369c8f803e705bec4 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Tue, 10 Dec 2024 15:15:21 +0100 Subject: [PATCH 098/112] chore: update subgraph.name attribute name --- apollo-router/src/plugins/fleet_detector.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 2e54b44877..46fadedd55 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -305,7 +305,7 @@ impl PluginPrivate for FleetDetector { "apollo.router.operations.fetch.request_size", "Total number of request bytes for subgraph fetches", bytes.len() as u64, - subgraph.service.name = sn.to_string() + subgraph.name = sn.to_string() ); } })) @@ -322,7 +322,7 @@ impl PluginPrivate for FleetDetector { "apollo.router.operations.fetch", "Number of subgraph fetches", 1u64, - subgraph.service.name = sn.to_string(), + subgraph.name = sn.to_string(), client_error = false, http.response.status_code = res.http_response.status().as_u16() as i64 ); @@ -337,7 +337,7 @@ impl PluginPrivate for FleetDetector { "apollo.router.operations.fetch.response_size", "Total number of response bytes for subgraph fetches", bytes.len() as u64, - subgraph.service.name = sn.to_string() + subgraph.name = sn.to_string() ); } })) @@ -350,7 +350,7 @@ impl PluginPrivate for FleetDetector { "apollo.router.operations.fetch", "Number of subgraph fetches", 1u64, - subgraph.service.name = sn.to_string(), + subgraph.name = sn.to_string(), client_error = true ); Err(err) @@ -641,17 +641,17 @@ mod tests { assert!(!collect_metrics().metric_exists::( "apollo.router.operations.fetch", MetricType::Counter, - &[KeyValue::new("subgraph.service.name", "subgraph"),], + &[KeyValue::new("subgraph.name", "subgraph"),], )); assert!(!collect_metrics().metric_exists::( "apollo.router.operations.fetch.request_size", MetricType::Counter, - &[KeyValue::new("subgraph.service.name", "subgraph"),], + &[KeyValue::new("subgraph.name", "subgraph"),], )); assert!(!collect_metrics().metric_exists::( "apollo.router.operations.fetch.response_size", MetricType::Counter, - &[KeyValue::new("subgraph.service.name", "subgraph"),], + &[KeyValue::new("subgraph.name", "subgraph"),], )); } .with_metrics() @@ -716,7 +716,7 @@ mod tests { "apollo.router.operations.fetch", 1, &[ - KeyValue::new("subgraph.service.name", "subgraph"), + KeyValue::new("subgraph.name", "subgraph"), KeyValue::new("http.response.status_code", 400), KeyValue::new("client_error", false) ] @@ -724,12 +724,12 @@ mod tests { assert_counter!( "apollo.router.operations.fetch.request_size", 7, - &[KeyValue::new("subgraph.service.name", "subgraph"),] + &[KeyValue::new("subgraph.name", "subgraph"),] ); assert_counter!( "apollo.router.operations.fetch.response_size", 7, - &[KeyValue::new("subgraph.service.name", "subgraph"),] + &[KeyValue::new("subgraph.name", "subgraph"),] ); } .with_metrics() From bb61985458e5a2176ca9195c5baff35c925ff29a Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Tue, 10 Dec 2024 10:17:57 -0500 Subject: [PATCH 099/112] chore(federation): Improve backwards compatibility of `FederationError`s (#6421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renée --- apollo-federation/src/error/mod.rs | 22 ++-- apollo-federation/src/link/argument.rs | 16 --- apollo-federation/src/link/database.rs | 4 +- apollo-federation/src/link/mod.rs | 103 ++++++++++-------- .../src/query_plan/query_planner.rs | 15 ++- apollo-router/src/error.rs | 61 ++++++++++- .../src/query_planner/bridge_query_planner.rs | 22 +--- 7 files changed, 142 insertions(+), 101 deletions(-) diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 0e7ae8e4c5..c0ea25e81b 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -3,7 +3,6 @@ use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Write; -use apollo_compiler::executable::GetOperationError; use apollo_compiler::validation::DiagnosticList; use apollo_compiler::validation::WithErrors; use apollo_compiler::InvalidNameError; @@ -108,10 +107,6 @@ impl From for String { #[derive(Clone, Debug, strum_macros::Display, PartialEq, Eq)] pub enum UnsupportedFeatureKind { - #[strum(to_string = "defer")] - Defer, - #[strum(to_string = "context")] - Context, #[strum(to_string = "alias")] Alias, } @@ -136,6 +131,8 @@ pub enum SingleFederationError { InvalidSubgraph { message: String }, #[error("Operation name not found")] UnknownOperation, + #[error("Must provide operation name if query contains multiple operations")] + OperationNameNotProvided, #[error("Unsupported custom directive @{name} on fragment spread. Due to query transformations during planning, the router requires directives on fragment spreads to support both the FRAGMENT_SPREAD and INLINE_FRAGMENT locations.")] UnsupportedSpreadDirective { name: Name }, #[error("{message}")] @@ -308,12 +305,13 @@ impl SingleFederationError { SingleFederationError::InvalidGraphQL { .. } | SingleFederationError::InvalidGraphQLName(_) => ErrorCode::InvalidGraphQL, SingleFederationError::InvalidSubgraph { .. } => ErrorCode::InvalidGraphQL, - // TODO(@goto-bus-stop): this should have a different error code: it's not the graphql - // that's invalid, but the operation name - SingleFederationError::UnknownOperation => ErrorCode::InvalidGraphQL, // TODO(@goto-bus-stop): this should have a different error code: it's not invalid, // just unsupported due to internal limitations. SingleFederationError::UnsupportedSpreadDirective { .. } => ErrorCode::InvalidGraphQL, + // TODO(@goto-bus-stop): this should have a different error code: it's not the graphql + // that's invalid, but the operation name + SingleFederationError::UnknownOperation => ErrorCode::InvalidGraphQL, + SingleFederationError::OperationNameNotProvided => ErrorCode::InvalidGraphQL, SingleFederationError::DirectiveDefinitionInvalid { .. } => { ErrorCode::DirectiveDefinitionInvalid } @@ -495,12 +493,6 @@ impl From for FederationError { } } -impl From for FederationError { - fn from(_: GetOperationError) -> Self { - SingleFederationError::UnknownOperation.into() - } -} - impl From for FederationError { fn from(err: FederationSpecError) -> Self { // TODO: When we get around to finishing the composition port, we should really switch it to @@ -598,7 +590,7 @@ impl Display for AggregateFederationError { // of GraphQLErrors, or take a vector of GraphQLErrors and group them together under an // AggregateGraphQLError which itself would have a specific error message and code, and throw that. // We represent all these cases with an enum, and delegate to the members. -#[derive(thiserror::Error)] +#[derive(Clone, thiserror::Error)] pub enum FederationError { #[error(transparent)] SingleFederationError(#[from] SingleFederationError), diff --git a/apollo-federation/src/link/argument.rs b/apollo-federation/src/link/argument.rs index d8ae1987f2..d6f9748438 100644 --- a/apollo-federation/src/link/argument.rs +++ b/apollo-federation/src/link/argument.rs @@ -100,22 +100,6 @@ pub(crate) fn directive_optional_boolean_argument( } } -#[allow(dead_code)] -pub(crate) fn directive_required_boolean_argument( - application: &Node, - name: &Name, -) -> Result { - directive_optional_boolean_argument(application, name)?.ok_or_else(|| { - SingleFederationError::Internal { - message: format!( - "Required argument \"{}\" of directive \"@{}\" was not present.", - name, application.name - ), - } - .into() - }) -} - pub(crate) fn directive_optional_variable_boolean_argument( application: &Node, name: &Name, diff --git a/apollo-federation/src/link/database.rs b/apollo-federation/src/link/database.rs index b4ae94c793..13e786f2c4 100644 --- a/apollo-federation/src/link/database.rs +++ b/apollo-federation/src/link/database.rs @@ -536,7 +536,7 @@ mod tests { let schema = Schema::parse(schema, "testSchema").unwrap(); let errors = links_metadata(&schema).expect_err("should error"); // TODO Multiple errors - insta::assert_snapshot!(errors, @r###"Invalid use of @link in schema: invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form { name: "", as: "" }."###); + insta::assert_snapshot!(errors, @r###"Invalid use of @link in schema: in "2", invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form { name: "", as: "" }."###); } #[test] @@ -561,7 +561,7 @@ mod tests { let schema = Schema::parse(schema, "testSchema").unwrap(); let errors = links_metadata(&schema).expect_err("should error"); // TODO Multiple errors - insta::assert_snapshot!(errors, @"Invalid use of @link in schema: invalid alias 'myKey' for import name '@key': should start with '@' since the imported name does"); + insta::assert_snapshot!(errors, @r###"Invalid use of @link in schema: in "{name: "@key", as: "myKey"}", invalid alias 'myKey' for import name '@key': should start with '@' since the imported name does"###); } #[test] diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 1f59986d93..0b883fb36d 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -67,13 +67,12 @@ pub enum Purpose { impl Purpose { pub fn from_value(value: &Value) -> Result { - if let Value::Enum(value) = value { - Ok(value.parse::()?) - } else { - Err(LinkError::BootstrapError( - "invalid `purpose` value, should be an enum".to_string(), - )) - } + value + .as_enum() + .ok_or_else(|| { + LinkError::BootstrapError("invalid `purpose` value, should be an enum".to_string()) + }) + .and_then(|value| value.parse()) } } @@ -85,8 +84,7 @@ impl str::FromStr for Purpose { "SECURITY" => Ok(Purpose::SECURITY), "EXECUTION" => Ok(Purpose::EXECUTION), _ => Err(LinkError::BootstrapError(format!( - "invalid/unrecognized `purpose` value '{}'", - s + "invalid/unrecognized `purpose` value '{s}'" ))), } } @@ -133,11 +131,19 @@ impl Import { match value { Value::String(str) => { if let Some(directive_name) = str.strip_prefix('@') { - Ok(Import { element: Name::new(directive_name)?, is_directive: true, alias: None }) + Ok(Import { + element: Name::new(directive_name)?, + is_directive: true, + alias: None, + }) } else { - Ok(Import { element: Name::new(str)?, is_directive: false, alias: None }) + Ok(Import { + element: Name::new(str)?, + is_directive: false, + alias: None, + }) } - }, + } Value::Object(fields) => { let mut name: Option<&str> = None; let mut alias: Option<&str> = None; @@ -145,47 +151,58 @@ impl Import { match k.as_str() { "name" => { name = Some(v.as_str().ok_or_else(|| { - LinkError::BootstrapError("invalid value for `name` field in @link(import:) argument: must be a string".to_string()) + LinkError::BootstrapError(format!(r#"in "{}", invalid value for `name` field in @link(import:) argument: must be a string"#, value.serialize().no_indent())) })?) }, "as" => { alias = Some(v.as_str().ok_or_else(|| { - LinkError::BootstrapError("invalid value for `as` field in @link(import:) argument: must be a string".to_string()) + LinkError::BootstrapError(format!(r#"in "{}", invalid value for `as` field in @link(import:) argument: must be a string"#, value.serialize().no_indent())) })?) }, - _ => Err(LinkError::BootstrapError(format!("unknown field `{k}` in @link(import:) argument")))? + _ => Err(LinkError::BootstrapError(format!(r#"in "{}", unknown field `{k}` in @link(import:) argument"#, value.serialize().no_indent())))? } } - if let Some(element) = name { - if let Some(directive_name) = element.strip_prefix('@') { - if let Some(alias_str) = alias.as_ref() { - let Some(alias_str) = alias_str.strip_prefix('@') else { - return Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should start with '@' since the imported name does", alias_str, element))); - }; - alias = Some(alias_str); - } - Ok(Import { - element: Name::new(directive_name)?, - is_directive: true, - alias: alias.map(Name::new).transpose()?, - }) - } else { - if let Some(alias) = &alias { - if alias.starts_with('@') { - return Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should not start with '@' (or, if {} is a directive, then the name should start with '@')", alias, element, element))); - } - } - Ok(Import { - element: Name::new(element)?, - is_directive: false, - alias: alias.map(Name::new).transpose()?, - }) + let Some(element) = name else { + return Err(LinkError::BootstrapError(format!( + r#"in "{}", invalid entry in @link(import:) argument, missing mandatory `name` field"#, + value.serialize().no_indent() + ))); + }; + if let Some(directive_name) = element.strip_prefix('@') { + if let Some(alias_str) = alias.as_ref() { + let Some(alias_str) = alias_str.strip_prefix('@') else { + return Err(LinkError::BootstrapError(format!( + r#"in "{}", invalid alias '{alias_str}' for import name '{element}': should start with '@' since the imported name does"#, + value.serialize().no_indent() + ))); + }; + alias = Some(alias_str); } + Ok(Import { + element: Name::new(directive_name)?, + is_directive: true, + alias: alias.map(Name::new).transpose()?, + }) } else { - Err(LinkError::BootstrapError("invalid entry in @link(import:) argument, missing mandatory `name` field".to_string())) + if let Some(alias) = &alias { + if alias.starts_with('@') { + return Err(LinkError::BootstrapError(format!( + r#"in "{}", invalid alias '{alias}' for import name '{element}': should not start with '@' (or, if {element} is a directive, then the name should start with '@')"#, + value.serialize().no_indent() + ))); + } + } + Ok(Import { + element: Name::new(element)?, + is_directive: false, + alias: alias.map(Name::new).transpose()?, + }) } - }, - _ => Err(LinkError::BootstrapError("invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form { name: \"\", as: \"\" }.".to_string())) + } + _ => Err(LinkError::BootstrapError(format!( + r#"in "{}", invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form {{ name: "", as: "" }}."#, + value.serialize().no_indent() + ))), } } @@ -197,7 +214,7 @@ impl Import { } pub fn imported_name(&self) -> &Name { - return self.alias.as_ref().unwrap_or(&self.element); + self.alias.as_ref().unwrap_or(&self.element) } pub fn imported_display_name(&self) -> impl fmt::Display + '_ { diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index badfd4cd89..fb5051c29b 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -325,14 +325,17 @@ impl QueryPlanner { ) -> Result { let operation = document .operations - .get(operation_name.as_ref().map(|name| name.as_str()))?; - + .get(operation_name.as_ref().map(|name| name.as_str())) + .map_err(|_| { + if operation_name.is_some() { + SingleFederationError::UnknownOperation + } else { + SingleFederationError::OperationNameNotProvided + } + })?; if operation.selection_set.is_empty() { // This should never happen because `operation` comes from a known-valid document. - // We could panic here but we are returning a `Result` already anyways, so shrug! - return Err(FederationError::internal( - "Invalid operation: empty selection set", - )); + crate::bail!("Invalid operation: empty selection set") } let is_subscription = operation.is_subscription(); diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index d78cd86728..850634be2b 100644 --- a/apollo-router/src/error.rs +++ b/apollo-router/src/error.rs @@ -316,8 +316,60 @@ pub(crate) enum QueryPlannerError { PoolProcessing(String), /// Federation error: {0} - // TODO: make `FederationError` serializable and store it as-is? - FederationError(String), + FederationError(FederationErrorBridge), +} + +impl From for QueryPlannerError { + fn from(value: FederationErrorBridge) -> Self { + Self::FederationError(value) + } +} + +/// A temporary error type used to extract a few variants from `apollo-federation`'s +/// `FederationError`. For backwards compatability, these other variant need to be extracted so +/// that the correct status code (GRAPHQL_VALIDATION_ERROR) can be added to the response. For +/// router 2.0, apollo-federation should split its error type into internal and external types. +/// When this happens, this temp type should be replaced with that type. +// TODO(@TylerBloom): See the comment above +#[derive(Error, Debug, Display, Clone, Serialize, Deserialize)] +pub(crate) enum FederationErrorBridge { + /// {0} + UnknownOperation(String), + /// {0} + OperationNameNotProvided(String), + /// {0} + Other(String), +} + +impl From for FederationErrorBridge { + fn from(value: FederationError) -> Self { + match &value { + err @ FederationError::SingleFederationError( + apollo_federation::error::SingleFederationError::UnknownOperation, + ) => Self::UnknownOperation(err.to_string()), + err @ FederationError::SingleFederationError( + apollo_federation::error::SingleFederationError::OperationNameNotProvided, + ) => Self::OperationNameNotProvided(err.to_string()), + err => Self::Other(err.to_string()), + } + } +} + +impl IntoGraphQLErrors for FederationErrorBridge { + fn into_graphql_errors(self) -> Result, Self> { + match self { + FederationErrorBridge::UnknownOperation(msg) => Ok(vec![Error::builder() + .message(msg) + .extension_code("GRAPHQL_VALIDATION_FAILED") + .build()]), + FederationErrorBridge::OperationNameNotProvided(msg) => Ok(vec![Error::builder() + .message(msg) + .extension_code("GRAPHQL_VALIDATION_FAILED") + .build()]), + // All other errors will be pushed on and be treated as internal server errors + err => Err(err), + } + } } impl IntoGraphQLErrors for Vec { @@ -408,6 +460,9 @@ impl IntoGraphQLErrors for QueryPlannerError { ); Ok(errors) } + QueryPlannerError::FederationError(err) => err + .into_graphql_errors() + .map_err(QueryPlannerError::FederationError), err => Err(err), } } @@ -574,7 +629,7 @@ pub(crate) enum SchemaError { /// GraphQL validation error: {0} Validate(ValidationErrors), /// Federation error: {0} - FederationError(apollo_federation::error::FederationError), + FederationError(FederationError), /// Api error(s): {0} #[from(ignore)] Api(String), diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 4e8143d3fb..10edd392ae 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -30,6 +30,7 @@ use super::QueryKey; use crate::apollo_studio_interop::generate_usage_reporting; use crate::compute_job; use crate::configuration::QueryPlannerMode; +use crate::error::FederationErrorBridge; use crate::error::PlanErrors; use crate::error::QueryPlannerError; use crate::error::SchemaError; @@ -63,7 +64,6 @@ use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; pub(crate) const JS_QP_MODE: &str = "js"; -const UNSUPPORTED_CONTEXT: &str = "context"; const UNSUPPORTED_FED1: &str = "fed1"; const INTERNAL_INIT_ERROR: &str = "internal"; @@ -182,12 +182,10 @@ impl PlannerMode { SingleFederationError::UnsupportedFederationVersion { .. } => { metric_rust_qp_init(Some(UNSUPPORTED_FED1)); } - SingleFederationError::UnsupportedFeature { message: _, kind } => match kind { - apollo_federation::error::UnsupportedFeatureKind::Context => { - metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)) - } - _ => metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)), - }, + SingleFederationError::UnsupportedFeature { + message: _, + kind: _, + } => metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)), _ => { metric_rust_qp_init(Some(INTERNAL_INIT_ERROR)); } @@ -280,8 +278,7 @@ impl PlannerMode { 1 ); } - let result = - result.map_err(|e| QueryPlannerError::FederationError(e.to_string())); + let result = result.map_err(FederationErrorBridge::from); let elapsed = start.elapsed().as_secs_f64(); metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); @@ -1484,13 +1481,6 @@ mod tests { 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_FED1)); assert_counter!( "apollo.router.lifecycle.query_planner.init", From 96289d474243107e16556dc9ed2b62ef9ad75bd4 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 10 Dec 2024 09:45:28 -0800 Subject: [PATCH 100/112] Update `ensure!` macro to return its error (#6432) In v1.58.0, the `internal_error!` macro was updated to no longer return its error for release builds, but the `ensure!` macro (which calls `internal_error!`) wasn't updated accordingly. This means that `ensure!` callsites in the current code are silently ignoring their errors if their check fails in release builds. This PR updates `ensure!` to use the `bail!` macro, which does return its error. --- apollo-federation/src/error/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index c0ea25e81b..caeecf0a4a 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -82,7 +82,7 @@ macro_rules! ensure { #[cfg(not(debug_assertions))] if !$expr { - $crate::internal_error!( $( $arg )+ ); + $crate::bail!( $( $arg )+ ); } } } From f844b4e3c6ebef06507c1856a83c1ce046dd7c0c Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Tue, 10 Dec 2024 16:58:19 -0800 Subject: [PATCH 101/112] fix(federation): convert query graph node's type for fetch dependency graph path (#6433) Fetch dependency graph expects supergraph types. So, Interface object types from subgraphs need to be converted to interface type positions. Also, added `--type-conditioned-fetching` option to the CLI tool. --- apollo-federation/cli/src/main.rs | 4 + .../src/query_plan/query_planner.rs | 11 +- .../query_plan/query_planning_traversal.rs | 36 ++++++- .../interface_object.rs | 101 ++++++++++++++++++ ...th_interface_object_does_not_crash.graphql | 86 +++++++++++++++ 5 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 apollo-federation/tests/query_plan/supergraphs/test_type_conditioned_fetching_with_interface_object_does_not_crash.graphql diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index b8ce23f24e..c89afb689c 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -26,6 +26,9 @@ struct QueryPlannerArgs { /// Generate fragments to compress subgraph queries. #[arg(long, default_value_t = false)] generate_fragments: bool, + /// Enable type conditioned fetching. + #[arg(long, default_value_t = false)] + type_conditioned_fetching: bool, /// Run GraphQL validation check on generated subgraph queries. (default: true) #[arg(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] subgraph_validation: Option, @@ -103,6 +106,7 @@ impl QueryPlannerArgs { fn apply(&self, config: &mut QueryPlannerConfig) { config.incremental_delivery.enable_defer = self.enable_defer; config.generate_query_fragments = self.generate_fragments; + config.type_conditioned_fetching = self.type_conditioned_fetching; config.subgraph_graphql_validation = self.subgraph_validation.unwrap_or(true); if let Some(max_evaluated_plans) = self.max_evaluated_plans { config.debug.max_evaluated_plans = max_evaluated_plans; diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index fb5051c29b..6398c7e738 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -32,6 +32,7 @@ use crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNodePath; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToQueryPlanProcessor; +use crate::query_plan::query_planning_traversal::convert_type_from_subgraph; use crate::query_plan::query_planning_traversal::BestQueryPlanInfo; use crate::query_plan::query_planning_traversal::QueryPlanningParameters; use crate::query_plan::query_planning_traversal::QueryPlanningTraversal; @@ -563,6 +564,7 @@ fn compute_root_serial_dependency_graph( ); compute_root_fetch_groups( operation.root_kind, + federated_query_graph, &mut fetch_dependency_graph, &prev_path, parameters.config.type_conditioned_fetching, @@ -600,6 +602,7 @@ fn only_root_subgraph(graph: &FetchDependencyGraph) -> Result, Federati )] pub(crate) fn compute_root_fetch_groups( root_kind: SchemaRootDefinitionKind, + federated_query_graph: &QueryGraph, dependency_graph: &mut FetchDependencyGraph, path: &OpPathTree, type_conditioned_fetching_enabled: bool, @@ -635,6 +638,12 @@ pub(crate) fn compute_root_fetch_groups( dependency_graph.to_dot(), "tree_with_root_node" ); + let subgraph_schema = federated_query_graph.schema_by_source(subgraph_name)?; + let supergraph_root_type = convert_type_from_subgraph( + root_type, + subgraph_schema, + &dependency_graph.supergraph_schema, + )?; compute_nodes_for_tree( dependency_graph, &child.tree, @@ -642,7 +651,7 @@ pub(crate) fn compute_root_fetch_groups( FetchDependencyGraphNodePath::new( dependency_graph.supergraph_schema.clone(), type_conditioned_fetching_enabled, - root_type, + supergraph_root_type, )?, Default::default(), &Default::default(), diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 21585e1b73..1739fc21cc 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -7,6 +7,7 @@ use serde::Serialize; use tracing::trace; use super::fetch_dependency_graph::FetchIdGenerator; +use crate::ensure; use crate::error::FederationError; use crate::operation::Operation; use crate::operation::Selection; @@ -187,6 +188,29 @@ impl BestQueryPlanInfo { } } +pub(crate) fn convert_type_from_subgraph( + ty: CompositeTypeDefinitionPosition, + subgraph_schema: &ValidFederationSchema, + supergraph_schema: &ValidFederationSchema, +) -> Result { + if subgraph_schema.is_interface_object_type(ty.clone().into())? { + let type_in_supergraph_pos: CompositeTypeDefinitionPosition = supergraph_schema + .get_type(ty.type_name().clone())? + .try_into()?; + ensure!( + matches!( + type_in_supergraph_pos, + CompositeTypeDefinitionPosition::Interface(_) + ), + "Type {} should be an interface in the supergraph", + ty.type_name() + ); + Ok(type_in_supergraph_pos) + } else { + Ok(ty) + } +} + impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { #[cfg_attr( feature = "snapshot_tracing", @@ -1004,6 +1028,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { if is_root_path_tree { compute_root_fetch_groups( self.root_kind, + &self.parameters.federated_query_graph, dependency_graph, path_tree, type_conditioned_fetching_enabled, @@ -1024,6 +1049,15 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.root_kind, root_type.clone(), )?; + let subgraph_schema = self + .parameters + .federated_query_graph + .schema_by_source(&query_graph_node.source)?; + let supergraph_root_type = convert_type_from_subgraph( + root_type, + subgraph_schema, + &dependency_graph.supergraph_schema, + )?; compute_nodes_for_tree( dependency_graph, path_tree, @@ -1031,7 +1065,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { FetchDependencyGraphNodePath::new( dependency_graph.supergraph_schema.clone(), self.parameters.config.type_conditioned_fetching, - root_type, + supergraph_root_type, )?, Default::default(), &Default::default(), diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs index 2f8ec2a798..6331175baa 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use apollo_federation::query_plan::query_planner::QueryPlannerConfig; use apollo_federation::query_plan::FetchDataPathElement; use apollo_federation::query_plan::FetchDataRewrite; @@ -956,3 +957,103 @@ fn test_interface_object_advance_with_non_collecting_and_type_preserving_transit "### ); } + +#[test] +fn test_type_conditioned_fetching_with_interface_object_does_not_crash() { + let planner = planner!( + config = QueryPlannerConfig { + type_conditioned_fetching: true, + ..Default::default() + }, + S1: r#" + type I @interfaceObject @key(fields: "id") { + id: ID! + t: T + } + + type T { + relatedIs: [I] + } + "#, + S2: r#" + type Query { + i: I + } + + interface I @key(fields: "id") { + id: ID! + a: Int + } + + type A implements I @key(fields: "id") { + id: ID! + a: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + { + i { + t { + relatedIs { + a + } + } + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S2") { + { + i { + __typename + id + } + } + }, + Flatten(path: "i") { + Fetch(service: "S1") { + { + ... on I { + __typename + id + } + } => + { + ... on I { + t { + relatedIs { + __typename + id + } + } + } + } + }, + }, + Flatten(path: "i.t.relatedIs.@") { + Fetch(service: "S2") { + { + ... on I { + __typename + id + } + } => + { + ... on I { + __typename + a + } + } + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/test_type_conditioned_fetching_with_interface_object_does_not_crash.graphql b/apollo-federation/tests/query_plan/supergraphs/test_type_conditioned_fetching_with_interface_object_does_not_crash.graphql new file mode 100644 index 0000000000..4a21fe7ba1 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/test_type_conditioned_fetching_with_interface_object_does_not_crash.graphql @@ -0,0 +1,86 @@ +# Composed from subgraphs with hash: 161c48cab8f2c97bc5fef235b557994f82dc7e51 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +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 + +type A implements I + @join__implements(graph: S2, interface: "I") + @join__type(graph: S2, key: "id") +{ + id: ID! + a: Int + t: T @join__field +} + +interface I + @join__type(graph: S1, key: "id", isInterfaceObject: true) + @join__type(graph: S2, key: "id") +{ + id: ID! + t: T @join__field(graph: S1) + a: Int @join__field(graph: S2) +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") +} + +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: S1) + @join__type(graph: S2) +{ + i: I @join__field(graph: S2) +} + +type T + @join__type(graph: S1) +{ + relatedIs: [I] +} From 6b43a2f3143fd9ad4572ff6673a8d436017babd0 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 10 Dec 2024 17:23:09 -0800 Subject: [PATCH 102/112] Miscellaneous type-conditioned fetching fixes (#6434) This PR makes bugfixes to the Rust QP port of type-conditioned fetching. There's a few of these, so I'm going to leave comments on the code with relevant information. --- apollo-federation/src/query_plan/display.rs | 4 +- .../src/query_plan/fetch_dependency_graph.rs | 52 +++++++++---------- apollo-federation/src/query_plan/mod.rs | 4 +- apollo-router/src/query_planner/convert.rs | 20 +++---- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index 48e9168684..4a3530d236 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -382,8 +382,8 @@ impl fmt::Display for FetchDataPathElement { } } -fn write_conditions(conditions: &[Name], f: &mut fmt::Formatter<'_>) -> fmt::Result { - if !conditions.is_empty() { +fn write_conditions(conditions: &Option>, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(conditions) = conditions { write!(f, "|[{}]", conditions.join(",")) } else { Ok(()) diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index 14e259aa68..7c92af6fde 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -509,15 +509,14 @@ impl FetchDependencyGraphNodePath { type_conditioned_fetching_enabled: bool, root_type: CompositeTypeDefinitionPosition, ) -> Result { - let root_possible_types = if type_conditioned_fetching_enabled { + let root_possible_types: IndexSet = if type_conditioned_fetching_enabled { schema.possible_runtime_types(root_type)? } else { Default::default() } .into_iter() - .map(|pos| Ok(pos.get(schema.schema())?.name.clone())) - .collect::, _>>() - .map_err(|e: PositionLookupError| FederationError::from(e))?; + .map(|pos| Ok::<_, PositionLookupError>(pos.get(schema.schema())?.name.clone())) + .process_results(|c| c.sorted().collect())?; Ok(Self { schema, @@ -577,12 +576,13 @@ impl FetchDependencyGraphNodePath { None => self.possible_types.clone(), Some(tcp) => { let element_possible_types = self.schema.possible_runtime_types(tcp.clone())?; - element_possible_types + self.possible_types .iter() .filter(|&possible_type| { - self.possible_types.contains(&possible_type.type_name) + element_possible_types + .contains(&ObjectTypeDefinitionPosition::new(possible_type.clone())) }) - .map(|possible_type| possible_type.type_name.clone()) + .cloned() .collect() } }, @@ -592,15 +592,11 @@ impl FetchDependencyGraphNodePath { } fn advance_field_type(&self, element: &Field) -> Result, FederationError> { - if !element - .output_base_type() - .map(|base_type| base_type.is_composite_type()) - .unwrap_or_default() - { + if !element.output_base_type()?.is_composite_type() { return Ok(Default::default()); } - let mut res = self + let mut res: IndexSet = self .possible_types .clone() .into_iter() @@ -616,17 +612,13 @@ impl FetchDependencyGraphNodePath { .schema .possible_runtime_types(typ)? .into_iter() - .map(|ctdp| ctdp.type_name) - .collect::>()) + .map(|ctdp| ctdp.type_name)) }) - .collect::>, FederationError>>()? - .into_iter() - .flatten() - .collect::>(); + .process_results::<_, _, FederationError, _>(|c| c.flatten().collect())?; res.sort(); - Ok(res.into_iter().collect()) + Ok(res) } fn updated_response_path( @@ -649,17 +641,22 @@ impl FetchDependencyGraphNodePath { match new_path.pop() { Some(FetchDataPathElement::AnyIndex(_)) => { - new_path.push(FetchDataPathElement::AnyIndex( + new_path.push(FetchDataPathElement::AnyIndex(Some( conditions.iter().cloned().collect(), - )); + ))); } Some(FetchDataPathElement::Key(name, _)) => { new_path.push(FetchDataPathElement::Key( name, - conditions.iter().cloned().collect(), + Some(conditions.iter().cloned().collect()), )); } Some(other) => new_path.push(other), + // TODO: We should be emitting type conditions here on no element like the + // JS code, which requires a new FetchDataPathElement variant in Rust. + // This really has to do with a hack we did to avoid changing fetch + // data paths too much, in which type conditions ought to be their own + // variant entirely. None => {} } } @@ -5285,11 +5282,10 @@ mod tests { ) } - fn cond_to_string(conditions: &[Name]) -> String { - if conditions.is_empty() { - return Default::default(); + fn cond_to_string(conditions: &Option>) -> String { + if let Some(conditions) = conditions { + return format!("|[{}]", conditions.iter().map(|n| n.to_string()).join(",")); } - - format!("|[{}]", conditions.iter().map(|n| n.to_string()).join(",")) + Default::default() } } diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index ce7cfae3bd..60e0b396a6 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -249,8 +249,8 @@ pub struct FetchDataKeyRenamer { /// elements. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub enum FetchDataPathElement { - Key(Name, Conditions), - AnyIndex(Conditions), + Key(Name, Option), + AnyIndex(Option), TypenameEquals(Name), Parent, } diff --git a/apollo-router/src/query_planner/convert.rs b/apollo-router/src/query_planner/convert.rs index 640ecb624f..5d6488025e 100644 --- a/apollo-router/src/query_planner/convert.rs +++ b/apollo-router/src/query_planner/convert.rs @@ -310,19 +310,15 @@ impl From<&'_ next::FetchDataPathElement> for crate::json_ext::PathElement { match value { next::FetchDataPathElement::Key(name, conditions) => Self::Key( name.to_string(), - if conditions.is_empty() { - None - } else { - Some(conditions.iter().map(|c| c.to_string()).collect()) - }, + conditions + .as_ref() + .map(|conditions| conditions.iter().map(|c| c.to_string()).collect()), + ), + next::FetchDataPathElement::AnyIndex(conditions) => Self::Flatten( + conditions + .as_ref() + .map(|conditions| conditions.iter().map(|c| c.to_string()).collect()), ), - next::FetchDataPathElement::AnyIndex(conditions) => { - Self::Flatten(if conditions.is_empty() { - None - } else { - Some(conditions.iter().map(|c| c.to_string()).collect()) - }) - } next::FetchDataPathElement::TypenameEquals(value) => Self::Fragment(value.to_string()), next::FetchDataPathElement::Parent => Self::Key("..".to_owned(), None), } From 15b72a452b58b37b011f5d9f5bc637d6555a1b65 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Tue, 10 Dec 2024 19:22:42 -0800 Subject: [PATCH 103/112] fix(semantic-diff): fixed false mismatches with type conditioned fetching enabled (#6431) Two issues: 1. JS QP may have empty key roots in response path data, which is ignored by the router. On the other hand, Rust QP does not use such a placeholder. So, the semantic diff ignores the placeholder during comparison. 2. JS QP may have empty condition list, but it was deserialized incorrectly (for example, `k|[]` would be interpreted as a whole key name). This is now fixed in the router's deserializer. Co-authored-by: Sachin D. Shinde --- apollo-router/src/json_ext.rs | 119 +++++------------ .../src/query_planner/plan_compare.rs | 123 +++++++++++++++++- 2 files changed, 155 insertions(+), 87 deletions(-) diff --git a/apollo-router/src/json_ext.rs b/apollo-router/src/json_ext.rs index 81423deb8b..7fe1f5e3f5 100644 --- a/apollo-router/src/json_ext.rs +++ b/apollo-router/src/json_ext.rs @@ -26,10 +26,26 @@ pub(crate) type Object = Map; const FRAGMENT_PREFIX: &str = "... on "; static TYPE_CONDITIONS_REGEX: Lazy = Lazy::new(|| { - Regex::new(r"(?:\|\[)(?.+?)(?:,\s*|)(?:\])") + Regex::new(r"\|\[(?.+?)?\]") .expect("this regex to check for type conditions is valid") }); +/// Extract the condition list from the regex captures. +fn extract_matched_conditions(caps: &Captures) -> TypeConditions { + caps.name("condition") + .map(|c| c.as_str().split(',').map(|s| s.to_string()).collect()) + .unwrap_or_default() +} + +fn split_path_element_and_type_conditions(s: &str) -> (String, Option) { + let mut type_conditions = None; + let path_element = TYPE_CONDITIONS_REGEX.replace(s, |caps: &Captures| { + type_conditions = Some(extract_matched_conditions(caps)); + "" + }); + (path_element.to_string(), type_conditions) +} + macro_rules! extract_key_value_from_object { ($object:expr, $key:literal, $pattern:pat => $var:ident) => {{ match $object.remove($key) { @@ -842,7 +858,7 @@ impl<'de> serde::de::Visitor<'de> for FlattenVisitor { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!( formatter, - "a string that is '@', potentially preceded of followed by type conditions" + "a string that is '@', potentially followed by type conditions" ) } @@ -850,23 +866,9 @@ impl<'de> serde::de::Visitor<'de> for FlattenVisitor { where E: serde::de::Error, { - let mut type_conditions: Vec = Vec::new(); - let path = TYPE_CONDITIONS_REGEX.replace(s, |caps: &Captures| { - type_conditions.extend( - caps.name("condition") - .map(|c| { - c.as_str() - .split(',') - .map(|s| s.to_string()) - .collect::>() - }) - .unwrap_or_default(), - ); - "" - }); - - if path == "@" { - Ok((!type_conditions.is_empty()).then_some(type_conditions)) + let (path_element, type_conditions) = split_path_element_and_type_conditions(s); + if path_element == "@" { + Ok(type_conditions) } else { Err(serde::de::Error::invalid_value( serde::de::Unexpected::Str(s), @@ -884,11 +886,7 @@ where S: serde::Serializer, { let tc_string = if let Some(c) = type_conditions { - if !c.is_empty() { - format!("|[{}]", c.join(",")) - } else { - "".to_string() - } + format!("|[{}]", c.join(",")) } else { "".to_string() }; @@ -911,7 +909,7 @@ impl<'de> serde::de::Visitor<'de> for KeyVisitor { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!( formatter, - "a string, potentially preceded of followed by type conditions" + "a string, potentially followed by type conditions" ) } @@ -919,21 +917,7 @@ impl<'de> serde::de::Visitor<'de> for KeyVisitor { where E: serde::de::Error, { - let mut type_conditions = Vec::new(); - let key = TYPE_CONDITIONS_REGEX.replace(s, |caps: &Captures| { - type_conditions.extend( - caps.extract::<1>() - .1 - .map(|s| s.split(',').map(|s| s.to_string())) - .into_iter() - .flatten(), - ); - "" - }); - Ok(( - key.to_string(), - (!type_conditions.is_empty()).then_some(type_conditions), - )) + Ok(split_path_element_and_type_conditions(s)) } } @@ -946,11 +930,7 @@ where S: serde::Serializer, { let tc_string = if let Some(c) = type_conditions { - if !c.is_empty() { - format!("|[{}]", c.join(",")) - } else { - "".to_string() - } + format!("|[{}]", c.join(",")) } else { "".to_string() }; @@ -992,43 +972,16 @@ where } fn flatten_from_str(s: &str) -> Result { - let mut type_conditions = Vec::new(); - let path = TYPE_CONDITIONS_REGEX.replace(s, |caps: &Captures| { - type_conditions.extend( - caps.extract::<1>() - .1 - .map(|s| s.split(',').map(|s| s.to_string())) - .into_iter() - .flatten(), - ); - "" - }); - - if path != "@" { + let (path_element, type_conditions) = split_path_element_and_type_conditions(s); + if path_element != "@" { return Err("invalid flatten".to_string()); } - Ok(PathElement::Flatten( - (!type_conditions.is_empty()).then_some(type_conditions), - )) + Ok(PathElement::Flatten(type_conditions)) } fn key_from_str(s: &str) -> Result { - let mut type_conditions = Vec::new(); - let key = TYPE_CONDITIONS_REGEX.replace(s, |caps: &Captures| { - type_conditions.extend( - caps.extract::<1>() - .1 - .map(|s| s.split(',').map(|s| s.to_string())) - .into_iter() - .flatten(), - ); - "" - }); - - Ok(PathElement::Key( - key.to_string(), - (!type_conditions.is_empty()).then_some(type_conditions), - )) + let (key, type_conditions) = split_path_element_and_type_conditions(s); + Ok(PathElement::Key(key, type_conditions)) } /// A path into the result document. @@ -1119,9 +1072,7 @@ impl Path { PathElement::Key(key, type_conditions) => { let mut tc = String::new(); if let Some(c) = type_conditions { - if !c.is_empty() { - tc = format!("|[{}]", c.join(",")); - } + tc = format!("|[{}]", c.join(",")); }; Some(format!("{}{}", key, tc)) } @@ -1191,17 +1142,13 @@ impl fmt::Display for Path { PathElement::Key(key, type_conditions) => { write!(f, "{key}")?; if let Some(c) = type_conditions { - if !c.is_empty() { - write!(f, "|[{}]", c.join(","))?; - } + write!(f, "|[{}]", c.join(","))?; }; } PathElement::Flatten(type_conditions) => { write!(f, "@")?; if let Some(c) = type_conditions { - if !c.is_empty() { - write!(f, "|[{}]", c.join(","))?; - } + write!(f, "|[{}]", c.join(","))?; }; } PathElement::Fragment(name) => { diff --git a/apollo-router/src/query_planner/plan_compare.rs b/apollo-router/src/query_planner/plan_compare.rs index c9df821102..81532fe98b 100644 --- a/apollo-router/src/query_planner/plan_compare.rs +++ b/apollo-router/src/query_planner/plan_compare.rs @@ -23,6 +23,8 @@ use super::FlattenNode; use super::PlanNode; use super::Primary; use super::QueryPlanResult; +use crate::json_ext::Path; +use crate::json_ext::PathElement; //================================================================================================== // Public interface @@ -534,10 +536,49 @@ fn deferred_node_matches(this: &DeferredNode, other: &DeferredNode) -> Result<() fn flatten_node_matches(this: &FlattenNode, other: &FlattenNode) -> Result<(), MatchFailure> { let FlattenNode { path, node } = this; - check_match_eq!(*path, other.path); + check_match!(same_path(path, &other.path)); plan_node_matches(node, &other.node) } +fn same_path(this: &Path, other: &Path) -> bool { + // Ignore the empty key root from the JS query planner + match this.0.split_first() { + Some((PathElement::Key(k, type_conditions), rest)) + if k.is_empty() && type_conditions.is_none() => + { + vec_matches(rest, &other.0, same_path_element) + } + _ => vec_matches(&this.0, &other.0, same_path_element), + } +} + +fn same_path_element(this: &PathElement, other: &PathElement) -> bool { + match (this, other) { + (PathElement::Index(this), PathElement::Index(other)) => this == other, + (PathElement::Fragment(this), PathElement::Fragment(other)) => this == other, + ( + PathElement::Key(this_key, this_type_conditions), + PathElement::Key(other_key, other_type_conditions), + ) => { + this_key == other_key + && same_path_condition(this_type_conditions, other_type_conditions) + } + ( + PathElement::Flatten(this_type_conditions), + PathElement::Flatten(other_type_conditions), + ) => same_path_condition(this_type_conditions, other_type_conditions), + _ => false, + } +} + +fn same_path_condition(this: &Option>, other: &Option>) -> bool { + match (this, other) { + (Some(this), Some(other)) => vec_matches_sorted(this, other), + (None, None) => true, + _ => false, + } +} + // Copied and modified from `apollo_federation::operation::SelectionKey` #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum SelectionKey { @@ -1185,3 +1226,83 @@ mod qp_selection_comparison_tests { assert!(same_requires(&requires1, &requires2)); } } + +#[cfg(test)] +mod path_comparison_tests { + use serde_json::json; + + use super::*; + + macro_rules! matches_deserialized_path { + ($json:expr, $expected:expr) => { + let path: Path = serde_json::from_value($json).unwrap(); + assert_eq!(path, $expected); + }; + } + + #[test] + fn test_type_condition_deserialization() { + matches_deserialized_path!( + json!(["k"]), + Path(vec![PathElement::Key("k".to_string(), None)]) + ); + matches_deserialized_path!( + json!(["k|[A]"]), + Path(vec![PathElement::Key( + "k".to_string(), + Some(vec!["A".to_string()]) + )]) + ); + matches_deserialized_path!( + json!(["k|[A,B]"]), + Path(vec![PathElement::Key( + "k".to_string(), + Some(vec!["A".to_string(), "B".to_string()]) + )]) + ); + matches_deserialized_path!( + json!(["k|[]"]), + Path(vec![PathElement::Key("k".to_string(), Some(vec![]))]) + ); + } + + macro_rules! assert_path_match { + ($a:expr, $b:expr) => { + let legacy_path: Path = serde_json::from_value($a).unwrap(); + let native_path: Path = serde_json::from_value($b).unwrap(); + assert!(same_path(&legacy_path, &native_path)); + }; + } + + macro_rules! assert_path_mismatch { + ($a:expr, $b:expr) => { + let legacy_path: Path = serde_json::from_value($a).unwrap(); + let native_path: Path = serde_json::from_value($b).unwrap(); + assert!(!same_path(&legacy_path, &native_path)); + }; + } + + #[test] + fn test_same_path_basic() { + // Basic symmetry tests. + assert_path_match!(json!([]), json!([])); + assert_path_match!(json!(["a"]), json!(["a"])); + assert_path_match!(json!(["a", "b"]), json!(["a", "b"])); + + // Basic mismatch tests. + assert_path_mismatch!(json!([]), json!(["a"])); + assert_path_mismatch!(json!(["a"]), json!(["b"])); + assert_path_mismatch!(json!(["a", "b"]), json!(["a", "b", "c"])); + } + + #[test] + fn test_same_path_ignore_empty_root_key() { + assert_path_match!(json!(["", "k|[A]", "v"]), json!(["k|[A]", "v"])); + } + + #[test] + fn test_same_path_distinguishes_empty_conditions_from_no_conditions() { + // Create paths that use no type conditions and empty type conditions + assert_path_mismatch!(json!(["k|[]", "v"]), json!(["k", "v"])); + } +} From d9336e43f8ef1af4821b41b27f87f0a5fe2498ec Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Tue, 10 Dec 2024 19:48:11 -0800 Subject: [PATCH 104/112] refactor(semantic-diff): changed some wording in path tests (#6436) - follow-up of PR #6431 --- apollo-router/src/query_planner/plan_compare.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/apollo-router/src/query_planner/plan_compare.rs b/apollo-router/src/query_planner/plan_compare.rs index 81532fe98b..7367ac2caa 100644 --- a/apollo-router/src/query_planner/plan_compare.rs +++ b/apollo-router/src/query_planner/plan_compare.rs @@ -1274,7 +1274,7 @@ mod path_comparison_tests { }; } - macro_rules! assert_path_mismatch { + macro_rules! assert_path_differ { ($a:expr, $b:expr) => { let legacy_path: Path = serde_json::from_value($a).unwrap(); let native_path: Path = serde_json::from_value($b).unwrap(); @@ -1284,15 +1284,10 @@ mod path_comparison_tests { #[test] fn test_same_path_basic() { - // Basic symmetry tests. - assert_path_match!(json!([]), json!([])); - assert_path_match!(json!(["a"]), json!(["a"])); - assert_path_match!(json!(["a", "b"]), json!(["a", "b"])); - - // Basic mismatch tests. - assert_path_mismatch!(json!([]), json!(["a"])); - assert_path_mismatch!(json!(["a"]), json!(["b"])); - assert_path_mismatch!(json!(["a", "b"]), json!(["a", "b", "c"])); + // Basic dis-equality tests. + assert_path_differ!(json!([]), json!(["a"])); + assert_path_differ!(json!(["a"]), json!(["b"])); + assert_path_differ!(json!(["a", "b"]), json!(["a", "b", "c"])); } #[test] @@ -1303,6 +1298,6 @@ mod path_comparison_tests { #[test] fn test_same_path_distinguishes_empty_conditions_from_no_conditions() { // Create paths that use no type conditions and empty type conditions - assert_path_mismatch!(json!(["k|[]", "v"]), json!(["k", "v"])); + assert_path_differ!(json!(["k|[]", "v"]), json!(["k", "v"])); } } From 2caf26510d5f30b5af70e8dc2a459f2bd80e2a22 Mon Sep 17 00:00:00 2001 From: Benjamin <5719034+bnjjj@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:27:55 +0100 Subject: [PATCH 105/112] prep release: v1.59.0-rc.0 --- Cargo.lock | 8 ++++---- apollo-federation/Cargo.toml | 2 +- apollo-router-benchmarks/Cargo.toml | 2 +- apollo-router-scaffold/Cargo.toml | 2 +- apollo-router-scaffold/templates/base/Cargo.template.toml | 2 +- .../templates/base/xtask/Cargo.template.toml | 2 +- apollo-router/Cargo.toml | 4 ++-- dockerfiles/tracing/docker-compose.datadog.yml | 2 +- dockerfiles/tracing/docker-compose.jaeger.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- helm/chart/router/Chart.yaml | 4 ++-- helm/chart/router/README.md | 8 ++++---- scripts/install.sh | 2 +- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6258b95be..71b00af853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,7 +204,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.58.1" +version = "1.59.0-rc.0" dependencies = [ "apollo-compiler", "derive_more", @@ -257,7 +257,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.58.1" +version = "1.59.0-rc.0" dependencies = [ "access-json", "ahash", @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.58.1" +version = "1.59.0-rc.0" dependencies = [ "apollo-parser", "apollo-router", @@ -443,7 +443,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.58.1" +version = "1.59.0-rc.0" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 739d379f60..34d5b0e999 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.58.1" +version = "1.59.0-rc.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 0a0f627142..2211c639fe 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.58.1" +version = "1.59.0-rc.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index a4d9414020..899776abc9 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.58.1" +version = "1.59.0-rc.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 3c6cbb58e9..15fdabbbf0 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.58.1" +apollo-router = "1.59.0-rc.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index 2d695c9daf..bc31af94d8 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.58.1" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.59.0-rc.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index b2b139e9d7..5464546fdd 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.58.1" +version = "1.59.0-rc.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -66,7 +66,7 @@ features = ["docs_rs"] access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.58.1" } +apollo-federation = { path = "../apollo-federation", version = "=1.59.0-rc.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index adb40c2e47..a94b7ddd79 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.58.1 + image: ghcr.io/apollographql/router:v1.59.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 9ea992be09..1a0109fbba 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.58.1 + image: ghcr.io/apollographql/router:v1.59.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index dc6986d5b7..945b147b53 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.58.1 + image: ghcr.io/apollographql/router:v1.59.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index cbdd08e61b..fe7cdc4f77 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.58.1 +version: 1.59.0-rc.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.58.1" +appVersion: "v1.59.0-rc.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index f26ac4d052..a7f5cfb9d6 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.58.1](https://img.shields.io/badge/Version-1.58.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.58.1](https://img.shields.io/badge/AppVersion-v1.58.1-informational?style=flat-square) +![Version: 1.59.0-rc.0](https://img.shields.io/badge/Version-1.59.0--rc.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.59.0-rc.0](https://img.shields.io/badge/AppVersion-v1.59.0--rc.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.58.1 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0-rc.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.58.1 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.58.1 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0-rc.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ @@ -81,7 +81,7 @@ helm show values oci://ghcr.io/apollographql/helm-charts/router | resources | object | `{}` | | | restartPolicy | string | `"Always"` | Sets the restart policy of pods | | rollingUpdate | object | `{}` | Sets the [rolling update strategy parameters](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment). Can take absolute values or % values. | -| router | object | `{"args":["--hot-reload"],"configuration":{"health_check":{"listen":"0.0.0.0:8088"},"supergraph":{"listen":"0.0.0.0:4000"}}}` | See https://www.apollographql.com/docs/router/configuration/overview/#yaml-config-file for yaml structure | +| router | object | `{"args":["--hot-reload"],"configuration":{"health_check":{"listen":"0.0.0.0:8088"},"supergraph":{"listen":"0.0.0.0:4000"}}}` | See https://www.apollographql.com/docs/graphos/reference/router/configuration#yaml-config-file for yaml structure | | securityContext | object | `{}` | | | service.annotations | object | `{}` | | | service.port | int | `80` | | diff --git a/scripts/install.sh b/scripts/install.sh index d9e413f445..a987f38bfa 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.58.1" +PACKAGE_VERSION="v1.59.0-rc.0" download_binary() { downloader --check From 3969e073a25759da82cae9643c7f009ab772bf15 Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Thu, 12 Dec 2024 14:19:18 +0100 Subject: [PATCH 106/112] General availability of native query planner - docs and changelog (#6424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The router's native, Rust-based, query planner is now generally available and enabled by default. The native query planner achieves better performance for a variety of graphs. In our tests, we observe: * 10x median improvement in query planning time (observed via `apollo.router.query_planning.plan.duration`) * 2.9x improvement in router’s CPU utilization * 2.2x improvement in router’s memory usage > Note: you can expect generated plans and subgraph operations in the native query planner to have slight differences when compared to the legacy, JavaScript-based query planner. We've ascertained these differences to be semantically insignificant, based on comparing ~2.5 million known unique user operations in GraphOS as well as comparing ~630 million operations across actual router deployments in shadow mode for a four month duration. The native query planner supports Federation v2 supergraphs. If you are using Federation v1 today, see our migration guide on how to update your composition build step and subgraph changes are typically not needed. The legacy, JavaScript, query planner is deprecated in this release, but you can still switch back to it if you are still using Federation v1 supergraph: ```yaml experimental_query_planner_mode: legacy ``` > Note: The subgraph operations generated by the query planner are not guaranteed consistent release over release. We strongly recommend against relying on the shape of planned subgraph operations, as new router features and optimizations will continuously affect it. Co-authored-by: Chandrika Srinivasan Co-authored-by: Edward Huang Co-authored-by: Shane Myrick --- .changesets/feat_native_query_planner_ga.md | 37 ++++ .../source/reference/router/configuration.mdx | 175 +++++++++--------- docs/source/routing/about-router.mdx | 49 +++-- .../query-planning/native-query-planner.mdx | 69 +++---- 4 files changed, 181 insertions(+), 149 deletions(-) create mode 100644 .changesets/feat_native_query_planner_ga.md diff --git a/.changesets/feat_native_query_planner_ga.md b/.changesets/feat_native_query_planner_ga.md new file mode 100644 index 0000000000..4f0c8469b9 --- /dev/null +++ b/.changesets/feat_native_query_planner_ga.md @@ -0,0 +1,37 @@ +### General availability of native query planner + +The router's native, Rust-based, query planner is now [generally available](https://www.apollographql.com/docs/graphos/reference/feature-launch-stages#general-availability) and enabled by default. + +The native query planner achieves better performance for a variety of graphs. In our tests, we observe: + +* 10x median improvement in query planning time (observed via `apollo.router.query_planning.plan.duration`) +* 2.9x improvement in router’s CPU utilization +* 2.2x improvement in router’s memory usage + +> Note: you can expect generated plans and subgraph operations in the native +query planner to have slight differences when compared to the legacy, JavaScript-based query planner. We've ascertained these differences to be semantically insignificant, based on comparing ~2.5 million known unique user operations in GraphOS as well as +comparing ~630 million operations across actual router deployments in shadow +mode for a four month duration. + +The native query planner supports Federation v2 supergraphs. If you are using Federation v1 today, see our [migration guide](https://www.apollographql.com/docs/graphos/reference/migration/to-federation-version-2) on how to update your composition build step and subgraph changes are typically not needed. + +The legacy, JavaScript, query planner is deprecated in this release, but you can still switch +back to it if you are still using Federation v1 supergraph: + +``` +experimental_query_planner_mode: legacy +``` + +> Note: The subgraph operations generated by the query planner are not +guaranteed consistent release over release. We strongly recommend against +relying on the shape of planned subgraph operations, as new router features and +optimizations will continuously affect it. + +By [@sachindshinde](https://github.com/sachindshinde), +[@goto-bus-stop](https://github.com/goto-bus-stop), +[@duckki](https://github.com/duckki), +[@TylerBloom](https://github.com/TylerBloom), +[@SimonSapin](https://github.com/SimonSapin), +[@dariuszkuc](https://github.com/dariuszkuc), +[@lrlna](https://github.com/lrlna), [@clenfest](https://github.com/clenfest), +and [@o0Ignition0o](https://github.com/o0Ignition0o). \ No newline at end of file diff --git a/docs/source/reference/router/configuration.mdx b/docs/source/reference/router/configuration.mdx index 2de7a04ecb..481ff80cb6 100644 --- a/docs/source/reference/router/configuration.mdx +++ b/docs/source/reference/router/configuration.mdx @@ -106,7 +106,7 @@ This reference lists and describes the options supported by the `router` binary. The [supergraph schema](/federation/federated-types/overview#supergraph-schema) of a router. Specified by absolute or relative path (`-s` / `--supergraph `, or `APOLLO_ROUTER_SUPERGRAPH_PATH`), or a comma-separated list of URLs (`APOLLO_ROUTER_SUPERGRAPH_URLS`). -> 💡 Avoid embedding tokens in `APOLLO_ROUTER_SUPERGRAPH_URLS` because the URLs may appear in log messages. +> 💡 Avoid embedding tokens in `APOLLO_ROUTER_SUPERGRAPH_URLS` because the URLs may appear in log messages. Setting this option disables polling from Apollo Uplink to fetch the latest supergraph schema. @@ -176,7 +176,7 @@ If set, a router runs in dev mode to help with local development. -If set, the router watches for changes to its configuration file and any supergraph file passed with `--supergraph` and reloads them automatically without downtime. This setting only affects local files provided to the router. The supergraph and configuration provided from GraphOS via Launches (and delivered via Uplink) are _always_ loaded automatically, regardless of this setting. +If set, the router watches for changes to its configuration file and any supergraph file passed with `--supergraph` and reloads them automatically without downtime. This setting only affects local files provided to the router. The supergraph and configuration provided from GraphOS via Launches (and delivered via Uplink) are _always_ loaded automatically, regardless of this setting. @@ -301,7 +301,6 @@ If set, the listen address of the router. - @@ -445,7 +444,7 @@ supergraph: supergraph: # The socket address and port to listen on. # Note that this must be quoted to avoid interpretation as an array in YAML. - listen: '[::1]:4000' + listen: "[::1]:4000" ``` #### Unix socket @@ -511,39 +510,39 @@ The router can serve any of the following landing pages to browsers that visit i - A basic landing page that displays an example query `curl` command (default) - ```yaml title="router.yaml" - # This is the default behavior. You don't need to include this config. - homepage: - enabled: true - ``` + ```yaml title="router.yaml" + # This is the default behavior. You don't need to include this config. + homepage: + enabled: true + ``` - _No_ landing page - ```yaml title="router.yaml" - homepage: - enabled: false - ``` + ```yaml title="router.yaml" + homepage: + enabled: false + ``` - [Apollo Sandbox](/graphos/explorer/sandbox), which enables you to explore your schema and compose operations against it using the Explorer - Note the additional configuration required to use Sandbox: + Note the additional configuration required to use Sandbox: - ```yaml title="router.yaml" - sandbox: - enabled: true + ```yaml title="router.yaml" + sandbox: + enabled: true - # Sandbox uses introspection to obtain your router's schema. - supergraph: - introspection: true + # Sandbox uses introspection to obtain your router's schema. + supergraph: + introspection: true - # Sandbox requires the default landing page to be disabled. - homepage: - enabled: false - ``` + # Sandbox requires the default landing page to be disabled. + homepage: + enabled: false + ``` - **Do not enable Sandbox in production.** Sandbox requires enabling introspection, which is strongly discouraged in production environments. + **Do not enable Sandbox in production.** Sandbox requires enabling introspection, which is strongly discouraged in production environments. @@ -559,7 +558,7 @@ override_subgraph_url: accounts: "${env.ACCOUNTS_SUBGRAPH_HOST_URL}" ``` -In this example, the `organizations` subgraph URL is overridden to point to `http://localhost:8080`, and the `accounts` subgraph URL is overridden to point to a new URL using [variable expansion](#variable-expansion). The URL specified in the supergraph schema is ignored. +In this example, the `organizations` subgraph URL is overridden to point to `http://localhost:8080`, and the `accounts` subgraph URL is overridden to point to a new URL using [variable expansion](#variable-expansion). The URL specified in the supergraph schema is ignored. Any subgraphs that are _omitted_ from `override_subgraph_url` continue to use the routing URL specified in the supergraph schema. @@ -575,7 +574,8 @@ By default, the router stores the following data in its in-memory cache to impro You can configure certain caching behaviors for generated query plans and APQ (but not introspection responses). For details, see [In-Memory Caching in the router](/router/configuration/in-memory-caching/). -**If you have a GraphOS Enterprise plan:** +**If you have a GraphOS Enterprise plan:** + - You can configure a Redis-backed _distributed_ cache that enables multiple router instances to share cached values. For details, see [Distributed caching in the GraphOS Router](/router/configuration/distributed-caching/). - You can configure a Redis-backed _entity_ cache that enables a client query to retrieve cached entity data split between subgraph reponses. For details, see [Subgraph entity caching in the GraphOS Router](/router/configuration/entity-caching/). @@ -589,17 +589,12 @@ Starting with v1.49.0, the router can run a Rust-native query planner. This nati -Starting with v1.57.0, to run the most performant and resource-efficient native query planner and to disable the V8 JavaScript runtime in the router, set the following options in your `router.yaml`: - -```yaml title="router.yaml" -experimental_query_planner_mode: new -``` +Starting with v1.59.0, the native query planner is GA and is run by default. -You can also improve throughput by reducing the size of queries sent to subgraphs with the following option: +If you need to run the deprecated JavaScript-based implementation, configure your router's query planner mode to `legacy`: ```yaml title="router.yaml" -supergraph: - generate_query_fragments: true +experimental_query_planner_mode: legacy ``` @@ -612,8 +607,7 @@ Learn more in [Native Query Planner](/router/executing-operations/native-query-p - + You can improve the performance of the router's query planner by configuring parallelized query planning. @@ -624,12 +618,13 @@ To resolve such blocking scenarios, you can enable parallel query planning. Conf ```yaml title="router.yaml" supergraph: query_planning: - experimental_parallelism: auto # number of available cpus + experimental_parallelism: auto # number of available cpus ``` -The value of `experimental_parallelism` is the number of query planners in the router's _query planner pool_. A query planner pool is a preallocated set of query planners from which the router can use to plan operations. The total number of pools is the maximum number of query planners that can run in parallel and therefore the maximum number of operations that can be worked on simultaneously. +The value of `experimental_parallelism` is the number of query planners in the router's _query planner pool_. A query planner pool is a preallocated set of query planners from which the router can use to plan operations. The total number of pools is the maximum number of query planners that can run in parallel and therefore the maximum number of operations that can be worked on simultaneously. Valid values of `experimental_parallelism`: + - Any integer starting from `1` - The special value `auto`, which sets the number of query planners equal to the number of available CPUs on the router's host machine @@ -661,7 +656,7 @@ It also includes other improvements that make it more likely that two operations Configure enhanced operation signature normalization in `router.yaml` with the `telemetry.apollo.signature_normalization_algorithm` option: ```yaml title="router.yaml" -telemetry: +telemetry: apollo: signature_normalization_algorithm: enhanced # Default is legacy ``` @@ -678,14 +673,14 @@ Given the following example operation: ```graphql showLineNumbers=false query InlineInputTypeQuery { inputTypeQuery( - input: { - inputString: "foo", - inputInt: 42, - inputBoolean: null, - nestedType: { someFloat: 4.2 }, - enumInput: SOME_VALUE_1, - nestedTypeList: [ { someFloat: 4.2, someNullableFloat: null } ], - listInput: [1, 2, 3] + input: { + inputString: "foo" + inputInt: 42 + inputBoolean: null + nestedType: { someFloat: 4.2 } + enumInput: SOME_VALUE_1 + nestedTypeList: [{ someFloat: 4.2, someNullableFloat: null }] + listInput: [1, 2, 3] } ) { enumResponse @@ -709,16 +704,16 @@ The enhanced normalization algorithm generates the following signature: query InlineInputTypeQuery { inputTypeQuery( input: { - inputString: "", - inputInt: 0, - inputBoolean: null, - nestedType: {someFloat: 0}, - enumInput: SOME_VALUE_1, - nestedTypeList: [{someFloat: 0, someNullableFloat: null}], + inputString: "" + inputInt: 0 + inputBoolean: null + nestedType: { someFloat: 0 } + enumInput: SOME_VALUE_1 + nestedTypeList: [{ someFloat: 0, someNullableFloat: null }] listInput: [] } ) { - enumResponse + enumResponse } } ``` @@ -821,7 +816,6 @@ The router supports extended reference reporting in the following versions: - You can configure the router to report enum and input object references for enhanced insights and operation checks. @@ -831,10 +825,11 @@ Legacy reporting can also cause [inaccurate operation checks](#enhanced-operatio Configure extended reference reporting in `router.yaml` with the `telemetry.apollo.metrics_reference_mode` option like so: ```yaml title="router.yaml" -telemetry: +telemetry: apollo: metrics_reference_mode: extended # Default is legacy ``` + #### Configuration effect timing Once you configure extended reference reporting, you can view enum value and input field usage alongside object [field usage in GraphOS Studio](/graphos/metrics/field-usage) for all subsequent operations. @@ -883,6 +878,7 @@ Thanks to extended reference reporting, operation checks can more accurately fla Changing or removing default values for input object fields is considered a breaking change. + You can [configure checks to ignore default values changes](/graphos/platform/schema-management/checks#ignored-conditions-settings). @@ -891,6 +887,7 @@ You can [configure checks to ignore default values changes](/graphos/platform/sc ##### Nullable input object field removal + Removing a nullable input object field is always considered a breaking change. Removing a nullable input object field is only considered a breaking change if the nullable field is present in historical operations. If the nullable field is always omitted in historical operations, its removal isn't considered a breaking change. @@ -904,10 +901,10 @@ You can [configure checks to ignore default values changes](/graphos/platform/sc Changing a nullable input object field to non-nullable is considered a breaking change. Changing a nullable input object field to non-nullable is only considered a breaking change if the field had a null value in historical operations. If the field was always a non-null value in historical operations, changing it to non-nullable isn't considered a breaking change. + - You won't see an immediate change in checks behavior when you first turn on extended reference reporting. @@ -982,28 +979,31 @@ TLS support is configured in the `tls` section, under the `supergraph` key for t The list of supported TLS versions and algorithms is static, it cannot be configured. Supported TLS versions: -* TLS 1.2 -* TLS 1.3 + +- TLS 1.2 +- TLS 1.3 Supported cipher suites: -* TLS13_AES_256_GCM_SHA384 -* TLS13_AES_128_GCM_SHA256 -* TLS13_CHACHA20_POLY1305_SHA256 -* TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 -* TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 -* TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 -* TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 -* TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 -* TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + +- TLS13_AES_256_GCM_SHA384 +- TLS13_AES_128_GCM_SHA256 +- TLS13_CHACHA20_POLY1305_SHA256 +- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 +- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Supported key exchange groups: -* X25519 -* SECP256R1 -* SECP384R1 + +- X25519 +- SECP256R1 +- SECP384R1 #### TLS termination -Clients can connect to the router directly over HTTPS, without terminating TLS in an intermediary. You can configure this in the `tls` configuration section: +Clients can connect to the router directly over HTTPS, without terminating TLS in an intermediary. You can configure this in the `tls` configuration section: ```yaml tls: @@ -1013,7 +1013,7 @@ tls: key: ${file./path/to/key.pem} ``` -To set the file paths in your configuration with Unix-style expansion, you can follow the examples in the [variable expansion](#variable-expansion) guide. +To set the file paths in your configuration with Unix-style expansion, you can follow the examples in the [variable expansion](#variable-expansion) guide. The router expects the file referenced in the `certificate_chain` value to be a combination of several PEM certificates concatenated together into a single file (as is commonplace with Apache TLS configuration). @@ -1092,7 +1092,7 @@ apq: router: cache: redis: - urls: [ "rediss://redis.example.com:6379" ] + urls: ["rediss://redis.example.com:6379"] #highlight-start tls: certificate_authorities: ${file./path/to/ca.crt} @@ -1162,6 +1162,7 @@ Limit the maximum buffer size for the HTTP1 connection. Default is ~400kib. Note for Rust Crate Users: If you are using the Router as a Rust crate, the `http1_request_max_buf_size` option requires the `hyper_header_limits` feature and also necessitates using Apollo's fork of the Hyper crate until the [changes are merged upstream](https://github.com/hyperium/hyper/pull/3523). You can include this fork by adding the following patch to your Cargo.toml file: + ```toml [patch.crates-io] "hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" } @@ -1185,26 +1186,29 @@ In the example below, the `GetProducts` operation has a recursion of three, and ```graphql query GetProducts { - allProducts { #1 + allProducts { + #1 ...productVariation - delivery { #2 + delivery { + #2 fastestDelivery #3 } } } fragment ProductVariation on Product { - variation { #1 + variation { + #1 name #2 } } ``` -Note that the router calculates the recursion depth for each operation and fragment _separately_. Even if a fragment is included in an operation, that fragment's recursion depth does not contribute to the _operation's_ recursion depth. +Note that the router calculates the recursion depth for each operation and fragment _separately_. Even if a fragment is included in an operation, that fragment's recursion depth does not contribute to the _operation's_ recursion depth. ### Demand control -See [Demand Control](/router/executing-operations/demand-control) to learn how to analyze the cost of operations and to reject requests with operations that exceed customizable cost limits. +See [Demand Control](/router/executing-operations/demand-control) to learn how to analyze the cost of operations and to reject requests with operations that exceed customizable cost limits. ### Early cancel @@ -1223,7 +1227,6 @@ supergraph: experimental_log_on_broken_pipe: true ``` - ### Plugins You can customize the router's behavior with [plugins](/router/customizations/overview). Each plugin can have its own section in the configuration file with arbitrary values: @@ -1251,6 +1254,7 @@ The router uses Unix-style expansion. Here are some examples: Variable expansions are valid only for YAML _values_, not keys: + ```yaml supergraph: listen: "${env.MY_LISTEN_ADDRESS}" #highlight-line @@ -1259,6 +1263,7 @@ example: ``` + ### Automatic fragment generation @@ -1276,7 +1281,7 @@ supergraph: The legacy query planner still supports an experimental algorithm that attempts to -reuse fragments from the original operation while forming subgraph requests. The +reuse fragments from the original operation while forming subgraph requests. The legacy query planner has to be explicitly enabled. This experimental feature used to be enabled by default, but is still available to support subgraphs that rely on the specific shape of fragments in an operation: @@ -1338,8 +1343,8 @@ New releases of the router might introduce breaking changes to the [YAML config 1. The router emits a warning on startup. 2. The router attempts to translate your provided configuration to the new expected format. - - If the translation succeeds without errors, the router starts up as usual. - - If the translation fails, the router terminates. + - If the translation succeeds without errors, the router starts up as usual. + - If the translation fails, the router terminates. If you encounter this warning, you can use the `router config upgrade` command to see the new expected format for your existing configuration file: @@ -1355,4 +1360,4 @@ You can also view a diff of exactly which changes are necessary to upgrade your ## Related topics -* [Checklist for configuring the router for production](/technotes/TN0008-production-readiness-checklist/#apollo-router) +- [Checklist for configuring the router for production](/technotes/TN0008-production-readiness-checklist/#apollo-router) diff --git a/docs/source/routing/about-router.mdx b/docs/source/routing/about-router.mdx index 47c71d0de6..b1bbaf2caf 100644 --- a/docs/source/routing/about-router.mdx +++ b/docs/source/routing/about-router.mdx @@ -1,34 +1,38 @@ --- title: Supergraph Routing with GraphOS Router -subtitle: Learn the basics about router features and deployment types +subtitle: Learn the basics about router features and deployment types description: Apollo provides cloud and self-hosted GraphOS Router options. The router acts as an entry point to your GraphQL APIs and provides a unified interface for clients to interact with. redirectFrom: - - /graphos/routing + - /graphos/routing --- ## What is GraphOS Router? GraphOS Router is the runtime of the GraphOS platform. It executes client operations by planning and executing subgraph queries, then merging them into client responses. It's also the single entry point and gateway to your federated GraphQL API. - - + + -### Runtime of GraphOS platform +### Runtime of GraphOS platform As the runtime of the [GraphOS platform](/graphos/get-started/concepts/graphos), a GraphOS Router gets the supergraph schema—the blueprint of the federated graphs—from the GraphOS control plane. It then executes incoming clients operations based on that schema. Unlike API gateways that offer capabilities to manage API endpoints, the router isn't based on URLs or REST endpoints. Rather, the router is a GraphQL-native solution for handling client APIs. -### Subgraph query planner +### Subgraph query planner Whenever your router receives an incoming GraphQL operation, it needs to figure out how to use your subgraphs to populate data for each of that operation's fields. To do this, the router generates a _query plan_: - - + + A query plan is a blueprint for dividing a single incoming operation into one or more operations that are each resolvable by a single subgraph. Some of these operations depend on the results of other operations, so the query plan also defines any required ordering for their execution. The router's query planner determines the optimal set of subgraph queries for each client operation, then it merges the subgraph responses into a single response for the client. You can use the following tools for inspecting query plans: + - Use the [Explorer IDE](/graphos/platform/explorer/) to view dynamically calculated example query plans for your operations in its right-hand panel. - Use the [Apollo Solutions command line utility](https://github.com/apollosolutions/generate-query-plan) for generating a query plan locally. @@ -46,8 +50,11 @@ As the entry point to your supergraph, a GraphOS Router must be able to process You can choose for Apollo to provision and manage the runtime infrastructure for your routers. Apollo hosts and deploys each instance of router in the cloud. Each _cloud-hosted router_ instance is fully integrated and configurable within GraphOS. - - + + @@ -59,8 +66,11 @@ While cloud routers are hosted in the cloud, GraphQL subgraph servers are still You can choose to manage the runtime infrastructure for your routers by yourself. Using container images of router, you can host and deploy your router instances from your own infrastructure. These _self-hosted router_ instances allow you full control over their deployment. - - + + ### Common router core @@ -110,11 +120,12 @@ Apollo offers the following router options, in increasing order of configurabili You host and manage the router on your own infrastructure. Highly configurable and customizable, including all options for Cloud - Dedicated routers and additional [customization options](/graphos/routing/customization/overview). + Dedicated routers and additional [customization + options](/graphos/routing/customization/overview). - The Apollo Router Core is available as a free and source-available router. - Connecting your self-hosted router to GraphOS requires an{' '} + The Apollo Router Core is available as a free and source-available + router. Connecting your self-hosted router to GraphOS requires an{" "} Enterprise plan. @@ -123,7 +134,11 @@ Apollo offers the following router options, in increasing order of configurabili -**We've paused new sign-ups for Serverless and Dedicated plans while we improve our offerings based on user feedback. This means cloud routing is temporarily unavailable to new users. In the meantime, you can explore other GraphOS features with a [free trial](https://studio.apollographql.com/signup?referrer=docs-content). +**We've paused new sign-ups for Serverless and Dedicated plans while +we improve our offerings based on user feedback. This means cloud routing is +temporarily unavailable to new users. In the meantime, you can explore other +GraphOS features with a [free +trial](https://studio.apollographql.com/signup?referrer=docs-content). @@ -133,7 +148,6 @@ Although powered by the source-available Apollo Router Core binary, GraphOS Rout Cloud-hosted routers automatically have access to additional GraphOS Router features, while self-hosted routers must be authenticated with a GraphOS Enterprise license to gain access to these features. Refer to the [pricing page](https://www.apollographql.com/pricing#graphos-router) to compare GraphOS Router features across plan types. - ## Next steps - Learn more about Apollo-managed routers in [cloud-hosted router](/graphos/routing/cloud/) @@ -146,4 +160,3 @@ Cloud-hosted routers automatically have access to additional GraphOS Router feat - To learn more about the intricacies of query plans, see the [example graph](/graphos/reference/federation/query-plans#example-graph) and [query plan](/graphos/reference/federation/query-plans#example-graph) in reference docs -- For the most performant query planning, configure and use the [Rust-native query planner](/graphos/routing/query-planning/native-query-planner). diff --git a/docs/source/routing/query-planning/native-query-planner.mdx b/docs/source/routing/query-planning/native-query-planner.mdx index 7da2d5a099..2f6cb487ce 100644 --- a/docs/source/routing/query-planning/native-query-planner.mdx +++ b/docs/source/routing/query-planning/native-query-planner.mdx @@ -7,17 +7,13 @@ redirectFrom: - /router/executing-operations/native-query-planner --- - - -Learn to run the GraphOS Router with the Rust-native query planner and improve your query planning performance and scalability. +Learn about the Rust-native query planner in GraphOS Router. The planner is GA as of v1.59.0. ## Background about query planner implementations In v1.49.0 the router introduced a [query planner](/graphos/routing/about-router#query-planning) implemented natively in Rust. This native query planner improves the overall performance and resource utilization of query planning. It exists alongside the legacy JavaScript implementation that uses the V8 JavaScript engine, and it will eventually replace the legacy implementation. -### Comparing query planner implementations - -As part of the effort to ensure correctness and stability of the new query planner, starting in v1.53.0 the router enables both the new and legacy planners and runs them in parallel to compare their results by default. After their comparison, the router discards the native query planner's results and uses only the legacy planner to execute requests. The native query planner uses a single thread in the cold path of the router. It has a bounded queue of ten queries. If the queue is full, the router simply does not run the comparison to avoid excessive resource consumption. +As of v1.59.0, the native query planner is the default planner in the router. As part of this, Deno, and by extension v8, are no longer initialized at router startup. The legacy implementation is deprecated, but it is still possible to configure the router to run with the legacy query planner. ## Configuring query planning @@ -25,55 +21,36 @@ You can configure the `experimental_query_planner_mode` option in your `router.y The `experimental_query_planner_mode` option has the following supported modes: -- `new`- enables only the new Rust-native query planner -- `legacy` - enables only the legacy JavaScript query planner -- `both_best_effort` (default) - enables both new and legacy query planners for comparison. The legacy query planner is used for execution. - - - -## Optimize native query planner - - - -To run the native query planner with the best performance and resource utilization, configure your router with the following options: - -```yaml title="router.yaml" -experimental_query_planner_mode: new -``` - - - -In router v1.56, running the native query planner with the best performance and resource utilization also requires setting `experimental_introspection_mode: new`. +- `new` (default) - enables only the new Rust-native query planner +- `legacy` - enables only the legacy JavaScript query planner. The legacy planner is deprecated and will be removed in the next router release. +- `both_best_effort` - enables both new and legacy query planners for comparison. The legacy query planner is used for execution. - +`experimental_query_planner_mode` will be removed in the next router release. -Setting `experimental_query_planner_mode: new` not only enables native query planning and schema introspection, it also disables the V8 JavaScript runtime used by the legacy query planner. Disabling V8 frees up CPU and memory and improves native query planning performance. - -Additionally, to enable more optimal native query planning and faster throughput by reducing the size of queries sent to subgraphs, you can enable query fragment generation with the following option: - -```yaml title="router.yaml" -supergraph: - generate_query_fragments: true -``` - - - -Regarding [fragment reuse and generation](/router/configuration/overview#fragment-reuse-and-generation), in the future the `generate_query_fragments` option will be the only option for handling fragments. +### Comparing query planner implementations - +As part of the effort to ensure correctness and stability of the new query planner, starting in v1.53.0 the router enables both the new and legacy planners and runs them in parallel to compare their results by default. After their comparison, the router discards the native query planner's results and uses only the legacy planner to execute requests. The native query planner uses a single thread in the cold path of the router. It has a bounded queue of ten queries. If the queue is full, the router simply does not run the comparison to avoid excessive resource consumption. -## Metrics for native query planner +### Metrics for native query planner When running both query planners for comparison with `experimental_query_planner_mode: both_best_effort`, the following metrics track mismatches and errors: - `apollo.router.operations.query_planner.both` with the following attributes: - - `generation.is_matched` (bool) - - `generation.js_error` (bool) - - `generation.rust_error` (bool) + + - `generation.is_matched` (bool) + - `generation.js_error` (bool) + - `generation.rust_error` (bool) - `apollo.router.query_planning.plan.duration` with the following attributes to differentiate between planners: - - `planner` (rust | js) + - `planner` (rust | js) + +## Federation v1 composed supergraphs -## Limitations of native query planner +The native query planner does not support _supergraphs_ composed with Federation v1, so the router will fallback to the legacy planner for any variants still using a Federation v1 supergraph in v1.59.0. Users are highly encouraged to [recompose with Federation v2](https://www.apollographql.com/docs/graphos/reference/migration/to-federation-version-2#step-2-configure-your-composition-method). +Federation v1 _subgraphs_ continue to be supported. -The native query planner doesn't implement `@context`. This is planned to be implemented in a future router release. +Customers that are on Federation v1 composed supergraph should see this as an info level log on startup: + +```bash +2024-12-05T10:13:39.760333Z INFO 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 +``` From 99b57b0faf65a0b8be14241431060df19e059fbe Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Mon, 16 Dec 2024 11:39:02 +0100 Subject: [PATCH 107/112] Apply suggestions from code review Co-authored-by: Edward Huang --- .changesets/fix_bnjjj_fix_880.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.changesets/fix_bnjjj_fix_880.md b/.changesets/fix_bnjjj_fix_880.md index 3c9f0edc78..939fbda854 100644 --- a/.changesets/fix_bnjjj_fix_880.md +++ b/.changesets/fix_bnjjj_fix_880.md @@ -1,7 +1,9 @@ ### Fix telemetry instrumentation using supergraph query selector ([PR #6324](https://github.com/apollographql/router/pull/6324)) -Query selector was raising error logs like `this is a bug and should not happen`. It's now fixed. -Now this configuration will work properly: +Previously, router telemetry instrumentation that used query selectors could log errors with messages such as `this is a bug and should not happen`. + +These errors have now been fixed, and configurations with query selectors such as the following work properly: + ```yaml title=router.yaml telemetry: exporters: From f8bb2763d030048638fdb7b0a0621b491e2dfa7c Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Mon, 16 Dec 2024 11:02:28 +0000 Subject: [PATCH 108/112] Apply suggestions from code review Co-authored-by: Edward Huang --- ...at_feat_fleet_detector_add_schema_metrics.md | 6 +++--- .changesets/feat_glasser_pq_client_name.md | 17 ++++++++++------- .../feat_glasser_pq_safelist_override.md | 5 ++--- .../feat_jr_add_fleet_awareness_plugin.md | 13 +++++-------- .changesets/fix_address_dentist_buyer_frown.md | 7 +++++-- .changesets/fix_fix_query_hashing.md | 6 +++--- .changesets/maint_lrlna_remove_catch_unwind.md | 9 ++++----- ...maint_renee_router_297_monotonic_counters.md | 4 ++-- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md index 472682897f..b9ab16aa5a 100644 --- a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md +++ b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md @@ -1,6 +1,6 @@ -### Adds Fleet Awareness Schema Metrics +### Add fleet awareness schema metric ([PR #6283](https://github.com/apollographql/router/pull/6283)) -Adds an additional metric to the `fleet_detector` plugin in the form of `apollo.router.schema` with 2 attributes: -`schema_hash` and `launch_id`. + +The router now supports the `apollo.router.instance.schema` metric for its `fleet_detector` plugin. It has two attributes: `schema_hash` and `launch_id`. By [@loshz](https://github.com/loshz) and [@nmoutschen](https://github.com/nmoutschen) in https://github.com/apollographql/router/pull/6283 diff --git a/.changesets/feat_glasser_pq_client_name.md b/.changesets/feat_glasser_pq_client_name.md index 42157835d3..a9da1d73af 100644 --- a/.changesets/feat_glasser_pq_client_name.md +++ b/.changesets/feat_glasser_pq_client_name.md @@ -1,13 +1,16 @@ -### Client name support for Persisted Query Lists ([PR #6198](https://github.com/apollographql/router/pull/6198)) +### Support client name for persisted query lists ([PR #6198](https://github.com/apollographql/router/pull/6198)) -The persisted query manifest fetched from Uplink can now contain a `clientName` field in each operation. Two operations with the same `id` but different `clientName` are considered to be distinct operations (and may have distinct bodies). +The persisted query manifest fetched from Apollo Uplink can now contain a `clientName` field in each operation. Two operations with the same `id` but different `clientName` are considered to be distinct operations, and they may have distinct bodies. -Router resolves the client name by taking the first of these which exists: -- Reading the `apollo_persisted_queries::client_name` context key (which may be set by a `router_service` plugin) -- Reading the HTTP header named by `telemetry.apollo.client_name_header` (which defaults to `apollographql-client-name`) +The router resolves the client name by taking the first from the following that exists: +- Reading the `apollo_persisted_queries::client_name` context key that may be set by a `router_service` plugin +- Reading the HTTP header named by `telemetry.apollo.client_name_header`, which defaults to `apollographql-client-name` -If a client name can be resolved for a request, Router first tries to find a persisted query with the specified ID and the resolved client name. -If there is no operation with that ID and client name, or if a client name cannot be resolved, Router tries to find a persisted query with the specified ID and no client name specified. (This means that existing PQ lists that do not contain client names will continue to work.) +If a client name can be resolved for a request, the router first tries to find a persisted query with the specified ID and the resolved client name. + +If there is no operation with that ID and client name, or if a client name cannot be resolved, the router tries to find a persisted query with the specified ID and no client name specified. This means that existing PQ lists that don't contain client names will continue to work. + +To learn more, go to [persisted queries](https://www.apollographql.com/docs/graphos/routing/security/persisted-queries#apollo_persisted_queriesclient_name) docs. By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6198 \ No newline at end of file diff --git a/.changesets/feat_glasser_pq_safelist_override.md b/.changesets/feat_glasser_pq_safelist_override.md index 316febbca9..4d618c36ca 100644 --- a/.changesets/feat_glasser_pq_safelist_override.md +++ b/.changesets/feat_glasser_pq_safelist_override.md @@ -2,9 +2,8 @@ If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. -(This does not affect the logging of unknown operations by the `persisted_queries.log_unknown` option.) +> Note: this doesn't affect the logging of unknown operations by the `persisted_queries.log_unknown` option. -In cases where an operation would have been denied but is allowed due to the context key existing, the attribute -`persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value true. +In cases where an operation would have been denied but is allowed due to the context key existing, the attribute `persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value `true`. By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 \ No newline at end of file diff --git a/.changesets/feat_jr_add_fleet_awareness_plugin.md b/.changesets/feat_jr_add_fleet_awareness_plugin.md index da6bc19375..863e7e2166 100644 --- a/.changesets/feat_jr_add_fleet_awareness_plugin.md +++ b/.changesets/feat_jr_add_fleet_awareness_plugin.md @@ -1,13 +1,10 @@ -### Adds Fleet Awareness Plugin +### Add fleet awareness plugin ([PR #6151](https://github.com/apollographql/router/pull/6151)) -Adds a new plugin that reports telemetry to Apollo on the configuration and deployment of the Router. Initially this -covers CPU & Memory usage, CPU Frequency, and other deployment characteristics such as Operating System, and Cloud -Provider. For more details, along with a full list of data captured and how to opt out, please see our guidance -[here](https://www.apollographql.com/docs/graphos/reference/data-privacy). +A new `fleet_awareness` plugin has been added that reports telemetry to Apollo about the configuration and deployment of the router. + +The reported telemetry include CPU and memory usage, CPU frequency, and other deployment characteristics such as operating system and cloud provider. For more details, along with a full list of data captured and how to opt out, go to our +[data privacy policy](https://www.apollographql.com/docs/graphos/reference/data-privacy). -As part of the above PluginPrivate has been extended with a new `activate` hook which is guaranteed to be called once -the OTEL meter has been refreshed. This ensures that code, particularly that concerned with gauges, will survive a hot -reload of the router. By [@jonathanrainer](https://github.com/jonathanrainer), [@nmoutschen](https://github.com/nmoutschen), [@loshz](https://github.com/loshz) in https://github.com/apollographql/router/pull/6151 diff --git a/.changesets/fix_address_dentist_buyer_frown.md b/.changesets/fix_address_dentist_buyer_frown.md index d3a75eba72..243f51fa0b 100644 --- a/.changesets/fix_address_dentist_buyer_frown.md +++ b/.changesets/fix_address_dentist_buyer_frown.md @@ -1,5 +1,6 @@ ### Fix coprocessor empty body object panic ([PR #6398](https://github.com/apollographql/router/pull/6398)) -If a coprocessor responds with an empty body object at the supergraph stage then the router would panic. + +Previously, the router would panic if a coprocessor responds with an empty body object at the supergraph stage: ```json { @@ -8,6 +9,8 @@ If a coprocessor responds with an empty body object at the supergraph stage then } ``` -This does not affect coprocessors that respond with formed responses. +This has been fixed in this release. + +> Note: the previous issue didn't affect coprocessors that responded with formed responses. By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6398 diff --git a/.changesets/fix_fix_query_hashing.md b/.changesets/fix_fix_query_hashing.md index f7414b10c8..e371bd476e 100644 --- a/.changesets/fix_fix_query_hashing.md +++ b/.changesets/fix_fix_query_hashing.md @@ -1,8 +1,8 @@ -### Fix the query hashing algorithm ([PR #6205](https://github.com/apollographql/router/pull/6205)) +### Fix query hashing algorithm ([PR #6205](https://github.com/apollographql/router/pull/6205)) > [!IMPORTANT] -> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), updates to the query planner in this release will result in query plan caches being re-generated rather than re-used. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new query plans come into service. +> If you have enabled [distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), updates to the query planner in this release will result in query plan caches being regenerated rather than reused. On account of this, you should anticipate additional cache regeneration cost when updating to this router version while the new query plans come into service. -The Router includes a schema-aware query hashing algorithm designed to return the same hash across schema updates if the query remains unaffected. This update enhances the algorithm by addressing various corner cases, improving its reliability and consistency. +The router includes a schema-aware query hashing algorithm designed to return the same hash across schema updates if the query remains unaffected. This update enhances the algorithm by addressing various corner cases to improve its reliability and consistency. By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6205 diff --git a/.changesets/maint_lrlna_remove_catch_unwind.md b/.changesets/maint_lrlna_remove_catch_unwind.md index 0475d08cc0..8390cbe278 100644 --- a/.changesets/maint_lrlna_remove_catch_unwind.md +++ b/.changesets/maint_lrlna_remove_catch_unwind.md @@ -1,14 +1,13 @@ ### Remove catch_unwind wrapper around the native query planner ([PR #6397](https://github.com/apollographql/router/pull/6397)) -As part of internal maintenance of the query planner, we are removing the -catch_unwind wrapper around the native query planner. This wrapper was used as -an extra safeguard for potential panics the native planner could produce. The -native query planner no longer has any code paths that could panic. We have also +As part of internal maintenance of the query planner, the +`catch_unwind` wrapper around the native query planner has been removed. This wrapper served as an extra safeguard for potential panics the native planner could produce. The +native query planner however no longer has any code paths that could panic. We have also not witnessed a panic in the last four months, having processed 560 million real user operations through the native planner. -This maintenance work also removes backtrace capture for federation errors which +This maintenance work also removes backtrace capture for federation errors, which was used for debugging and is no longer necessary as we have the confidence in the native planner's implementation. diff --git a/.changesets/maint_renee_router_297_monotonic_counters.md b/.changesets/maint_renee_router_297_monotonic_counters.md index dffc9b6f3b..f90446e3f4 100644 --- a/.changesets/maint_renee_router_297_monotonic_counters.md +++ b/.changesets/maint_renee_router_297_monotonic_counters.md @@ -1,6 +1,6 @@ -### Docs-deprecate several metrics ([PR #6350](https://github.com/apollographql/router/pull/6350)) +### Deprecate various metrics ([PR #6350](https://github.com/apollographql/router/pull/6350)) -These metrics are deprecated in favor of better, OTel-compatible alternatives: +Several metrics have been deprecated in this release, in favor of OpenTelemetry-compatible alternatives: - `apollo_router_deduplicated_subscriptions_total` - use the `apollo.router.operations.subscriptions` metric's `subscriptions.deduplicated` attribute. - `apollo_authentication_failure_count` - use the `apollo.router.operations.authentication.jwt` metric's `authentication.jwt.failed` attribute. From 5a2b33c6bb2ee3cf7ba2e7b83459042b7c40e68a Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Mon, 16 Dec 2024 11:04:59 +0000 Subject: [PATCH 109/112] Apply suggestions from code review Co-authored-by: Edward Huang --- .changesets/config_simon_router_version_in_cache_key.md | 7 ++++--- .changesets/feat_glasser_pq_safelist_override.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.changesets/config_simon_router_version_in_cache_key.md b/.changesets/config_simon_router_version_in_cache_key.md index ce8b9a8690..e5028023b1 100644 --- a/.changesets/config_simon_router_version_in_cache_key.md +++ b/.changesets/config_simon_router_version_in_cache_key.md @@ -1,7 +1,8 @@ -### Distributed query plan cache keys include the Router version number ([PR #6406](https://github.com/apollographql/router/pull/6406)) +### Add version number to distributed query plan cache keys ([PR #6406](https://github.com/apollographql/router/pull/6406)) -More often than not, an Apollo Router release may contain changes that affect what query plans are generated or how they’re represented. To avoid using outdated entries from distributed cache, the cache key includes a counter that was manually incremented with relevant data structure or algorithm changes. Instead the cache key now includes the Router version number, so that different versions will always use separate cache entries. +The router now includes its version number in the cache keys of distributed cache entries. Given that a new router release may change how query plans are generated or represented, including the router version in a cache key enables the router to use separate cache entries for different versions. + +If you have enabled [distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), expect additional processing for your cache to update for this router release. -If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), starting with this release and going forward you should anticipate additional cache regeneration cost when updating between any Router versions. By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6406 diff --git a/.changesets/feat_glasser_pq_safelist_override.md b/.changesets/feat_glasser_pq_safelist_override.md index 4d618c36ca..f849df60a6 100644 --- a/.changesets/feat_glasser_pq_safelist_override.md +++ b/.changesets/feat_glasser_pq_safelist_override.md @@ -1,4 +1,4 @@ -### Ability to skip Persisted Query List safelisting enforcement via plugin ([PR #6403](https://github.com/apollographql/router/pull/6403)) +### Ability to skip persisted query list safelisting enforcement via plugin ([PR #6403](https://github.com/apollographql/router/pull/6403)) If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. From ef1a282fbf073455d18fe05203bcf33f906784bc Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Mon, 16 Dec 2024 11:10:07 +0000 Subject: [PATCH 110/112] Delete .changesets/docs_fix_helm_yaml_config_link.md As per docs suggestion, remove this changeset --- .changesets/docs_fix_helm_yaml_config_link.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changesets/docs_fix_helm_yaml_config_link.md diff --git a/.changesets/docs_fix_helm_yaml_config_link.md b/.changesets/docs_fix_helm_yaml_config_link.md deleted file mode 100644 index e3be0481b6..0000000000 --- a/.changesets/docs_fix_helm_yaml_config_link.md +++ /dev/null @@ -1,5 +0,0 @@ -### docs: Updates the router YAML config reference URL ([PR #6277](https://github.com/apollographql/router/pull/6277)) - -Updates the router YAML config URL in the Helm chart values.yaml file to the new location - -By [@lambertjosh](https://github.com/lambertjosh) in https://github.com/apollographql/router/pull/6277 From d15894e3862da35a42bfea501df552f991d75ba7 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Mon, 16 Dec 2024 11:27:11 +0000 Subject: [PATCH 111/112] Update .changesets/breaking_drop_reuse_fragment.md --- .changesets/breaking_drop_reuse_fragment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changesets/breaking_drop_reuse_fragment.md b/.changesets/breaking_drop_reuse_fragment.md index 643f061b64..ccdb22efec 100644 --- a/.changesets/breaking_drop_reuse_fragment.md +++ b/.changesets/breaking_drop_reuse_fragment.md @@ -1,4 +1,4 @@ -### Drop experimental reuse fragment query optimization option ([PR #6353](https://github.com/apollographql/router/pull/6353)) +### Drop experimental reuse fragment query optimization option ([PR #6354](https://github.com/apollographql/router/pull/6354)) Drop support for the experimental reuse fragment query optimization. This implementation was not only very slow but also very buggy due to its complexity. From bd8ea141993c0d9712c15fa1cd7eeab0e82f89a0 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Tue, 17 Dec 2024 10:36:15 +0000 Subject: [PATCH 112/112] prep release: v1.59.0 --- .changesets/breaking_drop_reuse_fragment.md | 7 - ...glasser_pq_metric_attribute_consistency.md | 9 - ...onfig_simon_router_version_in_cache_key.md | 8 - ..._feat_fleet_detector_add_schema_metrics.md | 6 - .changesets/feat_glasser_pq_client_name.md | 16 -- .../feat_glasser_pq_safelist_override.md | 9 - .../feat_jr_add_fleet_awareness_plugin.md | 10 - .changesets/feat_native_query_planner_ga.md | 37 --- .../fix_address_dentist_buyer_frown.md | 16 -- .changesets/fix_bnjjj_fix_880.md | 55 ---- .changesets/fix_bnjjj_fix_retry_metric.md | 6 - .changesets/fix_fix_query_hashing.md | 8 - .changesets/fix_renee_quieres_queries.md | 6 - .../fix_tninesling_cost_name_handling.md | 22 -- .../maint_lrlna_remove_catch_unwind.md | 14 - ...int_renee_router_297_monotonic_counters.md | 13 - CHANGELOG.md | 260 ++++++++++++++++++ Cargo.lock | 8 +- apollo-federation/Cargo.toml | 2 +- apollo-router-benchmarks/Cargo.toml | 2 +- apollo-router-scaffold/Cargo.toml | 2 +- .../templates/base/Cargo.template.toml | 2 +- .../templates/base/xtask/Cargo.template.toml | 2 +- apollo-router/Cargo.toml | 4 +- .../tracing/docker-compose.datadog.yml | 2 +- dockerfiles/tracing/docker-compose.jaeger.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- helm/chart/router/Chart.yaml | 4 +- helm/chart/router/README.md | 8 +- licenses.html | 4 +- scripts/install.sh | 2 +- 31 files changed, 283 insertions(+), 265 deletions(-) delete mode 100644 .changesets/breaking_drop_reuse_fragment.md delete mode 100644 .changesets/breaking_glasser_pq_metric_attribute_consistency.md delete mode 100644 .changesets/config_simon_router_version_in_cache_key.md delete mode 100644 .changesets/feat_feat_fleet_detector_add_schema_metrics.md delete mode 100644 .changesets/feat_glasser_pq_client_name.md delete mode 100644 .changesets/feat_glasser_pq_safelist_override.md delete mode 100644 .changesets/feat_jr_add_fleet_awareness_plugin.md delete mode 100644 .changesets/feat_native_query_planner_ga.md delete mode 100644 .changesets/fix_address_dentist_buyer_frown.md delete mode 100644 .changesets/fix_bnjjj_fix_880.md delete mode 100644 .changesets/fix_bnjjj_fix_retry_metric.md delete mode 100644 .changesets/fix_fix_query_hashing.md delete mode 100644 .changesets/fix_renee_quieres_queries.md delete mode 100644 .changesets/fix_tninesling_cost_name_handling.md delete mode 100644 .changesets/maint_lrlna_remove_catch_unwind.md delete mode 100644 .changesets/maint_renee_router_297_monotonic_counters.md diff --git a/.changesets/breaking_drop_reuse_fragment.md b/.changesets/breaking_drop_reuse_fragment.md deleted file mode 100644 index ccdb22efec..0000000000 --- a/.changesets/breaking_drop_reuse_fragment.md +++ /dev/null @@ -1,7 +0,0 @@ -### Drop experimental reuse fragment query optimization option ([PR #6354](https://github.com/apollographql/router/pull/6354)) - -Drop support for the experimental reuse fragment query optimization. This implementation was not only very slow but also very buggy due to its complexity. - -Auto generation of fragments is a much simpler (and faster) algorithm that in most cases produces better results. Fragment auto generation is the default optimization since v1.58 release. - -By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6353 diff --git a/.changesets/breaking_glasser_pq_metric_attribute_consistency.md b/.changesets/breaking_glasser_pq_metric_attribute_consistency.md deleted file mode 100644 index 87c320e4ad..0000000000 --- a/.changesets/breaking_glasser_pq_metric_attribute_consistency.md +++ /dev/null @@ -1,9 +0,0 @@ -### More consistent attributes on `apollo.router.operations.persisted_queries` metric ([PR #6403](https://github.com/apollographql/router/pull/6403)) - -Version 1.28.1 added several *unstable* metrics, including `apollo.router.operations.persisted_queries`. - -When an operation is rejected, Router includes a `persisted_queries.safelist.rejected.unknown` attribute on the metric. Previously, this attribute had the value `true` if the operation is logged (via `log_unknown`), and `false` if the operation is not logged. (The attribute is not included at all if the operation is not rejected.) This appears to have been a mistake, as you can also tell whether it is logged via the `persisted_queries.logged` attribute. - -Router now only sets this attribute to true, and never to false. This may be a breaking change for your use of metrics; note that these metrics should be treated as unstable and may change in the future. - -By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 diff --git a/.changesets/config_simon_router_version_in_cache_key.md b/.changesets/config_simon_router_version_in_cache_key.md deleted file mode 100644 index e5028023b1..0000000000 --- a/.changesets/config_simon_router_version_in_cache_key.md +++ /dev/null @@ -1,8 +0,0 @@ -### Add version number to distributed query plan cache keys ([PR #6406](https://github.com/apollographql/router/pull/6406)) - -The router now includes its version number in the cache keys of distributed cache entries. Given that a new router release may change how query plans are generated or represented, including the router version in a cache key enables the router to use separate cache entries for different versions. - -If you have enabled [distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), expect additional processing for your cache to update for this router release. - - -By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6406 diff --git a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md deleted file mode 100644 index b9ab16aa5a..0000000000 --- a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md +++ /dev/null @@ -1,6 +0,0 @@ -### Add fleet awareness schema metric ([PR #6283](https://github.com/apollographql/router/pull/6283)) - - -The router now supports the `apollo.router.instance.schema` metric for its `fleet_detector` plugin. It has two attributes: `schema_hash` and `launch_id`. - -By [@loshz](https://github.com/loshz) and [@nmoutschen](https://github.com/nmoutschen) in https://github.com/apollographql/router/pull/6283 diff --git a/.changesets/feat_glasser_pq_client_name.md b/.changesets/feat_glasser_pq_client_name.md deleted file mode 100644 index a9da1d73af..0000000000 --- a/.changesets/feat_glasser_pq_client_name.md +++ /dev/null @@ -1,16 +0,0 @@ -### Support client name for persisted query lists ([PR #6198](https://github.com/apollographql/router/pull/6198)) - -The persisted query manifest fetched from Apollo Uplink can now contain a `clientName` field in each operation. Two operations with the same `id` but different `clientName` are considered to be distinct operations, and they may have distinct bodies. - -The router resolves the client name by taking the first from the following that exists: -- Reading the `apollo_persisted_queries::client_name` context key that may be set by a `router_service` plugin -- Reading the HTTP header named by `telemetry.apollo.client_name_header`, which defaults to `apollographql-client-name` - - -If a client name can be resolved for a request, the router first tries to find a persisted query with the specified ID and the resolved client name. - -If there is no operation with that ID and client name, or if a client name cannot be resolved, the router tries to find a persisted query with the specified ID and no client name specified. This means that existing PQ lists that don't contain client names will continue to work. - -To learn more, go to [persisted queries](https://www.apollographql.com/docs/graphos/routing/security/persisted-queries#apollo_persisted_queriesclient_name) docs. - -By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6198 \ No newline at end of file diff --git a/.changesets/feat_glasser_pq_safelist_override.md b/.changesets/feat_glasser_pq_safelist_override.md deleted file mode 100644 index f849df60a6..0000000000 --- a/.changesets/feat_glasser_pq_safelist_override.md +++ /dev/null @@ -1,9 +0,0 @@ -### Ability to skip persisted query list safelisting enforcement via plugin ([PR #6403](https://github.com/apollographql/router/pull/6403)) - -If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. - -> Note: this doesn't affect the logging of unknown operations by the `persisted_queries.log_unknown` option. - -In cases where an operation would have been denied but is allowed due to the context key existing, the attribute `persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value `true`. - -By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 \ No newline at end of file diff --git a/.changesets/feat_jr_add_fleet_awareness_plugin.md b/.changesets/feat_jr_add_fleet_awareness_plugin.md deleted file mode 100644 index 863e7e2166..0000000000 --- a/.changesets/feat_jr_add_fleet_awareness_plugin.md +++ /dev/null @@ -1,10 +0,0 @@ -### Add fleet awareness plugin ([PR #6151](https://github.com/apollographql/router/pull/6151)) - -A new `fleet_awareness` plugin has been added that reports telemetry to Apollo about the configuration and deployment of the router. - -The reported telemetry include CPU and memory usage, CPU frequency, and other deployment characteristics such as operating system and cloud provider. For more details, along with a full list of data captured and how to opt out, go to our -[data privacy policy](https://www.apollographql.com/docs/graphos/reference/data-privacy). - - -By [@jonathanrainer](https://github.com/jonathanrainer), [@nmoutschen](https://github.com/nmoutschen), [@loshz](https://github.com/loshz) -in https://github.com/apollographql/router/pull/6151 diff --git a/.changesets/feat_native_query_planner_ga.md b/.changesets/feat_native_query_planner_ga.md deleted file mode 100644 index 4f0c8469b9..0000000000 --- a/.changesets/feat_native_query_planner_ga.md +++ /dev/null @@ -1,37 +0,0 @@ -### General availability of native query planner - -The router's native, Rust-based, query planner is now [generally available](https://www.apollographql.com/docs/graphos/reference/feature-launch-stages#general-availability) and enabled by default. - -The native query planner achieves better performance for a variety of graphs. In our tests, we observe: - -* 10x median improvement in query planning time (observed via `apollo.router.query_planning.plan.duration`) -* 2.9x improvement in router’s CPU utilization -* 2.2x improvement in router’s memory usage - -> Note: you can expect generated plans and subgraph operations in the native -query planner to have slight differences when compared to the legacy, JavaScript-based query planner. We've ascertained these differences to be semantically insignificant, based on comparing ~2.5 million known unique user operations in GraphOS as well as -comparing ~630 million operations across actual router deployments in shadow -mode for a four month duration. - -The native query planner supports Federation v2 supergraphs. If you are using Federation v1 today, see our [migration guide](https://www.apollographql.com/docs/graphos/reference/migration/to-federation-version-2) on how to update your composition build step and subgraph changes are typically not needed. - -The legacy, JavaScript, query planner is deprecated in this release, but you can still switch -back to it if you are still using Federation v1 supergraph: - -``` -experimental_query_planner_mode: legacy -``` - -> Note: The subgraph operations generated by the query planner are not -guaranteed consistent release over release. We strongly recommend against -relying on the shape of planned subgraph operations, as new router features and -optimizations will continuously affect it. - -By [@sachindshinde](https://github.com/sachindshinde), -[@goto-bus-stop](https://github.com/goto-bus-stop), -[@duckki](https://github.com/duckki), -[@TylerBloom](https://github.com/TylerBloom), -[@SimonSapin](https://github.com/SimonSapin), -[@dariuszkuc](https://github.com/dariuszkuc), -[@lrlna](https://github.com/lrlna), [@clenfest](https://github.com/clenfest), -and [@o0Ignition0o](https://github.com/o0Ignition0o). \ No newline at end of file diff --git a/.changesets/fix_address_dentist_buyer_frown.md b/.changesets/fix_address_dentist_buyer_frown.md deleted file mode 100644 index 243f51fa0b..0000000000 --- a/.changesets/fix_address_dentist_buyer_frown.md +++ /dev/null @@ -1,16 +0,0 @@ -### Fix coprocessor empty body object panic ([PR #6398](https://github.com/apollographql/router/pull/6398)) - -Previously, the router would panic if a coprocessor responds with an empty body object at the supergraph stage: - -```json -{ - ... // other fields - "body": {} // empty object -} -``` - -This has been fixed in this release. - -> Note: the previous issue didn't affect coprocessors that responded with formed responses. - -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6398 diff --git a/.changesets/fix_bnjjj_fix_880.md b/.changesets/fix_bnjjj_fix_880.md deleted file mode 100644 index 939fbda854..0000000000 --- a/.changesets/fix_bnjjj_fix_880.md +++ /dev/null @@ -1,55 +0,0 @@ -### Fix telemetry instrumentation using supergraph query selector ([PR #6324](https://github.com/apollographql/router/pull/6324)) - -Previously, router telemetry instrumentation that used query selectors could log errors with messages such as `this is a bug and should not happen`. - -These errors have now been fixed, and configurations with query selectors such as the following work properly: - -```yaml title=router.yaml -telemetry: - exporters: - metrics: - common: - views: - # Define a custom view because operation limits are different than the default latency-oriented view of OpenTelemetry - - name: oplimits.* - aggregation: - histogram: - buckets: - - 0 - - 5 - - 10 - - 25 - - 50 - - 100 - - 500 - - 1000 - instrumentation: - instruments: - supergraph: - oplimits.aliases: - value: - query: aliases - type: histogram - unit: number - description: "Aliases for an operation" - oplimits.depth: - value: - query: depth - type: histogram - unit: number - description: "Depth for an operation" - oplimits.height: - value: - query: height - type: histogram - unit: number - description: "Height for an operation" - oplimits.root_fields: - value: - query: root_fields - type: histogram - unit: number - description: "Root fields for an operation" -``` - -By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6324 \ No newline at end of file diff --git a/.changesets/fix_bnjjj_fix_retry_metric.md b/.changesets/fix_bnjjj_fix_retry_metric.md deleted file mode 100644 index eaaa220e80..0000000000 --- a/.changesets/fix_bnjjj_fix_retry_metric.md +++ /dev/null @@ -1,6 +0,0 @@ -### Fix and test experimental_retry ([PR #6338](https://github.com/apollographql/router/pull/6338)) - -Fix the behavior of `experimental_retry` and make sure both the feature and metrics are working. -An entry in the context was also added, which would be useful later to implement a new standard attribute and selector for advanced telemetry. - -By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6338 \ No newline at end of file diff --git a/.changesets/fix_fix_query_hashing.md b/.changesets/fix_fix_query_hashing.md deleted file mode 100644 index e371bd476e..0000000000 --- a/.changesets/fix_fix_query_hashing.md +++ /dev/null @@ -1,8 +0,0 @@ -### Fix query hashing algorithm ([PR #6205](https://github.com/apollographql/router/pull/6205)) - -> [!IMPORTANT] -> If you have enabled [distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), updates to the query planner in this release will result in query plan caches being regenerated rather than reused. On account of this, you should anticipate additional cache regeneration cost when updating to this router version while the new query plans come into service. - -The router includes a schema-aware query hashing algorithm designed to return the same hash across schema updates if the query remains unaffected. This update enhances the algorithm by addressing various corner cases to improve its reliability and consistency. - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6205 diff --git a/.changesets/fix_renee_quieres_queries.md b/.changesets/fix_renee_quieres_queries.md deleted file mode 100644 index 76152d2cc8..0000000000 --- a/.changesets/fix_renee_quieres_queries.md +++ /dev/null @@ -1,6 +0,0 @@ -### Fix typo in persisted query metric attribute ([PR #6332](https://github.com/apollographql/router/pull/6332)) - -The `apollo.router.operations.persisted_queries` metric reports an attribute when a persisted query was not found. -Previously, the attribute name was `persisted_quieries.not_found`, with one `i` too many. Now it's `persisted_queries.not_found`. - -By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6332 \ No newline at end of file diff --git a/.changesets/fix_tninesling_cost_name_handling.md b/.changesets/fix_tninesling_cost_name_handling.md deleted file mode 100644 index fe911c43ed..0000000000 --- a/.changesets/fix_tninesling_cost_name_handling.md +++ /dev/null @@ -1,22 +0,0 @@ -### Ensure cost directives are picked up when not explicitly imported ([PR #6328](https://github.com/apollographql/router/pull/6328)) - -With the recent composition changes, importing `@cost` results in a supergraph schema with the cost specification import at the top. The `@cost` directive itself is not explicitly imported, as it's expected to be available as the default export from the cost link. In contrast, uses of `@listSize` to translate to an explicit import in the supergraph. - -Old SDL link - -``` -@link( - url: "https://specs.apollo.dev/cost/v0.1" - import: ["@cost", "@listSize"] -) -``` - -New SDL link - -``` -@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) -``` - -Instead of using the directive names from the import list in the link, the directive names now come from `SpecDefinition::directive_name_in_schema`, which is equivalent to the change we made on the composition side. - -By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6328 diff --git a/.changesets/maint_lrlna_remove_catch_unwind.md b/.changesets/maint_lrlna_remove_catch_unwind.md deleted file mode 100644 index 8390cbe278..0000000000 --- a/.changesets/maint_lrlna_remove_catch_unwind.md +++ /dev/null @@ -1,14 +0,0 @@ - -### Remove catch_unwind wrapper around the native query planner ([PR #6397](https://github.com/apollographql/router/pull/6397)) - -As part of internal maintenance of the query planner, the -`catch_unwind` wrapper around the native query planner has been removed. This wrapper served as an extra safeguard for potential panics the native planner could produce. The -native query planner however no longer has any code paths that could panic. We have also -not witnessed a panic in the last four months, having processed 560 million real -user operations through the native planner. - -This maintenance work also removes backtrace capture for federation errors, which -was used for debugging and is no longer necessary as we have the confidence in -the native planner's implementation. - -By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6397 diff --git a/.changesets/maint_renee_router_297_monotonic_counters.md b/.changesets/maint_renee_router_297_monotonic_counters.md deleted file mode 100644 index f90446e3f4..0000000000 --- a/.changesets/maint_renee_router_297_monotonic_counters.md +++ /dev/null @@ -1,13 +0,0 @@ -### Deprecate various metrics ([PR #6350](https://github.com/apollographql/router/pull/6350)) - -Several metrics have been deprecated in this release, in favor of OpenTelemetry-compatible alternatives: - -- `apollo_router_deduplicated_subscriptions_total` - use the `apollo.router.operations.subscriptions` metric's `subscriptions.deduplicated` attribute. -- `apollo_authentication_failure_count` - use the `apollo.router.operations.authentication.jwt` metric's `authentication.jwt.failed` attribute. -- `apollo_authentication_success_count` - use the `apollo.router.operations.authentication.jwt` metric instead. If the `authentication.jwt.failed` attribute is *absent* or `false`, the authentication succeeded. -- `apollo_require_authentication_failure_count` - use the `http.server.request.duration` metric's `http.response.status_code` attribute. Requests with authentication failures have HTTP status code 401. -- `apollo_router_timeout` - this metric conflates timed-out requests from client to the router, and requests from the router to subgraphs. Timed-out requests have HTTP status code 504. Use the `http.response.status_code` attribute on the `http.server.request.duration` metric to identify timed-out router requests, and the same attribute on the `http.client.request.duration` metric to identify timed-out subgraph requests. - -The deprecated metrics will continue to work in the 1.x release line. - -By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6350 diff --git a/CHANGELOG.md b/CHANGELOG.md index cbab02293c..39162412b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,266 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.59.0] - 2024-12-17 + +> [!IMPORTANT] +> If you have enabled [distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), updates to the query planner in this release will result in query plan caches being regenerated rather than reused. On account of this, you should anticipate additional cache regeneration cost when updating to this router version while the new query plans come into service. + +## 🚀 Features + +### General availability of native query planner + +The router's native, Rust-based, query planner is now [generally available](https://www.apollographql.com/docs/graphos/reference/feature-launch-stages#general-availability) and enabled by default. + +The native query planner achieves better performance for a variety of graphs. In our tests, we observe: + +* 10x median improvement in query planning time (observed via `apollo.router.query_planning.plan.duration`) +* 2.9x improvement in router’s CPU utilization +* 2.2x improvement in router’s memory usage + +> Note: you can expect generated plans and subgraph operations in the native +query planner to have slight differences when compared to the legacy, JavaScript-based query planner. We've ascertained these differences to be semantically insignificant, based on comparing ~2.5 million known unique user operations in GraphOS as well as +comparing ~630 million operations across actual router deployments in shadow +mode for a four month duration. + +The native query planner supports Federation v2 supergraphs. If you are using Federation v1 today, see our [migration guide](https://www.apollographql.com/docs/graphos/reference/migration/to-federation-version-2) on how to update your composition build step. Subgraph changes are typically not needed. + +The legacy, JavaScript, query planner is deprecated in this release, but you can still switch +back to it if you are still using Federation v1 supergraph: + +``` +experimental_query_planner_mode: legacy +``` + +> Note: The subgraph operations generated by the query planner are not +guaranteed consistent release over release. We strongly recommend against +relying on the shape of planned subgraph operations, as new router features and +optimizations will continuously affect it. + +By [@sachindshinde](https://github.com/sachindshinde), +[@goto-bus-stop](https://github.com/goto-bus-stop), +[@duckki](https://github.com/duckki), +[@TylerBloom](https://github.com/TylerBloom), +[@SimonSapin](https://github.com/SimonSapin), +[@dariuszkuc](https://github.com/dariuszkuc), +[@lrlna](https://github.com/lrlna), [@clenfest](https://github.com/clenfest), +and [@o0Ignition0o](https://github.com/o0Ignition0o). + +### Ability to skip persisted query list safelisting enforcement via plugin ([PR #6403](https://github.com/apollographql/router/pull/6403)) + +If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. + +> Note: this doesn't affect the logging of unknown operations by the `persisted_queries.log_unknown` option. + +In cases where an operation would have been denied but is allowed due to the context key existing, the attribute `persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value `true`. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 + +### Add fleet awareness plugin ([PR #6151](https://github.com/apollographql/router/pull/6151)) + +A new `fleet_awareness` plugin has been added that reports telemetry to Apollo about the configuration and deployment of the router. + +The reported telemetry include CPU and memory usage, CPU frequency, and other deployment characteristics such as operating system and cloud provider. For more details, along with a full list of data captured and how to opt out, go to our +[data privacy policy](https://www.apollographql.com/docs/graphos/reference/data-privacy). + +By [@jonathanrainer](https://github.com/jonathanrainer), [@nmoutschen](https://github.com/nmoutschen), [@loshz](https://github.com/loshz) +in https://github.com/apollographql/router/pull/6151 + +### Add fleet awareness schema metric ([PR #6283](https://github.com/apollographql/router/pull/6283)) + +The router now supports the `apollo.router.instance.schema` metric for its `fleet_detector` plugin. It has two attributes: `schema_hash` and `launch_id`. + +By [@loshz](https://github.com/loshz) and [@nmoutschen](https://github.com/nmoutschen) in https://github.com/apollographql/router/pull/6283 + +### Support client name for persisted query lists ([PR #6198](https://github.com/apollographql/router/pull/6198)) + +The persisted query manifest fetched from Apollo Uplink can now contain a `clientName` field in each operation. Two operations with the same `id` but different `clientName` are considered to be distinct operations, and they may have distinct bodies. + +The router resolves the client name by taking the first from the following that exists: +- Reading the `apollo_persisted_queries::client_name` context key that may be set by a `router_service` plugin +- Reading the HTTP header named by `telemetry.apollo.client_name_header`, which defaults to `apollographql-client-name` + + +If a client name can be resolved for a request, the router first tries to find a persisted query with the specified ID and the resolved client name. + +If there is no operation with that ID and client name, or if a client name cannot be resolved, the router tries to find a persisted query with the specified ID and no client name specified. This means that existing PQ lists that don't contain client names will continue to work. + +To learn more, go to [persisted queries](https://www.apollographql.com/docs/graphos/routing/security/persisted-queries#apollo_persisted_queriesclient_name) docs. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6198 + +## 🐛 Fixes + +### Fix coprocessor empty body object panic ([PR #6398](https://github.com/apollographql/router/pull/6398)) + +Previously, the router would panic if a coprocessor responds with an empty body object at the supergraph stage: + +```json +{ + ... // other fields + "body": {} // empty object +} +``` + +This has been fixed in this release. + +> Note: the previous issue didn't affect coprocessors that responded with formed responses. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6398 + +### Ensure cost directives are picked up when not explicitly imported ([PR #6328](https://github.com/apollographql/router/pull/6328)) + +With the recent composition changes, importing `@cost` results in a supergraph schema with the cost specification import at the top. The `@cost` directive itself is not explicitly imported, as it's expected to be available as the default export from the cost link. In contrast, uses of `@listSize` to translate to an explicit import in the supergraph. + +Old SDL link + +``` +@link( + url: "https://specs.apollo.dev/cost/v0.1" + import: ["@cost", "@listSize"] +) +``` + +New SDL link + +``` +@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) +``` + +Instead of using the directive names from the import list in the link, the directive names now come from `SpecDefinition::directive_name_in_schema`, which is equivalent to the change we made on the composition side. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6328 + +### Fix query hashing algorithm ([PR #6205](https://github.com/apollographql/router/pull/6205)) + +The router includes a schema-aware query hashing algorithm designed to return the same hash across schema updates if the query remains unaffected. This update enhances the algorithm by addressing various corner cases to improve its reliability and consistency. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6205 + +### Fix typo in persisted query metric attribute ([PR #6332](https://github.com/apollographql/router/pull/6332)) + +The `apollo.router.operations.persisted_queries` metric reports an attribute when a persisted query was not found. +Previously, the attribute name was `persisted_quieries.not_found`, with one `i` too many. Now it's `persisted_queries.not_found`. + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6332 + +### Fix telemetry instrumentation using supergraph query selector ([PR #6324](https://github.com/apollographql/router/pull/6324)) + +Previously, router telemetry instrumentation that used query selectors could log errors with messages such as `this is a bug and should not happen`. + +These errors have now been fixed, and configurations with query selectors such as the following work properly: + +```yaml title=router.yaml +telemetry: + exporters: + metrics: + common: + views: + # Define a custom view because operation limits are different than the default latency-oriented view of OpenTelemetry + - name: oplimits.* + aggregation: + histogram: + buckets: + - 0 + - 5 + - 10 + - 25 + - 50 + - 100 + - 500 + - 1000 + instrumentation: + instruments: + supergraph: + oplimits.aliases: + value: + query: aliases + type: histogram + unit: number + description: "Aliases for an operation" + oplimits.depth: + value: + query: depth + type: histogram + unit: number + description: "Depth for an operation" + oplimits.height: + value: + query: height + type: histogram + unit: number + description: "Height for an operation" + oplimits.root_fields: + value: + query: root_fields + type: histogram + unit: number + description: "Root fields for an operation" +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6324 + +### More consistent attributes on `apollo.router.operations.persisted_queries` metric ([PR #6403](https://github.com/apollographql/router/pull/6403)) + +Version 1.28.1 added several *unstable* metrics, including `apollo.router.operations.persisted_queries`. + +When an operation is rejected, Router includes a `persisted_queries.safelist.rejected.unknown` attribute on the metric. Previously, this attribute had the value `true` if the operation is logged (via `log_unknown`), and `false` if the operation is not logged. (The attribute is not included at all if the operation is not rejected.) This appears to have been a mistake, as you can also tell whether it is logged via the `persisted_queries.logged` attribute. + +Router now only sets this attribute to true, and never to false. Note these metrics are unstable and will continue to change. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 + +### Drop experimental reuse fragment query optimization option ([PR #6354](https://github.com/apollographql/router/pull/6354)) + +Drop support for the experimental reuse fragment query optimization. This implementation was not only very slow but also very buggy due to its complexity. + +Auto generation of fragments is a much simpler (and faster) algorithm that in most cases produces better results. Fragment auto generation is the default optimization since v1.58 release. + +By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6353 + +## 📃 Configuration + +### Add version number to distributed query plan cache keys ([PR #6406](https://github.com/apollographql/router/pull/6406)) + +The router now includes its version number in the cache keys of distributed cache entries. Given that a new router release may change how query plans are generated or represented, including the router version in a cache key enables the router to use separate cache entries for different versions. + +If you have enabled [distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), expect additional processing for your cache to update for this router release. + + +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6406 + +## 🛠 Maintenance + +### Remove catch_unwind wrapper around the native query planner ([PR #6397](https://github.com/apollographql/router/pull/6397)) + +As part of internal maintenance of the query planner, the +`catch_unwind` wrapper around the native query planner has been removed. This wrapper served as an extra safeguard for potential panics the native planner could produce. The +native query planner however no longer has any code paths that could panic. We have also +not witnessed a panic in the last four months, having processed 560 million real +user operations through the native planner. + +This maintenance work also removes backtrace capture for federation errors, which +was used for debugging and is no longer necessary as we have the confidence in +the native planner's implementation. + +By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6397 + +### Deprecate various metrics ([PR #6350](https://github.com/apollographql/router/pull/6350)) + +Several metrics have been deprecated in this release, in favor of OpenTelemetry-compatible alternatives: + +- `apollo_router_deduplicated_subscriptions_total` - use the `apollo.router.operations.subscriptions` metric's `subscriptions.deduplicated` attribute. +- `apollo_authentication_failure_count` - use the `apollo.router.operations.authentication.jwt` metric's `authentication.jwt.failed` attribute. +- `apollo_authentication_success_count` - use the `apollo.router.operations.authentication.jwt` metric instead. If the `authentication.jwt.failed` attribute is *absent* or `false`, the authentication succeeded. +- `apollo_require_authentication_failure_count` - use the `http.server.request.duration` metric's `http.response.status_code` attribute. Requests with authentication failures have HTTP status code 401. +- `apollo_router_timeout` - this metric conflates timed-out requests from client to the router, and requests from the router to subgraphs. Timed-out requests have HTTP status code 504. Use the `http.response.status_code` attribute on the `http.server.request.duration` metric to identify timed-out router requests, and the same attribute on the `http.client.request.duration` metric to identify timed-out subgraph requests. + +The deprecated metrics will continue to work in the 1.x release line. + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6350 + + + # [1.58.1] - 2024-12-05 > [!IMPORTANT] diff --git a/Cargo.lock b/Cargo.lock index 71b00af853..2e367d7c41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,7 +204,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.59.0-rc.0" +version = "1.59.0" dependencies = [ "apollo-compiler", "derive_more", @@ -257,7 +257,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.59.0-rc.0" +version = "1.59.0" dependencies = [ "access-json", "ahash", @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.59.0-rc.0" +version = "1.59.0" dependencies = [ "apollo-parser", "apollo-router", @@ -443,7 +443,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.59.0-rc.0" +version = "1.59.0" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 34d5b0e999..9469277646 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.59.0-rc.0" +version = "1.59.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 2211c639fe..53757506a1 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.59.0-rc.0" +version = "1.59.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 899776abc9..3ce55f07d0 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.59.0-rc.0" +version = "1.59.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 15fdabbbf0..7e188815b2 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.59.0-rc.0" +apollo-router = "1.59.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index bc31af94d8..59e99a9f2e 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.59.0-rc.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.59.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 5464546fdd..061e24593e 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.59.0-rc.0" +version = "1.59.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -66,7 +66,7 @@ features = ["docs_rs"] access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.59.0-rc.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.59.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index a94b7ddd79..4eec369eb4 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.59.0-rc.0 + image: ghcr.io/apollographql/router:v1.59.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 1a0109fbba..72a2857397 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.59.0-rc.0 + image: ghcr.io/apollographql/router:v1.59.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 945b147b53..0ebdfb9ce7 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.59.0-rc.0 + image: ghcr.io/apollographql/router:v1.59.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index fe7cdc4f77..95cf5805eb 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.59.0-rc.0 +version: 1.59.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.59.0-rc.0" +appVersion: "v1.59.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index a7f5cfb9d6..a9a5af6b71 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.59.0-rc.0](https://img.shields.io/badge/Version-1.59.0--rc.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.59.0-rc.0](https://img.shields.io/badge/AppVersion-v1.59.0--rc.0-informational?style=flat-square) +![Version: 1.59.0](https://img.shields.io/badge/Version-1.59.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.59.0](https://img.shields.io/badge/AppVersion-v1.59.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0-rc.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0-rc.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0-rc.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.59.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ @@ -98,4 +98,4 @@ helm show values oci://ghcr.io/apollographql/helm-charts/router | virtualservice.enabled | bool | `false` | | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) +Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) diff --git a/licenses.html b/licenses.html index 67a2901d9a..3b497f5e45 100644 --- a/licenses.html +++ b/licenses.html @@ -6122,6 +6122,7 @@

    Used by:

  • erased-serde
  • ghost
  • itoa
  • +
  • libc
  • linkme
  • paste
  • prettyplease
  • @@ -6138,6 +6139,7 @@

    Used by:

  • serde_path_to_error
  • serde_qs
  • serde_urlencoded
  • +
  • syn
  • thiserror
  • thiserror-impl
  • unicode-ident
  • @@ -12603,7 +12605,6 @@

    Used by:

  • graphql_query_derive
  • http-serde
  • ident_case
  • -
  • libc
  • libssh2-sys
  • linkme-impl
  • md5
  • @@ -12611,7 +12612,6 @@

    Used by:

  • prost
  • rhai_codegen
  • siphasher
  • -
  • syn
  • system-configuration
  • system-configuration-sys
  • thrift
  • diff --git a/scripts/install.sh b/scripts/install.sh index a987f38bfa..4403b9866e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.59.0-rc.0" +PACKAGE_VERSION="v1.59.0" download_binary() { downloader --check

    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 J6{Tz*{9kPNZ><0T literal 0 HcmV?d00001 diff --git a/docs/source/reference/router/telemetry/metrics-exporters/datadog.mdx b/docs/source/reference/router/telemetry/metrics-exporters/datadog.mdx index 4aa7850a76..4526ab38d4 100644 --- a/docs/source/reference/router/telemetry/metrics-exporters/datadog.mdx +++ b/docs/source/reference/router/telemetry/metrics-exporters/datadog.mdx @@ -8,27 +8,13 @@ Enable and configure the [OTLP exporter](/router/configuration/telemetry/exporte For general tracing configuration, refer to [Router Metrics Configuration](/router/configuration/telemetry/exporters/metrics/overview). -## Datadog configuration +## Configuration -To export metrics to Datadog, you must both: - -- Configure the Datadog agent to accept OpenTelemetry Protocol (OTLP) metrics, and -- Configure the router to send traces to the Datadog agent. - -### Datadog agent configuration - -To configure the Datadog agent, add OTLP configuration (`otlp_config`) to your `datadog.yaml`. For example: - -```yaml title="datadog.yaml" -otlp_config: - receiver: - protocols: - grpc: - endpoint: :4317 -``` +To export metrics to Datadog, you must configure both the router to send traces to the Datadog agent and the Datadog agent to accept OpenTelemetry Protocol (OTLP) metrics. ### Router configuration -To configure the router, enable the [OTLP exporter](/router/configuration/telemetry/exporters/metrics/otlp#configuration) and set both `temporality: delta` and `endpoint: `. For example: + +You should enable the [OTLP exporter](/router/configuration/telemetry/exporters/metrics/otlp#configuration) and set both `temporality: delta` and `endpoint: `. For example: ```yaml title="router.yaml" telemetry: @@ -44,9 +30,20 @@ telemetry: -**You must set `temporality: delta`**, otherwise the router generates incorrect metrics. +You must set `temporality: delta`, otherwise the router generates incorrect metrics. -For more details about Datadog configuration, see [Datadog's docs on Agent configuration](https://docs.datadoghq.com/opentelemetry/otlp_ingest_in_the_agent/?tab=host). +### Datadog agent configuration + +To configure the Datadog agent, add OTLP configuration (`otlp_config`) to your `datadog.yaml`. For example: +```yaml title="datadog.yaml" +otlp_config: + receiver: + protocols: + grpc: + endpoint: :4317 +``` + +For more details about Datadog configuration, see [Datadog's docs on Agent configuration](https://docs.datadoghq.com/opentelemetry/otlp_ingest_in_the_agent/?tab=host). diff --git a/docs/source/reference/router/telemetry/trace-exporters/datadog.mdx b/docs/source/reference/router/telemetry/trace-exporters/datadog.mdx index e1b105d338..23cd378332 100644 --- a/docs/source/reference/router/telemetry/trace-exporters/datadog.mdx +++ b/docs/source/reference/router/telemetry/trace-exporters/datadog.mdx @@ -10,6 +10,52 @@ Enable and configure the [Datadog](https://www.datadoghq.com/) exporter for trac For general tracing configuration, refer to [Router Tracing Configuration](/router/configuration/telemetry/exporters/tracing/overview). +## Attributes for Datadog APM UI + +The router should set attributes that Datadog uses to organize its APM view and other UI: + +- `otel.name`: span name that's fixed for Datadog +- `resource.name`: Datadog resource name that's displayed in traces +- `operation.name`: Datadog operation name that populates a dropdown menu in the Datadog service page + +You should add these attributes to your `router.yaml` configuration file. The example below sets these attributes for the `router`, `supergraph`, and `subgraph` stages of the router's request lifecycle: + +```yaml title="router.yaml" +telemetry: + instrumentation: + spans: + mode: spec_compliant + router: + attributes: + otel.name: router + operation.name: "router" + resource.name: + request_method: true + + supergraph: + attributes: + otel.name: supergraph + operation.name: "supergraph" + resource.name: + operation_name: string + + subgraph: + attributes: + otel.name: subgraph + operation.name: "subgraph" + resource.name: + subgraph_operation_name: string +``` + +Consequently you can filter for these operations in Datadog APM: + +Datadog APM showing operations set with example attributes set in router.yaml + ## OTLP configuration To export traces to Datadog via OTLP, you must do the following: From 6037663c77e9e1a802489d8877be0a15c119b4fa Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Mon, 25 Nov 2024 10:57:40 +0100 Subject: [PATCH 022/112] fix(telemetry): fix supergraph query selector (#6324) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- .changesets/fix_bnjjj_fix_880.md | 53 +++++++++++++++++++ .../telemetry/config_new/instruments.rs | 4 +- .../plugins/telemetry/config_new/selectors.rs | 25 --------- .../telemetry/fixtures/graphql.router.yaml | 30 +++++++++-- .../tests/integration/telemetry/metrics.rs | 21 ++++++++ 5 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 .changesets/fix_bnjjj_fix_880.md diff --git a/.changesets/fix_bnjjj_fix_880.md b/.changesets/fix_bnjjj_fix_880.md new file mode 100644 index 0000000000..3c9f0edc78 --- /dev/null +++ b/.changesets/fix_bnjjj_fix_880.md @@ -0,0 +1,53 @@ +### Fix telemetry instrumentation using supergraph query selector ([PR #6324](https://github.com/apollographql/router/pull/6324)) + +Query selector was raising error logs like `this is a bug and should not happen`. It's now fixed. +Now this configuration will work properly: +```yaml title=router.yaml +telemetry: + exporters: + metrics: + common: + views: + # Define a custom view because operation limits are different than the default latency-oriented view of OpenTelemetry + - name: oplimits.* + aggregation: + histogram: + buckets: + - 0 + - 5 + - 10 + - 25 + - 50 + - 100 + - 500 + - 1000 + instrumentation: + instruments: + supergraph: + oplimits.aliases: + value: + query: aliases + type: histogram + unit: number + description: "Aliases for an operation" + oplimits.depth: + value: + query: depth + type: histogram + unit: number + description: "Depth for an operation" + oplimits.height: + value: + query: height + type: histogram + unit: number + description: "Height for an operation" + oplimits.root_fields: + value: + query: root_fields + type: histogram + unit: number + description: "Root fields for an operation" +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6324 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/instruments.rs b/apollo-router/src/plugins/telemetry/config_new/instruments.rs index 341f84ad35..88760f1f1c 100644 --- a/apollo-router/src/plugins/telemetry/config_new/instruments.rs +++ b/apollo-router/src/plugins/telemetry/config_new/instruments.rs @@ -1446,7 +1446,7 @@ where }) } None => { - ::tracing::error!("cannot convert static instrument into a counter, this is an error; please fill an issue on GitHub"); + failfast_debug!("cannot convert static instrument into a counter, this is an error; please fill an issue on GitHub"); } } } @@ -1501,7 +1501,7 @@ where }); } None => { - ::tracing::error!("cannot convert static instrument into a histogram, this is an error; please fill an issue on GitHub"); + failfast_debug!("cannot convert static instrument into a histogram, this is an error; please fill an issue on GitHub"); } } } diff --git a/apollo-router/src/plugins/telemetry/config_new/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/selectors.rs index 1b49d9548c..3324047b2f 100644 --- a/apollo-router/src/plugins/telemetry/config_new/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/selectors.rs @@ -1069,25 +1069,6 @@ impl Selector for SupergraphSelector { ctx: &Context, ) -> Option { match self { - SupergraphSelector::Query { query, .. } => { - let limits_opt = ctx - .extensions() - .with_lock(|lock| lock.get::>().cloned()); - match query { - Query::Aliases => { - limits_opt.map(|limits| opentelemetry::Value::I64(limits.aliases as i64)) - } - Query::Depth => { - limits_opt.map(|limits| opentelemetry::Value::I64(limits.depth as i64)) - } - Query::Height => { - limits_opt.map(|limits| opentelemetry::Value::I64(limits.height as i64)) - } - Query::RootFields => limits_opt - .map(|limits| opentelemetry::Value::I64(limits.root_fields as i64)), - Query::String => None, - } - } SupergraphSelector::ResponseData { response_data, default, @@ -3289,12 +3270,6 @@ mod test { .unwrap(), 4.into() ); - assert_eq!( - selector - .on_response_event(&crate::graphql::Response::builder().build(), &context) - .unwrap(), - 4.into() - ); } #[test] diff --git a/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml index 24002e9be0..e1ff03b1c7 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml @@ -9,6 +9,31 @@ telemetry: instrumentation: instruments: + supergraph: + oplimits.aliases: + value: + query: aliases + type: histogram + unit: number + description: "Aliases for an operation" + oplimits.depth: + value: + query: depth + type: histogram + unit: number + description: "Depth for an operation" + oplimits.height: + value: + query: height + type: histogram + unit: number + description: "Height for an operation" + oplimits.root_fields: + value: + query: root_fields + type: histogram + unit: number + description: "Root fields for an operation" graphql: field.execution: true list.length: true @@ -38,7 +63,4 @@ telemetry: condition: eq: - field_name: string - - "topProducts" - - - + - "topProducts" \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index 1ae93f2d4f..cbce509c13 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -201,6 +201,27 @@ async fn test_graphql_metrics() { router.start().await; router.assert_started().await; router.execute_default_query().await; + router + .assert_log_not_contains("this is a bug and should not happen") + .await; + router + .assert_metrics_contains( + r#"oplimits_aliases_sum{otel_scope_name="apollo/router"} 0"#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"oplimits_root_fields_sum{otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"oplimits_depth_sum{otel_scope_name="apollo/router"} 2"#, + None, + ) + .await; router .assert_metrics_contains(r#"graphql_field_list_length_sum{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router"} 3"#, None) .await; From 00640a86da7c188dd1e34490ebb3b9f35d8a0c46 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Mon, 25 Nov 2024 11:44:36 +0000 Subject: [PATCH 023/112] plugin: add launch_id --- apollo-router/src/plugin/mod.rs | 10 ++++++++++ apollo-router/src/plugins/fleet_detector.rs | 7 ++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index bfdaa631e3..57de7f909e 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -77,6 +77,9 @@ pub struct PluginInit { /// The parsed subgraph schemas from the query planner, keyed by subgraph name pub(crate) subgraph_schemas: Arc, + /// Launch ID + pub(crate) launch_id: Option>, + pub(crate) notify: Notify, } @@ -173,6 +176,7 @@ where supergraph_schema_id: Arc, supergraph_schema: Arc>, subgraph_schemas: Option>, + launch_id: Option>, notify: Notify, ) -> Self { PluginInit { @@ -181,6 +185,7 @@ where supergraph_schema_id, supergraph_schema, subgraph_schemas: subgraph_schemas.unwrap_or_default(), + launch_id, notify, } } @@ -196,6 +201,7 @@ where supergraph_schema_id: Arc, supergraph_schema: Arc>, subgraph_schemas: Option>, + launch_id: Option>, notify: Notify, ) -> Result { let config: T = serde_json::from_value(config)?; @@ -205,6 +211,7 @@ where supergraph_schema, supergraph_schema_id, subgraph_schemas: subgraph_schemas.unwrap_or_default(), + launch_id, notify, }) } @@ -217,6 +224,7 @@ where supergraph_schema_id: Option>, supergraph_schema: Option>>, subgraph_schemas: Option>, + launch_id: Option>, notify: Option>, ) -> Self { PluginInit { @@ -226,6 +234,7 @@ where supergraph_schema: supergraph_schema .unwrap_or_else(|| Arc::new(Valid::assume_valid(Schema::new()))), subgraph_schemas: subgraph_schemas.unwrap_or_default(), + launch_id, notify: notify.unwrap_or_else(Notify::for_tests), } } @@ -243,6 +252,7 @@ impl PluginInit { .supergraph_schema_id(self.supergraph_schema_id) .supergraph_sdl(self.supergraph_sdl) .subgraph_schemas(self.subgraph_schemas) + .launch_id(self.launch_id) .notify(self.notify.clone()) .build() } diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 8304e146dc..0d890fc75a 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -1,4 +1,3 @@ -use core::slice::SlicePattern; use std::env; use std::env::consts::ARCH; use std::sync::Arc; @@ -149,10 +148,8 @@ impl GaugeStore { #[derive(Default)] struct GaugeOptions { - launch_id: Option, - - // Router Supergraph Schema Hash (SHA256 of the SDL) supergraph_schema_hash: String, + launch_id: Option, } #[derive(Default)] @@ -175,8 +172,8 @@ impl PluginPrivate for FleetDetector { } let gauge_options = GaugeOptions { - launch_id: None, supergraph_schema_hash: plugin.supergraph_schema_id.to_string(), + launch_id: plugin.launch_id, }; Ok(FleetDetector { From 9842d3263d53d77966eb256d1aa81e31ff896a85 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Mon, 25 Nov 2024 12:21:22 +0000 Subject: [PATCH 024/112] fix type cast --- apollo-router/src/plugin/mod.rs | 3 ++- apollo-router/src/plugins/fleet_detector.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index 57de7f909e..15648ce4fd 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -140,6 +140,7 @@ where .supergraph_schema_id(crate::spec::Schema::schema_id(&supergraph_sdl).into()) .supergraph_sdl(supergraph_sdl) .supergraph_schema(supergraph_schema) + .launch_id(Some("launch_id".to_string())) .notify(Notify::for_tests()) .build() } @@ -252,7 +253,7 @@ impl PluginInit { .supergraph_schema_id(self.supergraph_schema_id) .supergraph_sdl(self.supergraph_sdl) .subgraph_schemas(self.subgraph_schemas) - .launch_id(self.launch_id) + // .launch_id(self.launch_id) .notify(self.notify.clone()) .build() } diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 0d890fc75a..e750dedd0d 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -173,7 +173,7 @@ impl PluginPrivate for FleetDetector { let gauge_options = GaugeOptions { supergraph_schema_hash: plugin.supergraph_schema_id.to_string(), - launch_id: plugin.launch_id, + launch_id: plugin.launch_id.and_then(|id| Some(id)), }; Ok(FleetDetector { From 1d469a166e97d5c5586a41274ad97272952a5075 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Mon, 25 Nov 2024 13:50:03 +0000 Subject: [PATCH 025/112] fix plugin types --- apollo-router/src/plugin/mod.rs | 2 +- apollo-router/src/plugins/fleet_detector.rs | 2 +- apollo-router/src/router_factory.rs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index 15648ce4fd..f97d7df020 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -140,7 +140,7 @@ where .supergraph_schema_id(crate::spec::Schema::schema_id(&supergraph_sdl).into()) .supergraph_sdl(supergraph_sdl) .supergraph_schema(supergraph_schema) - .launch_id(Some("launch_id".to_string())) + .launch_id(Arc::new("launch_id".to_string())) .notify(Notify::for_tests()) .build() } diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index e750dedd0d..b3b5ac415d 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -173,7 +173,7 @@ impl PluginPrivate for FleetDetector { let gauge_options = GaugeOptions { supergraph_schema_hash: plugin.supergraph_schema_id.to_string(), - launch_id: plugin.launch_id.and_then(|id| Some(id)), + launch_id: plugin.launch_id.and_then(|id| Some(id.to_string())), }; Ok(FleetDetector { diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index b3a4bb27b6..d9e51f6099 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -516,6 +516,7 @@ pub(crate) async fn add_plugin( schema_id: Arc, supergraph_schema: Arc>, subgraph_schemas: Arc>>>, + launch_id: Option>, notify: &crate::notification::Notify, plugin_instances: &mut Plugins, errors: &mut Vec, @@ -528,6 +529,7 @@ pub(crate) async fn add_plugin( .supergraph_schema_id(schema_id) .supergraph_schema(supergraph_schema) .subgraph_schemas(subgraph_schemas) + .launch_id(launch_id) .notify(notify.clone()) .build(), ) From e73e94f3af53b393b78d62c8154fbc8ca69fe7c7 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 25 Nov 2024 15:32:11 +0100 Subject: [PATCH 026/112] Allow non-trivial root __typename-only queries when introspection is disabled (#6282) --- apollo-router/src/introspection.rs | 12 +++++++++--- apollo-router/tests/integration/typename.rs | 12 +++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index b69b03c68b..20098af96b 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -55,10 +55,16 @@ impl IntrospectionCache { ) -> ControlFlow { Self::maybe_lone_root_typename(schema, doc)?; if doc.operation.is_query() { - if doc.has_explicit_root_fields && doc.has_schema_introspection { - ControlFlow::Break(Self::mixed_fields_error())?; + if doc.has_schema_introspection { + if doc.has_explicit_root_fields { + ControlFlow::Break(Self::mixed_fields_error())?; + } else { + ControlFlow::Break(self.cached_introspection(schema, key, doc).await)? + } } else if !doc.has_explicit_root_fields { - ControlFlow::Break(self.cached_introspection(schema, key, doc).await)? + // root __typename only, probably a small query + // Execute it without caching: + ControlFlow::Break(Self::execute_introspection(schema, doc))? } } ControlFlow::Continue(()) diff --git a/apollo-router/tests/integration/typename.rs b/apollo-router/tests/integration/typename.rs index 782e90adb6..15b2363503 100644 --- a/apollo-router/tests/integration/typename.rs +++ b/apollo-router/tests/integration/typename.rs @@ -106,11 +106,7 @@ async fn aliased() { "###); } -// FIXME: bellow test panic because of bug in query planner, failing with: -// "value retrieval failed: empty query plan. This behavior is unexpected and we suggest opening an issue to apollographql/router with a reproduction." -// See: https://github.com/apollographql/router/issues/6154 #[tokio::test] -#[should_panic] async fn inside_inline_fragment() { let request = Request::fake_builder() .query("{ ... { __typename } }") @@ -120,14 +116,13 @@ async fn inside_inline_fragment() { insta::assert_json_snapshot!(response, @r###" { "data": { - "n": "MyQuery" + "__typename": "MyQuery" } } "###); } #[tokio::test] -#[should_panic] // See above FIXME async fn inside_fragment() { let query = r#" { ...SomeFragment } @@ -141,14 +136,13 @@ async fn inside_fragment() { insta::assert_json_snapshot!(response, @r###" { "data": { - "n": "MyQuery" + "__typename": "MyQuery" } } "###); } #[tokio::test] -#[should_panic] // See above FIXME async fn deeply_nested_inside_fragments() { let query = r#" { ...SomeFragment } @@ -168,7 +162,7 @@ async fn deeply_nested_inside_fragments() { insta::assert_json_snapshot!(response, @r###" { "data": { - "n": "MyQuery" + "__typename": "MyQuery" } } "###); From 207f3b697d0c9d76a07a6f267e957abca8c56ec7 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Mon, 25 Nov 2024 16:04:28 +0000 Subject: [PATCH 027/112] add launch_id to schema --- apollo-router/src/router_factory.rs | 1 + apollo-router/src/spec/schema.rs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index d9e51f6099..0fafec515e 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -587,6 +587,7 @@ pub(crate) async fn create_plugins( supergraph_schema_id.clone(), supergraph_schema.clone(), subgraph_schemas.clone(), + schema.launch_id, &configuration.notify.clone(), &mut plugin_instances, &mut errors, diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index 2208a5863e..d925869b89 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -30,6 +30,7 @@ pub(crate) struct Schema { pub(crate) implementers_map: apollo_compiler::collections::HashMap, api_schema: ApiSchema, pub(crate) schema_id: Arc, + pub(crate) launch_id: Option>, } /// Wrapper type to distinguish from `Schema::definitions` for the supergraph schema @@ -130,6 +131,7 @@ impl Schema { implementers_map, api_schema: ApiSchema(api_schema), schema_id, + launch_id: None, // TODO: get from uplink }) } @@ -336,7 +338,8 @@ impl std::fmt::Debug for Schema { subgraphs, implementers_map, api_schema: _, // skip - schema_id: _, + schema_id: _, // skip + launch_id: _, // skip } = self; f.debug_struct("Schema") .field("raw_sdl", raw_sdl) @@ -406,7 +409,7 @@ mod tests { type Baz { me: String } - + union UnionType2 = Foo | Bar "#, ); From 1c85fa8713366a7e8844835ac5621a59883e8bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Mon, 25 Nov 2024 16:58:52 +0000 Subject: [PATCH 028/112] fix(federation): avoid constantly recomputing conditions (#6326) --- .../src/query_plan/fetch_dependency_graph.rs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index 91482c2495..b91699ecc4 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -213,9 +213,10 @@ impl FetchIdGenerator { pub(crate) struct FetchSelectionSet { /// The selection set to be fetched from the subgraph. pub(crate) selection_set: Arc, - /// The conditions determining whether the fetch should be executed (which must be recomputed - /// from the selection set when it changes). - pub(crate) conditions: Conditions, + /// The conditions determining whether the fetch should be executed, derived from the selection + /// set. + #[serde(skip)] + conditions: OnceLock, } // PORT_NOTE: The JS codebase additionally has a property `onUpdateCallback`. This was only ever @@ -1781,7 +1782,7 @@ impl FetchDependencyGraph { .ok_or_else(|| FederationError::internal("Node unexpectedly missing"))?; let conditions = node .selection_set - .conditions + .conditions()? .update_with(&handled_conditions); let new_handled_conditions = conditions.clone().merge(handled_conditions); @@ -3181,9 +3182,8 @@ impl FetchSelectionSet { type_position: CompositeTypeDefinitionPosition, ) -> Result { let selection_set = Arc::new(SelectionSet::empty(schema, type_position)); - let conditions = selection_set.conditions()?; Ok(Self { - conditions, + conditions: OnceLock::new(), selection_set, }) } @@ -3194,19 +3194,35 @@ impl FetchSelectionSet { selection_set: Option<&Arc>, ) -> Result<(), FederationError> { Arc::make_mut(&mut self.selection_set).add_at_path(path_in_node, selection_set)?; - // TODO: when calling this multiple times, maybe only re-compute conditions at the end? - // Or make it lazily-initialized and computed on demand? - self.conditions = self.selection_set.conditions()?; + self.conditions.take(); Ok(()) } fn add_selections(&mut self, selection_set: &Arc) -> Result<(), FederationError> { Arc::make_mut(&mut self.selection_set).add_selection_set(selection_set)?; - // TODO: when calling this multiple times, maybe only re-compute conditions at the end? - // Or make it lazily-initialized and computed on demand? - self.conditions = self.selection_set.conditions()?; + self.conditions.take(); Ok(()) } + + /// The conditions determining whether the fetch should be executed. + fn conditions(&self) -> Result<&Conditions, FederationError> { + // This is a bit inefficient, because `get_or_try_init` is unstable. + // https://github.com/rust-lang/rust/issues/109737 + // + // Essentially we do `.get()` twice. This is still much better than eagerly recomputing the + // selection set all the time, though :) + if let Some(conditions) = self.conditions.get() { + return Ok(conditions); + } + + // Separating this call and the `.get_or_init` call means we could, if called from multiple + // threads, do the same work twice. + // The query planner does not use multiple threads for a single plan at the moment, and + // even if it did, occasionally computing this twice would still be better than eagerly + // recomputing it after every change. + let conditions = self.selection_set.conditions()?; + Ok(self.conditions.get_or_init(|| conditions)) + } } impl FetchInputs { From 5d25d92784dbc34eea27f99bf3bc7295e2f1638d Mon Sep 17 00:00:00 2001 From: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:00:39 -0500 Subject: [PATCH 029/112] Fix YAML reference URL link in values.yaml (#6277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renée Co-authored-by: Jesse Rosenberger --- .changesets/docs_fix_helm_yaml_config_link.md | 5 +++++ helm/chart/router/values.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changesets/docs_fix_helm_yaml_config_link.md diff --git a/.changesets/docs_fix_helm_yaml_config_link.md b/.changesets/docs_fix_helm_yaml_config_link.md new file mode 100644 index 0000000000..e3be0481b6 --- /dev/null +++ b/.changesets/docs_fix_helm_yaml_config_link.md @@ -0,0 +1,5 @@ +### docs: Updates the router YAML config reference URL ([PR #6277](https://github.com/apollographql/router/pull/6277)) + +Updates the router YAML config URL in the Helm chart values.yaml file to the new location + +By [@lambertjosh](https://github.com/lambertjosh) in https://github.com/apollographql/router/pull/6277 diff --git a/helm/chart/router/values.yaml b/helm/chart/router/values.yaml index 35f45618a7..d497aef33e 100644 --- a/helm/chart/router/values.yaml +++ b/helm/chart/router/values.yaml @@ -4,7 +4,7 @@ replicaCount: 1 -# -- See https://www.apollographql.com/docs/router/configuration/overview/#yaml-config-file for yaml structure +# -- See https://www.apollographql.com/docs/graphos/reference/router/configuration#yaml-config-file for yaml structure router: configuration: supergraph: From 5b1f7e29c42d99213d2d2b2624e092b854a00ba9 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Tue, 26 Nov 2024 11:23:57 +0000 Subject: [PATCH 030/112] compile, please --- apollo-router/src/plugin/mod.rs | 4 ++-- apollo-router/src/plugins/fleet_detector.rs | 17 ++++++++++------- apollo-router/src/router_factory.rs | 2 +- apollo-router/src/state_machine.rs | 15 ++++++++------- apollo-router/src/uplink/mod.rs | 3 +-- apollo-router/src/uplink/schema.rs | 7 ++++--- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index f97d7df020..e18d2bd921 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -177,7 +177,7 @@ where supergraph_schema_id: Arc, supergraph_schema: Arc>, subgraph_schemas: Option>, - launch_id: Option>, + launch_id: Option>>, notify: Notify, ) -> Self { PluginInit { @@ -186,7 +186,7 @@ where supergraph_schema_id, supergraph_schema, subgraph_schemas: subgraph_schemas.unwrap_or_default(), - launch_id, + launch_id: launch_id.flatten(), notify, } } diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index b3b5ac415d..093e79192b 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -125,17 +125,20 @@ impl GaugeStore { ); } { + let opts = opts.clone(); gauges.push( meter .u64_observable_gauge("apollo.router.schema") .with_description("Details about the current in-use schema") - .with_callback(|gauge| { + .with_callback(move |gauge| { // NOTE: this is a fixed gauge. We only care about observing the included // attributes. - let mut attributes: Vec = - vec![KeyValue::new("schema_hash", opts.supergraph_schema_hash)]; - if let Some(launch_id) = opts.launch_id { - attributes.push(KeyValue::new("launch_id", launch_id)); + let mut attributes: Vec = vec![KeyValue::new( + "schema_hash", + opts.supergraph_schema_hash.clone(), + )]; + if let Some(launch_id) = opts.launch_id.as_ref() { + attributes.push(KeyValue::new("launch_id", launch_id.to_string())); } gauge.observe(1, attributes.as_slice()) }) @@ -146,7 +149,7 @@ impl GaugeStore { } } -#[derive(Default)] +#[derive(Clone, Default)] struct GaugeOptions { supergraph_schema_hash: String, launch_id: Option, @@ -173,7 +176,7 @@ impl PluginPrivate for FleetDetector { let gauge_options = GaugeOptions { supergraph_schema_hash: plugin.supergraph_schema_id.to_string(), - launch_id: plugin.launch_id.and_then(|id| Some(id.to_string())), + launch_id: plugin.launch_id.map(|s| s.to_string()), }; Ok(FleetDetector { diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index 0fafec515e..d34eae8b45 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -587,7 +587,7 @@ pub(crate) async fn create_plugins( supergraph_schema_id.clone(), supergraph_schema.clone(), subgraph_schemas.clone(), - schema.launch_id, + schema.launch_id.clone(), &configuration.notify.clone(), &mut plugin_instances, &mut errors, diff --git a/apollo-router/src/state_machine.rs b/apollo-router/src/state_machine.rs index 7d50b0ab39..ee5d06f3b6 100644 --- a/apollo-router/src/state_machine.rs +++ b/apollo-router/src/state_machine.rs @@ -39,6 +39,7 @@ use crate::spec::Schema; use crate::uplink::license_enforcement::LicenseEnforcementReport; use crate::uplink::license_enforcement::LicenseState; use crate::uplink::license_enforcement::LICENSE_EXPIRED_URL; +use crate::uplink::schema::SchemaState; use crate::ApolloRouterError::NoLicense; const STATE_CHANGE: &str = "state change"; @@ -54,14 +55,14 @@ pub(crate) struct ListenAddresses { enum State { Startup { configuration: Option>, - schema: Option>, + schema: Option>, license: Option, listen_addresses_guard: OwnedRwLockWriteGuard, }, Running { configuration: Arc, _metrics: Option, - schema: Arc, + schema: Arc, license: LicenseState, server_handle: Option, router_service_factory: FA::RouterFactory, @@ -118,7 +119,7 @@ impl State { async fn update_inputs( mut self, state_machine: &mut StateMachine, - new_schema: Option>, + new_schema: Option>, new_configuration: Option>, new_license: Option, ) -> Self @@ -308,7 +309,7 @@ impl State { server_handle: &mut Option, previous_router_service_factory: Option<&FA::RouterFactory>, configuration: Arc, - sdl: Arc, + schema_state: Arc, license: LicenseState, listen_addresses_guard: &mut OwnedRwLockWriteGuard, mut all_connections_stopped_signals: Vec>, @@ -318,7 +319,7 @@ impl State { FA: RouterSuperServiceFactory, { let schema = Arc::new( - Schema::parse_arc(sdl.clone(), &configuration) + Schema::parse_arc(Arc::new(schema_state.sdl.clone()), &configuration) .map_err(|e| ServiceCreationError(e.to_string().into()))?, ); // Check the license @@ -422,7 +423,7 @@ impl State { Ok(Running { configuration, _metrics: metrics, - schema: sdl, + schema: schema_state, license, server_handle: Some(server_handle), router_service_factory, @@ -539,7 +540,7 @@ where NoMoreConfiguration => state.no_more_configuration().await, UpdateSchema(schema) => { state - .update_inputs(&mut self, Some(Arc::new(schema.sdl)), None, None) + .update_inputs(&mut self, Some(Arc::new(schema)), None, None) .await } NoMoreSchema => state.no_more_schema().await, diff --git a/apollo-router/src/uplink/mod.rs b/apollo-router/src/uplink/mod.rs index 85d4a62bf7..5b54261d96 100644 --- a/apollo-router/src/uplink/mod.rs +++ b/apollo-router/src/uplink/mod.rs @@ -17,10 +17,9 @@ use url::Url; pub(crate) mod license_enforcement; pub(crate) mod license_stream; pub(crate) mod persisted_queries_manifest_stream; +pub(crate) mod schema; pub(crate) mod schema_stream; -pub mod schema; - const GCP_URL: &str = "https://uplink.api.apollographql.com"; const AWS_URL: &str = "https://aws.uplink.api.apollographql.com"; diff --git a/apollo-router/src/uplink/schema.rs b/apollo-router/src/uplink/schema.rs index dcfafb7698..0c9b24953e 100644 --- a/apollo-router/src/uplink/schema.rs +++ b/apollo-router/src/uplink/schema.rs @@ -1,5 +1,6 @@ /// Represents the new state of a schema after an update. -pub struct SchemaState { - pub sdl: String, - pub launch_id: Option, +#[derive(Eq, PartialEq)] +pub(crate) struct SchemaState { + pub(crate) sdl: String, + pub(crate) launch_id: Option, } From 5cda200476ba6a52adefa4fb663d6d23b28ec660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Tue, 26 Nov 2024 13:49:57 +0000 Subject: [PATCH 031/112] Fix typo in persisted query metric attribute (#6332) --- .changesets/fix_renee_quieres_queries.md | 6 ++++++ apollo-router/src/services/layers/persisted_queries/mod.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changesets/fix_renee_quieres_queries.md diff --git a/.changesets/fix_renee_quieres_queries.md b/.changesets/fix_renee_quieres_queries.md new file mode 100644 index 0000000000..76152d2cc8 --- /dev/null +++ b/.changesets/fix_renee_quieres_queries.md @@ -0,0 +1,6 @@ +### Fix typo in persisted query metric attribute ([PR #6332](https://github.com/apollographql/router/pull/6332)) + +The `apollo.router.operations.persisted_queries` metric reports an attribute when a persisted query was not found. +Previously, the attribute name was `persisted_quieries.not_found`, with one `i` too many. Now it's `persisted_queries.not_found`. + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6332 \ No newline at end of file diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 0444590958..457ea67820 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -133,7 +133,7 @@ impl PersistedQueryLayer { } else { tracing::info!( monotonic_counter.apollo.router.operations.persisted_queries = 1u64, - persisted_quieries.not_found = true + persisted_queries.not_found = true ); // if APQ is not enabled, return an error indicating the query was not found Err(supergraph_err_operation_not_found( From 953ff074946f936f3818dc63f3078328612c2956 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Tue, 26 Nov 2024 14:00:30 +0000 Subject: [PATCH 032/112] remove launch_id from PluginInit serde --- apollo-router/src/plugin/mod.rs | 1 - apollo-router/src/spec/schema.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apollo-router/src/plugin/mod.rs b/apollo-router/src/plugin/mod.rs index e18d2bd921..9479e7f91a 100644 --- a/apollo-router/src/plugin/mod.rs +++ b/apollo-router/src/plugin/mod.rs @@ -253,7 +253,6 @@ impl PluginInit { .supergraph_schema_id(self.supergraph_schema_id) .supergraph_sdl(self.supergraph_sdl) .subgraph_schemas(self.subgraph_schemas) - // .launch_id(self.launch_id) .notify(self.notify.clone()) .build() } diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index d925869b89..114dcc1614 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -409,7 +409,7 @@ mod tests { type Baz { me: String } - + union UnionType2 = Foo | Bar "#, ); From 4cfabafb9186dca2a16cbe105fdc20ff3e5f5316 Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Tue, 26 Nov 2024 15:04:29 -0500 Subject: [PATCH 033/112] chore(federation): Improved serialization impls (#6337) --- Cargo.lock | 4 +- apollo-federation/src/display_helpers.rs | 30 ------- .../src/operation/directive_list.rs | 12 ++- apollo-federation/src/operation/mod.rs | 28 +++--- .../src/operation/selection_map.rs | 4 - .../src/query_graph/graph_path.rs | 17 ++-- apollo-federation/src/query_plan/mod.rs | 24 +++-- apollo-federation/src/utils/mod.rs | 1 + apollo-federation/src/utils/serde_bridge.rs | 89 +++++++++++++++++++ 9 files changed, 139 insertions(+), 70 deletions(-) create mode 100644 apollo-federation/src/utils/serde_bridge.rs diff --git a/Cargo.lock b/Cargo.lock index a774605d8d..c2d3166339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7308,9 +7308,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", diff --git a/apollo-federation/src/display_helpers.rs b/apollo-federation/src/display_helpers.rs index 330ec64003..898e7efd3e 100644 --- a/apollo-federation/src/display_helpers.rs +++ b/apollo-federation/src/display_helpers.rs @@ -1,9 +1,6 @@ use std::fmt; -use std::fmt::Debug; use std::fmt::Display; -use serde::Serializer; - pub(crate) struct State<'fmt, 'fmt2> { indent_level: usize, output: &'fmt mut fmt::Formatter<'fmt2>, @@ -98,30 +95,3 @@ impl Display for DisplayOption { } } } - -pub(crate) fn serialize_as_debug_string(data: &T, ser: S) -> Result -where - T: Debug, - S: Serializer, -{ - ser.serialize_str(&format!("{data:?}")) -} - -pub(crate) fn serialize_as_string(data: &T, ser: S) -> Result -where - T: ToString, - S: Serializer, -{ - ser.serialize_str(&data.to_string()) -} - -pub(crate) fn serialize_optional_vec_as_string( - data: &Option>, - ser: S, -) -> Result -where - T: Display, - S: Serializer, -{ - serialize_as_string(&DisplayOption(data.as_deref().map(DisplaySlice)), ser) -} diff --git a/apollo-federation/src/operation/directive_list.rs b/apollo-federation/src/operation/directive_list.rs index ad716dd1b4..bd2f021f1a 100644 --- a/apollo-federation/src/operation/directive_list.rs +++ b/apollo-federation/src/operation/directive_list.rs @@ -10,6 +10,7 @@ use std::sync::OnceLock; use apollo_compiler::executable; use apollo_compiler::Name; use apollo_compiler::Node; +use serde::Serialize; use super::sort_arguments; @@ -102,16 +103,23 @@ fn compare_sorted_arguments( static EMPTY_DIRECTIVE_LIST: executable::DirectiveList = executable::DirectiveList(vec![]); /// Contents for a non-empty directive list. -#[derive(Debug, Clone)] +// NOTE: For serialization, we skip everything but the directives. This will require manually +// implementing `Deserialize` as all other fields are derived from the directives. This could also +// mean flattening the serialization and making this type deserialize from +// `executable::DirectiveList` directly. +#[derive(Debug, Clone, Serialize)] 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). + #[serde(skip)] hash: u64, // Mutable access to the underlying directive list should not be handed out because `sort_order` // may get out of sync. + #[serde(serialize_with = "crate::utils::serde_bridge::serialize_exe_directive_list")] directives: executable::DirectiveList, + #[serde(skip)] sort_order: Vec, } @@ -166,7 +174,7 @@ impl DirectiveListInner { /// /// 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)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] pub(crate) struct DirectiveList { inner: Option>, } diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index 1fe1f16287..f93f805661 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -29,7 +29,6 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Node; use itertools::Itertools; -use serde::Serialize; use crate::compat::coerce_executable_values; use crate::error::FederationError; @@ -71,12 +70,11 @@ static NEXT_ID: atomic::AtomicUsize = atomic::AtomicUsize::new(1); /// Opaque wrapper of the unique selection ID type. /// -/// Note that we shouldn't add `derive(Serialize, Deserialize)` to this without changing the types -/// to be something like UUIDs. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -// NOTE(@TylerBloom): This feature gate can be removed once the condition in the comment above is -// met. Note that there are `serde(skip)` statements that should be removed once this is removed. -#[cfg_attr(feature = "snapshot_tracing", derive(Serialize))] +/// NOTE: This ID does not ensure that IDs are unique because its internal counter resets on +/// startup. It currently implements `Serialize` for debugging purposes. It should not implement +/// `Deserialize`, and, more specfically, it should not be used for caching until uniqueness is +/// provided (i.e. the inner type is a `Uuid` or the like). +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize)] pub(crate) struct SelectionId(usize); impl SelectionId { @@ -91,9 +89,12 @@ impl SelectionId { /// All arguments and input object values are sorted in a consistent order. /// /// This type is immutable and cheaply cloneable. -#[derive(Clone, PartialEq, Eq, Default)] +#[derive(Clone, PartialEq, Eq, Default, serde::Serialize)] pub(crate) struct ArgumentList { /// The inner list *must* be sorted with `sort_arguments`. + #[serde( + serialize_with = "crate::utils::serde_bridge::serialize_optional_slice_of_exe_argument_nodes" + )] inner: Option]>>, } @@ -242,7 +243,7 @@ impl Operation { /// - For the type, stores the schema and the position in that schema instead of just the /// `NamedType`. /// - Stores selections in a map so they can be normalized efficiently. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, serde::Serialize)] pub(crate) struct SelectionSet { #[serde(skip)] pub(crate) schema: ValidFederationSchema, @@ -270,7 +271,7 @@ pub(crate) use selection_map::SelectionValue; /// An analogue of the apollo-compiler type `Selection` that stores our other selection analogues /// instead of the apollo-compiler types. -#[derive(Debug, Clone, PartialEq, Eq, derive_more::IsVariant, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, derive_more::IsVariant, serde::Serialize)] pub(crate) enum Selection { Field(Arc), FragmentSpread(Arc), @@ -658,9 +659,7 @@ mod field_selection { pub(crate) schema: ValidFederationSchema, pub(crate) field_position: FieldDefinitionPosition, pub(crate) alias: Option, - #[serde(serialize_with = "crate::display_helpers::serialize_as_debug_string")] pub(crate) arguments: ArgumentList, - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] pub(crate) directives: DirectiveList, pub(crate) sibling_typename: Option, } @@ -868,16 +867,13 @@ mod fragment_spread_selection { pub(crate) fragment_name: Name, 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: 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: DirectiveList, - #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] pub(crate) selection_id: SelectionId, } @@ -1062,9 +1058,7 @@ mod inline_fragment_selection { pub(crate) schema: ValidFederationSchema, pub(crate) parent_type_position: CompositeTypeDefinitionPosition, pub(crate) type_condition_position: Option, - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] pub(crate) directives: DirectiveList, - #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] pub(crate) selection_id: SelectionId, } diff --git a/apollo-federation/src/operation/selection_map.rs b/apollo-federation/src/operation/selection_map.rs index 19857ba274..b8fc736895 100644 --- a/apollo-federation/src/operation/selection_map.rs +++ b/apollo-federation/src/operation/selection_map.rs @@ -34,26 +34,22 @@ pub(crate) enum SelectionKey<'a> { /// The field alias (if specified) or field name in the resulting selection set. response_name: &'a Name, /// directives applied on the field - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] directives: &'a DirectiveList, }, FragmentSpread { /// The name of the fragment. fragment_name: &'a Name, /// Directives applied on the fragment spread (does not contain @defer). - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] directives: &'a DirectiveList, }, InlineFragment { /// The optional type condition of the fragment. type_condition: Option<&'a Name>, /// Directives applied on the fragment spread (does not contain @defer). - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] directives: &'a DirectiveList, }, Defer { /// Unique selection ID used to distinguish deferred fragment spreads that cannot be merged. - #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] deferred_id: SelectionId, }, } diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 3bf55adc7d..b7505bebf5 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -252,21 +252,20 @@ pub(crate) struct SubgraphEnteringEdgeInfo { /// Wrapper for an override ID, which indicates a relationship between a group of `OpGraphPath`s /// where one "overrides" the others in the group. /// -/// Note that we shouldn't add `derive(Serialize, Deserialize)` to this without changing the types -/// to be something like UUIDs. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -// NOTE(@TylerBloom): This feature gate can be removed once the condition in the comment above is -// met. -#[cfg_attr(feature = "snapshot_tracing", derive(serde::Serialize))] +/// NOTE: This ID does not ensure that IDs are unique because its internal counter resets on +/// startup. It currently implements `Serialize` for debugging purposes. It should not implement +/// `Deserialize`, and, more specfically, it should not be used for caching until uniqueness is +/// provided (i.e. the inner type is a `Uuid` or the like). +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize)] pub(crate) struct OverrideId(usize); -/// Global storage for the counter used to allocate `OverrideId`s. -static NEXT_OVERRIDE_ID: atomic::AtomicUsize = atomic::AtomicUsize::new(1); +// Global storage for the counter used to uniquely identify selections +static NEXT_ID: atomic::AtomicUsize = atomic::AtomicUsize::new(1); impl OverrideId { fn new() -> Self { // atomically increment global counter - Self(NEXT_OVERRIDE_ID.fetch_add(1, atomic::Ordering::AcqRel)) + Self(NEXT_ID.fetch_add(1, atomic::Ordering::AcqRel)) } } diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index 313b9f6322..41733af9b9 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -18,6 +18,16 @@ pub(crate) mod query_planning_traversal; pub type QueryPlanCost = f64; +// NOTE: This type implements `Serialize` for debugging purposes; however, it should not implement +// `Deserialize` until two requires are met. +// 1) `SelectionId`s and `OverrideId`s are only unique per lifetime of the application. To avoid +// problems when caching, this needs to be changes. +// 2) There are several types transatively used in the query plan that are from `apollo-compiler`. +// They are serialized as strings and use the `serialize` methods provided by that crate. In +// order to implement `Deserialize`, care needs to be taken to deserialize these correctly. +// Moreover, how we serialize these types should also be revisited to make sure we can and want +// to support how they are serialized long term (e.g. how `DirectiveList` is serialized can be +// optimized). #[derive(Debug, Default, PartialEq, Serialize)] pub struct QueryPlan { pub node: Option, @@ -68,15 +78,17 @@ pub struct FetchNode { /// `FragmentSpread`. // PORT_NOTE: This was its own type in the JS codebase, but it's likely simpler to just have the // constraint be implicit for router instead of creating a new type. - #[serde(serialize_with = "crate::display_helpers::serialize_optional_vec_as_string")] + #[serde( + serialize_with = "crate::utils::serde_bridge::serialize_optional_vec_of_exe_selection" + )] pub requires: Option>, // PORT_NOTE: We don't serialize the "operation" string in this struct, as these query plan // nodes are meant for direct consumption by router (without any serdes), so we leave the // question of whether it needs to be serialized to router. - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + #[serde(serialize_with = "crate::utils::serde_bridge::serialize_valid_executable_document")] pub operation_document: Valid, pub operation_name: Option, - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + #[serde(serialize_with = "crate::utils::serde_bridge::serialize_exe_operation_type")] pub operation_kind: executable::OperationType, /// Optionally describe a number of "rewrites" that query plan executors should apply to the /// data that is sent as the input of this fetch. Note that such rewrites should only impact the @@ -166,7 +178,7 @@ pub struct DeferredDeferBlock { pub query_path: Vec, /// The part of the original query that "selects" the data to send in the deferred response /// (once the plan in `node` completes). Will be set _unless_ `node` is a `DeferNode` itself. - #[serde(serialize_with = "crate::display_helpers::serialize_as_debug_string")] + #[serde(serialize_with = "crate::utils::serde_bridge::serialize_optional_exe_selection_set")] pub sub_selection: Option, /// The plan to get all the data for this deferred block. Usually set, but can be `None` for a /// `@defer` application where everything has been fetched in the "primary block" (i.e. when @@ -249,9 +261,9 @@ pub type Conditions = Vec; /// an inline fragment in a query. #[derive(Debug, Clone, PartialEq, serde::Serialize)] pub enum QueryPathElement { - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + #[serde(serialize_with = "crate::utils::serde_bridge::serialize_exe_field")] Field(executable::Field), - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + #[serde(serialize_with = "crate::utils::serde_bridge::serialize_exe_inline_fragment")] InlineFragment(executable::InlineFragment), } diff --git a/apollo-federation/src/utils/mod.rs b/apollo-federation/src/utils/mod.rs index 8d62d08a04..3d7675252d 100644 --- a/apollo-federation/src/utils/mod.rs +++ b/apollo-federation/src/utils/mod.rs @@ -2,5 +2,6 @@ mod fallible_iterator; pub(crate) mod logging; +pub(crate) mod serde_bridge; pub(crate) use fallible_iterator::*; diff --git a/apollo-federation/src/utils/serde_bridge.rs b/apollo-federation/src/utils/serde_bridge.rs new file mode 100644 index 0000000000..2cbc958809 --- /dev/null +++ b/apollo-federation/src/utils/serde_bridge.rs @@ -0,0 +1,89 @@ +/// This module contains functions used to bridge the apollo compiler serialization methods with +/// serialization with serde. +use apollo_compiler::executable; +use apollo_compiler::validation::Valid; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Node; +use serde::ser::SerializeSeq; +use serde::Serializer; + +pub(crate) fn serialize_exe_field( + field: &executable::Field, + ser: S, +) -> Result { + ser.serialize_str(&field.serialize().no_indent().to_string()) +} + +pub(crate) fn serialize_exe_inline_fragment( + fragment: &executable::InlineFragment, + ser: S, +) -> Result { + ser.serialize_str(&fragment.serialize().no_indent().to_string()) +} + +pub(crate) fn serialize_optional_exe_selection_set( + set: &Option, + ser: S, +) -> Result { + match set { + Some(set) => ser.serialize_str(&set.serialize().no_indent().to_string()), + None => ser.serialize_none(), + } +} + +pub(crate) fn serialize_optional_slice_of_exe_argument_nodes< + S: Serializer, + Args: AsRef<[Node]>, +>( + args: &Option, + ser: S, +) -> Result { + let Some(args) = args else { + return ser.serialize_none(); + }; + let args = args.as_ref(); + let mut ser = ser.serialize_seq(Some(args.len()))?; + args.iter().try_for_each(|arg| { + ser.serialize_element(&format!( + "{}: {}", + arg.name, + arg.value.serialize().no_indent() + )) + })?; + ser.end() +} + +pub(crate) fn serialize_exe_directive_list( + list: &executable::DirectiveList, + ser: S, +) -> Result { + ser.serialize_str(&list.serialize().no_indent().to_string()) +} + +pub(crate) fn serialize_optional_vec_of_exe_selection( + selection: &Option>, + ser: S, +) -> Result { + let Some(selections) = selection else { + return ser.serialize_none(); + }; + let mut ser = ser.serialize_seq(Some(selections.len()))?; + selections.iter().try_for_each(|selection| { + ser.serialize_element(&selection.serialize().no_indent().to_string()) + })?; + ser.end() +} + +pub(crate) fn serialize_valid_executable_document( + doc: &Valid, + ser: S, +) -> Result { + ser.serialize_str(&doc.serialize().no_indent().to_string()) +} + +pub(crate) fn serialize_exe_operation_type( + ty: &executable::OperationType, + ser: S, +) -> Result { + ser.serialize_str(&ty.to_string()) +} From d32a9579243ca07f8fdb69079f594209c1dfb9e6 Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Wed, 27 Nov 2024 10:24:22 -0600 Subject: [PATCH 034/112] Enable test to look for invalid federation imports. (#6340) --- apollo-federation/src/link/database.rs | 69 ++++++++++++++++++++++++-- apollo-federation/src/link/mod.rs | 2 + apollo-federation/src/subgraph/spec.rs | 2 + 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/apollo-federation/src/link/database.rs b/apollo-federation/src/link/database.rs index ced0dc7b07..b4ae94c793 100644 --- a/apollo-federation/src/link/database.rs +++ b/apollo-federation/src/link/database.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use apollo_compiler::ast::Directive; use apollo_compiler::ast::DirectiveLocation; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::schema::DirectiveDefinition; use apollo_compiler::ty; @@ -14,6 +15,28 @@ use crate::link::Link; use crate::link::LinkError; use crate::link::LinksMetadata; use crate::link::DEFAULT_LINK_NAME; +use crate::subgraph::spec::FEDERATION_V2_DIRECTIVE_NAMES; +use crate::subgraph::spec::FEDERATION_V2_ELEMENT_NAMES; + +fn validate_federation_imports(link: &Link) -> Result<(), LinkError> { + let federation_directives: HashSet<_> = FEDERATION_V2_DIRECTIVE_NAMES.into_iter().collect(); + let federation_elements: HashSet<_> = FEDERATION_V2_ELEMENT_NAMES.into_iter().collect(); + + for imp in &link.imports { + if imp.is_directive && !federation_directives.contains(&imp.element) { + return Err(LinkError::InvalidImport(format!( + "Cannot import unknown federation directive \"@{}\".", + imp.element, + ))); + } else if !imp.is_directive && !federation_elements.contains(&imp.element) { + return Err(LinkError::InvalidImport(format!( + "Cannot import unknown federation element \"{}\".", + imp.element, + ))); + } + } + Ok(()) +} /// Extract @link metadata from a schema. pub fn links_metadata(schema: &Schema) -> Result, LinkError> { @@ -80,6 +103,10 @@ pub fn links_metadata(schema: &Schema) -> Result, LinkErro // We do a 2nd pass to collect and validate all the imports (it's a separate path so we // know all the names of the spec linked in the schema). for link in &links { + if link.url.identity == Identity::federation_identity() { + validate_federation_imports(link)?; + } + for import in &link.imports { let imported_name = import.imported_name(); let element_map = if import.is_directive { @@ -537,8 +564,6 @@ mod tests { insta::assert_snapshot!(errors, @"Invalid use of @link in schema: invalid alias 'myKey' for import name '@key': should start with '@' since the imported name does"); } - // TODO Implement - /* #[test] fn errors_on_importing_unknown_elements_for_known_features() { let schema = r#" @@ -557,8 +582,44 @@ mod tests { let schema = Schema::parse(schema, "testSchema").unwrap(); let errors = links_metadata(&schema).expect_err("should error"); - insta::assert_snapshot!(errors, @""); + insta::assert_snapshot!(errors, @"Unknown import: Cannot import unknown federation directive \"@foo\"."); + + // TODO Support multiple errors, in the meantime we'll just clone the code and run again + let schema = r#" + extend schema @link(url: "https://specs.apollo.dev/link/v1.0") + extend schema @link( + url: "https://specs.apollo.dev/federation/v2.0", + import: [ "key", { name: "@sharable" } ] + ) + + type Query { + q: Int + } + + directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA + "#; + + let schema = Schema::parse(schema, "testSchema").unwrap(); + let errors = links_metadata(&schema).expect_err("should error"); + insta::assert_snapshot!(errors, @"Unknown import: Cannot import unknown federation element \"key\"."); + + let schema = r#" + extend schema @link(url: "https://specs.apollo.dev/link/v1.0") + extend schema @link( + url: "https://specs.apollo.dev/federation/v2.0", + import: [ { name: "@sharable" } ] + ) + + type Query { + q: Int + } + + directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA + "#; + + let schema = Schema::parse(schema, "testSchema").unwrap(); + let errors = links_metadata(&schema).expect_err("should error"); + insta::assert_snapshot!(errors, @"Unknown import: Cannot import unknown federation directive \"@sharable\"."); } - */ } } diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 39f51e9499..971428257d 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -45,6 +45,8 @@ pub enum LinkError { InvalidName(#[from] InvalidNameError), #[error("Invalid use of @link in schema: {0}")] BootstrapError(String), + #[error("Unknown import: {0}")] + InvalidImport(String), } // TODO: Replace LinkError usages with FederationError. diff --git a/apollo-federation/src/subgraph/spec.rs b/apollo-federation/src/subgraph/spec.rs index 4e8676e498..79e3d72963 100644 --- a/apollo-federation/src/subgraph/spec.rs +++ b/apollo-federation/src/subgraph/spec.rs @@ -84,6 +84,8 @@ pub const FEDERATION_V2_DIRECTIVE_NAMES: [Name; 13] = [ TAG_DIRECTIVE_NAME, ]; +pub(crate) const FEDERATION_V2_ELEMENT_NAMES: [Name; 1] = [FIELDSET_SCALAR_NAME]; + // This type and the subsequent IndexMap exist purely so we can use match with Names; see comment // in FederationSpecDefinitions.directive_definition() for more information. enum FederationDirectiveName { From b7320f62672d68337e0f7dbd45b7a1a57f0f7198 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Wed, 27 Nov 2024 11:33:57 -0800 Subject: [PATCH 035/112] fix(dual-query-planner): sort directives before comparison in semantic diff (#6343) --- .../src/query_planner/dual_query_planner.rs | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 04d393c2cf..9e5250a447 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -14,6 +14,7 @@ use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; +use apollo_compiler::Node; use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use apollo_federation::query_plan::QueryPlan; @@ -1003,6 +1004,24 @@ fn same_ast_argument(x: &ast::Argument, y: &ast::Argument) -> bool { x.name == y.name && same_ast_argument_value(&x.value, &y.value) } +fn same_ast_arguments(x: &[Node], y: &[Node]) -> bool { + vec_matches_sorted_by( + x, + y, + |a, b| a.name.cmp(&b.name), + |a, b| same_ast_argument(a, b), + ) +} + +fn same_directives(x: &ast::DirectiveList, y: &ast::DirectiveList) -> bool { + vec_matches_sorted_by( + x, + y, + |a, b| a.name.cmp(&b.name), + |a, b| a.name == b.name && same_ast_arguments(&a.arguments, &b.arguments), + ) +} + fn get_ast_selection_key( selection: &ast::Selection, fragment_map: &HashMap, @@ -1035,24 +1054,20 @@ fn same_ast_selection( (ast::Selection::Field(x), ast::Selection::Field(y)) => { x.name == y.name && x.alias == y.alias - && vec_matches_sorted_by( - &x.arguments, - &y.arguments, - |a, b| a.name.cmp(&b.name), - |a, b| same_ast_argument(a, b), - ) - && x.directives == y.directives + && same_ast_arguments(&x.arguments, &y.arguments) + && same_directives(&x.directives, &y.directives) && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) } (ast::Selection::FragmentSpread(x), ast::Selection::FragmentSpread(y)) => { let mapped_fragment_name = fragment_map .get(&x.fragment_name) .unwrap_or(&x.fragment_name); - *mapped_fragment_name == y.fragment_name && x.directives == y.directives + *mapped_fragment_name == y.fragment_name + && same_directives(&x.directives, &y.directives) } (ast::Selection::InlineFragment(x), ast::Selection::InlineFragment(y)) => { x.type_condition == y.type_condition - && x.directives == y.directives + && same_directives(&x.directives, &y.directives) && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) } _ => false, @@ -1200,6 +1215,15 @@ mod ast_comparison_tests { assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } + #[test] + fn test_selection_directive_order() { + let op_x = r#"{ x @include(if:true) @skip(if:false) }"#; + let op_y = r#"{ x @skip(if:false) @include(if:true) }"#; + 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).is_ok()); + } + #[test] fn test_string_to_id_coercion_difference() { // JS QP coerces strings into integer for ID type, while Rust QP doesn't. From c5e12464d52f8ffcfa66669c9f066bf03a8ba0e5 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 29 Nov 2024 17:32:53 +0100 Subject: [PATCH 036/112] Fix the query hashing algorithm (#6205) Co-authored-by: Ivan Goncharov Co-authored-by: Jeremy Lempereur Co-authored-by: Gary Pennington Co-authored-by: Jesse Rosenberger --- .changesets/fix_fix_query_hashing.md | 8 + ...dden_field_yields_expected_query_plan.snap | 2 +- ...dden_field_yields_expected_query_plan.snap | 4 +- ...y_plan__tests__it_expose_query_plan-2.snap | 8 +- ...ery_plan__tests__it_expose_query_plan.snap | 8 +- ...ridge_query_planner__tests__plan_root.snap | 2 +- apollo-router/src/spec/query.rs | 4 +- apollo-router/src/spec/query/change.rs | 1921 +++++++++++++++-- apollo-router/tests/integration/redis.rs | 26 +- ...tegration__redis__query_planner_cache.snap | 2 +- .../snapshots/set_context__set_context.snap | 4 +- ...__set_context_dependent_fetch_failure.snap | 4 +- .../set_context__set_context_list.snap | 4 +- ...et_context__set_context_list_of_lists.snap | 4 +- ...set_context__set_context_no_typenames.snap | 4 +- ...et_context__set_context_type_mismatch.snap | 4 +- .../set_context__set_context_union.snap | 6 +- ...__set_context_unrelated_fetch_failure.snap | 6 +- .../set_context__set_context_with_null.snap | 4 +- ...ons___test_type_conditions_disabled-2.snap | 4 +- ...tions___test_type_conditions_disabled.snap | 4 +- ...ions___test_type_conditions_enabled-2.snap | 6 +- ...itions___test_type_conditions_enabled.snap | 6 +- ...ns_enabled_generate_query_fragments-2.snap | 6 +- ...ions_enabled_generate_query_fragments.snap | 6 +- ...ype_conditions_enabled_list_of_list-2.snap | 6 +- ..._type_conditions_enabled_list_of_list.snap | 6 +- ...itions_enabled_list_of_list_of_list-2.snap | 6 +- ...nditions_enabled_list_of_list_of_list.snap | 6 +- ...enabled_shouldnt_make_article_fetch-2.snap | 6 +- ...s_enabled_shouldnt_make_article_fetch.snap | 6 +- 31 files changed, 1837 insertions(+), 256 deletions(-) create mode 100644 .changesets/fix_fix_query_hashing.md diff --git a/.changesets/fix_fix_query_hashing.md b/.changesets/fix_fix_query_hashing.md new file mode 100644 index 0000000000..f7414b10c8 --- /dev/null +++ b/.changesets/fix_fix_query_hashing.md @@ -0,0 +1,8 @@ +### Fix the query hashing algorithm ([PR #6205](https://github.com/apollographql/router/pull/6205)) + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), updates to the query planner in this release will result in query plan caches being re-generated rather than re-used. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new query plans come into service. + +The Router includes a schema-aware query hashing algorithm designed to return the same hash across schema updates if the query remains unaffected. This update enhances the algorithm by addressing various corner cases, improving its reliability and consistency. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6205 diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap index 01cca77a5b..9ae3534578 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap @@ -19,7 +19,7 @@ expression: query_plan "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "23605b350473485e40bc8b1245f0c5c226a2997a96291bf3ad3412570a5172bb", + "schemaAwareHash": "cdab250089cc24ee95f749d187d2f936878348e62061b85fe6d1dccb9f4c26a1", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap index 455898049f..22296803a2 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap @@ -24,7 +24,7 @@ expression: query_plan "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "d14f50b039a3b961385f4d2a878c5800dd01141cddd3f8f1874a5499bbe397a9", + "schemaAwareHash": "113c32833cf7c2bb4324b08c068eb6613ebe0f77efaf3098ae6f0ed7b2df11de", "authorization": { "is_authenticated": false, "scopes": [], @@ -63,7 +63,7 @@ expression: query_plan "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "caa182daf66e4ffe9b1af8c386092ba830887bbae0d58395066fa480525080ec", + "schemaAwareHash": "7ef79e08871a3e122407b86d57079c3607cfd26a1993e2c239603a39d04d1bd8", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap index b8130ecf59..f2a3c247a1 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap @@ -69,7 +69,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "c595a39efeab9494c75a29de44ec4748c1741ddb96e1833e99139b058aa9da84", + "schemaAwareHash": "2504c66db02c170d0040785e0ac3455155db03beddcb7cc4d16f08ca02201fac", "authorization": { "is_authenticated": false, "scopes": [], @@ -109,7 +109,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7054d7662e20905b01d6f937e6b588ed422e0e79de737c98e3d51b6dc610179f", + "schemaAwareHash": "0f8abdb350d59e86567b72717be114a465c59ac4e6027d7179de6448b0fbc5a4", "authorization": { "is_authenticated": false, "scopes": [], @@ -156,7 +156,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "76d400fc6a494cbe05a44751923e570ee31928f0fb035ea36c14d4d6f4545482", + "schemaAwareHash": "a1e19c2c170464974293f946f09116216e71424a821908beb0062091475dad11", "authorization": { "is_authenticated": false, "scopes": [], @@ -200,7 +200,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "bff0ce0cfd6e2830949c59ae26f350d06d76150d6041b08c3d0c4384bc20b271", + "schemaAwareHash": "466ef25e373cf367e18ed264f76c7c4b2f27fe2ef105cb39d8528320addaedc7", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap index b8130ecf59..f2a3c247a1 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap @@ -69,7 +69,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "c595a39efeab9494c75a29de44ec4748c1741ddb96e1833e99139b058aa9da84", + "schemaAwareHash": "2504c66db02c170d0040785e0ac3455155db03beddcb7cc4d16f08ca02201fac", "authorization": { "is_authenticated": false, "scopes": [], @@ -109,7 +109,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7054d7662e20905b01d6f937e6b588ed422e0e79de737c98e3d51b6dc610179f", + "schemaAwareHash": "0f8abdb350d59e86567b72717be114a465c59ac4e6027d7179de6448b0fbc5a4", "authorization": { "is_authenticated": false, "scopes": [], @@ -156,7 +156,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "76d400fc6a494cbe05a44751923e570ee31928f0fb035ea36c14d4d6f4545482", + "schemaAwareHash": "a1e19c2c170464974293f946f09116216e71424a821908beb0062091475dad11", "authorization": { "is_authenticated": false, "scopes": [], @@ -200,7 +200,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "bff0ce0cfd6e2830949c59ae26f350d06d76150d6041b08c3d0c4384bc20b271", + "schemaAwareHash": "466ef25e373cf367e18ed264f76c7c4b2f27fe2ef105cb39d8528320addaedc7", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap index 16ba934103..7b2e0c912f 100644 --- a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap +++ b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap @@ -15,7 +15,7 @@ Fetch( output_rewrites: None, context_rewrites: None, schema_aware_hash: QueryHash( - "5c5036eef33484e505dd5a8666fd0a802e60d830964a4dbbf662526398563ffd", + "65e550250ef331b8dc49d9e2da8f4cd5add979720cbe83ba545a0f78ece8d329", ), authorization: CacheKeyMetadata { is_authenticated: false, diff --git a/apollo-router/src/spec/query.rs b/apollo-router/src/spec/query.rs index 3ad855870a..460f8cac3d 100644 --- a/apollo-router/src/spec/query.rs +++ b/apollo-router/src/spec/query.rs @@ -324,7 +324,9 @@ impl Query { let operation = Operation::from_hir(&operation, schema, &mut defer_stats, &fragments)?; let mut visitor = - QueryHashVisitor::new(schema.supergraph_schema(), &schema.raw_sdl, document); + QueryHashVisitor::new(schema.supergraph_schema(), &schema.raw_sdl, document).map_err( + |e| SpecError::QueryHashing(format!("could not calculate the query hash: {e}")), + )?; traverse::document(&mut visitor, document, operation_name).map_err(|e| { SpecError::QueryHashing(format!("could not calculate the query hash: {e}")) })?; diff --git a/apollo-router/src/spec/query/change.rs b/apollo-router/src/spec/query/change.rs index 8bca0e025b..73bda2881f 100644 --- a/apollo-router/src/spec/query/change.rs +++ b/apollo-router/src/spec/query/change.rs @@ -1,16 +1,57 @@ +//! Schema aware query hashing algorithm +//! +//! This is a query visitor that calculates a hash of all fields, along with all +//! the relevant types and directives in the schema. It is designed to generate +//! the same hash for the same query across schema updates if the schema change +//! would not affect that query. As an example, if a new type is added to the +//! schema, we know that it will have no impact to an existing query that cannot +//! be using it. +//! This algorithm is used in 2 places: +//! * in the query planner cache: generating query plans can be expensive, so the +//! router has a warm up feature, where upon receving a new schema, it will take +//! the most used queries and plan them, before switching traffic to the new +//! schema. Generating all of those plans takes a lot of time. By using this +//! hashing algorithm, we can detect that the schema change does not affect the +//! query, which means that we can reuse the old query plan directly and avoid +//! the expensive planning task +//! * in entity caching: the responses returned by subgraphs can change depending +//! on the schema (example: a field moving from String to Int), so we need to +//! detect that. One way to do it was to add the schema hash to the cache key, but +//! as a result it wipes the cache on every schema update, which will cause +//! performance and reliability issues. With this hashing algorithm, cached entries +//! can be kept across schema updates +//! +//! ## Technical details +//! +//! ### Query string hashing +//! A full hash of the query string is added along with the schema level data. This +//! is technically making the algorithm less useful, because the same query with +//! different indentation would get a different hash, while there would be no difference +//! in the query plan or the subgraph response. But this makes sure that if we forget +//! something in the way we hash the query, we will avoid collisions. +//! +//! ### Prefixes and suffixes +//! Across the entire visitor, we add prefixes and suffixes like this: +//! +//! ```rust +//! "^SCHEMA".hash(self); +//! ``` +//! +//! This prevents possible collision while hashing multiple things in a sequence. The +//! `^` character cannot be present in GraphQL names, so this is a good separator. use std::collections::HashMap; use std::collections::HashSet; use std::hash::Hash; use std::hash::Hasher; use apollo_compiler::ast; -use apollo_compiler::ast::Argument; use apollo_compiler::ast::FieldDefinition; use apollo_compiler::executable; use apollo_compiler::parser::Parser; use apollo_compiler::schema; use apollo_compiler::schema::DirectiveList; use apollo_compiler::schema::ExtendedType; +use apollo_compiler::schema::InterfaceType; use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Node; @@ -25,6 +66,8 @@ use crate::plugins::progressive_override::JOIN_SPEC_BASE_URL; use crate::spec::Schema; pub(crate) const JOIN_TYPE_DIRECTIVE_NAME: &str = "join__type"; +pub(crate) const CONTEXT_SPEC_BASE_URL: &str = "https://specs.apollo.dev/context"; +pub(crate) const CONTEXT_DIRECTIVE_NAME: &str = "context"; /// Calculates a hash of the query and the schema, but only looking at the parts of the /// schema which affect the query. @@ -33,17 +76,19 @@ pub(crate) const JOIN_TYPE_DIRECTIVE_NAME: &str = "join__type"; pub(crate) struct QueryHashVisitor<'a> { schema: &'a schema::Schema, // TODO: remove once introspection has been moved out of query planning - // For now, introspection is stiull handled by the planner, so when an + // For now, introspection is still handled by the planner, so when an // introspection query is hashed, it should take the whole schema into account schema_str: &'a str, hasher: Sha256, fragments: HashMap<&'a Name, &'a Node>, hashed_types: HashSet, - // name, field - hashed_fields: HashSet<(String, String)>, + hashed_field_definitions: HashSet<(String, String)>, seen_introspection: bool, join_field_directive_name: Option, join_type_directive_name: Option, + context_directive_name: Option, + // map from context string to list of type names + contexts: HashMap>, } impl<'a> QueryHashVisitor<'a> { @@ -51,14 +96,14 @@ impl<'a> QueryHashVisitor<'a> { schema: &'a schema::Schema, schema_str: &'a str, executable: &'a executable::ExecutableDocument, - ) -> Self { - Self { + ) -> Result { + let mut visitor = Self { schema, schema_str, hasher: Sha256::new(), fragments: executable.fragments.iter().collect(), hashed_types: HashSet::new(), - hashed_fields: HashSet::new(), + hashed_field_definitions: HashSet::new(), seen_introspection: false, // should we just return an error if we do not find those directives? join_field_directive_name: Schema::directive_name( @@ -73,7 +118,30 @@ impl<'a> QueryHashVisitor<'a> { ">=0.1.0", JOIN_TYPE_DIRECTIVE_NAME, ), + context_directive_name: Schema::directive_name( + schema, + CONTEXT_SPEC_BASE_URL, + ">=0.1.0", + CONTEXT_DIRECTIVE_NAME, + ), + contexts: HashMap::new(), + }; + + visitor.hash_schema()?; + + Ok(visitor) + } + + pub(crate) fn hash_schema(&mut self) -> Result<(), BoxError> { + "^SCHEMA".hash(self); + for directive_definition in self.schema.directive_definitions.values() { + self.hash_directive_definition(directive_definition)?; } + + self.hash_directive_list_schema(&self.schema.schema_definition.directives); + + "^SCHEMA-END".hash(self); + Ok(()) } pub(crate) fn hash_query( @@ -82,8 +150,9 @@ impl<'a> QueryHashVisitor<'a> { executable: &'a executable::ExecutableDocument, operation_name: Option<&str>, ) -> Result, BoxError> { - let mut visitor = QueryHashVisitor::new(schema, schema_str, executable); + let mut visitor = QueryHashVisitor::new(schema, schema_str, executable)?; traverse::document(&mut visitor, executable, operation_name)?; + // hash the entire query string to prevent collisions executable.to_string().hash(&mut visitor); Ok(visitor.finish()) } @@ -92,180 +161,317 @@ impl<'a> QueryHashVisitor<'a> { self.hasher.finalize().as_slice().into() } + fn hash_directive_definition( + &mut self, + directive_definition: &Node, + ) -> Result<(), BoxError> { + "^DIRECTIVE_DEFINITION".hash(self); + directive_definition.name.as_str().hash(self); + "^ARGUMENT_LIST".hash(self); + for argument in &directive_definition.arguments { + self.hash_input_value_definition(argument)?; + } + "^ARGUMENT_LIST_END".hash(self); + + "^DIRECTIVE_DEFINITION-END".hash(self); + + Ok(()) + } + + fn hash_directive_list_schema(&mut self, directive_list: &schema::DirectiveList) { + "^DIRECTIVE_LIST".hash(self); + for directive in directive_list { + self.hash_directive(directive); + } + "^DIRECTIVE_LIST_END".hash(self); + } + + fn hash_directive_list_ast(&mut self, directive_list: &ast::DirectiveList) { + "^DIRECTIVE_LIST".hash(self); + for directive in directive_list { + self.hash_directive(directive); + } + "^DIRECTIVE_LIST_END".hash(self); + } + fn hash_directive(&mut self, directive: &Node) { + "^DIRECTIVE".hash(self); directive.name.as_str().hash(self); + "^ARGUMENT_LIST".hash(self); for argument in &directive.arguments { - self.hash_argument(argument) + self.hash_argument(argument); } + "^ARGUMENT_END".hash(self); + + "^DIRECTIVE-END".hash(self); } fn hash_argument(&mut self, argument: &Node) { + "^ARGUMENT".hash(self); argument.name.hash(self); self.hash_value(&argument.value); + "^ARGUMENT-END".hash(self); } fn hash_value(&mut self, value: &ast::Value) { + "^VALUE".hash(self); + match value { - schema::Value::Null => "null".hash(self), + schema::Value::Null => "^null".hash(self), schema::Value::Enum(e) => { - "enum".hash(self); + "^enum".hash(self); e.hash(self); } schema::Value::Variable(v) => { - "variable".hash(self); + "^variable".hash(self); v.hash(self); } schema::Value::String(s) => { - "string".hash(self); + "^string".hash(self); s.hash(self); } schema::Value::Float(f) => { - "float".hash(self); + "^float".hash(self); f.hash(self); } schema::Value::Int(i) => { - "int".hash(self); + "^int".hash(self); i.hash(self); } schema::Value::Boolean(b) => { - "boolean".hash(self); + "^boolean".hash(self); b.hash(self); } schema::Value::List(l) => { - "list[".hash(self); + "^list[".hash(self); for v in l.iter() { self.hash_value(v); } - "]".hash(self); + "^]".hash(self); } schema::Value::Object(o) => { - "object{".hash(self); + "^object{".hash(self); for (k, v) in o.iter() { + "^key".hash(self); + k.hash(self); - ":".hash(self); + "^value:".hash(self); self.hash_value(v); } - "}".hash(self); + "^}".hash(self); } } + "^VALUE-END".hash(self); } - fn hash_type_by_name(&mut self, t: &str) -> Result<(), BoxError> { - if self.hashed_types.contains(t) { + fn hash_type_by_name(&mut self, name: &str) -> Result<(), BoxError> { + "^TYPE_BY_NAME".hash(self); + + name.hash(self); + + // we need this this to avoid an infinite loop when hashing types that refer to each other + if self.hashed_types.contains(name) { return Ok(()); } - self.hashed_types.insert(t.to_string()); + self.hashed_types.insert(name.to_string()); - if let Some(ty) = self.schema.types.get(t) { + if let Some(ty) = self.schema.types.get(name) { self.hash_extended_type(ty)?; } + "^TYPE_BY_NAME-END".hash(self); + Ok(()) } fn hash_extended_type(&mut self, t: &'a ExtendedType) -> Result<(), BoxError> { + "^EXTENDED_TYPE".hash(self); + match t { ExtendedType::Scalar(s) => { - for directive in &s.directives { - self.hash_directive(&directive.node); - } + "^SCALAR".hash(self); + self.hash_directive_list_schema(&s.directives); + "^SCALAR_END".hash(self); } + // this only hashes the type level info, not the fields, because those will be taken from the query + // we will still hash the fields using for the key ExtendedType::Object(o) => { - for directive in &o.directives { - self.hash_directive(&directive.node); - } + "^OBJECT".hash(self); + + self.hash_directive_list_schema(&o.directives); self.hash_join_type(&o.name, &o.directives)?; + + self.record_context(&o.name, &o.directives)?; + + "^IMPLEMENTED_INTERFACES_LIST".hash(self); + for interface in &o.implements_interfaces { + self.hash_type_by_name(&interface.name)?; + } + "^IMPLEMENTED_INTERFACES_LIST_END".hash(self); + "^OBJECT_END".hash(self); } ExtendedType::Interface(i) => { - for directive in &i.directives { - self.hash_directive(&directive.node); - } + "^INTERFACE".hash(self); + + self.hash_directive_list_schema(&i.directives); + self.hash_join_type(&i.name, &i.directives)?; + + self.record_context(&i.name, &i.directives)?; + + "^IMPLEMENTED_INTERFACES_LIST".hash(self); + for implementor in &i.implements_interfaces { + self.hash_type_by_name(&implementor.name)?; + } + "^IMPLEMENTED_INTERFACES_LIST_END".hash(self); + + if let Some(implementers) = self.schema().implementers_map().get(&i.name) { + "^IMPLEMENTER_OBJECT_LIST".hash(self); + + for object in &implementers.objects { + self.hash_type_by_name(object)?; + } + "^IMPLEMENTER_OBJECT_LIST_END".hash(self); + + "^IMPLEMENTER_INTERFACE_LIST".hash(self); + for interface in &implementers.interfaces { + self.hash_type_by_name(interface)?; + } + "^IMPLEMENTER_INTERFACE_LIST_END".hash(self); + } + + "^INTERFACE_END".hash(self); } ExtendedType::Union(u) => { - for directive in &u.directives { - self.hash_directive(&directive.node); - } + "^UNION".hash(self); + + self.hash_directive_list_schema(&u.directives); + self.record_context(&u.name, &u.directives)?; + + "^MEMBER_LIST".hash(self); for member in &u.members { self.hash_type_by_name(member.as_str())?; } + "^MEMBER_LIST_END".hash(self); + "^UNION_END".hash(self); } ExtendedType::Enum(e) => { - for directive in &e.directives { - self.hash_directive(&directive.node); - } + "^ENUM".hash(self); + self.hash_directive_list_schema(&e.directives); + + "^ENUM_VALUE_LIST".hash(self); for (value, def) in &e.values { + "^VALUE".hash(self); + value.hash(self); - for directive in &def.directives { - self.hash_directive(directive); - } + self.hash_directive_list_ast(&def.directives); + "^VALUE_END".hash(self); } + "^ENUM_VALUE_LIST_END".hash(self); + "^ENUM_END".hash(self); } ExtendedType::InputObject(o) => { - for directive in &o.directives { - self.hash_directive(&directive.node); - } + "^INPUT_OBJECT".hash(self); + self.hash_directive_list_schema(&o.directives); + "^FIELD_LIST".hash(self); for (name, ty) in &o.fields { - if ty.default_value.is_some() { - name.hash(self); - self.hash_input_value_definition(&ty.node)?; - } + "^NAME".hash(self); + name.hash(self); + + "^ARGUMENT".hash(self); + self.hash_input_value_definition(&ty.node)?; } + "^FIELD_LIST_END".hash(self); + "^INPUT_OBJECT_END".hash(self); } } + "^EXTENDED_TYPE-END".hash(self); + Ok(()) } fn hash_type(&mut self, t: &ast::Type) -> Result<(), BoxError> { + "^TYPE".hash(self); + match t { - schema::Type::Named(name) => self.hash_type_by_name(name.as_str()), + schema::Type::Named(name) => self.hash_type_by_name(name.as_str())?, schema::Type::NonNullNamed(name) => { "!".hash(self); - self.hash_type_by_name(name.as_str()) + self.hash_type_by_name(name.as_str())?; } schema::Type::List(t) => { "[]".hash(self); - self.hash_type(t) + self.hash_type(t)?; } schema::Type::NonNullList(t) => { "[]!".hash(self); - self.hash_type(t) + self.hash_type(t)?; } } + "^TYPE-END".hash(self); + Ok(()) } fn hash_field( &mut self, - parent_type: String, - type_name: String, + parent_type: &str, field_def: &FieldDefinition, - arguments: &[Node], + node: &executable::Field, ) -> Result<(), BoxError> { - if self.hashed_fields.insert((parent_type.clone(), type_name)) { - self.hash_type_by_name(&parent_type)?; + "^FIELD".hash(self); + self.hash_field_definition(parent_type, field_def)?; + + "^ARGUMENT_LIST".hash(self); + for argument in &node.arguments { + self.hash_argument(argument); + } + "^ARGUMENT_LIST_END".hash(self); - field_def.name.hash(self); + self.hash_directive_list_ast(&node.directives); - for argument in &field_def.arguments { - self.hash_input_value_definition(argument)?; - } + node.alias.hash(self); + "^FIELD-END".hash(self); - for argument in arguments { - self.hash_argument(argument); - } + Ok(()) + } - self.hash_type(&field_def.ty)?; + fn hash_field_definition( + &mut self, + parent_type: &str, + field_def: &FieldDefinition, + ) -> Result<(), BoxError> { + "^FIELD_DEFINITION".hash(self); - for directive in &field_def.directives { - self.hash_directive(directive); - } + let field_index = (parent_type.to_string(), field_def.name.as_str().to_string()); + if self.hashed_field_definitions.contains(&field_index) { + return Ok(()); + } - self.hash_join_field(&parent_type, &field_def.directives)?; + self.hashed_field_definitions.insert(field_index); + + self.hash_type_by_name(parent_type)?; + + field_def.name.hash(self); + self.hash_type(&field_def.ty)?; + + // for every field, we also need to look at fields defined in `@requires` because + // they will affect the query plan + self.hash_join_field(parent_type, &field_def.directives)?; + + self.hash_directive_list_ast(&field_def.directives); + + "^ARGUMENT_DEF_LIST".hash(self); + for argument in &field_def.arguments { + self.hash_input_value_definition(argument)?; } + "^ARGUMENT_DEF_LIST_END".hash(self); + + "^FIELD_DEFINITION_END".hash(self); + Ok(()) } @@ -273,17 +479,23 @@ impl<'a> QueryHashVisitor<'a> { &mut self, t: &Node, ) -> Result<(), BoxError> { + "^INPUT_VALUE".hash(self); + self.hash_type(&t.ty)?; - for directive in &t.directives { - self.hash_directive(directive); - } + self.hash_directive_list_ast(&t.directives); + if let Some(value) = t.default_value.as_ref() { self.hash_value(value); + } else { + "^INPUT_VALUE-NO_DEFAULT".hash(self); } + "^INPUT_VALUE-END".hash(self); Ok(()) } fn hash_join_type(&mut self, name: &Name, directives: &DirectiveList) -> Result<(), BoxError> { + "^JOIN_TYPE".hash(self); + if let Some(dir_name) = self.join_type_directive_name.as_deref() { if let Some(dir) = directives.get(dir_name) { if let Some(key) = dir @@ -306,6 +518,7 @@ impl<'a> QueryHashVisitor<'a> { } } } + "^JOIN_TYPE-END".hash(self); Ok(()) } @@ -315,6 +528,8 @@ impl<'a> QueryHashVisitor<'a> { parent_type: &str, directives: &ast::DirectiveList, ) -> Result<(), BoxError> { + "^JOIN_FIELD".hash(self); + if let Some(dir_name) = self.join_field_directive_name.as_deref() { if let Some(dir) = directives.get(dir_name) { if let Some(requires) = dir @@ -338,9 +553,114 @@ impl<'a> QueryHashVisitor<'a> { } } } + + if let Some(context_arguments) = dir + .specified_argument_by_name("contextArguments") + .and_then(|value| value.as_list()) + { + for argument in context_arguments { + self.hash_context_argument(argument)?; + } + } + } + } + "^JOIN_FIELD-END".hash(self); + + Ok(()) + } + + fn record_context( + &mut self, + parent_type: &str, + directives: &DirectiveList, + ) -> Result<(), BoxError> { + if let Some(dir_name) = self.context_directive_name.as_deref() { + if let Some(dir) = directives.get(dir_name) { + if let Some(name) = dir + .specified_argument_by_name("name") + .and_then(|arg| arg.as_str()) + { + self.contexts + .entry(name.to_string()) + .or_default() + .push(parent_type.to_string()); + } + } + } + Ok(()) + } + + /// Hashes the context argument of a field + /// + /// contextArgument contains a selection that must be applied to a parent type in the + /// query that matches the context name. We store in advance which type names map to + /// which contexts, to reuse them here when we encounter the selection. + fn hash_context_argument(&mut self, argument: &ast::Value) -> Result<(), BoxError> { + if let Some(obj) = argument.as_object() { + let context_name = Name::new("context")?; + let selection_name = Name::new("selection")?; + // the contextArgument input type is defined as follows: + // input join__ContextArgument { + // name: String! + // type: String! + // context: String! + // selection: join__FieldValue! + // } + // and that is checked by schema validation, so the `context` and `selection` fields + // are guaranteed to be present and to be strings. + if let (Some(context), Some(selection)) = ( + obj.iter() + .find(|(k, _)| k == &context_name) + .and_then(|(_, v)| v.as_str()), + obj.iter() + .find(|(k, _)| k == &selection_name) + .and_then(|(_, v)| v.as_str()), + ) { + if let Some(types) = self.contexts.get(context).cloned() { + for ty in types { + if let Ok(parent_type) = Name::new(ty.as_str()) { + let mut parser = Parser::new(); + + // we assume that the selection was already checked by schema validation + if let Ok(field_set) = parser.parse_field_set( + Valid::assume_valid_ref(self.schema), + parent_type.clone(), + selection, + std::path::Path::new("schema.graphql"), + ) { + traverse::selection_set( + self, + parent_type.as_str(), + &field_set.selection_set.selections[..], + )?; + } + } + } + } + } + Ok(()) + } else { + Err("context argument value is not an object".into()) + } + } + + fn hash_interface_implementers( + &mut self, + intf: &InterfaceType, + node: &executable::Field, + ) -> Result<(), BoxError> { + "^INTERFACE_IMPL".hash(self); + + if let Some(implementers) = self.schema.implementers_map().get(&intf.name) { + "^IMPLEMENTER_LIST".hash(self); + for object in &implementers.objects { + self.hash_type_by_name(object)?; + traverse::selection_set(self, object, &node.selection_set.selections)?; } + "^IMPLEMENTER_LIST_END".hash(self); } + "^INTERFACE_IMPL-END".hash(self); Ok(()) } } @@ -351,16 +671,41 @@ impl<'a> Hasher for QueryHashVisitor<'a> { } fn write(&mut self, bytes: &[u8]) { + // byte separator between each part that is hashed + self.hasher.update(&[0xFF][..]); self.hasher.update(bytes); } } impl<'a> Visitor for QueryHashVisitor<'a> { fn operation(&mut self, root_type: &str, node: &executable::Operation) -> Result<(), BoxError> { + "^VISIT_OPERATION".hash(self); + root_type.hash(self); self.hash_type_by_name(root_type)?; + node.operation_type.hash(self); + node.name.hash(self); - traverse::operation(self, root_type, node) + "^VARIABLE_LIST".hash(self); + for variable in &node.variables { + variable.name.hash(self); + self.hash_type(&variable.ty)?; + + if let Some(value) = variable.default_value.as_ref() { + self.hash_value(value); + } else { + "^VISIT_OPERATION-NO_DEFAULT".hash(self); + } + + self.hash_directive_list_ast(&variable.directives); + } + "^VARIABLE_LIST_END".hash(self); + + self.hash_directive_list_ast(&node.directives); + + traverse::operation(self, root_type, node)?; + "^VISIT_OPERATION-END".hash(self); + Ok(()) } fn field( @@ -369,30 +714,44 @@ impl<'a> Visitor for QueryHashVisitor<'a> { field_def: &ast::FieldDefinition, node: &executable::Field, ) -> Result<(), BoxError> { + "^VISIT_FIELD".hash(self); + if !self.seen_introspection && (field_def.name == "__schema" || field_def.name == "__type") { self.seen_introspection = true; self.schema_str.hash(self); } - self.hash_field( - parent_type.to_string(), - field_def.name.as_str().to_string(), - field_def, - &node.arguments, - )?; + self.hash_field(parent_type, field_def, node)?; - traverse::field(self, field_def, node) + if let Some(ExtendedType::Interface(intf)) = + self.schema.types.get(field_def.ty.inner_named_type()) + { + self.hash_interface_implementers(intf, node)?; + } + + traverse::field(self, field_def, node)?; + "^VISIT_FIELD_END".hash(self); + Ok(()) } fn fragment(&mut self, node: &executable::Fragment) -> Result<(), BoxError> { + "^VISIT_FRAGMENT".hash(self); + node.name.hash(self); self.hash_type_by_name(node.type_condition())?; - traverse::fragment(self, node) + self.hash_directive_list_ast(&node.directives); + + traverse::fragment(self, node)?; + "^VISIT_FRAGMENT-END".hash(self); + + Ok(()) } fn fragment_spread(&mut self, node: &executable::FragmentSpread) -> Result<(), BoxError> { + "^VISIT_FRAGMENT_SPREAD".hash(self); + node.fragment_name.hash(self); let type_condition = &self .fragments @@ -401,7 +760,12 @@ impl<'a> Visitor for QueryHashVisitor<'a> { .type_condition(); self.hash_type_by_name(type_condition)?; - traverse::fragment_spread(self, node) + self.hash_directive_list_ast(&node.directives); + + traverse::fragment_spread(self, node)?; + "^VISIT_FRAGMENT_SPREAD-END".hash(self); + + Ok(()) } fn inline_fragment( @@ -409,10 +773,16 @@ impl<'a> Visitor for QueryHashVisitor<'a> { parent_type: &str, node: &executable::InlineFragment, ) -> Result<(), BoxError> { + "^VISIT_INLINE_FRAGMENT".hash(self); + if let Some(type_condition) = &node.type_condition { self.hash_type_by_name(type_condition)?; } - traverse::inline_fragment(self, parent_type, node) + self.hash_directive_list_ast(&node.directives); + + traverse::inline_fragment(self, parent_type, node)?; + "^VISIT_INLINE_FRAGMENT-END".hash(self); + Ok(()) } fn schema(&self) -> &apollo_compiler::Schema { @@ -470,7 +840,7 @@ mod tests { .unwrap() .validate(&schema) .unwrap(); - let mut visitor = QueryHashVisitor::new(&schema, schema_str, &exec); + let mut visitor = QueryHashVisitor::new(&schema, schema_str, &exec).unwrap(); traverse::document(&mut visitor, &exec, None).unwrap(); ( @@ -489,7 +859,7 @@ mod tests { .unwrap() .validate(&schema) .unwrap(); - let mut visitor = QueryHashVisitor::new(&schema, schema_str, &exec); + let mut visitor = QueryHashVisitor::new(&schema, schema_str, &exec).unwrap(); traverse::document(&mut visitor, &exec, None).unwrap(); hex::encode(visitor.finish()) @@ -498,10 +868,6 @@ mod tests { #[test] fn me() { let schema1: &str = r#" - schema { - query: Query - } - type Query { me: User customer: User @@ -514,10 +880,6 @@ mod tests { "#; let schema2: &str = r#" - schema { - query: Query - } - type Query { me: User } @@ -546,38 +908,84 @@ mod tests { #[test] fn directive() { let schema1: &str = r#" - schema { - query: Query - } - directive @test on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @test on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM | UNION | INPUT_OBJECT type Query { me: User customer: User + s: S + u: U + e: E + inp(i: I): ID } type User { id: ID! name: String } + + scalar S + + type A { + a: ID + } + + type B { + b: ID + } + + union U = A | B + + enum E { + A + B + } + + input I { + a: Int = 0 + b: Int + } "#; let schema2: &str = r#" - schema { - query: Query - } - directive @test on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM - + directive @test on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM | UNION | INPUT_OBJECT + type Query { me: User customer: User @test + s: S + u: U + e: E + inp(i: I): ID } - type User { id: ID! @test name: String } + + scalar S @test + + type A { + a: ID + } + + type B { + b: ID + } + + union U @test = A | B + + enum E @test { + A + B + } + + + input I @test { + a: Int = 0 + b: Int + } "#; let query = "query { me { name } }"; assert!(hash(schema1, query).equals(&hash(schema2, query))); @@ -587,14 +995,23 @@ mod tests { let query = "query { customer { id } }"; assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { s }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { u { ...on A { a } ...on B { b } } }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { e }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { inp(i: { b: 0 }) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); } #[test] fn interface() { let schema1: &str = r#" - schema { - query: Query - } directive @test on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM type Query { @@ -634,7 +1051,7 @@ mod tests { "#; let query = "query { me { id name } }"; - assert!(hash(schema1, query).equals(&hash(schema2, query))); + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); let query = "query { customer { id } }"; assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); @@ -644,12 +1061,12 @@ mod tests { } #[test] - fn arguments() { + fn arguments_int() { let schema1: &str = r#" type Query { a(i: Int): Int b(i: Int = 1): Int - c(i: Int = 1, j: Int): Int + c(i: Int = 1, j: Int = null): Int } "#; @@ -657,7 +1074,7 @@ mod tests { type Query { a(i: Int!): Int b(i: Int = 2): Int - c(i: Int = 2, j: Int): Int + c(i: Int = 2, j: Int = null): Int } "#; @@ -678,66 +1095,181 @@ mod tests { } #[test] - fn entities() { + fn arguments_float() { let schema1: &str = r#" - schema { - query: Query - } - - scalar _Any - - union _Entity = User - type Query { - _entities(representations: [_Any!]!): [_Entity]! - me: User - customer: User - } - - type User { - id: ID - name: String + a(i: Float): Int + b(i: Float = 1.0): Int + c(i: Float = 1.0, j: Int): Int } "#; let schema2: &str = r#" - schema { - query: Query - } - - scalar _Any - - union _Entity = User - type Query { - _entities(representations: [_Any!]!): [_Entity]! - me: User - } - - - type User { - id: ID! - name: String - } + a(i: Float!): Int + b(i: Float = 2.0): Int + c(i: Float = 2.0, j: Int): Int + } "#; - let query1 = r#"query Query1($representations:[_Any!]!){ - _entities(representations:$representations){ - ...on User { - id - name - } - } - }"#; - - println!("query1: {query1}"); + let query = "query { a(i: 0) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); - let hash1 = hash_subgraph_query(schema1, query1); - println!("hash1: {hash1}"); + let query = "query { b }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); - let hash2 = hash_subgraph_query(schema2, query1); - println!("hash2: {hash2}"); + let query = "query { b(i: 0)}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + let query = "query { c(j: 0)}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { c(i:0, j: 0)}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + } + + #[test] + fn arguments_list() { + let schema1: &str = r#" + type Query { + a(i: [Float]): Int + b(i: [Float] = [1.0]): Int + c(i: [Float] = [1.0], j: Int): Int + } + "#; + + let schema2: &str = r#" + type Query { + a(i: [Float!]): Int + b(i: [Float] = [2.0]): Int + c(i: [Float] = [2.0], j: Int): Int + } + "#; + + let query = "query { a(i: [0]) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { b }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { b(i: [0])}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { c(j: 0)}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { c(i: [0], j: 0)}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + } + + #[test] + fn arguments_object() { + let schema1: &str = r#" + input T { + d: Int + e: String + } + input U { + c: Int + } + input V { + d: Int = 0 + } + + type Query { + a(i: T): Int + b(i: T = { d: 1, e: "a" }): Int + c(c: U): Int + d(d: V): Int + } + "#; + + let schema2: &str = r#" + input T { + d: Int + e: String + } + input U { + c: Int! + } + input V { + d: Int = 1 + } + + type Query { + a(i: T!): Int + b(i: T = { d: 2, e: "b" }): Int + c(c: U): Int + d(d: V): Int + } + "#; + + let query = "query { a(i: { d: 1, e: \"a\" }) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { b }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { b(i: { d: 3, e: \"c\" })}"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { c(c: { c: 0 }) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { d(d: { }) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + + let query = "query { d(d: { d: 2 }) }"; + assert!(hash(schema1, query).doesnt_match(&hash(schema2, query))); + } + + #[test] + fn entities() { + let schema1: &str = r#" + scalar _Any + + union _Entity = User + + type Query { + _entities(representations: [_Any!]!): [_Entity]! + me: User + customer: User + } + + type User { + id: ID + name: String + } + "#; + + let schema2: &str = r#" + scalar _Any + + union _Entity = User + + type Query { + _entities(representations: [_Any!]!): [_Entity]! + me: User + } + + + type User { + id: ID! + name: String + } + "#; + + let query1 = r#"query Query1($representations:[_Any!]!){ + _entities(representations:$representations){ + ...on User { + id + name + } + } + }"#; + + let hash1 = hash_subgraph_query(schema1, query1); + let hash2 = hash_subgraph_query(schema2, query1); assert_ne!(hash1, hash2); let query2 = r#"query Query1($representations:[_Any!]!){ @@ -748,14 +1280,8 @@ mod tests { } }"#; - println!("query2: {query2}"); - let hash1 = hash_subgraph_query(schema1, query2); - println!("hash1: {hash1}"); - let hash2 = hash_subgraph_query(schema2, query2); - println!("hash2: {hash2}"); - assert_eq!(hash1, hash2); } @@ -1041,10 +1567,6 @@ mod tests { #[test] fn fields_with_different_arguments_have_different_hashes() { let schema: &str = r#" - schema { - query: Query - } - type Query { test(arg: Int): String } @@ -1063,19 +1585,35 @@ mod tests { } #[test] - fn fields_with_different_aliases_have_different_hashes() { + fn fields_with_different_arguments_on_nest_field_different_hashes() { let schema: &str = r#" - schema { - query: Query + type Test { + test(arg: Int): String + recursiveLink: Test } - + + type Query { + directLink: Test + } + "#; + + let query_one = "{ directLink { test recursiveLink { test(arg: 1) } } }"; + let query_two = "{ directLink { test recursiveLink { test(arg: 2) } } }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn fields_with_different_aliases_have_different_hashes() { + let schema: &str = r#" type Query { test(arg: Int): String } "#; - let query_one = "query { a: test }"; - let query_two = "query { b: test }"; + let query_one = "{ a: test }"; + let query_two = "{ b: test }"; // This assertion tests an internal hash function that isn't directly // used for the query hash, and we'll need to make it pass to rely @@ -1084,4 +1622,1035 @@ mod tests { // assert!(hash(schema, query_one).doesnt_match(&hash(schema, query_two))); assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); } + + #[test] + fn operations_with_different_names_have_different_hash() { + let schema: &str = r#" + type Query { + test: String + } + "#; + + let query_one = "query Foo { test }"; + let query_two = "query Bar { test }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn adding_directive_on_operation_changes_hash() { + let schema: &str = r#" + directive @test on QUERY + type Query { + test: String + } + "#; + + let query_one = "query { test }"; + let query_two = "query @test { test }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn order_of_variables_changes_hash() { + let schema: &str = r#" + type Query { + test1(arg: Int): String + test2(arg: Int): String + } + "#; + + let query_one = "query ($foo: Int, $bar: Int) { test1(arg: $foo) test2(arg: $bar) }"; + let query_two = "query ($foo: Int, $bar: Int) { test1(arg: $bar) test2(arg: $foo) }"; + + assert!(hash(schema, query_one).doesnt_match(&hash(schema, query_two))); + } + + #[test] + fn query_variables_with_different_types_have_different_hash() { + let schema: &str = r#" + type Query { + test(arg: Int): String + } + "#; + + let query_one = "query ($var: Int) { test(arg: $var) }"; + let query_two = "query ($var: Int!) { test(arg: $var) }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn query_variables_with_different_default_values_have_different_hash() { + let schema: &str = r#" + type Query { + test(arg: Int): String + } + "#; + + let query_one = "query ($var: Int = 1) { test(arg: $var) }"; + let query_two = "query ($var: Int = 2) { test(arg: $var) }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn adding_directive_to_query_variable_change_hash() { + let schema: &str = r#" + directive @test on VARIABLE_DEFINITION + + type Query { + test(arg: Int): String + } + "#; + + let query_one = "query ($var: Int) { test(arg: $var) }"; + let query_two = "query ($var: Int @test) { test(arg: $var) }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn order_of_directives_change_hash() { + let schema: &str = r#" + directive @foo on FIELD + directive @bar on FIELD + + type Query { + test(arg: Int): String + } + "#; + + let query_one = "{ test @foo @bar }"; + let query_two = "{ test @bar @foo }"; + + assert!(hash(schema, query_one).from_hash_query != hash(schema, query_two).from_hash_query); + assert!(hash(schema, query_one).from_visitor != hash(schema, query_two).from_visitor); + } + + #[test] + fn directive_argument_type_change_hash() { + let schema1: &str = r#" + directive @foo(a: Int) on FIELD + directive @bar on FIELD + + type Query { + test(arg: Int): String + } + "#; + + let schema2: &str = r#" + directive @foo(a: Int!) on FIELD + directive @bar on FIELD + + type Query { + test(arg: Int): String + } + "#; + + let query = "{ test @foo(a: 1) }"; + + assert!(hash(schema1, query).from_hash_query != hash(schema2, query).from_hash_query); + assert!(hash(schema1, query).from_visitor != hash(schema2, query).from_visitor); + } + + #[test] + fn adding_directive_on_schema_changes_hash() { + let schema1: &str = r#" + schema { + query: Query + } + + type Query { + foo: String + } + "#; + + let schema2: &str = r#" + directive @test on SCHEMA + schema @test { + query: Query + } + + type Query { + foo: String + } + "#; + + let query = "{ foo }"; + + assert!(hash(schema1, query).from_hash_query != hash(schema2, query).from_hash_query); + assert!(hash(schema1, query).from_visitor != hash(schema2, query).from_visitor); + } + + #[test] + fn changing_type_of_field_changes_hash() { + let schema1: &str = r#" + type Query { + test: Int + } + "#; + + let schema2: &str = r#" + type Query { + test: Float + } + "#; + + let query = "{ test }"; + + assert!(hash(schema1, query).from_hash_query != hash(schema2, query).from_hash_query); + assert!(hash(schema1, query).from_visitor != hash(schema2, query).from_visitor); + } + + #[test] + fn changing_type_to_interface_changes_hash() { + let schema1: &str = r#" + type Query { + foo: Foo + } + + interface Foo { + value: String + } + "#; + + let schema2: &str = r#" + type Query { + foo: Foo + } + + type Foo { + value: String + } + "#; + + let query = "{ foo { value } }"; + + assert!(hash(schema1, query).from_hash_query != hash(schema2, query).from_hash_query); + assert!(hash(schema1, query).from_visitor != hash(schema2, query).from_visitor); + } + + #[test] + fn changing_operation_kind_changes_hash() { + let schema: &str = r#" + schema { + query: Test + mutation: Test + } + + type Test { + test: String + } + "#; + + let query_one = "query { test }"; + let query_two = "mutation { test }"; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn adding_directive_on_field_should_change_hash() { + let schema: &str = r#" + directive @test on FIELD + + type Query { + test: String + } + "#; + + let query_one = "{ test }"; + let query_two = "{ test @test }"; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn adding_directive_on_fragment_spread_change_hash() { + let schema: &str = r#" + type Query { + test: String + } + "#; + + let query_one = r#" + { ...Test } + + fragment Test on Query { + test + } + "#; + let query_two = r#" + { ...Test @skip(if: false) } + + fragment Test on Query { + test + } + "#; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn adding_directive_on_fragment_change_hash() { + let schema: &str = r#" + directive @test on FRAGMENT_DEFINITION + + type Query { + test: String + } + "#; + + let query_one = r#" + { ...Test } + + fragment Test on Query { + test + } + "#; + let query_two = r#" + { ...Test } + + fragment Test on Query @test { + test + } + "#; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn adding_directive_on_inline_fragment_change_hash() { + let schema: &str = r#" + type Query { + test: String + } + "#; + + let query_one = "{ ... { test } }"; + let query_two = "{ ... @skip(if: false) { test } }"; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn moving_field_changes_hash() { + let schema: &str = r#" + type Query { + me: User + } + + type User { + id: ID + name: String + friend: User + } + "#; + + let query_one = r#" + { + me { + friend { + id + name + } + } + } + "#; + let query_two = r#" + { + me { + friend { + id + } + name + } + } + "#; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn changing_type_of_fragment_changes_hash() { + let schema: &str = r#" + type Query { + fooOrBar: FooOrBar + } + + type Foo { + id: ID + value: String + } + + type Bar { + id: ID + value: String + } + + union FooOrBar = Foo | Bar + "#; + + let query_one = r#" + { + fooOrBar { + ... on Foo { id } + ... on Bar { id } + ... Test + } + } + + fragment Test on Foo { + value + } + "#; + let query_two = r#" + { + fooOrBar { + ... on Foo { id } + ... on Bar { id } + ... Test + } + } + + fragment Test on Bar { + value + } + "#; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn changing_interface_implementors_changes_hash() { + let schema1: &str = r#" + type Query { + data: I + } + + interface I { + id: ID + value: String + } + + type Foo implements I { + id: ID + value: String + foo: String + } + + type Bar { + id: ID + value: String + bar: String + } + "#; + + let schema2: &str = r#" + type Query { + data: I + } + + interface I { + id: ID + value: String + } + + type Foo implements I { + id: ID + value: String + foo2: String + } + + type Bar { + id: ID + value: String + bar: String + } + "#; + + let schema3: &str = r#" + type Query { + data: I + } + + interface I { + id: ID + value: String + } + + type Foo implements I { + id: ID + value: String + foo: String + } + + type Bar implements I { + id: ID + value: String + bar: String + } + "#; + + let query = r#" + { + data { + id + value + } + } + "#; + + // changing an unrelated field in implementors does not change the hash + assert_eq!( + hash(schema1, query).from_hash_query, + hash(schema2, query).from_hash_query + ); + assert_eq!( + hash(schema1, query).from_visitor, + hash(schema2, query).from_visitor + ); + + // adding a new implementor changes the hash + assert_ne!( + hash(schema1, query).from_hash_query, + hash(schema3, query).from_hash_query + ); + assert_ne!( + hash(schema1, query).from_visitor, + hash(schema3, query).from_visitor + ); + } + + #[test] + fn changing_interface_directives_changes_hash() { + let schema1: &str = r#" + directive @a(name: String) on INTERFACE + + type Query { + data: I + } + + interface I @a { + id: ID + value: String + } + + type Foo implements I { + id: ID + value: String + foo: String + } + "#; + + let schema2: &str = r#" + directive @a(name: String) on INTERFACE + + type Query { + data: I + } + + interface I @a(name: "abc") { + id: ID + value: String + } + + type Foo implements I { + id: ID + value: String + foo2: String + } + + "#; + + let query = r#" + { + data { + id + value + } + } + "#; + + // changing a directive applied on the interface definition changes the hash + assert_ne!( + hash(schema1, query).from_hash_query, + hash(schema2, query).from_hash_query + ); + assert_ne!( + hash(schema1, query).from_visitor, + hash(schema2, query).from_visitor + ); + } + + #[test] + fn it_is_weird_so_i_dont_know_how_to_name_it_change_hash() { + let schema: &str = r#" + type Query { + id: ID + someField: SomeType + test: String + } + + type SomeType { + id: ID + test: String + } + "#; + + let query_one = r#" + { + test + someField { id test } + id + } + "#; + let query_two = r#" + { + ...test + someField { id } + } + + fragment test on Query { + id + } + "#; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn it_change_directive_location() { + let schema: &str = r#" + directive @foo on QUERY | VARIABLE_DEFINITION + + type Query { + field(arg: String): String + } + "#; + + let query_one = r#" + query Test ($arg: String @foo) { + field(arg: $arg) + } + "#; + let query_two = r#" + query Test ($arg: String) @foo { + field(arg: $arg) + } + "#; + + assert_ne!( + hash(schema, query_one).from_hash_query, + hash(schema, query_two).from_hash_query + ); + assert_ne!( + hash(schema, query_one).from_visitor, + hash(schema, query_two).from_visitor + ); + } + + #[test] + fn it_changes_on_implementors_list_changes() { + let schema_one: &str = r#" + interface SomeInterface { + value: String + } + + type Foo implements SomeInterface { + value: String + } + + type Bar implements SomeInterface { + value: String + } + + union FooOrBar = Foo | Bar + + type Query { + fooOrBar: FooOrBar + } + "#; + let schema_two: &str = r#" + interface SomeInterface { + value: String + } + + type Foo { + value: String # <= This field shouldn't be a part of query plan anymore + } + + type Bar implements SomeInterface { + value: String + } + + union FooOrBar = Foo | Bar + + type Query { + fooOrBar: FooOrBar + } + "#; + + let query = r#" + { + fooOrBar { + ... on SomeInterface { + value + } + } + } + "#; + + assert_ne!( + hash(schema_one, query).from_hash_query, + hash(schema_two, query).from_hash_query + ); + assert_ne!( + hash(schema_one, query).from_visitor, + hash(schema_two, query).from_visitor + ); + } + + #[test] + fn it_changes_on_context_changes() { + let schema_one: &str = r#" + 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/context/v0.1", for: SECURITY) { + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: String) on ARGUMENT_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 + +scalar context__context + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2") +} + +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: SUBGRAPH1) { + t: T! @join__field(graph: SUBGRAPH1) +} + + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") { + id: ID! + u: U! + uList: [U]! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context" + name: "a" + type: "String" + selection: "{ prop }" + } + ] + ) +} + "#; + + // changing T.prop from String! to String + let schema_two: &str = r#" + 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/context/v0.1", for: SECURITY) { + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: String) on ARGUMENT_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 + +scalar context__context + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2") +} + +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: SUBGRAPH1) { + t: T! @join__field(graph: SUBGRAPH1) +} + + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") { + id: ID! + u: U! + uList: [U]! + prop: String +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context" + name: "a" + type: "String" + selection: "{ prop }" + } + ] + ) +} + "#; + + let query = r#" + query Query { + t { + __typename + id + u { + __typename + field + } + } + } + "#; + + assert_ne!( + hash(schema_one, query).from_hash_query, + hash(schema_two, query).from_hash_query + ); + assert_ne!( + hash(schema_one, query).from_visitor, + hash(schema_two, query).from_visitor + ); + } } diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 955cbc9290..34e6b11093 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -51,7 +51,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { } // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:cache:1:federation:v2.9.3:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; + let known_cache_key = "plan:cache:1:federation:v2.9.3:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -448,14 +448,15 @@ async fn entity_cache_basic() -> Result<(), BoxError> { .unwrap(); insta::assert_json_snapshot!(response); + // if this is failing due to a cache key change, hook up redis-cli with the MONITOR command to see the keys being set let s:String = client - .get("version:1.0:subgraph:products:type:Query:hash:0b4d791a3403d76643db0a9e4a8d304b1cd1f8c4ab68cb58ab7ccdc116a1da1c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:products:type:Query:hash:ff69b4487720d4776dd85eef89ca7a077bbed2f37bbcec6252905cc701415728:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); insta::assert_json_snapshot!(v.as_object().unwrap().get("data").unwrap()); - let s: String = client.get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:04c47a3b857394fb0feef5b999adc073b8ab7416e3bc871f54c0b885daae8359:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c").await.unwrap(); + let s: String = client.get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:ea227d6d9f1e595fe4aa16ab7f90e7d3b7676bb065cd836d960e99e1edf94bef:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c").await.unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); insta::assert_json_snapshot!(v.as_object().unwrap().get("data").unwrap()); @@ -567,7 +568,7 @@ async fn entity_cache_basic() -> Result<(), BoxError> { insta::assert_json_snapshot!(response); let s:String = client - .get("version:1.0:subgraph:reviews:type:Product:entity:d9a4cd73308dd13ca136390c10340823f94c335b9da198d2339c886c738abf0d:hash:04c47a3b857394fb0feef5b999adc073b8ab7416e3bc871f54c0b885daae8359:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:reviews:type:Product:entity:d9a4cd73308dd13ca136390c10340823f94c335b9da198d2339c886c738abf0d:hash:ea227d6d9f1e595fe4aa16ab7f90e7d3b7676bb065cd836d960e99e1edf94bef:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -796,7 +797,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { insta::assert_json_snapshot!(response); let s:String = client - .get("version:1.0:subgraph:products:type:Query:hash:0b4d791a3403d76643db0a9e4a8d304b1cd1f8c4ab68cb58ab7ccdc116a1da1c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:products:type:Query:hash:ff69b4487720d4776dd85eef89ca7a077bbed2f37bbcec6252905cc701415728:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -817,7 +818,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { ); let s: String = client - .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:04c47a3b857394fb0feef5b999adc073b8ab7416e3bc871f54c0b885daae8359:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:ea227d6d9f1e595fe4aa16ab7f90e7d3b7676bb065cd836d960e99e1edf94bef:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -861,7 +862,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { insta::assert_json_snapshot!(response); let s:String = client - .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:f7d6d3af2706afe346e3d5fd353e61bd186d2fc64cb7b3c13a62162189519b5f:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:00a80cad9114a41b85ea6df444a905f65e12ed82aba261d1716c71863608da35:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -976,7 +977,7 @@ async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0ade8e18db172d9d51b36a2112513c15032d103100644df418a50596de3adfba", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0ade8e18db172d9d51b36a2112513c15032d103100644df418a50596de3adfba", ) .await; } @@ -1006,7 +1007,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:066f41523274aed2428e0f08c9de077ee748a1d8470ec31edb5224030a198f3b", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:066f41523274aed2428e0f08c9de077ee748a1d8470ec31edb5224030a198f3b", ) .await; } @@ -1028,7 +1029,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:b31d320db1af4015998cc89027f0ede2305dcc61724365e9b76d4252f90c7677", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:b31d320db1af4015998cc89027f0ede2305dcc61724365e9b76d4252f90c7677", ) .await; } @@ -1050,7 +1051,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:d54414eeede3a1bf631d88a84a1e3a354683be87746e79a69769cf18d919cc01", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:d54414eeede3a1bf631d88a84a1e3a354683be87746e79a69769cf18d919cc01", ) .await; } @@ -1074,7 +1075,8 @@ 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:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; + // If the tests above are failing, this is the key that needs to be changed first. + let starting_key = "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap index d7330676f2..077d5130ee 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap @@ -13,7 +13,7 @@ expression: query_plan "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "d38dcce02eea33b3834447eefedabb09d3b14f3b01ad512e881f9e65137f0565", + "schemaAwareHash": "3981edbf89170585b228f36239d5a9fac84d78945994c2add49de1cefca874ba", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/set_context__set_context.snap b/apollo-router/tests/snapshots/set_context__set_context.snap index 18bfcbfcc9..3630f5d86a 100644 --- a/apollo-router/tests/snapshots/set_context__set_context.snap +++ b/apollo-router/tests/snapshots/set_context__set_context.snap @@ -34,7 +34,7 @@ expression: response "operationKind": "query", "operationName": "Query__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "0163c552923b61fbde6dbcd879ffc2bb887175dc41bbf75a272875524e664e8d", + "schemaAwareHash": "b45f75d11c91f90d616e0786fe9a1a675f4f478a6688aa38b9809b3416b66507", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -80,7 +80,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "e64d79913c52a4a8b95bfae44986487a1ac73118f27df3b602972a5cbb1f360a", + "schemaAwareHash": "02dbfc4ce65b1eb8ee39c37f09a88b56ee4671bbcdc935f3ec2a7e25e36c2931", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap index 67390167e7..c0c964b497 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap @@ -43,7 +43,7 @@ expression: response "operationKind": "query", "operationName": "Query_fetch_dependent_failure__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "6bcaa7a2d52a416d5278eaef6be102427f328b6916075f193c87459516a7fb6d", + "schemaAwareHash": "e8671657b38c13454f18f2bf8df9ebbeb80235c50592f72a2c4141803fe6db59", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -89,7 +89,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "0e56752501c8cbf53429c5aa2df95765ea2c7cba95db9213ce42918699232651", + "schemaAwareHash": "8499a69f5ac2e4ce2e0acc76b38b7839b89b6ccba9142494d1a82dd17dd0e5f2", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_list.snap b/apollo-router/tests/snapshots/set_context__set_context_list.snap index d6dd312f0a..fa0a57fc15 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_list.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_list.snap @@ -40,7 +40,7 @@ expression: response "operationKind": "query", "operationName": "Query__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "805348468cefee0e3e745cb1bcec0ab4bd44ba55f6ddb91e52e0bc9b437c2dee", + "schemaAwareHash": "50ba3d7291f38802f222251fe79055e06345e62252e74eba9e01bbec34510cea", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -86,7 +86,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "e64d79913c52a4a8b95bfae44986487a1ac73118f27df3b602972a5cbb1f360a", + "schemaAwareHash": "02dbfc4ce65b1eb8ee39c37f09a88b56ee4671bbcdc935f3ec2a7e25e36c2931", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap index c390c1db88..13e0282397 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap @@ -44,7 +44,7 @@ expression: response "operationKind": "query", "operationName": "QueryLL__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "53e85332dda78d566187c8886c207b81acfe3ab5ea0cafd3d71fb0b153026d80", + "schemaAwareHash": "589a7dec7f09fdedd06128f1e7396c727740ac1f84ad936ea9c61c3cf96d3ee4", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -90,7 +90,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "8ed6f85b6a77c293c97171b4a98f7dd563e98a737d4c3a9f5c54911248498ec7", + "schemaAwareHash": "0c966292093d13acca6c8ebb257a146a46840e5a04c9cbaede12e08df98cd489", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap b/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap index e9743a7902..0b45046ad7 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap @@ -32,7 +32,7 @@ expression: response "operationKind": "query", "operationName": "Query__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "0163c552923b61fbde6dbcd879ffc2bb887175dc41bbf75a272875524e664e8d", + "schemaAwareHash": "b45f75d11c91f90d616e0786fe9a1a675f4f478a6688aa38b9809b3416b66507", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -78,7 +78,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "e64d79913c52a4a8b95bfae44986487a1ac73118f27df3b602972a5cbb1f360a", + "schemaAwareHash": "02dbfc4ce65b1eb8ee39c37f09a88b56ee4671bbcdc935f3ec2a7e25e36c2931", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap index 3208b9bf0a..2c9c27bc14 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap @@ -32,7 +32,7 @@ expression: response "operationKind": "query", "operationName": "Query_type_mismatch__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "34c8f7c0f16220c5d4b589c8da405f49510e092756fa98629c73dea06fd7c243", + "schemaAwareHash": "f47b7620f3ba24d2c15a2978451bd7b59f462e63dc3259a244efe1d971979bfa", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -78,7 +78,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "feb578fd1831280f376d8961644e670dd8c3508d0a18fcf69a6de651e25e9ca8", + "schemaAwareHash": "a6ff3cddbf800b647fdb15f6da6d5e68a71979be93d51852bd289f047202d8ac", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_union.snap b/apollo-router/tests/snapshots/set_context__set_context_union.snap index 6c995c1e8b..50c3e865df 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_union.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_union.snap @@ -31,7 +31,7 @@ expression: response "operationKind": "query", "operationName": "QueryUnion__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "3e768a1879f4ced427937721980688052b471dbfee0d653b212c85f2732591cc", + "schemaAwareHash": "b6ed60b7e69ed10f45f85aba713969cd99b0e1a832464ba3f225fdf055706424", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -80,7 +80,7 @@ expression: response "typeCondition": "V" } ], - "schemaAwareHash": "0c190d5db5b15f89fa45de844d2cec59725986e44fcb0dbdb9ab870a197cf026", + "schemaAwareHash": "99faa73249f207ea11b1b5064d77f278367398cfee881b2fc3a8a9ebe53f44fe", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_1" @@ -134,7 +134,7 @@ expression: response "typeCondition": "V" } ], - "schemaAwareHash": "2d7376a8d1f7f2a929361e838bb0435ed4c4a6194fa8754af52d4b6dc7140508", + "schemaAwareHash": "e925299b31ea9338d10257fd150ec7ece230f55117105dd631181f4b2a33075a", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_1" diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap index ead3b10258..b80bb0c853 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap @@ -37,7 +37,7 @@ expression: response "operationKind": "query", "operationName": "Query_fetch_failure__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "84a7305d62d79b5bbca976c5522d6b32c5bbcbf76b495e4430f9cdcb51c80a57", + "schemaAwareHash": "d321568b33e32986df6d30a82443ebb919949617ffc33affe8b413658af52b8a", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -76,7 +76,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "acb960692b01a756fcc627cafef1c47ead8afa60fa70828e5011ba9f825218ab", + "schemaAwareHash": "a9aa68bb30f2040298629fc2fe72dc8438ce16bcdfdbe1a16ff088cf61d38719", "serviceName": "Subgraph2", "variableUsages": [] }, @@ -128,7 +128,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "9fd65f6f213899810bce20180de6754354a25dc3c1bc97d0b7214a177cf8b0bb", + "schemaAwareHash": "da0e31f9990723a68dbd1e1bb164a068342da5561db1a28679693a406429d09a", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_with_null.snap b/apollo-router/tests/snapshots/set_context__set_context_with_null.snap index badc32bc8a..f4c5eb3898 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_with_null.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_with_null.snap @@ -29,7 +29,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "4c0c9f83a57e9a50ff1f6dd601ec0a1588f1485d5cfb1015822af4017263e807", + "schemaAwareHash": "73819a48542fc2a3eb3a831b27ab5cc0b1286a73c2750279d8886fc529ba9e9e", "authorization": { "is_authenticated": false, "scopes": [], @@ -82,7 +82,7 @@ expression: response "renameKeyTo": "contextualArgument_1_0" } ], - "schemaAwareHash": "8db802e78024d406645f1ddc8972255e917bc738bfbed281691a45e34c92debb", + "schemaAwareHash": "042955e454618e67e75f3c86c9b8c71e2da866f1c40d0dc462d52053e1861803", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap index 390a7c79df..18cc3a2fb3 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", + "schemaAwareHash": "c0f708d5078310a7416806cf545ed8820c88d554badbec4e6e928d37abd29813", "authorization": { "is_authenticated": false, "scopes": [], @@ -137,7 +137,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "317a722a677563080aeac92f60ac2257d9288ca6851a0e8980fcf18f58b462a8", + "schemaAwareHash": "7f9d59b380b10c77c3080665549c5f835bf1721dae62b60bc9d6dd39975d9583", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap index 687028717f..de7f4d2827 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "0e1644746fe4beab7def35ec8cc12bde39874c6bb8b9dfd928456196b814a111", + "schemaAwareHash": "18631e67bb0c6b514cb51e8dff155a2900c8000ad319ea4784e5ca8b1275aca2", "authorization": { "is_authenticated": false, "scopes": [], @@ -137,7 +137,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", + "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap index ed94ed7a85..022a6d2cc5 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", + "schemaAwareHash": "c0f708d5078310a7416806cf545ed8820c88d554badbec4e6e928d37abd29813", "authorization": { "is_authenticated": false, "scopes": [], @@ -140,7 +140,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", + "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", "authorization": { "is_authenticated": false, "scopes": [], @@ -199,7 +199,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", + "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap index 08a9782c85..39b73eabbc 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "0e1644746fe4beab7def35ec8cc12bde39874c6bb8b9dfd928456196b814a111", + "schemaAwareHash": "18631e67bb0c6b514cb51e8dff155a2900c8000ad319ea4784e5ca8b1275aca2", "authorization": { "is_authenticated": false, "scopes": [], @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", + "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -201,7 +201,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", + "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap index ed94ed7a85..022a6d2cc5 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", + "schemaAwareHash": "c0f708d5078310a7416806cf545ed8820c88d554badbec4e6e928d37abd29813", "authorization": { "is_authenticated": false, "scopes": [], @@ -140,7 +140,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", + "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", "authorization": { "is_authenticated": false, "scopes": [], @@ -199,7 +199,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", + "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap index 08a9782c85..39b73eabbc 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "0e1644746fe4beab7def35ec8cc12bde39874c6bb8b9dfd928456196b814a111", + "schemaAwareHash": "18631e67bb0c6b514cb51e8dff155a2900c8000ad319ea4784e5ca8b1275aca2", "authorization": { "is_authenticated": false, "scopes": [], @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", + "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -201,7 +201,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", + "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap index fc5007829d..3c550e108a 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "ff18ff586aee784ec507117854cb4b64f9693d528df1ee69c922b5d75ae637fb", + "schemaAwareHash": "91232230ba1d2d64e49c123c862bf68cc9b8421982d1587422930cd084a7885f", "authorization": { "is_authenticated": false, "scopes": [], @@ -203,7 +203,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", + "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", "authorization": { "is_authenticated": false, "scopes": [], @@ -263,7 +263,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", + "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap index 4c219874d6..ec86110080 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "70b62e564b3924984694d90de2b10947a2f5c14ceb76d154f43bb3c638c4830b", + "schemaAwareHash": "65c1648beef44b81ac988224191b18ff469c641fd33032ef0c84165245018b62", "authorization": { "is_authenticated": false, "scopes": [], @@ -204,7 +204,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", + "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -265,7 +265,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", + "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap index 5cc97759df..f8db053949 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap @@ -145,7 +145,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "cb374f6eaa19cb529eeae258f2b136dbc751e3784fdc279954e59622cfb1edde", + "schemaAwareHash": "c6459a421e05dfd0685c8a3ca96f8ba413381ba918ff34877a3ccba742d5395c", "authorization": { "is_authenticated": false, "scopes": [], @@ -208,7 +208,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", + "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", "authorization": { "is_authenticated": false, "scopes": [], @@ -269,7 +269,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", + "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap index 593bd573f6..e4ba5927a3 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap @@ -145,7 +145,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "26ae1da614855e4edee344061c0fc95ec4613a99e012de1f33207cb5318487b8", + "schemaAwareHash": "f2466229a91f69cadfa844a20343b03668b7f85fd1310a4b20ba9382ffa2f5e7", "authorization": { "is_authenticated": false, "scopes": [], @@ -209,7 +209,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", + "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -271,7 +271,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", + "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap index 41a6433f9f..81a55e44ea 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap @@ -54,7 +54,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "schemaAwareHash": "71f58fcd5058b3dd354703d8c507d08beb7e775fc7b0b7540e750fdfac67a145", "authorization": { "is_authenticated": false, "scopes": [], @@ -115,7 +115,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "schemaAwareHash": "37d7682b38a2504e078153b82f718b2cd2b026ff8cf09d79a09f65649e898559", "authorization": { "is_authenticated": false, "scopes": [], @@ -174,7 +174,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "schemaAwareHash": "07d8ac06a1e487014cc8d8adb3fc8842373f23b8dc177fa79586364276fedd46", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap index c8fe1fb487..8ae0b59f67 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap @@ -54,7 +54,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "5201830580c9c5fadd9c59aea072878f84465c1ae9d905207fa281aa7c1d5340", + "schemaAwareHash": "cc52bb826d3c06b3ccbc421340fe3f49a81dc2b71dcb6a931a9a769745038e3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -116,7 +116,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "62ff891f6971184d3e42b98f8166be72027b5479f9ec098af460a48ea6f6cbf4", + "schemaAwareHash": "6e83e0a67b509381f1a0c2dfe84db92d0dd6bf4bb23fe4c97ccd3d871364c9f4", "authorization": { "is_authenticated": false, "scopes": [], @@ -176,7 +176,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7e6f6850777335eb1421a30a45f6888bb9e5d0acf8f55d576d55d1c4b7d23ec7", + "schemaAwareHash": "67834874c123139d942b140fb9ff00ed4e22df25228c3e758eeb44b28d3847eb", "authorization": { "is_authenticated": false, "scopes": [], From fd2974b920a895adb274f662ce2588e350184403 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:04:39 +0200 Subject: [PATCH 037/112] chore(deps): update dependency slack to v4.15.0 (#5681) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 126c85a25d..95fc0fc9a0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2.1 # across projects. See https://circleci.com/orbs/ for more information. orbs: gh: circleci/github-cli@2.3.0 - slack: circleci/slack@4.12.6 + slack: circleci/slack@4.15.0 secops: apollo/circleci-secops-orb@2.0.7 executors: From 81b3d1223bf47a612e529183a4ba46b9dfafc6a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:09:19 +0200 Subject: [PATCH 038/112] chore(deps): update dependency typescript to v5.7.2 (#5851) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../tracing/datadog-subgraph/package-lock.json | 8 ++++---- dockerfiles/tracing/datadog-subgraph/package.json | 2 +- .../tracing/jaeger-subgraph/package-lock.json | 14 +++++++------- dockerfiles/tracing/jaeger-subgraph/package.json | 2 +- .../tracing/zipkin-subgraph/package-lock.json | 14 +++++++------- dockerfiles/tracing/zipkin-subgraph/package.json | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/dockerfiles/tracing/datadog-subgraph/package-lock.json b/dockerfiles/tracing/datadog-subgraph/package-lock.json index 849552a1fd..6bfbc35f9f 100644 --- a/dockerfiles/tracing/datadog-subgraph/package-lock.json +++ b/dockerfiles/tracing/datadog-subgraph/package-lock.json @@ -16,7 +16,7 @@ "graphql": "^16.5.0" }, "devDependencies": { - "typescript": "5.5.3" + "typescript": "5.7.2" } }, "node_modules/@apollo/cache-control-types": { @@ -1768,9 +1768,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/dockerfiles/tracing/datadog-subgraph/package.json b/dockerfiles/tracing/datadog-subgraph/package.json index 0d38223e9d..58aa9aa002 100644 --- a/dockerfiles/tracing/datadog-subgraph/package.json +++ b/dockerfiles/tracing/datadog-subgraph/package.json @@ -18,6 +18,6 @@ "graphql": "^16.5.0" }, "devDependencies": { - "typescript": "5.5.3" + "typescript": "5.7.2" } } diff --git a/dockerfiles/tracing/jaeger-subgraph/package-lock.json b/dockerfiles/tracing/jaeger-subgraph/package-lock.json index 3687cb6aa4..639115bb69 100644 --- a/dockerfiles/tracing/jaeger-subgraph/package-lock.json +++ b/dockerfiles/tracing/jaeger-subgraph/package-lock.json @@ -18,7 +18,7 @@ "opentracing": "^0.14.7" }, "devDependencies": { - "typescript": "5.5.3" + "typescript": "5.7.2" } }, "node_modules/@apollo/cache-control-types": { @@ -1430,9 +1430,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2588,9 +2588,9 @@ } }, "typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true }, "unpipe": { diff --git a/dockerfiles/tracing/jaeger-subgraph/package.json b/dockerfiles/tracing/jaeger-subgraph/package.json index 2be04d726e..c5c24dfa63 100644 --- a/dockerfiles/tracing/jaeger-subgraph/package.json +++ b/dockerfiles/tracing/jaeger-subgraph/package.json @@ -19,6 +19,6 @@ "opentracing": "^0.14.7" }, "devDependencies": { - "typescript": "5.5.3" + "typescript": "5.7.2" } } diff --git a/dockerfiles/tracing/zipkin-subgraph/package-lock.json b/dockerfiles/tracing/zipkin-subgraph/package-lock.json index dbfad73245..c3dbbfe3e4 100644 --- a/dockerfiles/tracing/zipkin-subgraph/package-lock.json +++ b/dockerfiles/tracing/zipkin-subgraph/package-lock.json @@ -19,7 +19,7 @@ "zipkin-javascript-opentracing": "^3.0.0" }, "devDependencies": { - "typescript": "5.5.3" + "typescript": "5.7.2" } }, "node_modules/@apollo/cache-control-types": { @@ -1457,9 +1457,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2659,9 +2659,9 @@ } }, "typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true }, "unpipe": { diff --git a/dockerfiles/tracing/zipkin-subgraph/package.json b/dockerfiles/tracing/zipkin-subgraph/package.json index de7de6f96b..96688dcfa9 100644 --- a/dockerfiles/tracing/zipkin-subgraph/package.json +++ b/dockerfiles/tracing/zipkin-subgraph/package.json @@ -20,6 +20,6 @@ "zipkin-javascript-opentracing": "^3.0.0" }, "devDependencies": { - "typescript": "5.5.3" + "typescript": "5.7.2" } } From 92e684745ce82e6519428993898a46a7579f84d3 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 30 Nov 2024 09:27:17 +0200 Subject: [PATCH 039/112] tests: Fix snapshots after #6205 was merged (#6367) --- .../apollo_router__plugins__cache__tests__insert-5.snap | 4 ++-- .../apollo_router__plugins__cache__tests__insert.snap | 4 ++-- .../apollo_router__plugins__cache__tests__no_data-3.snap | 5 +++-- .../apollo_router__plugins__cache__tests__no_data.snap | 4 ++-- .../apollo_router__plugins__cache__tests__private-3.snap | 5 +++-- .../apollo_router__plugins__cache__tests__private-5.snap | 5 +++-- .../apollo_router__plugins__cache__tests__private.snap | 5 +++-- ...ontext__set_context_dependent_fetch_failure_rust_qp.snap | 4 ++-- .../set_context__set_context_list_of_lists_rust_qp.snap | 4 ++-- .../snapshots/set_context__set_context_list_rust_qp.snap | 4 ++-- .../set_context__set_context_no_typenames_rust_qp.snap | 4 ++-- .../tests/snapshots/set_context__set_context_rust_qp.snap | 4 ++-- .../set_context__set_context_type_mismatch_rust_qp.snap | 4 ++-- .../snapshots/set_context__set_context_union_rust_qp.snap | 6 +++--- ...ontext__set_context_unrelated_fetch_failure_rust_qp.snap | 6 +++--- .../set_context__set_context_with_null_rust_qp.snap | 4 ++-- .../type_conditions___test_type_conditions_disabled-2.snap | 4 ++-- .../type_conditions___test_type_conditions_enabled-2.snap | 6 +++--- ..._type_conditions_enabled_generate_query_fragments-2.snap | 6 +++--- ...tions___test_type_conditions_enabled_list_of_list-2.snap | 6 +++--- ...test_type_conditions_enabled_list_of_list_of_list-2.snap | 6 +++--- ...pe_conditions_enabled_shouldnt_make_article_fetch-2.snap | 6 +++--- 22 files changed, 55 insertions(+), 51 deletions(-) diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap index dd1ee738c2..19faf7f003 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap @@ -4,12 +4,12 @@ expression: cache_keys --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "public" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "public" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap index 1375808b78..cffe19303d 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap @@ -4,12 +4,12 @@ expression: cache_keys --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "public" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "public" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap index 1322b59275..473208cb48 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap @@ -1,15 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys +snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "[REDACTED]" }, { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "[REDACTED]" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap index 87c750131f..1c84fc3dab 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap @@ -4,12 +4,12 @@ expression: cache_keys --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "[REDACTED]" }, { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "[REDACTED]" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap index c9839a8823..3d98d643ef 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap @@ -1,15 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys +snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap index c9839a8823..3d98d643ef 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap @@ -1,15 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys +snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap index 69027e4644..afe8ae3850 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap @@ -1,15 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys +snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "private" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "new", "cache_control": "private" } diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap index 5caea23420..d8de0b56a4 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap @@ -43,7 +43,7 @@ expression: response "operationKind": "query", "operationName": "Query_fetch_dependent_failure__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "6b659295c8e5aff7b3d7146b878e848b43ad58fba3f4dfce2988530631c3448a", + "schemaAwareHash": "a34fdce551a4d3b8e3d747620c2389d09e60b3b301afc8c959bd8ff37b2799c8", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -89,7 +89,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "3bc84712c95d01c4e9118cc1f8179e071662862a04cef56d39a0ac6a621daf36", + "schemaAwareHash": "d0c5e77f5153414f81fe851685f9df9a2f0f3b260929ebe252844fdb83df2aa0", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap index 0945ccf058..c28ed4e1cb 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap @@ -44,7 +44,7 @@ expression: response "operationKind": "query", "operationName": "QueryLL__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "0a6255094b34a44c5addf88a5a9bb37847f19ecf10370be675ba55a1330b4ac7", + "schemaAwareHash": "99adc22aaf3a356059916f2e81cb475d1ab0f35c618aec188365315ec7c1b190", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -90,7 +90,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "71e6d73b679197d0e979c07446c670bad69897d77bd280dc9c39276fde6e8d99", + "schemaAwareHash": "52bf35c65bc562f6b55c008311e28307b05eec4cd9f6ee0cbd9e375ac361d14e", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap index ab643923c0..38f083a2c5 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap @@ -40,7 +40,7 @@ expression: response "operationKind": "query", "operationName": "set_context_list_rust_qp__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "fd215e24828b3a7296abe901f843f68b525d8eaf35a019ac34a2198738c91230", + "schemaAwareHash": "08ca351ae6b0b7073323e2fc82b39d4d7e36f85b0fedb9a39b3123fb8b346138", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -86,7 +86,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "96a8bf16a86cbddab93bb46364f8b0e63635a928924dcb681dc2371b810eee02", + "schemaAwareHash": "93cbeb9112a7d101127234e0874bc243d5e28833d20cdbee65479647d9ce408e", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap index 6a47496428..cd75b198c4 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap @@ -32,7 +32,7 @@ expression: response "operationKind": "query", "operationName": "set_context_no_typenames_rust_qp__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "9c1d7c67821fc43d63e8a217417fbe600a9100e1a43ba50e2f961d4fd4974144", + "schemaAwareHash": "ffb1965aacb1975285b1391e5c36540e193c3ff181e9afbc60ab6acd770351c9", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -78,7 +78,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "5fdc56a38428bad98d0c5e46f096c0179886815509ffc1918f5c6e0a784e2547", + "schemaAwareHash": "2295ba6079ae261664bd0b31d88aea65365eb43f390be2d407f298e00305b448", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap index fecc966894..f114331c67 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap @@ -34,7 +34,7 @@ expression: response "operationKind": "query", "operationName": "set_context_rust_qp__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "7fb5b477b89d2dcf76239dd30abcf6210462e144376a6b1b589ceb603edd55cd", + "schemaAwareHash": "1bf26b6306e97cbce50243fe3de6103ca3aca7338f893c0eddeb61e03f3f102f", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -80,7 +80,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "fef499e9ca815057242c5a03e9f0960d5c50d6958b0ac7329fc23b5a6e714eab", + "schemaAwareHash": "894a9777780202b6851ce5c7dda110647c9a688dbca7fd0ae0464ecfe2003b74", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap index 516c42f5fe..c371ae6c6b 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap @@ -32,7 +32,7 @@ expression: response "operationKind": "query", "operationName": "Query_type_mismatch__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "29f5e6a254fac05382ddc3e4aac47368dc9847abe711ecf17dbfca7945097faf", + "schemaAwareHash": "7235a2342601274677b87b87b9772b51e7a38f4ec96309a82f6669cd3f185d12", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -78,7 +78,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "864f2eecd06e2c450e48f2cb551d4e95946575eb7e537a17a04c9a1716c0a482", + "schemaAwareHash": "0d0587044ff993407d5657e09fe5a5ded13a6db70ce5733062e90dee6c610cd4", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap index 74e41f53c9..0040d7e8b0 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap @@ -31,7 +31,7 @@ expression: response "operationKind": "query", "operationName": "QueryUnion__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "eae4d791b0314c4e2509735ad3d0dd0ca5de8ee4c7f315513931df6e4cb5102d", + "schemaAwareHash": "e2ac947e685882790c24243e1312a112923780c6dc9cb18e587f55e1728c5a18", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -89,7 +89,7 @@ expression: response "typeCondition": "V" } ], - "schemaAwareHash": "b1ba6dd8a0e2edc415efd084401bfa01ecbaaa76a0f7896c27c431bed8c20a08", + "schemaAwareHash": "a4707e1dbe9c5c20130adc188662691c3d601a2f1ff6ddd499301f45c62f009c", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_1" @@ -151,7 +151,7 @@ expression: response "typeCondition": "V" } ], - "schemaAwareHash": "45785998d1610758abe68519f9bc100828afa2ba56c7e55b9d18ad69f3ad27eb", + "schemaAwareHash": "a4d03082699251d18ad67b92c96a9c33f0c70af8713d10f1b40514cc6b369e33", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_1" diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap index 8194cfc237..5effdbec4d 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap @@ -38,7 +38,7 @@ snapshot_kind: text "operationKind": "query", "operationName": "Query_fetch_failure__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "d3f1ad875170d008059583ca6074e732a178f74474ac31de3bb4397c5080020d", + "schemaAwareHash": "cb2490c26be37beda781fc72a6632c104b5e5c57cfabb5061c7f1429582382d9", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -87,7 +87,7 @@ snapshot_kind: text "typeCondition": "U" } ], - "schemaAwareHash": "05dc59a826cec26ad8263101508c298dd8d31d79d36f18194dd4cf8cd5f02dc3", + "schemaAwareHash": "6679327c6be3db6836531c0d50d583ad66b151113a833f666dcb9ea14af4c807", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" @@ -130,7 +130,7 @@ snapshot_kind: text "typeCondition": "U" } ], - "schemaAwareHash": "a3c7e6df6f9c93b228f16a937b7159ccf1294fec50a92f60ba004dbebbb64b50", + "schemaAwareHash": "cde44282bd7d30b098be84c3f00790e23bbdd635f44454abd7c22b283adba034", "serviceName": "Subgraph2", "variableUsages": [] }, diff --git a/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap index 6f775414e3..8614668d4c 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap @@ -29,7 +29,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "4fc423a49bbddcc8869c014934dfd128dd61a1760c4eb619940ad46f614c843b", + "schemaAwareHash": "232e6f39e34056c6f8add0806d1dd6e2c6219fc9d451182da0188bd0348163d8", "authorization": { "is_authenticated": false, "scopes": [], @@ -81,7 +81,7 @@ expression: response "renameKeyTo": "contextualArgument_1_0" } ], - "schemaAwareHash": "d863b0ef9ef616faaade4c73b2599395e074ec1521ec07634471894145e97f44", + "schemaAwareHash": "9973ece3eaba0503afb6325c9f7afcfb007b8ef43b55ded3c1bf5be39c89d301", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap index 18cc3a2fb3..69a3e9e530 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "c0f708d5078310a7416806cf545ed8820c88d554badbec4e6e928d37abd29813", + "schemaAwareHash": "08dd5362fc4c5c7e878861f83452d62b7281b556a214a4470940cd31d78254c8", "authorization": { "is_authenticated": false, "scopes": [], @@ -137,7 +137,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7f9d59b380b10c77c3080665549c5f835bf1721dae62b60bc9d6dd39975d9583", + "schemaAwareHash": "f31b2449c96808b927a60495a2f0d50233f41965d72af96b49f8759f101c1184", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap index 022a6d2cc5..67a3ceb6f7 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "c0f708d5078310a7416806cf545ed8820c88d554badbec4e6e928d37abd29813", + "schemaAwareHash": "08dd5362fc4c5c7e878861f83452d62b7281b556a214a4470940cd31d78254c8", "authorization": { "is_authenticated": false, "scopes": [], @@ -140,7 +140,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", + "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -199,7 +199,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", + "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap index 022a6d2cc5..67a3ceb6f7 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "c0f708d5078310a7416806cf545ed8820c88d554badbec4e6e928d37abd29813", + "schemaAwareHash": "08dd5362fc4c5c7e878861f83452d62b7281b556a214a4470940cd31d78254c8", "authorization": { "is_authenticated": false, "scopes": [], @@ -140,7 +140,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", + "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -199,7 +199,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", + "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap index 3c550e108a..a0d484f417 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "91232230ba1d2d64e49c123c862bf68cc9b8421982d1587422930cd084a7885f", + "schemaAwareHash": "61632fc2833efa5fa0c26669f3b8f33e9d872b21d819c0e6749337743de088ed", "authorization": { "is_authenticated": false, "scopes": [], @@ -203,7 +203,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", + "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -263,7 +263,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", + "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap index f8db053949..45dfb6808f 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap @@ -145,7 +145,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "c6459a421e05dfd0685c8a3ca96f8ba413381ba918ff34877a3ccba742d5395c", + "schemaAwareHash": "a5ca7a9623bbc93a0f7d1614e865a98cf376c8084fb2868702bdc38aed9bcde2", "authorization": { "is_authenticated": false, "scopes": [], @@ -208,7 +208,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "b45a44dd08f77faed796ca4dccee129f692b920f3ef98ff718ea5f30207887ea", + "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", "authorization": { "is_authenticated": false, "scopes": [], @@ -269,7 +269,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "8c0700aefc3fb87461c461587656dbbe0489e09fff0ecb03f987df1f81a84b2b", + "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap index 81a55e44ea..5bebfc6359 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap @@ -54,7 +54,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "71f58fcd5058b3dd354703d8c507d08beb7e775fc7b0b7540e750fdfac67a145", + "schemaAwareHash": "130217826766119286dfcc6c95317df332634acf49fcdc5408c6c4d1b0669517", "authorization": { "is_authenticated": false, "scopes": [], @@ -115,7 +115,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "37d7682b38a2504e078153b82f718b2cd2b026ff8cf09d79a09f65649e898559", + "schemaAwareHash": "a331620be2259b8a89122f1c41feb3abdfbd782ebbc2a1e17e3de128c4bb5cb5", "authorization": { "is_authenticated": false, "scopes": [], @@ -174,7 +174,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "07d8ac06a1e487014cc8d8adb3fc8842373f23b8dc177fa79586364276fedd46", + "schemaAwareHash": "115c6ba516a8368e9b331769942157a1e438d3f79b60690de68378da16bde989", "authorization": { "is_authenticated": false, "scopes": [], From 900f305c2ae627e56e4a98ce3ec127b966693145 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 10:42:35 +0200 Subject: [PATCH 040/112] chore(deps): update cimg/redis docker tag to v7.4.1 (#6364) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 95fc0fc9a0..afd2dce7e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ executors: amd_linux_test: &amd_linux_test_executor docker: - image: cimg/base:stable - - image: cimg/redis:7.2.4 + - image: cimg/redis:7.4.1 - image: jaegertracing/all-in-one:1.54.0 - image: openzipkin/zipkin:2.23.2 - image: ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:v1.17.0 From cca0cd43d3101b9c959264bda30d7e572deb79b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 10:44:17 +0200 Subject: [PATCH 041/112] chore(deps): update dependency gh to v2.6.0 (#6365) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index afd2dce7e9..af215b7226 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ version: 2.1 # These "CircleCI Orbs" are reusable bits of configuration that can be shared # across projects. See https://circleci.com/orbs/ for more information. orbs: - gh: circleci/github-cli@2.3.0 + gh: circleci/github-cli@2.6.0 slack: circleci/slack@4.15.0 secops: apollo/circleci-secops-orb@2.0.7 From c8124eb41a2e28b62a8b4b02ad918b6a1714e094 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 10:49:06 +0200 Subject: [PATCH 042/112] chore(deps): update actions/checkout digest to 11bd719 (#6363) --- .github/workflows/update_apollo_protobuf.yaml | 2 +- .github/workflows/update_uplink_schema.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update_apollo_protobuf.yaml b/.github/workflows/update_apollo_protobuf.yaml index cdb6aa84b4..507413cc41 100644 --- a/.github/workflows/update_apollo_protobuf.yaml +++ b/.github/workflows/update_apollo_protobuf.yaml @@ -9,7 +9,7 @@ jobs: Update-Protobuf-Schema: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Make changes to pull request run: | curl -f https://usage-reporting.api.apollographql.com/proto/reports.proto > ./apollo-router/src/plugins/telemetry/proto/reports.proto diff --git a/.github/workflows/update_uplink_schema.yml b/.github/workflows/update_uplink_schema.yml index dd89b1ecdb..a27761f758 100644 --- a/.github/workflows/update_uplink_schema.yml +++ b/.github/workflows/update_uplink_schema.yml @@ -9,7 +9,7 @@ jobs: Update-Uplink-Schema: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Install Rover run: | curl -sSL https://rover.apollo.dev/nix/v0.14.1 | sh From 7c29b84cdd8bcfbf49983c8e922dbfb9cd1c9dec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:27:12 +0200 Subject: [PATCH 043/112] chore(deps): update peter-evans/create-pull-request action to v7 (#6366) --- .github/workflows/update_apollo_protobuf.yaml | 2 +- .github/workflows/update_uplink_schema.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update_apollo_protobuf.yaml b/.github/workflows/update_apollo_protobuf.yaml index 507413cc41..fc9659c1b8 100644 --- a/.github/workflows/update_apollo_protobuf.yaml +++ b/.github/workflows/update_apollo_protobuf.yaml @@ -15,7 +15,7 @@ jobs: curl -f https://usage-reporting.api.apollographql.com/proto/reports.proto > ./apollo-router/src/plugins/telemetry/proto/reports.proto - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v7 with: commit-message: Update Apollo Protobuf schema committer: GitHub diff --git a/.github/workflows/update_uplink_schema.yml b/.github/workflows/update_uplink_schema.yml index a27761f758..2b80c65946 100644 --- a/.github/workflows/update_uplink_schema.yml +++ b/.github/workflows/update_uplink_schema.yml @@ -19,7 +19,7 @@ jobs: rover graph introspect https://uplink.api.apollographql.com/ | perl -pe 'chomp if eof' > ./apollo-router/src/uplink/uplink.graphql - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v7 with: commit-message: Update Uplink schema committer: GitHub From 7eb9b0b52af483d3b56ded11b82c49d169a7a37e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:00:58 +0200 Subject: [PATCH 044/112] chore(deps): update openzipkin/zipkin docker tag to v3.4.2 (#6371) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index af215b7226..0ee71dfa04 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,7 +26,7 @@ executors: - image: cimg/base:stable - image: cimg/redis:7.4.1 - image: jaegertracing/all-in-one:1.54.0 - - image: openzipkin/zipkin:2.23.2 + - image: openzipkin/zipkin:3.4.2 - image: ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:v1.17.0 resource_class: xlarge environment: diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index a84a7f1fcf..c2b4b5c25c 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -36,6 +36,6 @@ services: zipkin: container_name: zipkin - image: openzipkin/zipkin:3.0.6 + image: openzipkin/zipkin:3.4.2 ports: - 9411:9411 From 268aa99f83dcdacd73d349be704c25afd48f4510 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Mon, 2 Dec 2024 13:02:05 +0100 Subject: [PATCH 045/112] plugins/fleet_detector: add instance gauge --- Cargo.lock | 267 +++++++++++++++----- apollo-router/Cargo.toml | 1 + apollo-router/src/plugins/fleet_detector.rs | 41 +++ 3 files changed, 251 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bda4735e66..1900f9de91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -176,6 +191,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "apollo-environment-detector" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c628346f10c7615f1dd9e3f486d55bcad9edb667f4444dcbcb9cb5943815583a" +dependencies = [ + "libc", + "serde", + "wmi", +] + [[package]] name = "apollo-federation" version = "1.57.1" @@ -236,6 +262,7 @@ dependencies = [ "ahash", "anyhow", "apollo-compiler", + "apollo-environment-detector", "apollo-federation", "arc-swap", "async-channel 1.9.0", @@ -651,7 +678,7 @@ dependencies = [ "proc-macro2", "quote", "strum 0.25.0", - "syn 2.0.76", + "syn 2.0.90", "thiserror", ] @@ -819,7 +846,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -836,7 +863,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -1410,7 +1437,7 @@ dependencies = [ "proc-macro2", "quote", "str_inflector", - "syn 2.0.76", + "syn 2.0.90", "thiserror", "try_match", ] @@ -1517,6 +1544,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "ci_info" version = "0.14.14" @@ -1586,7 +1626,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -1926,7 +1966,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -1937,7 +1977,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -1997,7 +2037,7 @@ checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" dependencies = [ "deno-proc-macro-rules-macros", "proc-macro2", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2009,7 +2049,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2064,7 +2104,7 @@ dependencies = [ "strum 0.25.0", "strum_macros 0.25.3", "syn 1.0.109", - "syn 2.0.76", + "syn 2.0.90", "thiserror", ] @@ -2145,7 +2185,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2158,7 +2198,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.0", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2249,7 +2289,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2355,7 +2395,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2629,7 +2669,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2797,7 +2837,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -2896,7 +2936,7 @@ checksum = "b0e085ded9f1267c32176b40921b9754c474f7dd96f7e808d4a982e48aa1e854" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -3446,6 +3486,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3823,9 +3886,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libfuzzer-sys" @@ -3933,7 +3996,7 @@ checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -4122,7 +4185,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -4792,7 +4855,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -4835,7 +4898,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -4913,7 +4976,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -5022,9 +5085,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -5124,7 +5187,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -5522,7 +5585,7 @@ checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -5654,7 +5717,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.76", + "syn 2.0.90", "walkdir", ] @@ -5830,7 +5893,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -5915,9 +5978,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -5933,13 +5996,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -5950,7 +6013,7 @@ checksum = "afb2522c2a87137bf6c2b3493127fed12877ef1b9476f074d6664edc98acd8a7" dependencies = [ "quote", "regex", - "syn 2.0.76", + "syn 2.0.90", "thiserror", ] @@ -5962,7 +6025,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6084,7 +6147,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6311,7 +6374,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6324,7 +6387,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6358,9 +6421,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.76" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -6394,7 +6457,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows", + "windows 0.57.0", ] [[package]] @@ -6473,7 +6536,7 @@ checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6508,7 +6571,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6686,7 +6749,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -6955,7 +7018,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -7067,7 +7130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -7103,7 +7166,7 @@ checksum = "b0a91713132798caecb23c977488945566875e7b61b902fb111979871cbff34e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] @@ -7446,7 +7509,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -7480,7 +7543,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7575,7 +7638,26 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core", + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ "windows-targets 0.52.6", ] @@ -7585,9 +7667,22 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", "windows-targets 0.52.6", ] @@ -7599,7 +7694,18 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -7610,7 +7716,18 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -7622,6 +7739,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7877,6 +8013,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "wmi" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70df482bbec7017ce4132154233642de658000b24b805345572036782a66ad55" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror", + "windows 0.58.0", + "windows-core 0.58.0", +] + [[package]] name = "wsl" version = "0.1.0" @@ -7921,7 +8072,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.90", ] [[package]] diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index b69b9d7fc8..e5a167bb49 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -273,6 +273,7 @@ bytesize = { version = "1.3.0", features = ["serde"] } ahash = "0.8.11" itoa = "1.0.9" ryu = "1.0.15" +apollo-environment-detector = "0.1.0" [target.'cfg(macos)'.dependencies] uname = "0.1.1" diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 70786f1795..1725e3fb4e 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -1,5 +1,6 @@ use std::env; use std::env::consts::ARCH; +use std::env::consts::OS; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -65,7 +66,37 @@ impl GaugeStore { fn active() -> GaugeStore { let system_getter = Arc::new(Mutex::new(SystemGetter::new())); let meter = meter_provider().meter("apollo/router"); + let mut gauges = Vec::new(); + { + let mut attributes = Vec::new(); + // CPU architecture + attributes.push(KeyValue::new("host.arch", get_otel_arch())); + // Operating System + attributes.push(KeyValue::new("os.type", get_otel_os())); + if OS == "linux" { + attributes.push(KeyValue::new( + "linux.distribution", + System::distribution_id(), + )); + } + // Compute Environment + if let Some(env) = apollo_environment_detector::detect_one(24576) { + attributes.push(KeyValue::new("cloud.platform", env.platform_code())); + if let Some(cloud_provider) = env.cloud_provider() { + attributes.push(KeyValue::new("cloud.provider", cloud_provider.code())); + } + } + gauges.push( + meter + .u64_observable_gauge("apollo.router.instance") + .with_description("The number of instances the router is running on") + .with_callback(move |i| { + i.observe(1, &attributes); + }) + .init(), + ); + } { let system_getter = system_getter.clone(); gauges.push( @@ -247,4 +278,14 @@ fn get_otel_arch() -> &'static str { } } +fn get_otel_os() -> &'static str { + match OS { + "apple" => "darwin", + "dragonfly" => "dragonflybsd", + "macos" => "darwin", + "ios" => "darwin", + a => a, + } +} + register_private_plugin!("apollo", "fleet_detector", FleetDetector); From 53da9c86e8fda774162722596524636bb8e03650 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Mon, 2 Dec 2024 13:50:36 +0100 Subject: [PATCH 046/112] chore(fleet_detector): store magic number as const --- apollo-router/src/plugins/fleet_detector.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 1725e3fb4e..f95a520a67 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -22,6 +22,7 @@ use crate::plugin::PluginInit; use crate::plugin::PluginPrivate; const REFRESH_INTERVAL: Duration = Duration::from_secs(60); +const COMPUTE_DETECTOR_THRESHOLD: u16 = 24576; #[derive(Debug, Default, Deserialize, JsonSchema)] struct Conf {} @@ -81,7 +82,7 @@ impl GaugeStore { )); } // Compute Environment - if let Some(env) = apollo_environment_detector::detect_one(24576) { + if let Some(env) = apollo_environment_detector::detect_one(COMPUTE_DETECTOR_THRESHOLD) { attributes.push(KeyValue::new("cloud.platform", env.platform_code())); if let Some(cloud_provider) = env.cloud_provider() { attributes.push(KeyValue::new("cloud.provider", cloud_provider.code())); From 8f9c9754c6c7da82cc15a31e18df83b9fd676620 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Mon, 2 Dec 2024 13:06:39 +0000 Subject: [PATCH 047/112] FLEET-19 Update CHANGELOG --- .changesets/feat_jr_add_fleet_awareness_plugin.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.changesets/feat_jr_add_fleet_awareness_plugin.md b/.changesets/feat_jr_add_fleet_awareness_plugin.md index a39fbca968..da6bc19375 100644 --- a/.changesets/feat_jr_add_fleet_awareness_plugin.md +++ b/.changesets/feat_jr_add_fleet_awareness_plugin.md @@ -1,7 +1,9 @@ ### Adds Fleet Awareness Plugin -Adds a new plugin that reports telemetry to Apollo on the configuration and deployment of the Router. Initially only -reports, memory and CPU usage but will be expanded to cover other non-intrusive measures in future. 🚀 +Adds a new plugin that reports telemetry to Apollo on the configuration and deployment of the Router. Initially this +covers CPU & Memory usage, CPU Frequency, and other deployment characteristics such as Operating System, and Cloud +Provider. For more details, along with a full list of data captured and how to opt out, please see our guidance +[here](https://www.apollographql.com/docs/graphos/reference/data-privacy). As part of the above PluginPrivate has been extended with a new `activate` hook which is guaranteed to be called once the OTEL meter has been refreshed. This ensures that code, particularly that concerned with gauges, will survive a hot From f64080569955591293333865a44fd0f43245801a Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Mon, 2 Dec 2024 13:38:18 +0000 Subject: [PATCH 048/112] FLEET-19 Fix environment variable gate Clap's default behaviour was causing this check to fall over. --- apollo-router/src/plugins/fleet_detector.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index f95a520a67..5e3d9c7513 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -172,9 +172,11 @@ impl PluginPrivate for FleetDetector { async fn new(_: PluginInit) -> Result { debug!("initialising fleet detection plugin"); - if env::var(APOLLO_TELEMETRY_DISABLED).is_ok() { - debug!("fleet detection disabled, no telemetry will be sent"); - return Ok(FleetDetector::default()); + if let Ok(val) = env::var(APOLLO_TELEMETRY_DISABLED) { + if val == "true" { + debug!("fleet detection disabled, no telemetry will be sent"); + return Ok(FleetDetector::default()); + } } Ok(FleetDetector { From f2ee42b6c04738519ffcfb28e367bf7ef3cbb0e4 Mon Sep 17 00:00:00 2001 From: jonathanrainer Date: Mon, 2 Dec 2024 14:04:34 +0000 Subject: [PATCH 049/112] FLEET-19 Update deny list for public metrics --- apollo-router/src/metrics/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/metrics/filter.rs b/apollo-router/src/metrics/filter.rs index 529b25883a..59bd991966 100644 --- a/apollo-router/src/metrics/filter.rs +++ b/apollo-router/src/metrics/filter.rs @@ -105,7 +105,7 @@ impl FilterMeterProvider { FilterMeterProvider::builder() .delegate(delegate) .deny( - Regex::new(r"apollo\.router\.(config|entities)(\..*|$)") + Regex::new(r"apollo\.router\.(config|entities|instance)(\..*|$)") .expect("regex should have been valid"), ) .build() From a9e83aadbd4031cf9819de086786a099e98cccb0 Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Tue, 3 Dec 2024 11:51:55 +0100 Subject: [PATCH 050/112] add new selector for retry (#6351) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- .changesets/fix_bnjjj_fix_retry_metric.md | 6 + ...nfiguration__tests__schema_generation.snap | 34 ++- .../telemetry/config_new/attributes.rs | 80 ++++--- .../telemetry/config_new/cache/attributes.rs | 10 +- .../plugins/telemetry/config_new/cache/mod.rs | 2 +- .../plugins/telemetry/config_new/cost/mod.rs | 32 ++- .../plugins/telemetry/config_new/events.rs | 52 +++-- .../telemetry/config_new/extendable.rs | 17 +- .../config_new/graphql/attributes.rs | 12 +- .../telemetry/config_new/graphql/mod.rs | 3 + .../telemetry/config_new/instruments.rs | 155 +++++++------ .../src/plugins/telemetry/config_new/mod.rs | 12 +- .../plugins/telemetry/config_new/selectors.rs | 56 +++++ .../plugins/telemetry/metrics/apollo/mod.rs | 3 +- apollo-router/src/plugins/telemetry/mod.rs | 1 + apollo-router/src/plugins/test.rs | 4 +- .../src/plugins/traffic_shaping/mod.rs | 1 - .../src/plugins/traffic_shaping/retry.rs | 204 ++++++++++++++++-- apollo-router/src/services/http/service.rs | 1 + .../telemetry/instrumentation/selectors.mdx | 1 + .../instrumentation/standard-attributes.mdx | 1 + .../instrumentation/standard-instruments.mdx | 39 +++- 22 files changed, 542 insertions(+), 184 deletions(-) create mode 100644 .changesets/fix_bnjjj_fix_retry_metric.md diff --git a/.changesets/fix_bnjjj_fix_retry_metric.md b/.changesets/fix_bnjjj_fix_retry_metric.md new file mode 100644 index 0000000000..eaaa220e80 --- /dev/null +++ b/.changesets/fix_bnjjj_fix_retry_metric.md @@ -0,0 +1,6 @@ +### Fix and test experimental_retry ([PR #6338](https://github.com/apollographql/router/pull/6338)) + +Fix the behavior of `experimental_retry` and make sure both the feature and metrics are working. +An entry in the context was also added, which would be useful later to implement a new standard attribute and selector for advanced telemetry. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6338 \ No newline at end of file diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index a7a0bc8d08..6dd57e693f 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -1,7 +1,6 @@ --- source: apollo-router/src/configuration/tests.rs expression: "&schema" -snapshot_kind: text --- { "$schema": "http://json-schema.org/draft-07/schema#", @@ -5744,6 +5743,11 @@ snapshot_kind: text "SubgraphAttributes": { "additionalProperties": false, "properties": { + "http.request.resend_count": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true + }, "subgraph.graphql.document": { "$ref": "#/definitions/StandardAttribute", "description": "#/definitions/StandardAttribute", @@ -6238,6 +6242,24 @@ snapshot_kind: text ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "default": { + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue", + "nullable": true + }, + "subgraph_resend_count": { + "description": "The subgraph http resend count", + "type": "boolean" + } + }, + "required": [ + "subgraph_resend_count" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -7756,6 +7778,11 @@ snapshot_kind: text "description": "#/definitions/conditional_attribute_apollo_router::plugins::telemetry::config_new::selectors::SubgraphSelector" }, "properties": { + "http.request.resend_count": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true + }, "subgraph.graphql.document": { "$ref": "#/definitions/StandardAttribute", "description": "#/definitions/StandardAttribute", @@ -7785,6 +7812,11 @@ snapshot_kind: text "description": "#/definitions/SubgraphSelector" }, "properties": { + "http.request.resend_count": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true + }, "subgraph.graphql.document": { "$ref": "#/definitions/StandardAttribute", "description": "#/definitions/StandardAttribute", diff --git a/apollo-router/src/plugins/telemetry/config_new/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/attributes.rs index 53640cd87b..0da52d42f2 100644 --- a/apollo-router/src/plugins/telemetry/config_new/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/attributes.rs @@ -47,10 +47,12 @@ use crate::plugins::telemetry::otlp::TelemetryDataKind; use crate::services::router; use crate::services::router::Request; use crate::services::subgraph; +use crate::services::subgraph::SubgraphRequestId; use crate::services::supergraph; use crate::Context; pub(crate) const SUBGRAPH_NAME: Key = Key::from_static_str("subgraph.name"); +pub(crate) const HTTP_REQUEST_RESEND_COUNT: Key = Key::from_static_str("http.request.resend_count"); pub(crate) const SUBGRAPH_GRAPHQL_DOCUMENT: Key = Key::from_static_str("subgraph.graphql.document"); pub(crate) const SUBGRAPH_GRAPHQL_OPERATION_NAME: Key = Key::from_static_str("subgraph.graphql.operation.name"); @@ -231,6 +233,10 @@ pub(crate) struct SubgraphAttributes { /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.operation.type")] graphql_operation_type: Option, + + /// The number of times the request has been resent + #[serde(rename = "http.request.resend_count")] + http_request_resend_count: Option, } impl DefaultForLevel for SubgraphAttributes { @@ -596,11 +602,7 @@ impl DefaultForLevel for HttpServerAttributes { } } -impl Selectors for RouterAttributes { - type Request = router::Request; - type Response = router::Response; - type EventResponse = (); - +impl Selectors for RouterAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = self.common.on_request(request); attrs.extend(self.server.on_request(request)); @@ -647,11 +649,7 @@ impl Selectors for RouterAttributes { } } -impl Selectors for HttpCommonAttributes { - type Request = router::Request; - type Response = router::Response; - type EventResponse = (); - +impl Selectors for HttpCommonAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = Vec::new(); if let Some(key) = self @@ -801,11 +799,7 @@ impl Selectors for HttpCommonAttributes { } } -impl Selectors for HttpServerAttributes { - type Request = router::Request; - type Response = router::Response; - type EventResponse = (); - +impl Selectors for HttpServerAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = Vec::new(); if let Some(key) = self.http_route.as_ref().and_then(|a| a.key(HTTP_ROUTE)) { @@ -1003,11 +997,9 @@ impl HttpServerAttributes { } } -impl Selectors for SupergraphAttributes { - type Request = supergraph::Request; - type Response = supergraph::Response; - type EventResponse = crate::graphql::Response; - +impl Selectors + for SupergraphAttributes +{ fn on_request(&self, request: &supergraph::Request) -> Vec { let mut attrs = Vec::new(); if let Some(key) = self @@ -1057,7 +1049,7 @@ impl Selectors for SupergraphAttributes { fn on_response_event( &self, - response: &Self::EventResponse, + response: &crate::graphql::Response, context: &Context, ) -> Vec { let mut attrs = Vec::new(); @@ -1070,11 +1062,7 @@ impl Selectors for SupergraphAttributes { } } -impl Selectors for SubgraphAttributes { - type Request = subgraph::Request; - type Response = subgraph::Response; - type EventResponse = (); - +impl Selectors for SubgraphAttributes { fn on_request(&self, request: &subgraph::Request) -> Vec { let mut attrs = Vec::new(); if let Some(key) = self @@ -1122,8 +1110,24 @@ impl Selectors for SubgraphAttributes { attrs } - fn on_response(&self, _response: &subgraph::Response) -> Vec { - Vec::default() + fn on_response(&self, response: &subgraph::Response) -> Vec { + let mut attrs = Vec::new(); + if let Some(key) = self + .http_request_resend_count + .as_ref() + .and_then(|a| a.key(HTTP_REQUEST_RESEND_COUNT)) + { + if let Some(resend_count) = response + .context + .get::<_, usize>(SubgraphRequestResendCountKey::new(&response.id)) + .ok() + .flatten() + { + attrs.push(KeyValue::new(key, resend_count as i64)); + } + } + + attrs } fn on_error(&self, _error: &BoxError, _ctx: &Context) -> Vec { @@ -1131,6 +1135,26 @@ impl Selectors for SubgraphAttributes { } } +/// Key used in context to save number of retries for a subgraph http request +pub(crate) struct SubgraphRequestResendCountKey<'a> { + subgraph_req_id: &'a SubgraphRequestId, +} + +impl<'a> SubgraphRequestResendCountKey<'a> { + pub(crate) fn new(subgraph_req_id: &'a SubgraphRequestId) -> Self { + Self { subgraph_req_id } + } +} + +impl<'a> From> for String { + fn from(value: SubgraphRequestResendCountKey) -> Self { + format!( + "apollo::telemetry::http_request_resend_count_{}", + value.subgraph_req_id + ) + } +} + #[cfg(test)] mod test { use std::net::SocketAddr; diff --git a/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs index 00d0b4b240..1d3085ffd1 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs @@ -36,16 +36,12 @@ impl DefaultForLevel for CacheAttributes { // Nothing to do here because we're using a trick because entity_type is related to CacheControl data we put in the context and for one request we have several entity types // and so several metrics to generate it can't be done here -impl Selectors for CacheAttributes { - type Request = subgraph::Request; - type Response = subgraph::Response; - type EventResponse = (); - - fn on_request(&self, _request: &Self::Request) -> Vec { +impl Selectors for CacheAttributes { + fn on_request(&self, _request: &subgraph::Request) -> Vec { Vec::default() } - fn on_response(&self, _response: &Self::Response) -> Vec { + fn on_response(&self, _response: &subgraph::Response) -> Vec { Vec::default() } diff --git a/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs index 7000278edc..894d938c1f 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs @@ -49,7 +49,7 @@ impl DefaultForLevel for CacheInstrumentsConfig { pub(crate) struct CacheInstruments { pub(crate) cache_hit: Option< - CustomCounter, + CustomCounter, >, } diff --git a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs index 8341790eac..a3fad1f93e 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::marker::PhantomData; use std::sync::Arc; use opentelemetry::metrics::MeterProvider; @@ -12,6 +13,7 @@ use tower::BoxError; use super::attributes::StandardAttribute; use super::instruments::Increment; use super::instruments::StaticInstrument; +use crate::graphql; use crate::metrics; use crate::plugins::demand_control::COST_ACTUAL_KEY; use crate::plugins::demand_control::COST_DELTA_KEY; @@ -59,16 +61,14 @@ pub(crate) struct SupergraphCostAttributes { cost_result: Option, } -impl Selectors for SupergraphCostAttributes { - type Request = supergraph::Request; - type Response = supergraph::Response; - type EventResponse = crate::graphql::Response; - - fn on_request(&self, _request: &Self::Request) -> Vec { +impl Selectors + for SupergraphCostAttributes +{ + fn on_request(&self, _request: &supergraph::Request) -> Vec { Vec::default() } - fn on_response(&self, _response: &Self::Response) -> Vec { + fn on_response(&self, _response: &supergraph::Response) -> Vec { Vec::default() } @@ -76,7 +76,11 @@ impl Selectors for SupergraphCostAttributes { Vec::default() } - fn on_response_event(&self, _response: &Self::EventResponse, ctx: &Context) -> Vec { + fn on_response_event( + &self, + _response: &crate::graphql::Response, + ctx: &Context, + ) -> Vec { let mut attrs = Vec::with_capacity(4); if let Some(estimated_cost) = self.estimated_cost_if_configured(ctx) { attrs.push(estimated_cost); @@ -216,7 +220,13 @@ impl CostInstrumentsConfig { config: &DefaultedStandardInstrument>, selector: SupergraphSelector, static_instruments: &Arc>, - ) -> CustomHistogram { + ) -> CustomHistogram< + Request, + Response, + graphql::Response, + SupergraphAttributes, + SupergraphSelector, + > { let mut nb_attributes = 0; let selectors = match config { DefaultedStandardInstrument::Bool(_) | DefaultedStandardInstrument::Unset => None, @@ -241,6 +251,7 @@ impl CostInstrumentsConfig { selector: Some(Arc::new(selector)), selectors, updated: false, + _phantom: PhantomData, }), } } @@ -254,6 +265,7 @@ pub(crate) struct CostInstruments { CustomHistogram< supergraph::Request, supergraph::Response, + crate::graphql::Response, SupergraphAttributes, SupergraphSelector, >, @@ -264,6 +276,7 @@ pub(crate) struct CostInstruments { CustomHistogram< supergraph::Request, supergraph::Response, + crate::graphql::Response, SupergraphAttributes, SupergraphSelector, >, @@ -273,6 +286,7 @@ pub(crate) struct CostInstruments { CustomHistogram< supergraph::Request, supergraph::Response, + crate::graphql::Response, SupergraphAttributes, SupergraphSelector, >, diff --git a/apollo-router/src/plugins/telemetry/config_new/events.rs b/apollo-router/src/plugins/telemetry/config_new/events.rs index c5fac133a2..763686d807 100644 --- a/apollo-router/src/plugins/telemetry/config_new/events.rs +++ b/apollo-router/src/plugins/telemetry/config_new/events.rs @@ -1,4 +1,5 @@ use std::fmt::Debug; +use std::marker::PhantomData; use std::sync::Arc; #[cfg(test)] @@ -16,6 +17,7 @@ use super::instruments::Instrumented; use super::Selector; use super::Selectors; use super::Stage; +use crate::graphql; use crate::plugins::telemetry::config_new::attributes::RouterAttributes; use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; @@ -59,6 +61,7 @@ impl Events { selectors: event_cfg.attributes.clone().into(), condition: event_cfg.condition.clone(), attributes: Vec::new(), + _phantom: PhantomData, }), }), }) @@ -88,6 +91,7 @@ impl Events { selectors: event_cfg.attributes.clone().into(), condition: event_cfg.condition.clone(), attributes: Vec::new(), + _phantom: PhantomData, }), }), }) @@ -117,6 +121,7 @@ impl Events { selectors: event_cfg.attributes.clone().into(), condition: event_cfg.condition.clone(), attributes: Vec::new(), + _phantom: PhantomData, }), }), }) @@ -180,31 +185,32 @@ impl Events { } pub(crate) type RouterEvents = - CustomEvents; + CustomEvents; pub(crate) type SupergraphEvents = CustomEvents< supergraph::Request, supergraph::Response, + graphql::Response, SupergraphAttributes, SupergraphSelector, >; pub(crate) type SubgraphEvents = - CustomEvents; + CustomEvents; -pub(crate) struct CustomEvents +pub(crate) struct CustomEvents where - Attributes: Selectors + Default, + Attributes: Selectors + Default, Sel: Selector + Debug, { request: StandardEvent, response: StandardEvent, error: StandardEvent, - custom: Vec>, + custom: Vec>, } impl Instrumented - for CustomEvents + for CustomEvents { type Request = router::Request; type Response = router::Response; @@ -331,6 +337,7 @@ impl Instrumented for CustomEvents< supergraph::Request, supergraph::Response, + crate::graphql::Response, SupergraphAttributes, SupergraphSelector, > @@ -438,7 +445,13 @@ impl Instrumented } impl Instrumented - for CustomEvents + for CustomEvents< + subgraph::Request, + subgraph::Response, + (), + SubgraphAttributes, + SubgraphSelector, + > { type Request = subgraph::Request; type Response = subgraph::Response; @@ -628,9 +641,7 @@ where impl Event where - A: Selectors - + Default - + Debug, + A: Selectors + Default + Debug, E: Selector + Debug, { pub(crate) fn validate(&self) -> Result<(), String> { @@ -655,17 +666,17 @@ pub(crate) enum EventOn { Error, } -pub(crate) struct CustomEvent +pub(crate) struct CustomEvent where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug, { - inner: Mutex>, + inner: Mutex>, } -struct CustomEventInner +struct CustomEventInner where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug, { name: String, @@ -675,11 +686,13 @@ where selectors: Option>>, condition: Condition, attributes: Vec, + _phantom: PhantomData, } -impl Instrumented for CustomEvent +impl Instrumented + for CustomEvent where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug + Debug, @@ -765,9 +778,10 @@ where } } -impl CustomEventInner +impl + CustomEventInner where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug + Debug, { #[inline] diff --git a/apollo-router/src/plugins/telemetry/config_new/extendable.rs b/apollo-router/src/plugins/telemetry/config_new/extendable.rs index c515a352bd..7f4db376d8 100644 --- a/apollo-router/src/plugins/telemetry/config_new/extendable.rs +++ b/apollo-router/src/plugins/telemetry/config_new/extendable.rs @@ -181,16 +181,13 @@ where } } -impl Selectors for Extendable +impl Selectors + for Extendable where - A: Default + Selectors, + A: Default + Selectors, E: Selector, { - type Request = Request; - type Response = Response; - type EventResponse = EventResponse; - - fn on_request(&self, request: &Self::Request) -> Vec { + fn on_request(&self, request: &Request) -> Vec { let mut attrs = self.attributes.on_request(request); let custom_attributes = self.custom.iter().filter_map(|(key, value)| { value @@ -202,7 +199,7 @@ where attrs } - fn on_response(&self, response: &Self::Response) -> Vec { + fn on_response(&self, response: &Response) -> Vec { let mut attrs = self.attributes.on_response(response); let custom_attributes = self.custom.iter().filter_map(|(key, value)| { value @@ -226,7 +223,7 @@ where attrs } - fn on_response_event(&self, response: &Self::EventResponse, ctx: &Context) -> Vec { + fn on_response_event(&self, response: &EventResponse, ctx: &Context) -> Vec { let mut attrs = self.attributes.on_response_event(response, ctx); let custom_attributes = self.custom.iter().filter_map(|(key, value)| { value @@ -258,7 +255,7 @@ where impl Extendable where - A: Default + Selectors, + A: Default + Selectors, E: Selector, { pub(crate) fn validate(&self, restricted_stage: Option) -> Result<(), String> { diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs index 8765348d78..60d9e168c6 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs @@ -58,16 +58,14 @@ impl DefaultForLevel for GraphQLAttributes { } } -impl Selectors for GraphQLAttributes { - type Request = supergraph::Request; - type Response = supergraph::Response; - type EventResponse = crate::graphql::Response; - - fn on_request(&self, _request: &Self::Request) -> Vec { +impl Selectors + for GraphQLAttributes +{ + fn on_request(&self, _request: &supergraph::Request) -> Vec { Vec::default() } - fn on_response(&self, _response: &Self::Response) -> Vec { + fn on_response(&self, _response: &supergraph::Response) -> Vec { Vec::default() } diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs index 3cabed05ff..c73d9ec099 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs @@ -62,6 +62,7 @@ impl DefaultForLevel for GraphQLInstrumentsConfig { pub(crate) type GraphQLCustomInstruments = CustomInstruments< supergraph::Request, supergraph::Response, + crate::graphql::Response, GraphQLAttributes, GraphQLSelector, GraphQLValue, @@ -72,6 +73,7 @@ pub(crate) struct GraphQLInstruments { CustomHistogram< supergraph::Request, supergraph::Response, + crate::graphql::Response, GraphQLAttributes, GraphQLSelector, >, @@ -80,6 +82,7 @@ pub(crate) struct GraphQLInstruments { CustomCounter< supergraph::Request, supergraph::Response, + crate::graphql::Response, GraphQLAttributes, GraphQLSelector, >, diff --git a/apollo-router/src/plugins/telemetry/config_new/instruments.rs b/apollo-router/src/plugins/telemetry/config_new/instruments.rs index 88760f1f1c..6dad942576 100644 --- a/apollo-router/src/plugins/telemetry/config_new/instruments.rs +++ b/apollo-router/src/plugins/telemetry/config_new/instruments.rs @@ -289,6 +289,7 @@ impl InstrumentsConfig { } }, updated: false, + _phantom: PhantomData, }), }); let http_server_request_body_size = @@ -329,6 +330,7 @@ impl InstrumentsConfig { })), selectors, updated: false, + _phantom: PhantomData, }), } }); @@ -372,6 +374,7 @@ impl InstrumentsConfig { })), selectors, updated: false, + _phantom: PhantomData, }), } }); @@ -592,6 +595,7 @@ impl InstrumentsConfig { selector: None, selectors, updated: false, + _phantom: PhantomData, }), } }); @@ -633,6 +637,7 @@ impl InstrumentsConfig { })), selectors, updated: false, + _phantom: PhantomData, }), } }); @@ -674,6 +679,7 @@ impl InstrumentsConfig { })), selectors, updated: false, + _phantom: PhantomData, }), } }); @@ -781,6 +787,7 @@ impl InstrumentsConfig { })), selectors, updated: false, + _phantom: PhantomData, }), } }), @@ -818,6 +825,7 @@ impl InstrumentsConfig { selector: None, selectors, incremented: false, + _phantom: PhantomData, }), } }), @@ -882,6 +890,7 @@ impl InstrumentsConfig { })), selectors, incremented: false, + _phantom: PhantomData, }), } }), @@ -1057,22 +1066,19 @@ where } } -impl Selectors for DefaultedStandardInstrument +impl Selectors + for DefaultedStandardInstrument where - T: Selectors, + T: Selectors, { - type Request = Request; - type Response = Response; - type EventResponse = EventResponse; - - fn on_request(&self, request: &Self::Request) -> Vec { + fn on_request(&self, request: &Request) -> Vec { match self { Self::Bool(_) | Self::Unset => Vec::with_capacity(0), Self::Extendable { attributes } => attributes.on_request(request), } } - fn on_response(&self, response: &Self::Response) -> Vec { + fn on_response(&self, response: &Response) -> Vec { match self { Self::Bool(_) | Self::Unset => Vec::with_capacity(0), Self::Extendable { attributes } => attributes.on_response(response), @@ -1086,7 +1092,7 @@ where } } - fn on_response_event(&self, response: &Self::EventResponse, ctx: &Context) -> Vec { + fn on_response_event(&self, response: &EventResponse, ctx: &Context) -> Vec { match self { Self::Bool(_) | Self::Unset => Vec::with_capacity(0), Self::Extendable { attributes } => attributes.on_response_event(response, ctx), @@ -1174,30 +1180,24 @@ where condition: Condition, } -impl Selectors - for Instrument +impl + Selectors for Instrument where - A: Debug - + Default - + Selectors, + A: Debug + Default + Selectors, E: Debug + Selector, for<'a> &'a SelectorValue: Into>, { - type Request = Request; - type Response = Response; - type EventResponse = EventResponse; - - fn on_request(&self, request: &Self::Request) -> Vec { + fn on_request(&self, request: &Request) -> Vec { self.attributes.on_request(request) } - fn on_response(&self, response: &Self::Response) -> Vec { + fn on_response(&self, response: &Response) -> Vec { self.attributes.on_response(response) } fn on_response_event( &self, - response: &Self::EventResponse, + response: &EventResponse, ctx: &Context, ) -> Vec { self.attributes.on_response_event(response, ctx) @@ -1293,9 +1293,7 @@ impl Instrumented where A: Default + Instrumented, - B: Default - + Debug - + Selectors, + B: Default + Debug + Selectors, E: Debug + Selector, for<'a> InstrumentValue: From<&'a SelectorValue>, { @@ -1330,12 +1328,8 @@ where } } -impl Selectors for SubgraphInstrumentsConfig { - type Request = subgraph::Request; - type Response = subgraph::Response; - type EventResponse = (); - - fn on_request(&self, request: &Self::Request) -> Vec { +impl Selectors for SubgraphInstrumentsConfig { + fn on_request(&self, request: &subgraph::Request) -> Vec { let mut attrs = self.http_client_request_body_size.on_request(request); attrs.extend(self.http_client_request_duration.on_request(request)); attrs.extend(self.http_client_response_body_size.on_request(request)); @@ -1343,7 +1337,7 @@ impl Selectors for SubgraphInstrumentsConfig { attrs } - fn on_response(&self, response: &Self::Response) -> Vec { + fn on_response(&self, response: &subgraph::Response) -> Vec { let mut attrs = self.http_client_request_body_size.on_response(response); attrs.extend(self.http_client_request_duration.on_response(response)); attrs.extend(self.http_client_response_body_size.on_response(response)); @@ -1360,20 +1354,26 @@ impl Selectors for SubgraphInstrumentsConfig { } } -pub(crate) struct CustomInstruments -where - Attributes: Selectors + Default, +pub(crate) struct CustomInstruments< + Request, + Response, + EventResponse, + Attributes, + Select, + SelectorValue, +> where + Attributes: Selectors + Default, Select: Selector + Debug, { _phantom: PhantomData, - counters: Vec>, - histograms: Vec>, + counters: Vec>, + histograms: Vec>, } -impl - CustomInstruments +impl + CustomInstruments where - Attributes: Selectors + Default, + Attributes: Selectors + Default, Select: Selector + Debug, { pub(crate) fn is_empty(&self) -> bool { @@ -1381,10 +1381,10 @@ where } } -impl - CustomInstruments +impl + CustomInstruments where - Attributes: Selectors + Default + Debug + Clone, + Attributes: Selectors + Default + Debug + Clone, Select: Selector + Debug + Clone, for<'a> &'a SelectorValue: Into>, { @@ -1440,6 +1440,7 @@ where selector, selectors: Some(instrument.attributes.clone()), incremented: false, + _phantom: PhantomData, }; counters.push(CustomCounter { inner: Mutex::new(counter), @@ -1494,6 +1495,7 @@ where selector, selectors: Some(instrument.attributes.clone()), updated: false, + _phantom: PhantomData, }; histograms.push(CustomHistogram { @@ -1517,10 +1519,9 @@ where } impl Instrumented - for CustomInstruments + for CustomInstruments where - Attributes: - Selectors + Default, + Attributes: Selectors + Default, Select: Selector + Debug, { type Request = Request; @@ -1581,14 +1582,14 @@ where pub(crate) struct RouterInstruments { http_server_request_duration: Option< - CustomHistogram, + CustomHistogram, >, http_server_active_requests: Option, http_server_request_body_size: Option< - CustomHistogram, + CustomHistogram, >, http_server_response_body_size: Option< - CustomHistogram, + CustomHistogram, >, custom: RouterCustomInstruments, } @@ -1683,6 +1684,7 @@ pub(crate) struct SubgraphInstruments { CustomHistogram< subgraph::Request, subgraph::Response, + (), SubgraphAttributes, SubgraphSelector, >, @@ -1691,6 +1693,7 @@ pub(crate) struct SubgraphInstruments { CustomHistogram< subgraph::Request, subgraph::Response, + (), SubgraphAttributes, SubgraphSelector, >, @@ -1699,6 +1702,7 @@ pub(crate) struct SubgraphInstruments { CustomHistogram< subgraph::Request, subgraph::Response, + (), SubgraphAttributes, SubgraphSelector, >, @@ -1754,6 +1758,7 @@ impl Instrumented for SubgraphInstruments { pub(crate) type RouterCustomInstruments = CustomInstruments< router::Request, router::Response, + (), RouterAttributes, RouterSelector, RouterValue, @@ -1762,6 +1767,7 @@ pub(crate) type RouterCustomInstruments = CustomInstruments< pub(crate) type SupergraphCustomInstruments = CustomInstruments< supergraph::Request, supergraph::Response, + crate::graphql::Response, SupergraphAttributes, SupergraphSelector, SupergraphValue, @@ -1770,6 +1776,7 @@ pub(crate) type SupergraphCustomInstruments = CustomInstruments< pub(crate) type SubgraphCustomInstruments = CustomInstruments< subgraph::Request, subgraph::Response, + (), SubgraphAttributes, SubgraphSelector, SubgraphValue, @@ -1798,17 +1805,18 @@ fn to_i64(value: opentelemetry::Value) -> Option { } } -pub(crate) struct CustomCounter +pub(crate) struct CustomCounter where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug, { - pub(crate) inner: Mutex>, + pub(crate) inner: Mutex>, } -impl Clone for CustomCounter +impl Clone + for CustomCounter where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug + Clone, { fn clone(&self) -> Self { @@ -1818,9 +1826,9 @@ where } } -pub(crate) struct CustomCounterInner +pub(crate) struct CustomCounterInner where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug, { pub(crate) increment: Increment, @@ -1831,11 +1839,13 @@ where pub(crate) attributes: Vec, // Useful when it's a counter on events to know if we have to count for an event or not pub(crate) incremented: bool, + pub(crate) _phantom: PhantomData, } -impl Clone for CustomCounterInner +impl Clone + for CustomCounterInner where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug + Clone, { fn clone(&self) -> Self { @@ -1847,13 +1857,15 @@ where condition: self.condition.clone(), attributes: self.attributes.clone(), incremented: self.incremented, + _phantom: PhantomData, } } } -impl Instrumented for CustomCounter +impl Instrumented + for CustomCounter where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug + Debug, @@ -2109,9 +2121,10 @@ where } } -impl Drop for CustomCounter +impl Drop + for CustomCounter where - A: Selectors + Default, + A: Selectors + Default, T: Selector + Debug, { fn drop(&mut self) { @@ -2230,17 +2243,17 @@ impl Drop for ActiveRequestsCounter { // ---------------- Histogram ----------------------- -pub(crate) struct CustomHistogram +pub(crate) struct CustomHistogram where - A: Selectors + Default, + A: Selectors + Default, T: Selector, { - pub(crate) inner: Mutex>, + pub(crate) inner: Mutex>, } -pub(crate) struct CustomHistogramInner +pub(crate) struct CustomHistogramInner where - A: Selectors + Default, + A: Selectors + Default, T: Selector, { pub(crate) increment: Increment, @@ -2251,12 +2264,13 @@ where pub(crate) attributes: Vec, // Useful when it's an histogram on events to know if we have to count for an event or not pub(crate) updated: bool, + pub(crate) _phantom: PhantomData, } impl Instrumented - for CustomHistogram + for CustomHistogram where - A: Selectors + Default, + A: Selectors + Default, T: Selector, { type Request = Request; @@ -2501,9 +2515,10 @@ where } } -impl Drop for CustomHistogram +impl Drop + for CustomHistogram where - A: Selectors + Default, + A: Selectors + Default, T: Selector, { fn drop(&mut self) { diff --git a/apollo-router/src/plugins/telemetry/config_new/mod.rs b/apollo-router/src/plugins/telemetry/config_new/mod.rs index 082d0a438e..d619dafcaf 100644 --- a/apollo-router/src/plugins/telemetry/config_new/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/mod.rs @@ -30,14 +30,10 @@ pub(crate) mod logging; pub(crate) mod selectors; pub(crate) mod spans; -pub(crate) trait Selectors { - type Request; - type Response; - type EventResponse; - - fn on_request(&self, request: &Self::Request) -> Vec; - fn on_response(&self, response: &Self::Response) -> Vec; - fn on_response_event(&self, _response: &Self::EventResponse, _ctx: &Context) -> Vec { +pub(crate) trait Selectors { + fn on_request(&self, request: &Request) -> Vec; + fn on_response(&self, response: &Response) -> Vec; + fn on_response_event(&self, _response: &EventResponse, _ctx: &Context) -> Vec { Vec::with_capacity(0) } fn on_error(&self, error: &BoxError, ctx: &Context) -> Vec; diff --git a/apollo-router/src/plugins/telemetry/config_new/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/selectors.rs index 3324047b2f..d894a543e0 100644 --- a/apollo-router/src/plugins/telemetry/config_new/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/selectors.rs @@ -7,6 +7,7 @@ use serde_json_bytes::path::JsonPathInst; use serde_json_bytes::ByteString; use sha2::Digest; +use super::attributes::SubgraphRequestResendCountKey; use crate::context::CONTAINS_GRAPHQL_ERROR; use crate::context::OPERATION_KIND; use crate::context::OPERATION_NAME; @@ -526,6 +527,12 @@ pub(crate) enum SubgraphSelector { /// The subgraph http response status code. subgraph_response_status: ResponseStatus, }, + SubgraphResendCount { + /// The subgraph http resend count + subgraph_resend_count: bool, + /// Optional default value. + default: Option, + }, SupergraphOperationName { /// The supergraph query operation name. supergraph_operation_name: OperationName, @@ -1577,6 +1584,18 @@ impl Selector for SubgraphSelector { SubgraphSelector::OnGraphQLError { subgraph_on_graphql_error: on_graphql_error, } if *on_graphql_error => Some((!response.response.body().errors.is_empty()).into()), + SubgraphSelector::SubgraphResendCount { + subgraph_resend_count, + default, + } if *subgraph_resend_count => { + response + .context + .get::<_, usize>(SubgraphRequestResendCountKey::new(&response.id)) + .ok() + .flatten() + .map(|v| opentelemetry::Value::from(v as i64)) + } + .or_else(|| default.maybe_to_otel_value()), SubgraphSelector::Static(val) => Some(val.clone().into()), SubgraphSelector::StaticField { r#static } => Some(r#static.clone().into()), SubgraphSelector::Cache { cache, entity_type } => { @@ -1771,12 +1790,14 @@ mod test { use crate::plugins::telemetry::config_new::selectors::ResponseStatus; use crate::plugins::telemetry::config_new::selectors::RouterSelector; use crate::plugins::telemetry::config_new::selectors::SubgraphQuery; + use crate::plugins::telemetry::config_new::selectors::SubgraphRequestResendCountKey; use crate::plugins::telemetry::config_new::selectors::SubgraphSelector; use crate::plugins::telemetry::config_new::selectors::SupergraphSelector; use crate::plugins::telemetry::config_new::selectors::TraceIdFormat; use crate::plugins::telemetry::config_new::Selector; use crate::plugins::telemetry::otel; use crate::query_planner::APOLLO_OPERATION_ID; + use crate::services::subgraph::SubgraphRequestId; use crate::services::FIRST_EVENT_CONTEXT_KEY; use crate::spec::operation_limits::OperationLimits; @@ -2477,6 +2498,41 @@ mod test { ); } + #[test] + fn subgraph_resend_count() { + let selector = SubgraphSelector::SubgraphResendCount { + subgraph_resend_count: true, + default: Some("defaulted".into()), + }; + let context = crate::context::Context::new(); + assert_eq!( + selector + .on_response( + &crate::services::SubgraphResponse::fake2_builder() + .context(context.clone()) + .build() + .unwrap() + ) + .unwrap(), + "defaulted".into() + ); + let subgraph_req_id = SubgraphRequestId(String::from("test")); + let _ = context.insert(SubgraphRequestResendCountKey::new(&subgraph_req_id), 2usize); + + assert_eq!( + selector + .on_response( + &crate::services::SubgraphResponse::fake2_builder() + .context(context.clone()) + .id(subgraph_req_id) + .build() + .unwrap() + ) + .unwrap(), + 2i64.into() + ); + } + #[test] fn router_baggage() { let subscriber = tracing_subscriber::registry().with(otel::layer()); diff --git a/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs b/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs index d721bad5a6..3eacc09620 100644 --- a/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs +++ b/apollo-router/src/plugins/telemetry/metrics/apollo/mod.rs @@ -190,6 +190,7 @@ mod test { use super::studio::SingleStatsReport; use super::*; use crate::context::OPERATION_KIND; + use crate::plugin::Plugin; use crate::plugin::PluginInit; use crate::plugin::PluginPrivate; use crate::plugins::subscription; @@ -421,7 +422,7 @@ mod test { } async fn create_subscription_plugin() -> Result { - subscription::Subscription::new(PluginInit::fake_new( + ::new(PluginInit::fake_new( subscription::SubscriptionConfig::default(), Default::default(), )) diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index bea99b93b7..14ba7199f9 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -133,6 +133,7 @@ use crate::plugins::telemetry::tracing::apollo_telemetry::APOLLO_PRIVATE_OPERATI use crate::plugins::telemetry::tracing::TracingConfigurator; use crate::plugins::telemetry::utils::TracingUtils; use crate::query_planner::OperationKind; +use crate::register_private_plugin; use crate::router_factory::Endpoint; use crate::services::execution; use crate::services::router; diff --git a/apollo-router/src/plugins/test.rs b/apollo-router/src/plugins/test.rs index c8924f0ef0..8535c9f5f1 100644 --- a/apollo-router/src/plugins/test.rs +++ b/apollo-router/src/plugins/test.rs @@ -12,8 +12,8 @@ use tower_service::Service; use crate::introspection::IntrospectionCache; use crate::plugin::DynPlugin; -use crate::plugin::Plugin; use crate::plugin::PluginInit; +use crate::plugin::PluginPrivate; use crate::query_planner::BridgeQueryPlanner; use crate::query_planner::PlannerMode; use crate::services::execution; @@ -217,7 +217,7 @@ impl> + 'static> PluginTestHarness { impl Deref for PluginTestHarness where - T: Plugin, + T: PluginPrivate, { type Target = T; diff --git a/apollo-router/src/plugins/traffic_shaping/mod.rs b/apollo-router/src/plugins/traffic_shaping/mod.rs index 4335bfe988..935a670164 100644 --- a/apollo-router/src/plugins/traffic_shaping/mod.rs +++ b/apollo-router/src/plugins/traffic_shaping/mod.rs @@ -396,7 +396,6 @@ impl TrafficShaping { config.min_per_sec, config.retry_percent, config.retry_mutations, - name.to_string(), ); tower::retry::RetryLayer::new(retry_policy) }); diff --git a/apollo-router/src/plugins/traffic_shaping/retry.rs b/apollo-router/src/plugins/traffic_shaping/retry.rs index 40727cc6dd..2cf9132640 100644 --- a/apollo-router/src/plugins/traffic_shaping/retry.rs +++ b/apollo-router/src/plugins/traffic_shaping/retry.rs @@ -5,6 +5,7 @@ use std::time::Duration; use tower::retry::budget::Budget; use tower::retry::Policy; +use crate::plugins::telemetry::config_new::attributes::SubgraphRequestResendCountKey; use crate::query_planner::OperationKind; use crate::services::subgraph; @@ -12,7 +13,6 @@ use crate::services::subgraph; pub(crate) struct RetryPolicy { budget: Arc, retry_mutations: bool, - subgraph_name: String, } impl RetryPolicy { @@ -21,7 +21,6 @@ impl RetryPolicy { min_per_sec: Option, retry_percent: Option, retry_mutations: Option, - subgraph_name: String, ) -> Self { Self { budget: Arc::new(Budget::new( @@ -30,21 +29,59 @@ impl RetryPolicy { retry_percent.unwrap_or(0.2), )), retry_mutations: retry_mutations.unwrap_or(false), - subgraph_name, } } } -impl Policy for RetryPolicy { +impl Policy for RetryPolicy { type Future = future::Ready; - fn retry(&self, req: &subgraph::Request, result: Result<&Res, &E>) -> Option { + fn retry( + &self, + req: &subgraph::Request, + result: Result<&subgraph::Response, &E>, + ) -> Option { + let subgraph_name = req.subgraph_name.clone().unwrap_or_default(); match result { - Ok(_) => { - // Treat all `Response`s as success, - // so deposit budget and don't retry... - self.budget.deposit(); - None + Ok(resp) => { + if resp.response.status() >= http::StatusCode::BAD_REQUEST { + if req.operation_kind == OperationKind::Mutation && !self.retry_mutations { + return None; + } + + let withdrew = self.budget.withdraw(); + if withdrew.is_err() { + u64_counter!( + "apollo_router_http_request_retry_total", + "Number of retries for an http request to a subgraph", + 1u64, + status = "aborted", + subgraph = subgraph_name + ); + + return None; + } + + let _ = req + .context + .upsert::<_, usize>(SubgraphRequestResendCountKey::new(&req.id), |val| { + val + 1 + }); + + u64_counter!( + "apollo_router_http_request_retry_total", + "Number of retries for an http request to a subgraph", + 1u64, + subgraph = subgraph_name + ); + + Some(future::ready(self.clone())) + } else { + // Treat all `Response`s as success, + // so deposit budget and don't retry... + self.budget.deposit(); + None + } } Err(_e) => { if req.operation_kind == OperationKind::Mutation && !self.retry_mutations { @@ -53,20 +90,27 @@ impl Policy for RetryPolicy { let withdrew = self.budget.withdraw(); if withdrew.is_err() { - tracing::info!( - monotonic_counter.apollo_router_http_request_retry_total = 1u64, + u64_counter!( + "apollo_router_http_request_retry_total", + "Number of retries for an http request to a subgraph", + 1u64, status = "aborted", - subgraph = %self.subgraph_name, + subgraph = subgraph_name ); return None; } - - tracing::info!( - monotonic_counter.apollo_router_http_request_retry_total = 1u64, - subgraph = %self.subgraph_name, + u64_counter!( + "apollo_router_http_request_retry_total", + "Number of retries for an http request to a subgraph", + 1u64, + subgraph = subgraph_name ); + let _ = req + .context + .upsert::<_, usize>(SubgraphRequestResendCountKey::new(&req.id), |val| val + 1); + Some(future::ready(self.clone())) } } @@ -76,3 +120,129 @@ impl Policy for RetryPolicy { Some(req.clone()) } } + +#[cfg(test)] +mod tests { + use http::StatusCode; + use tower::BoxError; + + use super::*; + use crate::error::FetchError; + use crate::graphql; + use crate::http_ext; + use crate::metrics::FutureMetricsExt; + + #[tokio::test] + async fn test_retry_with_error() { + async { + let retry = RetryPolicy::new( + Some(Duration::from_secs(10)), + Some(10), + Some(0.2), + Some(false), + ); + + let subgraph_req = subgraph::Request::fake_builder() + .subgraph_name("my_subgraph_name_error") + .subgraph_request( + http_ext::Request::fake_builder() + .header("test", "my_value_set") + .body( + graphql::Request::fake_builder() + .query(String::from("query { test }")) + .build(), + ) + .build() + .unwrap(), + ) + .build(); + + assert!(retry + .retry( + &subgraph_req, + Err(&Box::new(FetchError::SubrequestHttpError { + status_code: None, + service: String::from("my_subgraph_name_error"), + reason: String::from("cannot contact the subgraph"), + })) + ) + .is_some()); + + assert!(retry + .retry( + &subgraph_req, + Err(&Box::new(FetchError::SubrequestHttpError { + status_code: None, + service: String::from("my_subgraph_name_error"), + reason: String::from("cannot contact the subgraph"), + })) + ) + .is_some()); + + assert_counter!( + "apollo_router_http_request_retry_total", + 2, + "subgraph" = "my_subgraph_name_error" + ); + } + .with_metrics() + .await; + } + + #[tokio::test] + async fn test_retry_with_http_status_code() { + async { + let retry = RetryPolicy::new( + Some(Duration::from_secs(10)), + Some(10), + Some(0.2), + Some(false), + ); + + let subgraph_req = subgraph::Request::fake_builder() + .subgraph_name("my_subgraph_name_error") + .subgraph_request( + http_ext::Request::fake_builder() + .header("test", "my_value_set") + .body( + graphql::Request::fake_builder() + .query(String::from("query { test }")) + .build(), + ) + .build() + .unwrap(), + ) + .build(); + + assert!(retry + .retry( + &subgraph_req, + Ok::<&subgraph::Response, &BoxError>( + &subgraph::Response::fake_builder() + .status_code(StatusCode::BAD_REQUEST) + .build() + ) + ) + .is_some()); + + assert!(retry + .retry( + &subgraph_req, + Ok::<&subgraph::Response, &BoxError>( + &subgraph::Response::fake_builder() + .status_code(StatusCode::BAD_REQUEST) + .build() + ) + ) + .is_some()); + + assert_counter!( + "apollo_router_http_request_retry_total", + 2, + "subgraph" = "my_subgraph_name_error" + ); + } + .with_metrics() + .await; + } +} diff --git a/apollo-router/src/services/http/service.rs b/apollo-router/src/services/http/service.rs index d629412f0c..b8bfb38f04 100644 --- a/apollo-router/src/services/http/service.rs +++ b/apollo-router/src/services/http/service.rs @@ -233,6 +233,7 @@ impl tower::Service for HttpClientService { let HttpRequest { mut http_request, context, + .. } = request; let schema_uri = http_request.uri(); diff --git a/docs/source/reference/router/telemetry/instrumentation/selectors.mdx b/docs/source/reference/router/telemetry/instrumentation/selectors.mdx index 580ed124ca..eafd42e148 100644 --- a/docs/source/reference/router/telemetry/instrumentation/selectors.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/selectors.mdx @@ -89,6 +89,7 @@ The subgraph service executes multiple times during query execution, with each e | `supergraph_operation_kind` | Yes | `string` | The operation kind from the supergraph query | | `supergraph_query` | Yes | `string` | The graphql query to the supergraph | | `supergraph_query_variable` | Yes | | The name of a supergraph query variable | +| `subgraph_resend_count` | Yes | `true`\|`false` | Number of retries for an http request to a subgraph | | `request_context` | Yes | | The name of a request context key | | `response_context` | Yes | | The name of a response context key | | `baggage` | Yes | | The name of a baggage item | diff --git a/docs/source/reference/router/telemetry/instrumentation/standard-attributes.mdx b/docs/source/reference/router/telemetry/instrumentation/standard-attributes.mdx index b1440ca115..eb264481e3 100644 --- a/docs/source/reference/router/telemetry/instrumentation/standard-attributes.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/standard-attributes.mdx @@ -108,3 +108,4 @@ Standard attributes of the `subgraph` service: | `subgraph.graphql.operation.name` | | The operation name from the subgraph query (need `spec_compliant` [mode](/router/configuration/telemetry/instrumentation/spans/#mode) to disable it) | | `subgraph.graphql.operation.type` | `query`\|`mutation`\|`subscription` | The operation kind from the subgraph query | | `subgraph.graphql.document` | | The GraphQL query to the subgraph (need `spec_compliant` [mode](/router/configuration/telemetry/instrumentation/spans/#mode) to disable it) | +| `http.request.resend_count` | `true`\|`false` | Number of retries for an http request to a subgraph | diff --git a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx index da9b655036..9bcf4c83fa 100644 --- a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx @@ -17,9 +17,6 @@ These instruments can be consumed by configuring a [metrics exporter](/router/co - `subgraph`: (Optional) The subgraph being queried - `apollo_router_http_requests_total` - Total number of HTTP requests by HTTP status - `apollo_router_timeout` - Number of triggered timeouts -- `apollo_router_http_request_retry_total` - Number of subgraph requests retried, attributes: - - `subgraph`: The subgraph being queried - - `status` : If the retry was aborted (`aborted`) ### GraphQL @@ -126,3 +123,39 @@ The initial call to Uplink during router startup is not reflected in metrics. The following metrics have been deprecated and should not be used. - `apollo_router_span` - **Deprecated**—use `apollo_router_processing_time` instead. +- `apollo_router_http_request_retry_total` **Deprecated**- Number of subgraph requests retried. You should use: + +```yaml title="config.router.yaml" +telemetry: + exporters: + metrics: + common: + views: + - name: apollo_router_http_request_retry_total # Drop the deprecated metric + aggregation: drop + - name: http.client.request.retry # Set the right buckets for the new metric + aggregation: + histogram: + buckets: + - 1 + - 10 + - 100 + - 1000 + instrumentation: + instruments: + subgraph: + http.client.request.retry: # Create custom histogram measuring the number of retries for http request to a subgraph + type: histogram + value: + subgraph_resend_count: true + unit: hit + description: histogram of subgraph request resend count + condition: + gt: + - subgraph_resend_count: true + - 0 + attributes: + subgraph.name: true + supergraph.operation.name: + supergraph_operation_name: string +``` \ No newline at end of file From 59d8cc9b2623e26820334f176fa2edf7a902a567 Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Tue, 3 Dec 2024 14:46:40 +0100 Subject: [PATCH 051/112] docs(telemetry): add missing subgraph selector (#6327) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- .../reference/router/telemetry/instrumentation/selectors.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/router/telemetry/instrumentation/selectors.mdx b/docs/source/reference/router/telemetry/instrumentation/selectors.mdx index eafd42e148..828ee39b65 100644 --- a/docs/source/reference/router/telemetry/instrumentation/selectors.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/selectors.mdx @@ -84,11 +84,12 @@ The subgraph service executes multiple times during query execution, with each e | `subgraph_request_header` | Yes | | The name of a subgraph request header | | `subgraph_response_header` | Yes | | The name of a subgraph response header | | `subgraph_response_status` | Yes | `code`\|`reason` | The status of a subgraph response | -| `subgraph_on_graphql_error` | No | `true`\|`false` | Boolean set to true if the subgraph response payload contains a GraphQL error | +| `subgraph_on_graphql_error` | No | `true`\|`false` | Boolean set to true if the subgraph response payload contains a GraphQL error | | `supergraph_operation_name` | Yes | `string`\|`hash` | The operation name from the supergraph query | | `supergraph_operation_kind` | Yes | `string` | The operation kind from the supergraph query | | `supergraph_query` | Yes | `string` | The graphql query to the supergraph | | `supergraph_query_variable` | Yes | | The name of a supergraph query variable | +| `supergraph_request_header` | Yes | | The name of a supergraph request header | | `subgraph_resend_count` | Yes | `true`\|`false` | Number of retries for an http request to a subgraph | | `request_context` | Yes | | The name of a request context key | | `response_context` | Yes | | The name of a response context key | From 89795a7d006087a3e6a263b2bffb52902e0a5bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Tue, 3 Dec 2024 15:12:03 +0000 Subject: [PATCH 052/112] Migrate monotonic counter metrics to `u64_counter!` (#6350) Co-authored-by: Coenen Benjamin --- ...int_renee_router_297_monotonic_counters.md | 13 +++++ apollo-router/src/cache/storage.rs | 40 +++++++++------ apollo-router/src/graphql/request.rs | 16 +++--- apollo-router/src/metrics/mod.rs | 31 +++++++++++- apollo-router/src/notification.rs | 6 ++- .../src/plugins/authentication/mod.rs | 35 +++++++------ .../src/plugins/authentication/subgraph.rs | 16 +++--- .../src/plugins/authorization/mod.rs | 14 ++++-- apollo-router/src/plugins/subscription.rs | 20 +++++--- apollo-router/src/plugins/telemetry/mod.rs | 49 ++++++++++--------- .../plugins/traffic_shaping/timeout/future.rs | 6 ++- apollo-router/src/protocols/websocket.rs | 22 +++------ apollo-router/src/query_planner/execution.rs | 6 ++- apollo-router/src/query_planner/fetch.rs | 6 ++- .../services/layers/persisted_queries/mod.rs | 38 +++++++++----- apollo-router/src/services/router/service.rs | 20 +++++--- .../src/services/subgraph_service.rs | 47 +++++++++++------- apollo-router/src/state_machine.rs | 11 ++++- apollo-router/src/uplink/mod.rs | 18 ++++--- .../instrumentation/standard-instruments.mdx | 18 +++++-- 20 files changed, 285 insertions(+), 147 deletions(-) create mode 100644 .changesets/maint_renee_router_297_monotonic_counters.md diff --git a/.changesets/maint_renee_router_297_monotonic_counters.md b/.changesets/maint_renee_router_297_monotonic_counters.md new file mode 100644 index 0000000000..dffc9b6f3b --- /dev/null +++ b/.changesets/maint_renee_router_297_monotonic_counters.md @@ -0,0 +1,13 @@ +### Docs-deprecate several metrics ([PR #6350](https://github.com/apollographql/router/pull/6350)) + +These metrics are deprecated in favor of better, OTel-compatible alternatives: + +- `apollo_router_deduplicated_subscriptions_total` - use the `apollo.router.operations.subscriptions` metric's `subscriptions.deduplicated` attribute. +- `apollo_authentication_failure_count` - use the `apollo.router.operations.authentication.jwt` metric's `authentication.jwt.failed` attribute. +- `apollo_authentication_success_count` - use the `apollo.router.operations.authentication.jwt` metric instead. If the `authentication.jwt.failed` attribute is *absent* or `false`, the authentication succeeded. +- `apollo_require_authentication_failure_count` - use the `http.server.request.duration` metric's `http.response.status_code` attribute. Requests with authentication failures have HTTP status code 401. +- `apollo_router_timeout` - this metric conflates timed-out requests from client to the router, and requests from the router to subgraphs. Timed-out requests have HTTP status code 504. Use the `http.response.status_code` attribute on the `http.server.request.duration` metric to identify timed-out router requests, and the same attribute on the `http.client.request.duration` metric to identify timed-out subgraph requests. + +The deprecated metrics will continue to work in the 1.x release line. + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6350 diff --git a/apollo-router/src/cache/storage.rs b/apollo-router/src/cache/storage.rs index 15f452ed28..4408d24c5c 100644 --- a/apollo-router/src/cache/storage.rs +++ b/apollo-router/src/cache/storage.rs @@ -170,10 +170,12 @@ where match res { Some(v) => { - tracing::info!( - monotonic_counter.apollo_router_cache_hit_count = 1u64, - kind = %self.caller, - storage = &tracing::field::display(CacheStorageName::Memory), + u64_counter!( + "apollo_router_cache_hit_count", + "Number of cache hits", + 1, + kind = self.caller, + storage = CacheStorageName::Memory.to_string() ); let duration = instant_memory.elapsed().as_secs_f64(); tracing::info!( @@ -190,10 +192,12 @@ where kind = %self.caller, storage = &tracing::field::display(CacheStorageName::Memory), ); - tracing::info!( - monotonic_counter.apollo_router_cache_miss_count = 1u64, - kind = %self.caller, - storage = &tracing::field::display(CacheStorageName::Memory), + u64_counter!( + "apollo_router_cache_miss_count", + "Number of cache misses", + 1, + kind = self.caller, + storage = CacheStorageName::Memory.to_string() ); let instant_redis = Instant::now(); @@ -214,10 +218,12 @@ where Some(v) => { self.insert_in_memory(key.clone(), v.0.clone()).await; - tracing::info!( - monotonic_counter.apollo_router_cache_hit_count = 1u64, - kind = %self.caller, - storage = &tracing::field::display(CacheStorageName::Redis), + u64_counter!( + "apollo_router_cache_hit_count", + "Number of cache hits", + 1, + kind = self.caller, + storage = CacheStorageName::Redis.to_string() ); let duration = instant_redis.elapsed().as_secs_f64(); tracing::info!( @@ -228,10 +234,12 @@ where Some(v.0) } None => { - tracing::info!( - monotonic_counter.apollo_router_cache_miss_count = 1u64, - kind = %self.caller, - storage = &tracing::field::display(CacheStorageName::Redis), + u64_counter!( + "apollo_router_cache_miss_count", + "Number of cache misses", + 1, + kind = self.caller, + storage = CacheStorageName::Redis.to_string() ); let duration = instant_redis.elapsed().as_secs_f64(); tracing::info!( diff --git a/apollo-router/src/graphql/request.rs b/apollo-router/src/graphql/request.rs index 1e51262dbf..fc572f70cb 100644 --- a/apollo-router/src/graphql/request.rs +++ b/apollo-router/src/graphql/request.rs @@ -202,9 +202,11 @@ impl Request { mode = %BatchingMode::BatchHttpLink // Only supported mode right now ); - tracing::info!( - monotonic_counter.apollo.router.operations.batching = 1u64, - mode = %BatchingMode::BatchHttpLink // Only supported mode right now + u64_counter!( + "apollo.router.operations.batching", + "Total requests with batched operations", + 1, + mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now ); for entry in value .as_array() @@ -229,9 +231,11 @@ impl Request { mode = "batch_http_link" // Only supported mode right now ); - tracing::info!( - monotonic_counter.apollo.router.operations.batching = 1u64, - mode = "batch_http_link" // Only supported mode right now + u64_counter!( + "apollo.router.operations.batching", + "Total requests with batched operations", + 1, + mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now ); for entry in value .as_array() diff --git a/apollo-router/src/metrics/mod.rs b/apollo-router/src/metrics/mod.rs index e24317cd06..4b2aa0b888 100644 --- a/apollo-router/src/metrics/mod.rs +++ b/apollo-router/src/metrics/mod.rs @@ -496,7 +496,9 @@ pub(crate) fn meter_provider() -> AggregateMeterProvider { } #[macro_export] -/// Get or create a u64 monotonic counter metric and add a value to it +/// Get or create a `u64` monotonic counter metric and add a value to it. +/// +/// Each metric needs a description. /// /// This macro is a replacement for the telemetry crate's MetricsLayer. We will eventually convert all metrics to use these macros and deprecate the MetricsLayer. /// The reason for this is that the MetricsLayer has: @@ -506,6 +508,33 @@ pub(crate) fn meter_provider() -> AggregateMeterProvider { /// * Imperfect mapping to metrics API that can only be checked at runtime. /// /// New metrics should be added using these macros. +/// +/// # Examples +/// ```ignore +/// // Count a thing: +/// u64_counter!( +/// "apollo.router.operations.frobbles", +/// "The amount of frobbles we've operated on", +/// 1 +/// ); +/// // Count a thing with attributes: +/// u64_counter!( +/// "apollo.router.operations.frobbles", +/// "The amount of frobbles we've operated on", +/// 1, +/// frobbles.color = "blue" +/// ); +/// // Count a thing with dynamic attributes: +/// let attributes = [ +/// opentelemetry::KeyValue::new("frobbles.color".to_string(), "blue".into()), +/// ]; +/// u64_counter!( +/// "apollo.router.operations.frobbles", +/// "The amount of frobbles we've operated on", +/// 1, +/// attributes +/// ); +/// ``` #[allow(unused_macros)] macro_rules! u64_counter { ($($name:ident).+, $description:literal, $value: expr, $($attr_key:literal = $attr_value:expr),+) => { diff --git a/apollo-router/src/notification.rs b/apollo-router/src/notification.rs index 7cfba87e7a..ac1f33645d 100644 --- a/apollo-router/src/notification.rs +++ b/apollo-router/src/notification.rs @@ -510,7 +510,11 @@ where match Pin::new(&mut this.msg_receiver).poll_next(cx) { Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(_)))) => { - tracing::info!(monotonic_counter.apollo_router_skipped_event_count = 1u64,); + u64_counter!( + "apollo_router_skipped_event_count", + "Amount of events dropped from the internal message queue", + 1u64 + ); self.poll_next(cx) } Poll::Ready(None) => Poll::Ready(None), diff --git a/apollo-router/src/plugins/authentication/mod.rs b/apollo-router/src/plugins/authentication/mod.rs index 9f9abc8040..0239e1f005 100644 --- a/apollo-router/src/plugins/authentication/mod.rs +++ b/apollo-router/src/plugins/authentication/mod.rs @@ -539,8 +539,6 @@ fn authenticate( jwks_manager: &JwksManager, request: router::Request, ) -> ControlFlow { - const AUTHENTICATION_KIND: &str = "JWT"; - // We are going to do a lot of similar checking so let's define a local function // to help reduce repetition fn failure_message( @@ -549,17 +547,16 @@ fn authenticate( status: StatusCode, ) -> ControlFlow { // This is a metric and will not appear in the logs - tracing::info!( - monotonic_counter.apollo_authentication_failure_count = 1u64, - kind = %AUTHENTICATION_KIND + u64_counter!( + "apollo_authentication_failure_count", + "Number of requests with failed JWT authentication (deprecated)", + 1, + kind = "JWT" ); - tracing::info!( - monotonic_counter - .apollo - .router - .operations - .authentication - .jwt = 1, + u64_counter!( + "apollo.router.operations.authentication.jwt", + "Number of requests with JWT authentication", + 1, authentication.jwt.failed = true ); tracing::info!(message = %error, "jwt authentication failure"); @@ -662,11 +659,17 @@ fn authenticate( ); } // This is a metric and will not appear in the logs - tracing::info!( - monotonic_counter.apollo_authentication_success_count = 1u64, - kind = %AUTHENTICATION_KIND + u64_counter!( + "apollo_authentication_success_count", + "Number of requests with successful JWT authentication (deprecated)", + 1, + kind = "JWT" + ); + u64_counter!( + "apollo.router.operations.jwt", + "Number of requests with JWT authentication", + 1 ); - tracing::info!(monotonic_counter.apollo.router.operations.jwt = 1u64); return ControlFlow::Continue(request); } diff --git a/apollo-router/src/plugins/authentication/subgraph.rs b/apollo-router/src/plugins/authentication/subgraph.rs index 568aa9c8ac..4a0bcd4d65 100644 --- a/apollo-router/src/plugins/authentication/subgraph.rs +++ b/apollo-router/src/plugins/authentication/subgraph.rs @@ -409,17 +409,21 @@ impl SigningParamsConfig { } fn increment_success_counter(subgraph_name: &str) { - tracing::info!( - monotonic_counter.apollo.router.operations.authentication.aws.sigv4 = 1u64, + u64_counter!( + "apollo.router.operations.authentication.aws.sigv4", + "Number of subgraph requests signed with AWS SigV4", + 1, authentication.aws.sigv4.failed = false, - subgraph.service.name = %subgraph_name, + subgraph.service.name = subgraph_name.to_string() ); } fn increment_failure_counter(subgraph_name: &str) { - tracing::info!( - monotonic_counter.apollo.router.operations.authentication.aws.sigv4 = 1u64, + u64_counter!( + "apollo.router.operations.authentication.aws.sigv4", + "Number of subgraph requests signed with AWS SigV4", + 1, authentication.aws.sigv4.failed = true, - subgraph.service.name = %subgraph_name, + subgraph.service.name = subgraph_name.to_string() ); } diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 331641a726..63b2062a05 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -556,8 +556,10 @@ impl Plugin for AuthorizationPlugin { Ok(ControlFlow::Continue(request)) } else { // This is a metric and will not appear in the logs - tracing::info!( - monotonic_counter.apollo_require_authentication_failure_count = 1u64, + u64_counter!( + "apollo_require_authentication_failure_count", + "Number of unauthenticated requests (deprecated)", + 1 ); tracing::error!("rejecting unauthenticated request"); let response = supergraph::Response::error_builder() @@ -588,11 +590,13 @@ impl Plugin for AuthorizationPlugin { let needs_requires_scopes = request.context.contains_key(REQUIRED_SCOPES_KEY); if needs_authenticated || needs_requires_scopes { - tracing::info!( - monotonic_counter.apollo.router.operations.authorization = 1u64, + u64_counter!( + "apollo.router.operations.authorization", + "Number of subgraph requests requiring authorization", + 1, authorization.filtered = filtered, authorization.needs_authenticated = needs_authenticated, - authorization.needs_requires_scopes = needs_requires_scopes, + authorization.needs_requires_scopes = needs_requires_scopes ); } diff --git a/apollo-router/src/plugins/subscription.rs b/apollo-router/src/plugins/subscription.rs index 50d5e78ead..1d342f586f 100644 --- a/apollo-router/src/plugins/subscription.rs +++ b/apollo-router/src/plugins/subscription.rs @@ -503,10 +503,12 @@ impl Service for CallbackService { }; // Keep the subscription to the client opened payload.subscribed = Some(true); - tracing::info!( - monotonic_counter.apollo.router.operations.subscriptions.events = 1u64, - subscriptions.mode="callback" - ); + u64_counter!( + "apollo.router.operations.subscriptions.events", + "Number of subscription events", + 1, + subscriptions.mode = "callback" + ); handle.send_sync(payload)?; Ok(router::Response { @@ -626,10 +628,12 @@ impl Service for CallbackService { }); } }; - tracing::info!( - monotonic_counter.apollo.router.operations.subscriptions.events = 1u64, - subscriptions.mode="callback", - subscriptions.complete=true + u64_counter!( + "apollo.router.operations.subscriptions.events", + "Number of subscription events", + 1, + subscriptions.mode = "callback", + subscriptions.complete = true ); if let Err(_err) = handle.send_sync( graphql::Response::builder().errors(errors).build(), diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 14ba7199f9..da5361e1c1 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -131,7 +131,6 @@ use crate::plugins::telemetry::reload::OPENTELEMETRY_TRACER_HANDLE; use crate::plugins::telemetry::tracing::apollo_telemetry::decode_ftv1_trace; use crate::plugins::telemetry::tracing::apollo_telemetry::APOLLO_PRIVATE_OPERATION_SIGNATURE; use crate::plugins::telemetry::tracing::TracingConfigurator; -use crate::plugins::telemetry::utils::TracingUtils; use crate::query_planner::OperationKind; use crate::register_private_plugin; use crate::router_factory::Endpoint; @@ -1747,28 +1746,32 @@ impl Telemetry { } fn plugin_metrics(config: &Arc) { - let metrics_prom_used = config.exporters.metrics.prometheus.enabled; - let metrics_otlp_used = MetricsConfigurator::enabled(&config.exporters.metrics.otlp); - let tracing_otlp_used = TracingConfigurator::enabled(&config.exporters.tracing.otlp); - let tracing_datadog_used = config.exporters.tracing.datadog.enabled(); - let tracing_jaeger_used = config.exporters.tracing.jaeger.enabled(); - let tracing_zipkin_used = config.exporters.tracing.zipkin.enabled(); - - if metrics_prom_used - || metrics_otlp_used - || tracing_jaeger_used - || tracing_otlp_used - || tracing_zipkin_used - || tracing_datadog_used - { - ::tracing::info!( - monotonic_counter.apollo.router.operations.telemetry = 1u64, - telemetry.metrics.otlp = metrics_otlp_used.or_empty(), - telemetry.metrics.prometheus = metrics_prom_used.or_empty(), - telemetry.tracing.otlp = tracing_otlp_used.or_empty(), - telemetry.tracing.datadog = tracing_datadog_used.or_empty(), - telemetry.tracing.jaeger = tracing_jaeger_used.or_empty(), - telemetry.tracing.zipkin = tracing_zipkin_used.or_empty(), + let mut attributes = Vec::new(); + if MetricsConfigurator::enabled(&config.exporters.metrics.otlp) { + attributes.push(KeyValue::new("telemetry.metrics.otlp", true)); + } + if config.exporters.metrics.prometheus.enabled { + attributes.push(KeyValue::new("telemetry.metrics.prometheus", true)); + } + if TracingConfigurator::enabled(&config.exporters.tracing.otlp) { + attributes.push(KeyValue::new("telemetry.tracing.otlp", true)); + } + if config.exporters.tracing.datadog.enabled() { + attributes.push(KeyValue::new("telemetry.tracing.datadog", true)); + } + if config.exporters.tracing.jaeger.enabled() { + attributes.push(KeyValue::new("telemetry.tracing.jaeger", true)); + } + if config.exporters.tracing.zipkin.enabled() { + attributes.push(KeyValue::new("telemetry.tracing.zipkin", true)); + } + + if !attributes.is_empty() { + u64_counter!( + "apollo.router.operations.telemetry", + "Telemetry exporters enabled", + 1, + attributes ); } } diff --git a/apollo-router/src/plugins/traffic_shaping/timeout/future.rs b/apollo-router/src/plugins/traffic_shaping/timeout/future.rs index 8a390b393e..eda4100198 100644 --- a/apollo-router/src/plugins/traffic_shaping/timeout/future.rs +++ b/apollo-router/src/plugins/traffic_shaping/timeout/future.rs @@ -49,7 +49,11 @@ where match Pin::new(&mut this.sleep).poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(_) => { - tracing::info!(monotonic_counter.apollo_router_timeout = 1u64,); + u64_counter!( + "apollo_router_timeout", + "Number of timed out client requests", + 1 + ); Poll::Ready(Err(Elapsed::new().into())) } } diff --git a/apollo-router/src/protocols/websocket.rs b/apollo-router/src/protocols/websocket.rs index bd556232ac..13700e84ce 100644 --- a/apollo-router/src/protocols/websocket.rs +++ b/apollo-router/src/protocols/websocket.rs @@ -300,13 +300,10 @@ where request: graphql::Request, heartbeat_interval: Option, ) -> Result, graphql::Error> { - tracing::info!( - monotonic_counter - .apollo - .router - .operations - .subscriptions - .events = 1u64, + u64_counter!( + "apollo.router.operations.subscriptions.events", + "Number of subscription events", + 1, subscriptions.mode = "passthrough" ); @@ -443,13 +440,10 @@ where tracing::trace!("cannot shutdown sink: {err:?}"); }; - tracing::info!( - monotonic_counter - .apollo - .router - .operations - .subscriptions - .events = 1u64, + u64_counter!( + "apollo.router.operations.subscriptions.events", + "Number of subscription events", + 1, subscriptions.mode = "passthrough", subscriptions.complete = true ); diff --git a/apollo-router/src/query_planner/execution.rs b/apollo-router/src/query_planner/execution.rs index bd23f549ea..9a5b647350 100644 --- a/apollo-router/src/query_planner/execution.rs +++ b/apollo-router/src/query_planner/execution.rs @@ -82,7 +82,11 @@ impl QueryPlan { ) .await; if !deferred_fetches.is_empty() { - tracing::info!(monotonic_counter.apollo.router.operations.defer = 1u64); + u64_counter!( + "apollo.router.operations.defer", + "Number of requests that request deferred data", + 1 + ); } Response::builder().data(value).errors(errors).build() diff --git a/apollo-router/src/query_planner/fetch.rs b/apollo-router/src/query_planner/fetch.rs index 47069283ca..3ec89c1e95 100644 --- a/apollo-router/src/query_planner/fetch.rs +++ b/apollo-router/src/query_planner/fetch.rs @@ -521,7 +521,11 @@ impl FetchNode { self.response_at_path(parameters.schema, current_dir, paths, response); if let Some(id) = &self.id { if let Some(sender) = parameters.deferred_fetches.get(id.as_str()) { - tracing::info!(monotonic_counter.apollo.router.operations.defer.fetch = 1u64); + u64_counter!( + "apollo.router.operations.defer.fetch", + "Number of deferred responses fetched from subgraphs", + 1 + ); if let Err(e) = sender.clone().send((value.clone(), errors.clone())) { tracing::error!("error sending fetch result at path {} and id {:?} for deferred response building: {}", current_dir, self.id, e); } diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 457ea67820..48c4dcba1d 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -122,7 +122,11 @@ impl PersistedQueryLayer { .context .extensions() .with_lock(|mut lock| lock.insert(UsedQueryIdFromManifest)); - tracing::info!(monotonic_counter.apollo.router.operations.persisted_queries = 1u64); + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1 + ); Ok(request) } else if manifest_poller.augmenting_apq_with_pre_registration_and_no_safelisting() { // The query ID isn't in our manifest, but we have APQ enabled @@ -131,8 +135,10 @@ impl PersistedQueryLayer { // safelist later for log_unknown!) Ok(request) } else { - tracing::info!( - monotonic_counter.apollo.router.operations.persisted_queries = 1u64, + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1, persisted_queries.not_found = true ); // if APQ is not enabled, return an error indicating the query was not found @@ -209,28 +215,38 @@ impl PersistedQueryLayer { match manifest_poller.action_for_freeform_graphql(Ok(&doc.ast)) { FreeformGraphQLAction::Allow => { - tracing::info!(monotonic_counter.apollo.router.operations.persisted_queries = 1u64,); + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1 + ); Ok(request) } FreeformGraphQLAction::Deny => { - tracing::info!( - monotonic_counter.apollo.router.operations.persisted_queries = 1u64, - persisted_queries.safelist.rejected.unknown = false, + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1, + persisted_queries.safelist.rejected.unknown = false ); Err(supergraph_err_operation_not_in_safelist(request)) } // Note that this might even include complaining about an operation that came via APQs. FreeformGraphQLAction::AllowAndLog => { - tracing::info!( - monotonic_counter.apollo.router.operations.persisted_queries = 1u64, + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1, persisted_queries.logged = true ); log_unknown_operation(operation_body); Ok(request) } FreeformGraphQLAction::DenyAndLog => { - tracing::info!( - monotonic_counter.apollo.router.operations.persisted_queries = 1u64, + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1, persisted_queries.safelist.rejected.unknown = true, persisted_queries.logged = true ); diff --git a/apollo-router/src/services/router/service.rs b/apollo-router/src/services/router/service.rs index e5792c1a4d..acc67e332d 100644 --- a/apollo-router/src/services/router/service.rs +++ b/apollo-router/src/services/router/service.rs @@ -378,8 +378,10 @@ impl RouterService { Ok(RouterResponse { response, context }) } else { - tracing::info!( - monotonic_counter.apollo.router.graphql_error = 1u64, + u64_counter!( + "apollo.router.graphql_error", + "Number of GraphQL error responses returned by the router", + 1, code = "INVALID_ACCEPT_HEADER" ); // Useful for selector in spans/instruments/events @@ -799,12 +801,18 @@ impl RouterService { for (code, count) in map { match code { None => { - tracing::info!(monotonic_counter.apollo.router.graphql_error = count,); + u64_counter!( + "apollo.router.graphql_error", + "Number of GraphQL error responses returned by the router", + count + ); } Some(code) => { - tracing::info!( - monotonic_counter.apollo.router.graphql_error = count, - code = code + u64_counter!( + "apollo.router.graphql_error", + "Number of GraphQL error responses returned by the router", + count, + code = code.to_string() ); } } diff --git a/apollo-router/src/services/subgraph_service.rs b/apollo-router/src/services/subgraph_service.rs index 9dbb9fb773..dc93499143 100644 --- a/apollo-router/src/services/subgraph_service.rs +++ b/apollo-router/src/services/subgraph_service.rs @@ -300,16 +300,20 @@ impl tower::Service for SubgraphService { })?; stream_tx.send(Box::pin(handle.into_stream())).await?; - tracing::info!( - monotonic_counter.apollo.router.operations.subscriptions = 1u64, - subscriptions.mode = %"callback", + u64_counter!( + "apollo.router.operations.subscriptions", + "Total requests with subscription operations", + 1, + subscriptions.mode = "callback", subscriptions.deduplicated = !created, - subgraph.service.name = service_name, + subgraph.service.name = service_name.clone() ); if !created { - tracing::info!( - monotonic_counter.apollo_router_deduplicated_subscriptions_total = 1u64, - mode = %"callback", + u64_counter!( + "apollo_router_deduplicated_subscriptions_total", + "Total deduplicated subscription requests (deprecated)", + 1, + mode = "callback" ); // Dedup happens here return Ok(SubgraphResponse::builder() @@ -507,19 +511,23 @@ async fn call_websocket( let (handle, created) = notify .create_or_subscribe(subscription_hash.clone(), false) .await?; - tracing::info!( - monotonic_counter.apollo.router.operations.subscriptions = 1u64, - subscriptions.mode = %"passthrough", + u64_counter!( + "apollo.router.operations.subscriptions", + "Total requests with subscription operations", + 1, + subscriptions.mode = "passthrough", subscriptions.deduplicated = !created, - subgraph.service.name = service_name, + subgraph.service.name = service_name.clone() ); if !created { subscription_stream_tx .send(Box::pin(handle.into_stream())) .await?; - tracing::info!( - monotonic_counter.apollo_router_deduplicated_subscriptions_total = 1u64, - mode = %"passthrough", + u64_counter!( + "apollo_router_deduplicated_subscriptions_total", + "Total deduplicated subscription requests (deprecated)", + 1, + mode = "passthrough" ); // Dedup happens here @@ -868,9 +876,14 @@ pub(crate) async fn process_batch( subgraph = &service ); - tracing::info!(monotonic_counter.apollo.router.operations.batching = 1u64, - mode = %BatchingMode::BatchHttpLink, // Only supported mode right now - subgraph = &service + u64_counter!( + "apollo.router.operations.batching", + "Total requests with batched operations", + 1, + // XXX(@goto-bus-stop): Should these be `batching.mode`, `batching.subgraph`? + // Also, other metrics use a different convention to report the subgraph name + mode = BatchingMode::BatchHttpLink.to_string(), // Only supported mode right now + subgraph = service.clone() ); // Perform the actual fetch. If this fails then we didn't manage to make the call at all, so we can't do anything with it. diff --git a/apollo-router/src/state_machine.rs b/apollo-router/src/state_machine.rs index e3ce6c3a67..ed5765d4e3 100644 --- a/apollo-router/src/state_machine.rs +++ b/apollo-router/src/state_machine.rs @@ -557,13 +557,20 @@ where #[cfg(test)] self.notify_updated.notify_one(); - tracing::debug!( - monotonic_counter.apollo_router_state_change_total = 1u64, + tracing::info!( event = event_name, state = ?state, previous_state, "state machine transitioned" ); + u64_counter!( + "apollo_router_state_change_total", + "Router state changes", + 1, + event = event_name, + state = format!("{state:?}"), + previous_state = previous_state + ); // If we've errored then exit even if there are potentially more messages if matches!(&state, Stopped | Errored(_)) { diff --git a/apollo-router/src/uplink/mod.rs b/apollo-router/src/uplink/mod.rs index 6a8974699e..2ea483daf4 100644 --- a/apollo-router/src/uplink/mod.rs +++ b/apollo-router/src/uplink/mod.rs @@ -210,7 +210,7 @@ where Response: Send + 'static + Debug, TransformedResponse: Send + 'static + Debug, { - let query = query_name::(); + let query_name = query_name::(); let (sender, receiver) = channel(2); let client = match reqwest::Client::builder() .no_gzip() @@ -245,10 +245,12 @@ where .await { Ok(response) => { - tracing::info!( - monotonic_counter.apollo_router_uplink_fetch_count_total = 1u64, + u64_counter!( + "apollo_router_uplink_fetch_count_total", + "Total number of requests to Apollo Uplink", + 1u64, status = "success", - query + query = query_name ); match response { UplinkResponse::New { @@ -294,10 +296,12 @@ where } } Err(err) => { - tracing::info!( - monotonic_counter.apollo_router_uplink_fetch_count_total = 1u64, + u64_counter!( + "apollo_router_uplink_fetch_count_total", + "Total number of requests to Apollo Uplink", + 1u64, status = "failure", - query + query = query_name ); if let Err(e) = sender.send(Err(err)).await { tracing::debug!("failed to send error to uplink stream. This is likely to be because the router is shutting down: {e}"); diff --git a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx index 9bcf4c83fa..dead9fb70b 100644 --- a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx @@ -16,11 +16,10 @@ These instruments can be consumed by configuring a [metrics exporter](/router/co - `apollo_router_http_request_duration_seconds_bucket` - HTTP subgraph request duration, attributes: - `subgraph`: (Optional) The subgraph being queried - `apollo_router_http_requests_total` - Total number of HTTP requests by HTTP status -- `apollo_router_timeout` - Number of triggered timeouts ### GraphQL -- `apollo_router_graphql_error` - counts GraphQL errors in responses, attributes: +- `apollo.router.graphql_error` - counts GraphQL errors in responses, attributes: - `code`: error code ### Session @@ -122,8 +121,17 @@ The initial call to Uplink during router startup is not reflected in metrics. The following metrics have been deprecated and should not be used. -- `apollo_router_span` - **Deprecated**—use `apollo_router_processing_time` instead. -- `apollo_router_http_request_retry_total` **Deprecated**- Number of subgraph requests retried. You should use: +- `apollo_router_span` - **Deprecated**: use `apollo_router_processing_time` instead. +- `apollo_router_deduplicated_subscriptions_total` - **Deprecated**: use the `apollo.router.operations.subscriptions` metric's `subscriptions.deduplicated` attribute. +- `apollo_authentication_failure_count` - **Deprecated**: use the `apollo.router.operations.authentication.jwt` metric's `authentication.jwt.failed` attribute. +- `apollo_authentication_success_count` - **Deprecated**: use the `apollo.router.operations.authentication.jwt` metric instead. If the `authentication.jwt.failed` attribute is *absent* or `false`, the authentication succeeded. +- `apollo_require_authentication_failure_count` - **Deprecated**: use the `http.server.request.duration` metric's `http.response.status_code` attribute. Requests with authentication failures have HTTP status code 401. +- `apollo_router_timeout` - **Deprecated**: this metric conflates timed-out requests from client to the router, and requests from the router to subgraphs. Timed-out requests have HTTP status code 504. Use the `http.response.status_code` attribute on the `http.server.request.duration` metric to identify timed-out router requests, and the same attribute on the `http.client.request.duration` metric to identify timed-out subgraph requests. +- `apollo_router_http_request_retry_total` **Deprecated**: this can be achieved with custom telemetry instead. See [below for an example](#migrate-from-apollo_router_http_request_retry_total). + +#### Migrate from `apollo_router_http_request_retry_total` + +The below configuration filters the deprecated metric, and defines a custom histogram recording the number of retried HTTP requests to subgraphs. ```yaml title="config.router.yaml" telemetry: @@ -158,4 +166,4 @@ telemetry: subgraph.name: true supergraph.operation.name: supergraph_operation_name: string -``` \ No newline at end of file +``` From dcb192c7618581c3315306fe63bd45694d7e6231 Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Tue, 3 Dec 2024 16:45:20 +0100 Subject: [PATCH 053/112] add better migration for deprecated metric apollo_router_http_request_retry_total (#6384) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- .../telemetry/config_new/attributes.rs | 3 ++ .../instrumentation/standard-instruments.mdx | 41 +------------------ 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/config_new/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/attributes.rs index 0da52d42f2..e21626200f 100644 --- a/apollo-router/src/plugins/telemetry/config_new/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/attributes.rs @@ -264,6 +264,9 @@ impl DefaultForLevel for SubgraphAttributes { if self.graphql_operation_type.is_none() { self.graphql_operation_type = Some(StandardAttribute::Bool(true)); } + if self.http_request_resend_count.is_none() { + self.http_request_resend_count = Some(StandardAttribute::Bool(true)); + } } DefaultAttributeRequirementLevel::None => {} } diff --git a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx index dead9fb70b..499e6cc043 100644 --- a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx @@ -127,43 +127,4 @@ The following metrics have been deprecated and should not be used. - `apollo_authentication_success_count` - **Deprecated**: use the `apollo.router.operations.authentication.jwt` metric instead. If the `authentication.jwt.failed` attribute is *absent* or `false`, the authentication succeeded. - `apollo_require_authentication_failure_count` - **Deprecated**: use the `http.server.request.duration` metric's `http.response.status_code` attribute. Requests with authentication failures have HTTP status code 401. - `apollo_router_timeout` - **Deprecated**: this metric conflates timed-out requests from client to the router, and requests from the router to subgraphs. Timed-out requests have HTTP status code 504. Use the `http.response.status_code` attribute on the `http.server.request.duration` metric to identify timed-out router requests, and the same attribute on the `http.client.request.duration` metric to identify timed-out subgraph requests. -- `apollo_router_http_request_retry_total` **Deprecated**: this can be achieved with custom telemetry instead. See [below for an example](#migrate-from-apollo_router_http_request_retry_total). - -#### Migrate from `apollo_router_http_request_retry_total` - -The below configuration filters the deprecated metric, and defines a custom histogram recording the number of retried HTTP requests to subgraphs. - -```yaml title="config.router.yaml" -telemetry: - exporters: - metrics: - common: - views: - - name: apollo_router_http_request_retry_total # Drop the deprecated metric - aggregation: drop - - name: http.client.request.retry # Set the right buckets for the new metric - aggregation: - histogram: - buckets: - - 1 - - 10 - - 100 - - 1000 - instrumentation: - instruments: - subgraph: - http.client.request.retry: # Create custom histogram measuring the number of retries for http request to a subgraph - type: histogram - value: - subgraph_resend_count: true - unit: hit - description: histogram of subgraph request resend count - condition: - gt: - - subgraph_resend_count: true - - 0 - attributes: - subgraph.name: true - supergraph.operation.name: - supergraph_operation_name: string -``` +- `apollo_router_http_request_retry_total` **Deprecated**: use the `http.client.request.duration` metric's `http.request.resend_count` attribute. Requests with retries will contains `http.request.resend_count` set with the number of retries. \ No newline at end of file From 21d78ccee68635efeff0dc7d17bf6f8704d03707 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Wed, 4 Dec 2024 10:35:15 +0100 Subject: [PATCH 054/112] plugins/fleet_detector: detect if the Router was deployed using the official Apollo Helm chart --- apollo-router/src/plugins/fleet_detector.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 5e3d9c7513..302d1aba76 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -23,6 +23,7 @@ use crate::plugin::PluginPrivate; const REFRESH_INTERVAL: Duration = Duration::from_secs(60); const COMPUTE_DETECTOR_THRESHOLD: u16 = 24576; +const OFFICIAL_HELM_CHART_VAR: &str = "APOLLO_ROUTER_OFFICIAL_HELM_CHART"; #[derive(Debug, Default, Deserialize, JsonSchema)] struct Conf {} @@ -69,6 +70,7 @@ impl GaugeStore { let meter = meter_provider().meter("apollo/router"); let mut gauges = Vec::new(); + // apollo.router.instance { let mut attributes = Vec::new(); // CPU architecture @@ -88,6 +90,8 @@ impl GaugeStore { attributes.push(KeyValue::new("cloud.provider", cloud_provider.code())); } } + // Official helm chart + attributes.push(KeyValue::new("deployment.type", get_deployment_type())); gauges.push( meter .u64_observable_gauge("apollo.router.instance") @@ -98,6 +102,7 @@ impl GaugeStore { .init(), ); } + // apollo.router.instance.cpu_freq { let system_getter = system_getter.clone(); gauges.push( @@ -119,6 +124,7 @@ impl GaugeStore { .init(), ); } + // apollo.router.instance.cpu_count { let system_getter = system_getter.clone(); gauges.push( @@ -137,6 +143,7 @@ impl GaugeStore { .init(), ); } + // apollo.router.instance.total_memory { let system_getter = system_getter.clone(); gauges.push( @@ -291,4 +298,11 @@ fn get_otel_os() -> &'static str { } } +fn get_deployment_type() -> &'static str { + if std::env::var_os(OFFICIAL_HELM_CHART_VAR).is_some() { + return "official_helm_chart"; + } + "unknown" +} + register_private_plugin!("apollo", "fleet_detector", FleetDetector); From 0b3cdb76ebb9342ce220f05932b8d8e94c1e8878 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Wed, 4 Dec 2024 10:36:58 +0100 Subject: [PATCH 055/112] chore: fix comments --- apollo-router/src/plugins/fleet_detector.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 302d1aba76..7b2882770b 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -90,7 +90,7 @@ impl GaugeStore { attributes.push(KeyValue::new("cloud.provider", cloud_provider.code())); } } - // Official helm chart + // Deployment type attributes.push(KeyValue::new("deployment.type", get_deployment_type())); gauges.push( meter @@ -299,6 +299,7 @@ fn get_otel_os() -> &'static str { } fn get_deployment_type() -> &'static str { + // Official Apollo helm chart if std::env::var_os(OFFICIAL_HELM_CHART_VAR).is_some() { return "official_helm_chart"; } From 85f99f19e2a2907bd5597687773d68d63644a0c6 Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Wed, 4 Dec 2024 11:21:53 +0100 Subject: [PATCH 056/112] remove fix for experimental_retry (#6392) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- .../src/plugins/traffic_shaping/retry.rs | 104 +----------------- 1 file changed, 5 insertions(+), 99 deletions(-) diff --git a/apollo-router/src/plugins/traffic_shaping/retry.rs b/apollo-router/src/plugins/traffic_shaping/retry.rs index 2cf9132640..d04101dc3e 100644 --- a/apollo-router/src/plugins/traffic_shaping/retry.rs +++ b/apollo-router/src/plugins/traffic_shaping/retry.rs @@ -43,45 +43,11 @@ impl Policy for RetryPolicy { ) -> Option { let subgraph_name = req.subgraph_name.clone().unwrap_or_default(); match result { - Ok(resp) => { - if resp.response.status() >= http::StatusCode::BAD_REQUEST { - if req.operation_kind == OperationKind::Mutation && !self.retry_mutations { - return None; - } - - let withdrew = self.budget.withdraw(); - if withdrew.is_err() { - u64_counter!( - "apollo_router_http_request_retry_total", - "Number of retries for an http request to a subgraph", - 1u64, - status = "aborted", - subgraph = subgraph_name - ); - - return None; - } - - let _ = req - .context - .upsert::<_, usize>(SubgraphRequestResendCountKey::new(&req.id), |val| { - val + 1 - }); - - u64_counter!( - "apollo_router_http_request_retry_total", - "Number of retries for an http request to a subgraph", - 1u64, - subgraph = subgraph_name - ); - - Some(future::ready(self.clone())) - } else { - // Treat all `Response`s as success, - // so deposit budget and don't retry... - self.budget.deposit(); - None - } + Ok(_resp) => { + // Treat all `Response`s as success, + // so deposit budget and don't retry... + self.budget.deposit(); + None } Err(_e) => { if req.operation_kind == OperationKind::Mutation && !self.retry_mutations { @@ -123,9 +89,6 @@ impl Policy for RetryPolicy { #[cfg(test)] mod tests { - use http::StatusCode; - use tower::BoxError; - use super::*; use crate::error::FetchError; use crate::graphql; @@ -188,61 +151,4 @@ mod tests { .with_metrics() .await; } - - #[tokio::test] - async fn test_retry_with_http_status_code() { - async { - let retry = RetryPolicy::new( - Some(Duration::from_secs(10)), - Some(10), - Some(0.2), - Some(false), - ); - - let subgraph_req = subgraph::Request::fake_builder() - .subgraph_name("my_subgraph_name_error") - .subgraph_request( - http_ext::Request::fake_builder() - .header("test", "my_value_set") - .body( - graphql::Request::fake_builder() - .query(String::from("query { test }")) - .build(), - ) - .build() - .unwrap(), - ) - .build(); - - assert!(retry - .retry( - &subgraph_req, - Ok::<&subgraph::Response, &BoxError>( - &subgraph::Response::fake_builder() - .status_code(StatusCode::BAD_REQUEST) - .build() - ) - ) - .is_some()); - - assert!(retry - .retry( - &subgraph_req, - Ok::<&subgraph::Response, &BoxError>( - &subgraph::Response::fake_builder() - .status_code(StatusCode::BAD_REQUEST) - .build() - ) - ) - .is_some()); - - assert_counter!( - "apollo_router_http_request_retry_total", - 2, - "subgraph" = "my_subgraph_name_error" - ); - } - .with_metrics() - .await; - } } From 430c4e437c2cb33975e2f38c416d0f71add20650 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Wed, 4 Dec 2024 12:19:42 +0000 Subject: [PATCH 057/112] add changeset --- .changesets/feat_feat_fleet_detector_add_schema_metrics.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changesets/feat_feat_fleet_detector_add_schema_metrics.md diff --git a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md new file mode 100644 index 0000000000..a232a333f5 --- /dev/null +++ b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md @@ -0,0 +1,6 @@ +### Adds Fleet Awareness Schema Metrics + +Adds an additional metric to the `fleet_detector` plugin in the form of `apollo.router.schema` with 2 attributes: +`schema_hash` and `launch_id`. + +By [@loshz](https://github.com/loshz) in https://github.com/apollographql/router/pull/6283 From f8c7e3cb82fc4d242d13881337e6583eec7555c0 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Wed, 4 Dec 2024 14:25:32 +0100 Subject: [PATCH 058/112] feat: connect launch ID --- apollo-router/src/router/event/schema.rs | 7 ++--- apollo-router/src/spec/schema.rs | 17 +++++++---- apollo-router/src/state_machine.rs | 2 +- apollo-router/src/uplink/schema.rs | 16 +++++++++- apollo-router/src/uplink/schema_stream.rs | 36 +++++++++++++++++++++++ 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/apollo-router/src/router/event/schema.rs b/apollo-router/src/router/event/schema.rs index b728444377..f43e3dea4e 100644 --- a/apollo-router/src/router/event/schema.rs +++ b/apollo-router/src/router/event/schema.rs @@ -143,14 +143,11 @@ impl SchemaSource { } } SchemaSource::Registry(uplink_config) => { - stream_from_uplink::(uplink_config) + stream_from_uplink::(uplink_config) .filter_map(|res| { future::ready(match res { Ok(schema) => { - let update_schema = UpdateSchema(SchemaState { - sdl: schema, - launch_id: None, - }); + let update_schema = UpdateSchema(schema); Some(update_schema) } Err(e) => { diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index 114dcc1614..8bfda05e64 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -20,6 +20,7 @@ use sha2::Sha256; use crate::error::ParseErrors; use crate::error::SchemaError; use crate::query_planner::OperationKind; +use crate::uplink::schema::SchemaState; use crate::Configuration; /// A GraphQL schema. @@ -39,16 +40,16 @@ pub(crate) struct ApiSchema(pub(crate) ValidFederationSchema); impl Schema { pub(crate) fn parse(raw_sdl: &str, config: &Configuration) -> Result { - Self::parse_arc(raw_sdl.to_owned().into(), config) + Self::parse_arc(raw_sdl.parse::().unwrap().into(), config) } pub(crate) fn parse_arc( - raw_sdl: Arc, + raw_sdl: Arc, config: &Configuration, ) -> Result { let start = Instant::now(); let mut parser = apollo_compiler::parser::Parser::new(); - let result = parser.parse_ast(raw_sdl.as_ref(), "schema.graphql"); + let result = parser.parse_ast(&raw_sdl.sdl, "schema.graphql"); // Trace log recursion limit data let recursion_limit = parser.recursion_reached(); @@ -111,7 +112,7 @@ impl Schema { let implementers_map = definitions.implementers_map(); let supergraph = Supergraph::from_schema(definitions)?; - let schema_id = Arc::new(Schema::schema_id(&raw_sdl)); + let schema_id = Arc::new(Schema::schema_id(&raw_sdl.sdl)); let api_schema = supergraph .to_api_schema(ApiSchemaOptions { @@ -125,13 +126,17 @@ impl Schema { })?; Ok(Schema { - raw_sdl, + launch_id: raw_sdl + .launch_id + .as_ref() + .map(ToString::to_string) + .map(Arc::new), + raw_sdl: Arc::new(raw_sdl.sdl.to_string()), supergraph, subgraphs, implementers_map, api_schema: ApiSchema(api_schema), schema_id, - launch_id: None, // TODO: get from uplink }) } diff --git a/apollo-router/src/state_machine.rs b/apollo-router/src/state_machine.rs index ee5d06f3b6..eb00a12ce9 100644 --- a/apollo-router/src/state_machine.rs +++ b/apollo-router/src/state_machine.rs @@ -319,7 +319,7 @@ impl State { FA: RouterSuperServiceFactory, { let schema = Arc::new( - Schema::parse_arc(Arc::new(schema_state.sdl.clone()), &configuration) + Schema::parse_arc(schema_state.clone(), &configuration) .map_err(|e| ServiceCreationError(e.to_string().into()))?, ); // Check the license diff --git a/apollo-router/src/uplink/schema.rs b/apollo-router/src/uplink/schema.rs index 0c9b24953e..57cce6ba0c 100644 --- a/apollo-router/src/uplink/schema.rs +++ b/apollo-router/src/uplink/schema.rs @@ -1,6 +1,20 @@ +use std::convert::Infallible; +use std::str::FromStr; + /// Represents the new state of a schema after an update. -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Debug)] pub(crate) struct SchemaState { pub(crate) sdl: String, pub(crate) launch_id: Option, } + +impl FromStr for SchemaState { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self { + sdl: s.to_string(), + launch_id: None, + }) + } +} diff --git a/apollo-router/src/uplink/schema_stream.rs b/apollo-router/src/uplink/schema_stream.rs index ee1dcbda27..0cbbc78060 100644 --- a/apollo-router/src/uplink/schema_stream.rs +++ b/apollo-router/src/uplink/schema_stream.rs @@ -5,6 +5,7 @@ use graphql_client::GraphQLQuery; +use super::schema::SchemaState; use crate::uplink::schema_stream::supergraph_sdl_query::FetchErrorCode; use crate::uplink::schema_stream::supergraph_sdl_query::SupergraphSdlQueryRouterConfig; use crate::uplink::UplinkRequest; @@ -31,6 +32,41 @@ impl From for supergraph_sdl_query::Variables { } } +impl From for UplinkResponse { + fn from(response: supergraph_sdl_query::ResponseData) -> Self { + match response.router_config { + SupergraphSdlQueryRouterConfig::RouterConfigResult(result) => UplinkResponse::New { + response: SchemaState { + sdl: result.supergraph_sdl, + launch_id: Some(result.id.clone()), + }, + id: result.id, + // this will truncate the number of seconds to under u64::MAX, which should be + // a large enough delay anyway + delay: result.min_delay_seconds as u64, + }, + SupergraphSdlQueryRouterConfig::Unchanged(response) => UplinkResponse::Unchanged { + id: Some(response.id), + delay: Some(response.min_delay_seconds as u64), + }, + SupergraphSdlQueryRouterConfig::FetchError(err) => UplinkResponse::Error { + retry_later: err.code == FetchErrorCode::RETRY_LATER, + code: match err.code { + FetchErrorCode::AUTHENTICATION_FAILED => "AUTHENTICATION_FAILED".to_string(), + FetchErrorCode::ACCESS_DENIED => "ACCESS_DENIED".to_string(), + FetchErrorCode::UNKNOWN_REF => "UNKNOWN_REF".to_string(), + FetchErrorCode::RETRY_LATER => "RETRY_LATER".to_string(), + FetchErrorCode::NOT_IMPLEMENTED_ON_THIS_INSTANCE => { + "NOT_IMPLEMENTED_ON_THIS_INSTANCE".to_string() + } + FetchErrorCode::Other(other) => other, + }, + message: err.message, + }, + } + } +} + impl From for UplinkResponse { fn from(response: supergraph_sdl_query::ResponseData) -> Self { match response.router_config { From 188e8dee194ebe729bc2e84d6ca760447247405c Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Wed, 4 Dec 2024 13:38:24 +0000 Subject: [PATCH 059/112] update changeset --- ..._feat_fleet_detector_add_schema_metrics.md | 2 +- apollo-router/src/uplink/schema_stream.rs | 33 ++----------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md index a232a333f5..472682897f 100644 --- a/.changesets/feat_feat_fleet_detector_add_schema_metrics.md +++ b/.changesets/feat_feat_fleet_detector_add_schema_metrics.md @@ -3,4 +3,4 @@ Adds an additional metric to the `fleet_detector` plugin in the form of `apollo.router.schema` with 2 attributes: `schema_hash` and `launch_id`. -By [@loshz](https://github.com/loshz) in https://github.com/apollographql/router/pull/6283 +By [@loshz](https://github.com/loshz) and [@nmoutschen](https://github.com/nmoutschen) in https://github.com/apollographql/router/pull/6283 diff --git a/apollo-router/src/uplink/schema_stream.rs b/apollo-router/src/uplink/schema_stream.rs index 0cbbc78060..5c11e56c31 100644 --- a/apollo-router/src/uplink/schema_stream.rs +++ b/apollo-router/src/uplink/schema_stream.rs @@ -32,38 +32,9 @@ impl From for supergraph_sdl_query::Variables { } } -impl From for UplinkResponse { +impl From for UplinkResponse { fn from(response: supergraph_sdl_query::ResponseData) -> Self { - match response.router_config { - SupergraphSdlQueryRouterConfig::RouterConfigResult(result) => UplinkResponse::New { - response: SchemaState { - sdl: result.supergraph_sdl, - launch_id: Some(result.id.clone()), - }, - id: result.id, - // this will truncate the number of seconds to under u64::MAX, which should be - // a large enough delay anyway - delay: result.min_delay_seconds as u64, - }, - SupergraphSdlQueryRouterConfig::Unchanged(response) => UplinkResponse::Unchanged { - id: Some(response.id), - delay: Some(response.min_delay_seconds as u64), - }, - SupergraphSdlQueryRouterConfig::FetchError(err) => UplinkResponse::Error { - retry_later: err.code == FetchErrorCode::RETRY_LATER, - code: match err.code { - FetchErrorCode::AUTHENTICATION_FAILED => "AUTHENTICATION_FAILED".to_string(), - FetchErrorCode::ACCESS_DENIED => "ACCESS_DENIED".to_string(), - FetchErrorCode::UNKNOWN_REF => "UNKNOWN_REF".to_string(), - FetchErrorCode::RETRY_LATER => "RETRY_LATER".to_string(), - FetchErrorCode::NOT_IMPLEMENTED_ON_THIS_INSTANCE => { - "NOT_IMPLEMENTED_ON_THIS_INSTANCE".to_string() - } - FetchErrorCode::Other(other) => other, - }, - message: err.message, - }, - } + UplinkResponse::::from(response) } } From c5e43ea1967bed23fd94ca1024eccc26351e0e26 Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Wed, 4 Dec 2024 13:41:54 +0000 Subject: [PATCH 060/112] fix from type --- apollo-router/src/uplink/schema_stream.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/uplink/schema_stream.rs b/apollo-router/src/uplink/schema_stream.rs index 5c11e56c31..0817b7b675 100644 --- a/apollo-router/src/uplink/schema_stream.rs +++ b/apollo-router/src/uplink/schema_stream.rs @@ -38,7 +38,7 @@ impl From for UplinkResponse { } } -impl From for UplinkResponse { +impl From for UplinkResponse { fn from(response: supergraph_sdl_query::ResponseData) -> Self { match response.router_config { SupergraphSdlQueryRouterConfig::RouterConfigResult(result) => UplinkResponse::New { From ec62b5c3f66a49669decbc0c9291339118afc3eb Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Wed, 4 Dec 2024 14:45:34 +0000 Subject: [PATCH 061/112] fix schema source from --- apollo-router/src/uplink/schema_stream.rs | 33 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/apollo-router/src/uplink/schema_stream.rs b/apollo-router/src/uplink/schema_stream.rs index 0817b7b675..376f600e74 100644 --- a/apollo-router/src/uplink/schema_stream.rs +++ b/apollo-router/src/uplink/schema_stream.rs @@ -34,7 +34,33 @@ impl From for supergraph_sdl_query::Variables { impl From for UplinkResponse { fn from(response: supergraph_sdl_query::ResponseData) -> Self { - UplinkResponse::::from(response) + match response.router_config { + SupergraphSdlQueryRouterConfig::RouterConfigResult(result) => UplinkResponse::New { + response: result.supergraph_sdl, + id: result.id, + // this will truncate the number of seconds to under u64::MAX, which should be + // a large enough delay anyway + delay: result.min_delay_seconds as u64, + }, + SupergraphSdlQueryRouterConfig::Unchanged(response) => UplinkResponse::Unchanged { + id: Some(response.id), + delay: Some(response.min_delay_seconds as u64), + }, + SupergraphSdlQueryRouterConfig::FetchError(err) => UplinkResponse::Error { + retry_later: err.code == FetchErrorCode::RETRY_LATER, + code: match err.code { + FetchErrorCode::AUTHENTICATION_FAILED => "AUTHENTICATION_FAILED".to_string(), + FetchErrorCode::ACCESS_DENIED => "ACCESS_DENIED".to_string(), + FetchErrorCode::UNKNOWN_REF => "UNKNOWN_REF".to_string(), + FetchErrorCode::RETRY_LATER => "RETRY_LATER".to_string(), + FetchErrorCode::NOT_IMPLEMENTED_ON_THIS_INSTANCE => { + "NOT_IMPLEMENTED_ON_THIS_INSTANCE".to_string() + } + FetchErrorCode::Other(other) => other, + }, + message: err.message, + }, + } } } @@ -42,7 +68,10 @@ impl From for UplinkResponse { fn from(response: supergraph_sdl_query::ResponseData) -> Self { match response.router_config { SupergraphSdlQueryRouterConfig::RouterConfigResult(result) => UplinkResponse::New { - response: result.supergraph_sdl, + response: SchemaState { + sdl: result.supergraph_sdl, + launch_id: Some(result.id.clone()), + }, id: result.id, // this will truncate the number of seconds to under u64::MAX, which should be // a large enough delay anyway From 2a1492f244ce3d6eba0f34e0584925d1f6e8385b Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Wed, 4 Dec 2024 15:49:08 +0100 Subject: [PATCH 062/112] update hashbrown dep for compliance (#6395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are getting errors with an outdated version of hashbrown that needs to updated due to a bug in borsch serialisation 🍲 . This updates us to a version of hashbrown that fixes this vulnerability. ``` error[vulnerability]: Borsh serialization of HashMap is non-canonical ┌─ /Users/ira/Code/apollographql/router/Cargo.lock:261:1 │ 261 │ hashbrown 0.15.0 registry+https://github.com/rust-lang/crates.io-index │ ---------------------------------------------------------------------- security vulnerability detected │ = ID: RUSTSEC-2024-0402 = Advisory: https://rustsec.org/advisories/RUSTSEC-2024-0402 = The borsh serialization of the HashMap did not follow the borsh specification. It potentially produced non-canonical encodings dependent on insertion order. It also did not perform canonicty checks on decoding. ``` --- Cargo.lock | 6 +++--- apollo-federation/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a91864cf9b..8b403ed5ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,7 +209,7 @@ dependencies = [ "apollo-compiler", "derive_more", "either", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "hex", "indexmap 2.2.6", "insta", @@ -3136,9 +3136,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index c0103f435e..41b058ac17 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -22,7 +22,7 @@ time = { version = "0.3.34", default-features = false, features = [ "local-offset", ] } derive_more = "0.99.17" -hashbrown = "0.15.0" +hashbrown = "0.15.1" indexmap = { version = "2.2.6", features = ["serde"] } itertools = "0.13.0" lazy_static = "1.4.0" From 297678d5e5b7b9b8999220903b6c8c23369e0836 Mon Sep 17 00:00:00 2001 From: bryn Date: Wed, 4 Dec 2024 14:40:21 +0000 Subject: [PATCH 063/112] Fix coprocessor empty body panic If a coprocessor responds with an empty body at the supergraph stage then the router would panic as there was a false expectation that the query had already been validated. There is a deeper issue around coprocessors and their ability to modify the query before processing. As query analysis and pluginss may use the query to populate context modifying the query won't necessarily do what the user wants. In fact the router will simply ignore the modified query as the parsed version is already in context. --- .../fix_address_dentist_buyer_frown.md | 13 ++ .../src/services/supergraph/service.rs | 6 +- .../tests/integration/coprocessor.rs | 121 ++++++++++++++++++ .../fixtures/coprocessor.router.yaml | 24 ++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 .changesets/fix_address_dentist_buyer_frown.md create mode 100644 apollo-router/tests/integration/fixtures/coprocessor.router.yaml diff --git a/.changesets/fix_address_dentist_buyer_frown.md b/.changesets/fix_address_dentist_buyer_frown.md new file mode 100644 index 0000000000..d3a75eba72 --- /dev/null +++ b/.changesets/fix_address_dentist_buyer_frown.md @@ -0,0 +1,13 @@ +### Fix coprocessor empty body object panic ([PR #6398](https://github.com/apollographql/router/pull/6398)) +If a coprocessor responds with an empty body object at the supergraph stage then the router would panic. + +```json +{ + ... // other fields + "body": {} // empty object +} +``` + +This does not affect coprocessors that respond with formed responses. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6398 diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index 3f80ecf6f6..b059c7aa07 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -175,11 +175,15 @@ async fn service_call( body.operation_name.clone(), context.clone(), schema.clone(), + // We cannot assume that the query is present as it may have been modified by coprocessors or plugins. + // There is a deeper issue here in that query analysis is doing a bunch of stuff that it should not and + // places the results in context. Therefore plugins that have modified the query won't actually take effect. + // However, this can't be resolved before looking at the pipeline again. req.supergraph_request .body() .query .clone() - .expect("query presence was checked before"), + .unwrap_or_default(), ) .await { diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index d1492fbc87..d9ce741892 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -83,6 +83,127 @@ async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_coprocessor_response_handling() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + test_full_pipeline(400, "RouterRequest", empty_body_string).await; + test_full_pipeline(200, "RouterResponse", empty_body_string).await; + test_full_pipeline(500, "SupergraphRequest", empty_body_string).await; + test_full_pipeline(500, "SupergraphResponse", empty_body_string).await; + test_full_pipeline(200, "SubgraphRequest", empty_body_string).await; + test_full_pipeline(200, "SubgraphResponse", empty_body_string).await; + test_full_pipeline(500, "ExecutionRequest", empty_body_string).await; + test_full_pipeline(500, "ExecutionResponse", empty_body_string).await; + + test_full_pipeline(500, "RouterRequest", empty_body_object).await; + test_full_pipeline(500, "RouterResponse", empty_body_object).await; + test_full_pipeline(200, "SupergraphRequest", empty_body_object).await; + test_full_pipeline(200, "SupergraphResponse", empty_body_object).await; + test_full_pipeline(200, "SubgraphRequest", empty_body_object).await; + test_full_pipeline(200, "SubgraphResponse", empty_body_object).await; + test_full_pipeline(200, "ExecutionRequest", empty_body_object).await; + test_full_pipeline(200, "ExecutionResponse", empty_body_object).await; + + test_full_pipeline(200, "RouterRequest", remove_body).await; + test_full_pipeline(200, "RouterResponse", remove_body).await; + test_full_pipeline(200, "SupergraphRequest", remove_body).await; + test_full_pipeline(200, "SupergraphResponse", remove_body).await; + test_full_pipeline(200, "SubgraphRequest", remove_body).await; + test_full_pipeline(200, "SubgraphResponse", remove_body).await; + test_full_pipeline(200, "ExecutionRequest", remove_body).await; + test_full_pipeline(200, "ExecutionResponse", remove_body).await; + + test_full_pipeline(500, "RouterRequest", null_out_response).await; + test_full_pipeline(500, "RouterResponse", null_out_response).await; + test_full_pipeline(500, "SupergraphRequest", null_out_response).await; + test_full_pipeline(500, "SupergraphResponse", null_out_response).await; + test_full_pipeline(200, "SubgraphRequest", null_out_response).await; + test_full_pipeline(200, "SubgraphResponse", null_out_response).await; + test_full_pipeline(500, "ExecutionRequest", null_out_response).await; + test_full_pipeline(500, "ExecutionResponse", null_out_response).await; + Ok(()) +} + +fn empty_body_object(mut body: serde_json::Value) -> serde_json::Value { + *body + .as_object_mut() + .expect("body") + .get_mut("body") + .expect("body") = serde_json::Value::Object(serde_json::Map::new()); + body +} + +fn empty_body_string(mut body: serde_json::Value) -> serde_json::Value { + *body + .as_object_mut() + .expect("body") + .get_mut("body") + .expect("body") = serde_json::Value::String("".to_string()); + body +} + +fn remove_body(mut body: serde_json::Value) -> serde_json::Value { + body.as_object_mut().expect("body").remove("body"); + body +} + +fn null_out_response(_body: serde_json::Value) -> serde_json::Value { + serde_json::Value::String("".to_string()) +} + +async fn test_full_pipeline( + response_status: u16, + stage: &'static str, + coprocessor: impl Fn(serde_json::Value) -> serde_json::Value + Send + Sync + 'static, +) { + let mock_server = wiremock::MockServer::start().await; + let coprocessor_address = mock_server.uri(); + + // Expect a small query + Mock::given(method("POST")) + .and(path("/")) + .respond_with(move |req: &wiremock::Request| { + let mut body = req.body_json::().expect("body"); + if body + .as_object() + .unwrap() + .get("stage") + .unwrap() + .as_str() + .unwrap() + == stage + { + body = coprocessor(body); + } + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&mock_server) + .await; + + let mut router = IntegrationTest::builder() + .config( + include_str!("fixtures/coprocessor.router.yaml") + .replace("", &coprocessor_address), + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router.execute_default_query().await; + assert_eq!( + response.status(), + response_status, + "Failed at stage {}", + stage + ); + + router.graceful_shutdown().await; +} + #[tokio::test(flavor = "multi_thread")] async fn test_coprocessor_demand_control_access() -> Result<(), BoxError> { if !graph_os_enabled() { diff --git a/apollo-router/tests/integration/fixtures/coprocessor.router.yaml b/apollo-router/tests/integration/fixtures/coprocessor.router.yaml new file mode 100644 index 0000000000..a408b3a203 --- /dev/null +++ b/apollo-router/tests/integration/fixtures/coprocessor.router.yaml @@ -0,0 +1,24 @@ +# This coprocessor doesn't point to anything +coprocessor: + url: "" + router: + request: + body: true + response: + body: true + supergraph: + request: + body: true + response: + body: true + subgraph: + all: + request: + body: true + response: + body: true + execution: + request: + body: true + response: + body: true \ No newline at end of file From ce37c70945182780f3514d7361e353c11f4a2f87 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Thu, 5 Dec 2024 04:43:33 -0600 Subject: [PATCH 064/112] Drop experimental reuse fragments optimization from RS QP (#6354) Drop support for the experimental reuse fragment query optimization from Rust QP. This implementation was not only very slow but also very buggy due to its complexity. `experimental_reuse_query_fragments` option can be applied for the JS QP only. Auto generation of fragments is a much simpler (and faster) algorithm that in most cases produces better results. Fragment auto generation is the default optimization since v1.58 release. Co-authored-by: Iryna Shestak --- .changesets/breaking_drop_reuse_fragment.md | 7 + apollo-federation/cli/src/main.rs | 5 - apollo-federation/src/operation/mod.rs | 181 +- apollo-federation/src/operation/optimize.rs | 3480 +---------------- apollo-federation/src/operation/rebase.rs | 737 +--- .../src/operation/selection_map.rs | 5 - apollo-federation/src/operation/tests/mod.rs | 404 +- .../src/query_plan/fetch_dependency_graph.rs | 3 +- .../src/query_plan/query_planner.rs | 206 +- .../query_plan/build_query_plan_tests.rs | 4 +- .../build_query_plan_tests/entities.rs | 142 + .../fragment_autogeneration.rs | 462 ++- .../build_query_plan_tests/named_fragments.rs | 563 --- .../named_fragments_expansion.rs | 369 ++ .../named_fragments_preservation.rs | 1384 ------- ...nt_entity_fetches_to_same_subgraph.graphql | 97 + .../it_expands_nested_fragments.graphql | 75 + ...tion_from_operation_with_fragments.graphql | 102 + .../it_preserves_directives.graphql | 68 + ..._directives_on_collapsed_fragments.graphql | 75 + .../0031-reuse-query-fragments.yaml | 6 + apollo-router/src/configuration/mod.rs | 10 +- ...nfiguration__tests__schema_generation.snap | 2 +- apollo-router/src/configuration/tests.rs | 16 - .../src/query_planner/bridge_query_planner.rs | 4 - .../src/services/supergraph/tests.rs | 113 - ...g_update_reuse_query_fragments.router.yaml | 3 +- apollo-router/tests/integration/redis.rs | 24 +- .../source/reference/router/configuration.mdx | 32 +- 29 files changed, 2058 insertions(+), 6521 deletions(-) create mode 100644 .changesets/breaking_drop_reuse_fragment.md create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/entities.rs delete mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_expansion.rs delete mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs create mode 100644 apollo-federation/tests/query_plan/supergraphs/inefficient_entity_fetches_to_same_subgraph.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_expands_nested_fragments.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation_from_operation_with_fragments.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_preserves_directives.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_on_collapsed_fragments.graphql create mode 100644 apollo-router/src/configuration/migrations/0031-reuse-query-fragments.yaml diff --git a/.changesets/breaking_drop_reuse_fragment.md b/.changesets/breaking_drop_reuse_fragment.md new file mode 100644 index 0000000000..643f061b64 --- /dev/null +++ b/.changesets/breaking_drop_reuse_fragment.md @@ -0,0 +1,7 @@ +### Drop experimental reuse fragment query optimization option ([PR #6353](https://github.com/apollographql/router/pull/6353)) + +Drop support for the experimental reuse fragment query optimization. This implementation was not only very slow but also very buggy due to its complexity. + +Auto generation of fragments is a much simpler (and faster) algorithm that in most cases produces better results. Fragment auto generation is the default optimization since v1.58 release. + +By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6353 diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index 28bb5f7921..cfef296154 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -23,9 +23,6 @@ struct QueryPlannerArgs { /// Enable @defer support. #[arg(long, default_value_t = false)] enable_defer: bool, - /// Reuse fragments to compress subgraph queries. - #[arg(long, default_value_t = false)] - reuse_fragments: bool, /// Generate fragments to compress subgraph queries. #[arg(long, default_value_t = false)] generate_fragments: bool, @@ -109,8 +106,6 @@ enum Command { impl QueryPlannerArgs { fn apply(&self, config: &mut QueryPlannerConfig) { config.incremental_delivery.enable_defer = self.enable_defer; - // --generate-fragments trumps --reuse-fragments - config.reuse_query_fragments = self.reuse_fragments && !self.generate_fragments; config.generate_query_fragments = self.generate_fragments; config.subgraph_graphql_validation = self.subgraph_validation.unwrap_or(true); if let Some(max_evaluated_plans) = self.max_evaluated_plans { diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index f93f805661..9454842f1d 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -921,17 +921,6 @@ impl FragmentSpreadSelection { }) } - pub(crate) fn from_fragment( - fragment: &Node, - directives: &executable::DirectiveList, - ) -> Self { - let spread = FragmentSpread::from_fragment(fragment, directives); - Self { - spread, - selection_set: fragment.selection_set.clone(), - } - } - /// Creates a fragment spread selection (in an optimized operation). /// - `named_fragments`: Named fragment definitions that are rebased for the element's schema. pub(crate) fn new( @@ -2171,15 +2160,6 @@ impl SelectionSet { }) } - /// In a normalized selection set containing only fields and inline fragments, - /// iterate over all the fields that may be selected. - /// - /// # Preconditions - /// The selection set must not contain named fragment spreads. - pub(crate) fn field_selections(&self) -> FieldSelectionsIter<'_> { - FieldSelectionsIter::new(self.selections.values()) - } - /// # Preconditions /// The selection set must not contain named fragment spreads. fn fields_in_set(&self) -> Vec { @@ -2320,36 +2300,6 @@ impl<'a> IntoIterator for &'a SelectionSet { } } -pub(crate) struct FieldSelectionsIter<'sel> { - stack: Vec>, -} - -impl<'sel> FieldSelectionsIter<'sel> { - fn new(iter: selection_map::Values<'sel>) -> Self { - Self { stack: vec![iter] } - } -} - -impl<'sel> Iterator for FieldSelectionsIter<'sel> { - type Item = &'sel Arc; - - fn next(&mut self) -> Option { - match self.stack.last_mut()?.next() { - None if self.stack.len() == 1 => None, - None => { - self.stack.pop(); - self.next() - } - Some(Selection::Field(field)) => Some(field), - Some(Selection::InlineFragment(frag)) => { - self.stack.push(frag.selection_set.selections.values()); - self.next() - } - Some(Selection::FragmentSpread(_frag)) => unreachable!(), - } - } -} - #[derive(Clone, Debug)] pub(crate) struct SelectionSetAtPath { path: Vec, @@ -2625,16 +2575,6 @@ impl Field { pub(crate) fn parent_type_position(&self) -> CompositeTypeDefinitionPosition { self.field_position.parent() } - - pub(crate) fn types_can_be_merged(&self, other: &Self) -> Result { - let self_definition = self.field_position.get(self.schema().schema())?; - let other_definition = other.field_position.get(self.schema().schema())?; - types_can_be_merged( - &self_definition.ty, - &other_definition.ty, - self.schema().schema(), - ) - } } impl InlineFragmentSelection { @@ -2732,23 +2672,6 @@ impl InlineFragmentSelection { )) } - /// Construct a new InlineFragmentSelection out of a selection set. - /// - The new type condition will be the same as the selection set's type. - pub(crate) fn from_selection_set( - parent_type_position: CompositeTypeDefinitionPosition, - selection_set: SelectionSet, - directives: DirectiveList, - ) -> Self { - let inline_fragment_data = InlineFragment { - schema: selection_set.schema.clone(), - parent_type_position, - type_condition_position: selection_set.type_position.clone().into(), - directives, - selection_id: SelectionId::new(), - }; - InlineFragmentSelection::new(inline_fragment_data, selection_set) - } - pub(crate) fn casted_type(&self) -> &CompositeTypeDefinitionPosition { self.inline_fragment .type_condition_position @@ -2811,31 +2734,10 @@ impl NamedFragments { NamedFragments::initialize_in_dependency_order(fragments, schema) } - pub(crate) fn is_empty(&self) -> bool { - self.fragments.len() == 0 - } - - pub(crate) fn len(&self) -> usize { - self.fragments.len() - } - pub(crate) fn iter(&self) -> impl Iterator> { self.fragments.values() } - pub(crate) fn iter_rev(&self) -> impl Iterator> { - self.fragments.values().rev() - } - - pub(crate) fn iter_mut(&mut self) -> indexmap::map::IterMut<'_, Name, Node> { - Arc::make_mut(&mut self.fragments).iter_mut() - } - - // Calls `retain` on the underlying `IndexMap`. - pub(crate) fn retain(&mut self, mut predicate: impl FnMut(&Name, &Node) -> bool) { - Arc::make_mut(&mut self.fragments).retain(|name, fragment| predicate(name, fragment)); - } - fn insert(&mut self, fragment: Fragment) { Arc::make_mut(&mut self.fragments).insert(fragment.name.clone(), Node::new(fragment)); } @@ -2929,32 +2831,6 @@ impl NamedFragments { } }) } - - /// When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong - /// to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or - /// want to consider to ignore the fragment for that subgraph, and that is when: - /// 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway. - /// 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using - /// the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the - /// fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's - /// enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely - /// to be common is when the subset ends up being just `__typename`: this would basically mean the fragment - /// don't really apply to the subgraph, and that this will ensure this is the case. - pub(crate) fn is_selection_set_worth_using(selection_set: &SelectionSet) -> bool { - if selection_set.selections.len() == 0 { - return false; - } - if selection_set.selections.len() == 1 { - // true if NOT field selection OR non-leaf field - return if let Some(Selection::Field(field_selection)) = selection_set.selections.first() - { - field_selection.selection_set.is_some() - } else { - true - }; - } - true - } } // @defer handling: removing and normalization @@ -3393,49 +3269,6 @@ impl Operation { } } -// Collect fragment usages from operation types. - -impl Selection { - fn collect_used_fragment_names(&self, aggregator: &mut IndexMap) { - match self { - Selection::Field(field_selection) => { - if let Some(s) = &field_selection.selection_set { - s.collect_used_fragment_names(aggregator) - } - } - Selection::InlineFragment(inline) => { - inline.selection_set.collect_used_fragment_names(aggregator); - } - Selection::FragmentSpread(fragment) => { - let current_count = aggregator - .entry(fragment.spread.fragment_name.clone()) - .or_default(); - *current_count += 1; - } - } - } -} - -impl SelectionSet { - pub(crate) fn collect_used_fragment_names(&self, aggregator: &mut IndexMap) { - for s in self.selections.values() { - s.collect_used_fragment_names(aggregator); - } - } - - pub(crate) fn used_fragments(&self) -> IndexMap { - let mut usages = IndexMap::default(); - self.collect_used_fragment_names(&mut usages); - usages - } -} - -impl Fragment { - pub(crate) fn collect_used_fragment_names(&self, aggregator: &mut IndexMap) { - self.selection_set.collect_used_fragment_names(aggregator) - } -} - // Collect used variables from operation types. pub(crate) struct VariableCollector<'s> { @@ -3533,16 +3366,6 @@ impl<'s> VariableCollector<'s> { } } -impl Fragment { - /// Returns the variable names that are used by this fragment. - pub(crate) fn used_variables(&self) -> IndexSet<&'_ Name> { - let mut collector = VariableCollector::new(); - collector.visit_directive_list(&self.directives); - collector.visit_selection_set(&self.selection_set); - collector.into_inner() - } -} - impl SelectionSet { /// Returns the variable names that are used by this selection set, including through fragment /// spreads. @@ -3905,7 +3728,9 @@ pub(crate) fn normalize_operation( variables: Arc::new(operation.variables.clone()), directives: operation.directives.clone().into(), selection_set: normalized_selection_set, - named_fragments, + // fragments were already expanded into selection sets + // new ones will be generated when optimizing the final subgraph fetch operations + named_fragments: Default::default(), }; Ok(normalized_operation) } diff --git a/apollo-federation/src/operation/optimize.rs b/apollo-federation/src/operation/optimize.rs index c7e54b23f7..7bdd0842a2 100644 --- a/apollo-federation/src/operation/optimize.rs +++ b/apollo-federation/src/operation/optimize.rs @@ -38,17 +38,10 @@ use std::sync::Arc; use apollo_compiler::collections::IndexMap; -use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; -use apollo_compiler::executable::VariableDefinition; use apollo_compiler::Name; use apollo_compiler::Node; -use super::Containment; -use super::ContainmentOptions; -use super::DirectiveList; -use super::Field; -use super::FieldSelection; use super::Fragment; use super::FragmentSpreadSelection; use super::HasSelectionKey; @@ -62,152 +55,6 @@ use super::SelectionSet; use crate::error::FederationError; use crate::operation::FragmentSpread; use crate::operation::SelectionValue; -use crate::schema::position::CompositeTypeDefinitionPosition; - -#[derive(Debug)] -struct ReuseContext<'a> { - fragments: &'a NamedFragments, - operation_variables: Option>, -} - -impl<'a> ReuseContext<'a> { - fn for_fragments(fragments: &'a NamedFragments) -> Self { - Self { - fragments, - operation_variables: None, - } - } - - // Taking two separate parameters so the caller can still mutate the operation's selection set. - fn for_operation( - fragments: &'a NamedFragments, - operation_variables: &'a [Node], - ) -> Self { - Self { - fragments, - operation_variables: Some(operation_variables.iter().map(|var| &var.name).collect()), - } - } -} - -//============================================================================= -// Add __typename field for abstract types in named fragment definitions - -impl NamedFragments { - // - Expands all nested fragments - // - Applies the provided `mapper` to each selection set of the expanded fragments. - // - Finally, re-fragments the nested fragments. - // - `mapper` must return a fragment-spread-free selection set. - fn map_to_expanded_selection_sets( - &self, - mut mapper: impl FnMut(&SelectionSet) -> Result, - ) -> Result { - let mut result = NamedFragments::default(); - // Note: `self.fragments` has insertion order topologically sorted. - for fragment in self.fragments.values() { - let expanded_selection_set = fragment - .selection_set - .expand_all_fragments()? - .flatten_unnecessary_fragments( - &fragment.type_condition_position, - &Default::default(), - &fragment.schema, - )?; - let mut mapped_selection_set = mapper(&expanded_selection_set)?; - // `mapped_selection_set` must be fragment-spread-free. - mapped_selection_set.reuse_fragments(&ReuseContext::for_fragments(&result))?; - let updated = Fragment { - selection_set: mapped_selection_set, - schema: fragment.schema.clone(), - name: fragment.name.clone(), - type_condition_position: fragment.type_condition_position.clone(), - directives: fragment.directives.clone(), - }; - result.insert(updated); - } - Ok(result) - } - - pub(crate) fn add_typename_field_for_abstract_types_in_named_fragments( - &self, - ) -> Result { - // This method is a bit tricky due to potentially nested fragments. More precisely, suppose that - // we have: - // fragment MyFragment on T { - // a { - // b { - // ...InnerB - // } - // } - // } - // - // fragment InnerB on B { - // __typename - // x - // y - // } - // then if we were to "naively" add `__typename`, the first fragment would end up being: - // fragment MyFragment on T { - // a { - // __typename - // b { - // __typename - // ...InnerX - // } - // } - // } - // but that's not ideal because the inner-most `__typename` is already within `InnerX`. And that - // gets in the way to re-adding fragments (the `SelectionSet::reuse_fragments` method) because if we start - // with: - // { - // a { - // __typename - // b { - // __typename - // x - // y - // } - // } - // } - // and add `InnerB` first, we get: - // { - // a { - // __typename - // b { - // ...InnerB - // } - // } - // } - // and it becomes tricky to recognize the "updated-with-typename" version of `MyFragment` now (we "seem" - // to miss a `__typename`). - // - // Anyway, to avoid this issue, what we do is that for every fragment, we: - // 1. expand any nested fragments in its selection. - // 2. add `__typename` where we should in that expanded selection. - // 3. re-optimize all fragments (using the "updated-with-typename" versions). - // which is what `mapToExpandedSelectionSets` gives us. - - if self.is_empty() { - // PORT_NOTE: This was an assertion failure in JS version. But, it's actually ok to - // return unchanged if empty. - return Ok(self.clone()); - } - let updated = self.map_to_expanded_selection_sets(|ss| { - // Note: Since `ss` won't have any fragment spreads, `add_typename_field_for_abstract_types`'s return - // value won't have any fragment spreads. - ss.add_typename_field_for_abstract_types(/*parent_type_if_abstract*/ None) - })?; - // PORT_NOTE: The JS version asserts if `updated` is empty or not. But, we really want to - // check the `updated` has the same set of fragments. To avoid performance hit, only the - // size is checked here. - if updated.len() != self.len() { - return Err(FederationError::internal( - "Unexpected change in the number of fragments", - )); - } - Ok(updated) - } -} //============================================================================= // Selection/SelectionSet intersection/minus operations @@ -234,26 +81,6 @@ impl Selection { } Ok(None) } - - /// Computes the set-intersection of self and other - /// - If there are respective sub-selections, then we compute their intersections and add them - /// (if not empty). - /// - Otherwise, the intersection is same as `self`. - fn intersection(&self, other: &Selection) -> Result, FederationError> { - if let (Some(self_sub_selection), Some(other_sub_selection)) = - (self.selection_set(), other.selection_set()) - { - let common = self_sub_selection.intersection(other_sub_selection)?; - if common.is_empty() { - return Ok(None); - } else { - return self - .with_updated_selections(self_sub_selection.type_position.clone(), common) - .map(Some); - } - } - Ok(Some(self.clone())) - } } impl SelectionSet { @@ -279,1549 +106,279 @@ impl SelectionSet { iter, )) } - - /// Computes the set-intersection of self and other - fn intersection(&self, other: &SelectionSet) -> Result { - if self.is_empty() { - return Ok(self.clone()); - } - if other.is_empty() { - return Ok(other.clone()); - } - - let iter = self - .selections - .values() - .map(|v| { - if let Some(other_v) = other.selections.get(v.key()) { - v.intersection(other_v) - } else { - Ok(None) - } - }) - .collect::, _>>()? // early break in case of Err - .into_iter() - .flatten(); - Ok(SelectionSet::from_raw_selections( - self.schema.clone(), - self.type_position.clone(), - iter, - )) - } } //============================================================================= -// Collect applicable fragments at given type. - -impl Fragment { - /// Whether this fragment may apply _directly_ at the provided type, meaning that the fragment - /// sub-selection (_without_ the fragment condition, hence the "directly") can be normalized at - /// `ty` without overly "widening" the runtime types. - /// - /// * `ty` - the type at which we're looking at applying the fragment - // - // The runtime types of the fragment condition must be at least as general as those of the - // provided `ty`. Otherwise, putting it at `ty` without its condition would "generalize" - // more than the fragment meant to (and so we'd "widen" the runtime types more than what the - // query meant to. - fn can_apply_directly_at_type( - &self, - ty: &CompositeTypeDefinitionPosition, - ) -> Result { - // Short-circuit #1: the same type => trivially true. - if self.type_condition_position == *ty { - return Ok(true); - } - - // Short-circuit #2: The type condition is not an abstract type (too restrictive). - // - It will never cover all of the runtime types of `ty` unless it's the same type, which is - // already checked by short-circuit #1. - if !self.type_condition_position.is_abstract_type() { - return Ok(false); - } - - // Short-circuit #3: The type condition is not an object (due to short-circuit #2) nor a - // union type, but the `ty` may be too general. - // - In other words, the type condition must be an interface but `ty` is a (different) - // interface or a union. - // PORT_NOTE: In JS, this check was later on the return statement (negated). But, this - // should be checked before `possible_runtime_types` check, since this is - // cheaper to execute. - // PORT_NOTE: This condition may be too restrictive (potentially a bug leading to - // suboptimal compression). If ty is a union whose members all implements the - // type condition (interface). Then, this function should've returned true. - // Thus, `!ty.is_union_type()` might be needed. - if !self.type_condition_position.is_union_type() && !ty.is_object_type() { - return Ok(false); - } +// Matching fragments with selection set (`try_optimize_with_fragments`) - // Check if the type condition is a superset of the provided type. - // - The fragment condition must be at least as general as the provided type. - let condition_types = self - .schema - .possible_runtime_types(self.type_condition_position.clone())?; - let ty_types = self.schema.possible_runtime_types(ty.clone())?; - Ok(condition_types.is_superset(&ty_types)) - } +/// The return type for `SelectionSet::try_optimize_with_fragments`. +#[derive(derive_more::From)] +enum SelectionSetOrFragment { + SelectionSet(SelectionSet), + Fragment(Node), } -impl NamedFragments { - /// Returns fragments that can be applied directly at the given type. - fn get_all_may_apply_directly_at_type<'a>( - &'a self, - ty: &'a CompositeTypeDefinitionPosition, - ) -> impl Iterator, FederationError>> + 'a { - self.iter().filter_map(|fragment| { - fragment - .can_apply_directly_at_type(ty) - .map(|can_apply| can_apply.then_some(fragment)) - .transpose() - }) +// Note: `retain_fragments` methods may return a selection or a selection set. +impl From for SelectionMapperReturn { + fn from(value: SelectionOrSet) -> Self { + match value { + SelectionOrSet::Selection(selection) => selection.into(), + SelectionOrSet::SelectionSet(selections) => { + // The items in a selection set needs to be cloned here, since it's sub-selections + // are contained in an `Arc`. + Vec::from_iter(selections.selections.values().cloned()).into() + } + } } } //============================================================================= -// Field validation +// `reuse_fragments` methods (putting everything together) -// PORT_NOTE: Not having a validator and having a FieldsConflictValidator with empty -// `by_response_name` map has no difference in behavior. So, we could drop the `Option` from -// `Option`. However, `None` validator makes it clearer that validation is -// unnecessary. -struct FieldsConflictValidator { - by_response_name: IndexMap>>>, +/// Return type for `InlineFragmentSelection::reuse_fragments`. +#[derive(derive_more::From)] +enum FragmentSelection { + // Note: Enum variants are named to match those of `Selection`. + InlineFragment(InlineFragmentSelection), + FragmentSpread(FragmentSpreadSelection), } -impl FieldsConflictValidator { - /// Build a field merging validator for a selection set. - /// - /// # Preconditions - /// The selection set must not contain named fragment spreads. - fn from_selection_set(selection_set: &SelectionSet) -> Self { - Self::for_level(&[selection_set]) - } - - fn for_level<'a>(level: &[&'a SelectionSet]) -> Self { - // Group `level`'s fields by the response-name/field - let mut at_level: IndexMap>> = - IndexMap::default(); - for selection_set in level { - for field_selection in selection_set.field_selections() { - let response_name = field_selection.field.response_name(); - let at_response_name = at_level.entry(response_name.clone()).or_default(); - let entry = at_response_name - .entry(field_selection.field.clone()) - .or_default(); - if let Some(ref field_selection_set) = field_selection.selection_set { - entry.push(field_selection_set); - } - } - } - - // Collect validators per response-name/field - let mut by_response_name = IndexMap::default(); - for (response_name, fields) in at_level { - let mut at_response_name: IndexMap>> = - IndexMap::default(); - for (field, selection_sets) in fields { - if selection_sets.is_empty() { - at_response_name.insert(field, None); - } else { - let validator = Arc::new(Self::for_level(&selection_sets)); - at_response_name.insert(field, Some(validator)); - } - } - by_response_name.insert(response_name, at_response_name); - } - Self { by_response_name } - } - - fn for_field<'v>(&'v self, field: &Field) -> impl Iterator> + 'v { - self.by_response_name - .get(field.response_name()) - .into_iter() - .flat_map(|by_response_name| by_response_name.values()) - .flatten() - .cloned() - } - - fn has_same_response_shape( - &self, - other: &FieldsConflictValidator, - ) -> Result { - for (response_name, self_fields) in self.by_response_name.iter() { - let Some(other_fields) = other.by_response_name.get(response_name) else { - continue; - }; - - for (self_field, self_validator) in self_fields { - for (other_field, other_validator) in other_fields { - if !self_field.types_can_be_merged(other_field)? { - return Ok(false); - } - - if let Some(self_validator) = self_validator { - if let Some(other_validator) = other_validator { - if !self_validator.has_same_response_shape(other_validator)? { - return Ok(false); - } - } - } - } - } - } - Ok(true) - } - - fn do_merge_with(&self, other: &FieldsConflictValidator) -> Result { - for (response_name, self_fields) in self.by_response_name.iter() { - let Some(other_fields) = other.by_response_name.get(response_name) else { - continue; - }; - - // We're basically checking - // [FieldsInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()), but - // from 2 set of fields (`self_fields` and `other_fields`) of the same response that we - // know individually merge already. - for (self_field, self_validator) in self_fields { - for (other_field, other_validator) in other_fields { - if !self_field.types_can_be_merged(other_field)? { - return Ok(false); - } - - let p1 = self_field.parent_type_position(); - let p2 = other_field.parent_type_position(); - if p1 == p2 || !p1.is_object_type() || !p2.is_object_type() { - // Additional checks of `FieldsInSetCanMerge` when same parent type or one - // isn't object - if self_field.name() != other_field.name() - || self_field.arguments != other_field.arguments - { - return Ok(false); - } - if let (Some(self_validator), Some(other_validator)) = - (self_validator, other_validator) - { - if !self_validator.do_merge_with(other_validator)? { - return Ok(false); - } - } - } else { - // Otherwise, the sub-selection must pass - // [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()). - if let (Some(self_validator), Some(other_validator)) = - (self_validator, other_validator) - { - if !self_validator.has_same_response_shape(other_validator)? { - return Ok(false); - } - } - } - } - } +impl From for Selection { + fn from(value: FragmentSelection) -> Self { + match value { + FragmentSelection::InlineFragment(inline_fragment) => inline_fragment.into(), + FragmentSelection::FragmentSpread(fragment_spread) => fragment_spread.into(), } - Ok(true) - } - - fn do_merge_with_all<'a>( - &self, - mut iter: impl Iterator, - ) -> Result { - iter.try_fold(true, |acc, v| Ok(acc && v.do_merge_with(self)?)) } } -struct FieldsConflictMultiBranchValidator { - validators: Vec>, - used_spread_trimmed_part_at_level: Vec>, -} - -impl FieldsConflictMultiBranchValidator { - fn new(validators: Vec>) -> Self { - Self { - validators, - used_spread_trimmed_part_at_level: Vec::new(), - } - } - - fn from_initial_validator(validator: FieldsConflictValidator) -> Self { - Self { - validators: vec![Arc::new(validator)], - used_spread_trimmed_part_at_level: Vec::new(), - } - } - - fn for_field(&self, field: &Field) -> Self { - let for_all_branches = self.validators.iter().flat_map(|v| v.for_field(field)); - Self::new(for_all_branches.collect()) - } - - // When this method is used in the context of `try_optimize_with_fragments`, we know that the - // fragment, restricted to the current parent type, matches a subset of the sub-selection. - // However, there is still one case we we cannot use it that we need to check, and this is if - // using the fragment would create a field "conflict" (in the sense of the graphQL spec - // [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus - // create an invalid selection. To be clear, `at_type.selections` cannot create a conflict, - // since it is a subset of the target selection set and it is valid by itself. *But* there may - // be some part of the fragment that is not `at_type.selections` due to being "dead branches" - // for type `parent_type`. And while those branches _are_ "dead" as far as execution goes, the - // `FieldsInSetCanMerge` validation does not take this into account (it's 1st step says - // "including visiting fragments and inline fragments" but has no logic regarding ignoring any - // fragment that may not apply due to the intersection of runtime types between multiple - // fragment being empty). - fn check_can_reuse_fragment_and_track_it( - &mut self, - fragment_restriction: &FragmentRestrictionAtType, - ) -> Result { - // No validator means that everything in the fragment selection was part of the selection - // we're optimizing away (by using the fragment), and we know the original selection was - // ok, so nothing to check. - let Some(validator) = &fragment_restriction.validator else { - return Ok(true); // Nothing to check; Trivially ok. - }; - - if !validator.do_merge_with_all(self.validators.iter().map(Arc::as_ref))? { - return Ok(false); - } - - // We need to make sure the trimmed parts of `fragment` merges with the rest of the - // selection, but also that it merge with any of the trimmed parts of any fragment we have - // added already. - // Note: this last condition means that if 2 fragment conflict on their "trimmed" parts, - // then the choice of which is used can be based on the fragment ordering and selection - // order, which may not be optimal. This feels niche enough that we keep it simple for now, - // but we can revisit this decision if we run into real cases that justify it (but making - // it optimal would be a involved in general, as in theory you could have complex - // dependencies of fragments that conflict, even cycles, and you need to take the size of - // fragments into account to know what's best; and even then, this could even depend on - // overall usage, as it can be better to reuse a fragment that is used in other places, - // than to use one for which it's the only usage. Adding to all that the fact that conflict - // can happen in sibling branches). - if !validator.do_merge_with_all( - self.used_spread_trimmed_part_at_level - .iter() - .map(Arc::as_ref), - )? { - return Ok(false); - } - - // We're good, but track the fragment. - self.used_spread_trimmed_part_at_level - .push(validator.clone()); - Ok(true) +impl Operation { + /// Optimize the parsed size of the operation by generating fragments based on the selections + /// in the operation. + pub(crate) fn generate_fragments(&mut self) -> Result<(), FederationError> { + // Currently, this method simply pulls out every inline fragment into a named fragment. If + // multiple inline fragments are the same, they use the same named fragment. + // + // This method can generate named fragments that are only used once. It's not ideal, but it + // also doesn't seem that bad. Avoiding this is possible but more work, and keeping this + // as simple as possible is a big benefit for now. + // + // When we have more advanced correctness testing, we can add more features to fragment + // generation, like factoring out partial repeated slices of selection sets or only + // introducing named fragments for patterns that occur more than once. + let mut generator = FragmentGenerator::default(); + generator.visit_selection_set(&mut self.selection_set)?; + self.named_fragments = generator.into_inner(); + Ok(()) } } -//============================================================================= -// Matching fragments with selection set (`try_optimize_with_fragments`) - -/// Return type for `expanded_selection_set_at_type` method. -struct FragmentRestrictionAtType { - /// Selections that are expanded from a given fragment at a given type and then normalized. - /// - This represents the part of given type's sub-selections that are covered by the fragment. - selections: SelectionSet, - - /// A runtime validator to check the fragment selections against other fields. - /// - `None` means that there is nothing to check. - /// - See `check_can_reuse_fragment_and_track_it` for more details. - validator: Option>, +#[derive(Debug, Default)] +struct FragmentGenerator { + fragments: NamedFragments, + // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! + names: IndexMap<(String, usize), usize>, } -#[derive(Default)] -struct FragmentRestrictionAtTypeCache { - map: IndexMap<(Name, CompositeTypeDefinitionPosition), Arc>, -} +impl FragmentGenerator { + // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! + // In the future, we will just use `.next_name()`. + fn generate_name(&mut self, frag: &InlineFragmentSelection) -> Name { + use std::fmt::Write as _; -impl FragmentRestrictionAtTypeCache { - fn expanded_selection_set_at_type( - &mut self, - fragment: &Fragment, - ty: &CompositeTypeDefinitionPosition, - ) -> Result, FederationError> { - // I would like to avoid the Arc here, it seems unnecessary, but with `.entry()` - // the lifetime does not really want to work out. - // (&'cache mut self) -> Result<&'cache FragmentRestrictionAtType> - match self.map.entry((fragment.name.clone(), ty.clone())) { - indexmap::map::Entry::Occupied(entry) => Ok(Arc::clone(entry.get())), - indexmap::map::Entry::Vacant(entry) => Ok(Arc::clone( - entry.insert(Arc::new(fragment.expanded_selection_set_at_type(ty)?)), - )), - } - } -} + let type_condition = frag + .inline_fragment + .type_condition_position + .as_ref() + .map_or_else( + || "undefined".to_string(), + |condition| condition.to_string(), + ); + let selections = frag.selection_set.selections.len(); + let mut name = format!("_generated_on{type_condition}{selections}"); -impl FragmentRestrictionAtType { - fn new(selections: SelectionSet, validator: Option) -> Self { - Self { - selections, - validator: validator.map(Arc::new), - } + let key = (type_condition, selections); + let index = self + .names + .entry(key) + .and_modify(|index| *index += 1) + .or_default(); + _ = write!(&mut name, "_{index}"); + + Name::new_unchecked(&name) } - // It's possible that while the fragment technically applies at `parent_type`, it's "rebasing" on - // `parent_type` is empty, or contains only `__typename`. For instance, suppose we have - // a union `U = A | B | C`, and then a fragment: - // ```graphql - // fragment F on U { - // ... on A { - // x - // } - // ... on B { - // y - // } - // } - // ``` - // It is then possible to apply `F` when the parent type is `C`, but this ends up selecting - // nothing at all. - // - // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we - // skip it that case. This is essentially an optimization. - fn is_useless(&self) -> bool { - let mut iter = self.selections.iter(); + /// Is a selection set worth using for a newly generated named fragment? + fn is_worth_using(selection_set: &SelectionSet) -> bool { + let mut iter = selection_set.iter(); let Some(first) = iter.next() else { + // An empty selection is not worth using (and invalid!) + return false; + }; + let Selection::Field(field) = first else { return true; }; - iter.next().is_none() && first.is_typename_field() - } -} - -impl Fragment { - /// Computes the expanded selection set of this fragment along with its validator to check - /// against other fragments applied under the same selection set. - fn expanded_selection_set_at_type( - &self, - ty: &CompositeTypeDefinitionPosition, - ) -> Result { - let expanded_selection_set = self.selection_set.expand_all_fragments()?; - let selection_set = expanded_selection_set.flatten_unnecessary_fragments( - ty, - /*named_fragments*/ &Default::default(), - &self.schema, - )?; - - if !self.type_condition_position.is_object_type() { - // When the type condition of the fragment is not an object type, the - // `FieldsInSetCanMerge` rule is more restrictive and any fields can create conflicts. - // Thus, we have to use the full validator in this case. (see - // https://github.com/graphql/graphql-spec/issues/1085 for details.) - return Ok(FragmentRestrictionAtType::new( - selection_set.clone(), - Some(FieldsConflictValidator::from_selection_set( - &expanded_selection_set, - )), - )); - } - - // Use a smaller validator for efficiency. - // Note that `trimmed` is the difference of 2 selections that may not have been normalized - // on the same parent type, so in practice, it is possible that `trimmed` contains some of - // the selections that `selectionSet` contains, but that they have been simplified in - // `selectionSet` in such a way that the `minus` call does not see it. However, it is not - // trivial to deal with this, and it is fine given that we use trimmed to create the - // validator because we know the non-trimmed parts cannot create field conflict issues so - // we're trying to build a smaller validator, but it's ok if trimmed is not as small as it - // theoretically can be. - let trimmed = expanded_selection_set.minus(&selection_set)?; - let validator = - (!trimmed.is_empty()).then(|| FieldsConflictValidator::from_selection_set(&trimmed)); - Ok(FragmentRestrictionAtType::new( - selection_set.clone(), - validator, - )) - } - - /// Checks whether `self` fragment includes the other fragment (`other_fragment_name`). - // - // Note that this is slightly different from `self` "using" `other_fragment` in that this - // essentially checks if the full selection set of `other_fragment` is contained by `self`, so - // this only look at "top-level" usages. - // - // Note that this is guaranteed to return `false` if passed self's name. - // Note: This is a heuristic looking for the other named fragment used directly in the - // selection set. It may not return `true` even though the other fragment's selections - // are actually covered by self's selection set. - // PORT_NOTE: The JS version memoizes the result of this function. But, the current Rust port - // does not. - fn includes(&self, other_fragment_name: &Name) -> bool { - if self.name == *other_fragment_name { - return false; - } - - self.selection_set.selections.values().any(|selection| { - matches!( - selection, - Selection::FragmentSpread(fragment) if fragment.spread.fragment_name == *other_fragment_name - ) - }) + // If there's more than one selection, or one selection with a subselection, + // it's probably worth using + iter.next().is_some() || field.selection_set.is_some() } -} - -enum FullMatchingFragmentCondition<'a> { - ForFieldSelection, - ForInlineFragmentSelection { - // the type condition and directives on an inline fragment selection. - type_condition_position: &'a CompositeTypeDefinitionPosition, - directives: &'a DirectiveList, - }, -} -impl<'a> FullMatchingFragmentCondition<'a> { - /// Determines whether the given fragment is allowed to match the whole selection set by itself - /// (without another selection set wrapping it). - fn check(&self, fragment: &Node) -> bool { - match self { - // We can never apply a fragments that has directives on it at the field level. - Self::ForFieldSelection => fragment.directives.is_empty(), + /// Modify the selection set so that eligible inline fragments are moved to named fragment spreads. + fn visit_selection_set( + &mut self, + selection_set: &mut SelectionSet, + ) -> Result<(), FederationError> { + let mut new_selection_set = SelectionSet::empty( + selection_set.schema.clone(), + selection_set.type_position.clone(), + ); - // To be able to use a matching inline fragment, it needs to have either no directives, - // or if it has some, then: - // 1. All it's directives should also be on the current element. - // 2. The type condition of this element should be the fragment's condition. because - // If those 2 conditions are true, we can replace the whole current inline fragment - // with the match spread and directives will still match. - Self::ForInlineFragmentSelection { - type_condition_position, - directives, - } => { - if fragment.directives.is_empty() { - return true; + for selection in Arc::make_mut(&mut selection_set.selections).values_mut() { + match selection { + SelectionValue::Field(mut field) => { + if let Some(selection_set) = field.get_selection_set_mut() { + self.visit_selection_set(selection_set)?; + } + new_selection_set + .add_local_selection(&Selection::Field(Arc::clone(field.get())))?; + } + SelectionValue::FragmentSpread(frag) => { + new_selection_set + .add_local_selection(&Selection::FragmentSpread(Arc::clone(frag.get())))?; } + SelectionValue::InlineFragment(frag) + if !Self::is_worth_using(&frag.get().selection_set) => + { + new_selection_set + .add_local_selection(&Selection::InlineFragment(Arc::clone(frag.get())))?; + } + SelectionValue::InlineFragment(mut candidate) => { + self.visit_selection_set(candidate.get_selection_set_mut())?; - // PORT_NOTE: The JS version handles `@defer` directive differently. However, Rust - // version can't have `@defer` at this point (see comments on `enum SelectionKey` - // definition) - fragment.type_condition_position == **type_condition_position - && fragment - .directives - .iter() - .all(|d1| directives.iter().any(|d2| d1 == d2)) - } - } - } -} - -/// The return type for `SelectionSet::try_optimize_with_fragments`. -#[derive(derive_more::From)] -enum SelectionSetOrFragment { - SelectionSet(SelectionSet), - Fragment(Node), -} - -impl SelectionSet { - /// Reduce the list of applicable fragments by eliminating fragments that directly include - /// another fragment. - // - // We have found the list of fragments that applies to some subset of sub-selection. In - // general, we want to now produce the selection set with spread for those fragments plus - // any selection that is not covered by any of the fragments. For instance, suppose that - // `subselection` is `{ a b c d e }` and we have found that `fragment F1 on X { a b c }` - // and `fragment F2 on X { c d }` applies, then we will generate `{ ...F1 ...F2 e }`. - // - // In that example, `c` is covered by both fragments. And this is fine in this example as - // it is worth using both fragments in general. A special case of this however is if a - // fragment is entirely included into another. That is, consider that we now have `fragment - // F1 on X { a ...F2 }` and `fragment F2 on X { b c }`. In that case, the code above would - // still match both `F1 and `F2`, but as `F1` includes `F2` already, we really want to only - // use `F1`. So in practice, we filter away any fragment spread that is known to be - // included in another one that applies. - // - // TODO: note that the logic used for this is theoretically a bit sub-optimal. That is, we - // only check if one of the fragment happens to directly include a spread for another - // fragment at top-level as in the example above. We do this because it is cheap to check - // and is likely the most common case of this kind of inclusion. But in theory, we would - // have `fragment F1 on X { a b c }` and `fragment F2 on X { b c }`, in which case `F2` is - // still included in `F1`, but we'd have to work harder to figure this out and it's unclear - // it's a good tradeoff. And while you could argue that it's on the user to define its - // fragments a bit more optimally, it's actually a tad more complex because we're looking - // at fragments in a particular context/parent type. Consider an interface `I` and: - // ```graphql - // fragment F3 on I { - // ... on X { - // a - // } - // ... on Y { - // b - // c - // } - // } - // - // fragment F4 on I { - // ... on Y { - // c - // } - // ... on Z { - // d - // } - // } - // ``` - // In that case, neither fragment include the other per-se. But what if we have - // sub-selection `{ b c }` but where parent type is `Y`. In that case, both `F3` and `F4` - // applies, and in that particular context, `F3` is fully included in `F4`. Long story - // short, we'll currently return `{ ...F3 ...F4 }` in that case, but it would be - // technically better to return only `F4`. However, this feels niche, and it might be - // costly to verify such inclusions, so not doing it for now. - fn reduce_applicable_fragments( - applicable_fragments: &mut Vec<(Node, Arc)>, - ) { - // Note: It's not possible for two fragments to include each other. So, we don't need to - // worry about inclusion cycles. - let included_fragments: IndexSet = applicable_fragments - .iter() - .filter(|(fragment, _)| { - applicable_fragments - .iter() - .any(|(other_fragment, _)| other_fragment.includes(&fragment.name)) - }) - .map(|(fragment, _)| fragment.name.clone()) - .collect(); - - applicable_fragments.retain(|(fragment, _)| !included_fragments.contains(&fragment.name)); - } - - /// Try to reuse existing fragments to optimize this selection set. - /// Returns either - /// - a new selection set partially optimized by re-using given `fragments`, or - /// - a single fragment that covers the full selection set. - // PORT_NOTE: Moved from `Selection` class in JS code to SelectionSet struct in Rust. - // PORT_NOTE: `parent_type` argument seems always to be the same as `self.type_position`. - // PORT_NOTE: In JS, this was called `tryOptimizeWithFragments`. - fn try_apply_fragments( - &self, - parent_type: &CompositeTypeDefinitionPosition, - context: &ReuseContext<'_>, - validator: &mut FieldsConflictMultiBranchValidator, - fragments_at_type: &mut FragmentRestrictionAtTypeCache, - full_match_condition: FullMatchingFragmentCondition, - ) -> Result { - // We limit to fragments whose selection could be applied "directly" at `parent_type`, - // meaning without taking the fragment condition into account. The idea being that if the - // fragment condition would be needed inside `parent_type`, then that condition will not - // have been "normalized away" and so we want for this very call to be called on the - // fragment whose type _is_ the fragment condition (at which point, this - // `can_apply_directly_at_type` method will apply. Also note that this is because we have - // this restriction that calling `expanded_selection_set_at_type` is ok. - let candidates = context - .fragments - .get_all_may_apply_directly_at_type(parent_type); - - // First, we check which of the candidates do apply inside the selection set, if any. If we - // find a candidate that applies to the whole selection set, then we stop and only return - // that one candidate. Otherwise, we cumulate in `applicable_fragments` the list of fragments - // that applies to a subset. - let mut applicable_fragments = Vec::new(); - for candidate in candidates { - let candidate = candidate?; - let at_type = - fragments_at_type.expanded_selection_set_at_type(candidate, parent_type)?; - if at_type.is_useless() { - continue; - } - - // I don't love this, but fragments may introduce new fields to the operation, including - // fields that use variables that are not declared in the operation. There are two ways - // to work around this: adjusting the fragments so they only list the fields that we - // actually need, or excluding fragments that introduce variable references from reuse. - // The former would be ideal, as we would not execute more fields than required. It's - // also much trickier to do. The latter fixes this particular issue but leaves the - // output in a less than ideal state. - // The consideration here is: `generate_query_fragments` has significant advantages - // over fragment reuse, and so we do not want to invest a lot of time into improving - // fragment reuse. We do the simple, less-than-ideal thing. - if let Some(variable_definitions) = &context.operation_variables { - let fragment_variables = candidate.used_variables(); - if fragment_variables - .difference(variable_definitions) - .next() - .is_some() - { - continue; - } - } - - // As we check inclusion, we ignore the case where the fragment queries __typename - // but the `self` does not. The rational is that querying `__typename` - // unnecessarily is mostly harmless (it always works and it's super cheap) so we - // don't want to not use a fragment just to save querying a `__typename` in a few - // cases. But the underlying context of why this matters is that the query planner - // always requests __typename for abstract type, and will do so in fragments too, - // but we can have a field that _does_ return an abstract type within a fragment, - // but that _does not_ end up returning an abstract type when applied in a "more - // specific" context (think a fragment on an interface I1 where a inside field - // returns another interface I2, but applied in the context of a implementation - // type of I1 where that particular field returns an implementation of I2 rather - // than I2 directly; we would have added __typename to the fragment (because it's - // all interfaces), but the selection itself, which only deals with object type, - // may not have __typename requested; using the fragment might still be a good - // idea, and querying __typename needlessly is a very small price to pay for that). - let res = self.containment( - &at_type.selections, - ContainmentOptions { - ignore_missing_typename: true, - }, - ); - match res { - Containment::Equal if full_match_condition.check(candidate) => { - if !validator.check_can_reuse_fragment_and_track_it(&at_type)? { - // We cannot use it at all, so no point in adding to `applicable_fragments`. + // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! + // JS federation does not consider fragments without a type condition. + if candidate + .get() + .inline_fragment + .type_condition_position + .is_none() + { + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; continue; } - // Special case: Found a fragment that covers the full selection set. - return Ok(candidate.clone().into()); - } - // Note that if a fragment applies to only a subset of the sub-selections, then we - // really only can use it if that fragment is defined _without_ directives. - Containment::Equal | Containment::StrictlyContained - if candidate.directives.is_empty() => - { - applicable_fragments.push((candidate.clone(), at_type)); - } - // Not eligible; Skip it. - _ => (), - } - } - - if applicable_fragments.is_empty() { - return Ok(self.clone().into()); // Not optimizable - } - // Narrow down the list of applicable fragments by removing those that are included in - // another. - Self::reduce_applicable_fragments(&mut applicable_fragments); - - // Build a new optimized selection set. - let mut not_covered_so_far = self.clone(); - let mut optimized = SelectionSet::empty(self.schema.clone(), self.type_position.clone()); - for (fragment, at_type) in applicable_fragments { - if !validator.check_can_reuse_fragment_and_track_it(&at_type)? { - continue; - } - let not_covered = self.minus(&at_type.selections)?; - not_covered_so_far = not_covered_so_far.intersection(¬_covered)?; + let directives = &candidate.get().inline_fragment.directives; + let skip_include = directives + .iter() + .map(|directive| match directive.name.as_str() { + "skip" | "include" => Ok(directive.clone()), + _ => Err(()), + }) + .collect::>(); - // PORT_NOTE: The JS version uses `parent_type` as the "sourceType", which may be - // different from `fragment.type_condition_position`. But, Rust version does - // not have "sourceType" field for `FragmentSpreadSelection`. - let fragment_selection = FragmentSpreadSelection::from_fragment( - &fragment, - /*directives*/ &Default::default(), - ); - optimized.add_local_selection(&fragment_selection.into())?; - } + // If there are any directives *other* than @skip and @include, + // we can't just transfer them to the generated fragment spread, + // so we have to keep this inline fragment. + let Ok(skip_include) = skip_include else { + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; + continue; + }; - optimized.add_local_selection_set(¬_covered_so_far)?; - Ok(optimized.into()) - } -} + // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! + // JS does not special-case @skip and @include. It never extracts a fragment if + // there's any directives on it. This code duplicates the body from the + // previous condition so it's very easy to remove when we're ready :) + if !skip_include.is_empty() { + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; + continue; + } -//============================================================================= -// Retain fragments in selection sets while expanding the rest + let existing = self.fragments.iter().find(|existing| { + existing.type_condition_position + == candidate.get().inline_fragment.casted_type() + && existing.selection_set == candidate.get().selection_set + }); -impl Selection { - /// Expand fragments that are not in the `fragments_to_keep`. - // PORT_NOTE: The JS version's name was `expandFragments`, which was confusing with - // `expand_all_fragments`. So, it was renamed to `retain_fragments`. - fn retain_fragments( - &self, - parent_type: &CompositeTypeDefinitionPosition, - fragments_to_keep: &NamedFragments, - ) -> Result { - match self { - Selection::FragmentSpread(fragment) => { - if fragments_to_keep.contains(&fragment.spread.fragment_name) { - // Keep this spread - Ok(self.clone().into()) - } else { - // Expand the fragment - let expanded_sub_selections = - fragment.selection_set.retain_fragments(fragments_to_keep)?; - if *parent_type == fragment.spread.type_condition_position - && fragment.spread.directives.is_empty() - { - // The fragment is of the same type as the parent, so we can just use - // the expanded sub-selections directly. - Ok(expanded_sub_selections.into()) + let existing = if let Some(existing) = existing { + existing } else { - // Create an inline fragment since type condition is necessary. - let inline = InlineFragmentSelection::from_selection_set( - parent_type.clone(), - expanded_sub_selections, - fragment.spread.directives.clone(), - ); - Ok(Selection::from(inline).into()) - } + // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! + // This should be reverted to `self.next_name();` when we're ready. + let name = self.generate_name(candidate.get()); + self.fragments.insert(Fragment { + schema: selection_set.schema.clone(), + name: name.clone(), + type_condition_position: candidate.get().inline_fragment.casted_type(), + directives: Default::default(), + selection_set: candidate.get().selection_set.clone(), + }); + self.fragments.get(&name).unwrap() + }; + new_selection_set.add_local_selection(&Selection::from( + FragmentSpreadSelection { + spread: FragmentSpread { + schema: selection_set.schema.clone(), + fragment_name: existing.name.clone(), + type_condition_position: existing.type_condition_position.clone(), + directives: skip_include.into(), + fragment_directives: existing.directives.clone(), + selection_id: crate::operation::SelectionId::new(), + }, + selection_set: existing.selection_set.clone(), + }, + ))?; } } - - // Otherwise, expand the sub-selections. - _ => Ok(self - .map_selection_set(|selection_set| { - Ok(Some(selection_set.retain_fragments(fragments_to_keep)?)) - })? - .into()), - } - } -} - -// Note: `retain_fragments` methods may return a selection or a selection set. -impl From for SelectionMapperReturn { - fn from(value: SelectionOrSet) -> Self { - match value { - SelectionOrSet::Selection(selection) => selection.into(), - SelectionOrSet::SelectionSet(selections) => { - // The items in a selection set needs to be cloned here, since it's sub-selections - // are contained in an `Arc`. - Vec::from_iter(selections.selections.values().cloned()).into() - } - } - } -} - -impl SelectionSet { - /// Expand fragments that are not in the `fragments_to_keep`. - // PORT_NOTE: The JS version's name was `expandFragments`, which was confusing with - // `expand_all_fragments`. So, it was renamed to `retain_fragments`. - fn retain_fragments( - &self, - fragments_to_keep: &NamedFragments, - ) -> Result { - self.lazy_map(fragments_to_keep, |selection| { - Ok(selection - .retain_fragments(&self.type_position, fragments_to_keep)? - .into()) - }) - } -} - -//============================================================================= -// Optimize (or reduce) the named fragments in the query -// -// Things to consider: -// - Unused fragment definitions can be dropped without an issue. -// - Dropping low-usage named fragments and expanding them may insert other fragments resulting in -// increased usage of those inserted. -// -// Example: -// ```graphql -// query { -// ...F1 -// } -// -// fragment F1 { -// a { ...F2 } -// b { ...F2 } -// } -// -// fragment F2 { -// // something -// } -// ``` -// then at this point where we've only counted usages in the query selection, `usages` will be -// `{ F1: 1, F2: 0 }`. But we do not want to expand _both_ F1 and F2. Instead, we want to expand -// F1 first, and then realize that this increases F2 usages to 2, which means we stop there and keep F2. - -impl NamedFragments { - /// Updates `self` by computing the reduced set of NamedFragments that are used in the - /// selection set and other fragments at least `min_usage_to_optimize` times. Also, computes - /// the new selection set that uses only the reduced set of fragments by expanding the other - /// ones. - /// - Returned selection set will be normalized. - fn reduce( - &mut self, - selection_set: &SelectionSet, - min_usage_to_optimize: u32, - ) -> Result { - // Call `reduce_inner` repeatedly until we reach a fix-point, since newly computed - // selection set may drop some fragment references due to normalization, which could lead - // to further reduction. - // - It is hard to avoid this chain reaction, since we need to account for the effects of - // normalization. - let mut last_size = self.len(); - let mut last_selection_set = selection_set.clone(); - while last_size > 0 { - let new_selection_set = - self.reduce_inner(&last_selection_set, min_usage_to_optimize)?; - - // Reached a fix-point => stop - if self.len() == last_size { - // Assumes that `new_selection_set` is the same as `last_selection_set` in this - // case. - break; - } - - // If we've expanded some fragments but kept others, then it's not 100% impossible that - // some fragment was used multiple times in some expanded fragment(s), but that - // post-expansion all of it's usages are "dead" branches that are removed by the final - // `flatten_unnecessary_fragments`. In that case though, we need to ensure we don't include the now-unused - // fragment in the final list of fragments. - // TODO: remark that the same reasoning could leave a single instance of a fragment - // usage, so if we really really want to never have less than `minUsagesToOptimize`, we - // could do some loop of `expand then flatten` unless all fragments are provably used - // enough. We don't bother, because leaving this is not a huge deal and it's not worth - // the complexity, but it could be that we can refactor all this later to avoid this - // case without additional complexity. - - // Prepare the next iteration - last_size = self.len(); - last_selection_set = new_selection_set; - } - Ok(last_selection_set) - } - - /// The inner loop body of `reduce` method. - fn reduce_inner( - &mut self, - selection_set: &SelectionSet, - min_usage_to_optimize: u32, - ) -> Result { - let mut usages = selection_set.used_fragments(); - - // Short-circuiting: Nothing was used => Drop everything (selection_set is unchanged). - if usages.is_empty() { - *self = Default::default(); - return Ok(selection_set.clone()); - } - - // Determine which one to retain. - // - Calculate the usage count of each fragment in both query and other fragment definitions. - // - If a fragment is to keep, fragments used in it are counted. - // - If a fragment is to drop, fragments used in it are counted and multiplied by its usage. - // - Decide in reverse dependency order, so that at each step, the fragment being visited - // has following properties: - // - It is either indirectly used by a previous fragment; Or, not used directly by any - // one visited & retained before. - // - Its usage count should be correctly calculated as if dropped fragments were expanded. - // - We take advantage of the fact that `NamedFragments` is already sorted in dependency - // order. - // PORT_NOTE: The `computeFragmentsToKeep` function is implemented here. - let original_size = self.len(); - for fragment in self.iter_rev() { - let usage_count = usages.get(&fragment.name).copied().unwrap_or_default(); - if usage_count >= min_usage_to_optimize { - // Count indirect usages within the fragment definition. - fragment.collect_used_fragment_names(&mut usages); - } else { - // Compute the new usage count after expanding the `fragment`. - Self::update_usages(&mut usages, fragment, usage_count); - } } - self.retain(|name, _fragment| { - let usage_count = usages.get(name).copied().unwrap_or_default(); - usage_count >= min_usage_to_optimize - }); - - // Short-circuiting: Nothing was dropped (fully used) => Nothing to change. - if self.len() == original_size { - return Ok(selection_set.clone()); - } - - // Update the fragment definitions in `self` after reduction. - // Note: This is an unfortunate clone, since `self` can't be passed to `retain_fragments`, - // while being mutated. - let fragments_to_keep = self.clone(); - for (_, fragment) in self.iter_mut() { - Node::make_mut(fragment).selection_set = fragment - .selection_set - .retain_fragments(&fragments_to_keep)? - .flatten_unnecessary_fragments( - &fragment.selection_set.type_position, - &fragments_to_keep, - &fragment.schema, - )?; - } - - // Compute the new selection set based on the new reduced set of fragments. - // Note that optimizing all fragments to potentially re-expand some is not entirely - // optimal, but it's unclear how to do otherwise, and it probably don't matter too much in - // practice (we only call this optimization on the final computed query plan, so not a very - // hot path; plus in most cases we won't even reach that point either because there is no - // fragment, or none will have been optimized away so we'll exit above). - let reduced_selection_set = selection_set.retain_fragments(self)?; + *selection_set = new_selection_set; - // Expanding fragments could create some "inefficiencies" that we wouldn't have if we - // hadn't re-optimized the fragments to de-optimize it later, so we do a final "flatten" - // pass to remove those. - reduced_selection_set.flatten_unnecessary_fragments( - &reduced_selection_set.type_position, - self, - &selection_set.schema, - ) + Ok(()) } - fn update_usages( - usages: &mut IndexMap, - fragment: &Node, - usage_count: u32, - ) { - let mut inner_usages = IndexMap::default(); - fragment.collect_used_fragment_names(&mut inner_usages); - - for (name, inner_count) in inner_usages { - *usages.entry(name).or_insert(0) += inner_count * usage_count; - } + /// Consumes the generator and returns the fragments it generated. + fn into_inner(self) -> NamedFragments { + self.fragments } } //============================================================================= -// `reuse_fragments` methods (putting everything together) - -impl Selection { - fn reuse_fragments_inner( - &self, - context: &ReuseContext<'_>, - validator: &mut FieldsConflictMultiBranchValidator, - fragments_at_type: &mut FragmentRestrictionAtTypeCache, - ) -> Result { - match self { - Selection::Field(field) => Ok(field - .reuse_fragments_inner(context, validator, fragments_at_type)? - .into()), - Selection::FragmentSpread(_) => Ok(self.clone()), // Do nothing - Selection::InlineFragment(inline_fragment) => Ok(inline_fragment - .reuse_fragments_inner(context, validator, fragments_at_type)? - .into()), - } - } -} - -impl FieldSelection { - fn reuse_fragments_inner( - &self, - context: &ReuseContext<'_>, - validator: &mut FieldsConflictMultiBranchValidator, - fragments_at_type: &mut FragmentRestrictionAtTypeCache, - ) -> Result { - let Some(base_composite_type): Option = - self.field.output_base_type()?.try_into().ok() - else { - return Ok(self.clone()); - }; - let Some(ref selection_set) = self.selection_set else { - return Ok(self.clone()); - }; - - let mut field_validator = validator.for_field(&self.field); - - // First, see if we can reuse fragments for the selection of this field. - let opt = selection_set.try_apply_fragments( - &base_composite_type, - context, - &mut field_validator, - fragments_at_type, - FullMatchingFragmentCondition::ForFieldSelection, - )?; - - let mut optimized = match opt { - SelectionSetOrFragment::Fragment(fragment) => { - let fragment_selection = FragmentSpreadSelection::from_fragment( - &fragment, - /*directives*/ &Default::default(), - ); - SelectionSet::from_selection(base_composite_type, fragment_selection.into()) - } - SelectionSetOrFragment::SelectionSet(selection_set) => selection_set, - }; - optimized = - optimized.reuse_fragments_inner(context, &mut field_validator, fragments_at_type)?; - Ok(self.with_updated_selection_set(Some(optimized))) - } -} - -/// Return type for `InlineFragmentSelection::reuse_fragments`. -#[derive(derive_more::From)] -enum FragmentSelection { - // Note: Enum variants are named to match those of `Selection`. - InlineFragment(InlineFragmentSelection), - FragmentSpread(FragmentSpreadSelection), -} - -impl From for Selection { - fn from(value: FragmentSelection) -> Self { - match value { - FragmentSelection::InlineFragment(inline_fragment) => inline_fragment.into(), - FragmentSelection::FragmentSpread(fragment_spread) => fragment_spread.into(), - } - } -} - -impl InlineFragmentSelection { - fn reuse_fragments_inner( - &self, - context: &ReuseContext<'_>, - validator: &mut FieldsConflictMultiBranchValidator, - fragments_at_type: &mut FragmentRestrictionAtTypeCache, - ) -> Result { - let optimized; +// Tests - let type_condition_position = &self.inline_fragment.type_condition_position; - if let Some(type_condition_position) = type_condition_position { - let opt = self.selection_set.try_apply_fragments( - type_condition_position, - context, - validator, - fragments_at_type, - FullMatchingFragmentCondition::ForInlineFragmentSelection { - type_condition_position, - directives: &self.inline_fragment.directives, - }, - )?; +#[cfg(test)] +mod tests { + use super::*; + use crate::operation::tests::*; - match opt { - SelectionSetOrFragment::Fragment(fragment) => { - // We're fully matching the sub-selection. If the fragment condition is also - // this element condition, then we can replace the whole element by the spread - // (not just the sub-selection). - if *type_condition_position == fragment.type_condition_position { - // Optimized as `...`, dropping the original inline spread (`self`). + /// Returns a consistent GraphQL name for the given index. + fn fragment_name(mut index: usize) -> Name { + /// https://spec.graphql.org/draft/#NameContinue + const NAME_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + /// https://spec.graphql.org/draft/#NameStart + const NAME_START_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; - // Note that `FullMatchingFragmentCondition::ForInlineFragmentSelection` - // above guarantees that this element directives are a superset of the - // fragment directives. But there can be additional directives, and in that - // case they should be kept on the spread. - // PORT_NOTE: We are assuming directives on fragment definitions are - // carried over to their spread sites as JS version does, which - // is handled differently in Rust version (see `FragmentSpread`). - let directives: executable::DirectiveList = self - .inline_fragment - .directives - .iter() - .filter(|d1| !fragment.directives.iter().any(|d2| *d1 == d2)) - .cloned() - .collect(); - return Ok( - FragmentSpreadSelection::from_fragment(&fragment, &directives).into(), - ); - } else { - // Otherwise, we keep this element and use a sub-selection with just the spread. - // Optimized as `...on { ... }` - optimized = SelectionSet::from_selection( - type_condition_position.clone(), - FragmentSpreadSelection::from_fragment( - &fragment, - /*directives*/ &Default::default(), - ) - .into(), - ); - } - } - SelectionSetOrFragment::SelectionSet(selection_set) => { - optimized = selection_set; - } - } + if index < NAME_START_CHARS.len() { + Name::new_static_unchecked(&NAME_START_CHARS[index..index + 1]) } else { - optimized = self.selection_set.clone(); - } - - Ok(InlineFragmentSelection::new( - self.inline_fragment.clone(), - // Then, recurse inside the field sub-selection (note that if we matched some fragments - // above, this recursion will "ignore" those as `FragmentSpreadSelection`'s - // `reuse_fragments()` is a no-op). - optimized.reuse_fragments_inner(context, validator, fragments_at_type)?, - ) - .into()) - } -} - -impl SelectionSet { - fn reuse_fragments_inner( - &self, - context: &ReuseContext<'_>, - validator: &mut FieldsConflictMultiBranchValidator, - fragments_at_type: &mut FragmentRestrictionAtTypeCache, - ) -> Result { - self.lazy_map(context.fragments, |selection| { - Ok(selection - .reuse_fragments_inner(context, validator, fragments_at_type)? - .into()) - }) - } - - fn contains_fragment_spread(&self) -> bool { - self.iter().any(|selection| { - matches!(selection, Selection::FragmentSpread(_)) - || selection - .selection_set() - .map(|subselection| subselection.contains_fragment_spread()) - .unwrap_or(false) - }) - } - - /// ## Errors - /// Returns an error if the selection set contains a named fragment spread. - fn reuse_fragments(&mut self, context: &ReuseContext<'_>) -> Result<(), FederationError> { - if context.fragments.is_empty() { - return Ok(()); - } - - if self.contains_fragment_spread() { - return Err(FederationError::internal("reuse_fragments() must only be used on selection sets that do not contain named fragment spreads")); - } + let mut s = String::new(); - // Calling reuse_fragments() will not match a fragment that would have expanded at - // top-level. That is, say we have the selection set `{ x y }` for a top-level `Query`, and - // we have a fragment - // ``` - // fragment F on Query { - // x - // y - // } - // ``` - // then calling `self.reuse_fragments(fragments)` would only apply check if F apply to - // `x` and then `y`. - // - // To ensure the fragment match in this case, we "wrap" the selection into a trivial - // fragment of the selection parent, so in the example above, we create selection `... on - // Query { x y }`. With that, `reuse_fragments` will correctly match on the `on Query` - // fragment; after which we can unpack the final result. - let wrapped = InlineFragmentSelection::from_selection_set( - self.type_position.clone(), // parent type - self.clone(), // selection set - Default::default(), // directives - ); - let mut validator = FieldsConflictMultiBranchValidator::from_initial_validator( - FieldsConflictValidator::from_selection_set(self), - ); - let optimized = wrapped.reuse_fragments_inner( - context, - &mut validator, - &mut FragmentRestrictionAtTypeCache::default(), - )?; + let i = index % NAME_START_CHARS.len(); + s.push(NAME_START_CHARS.as_bytes()[i].into()); + index /= NAME_START_CHARS.len(); - // Now, it's possible we matched a full fragment, in which case `optimized` will be just - // the named fragment, and in that case we return a singleton selection with just that. - // Otherwise, it's our wrapping inline fragment with the sub-selections optimized, and we - // just return that subselection. - *self = match optimized { - FragmentSelection::FragmentSpread(spread) => { - SelectionSet::from_selection(self.type_position.clone(), spread.into()) + while index > 0 { + let i = index % NAME_CHARS.len(); + s.push(NAME_CHARS.as_bytes()[i].into()); + index /= NAME_CHARS.len(); } - FragmentSelection::InlineFragment(inline_fragment) => inline_fragment.selection_set, - }; - Ok(()) - } -} - -impl Operation { - // PORT_NOTE: The JS version of `reuse_fragments` takes an optional `minUsagesToOptimize` argument. - // However, it's only used in tests. So, it's removed in the Rust version. - const DEFAULT_MIN_USAGES_TO_OPTIMIZE: u32 = 2; - // `fragments` - rebased fragment definitions for the operation's subgraph - // - `self.selection_set` must be fragment-spread-free. - fn reuse_fragments_inner( - &mut self, - fragments: &NamedFragments, - min_usages_to_optimize: u32, - ) -> Result<(), FederationError> { - if fragments.is_empty() { - return Ok(()); - } - - // Optimize the operation's selection set by re-using existing fragments. - let before_optimization = self.selection_set.clone(); - self.selection_set - .reuse_fragments(&ReuseContext::for_operation(fragments, &self.variables))?; - if before_optimization == self.selection_set { - return Ok(()); - } - - // Optimize the named fragment definitions by dropping low-usage ones. - let mut final_fragments = fragments.clone(); - let final_selection_set = - final_fragments.reduce(&self.selection_set, min_usages_to_optimize)?; - - self.selection_set = final_selection_set; - self.named_fragments = final_fragments; - Ok(()) - } - - /// Optimize the parsed size of the operation by applying fragment spreads. Fragment spreads - /// are reused from the original user-provided fragments. - /// - /// `fragments` - rebased fragment definitions for the operation's subgraph - /// - // PORT_NOTE: In JS, this function was called "optimize". - pub(crate) fn reuse_fragments( - &mut self, - fragments: &NamedFragments, - ) -> Result<(), FederationError> { - self.reuse_fragments_inner(fragments, Self::DEFAULT_MIN_USAGES_TO_OPTIMIZE) - } - - /// Optimize the parsed size of the operation by generating fragments based on the selections - /// in the operation. - pub(crate) fn generate_fragments(&mut self) -> Result<(), FederationError> { - // Currently, this method simply pulls out every inline fragment into a named fragment. If - // multiple inline fragments are the same, they use the same named fragment. - // - // This method can generate named fragments that are only used once. It's not ideal, but it - // also doesn't seem that bad. Avoiding this is possible but more work, and keeping this - // as simple as possible is a big benefit for now. - // - // When we have more advanced correctness testing, we can add more features to fragment - // generation, like factoring out partial repeated slices of selection sets or only - // introducing named fragments for patterns that occur more than once. - let mut generator = FragmentGenerator::default(); - generator.visit_selection_set(&mut self.selection_set)?; - self.named_fragments = generator.into_inner(); - Ok(()) - } - - /// Used by legacy roundtrip tests. - /// - This lowers `min_usages_to_optimize` to `1` in order to make it easier to write unit tests. - #[cfg(test)] - fn reuse_fragments_for_roundtrip_test( - &mut self, - fragments: &NamedFragments, - ) -> Result<(), FederationError> { - self.reuse_fragments_inner(fragments, /*min_usages_to_optimize*/ 1) - } - - // PORT_NOTE: This mirrors the JS version's `Operation.expandAllFragments`. But this method is - // mainly for unit tests. The actual port of `expandAllFragments` is in `normalize_operation`. - #[cfg(test)] - fn expand_all_fragments_and_normalize(&self) -> Result { - let selection_set = self - .selection_set - .expand_all_fragments()? - .flatten_unnecessary_fragments( - &self.selection_set.type_position, - &self.named_fragments, - &self.schema, - )?; - Ok(Self { - named_fragments: Default::default(), - selection_set, - ..self.clone() - }) - } -} - -#[derive(Debug, Default)] -struct FragmentGenerator { - fragments: NamedFragments, - // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! - names: IndexMap<(String, usize), usize>, -} - -impl FragmentGenerator { - // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! - // In the future, we will just use `.next_name()`. - fn generate_name(&mut self, frag: &InlineFragmentSelection) -> Name { - use std::fmt::Write as _; - - let type_condition = frag - .inline_fragment - .type_condition_position - .as_ref() - .map_or_else( - || "undefined".to_string(), - |condition| condition.to_string(), - ); - let selections = frag.selection_set.selections.len(); - let mut name = format!("_generated_on{type_condition}{selections}"); - - let key = (type_condition, selections); - let index = self - .names - .entry(key) - .and_modify(|index| *index += 1) - .or_default(); - _ = write!(&mut name, "_{index}"); - - Name::new_unchecked(&name) - } - - /// Is a selection set worth using for a newly generated named fragment? - fn is_worth_using(selection_set: &SelectionSet) -> bool { - let mut iter = selection_set.iter(); - let Some(first) = iter.next() else { - // An empty selection is not worth using (and invalid!) - return false; - }; - let Selection::Field(field) = first else { - return true; - }; - // If there's more than one selection, or one selection with a subselection, - // it's probably worth using - iter.next().is_some() || field.selection_set.is_some() - } - - /// Modify the selection set so that eligible inline fragments are moved to named fragment spreads. - fn visit_selection_set( - &mut self, - selection_set: &mut SelectionSet, - ) -> Result<(), FederationError> { - let mut new_selection_set = SelectionSet::empty( - selection_set.schema.clone(), - selection_set.type_position.clone(), - ); - - for selection in Arc::make_mut(&mut selection_set.selections).values_mut() { - match selection { - SelectionValue::Field(mut field) => { - if let Some(selection_set) = field.get_selection_set_mut() { - self.visit_selection_set(selection_set)?; - } - new_selection_set - .add_local_selection(&Selection::Field(Arc::clone(field.get())))?; - } - SelectionValue::FragmentSpread(frag) => { - new_selection_set - .add_local_selection(&Selection::FragmentSpread(Arc::clone(frag.get())))?; - } - SelectionValue::InlineFragment(frag) - if !Self::is_worth_using(&frag.get().selection_set) => - { - new_selection_set - .add_local_selection(&Selection::InlineFragment(Arc::clone(frag.get())))?; - } - SelectionValue::InlineFragment(mut candidate) => { - self.visit_selection_set(candidate.get_selection_set_mut())?; - - // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! - // JS federation does not consider fragments without a type condition. - if candidate - .get() - .inline_fragment - .type_condition_position - .is_none() - { - new_selection_set.add_local_selection(&Selection::InlineFragment( - Arc::clone(candidate.get()), - ))?; - continue; - } - - let directives = &candidate.get().inline_fragment.directives; - let skip_include = directives - .iter() - .map(|directive| match directive.name.as_str() { - "skip" | "include" => Ok(directive.clone()), - _ => Err(()), - }) - .collect::>(); - - // If there are any directives *other* than @skip and @include, - // we can't just transfer them to the generated fragment spread, - // so we have to keep this inline fragment. - let Ok(skip_include) = skip_include else { - new_selection_set.add_local_selection(&Selection::InlineFragment( - Arc::clone(candidate.get()), - ))?; - continue; - }; - - // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! - // JS does not special-case @skip and @include. It never extracts a fragment if - // there's any directives on it. This code duplicates the body from the - // previous condition so it's very easy to remove when we're ready :) - if !skip_include.is_empty() { - new_selection_set.add_local_selection(&Selection::InlineFragment( - Arc::clone(candidate.get()), - ))?; - continue; - } - - let existing = self.fragments.iter().find(|existing| { - existing.type_condition_position - == candidate.get().inline_fragment.casted_type() - && existing.selection_set == candidate.get().selection_set - }); - - let existing = if let Some(existing) = existing { - existing - } else { - // XXX(@goto-bus-stop): This is temporary to support mismatch testing with JS! - // This should be reverted to `self.next_name();` when we're ready. - let name = self.generate_name(candidate.get()); - self.fragments.insert(Fragment { - schema: selection_set.schema.clone(), - name: name.clone(), - type_condition_position: candidate.get().inline_fragment.casted_type(), - directives: Default::default(), - selection_set: candidate.get().selection_set.clone(), - }); - self.fragments.get(&name).unwrap() - }; - new_selection_set.add_local_selection(&Selection::from( - FragmentSpreadSelection { - spread: FragmentSpread { - schema: selection_set.schema.clone(), - fragment_name: existing.name.clone(), - type_condition_position: existing.type_condition_position.clone(), - directives: skip_include.into(), - fragment_directives: existing.directives.clone(), - selection_id: crate::operation::SelectionId::new(), - }, - selection_set: existing.selection_set.clone(), - }, - ))?; - } - } - } - - *selection_set = new_selection_set; - - Ok(()) - } - - /// Consumes the generator and returns the fragments it generated. - fn into_inner(self) -> NamedFragments { - self.fragments - } -} - -//============================================================================= -// Tests - -#[cfg(test)] -mod tests { - use apollo_compiler::ExecutableDocument; - - use super::*; - use crate::operation::tests::*; - - macro_rules! assert_without_fragments { - ($operation: expr, @$expected: literal) => {{ - let without_fragments = $operation.expand_all_fragments_and_normalize().unwrap(); - insta::assert_snapshot!(without_fragments, @$expected); - without_fragments - }}; - } - - macro_rules! assert_optimized { - ($operation: expr, $named_fragments: expr, @$expected: literal) => {{ - let mut optimized = $operation.clone(); - optimized.reuse_fragments(&$named_fragments).unwrap(); - validate_operation(&$operation.schema, &optimized.to_string()); - insta::assert_snapshot!(optimized, @$expected) - }}; - } - - /// Returns a consistent GraphQL name for the given index. - fn fragment_name(mut index: usize) -> Name { - /// https://spec.graphql.org/draft/#NameContinue - const NAME_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; - /// https://spec.graphql.org/draft/#NameStart - const NAME_START_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; - - if index < NAME_START_CHARS.len() { - Name::new_static_unchecked(&NAME_START_CHARS[index..index + 1]) - } else { - let mut s = String::new(); - - let i = index % NAME_START_CHARS.len(); - s.push(NAME_START_CHARS.as_bytes()[i].into()); - index /= NAME_START_CHARS.len(); - - while index > 0 { - let i = index % NAME_CHARS.len(); - s.push(NAME_CHARS.as_bytes()[i].into()); - index /= NAME_CHARS.len(); - } - - Name::new_unchecked(&s) + Name::new_unchecked(&s) } } @@ -1832,1619 +389,6 @@ mod tests { assert_eq!(fragment_name(usize::MAX), "oS5Uz8g3Iqw"); } - #[test] - fn duplicate_fragment_spreads_after_fragment_expansion() { - // This is a regression test for FED-290, making sure `make_select` method can handle - // duplicate fragment spreads. - // During optimization, `make_selection` may merge multiple fragment spreads with the same - // key. This can happen in the case below where `F1` and `F2` are expanded and generating - // two duplicate `F_shared` spreads in the definition of `fragment F_target`. - let schema_doc = r#" - type Query { - t: T - t2: T - } - - type T { - id: ID! - a: Int! - b: Int! - c: Int! - } - "#; - - let query = r#" - fragment F_shared on T { - id - a - } - fragment F1 on T { - ...F_shared - b - } - - fragment F2 on T { - ...F_shared - c - } - - fragment F_target on T { - ...F1 - ...F2 - } - - query { - t { - ...F_target - } - t2 { - ...F_target - } - } - "#; - - let operation = parse_operation(&parse_schema(schema_doc), query); - let expanded = operation.expand_all_fragments_and_normalize().unwrap(); - assert_optimized!(expanded, operation.named_fragments, @r###" - fragment F_target on T { - id - a - b - c - } - - { - t { - ...F_target - } - t2 { - ...F_target - } - } - "###); - } - - #[test] - fn optimize_fragments_using_other_fragments_when_possible() { - let schema = r#" - type Query { - t: I - } - - interface I { - b: Int - u: U - } - - type T1 implements I { - a: Int - b: Int - u: U - } - - type T2 implements I { - x: String - y: String - b: Int - u: U - } - - union U = T1 | T2 - "#; - - let query = r#" - fragment OnT1 on T1 { - a - b - } - - fragment OnT2 on T2 { - x - y - } - - fragment OnI on I { - b - } - - fragment OnU on U { - ...OnI - ...OnT1 - ...OnT2 - } - - query { - t { - ...OnT1 - ...OnT2 - ...OnI - u { - ...OnU - } - } - } - "#; - - let operation = parse_operation(&parse_schema(schema), query); - - let expanded = assert_without_fragments!( - operation, - @r###" - { - t { - ... on T1 { - a - b - } - ... on T2 { - x - y - } - b - u { - ... on I { - b - } - ... on T1 { - a - b - } - ... on T2 { - x - y - } - } - } - } - "### - ); - - assert_optimized!(expanded, operation.named_fragments, @r###" - fragment OnU on U { - ... on I { - b - } - ... on T1 { - a - b - } - ... on T2 { - x - y - } - } - - { - t { - ...OnU - u { - ...OnU - } - } - } - "###); - } - - #[test] - fn handles_fragments_using_other_fragments() { - let schema = r#" - type Query { - t: I - } - - interface I { - b: Int - c: Int - u1: U - u2: U - } - - type T1 implements I { - a: Int - b: Int - c: Int - me: T1 - u1: U - u2: U - } - - type T2 implements I { - x: String - y: String - b: Int - c: Int - u1: U - u2: U - } - - union U = T1 | T2 - "#; - - let query = r#" - fragment OnT1 on T1 { - a - b - } - - fragment OnT2 on T2 { - x - y - } - - fragment OnI on I { - b - c - } - - fragment OnU on U { - ...OnI - ...OnT1 - ...OnT2 - } - - query { - t { - ...OnT1 - ...OnT2 - u1 { - ...OnU - } - u2 { - ...OnU - } - ... on T1 { - me { - ...OnI - } - } - } - } - "#; - - let operation = parse_operation(&parse_schema(schema), query); - - let expanded = assert_without_fragments!( - &operation, - @r###" - { - t { - ... on T1 { - a - b - me { - b - c - } - } - ... on T2 { - x - y - } - u1 { - ... on I { - b - c - } - ... on T1 { - a - b - } - ... on T2 { - x - y - } - } - u2 { - ... on I { - b - c - } - ... on T1 { - a - b - } - ... on T2 { - x - y - } - } - } - } - "###); - - // We should reuse and keep all fragments, because 1) onU is used twice and 2) - // all the other ones are used once in the query, and once in onU definition. - assert_optimized!(expanded, operation.named_fragments, @r###" - fragment OnT1 on T1 { - a - b - } - - fragment OnT2 on T2 { - x - y - } - - fragment OnI on I { - b - c - } - - fragment OnU on U { - ...OnI - ...OnT1 - ...OnT2 - } - - { - t { - ... on T1 { - ...OnT1 - me { - ...OnI - } - } - ...OnT2 - u1 { - ...OnU - } - u2 { - ...OnU - } - } - } - "###); - } - - macro_rules! test_fragments_roundtrip { - ($schema_doc: expr, $query: expr, @$expanded: literal) => {{ - let schema = parse_schema($schema_doc); - let operation = parse_operation(&schema, $query); - let without_fragments = operation.expand_all_fragments_and_normalize().unwrap(); - insta::assert_snapshot!(without_fragments, @$expanded); - - let mut optimized = without_fragments; - optimized.reuse_fragments(&operation.named_fragments).unwrap(); - validate_operation(&operation.schema, &optimized.to_string()); - assert_eq!(optimized.to_string(), operation.to_string()); - }}; - } - - /// Tests ported from JS codebase rely on special behavior of - /// `Operation::reuse_fragments_for_roundtrip_test` that is specific for testing, since it makes it - /// easier to write tests. - macro_rules! test_fragments_roundtrip_legacy { - ($schema_doc: expr, $query: expr, @$expanded: literal) => {{ - let schema = parse_schema($schema_doc); - let operation = parse_operation(&schema, $query); - let without_fragments = operation.expand_all_fragments_and_normalize().unwrap(); - insta::assert_snapshot!(without_fragments, @$expanded); - - let mut optimized = without_fragments; - optimized.reuse_fragments_for_roundtrip_test(&operation.named_fragments).unwrap(); - validate_operation(&operation.schema, &optimized.to_string()); - assert_eq!(optimized.to_string(), operation.to_string()); - }}; - } - - #[test] - fn handles_fragments_with_nested_selections() { - let schema_doc = r#" - type Query { - t1a: T1 - t2a: T1 - } - - type T1 { - t2: T2 - } - - type T2 { - x: String - y: String - } - "#; - - let query = r#" - fragment OnT1 on T1 { - t2 { - x - } - } - - query { - t1a { - ...OnT1 - t2 { - y - } - } - t2a { - ...OnT1 - } - } - "#; - - test_fragments_roundtrip!(schema_doc, query, @r###" - { - t1a { - t2 { - x - y - } - } - t2a { - t2 { - x - } - } - } - "###); - } - - #[test] - fn handles_nested_fragments_with_field_intersection() { - let schema_doc = r#" - type Query { - t: T - } - - type T { - a: A - b: Int - } - - type A { - x: String - y: String - z: String - } - "#; - - // The subtlety here is that `FA` contains `__typename` and so after we're reused it, the - // selection will look like: - // { - // t { - // a { - // ...FA - // } - // } - // } - // But to recognize that `FT` can be reused from there, we need to be able to see that - // the `__typename` that `FT` wants is inside `FA` (and since FA applies on the parent type `A` - // directly, it is fine to reuse). - let query = r#" - fragment FA on A { - __typename - x - y - } - - fragment FT on T { - a { - __typename - ...FA - } - } - - query { - t { - ...FT - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t { - a { - __typename - x - y - } - } - } - "###); - } - - #[test] - fn handles_fragment_matching_subset_of_field_selection() { - let schema_doc = r#" - type Query { - t: T - } - - type T { - a: String - b: B - c: Int - d: D - } - - type B { - x: String - y: String - } - - type D { - m: String - n: String - } - "#; - - let query = r#" - fragment FragT on T { - b { - __typename - x - } - c - d { - m - } - } - - { - t { - ...FragT - d { - n - } - a - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t { - b { - __typename - x - } - c - d { - m - n - } - a - } - } - "###); - } - - #[test] - fn handles_fragment_matching_subset_of_inline_fragment_selection() { - // Pretty much the same test than the previous one, but matching inside a fragment selection inside - // of inside a field selection. - // PORT_NOTE: ` implements I` was added in the definition of `type T`, so that validation can pass. - let schema_doc = r#" - type Query { - i: I - } - - interface I { - a: String - } - - type T implements I { - a: String - b: B - c: Int - d: D - } - - type B { - x: String - y: String - } - - type D { - m: String - n: String - } - "#; - - let query = r#" - fragment FragT on T { - b { - __typename - x - } - c - d { - m - } - } - - { - i { - ... on T { - ...FragT - d { - n - } - a - } - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - i { - ... on T { - b { - __typename - x - } - c - d { - m - n - } - a - } - } - } - "###); - } - - #[test] - fn intersecting_fragments() { - let schema_doc = r#" - type Query { - t: T - } - - type T { - a: String - b: B - c: Int - d: D - } - - type B { - x: String - y: String - } - - type D { - m: String - n: String - } - "#; - - // Note: the code that reuse fragments iterates on fragments in the order they are defined - // in the document, but when it reuse a fragment, it puts it at the beginning of the - // selection (somewhat random, it just feel often easier to read), so the net effect on - // this example is that `Frag2`, which will be reused after `Frag1` will appear first in - // the re-optimized selection. So we put it first in the input too so that input and output - // actually match (the `testFragmentsRoundtrip` compares strings, so it is sensible to - // ordering; we could theoretically use `Operation.equals` instead of string equality, - // which wouldn't really on ordering, but `Operation.equals` is not entirely trivial and - // comparing strings make problem a bit more obvious). - let query = r#" - fragment Frag1 on T { - b { - x - } - c - d { - m - } - } - - fragment Frag2 on T { - a - b { - __typename - x - } - d { - m - n - } - } - - { - t { - ...Frag1 - ...Frag2 - } - } - "#; - - // PORT_NOTE: `__typename` and `x`'s placements are switched in Rust. - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t { - b { - __typename - x - } - c - d { - m - n - } - a - } - } - "###); - } - - #[test] - fn fragments_application_makes_type_condition_trivial() { - let schema_doc = r#" - type Query { - t: T - } - - interface I { - x: String - } - - type T implements I { - x: String - a: String - } - "#; - - let query = r#" - fragment FragI on I { - x - ... on T { - a - } - } - - { - t { - ...FragI - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t { - x - a - } - } - "###); - } - - #[test] - fn handles_fragment_matching_at_the_top_level_of_another_fragment() { - let schema_doc = r#" - type Query { - t: T - } - - type T { - a: String - u: U - } - - type U { - x: String - y: String - } - "#; - - let query = r#" - fragment Frag1 on T { - a - } - - fragment Frag2 on T { - u { - x - y - } - ...Frag1 - } - - fragment Frag3 on Query { - t { - ...Frag2 - } - } - - { - ...Frag3 - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t { - u { - x - y - } - a - } - } - "###); - } - - #[test] - fn handles_fragments_used_in_context_where_they_get_trimmed() { - let schema_doc = r#" - type Query { - t1: T1 - } - - interface I { - x: Int - } - - type T1 implements I { - x: Int - y: Int - } - - type T2 implements I { - x: Int - z: Int - } - "#; - - let query = r#" - fragment FragOnI on I { - ... on T1 { - y - } - ... on T2 { - z - } - } - - { - t1 { - ...FragOnI - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t1 { - y - } - } - "###); - } - - #[test] - fn handles_fragments_used_in_the_context_of_non_intersecting_abstract_types() { - let schema_doc = r#" - type Query { - i2: I2 - } - - interface I1 { - x: Int - } - - interface I2 { - y: Int - } - - interface I3 { - z: Int - } - - type T1 implements I1 & I2 { - x: Int - y: Int - } - - type T2 implements I1 & I3 { - x: Int - z: Int - } - "#; - - let query = r#" - fragment FragOnI1 on I1 { - ... on I2 { - y - } - ... on I3 { - z - } - } - - { - i2 { - ...FragOnI1 - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - i2 { - ... on I1 { - ... on I2 { - y - } - ... on I3 { - z - } - } - } - } - "###); - } - - #[test] - fn handles_fragments_on_union_in_context_with_limited_intersection() { - let schema_doc = r#" - type Query { - t1: T1 - } - - union U = T1 | T2 - - type T1 { - x: Int - } - - type T2 { - y: Int - } - "#; - - let query = r#" - fragment OnU on U { - ... on T1 { - x - } - ... on T2 { - y - } - } - - { - t1 { - ...OnU - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - { - t1 { - x - } - } - "###); - } - - #[test] - fn off_by_1_error() { - let schema = r#" - type Query { - t: T - } - type T { - id: String! - a: A - v: V - } - type A { - id: String! - } - type V { - t: T! - } - "#; - - let query = r#" - { - t { - ...TFrag - v { - t { - id - a { - __typename - id - } - } - } - } - } - - fragment TFrag on T { - __typename - id - } - "#; - - let operation = parse_operation(&parse_schema(schema), query); - - let expanded = assert_without_fragments!( - operation, - @r###" - { - t { - __typename - id - v { - t { - id - a { - __typename - id - } - } - } - } - } - "### - ); - - assert_optimized!(expanded, operation.named_fragments, @r###" - fragment TFrag on T { - __typename - id - } - - { - t { - ...TFrag - v { - t { - ...TFrag - a { - __typename - id - } - } - } - } - } - "###); - } - - #[test] - fn removes_all_unused_fragments() { - let schema = r#" - type Query { - t1: T1 - } - - union U1 = T1 | T2 | T3 - union U2 = T2 | T3 - - type T1 { - x: Int - } - - type T2 { - y: Int - } - - type T3 { - z: Int - } - "#; - - let query = r#" - query { - t1 { - ...Outer - } - } - - fragment Outer on U1 { - ... on T1 { - x - } - ... on T2 { - ... Inner - } - ... on T3 { - ... Inner - } - } - - fragment Inner on U2 { - ... on T2 { - y - } - } - "#; - - let operation = parse_operation(&parse_schema(schema), query); - - let expanded = assert_without_fragments!( - operation, - @r###" - { - t1 { - x - } - } - "### - ); - - // This is a bit of contrived example, but the reusing code will be able - // to figure out that the `Outer` fragment can be reused and will initially - // do so, but it's only use once, so it will expand it, which yields: - // { - // t1 { - // ... on T1 { - // x - // } - // ... on T2 { - // ... Inner - // } - // ... on T3 { - // ... Inner - // } - // } - // } - // and so `Inner` will not be expanded (it's used twice). Except that - // the `flatten_unnecessary_fragments` code is apply then and will _remove_ both instances - // of `.... Inner`. Which is ok, but we must make sure the fragment - // itself is removed since it is not used now, which this test ensures. - assert_optimized!(expanded, operation.named_fragments, @r###" - { - t1 { - x - } - } - "###); - } - - #[test] - fn removes_fragments_only_used_by_unused_fragments() { - // Similar to the previous test, but we artificially add a - // fragment that is only used by the fragment that is finally - // unused. - let schema = r#" - type Query { - t1: T1 - } - - union U1 = T1 | T2 | T3 - union U2 = T2 | T3 - - type T1 { - x: Int - } - - type T2 { - y1: Y - y2: Y - } - - type T3 { - z: Int - } - - type Y { - v: Int - } - "#; - - let query = r#" - query { - t1 { - ...Outer - } - } - - fragment Outer on U1 { - ... on T1 { - x - } - ... on T2 { - ... Inner - } - ... on T3 { - ... Inner - } - } - - fragment Inner on U2 { - ... on T2 { - y1 { - ...WillBeUnused - } - y2 { - ...WillBeUnused - } - } - } - - fragment WillBeUnused on Y { - v - } - "#; - - let operation = parse_operation(&parse_schema(schema), query); - - let expanded = assert_without_fragments!( - operation, - @r###" - { - t1 { - x - } - } - "### - ); - - assert_optimized!(expanded, operation.named_fragments, @r###" - { - t1 { - x - } - } - "###); - } - - #[test] - fn keeps_fragments_used_by_other_fragments() { - let schema = r#" - type Query { - t1: T - t2: T - } - - type T { - a1: Int - a2: Int - b1: B - b2: B - } - - type B { - x: Int - y: Int - } - "#; - - let query = r#" - query { - t1 { - ...TFields - } - t2 { - ...TFields - } - } - - fragment TFields on T { - ...DirectFieldsOfT - b1 { - ...BFields - } - b2 { - ...BFields - } - } - - fragment DirectFieldsOfT on T { - a1 - a2 - } - - fragment BFields on B { - x - y - } - "#; - - let operation = parse_operation(&parse_schema(schema), query); - - let expanded = assert_without_fragments!( - operation, - @r###" - { - t1 { - a1 - a2 - b1 { - x - y - } - b2 { - x - y - } - } - t2 { - a1 - a2 - b1 { - x - y - } - b2 { - x - y - } - } - } - "### - ); - - // The `DirectFieldsOfT` fragments should not be kept as it is used only once within `TFields`, - // but the `BFields` one should be kept. - assert_optimized!(expanded, operation.named_fragments, @r###" - fragment BFields on B { - x - y - } - - fragment TFields on T { - a1 - a2 - b1 { - ...BFields - } - b2 { - ...BFields - } - } - - { - t1 { - ...TFields - } - t2 { - ...TFields - } - } - "###); - } - - /// - /// applied directives - /// - - #[test] - fn reuse_fragments_with_same_directive_in_the_fragment_selection() { - let schema_doc = r#" - type Query { - t1: T - t2: T - t3: T - } - - type T { - a: Int - b: Int - c: Int - d: Int - } - "#; - - let query = r#" - fragment DirectiveInDef on T { - a @include(if: $cond1) - } - - query myQuery($cond1: Boolean!, $cond2: Boolean!) { - t1 { - a - } - t2 { - ...DirectiveInDef - } - t3 { - a @include(if: $cond2) - } - } - "#; - - test_fragments_roundtrip_legacy!(schema_doc, query, @r###" - query myQuery($cond1: Boolean!, $cond2: Boolean!) { - t1 { - a - } - t2 { - a @include(if: $cond1) - } - t3 { - a @include(if: $cond2) - } - } - "###); - } - - #[test] - fn reuse_fragments_with_directives_on_inline_fragments() { - let schema_doc = r#" - type Query { - t1: T - t2: T - t3: T - } - - type T { - a: Int - b: Int - c: Int - d: Int - } - "#; - - let query = r#" - fragment NoDirectiveDef on T { - a - } - - query myQuery($cond1: Boolean!) { - t1 { - ...NoDirectiveDef - } - t2 { - ...NoDirectiveDef @include(if: $cond1) - } - } - "#; - - test_fragments_roundtrip!(schema_doc, query, @r###" - query myQuery($cond1: Boolean!) { - t1 { - a - } - t2 { - ... on T @include(if: $cond1) { - a - } - } - } - "###); - } - - #[test] - fn reuse_fragments_with_directive_on_typename() { - let schema = r#" - type Query { - t1: T - t2: T - t3: T - } - - type T { - a: Int - b: Int - c: Int - d: Int - } - "#; - let query = r#" - query A ($if: Boolean!) { - t1 { b a ...x } - t2 { ...x } - } - query B { - # Because this inline fragment is exactly the same shape as `x`, - # except for a `__typename` field, it may be tempting to reuse it. - # But `x.__typename` has a directive with a variable, and this query - # does not have that variable declared, so it can't be used. - t3 { ... on T { a c } } - } - fragment x on T { - __typename @include(if: $if) - a - c - } - "#; - let schema = parse_schema(schema); - let query = ExecutableDocument::parse_and_validate(schema.schema(), query, "query.graphql") - .unwrap(); - - let operation_a = - Operation::from_operation_document(schema.clone(), &query, Some("A")).unwrap(); - let operation_b = - Operation::from_operation_document(schema.clone(), &query, Some("B")).unwrap(); - let expanded_b = operation_b.expand_all_fragments_and_normalize().unwrap(); - - assert_optimized!(expanded_b, operation_a.named_fragments, @r###" - query B { - t3 { - a - c - } - } - "###); - } - - #[test] - fn reuse_fragments_with_non_intersecting_types() { - let schema = r#" - type Query { - t: T - s: S - s2: S - i: I - } - - interface I { - a: Int - b: Int - } - - type T implements I { - a: Int - b: Int - - c: Int - d: Int - } - type S implements I { - a: Int - b: Int - - f: Int - g: Int - } - "#; - let query = r#" - query A ($if: Boolean!) { - t { ...x } - s { ...x } - i { ...x } - } - query B { - s { - # this matches fragment x once it is flattened, - # because the `...on T` condition does not intersect with our - # current type `S` - __typename - a b - } - s2 { - # same snippet to get it to use the fragment - __typename - a b - } - } - fragment x on I { - __typename - a - b - ... on T { c d @include(if: $if) } - } - "#; - let schema = parse_schema(schema); - let query = ExecutableDocument::parse_and_validate(schema.schema(), query, "query.graphql") - .unwrap(); - - let operation_a = - Operation::from_operation_document(schema.clone(), &query, Some("A")).unwrap(); - let operation_b = - Operation::from_operation_document(schema.clone(), &query, Some("B")).unwrap(); - let expanded_b = operation_b.expand_all_fragments_and_normalize().unwrap(); - - assert_optimized!(expanded_b, operation_a.named_fragments, @r###" - query B { - s { - __typename - a - b - } - s2 { - __typename - a - b - } - } - "###); - } - /// /// empty branches removal /// diff --git a/apollo-federation/src/operation/rebase.rs b/apollo-federation/src/operation/rebase.rs index 09b8799067..9243916327 100644 --- a/apollo-federation/src/operation/rebase.rs +++ b/apollo-federation/src/operation/rebase.rs @@ -9,7 +9,6 @@ use itertools::Itertools; use super::runtime_types_intersect; use super::Field; use super::FieldSelection; -use super::Fragment; use super::FragmentSpread; use super::FragmentSpreadSelection; use super::InlineFragment; @@ -46,45 +45,23 @@ fn print_possible_runtimes( ) } -/// Options for handling rebasing errors. -#[derive(Clone, Copy, Default)] -enum OnNonRebaseableSelection { - /// Drop the selection that can't be rebased and continue. - Drop, - /// Propagate the rebasing error. - #[default] - Error, -} - impl Selection { fn rebase_inner( &self, parent_type: &CompositeTypeDefinitionPosition, named_fragments: &NamedFragments, schema: &ValidFederationSchema, - on_non_rebaseable_selection: OnNonRebaseableSelection, ) -> Result { match self { Selection::Field(field) => field - .rebase_inner( - parent_type, - named_fragments, - schema, - on_non_rebaseable_selection, - ) + .rebase_inner(parent_type, named_fragments, schema) .map(|field| field.into()), - Selection::FragmentSpread(spread) => spread.rebase_inner( - parent_type, - named_fragments, - schema, - on_non_rebaseable_selection, - ), - Selection::InlineFragment(inline) => inline.rebase_inner( - parent_type, - named_fragments, - schema, - on_non_rebaseable_selection, - ), + Selection::FragmentSpread(spread) => { + spread.rebase_inner(parent_type, named_fragments, schema) + } + Selection::InlineFragment(inline) => { + inline.rebase_inner(parent_type, named_fragments, schema) + } } } @@ -94,7 +71,7 @@ impl Selection { named_fragments: &NamedFragments, schema: &ValidFederationSchema, ) -> Result { - self.rebase_inner(parent_type, named_fragments, schema, Default::default()) + self.rebase_inner(parent_type, named_fragments, schema) } fn can_add_to( @@ -146,18 +123,6 @@ pub(crate) enum RebaseError { }, } -impl FederationError { - fn is_rebase_error(&self) -> bool { - matches!( - self, - crate::error::FederationError::SingleFederationError { - inner: crate::error::SingleFederationError::InternalRebaseError(_), - .. - } - ) - } -} - impl From for FederationError { fn from(value: RebaseError) -> Self { crate::error::SingleFederationError::from(value).into() @@ -311,7 +276,6 @@ impl FieldSelection { parent_type: &CompositeTypeDefinitionPosition, named_fragments: &NamedFragments, schema: &ValidFederationSchema, - on_non_rebaseable_selection: OnNonRebaseableSelection, ) -> Result { if &self.field.schema == schema && &self.field.field_position.parent() == parent_type { // we are rebasing field on the same parent within the same schema - we can just return self @@ -344,12 +308,8 @@ impl FieldSelection { }); } - let rebased_selection_set = selection_set.rebase_inner( - &rebased_base_type, - named_fragments, - schema, - on_non_rebaseable_selection, - )?; + let rebased_selection_set = + selection_set.rebase_inner(&rebased_base_type, named_fragments, schema)?; if rebased_selection_set.selections.is_empty() { Err(RebaseError::EmptySelectionSet.into()) } else { @@ -433,7 +393,6 @@ impl FragmentSpreadSelection { parent_type: &CompositeTypeDefinitionPosition, named_fragments: &NamedFragments, schema: &ValidFederationSchema, - on_non_rebaseable_selection: OnNonRebaseableSelection, ) -> Result { // We preserve the parent type here, to make sure we don't lose context, but we actually don't // want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named @@ -481,12 +440,9 @@ impl FragmentSpreadSelection { // important because the very logic we're hitting here may need to happen inside the rebase on the // fragment selection, but that logic would not be triggered if we used the rebased `named_fragment` since // `rebase_on_same_schema` would then be 'true'. - let expanded_selection_set = self.selection_set.rebase_inner( - parent_type, - named_fragments, - schema, - on_non_rebaseable_selection, - )?; + let expanded_selection_set = + self.selection_set + .rebase_inner(parent_type, named_fragments, schema)?; // In theory, we could return the selection set directly, but making `SelectionSet.rebase_on` sometimes // return a `SelectionSet` complicate things quite a bit. So instead, we encapsulate the selection set // in an "empty" inline fragment. This make for non-really-optimal selection sets in the (relatively @@ -523,7 +479,7 @@ impl FragmentSpreadSelection { named_fragments: &NamedFragments, schema: &ValidFederationSchema, ) -> Result { - self.rebase_inner(parent_type, named_fragments, schema, Default::default()) + self.rebase_inner(parent_type, named_fragments, schema) } } @@ -619,7 +575,6 @@ impl InlineFragmentSelection { parent_type: &CompositeTypeDefinitionPosition, named_fragments: &NamedFragments, schema: &ValidFederationSchema, - on_non_rebaseable_selection: OnNonRebaseableSelection, ) -> Result { if &self.inline_fragment.schema == schema && self.inline_fragment.parent_type_position == *parent_type @@ -636,12 +591,9 @@ impl InlineFragmentSelection { // we are within the same schema - selection set does not have to be rebased Ok(InlineFragmentSelection::new(rebased_fragment, self.selection_set.clone()).into()) } else { - let rebased_selection_set = self.selection_set.rebase_inner( - &rebased_casted_type, - named_fragments, - schema, - on_non_rebaseable_selection, - )?; + let rebased_selection_set = + self.selection_set + .rebase_inner(&rebased_casted_type, named_fragments, schema)?; if rebased_selection_set.selections.is_empty() { // empty selection set Err(RebaseError::EmptySelectionSet.into()) @@ -710,24 +662,11 @@ impl SelectionSet { parent_type: &CompositeTypeDefinitionPosition, named_fragments: &NamedFragments, schema: &ValidFederationSchema, - on_non_rebaseable_selection: OnNonRebaseableSelection, ) -> Result { let rebased_results = self .selections .values() - .map(|selection| { - selection.rebase_inner( - parent_type, - named_fragments, - schema, - on_non_rebaseable_selection, - ) - }) - // Remove selections with rebase errors if requested - .filter(|result| { - matches!(on_non_rebaseable_selection, OnNonRebaseableSelection::Error) - || !result.as_ref().is_err_and(|err| err.is_rebase_error()) - }); + .map(|selection| selection.rebase_inner(parent_type, named_fragments, schema)); Ok(SelectionSet { schema: schema.clone(), @@ -747,7 +686,7 @@ impl SelectionSet { named_fragments: &NamedFragments, schema: &ValidFederationSchema, ) -> Result { - self.rebase_inner(parent_type, named_fragments, schema, Default::default()) + self.rebase_inner(parent_type, named_fragments, schema) } /// Returns true if the selection set would select cleanly from the given type in the given @@ -762,641 +701,3 @@ impl SelectionSet { .fallible_all(|selection| selection.can_add_to(parent_type, schema)) } } - -impl NamedFragments { - pub(crate) fn rebase_on( - &self, - schema: &ValidFederationSchema, - ) -> Result { - let mut rebased_fragments = NamedFragments::default(); - for fragment in self.fragments.values() { - if let Some(rebased_type) = schema - .get_type(fragment.type_condition_position.type_name().clone()) - .ok() - .and_then(|ty| CompositeTypeDefinitionPosition::try_from(ty).ok()) - { - if let Ok(mut rebased_selection) = fragment.selection_set.rebase_inner( - &rebased_type, - &rebased_fragments, - schema, - OnNonRebaseableSelection::Drop, - ) { - // Rebasing can leave some inefficiencies in some case (particularly when a spread has to be "expanded", see `FragmentSpreadSelection.rebaseOn`), - // so we do a top-level normalization to keep things clean. - rebased_selection = rebased_selection.flatten_unnecessary_fragments( - &rebased_type, - &rebased_fragments, - schema, - )?; - if NamedFragments::is_selection_set_worth_using(&rebased_selection) { - let fragment = Fragment { - schema: schema.clone(), - name: fragment.name.clone(), - type_condition_position: rebased_type.clone(), - directives: fragment.directives.clone(), - selection_set: rebased_selection, - }; - rebased_fragments.insert(fragment); - } - } - } - } - Ok(rebased_fragments) - } -} - -#[cfg(test)] -mod tests { - use apollo_compiler::collections::IndexSet; - use apollo_compiler::name; - - use crate::operation::normalize_operation; - use crate::operation::tests::parse_schema_and_operation; - use crate::operation::tests::parse_subgraph; - use crate::operation::NamedFragments; - use crate::schema::position::InterfaceTypeDefinitionPosition; - - #[test] - fn skips_unknown_fragment_fields() { - let operation_fragments = r#" -query TestQuery { - t { - ...FragOnT - } -} - -fragment FragOnT on T { - v0 - v1 - v2 - u1 { - v3 - v4 - v5 - } - u2 { - v4 - v5 - } -} - -type Query { - t: T -} - -type T { - v0: Int - v1: Int - v2: Int - u1: U - u2: U -} - -type U { - v3: Int - v4: Int - v5: Int -} -"#; - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &IndexSet::default(), - ) - .unwrap(); - - let subgraph_schema = r#"type Query { - _: Int -} - -type T { - v1: Int - u1: U -} - -type U { - v3: Int - v5: Int -}"#; - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - assert!(!rebased_fragments.is_empty()); - assert!(rebased_fragments.contains(&name!("FragOnT"))); - let rebased_fragment = rebased_fragments.fragments.get("FragOnT").unwrap(); - - insta::assert_snapshot!(rebased_fragment, @r###" - fragment FragOnT on T { - v1 - u1 { - v3 - v5 - } - } - "###); - } - } - - #[test] - fn skips_unknown_fragment_on_condition() { - let operation_fragments = r#" -query TestQuery { - t { - ...FragOnT - } - u { - ...FragOnU - } -} - -fragment FragOnT on T { - x - y -} - -fragment FragOnU on U { - x - y -} - -type Query { - t: T - u: U -} - -type T { - x: Int - y: Int -} - -type U { - x: Int - y: Int -} -"#; - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - assert_eq!(2, executable_document.fragments.len()); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &IndexSet::default(), - ) - .unwrap(); - - let subgraph_schema = r#"type Query { - t: T -} - -type T { - x: Int - y: Int -}"#; - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - assert!(!rebased_fragments.is_empty()); - assert!(rebased_fragments.contains(&name!("FragOnT"))); - assert!(!rebased_fragments.contains(&name!("FragOnU"))); - let rebased_fragment = rebased_fragments.fragments.get("FragOnT").unwrap(); - - let expected = r#"fragment FragOnT on T { - x - y -}"#; - let actual = rebased_fragment.to_string(); - assert_eq!(actual, expected); - } - } - - #[test] - fn skips_unknown_type_within_fragment() { - let operation_fragments = r#" -query TestQuery { - i { - ...FragOnI - } -} - -fragment FragOnI on I { - id - otherId - ... on T1 { - x - } - ... on T2 { - y - } -} - -type Query { - i: I -} - -interface I { - id: ID! - otherId: ID! -} - -type T1 implements I { - id: ID! - otherId: ID! - x: Int -} - -type T2 implements I { - id: ID! - otherId: ID! - y: Int -} -"#; - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &IndexSet::default(), - ) - .unwrap(); - - let subgraph_schema = r#"type Query { - i: I -} - -interface I { - id: ID! -} - -type T2 implements I { - id: ID! - y: Int -} -"#; - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - assert!(!rebased_fragments.is_empty()); - assert!(rebased_fragments.contains(&name!("FragOnI"))); - let rebased_fragment = rebased_fragments.fragments.get("FragOnI").unwrap(); - - let expected = r#"fragment FragOnI on I { - id - ... on T2 { - y - } -}"#; - let actual = rebased_fragment.to_string(); - assert_eq!(actual, expected); - } - } - - #[test] - fn skips_typename_on_possible_interface_objects_within_fragment() { - let operation_fragments = r#" -query TestQuery { - i { - ...FragOnI - } -} - -fragment FragOnI on I { - __typename - id - x -} - -type Query { - i: I -} - -interface I { - id: ID! - x: String! -} - -type T implements I { - id: ID! - x: String! -} -"#; - - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let mut interface_objects: IndexSet = - IndexSet::default(); - interface_objects.insert(InterfaceTypeDefinitionPosition { - type_name: name!("I"), - }); - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &interface_objects, - ) - .unwrap(); - - let subgraph_schema = r#"extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.5", import: [{ name: "@interfaceObject" }, { name: "@key" }]) - -directive @link(url: String, as: String, import: [link__Import]) repeatable on SCHEMA - -directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE - -directive @interfaceObject on OBJECT - -type Query { - i: I -} - -type I @interfaceObject @key(fields: "id") { - id: ID! - x: String! -} - -scalar link__Import - -scalar federation__FieldSet -"#; - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - assert!(!rebased_fragments.is_empty()); - assert!(rebased_fragments.contains(&name!("FragOnI"))); - let rebased_fragment = rebased_fragments.fragments.get("FragOnI").unwrap(); - - let expected = r#"fragment FragOnI on I { - id - x -}"#; - let actual = rebased_fragment.to_string(); - assert_eq!(actual, expected); - } - } - - #[test] - fn skips_fragments_with_trivial_selections() { - let operation_fragments = r#" -query TestQuery { - t { - ...F1 - ...F2 - ...F3 - } -} - -fragment F1 on T { - a - b -} - -fragment F2 on T { - __typename - a - b -} - -fragment F3 on T { - __typename - a - b - c - d -} - -type Query { - t: T -} - -type T { - a: Int - b: Int - c: Int - d: Int -} -"#; - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &IndexSet::default(), - ) - .unwrap(); - - let subgraph_schema = r#"type Query { - t: T -} - -type T { - c: Int - d: Int -} -"#; - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - // F1 reduces to nothing, and F2 reduces to just __typename so we shouldn't keep them. - assert_eq!(1, rebased_fragments.len()); - assert!(rebased_fragments.contains(&name!("F3"))); - let rebased_fragment = rebased_fragments.fragments.get("F3").unwrap(); - - let expected = r#"fragment F3 on T { - __typename - c - d -}"#; - let actual = rebased_fragment.to_string(); - assert_eq!(actual, expected); - } - } - - #[test] - fn handles_skipped_fragments_within_fragments() { - let operation_fragments = r#" -query TestQuery { - ...TheQuery -} - -fragment TheQuery on Query { - t { - x - ... GetU - } -} - -fragment GetU on T { - u { - y - z - } -} - -type Query { - t: T -} - -type T { - x: Int - u: U -} - -type U { - y: Int - z: Int -} -"#; - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &IndexSet::default(), - ) - .unwrap(); - - let subgraph_schema = r#"type Query { - t: T -} - -type T { - x: Int -}"#; - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - // F1 reduces to nothing, and F2 reduces to just __typename so we shouldn't keep them. - assert_eq!(1, rebased_fragments.len()); - assert!(rebased_fragments.contains(&name!("TheQuery"))); - let rebased_fragment = rebased_fragments.fragments.get("TheQuery").unwrap(); - - let expected = r#"fragment TheQuery on Query { - t { - x - } -}"#; - let actual = rebased_fragment.to_string(); - assert_eq!(actual, expected); - } - } - - #[test] - fn handles_subtypes_within_subgraphs() { - let operation_fragments = r#" -query TestQuery { - ...TQuery -} - -fragment TQuery on Query { - t { - x - y - ... on T { - z - } - } -} - -type Query { - t: I -} - -interface I { - x: Int - y: Int -} - -type T implements I { - x: Int - y: Int - z: Int -} -"#; - let (schema, mut executable_document) = parse_schema_and_operation(operation_fragments); - assert!( - !executable_document.fragments.is_empty(), - "operation should have some fragments" - ); - - if let Some(operation) = executable_document.operations.named.get_mut("TestQuery") { - let normalized_operation = normalize_operation( - operation, - NamedFragments::new(&executable_document.fragments, &schema), - &schema, - &IndexSet::default(), - ) - .unwrap(); - - let subgraph_schema = r#"type Query { - t: T -} - -type T { - x: Int - y: Int - z: Int -} -"#; - - let subgraph = parse_subgraph("A", subgraph_schema); - let rebased_fragments = normalized_operation.named_fragments.rebase_on(&subgraph); - assert!(rebased_fragments.is_ok()); - let rebased_fragments = rebased_fragments.unwrap(); - // F1 reduces to nothing, and F2 reduces to just __typename so we shouldn't keep them. - assert_eq!(1, rebased_fragments.len()); - assert!(rebased_fragments.contains(&name!("TQuery"))); - let rebased_fragment = rebased_fragments.fragments.get("TQuery").unwrap(); - - let expected = r#"fragment TQuery on Query { - t { - x - y - z - } -}"#; - let actual = rebased_fragment.to_string(); - assert_eq!(actual, expected); - } - } -} diff --git a/apollo-federation/src/operation/selection_map.rs b/apollo-federation/src/operation/selection_map.rs index b8fc736895..477e2548ef 100644 --- a/apollo-federation/src/operation/selection_map.rs +++ b/apollo-federation/src/operation/selection_map.rs @@ -248,11 +248,6 @@ impl SelectionMap { self.selections.is_empty() } - /// Returns the first selection in the map, or None if the map is empty. - pub(crate) fn first(&self) -> Option<&Selection> { - self.selections.first() - } - /// Computes the hash of a selection key. fn hash(&self, key: SelectionKey<'_>) -> u64 { self.hash_builder.hash_one(key) diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index 6988b8e659..b0329cbb3d 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -16,10 +16,18 @@ use crate::query_graph::graph_path::OpPathElement; use crate::schema::position::InterfaceTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::ValidFederationSchema; -use crate::subgraph::Subgraph; mod defer; +macro_rules! assert_normalized { + ($schema_doc: expr, $query: expr, @$expected: literal) => {{ + let schema = parse_schema($schema_doc); + let without_fragments = parse_and_expand(&schema, $query).unwrap(); + insta::assert_snapshot!(without_fragments, @$expected); + without_fragments + }}; +} + pub(super) fn parse_schema_and_operation( schema_and_operation: &str, ) -> (ValidFederationSchema, ExecutableDocument) { @@ -30,12 +38,6 @@ pub(super) fn parse_schema_and_operation( (schema, executable_document) } -pub(super) fn parse_subgraph(name: &str, schema: &str) -> ValidFederationSchema { - let parsed_schema = - Subgraph::parse_and_expand(name, &format!("https://{name}"), schema).unwrap(); - ValidFederationSchema::new(parsed_schema.schema).unwrap() -} - pub(super) fn parse_schema(schema_doc: &str) -> ValidFederationSchema { let schema = Schema::parse_and_validate(schema_doc, "schema.graphql").unwrap(); ValidFederationSchema::new(schema).unwrap() @@ -65,17 +67,6 @@ pub(super) fn parse_and_expand( normalize_operation(operation, fragments, schema, &Default::default()) } -/// Parse and validate the query similarly to `parse_operation`, but does not construct the -/// `Operation` struct. -pub(super) fn validate_operation(schema: &ValidFederationSchema, query: &str) { - apollo_compiler::ExecutableDocument::parse_and_validate( - schema.schema(), - query, - "query.graphql", - ) - .unwrap(); -} - #[test] fn expands_named_fragments() { let operation_with_named_fragment = r#" @@ -1672,10 +1663,6 @@ fn directive_propagation() { ) .expect("directive applications to be valid"); insta::assert_snapshot!(query, @r###" - fragment DirectiveOnDef on T @fragDefOnly @fragAll { - a - } - { t2 { ... on T @fragInlineOnly @fragAll { @@ -1704,3 +1691,376 @@ fn directive_propagation() { .expect_err("directive @fragSpreadOnly to be rejected"); insta::assert_snapshot!(err, @"Unsupported custom directive @fragSpreadOnly on fragment spread. Due to query transformations during planning, the router requires directives on fragment spreads to support both the FRAGMENT_SPREAD and INLINE_FRAGMENT locations."); } + +#[test] +fn handles_fragment_matching_at_the_top_level_of_another_fragment() { + let schema_doc = r#" + type Query { + t: T + } + + type T { + a: String + u: U + } + + type U { + x: String + y: String + } + "#; + + let query = r#" + fragment Frag1 on T { + a + } + + fragment Frag2 on T { + u { + x + y + } + ...Frag1 + } + + fragment Frag3 on Query { + t { + ...Frag2 + } + } + + { + ...Frag3 + } + "#; + + assert_normalized!(schema_doc, query, @r###" + { + t { + u { + x + y + } + a + } + } + "###); +} + +#[test] +fn handles_fragments_used_in_context_where_they_get_trimmed() { + let schema_doc = r#" + type Query { + t1: T1 + } + + interface I { + x: Int + } + + type T1 implements I { + x: Int + y: Int + } + + type T2 implements I { + x: Int + z: Int + } + "#; + + let query = r#" + fragment FragOnI on I { + ... on T1 { + y + } + ... on T2 { + z + } + } + + { + t1 { + ...FragOnI + } + } + "#; + + assert_normalized!(schema_doc, query, @r###" + { + t1 { + y + } + } + "###); +} + +#[test] +fn handles_fragments_on_union_in_context_with_limited_intersection() { + let schema_doc = r#" + type Query { + t1: T1 + } + + union U = T1 | T2 + + type T1 { + x: Int + } + + type T2 { + y: Int + } + "#; + + let query = r#" + fragment OnU on U { + ... on T1 { + x + } + ... on T2 { + y + } + } + + { + t1 { + ...OnU + } + } + "#; + + assert_normalized!(schema_doc, query, @r###" + { + t1 { + x + } + } + "###); +} + +#[test] +fn off_by_1_error() { + let schema = r#" + type Query { + t: T + } + type T { + id: String! + a: A + v: V + } + type A { + id: String! + } + type V { + t: T! + } + "#; + + let query = r#" + { + t { + ...TFrag + v { + t { + id + a { + __typename + id + } + } + } + } + } + + fragment TFrag on T { + __typename + id + } + "#; + + assert_normalized!(schema, query,@r###" + { + t { + id + v { + t { + id + a { + id + } + } + } + } + } + "### + ); +} + +/// +/// applied directives +/// + +#[test] +fn reuse_fragments_with_same_directive_in_the_fragment_selection() { + let schema_doc = r#" + type Query { + t1: T + t2: T + t3: T + } + + type T { + a: Int + b: Int + c: Int + d: Int + } + "#; + + let query = r#" + fragment DirectiveInDef on T { + a @include(if: $cond1) + } + + query ($cond1: Boolean!, $cond2: Boolean!) { + t1 { + a + } + t2 { + ...DirectiveInDef + } + t3 { + a @include(if: $cond2) + } + } + "#; + + assert_normalized!(schema_doc, query, @r###" + query($cond1: Boolean!, $cond2: Boolean!) { + t1 { + a + } + t2 { + a @include(if: $cond1) + } + t3 { + a @include(if: $cond2) + } + } + "###); +} + +#[test] +fn reuse_fragments_with_directive_on_typename() { + let schema = r#" + type Query { + t1: T + t2: T + t3: T + } + + type T { + a: Int + b: Int + c: Int + d: Int + } + "#; + let query = r#" + query ($if: Boolean!) { + t1 { b a ...x } + t2 { ...x } + } + fragment x on T { + __typename @include(if: $if) + a + c + } + "#; + + assert_normalized!(schema, query, @r###" + query($if: Boolean!) { + t1 { + b + a + __typename @include(if: $if) + c + } + t2 { + __typename @include(if: $if) + a + c + } + } + "###); +} + +#[test] +fn reuse_fragments_with_non_intersecting_types() { + let schema = r#" + type Query { + t: T + s: S + s2: S + i: I + } + + interface I { + a: Int + b: Int + } + + type T implements I { + a: Int + b: Int + + c: Int + d: Int + } + type S implements I { + a: Int + b: Int + + f: Int + g: Int + } + "#; + let query = r#" + query ($if: Boolean!) { + t { ...x } + s { ...x } + i { ...x } + } + fragment x on I { + __typename + a + b + ... on T { c d @include(if: $if) } + } + "#; + + assert_normalized!(schema, query, @r###" + query($if: Boolean!) { + t { + a + b + c + d @include(if: $if) + } + s { + a + b + } + i { + a + b + ... on T { + c + d @include(if: $if) + } + } + } + "###); +} diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index b91699ecc4..70ddf4d0fa 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -2673,8 +2673,7 @@ impl FetchDependencyGraphNode { &operation_name, )? }; - let operation = - operation_compression.compress(&self.subgraph_name, subgraph_schema, operation)?; + let operation = operation_compression.compress(operation)?; let operation_document = operation.try_into().map_err(|err| match err { FederationError::SingleFederationError { inner: SingleFederationError::InvalidGraphQL { diagnostics }, diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index ed2a4b2589..0c94adde1d 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -53,16 +53,6 @@ use crate::Supergraph; #[derive(Debug, Clone, Hash, Serialize)] pub struct QueryPlannerConfig { - /// Whether the query planner should try to reuse the named fragments of the planned query in - /// subgraph fetches. - /// - /// Reusing fragments requires complicated validations, so it can take a long time on large - /// queries with many fragments. This option may be removed in the future in favour of - /// [`generate_query_fragments`][QueryPlannerConfig::generate_query_fragments]. - /// - /// Defaults to false. - pub reuse_query_fragments: bool, - /// If enabled, the query planner will extract inline fragments into fragment /// definitions before sending queries to subgraphs. This can significantly /// reduce the size of the query sent to subgraphs. @@ -104,7 +94,6 @@ pub struct QueryPlannerConfig { impl Default for QueryPlannerConfig { fn default() -> Self { Self { - reuse_query_fragments: false, generate_query_fragments: false, subgraph_graphql_validation: false, incremental_delivery: Default::default(), @@ -451,16 +440,6 @@ impl QueryPlanner { let operation_compression = if self.config.generate_query_fragments { SubgraphOperationCompression::GenerateFragments - } else if self.config.reuse_query_fragments { - // For all subgraph fetches we query `__typename` on every abstract types (see - // `FetchDependencyGraphNode::to_plan_node`) so if we want to have a chance to reuse - // fragments, we should make sure those fragments also query `__typename` for every - // abstract type. - SubgraphOperationCompression::ReuseFragments(RebasedFragments::new( - normalized_operation - .named_fragments - .add_typename_field_for_abstract_types_in_named_fragments()?, - )) } else { SubgraphOperationCompression::Disabled }; @@ -844,57 +823,15 @@ fn generate_condition_nodes<'a>( } } -/// Tracks fragments from the original operation, along with versions rebased on other subgraphs. -pub(crate) struct RebasedFragments { - original_fragments: NamedFragments, - /// Map key: subgraph name - rebased_fragments: IndexMap, NamedFragments>, -} - -impl RebasedFragments { - fn new(fragments: NamedFragments) -> Self { - Self { - original_fragments: fragments, - rebased_fragments: Default::default(), - } - } - - fn for_subgraph( - &mut self, - subgraph_name: impl Into>, - subgraph_schema: &ValidFederationSchema, - ) -> &NamedFragments { - self.rebased_fragments - .entry(subgraph_name.into()) - .or_insert_with(|| { - self.original_fragments - .rebase_on(subgraph_schema) - .unwrap_or_default() - }) - } -} - pub(crate) enum SubgraphOperationCompression { - ReuseFragments(RebasedFragments), GenerateFragments, Disabled, } impl SubgraphOperationCompression { /// Compress a subgraph operation. - pub(crate) fn compress( - &mut self, - subgraph_name: &Arc, - subgraph_schema: &ValidFederationSchema, - operation: Operation, - ) -> Result { + pub(crate) fn compress(&mut self, operation: Operation) -> Result { match self { - Self::ReuseFragments(fragments) => { - let rebased = fragments.for_subgraph(Arc::clone(subgraph_name), subgraph_schema); - let mut operation = operation; - operation.reuse_fragments(rebased)?; - Ok(operation) - } Self::GenerateFragments => { let mut operation = operation; operation.generate_fragments()?; @@ -1324,7 +1261,7 @@ type User } #[test] - fn test_optimize_basic() { + fn test_optimize_no_fragments_generated() { let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap(); let api_schema = supergraph.to_api_schema(Default::default()).unwrap(); let document = ExecutableDocument::parse_and_validate( @@ -1350,7 +1287,7 @@ type User .unwrap(); let config = QueryPlannerConfig { - reuse_query_fragments: true, + generate_query_fragments: true, ..Default::default() }; let planner = QueryPlanner::new(&supergraph, config).unwrap(); @@ -1362,149 +1299,14 @@ type User Fetch(service: "accounts") { { userById(id: 1) { - ...userFields id - } - another_user: userById(id: 2) { - ...userFields - } - } - - fragment userFields on User { - name - email - } - }, - } - "###); - } - - #[test] - fn test_optimize_inline_fragment() { - let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap(); - let api_schema = supergraph.to_api_schema(Default::default()).unwrap(); - let document = ExecutableDocument::parse_and_validate( - api_schema.schema(), - r#" - { - userById(id: 1) { - id - ...userFields - }, - partial_optimize: userById(id: 2) { - ... on User { - id - name - email - } - }, - full_optimize: userById(id: 3) { - ... on User { - name - email - } - } - } - fragment userFields on User { name email - } - "#, - "operation.graphql", - ) - .unwrap(); - - let config = QueryPlannerConfig { - reuse_query_fragments: true, - ..Default::default() - }; - let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner - .build_query_plan(&document, None, Default::default()) - .unwrap(); - insta::assert_snapshot!(plan, @r###" - QueryPlan { - Fetch(service: "accounts") { - { - userById(id: 1) { - ...userFields - id } - partial_optimize: userById(id: 2) { - ...userFields - id - } - full_optimize: userById(id: 3) { - ...userFields - } - } - - fragment userFields on User { - name - email - } - }, - } - "###); - } - - #[test] - fn test_optimize_fragment_definition() { - let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap(); - let api_schema = supergraph.to_api_schema(Default::default()).unwrap(); - let document = ExecutableDocument::parse_and_validate( - api_schema.schema(), - r#" - { - userById(id: 1) { - ...F1 - ...F2 - }, - case2: userById(id: 2) { - id - name - email - }, - } - fragment F1 on User { - name - email - } - fragment F2 on User { - id + another_user: userById(id: 2) { name email - } - "#, - "operation.graphql", - ) - .unwrap(); - - let config = QueryPlannerConfig { - reuse_query_fragments: true, - ..Default::default() - }; - let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner - .build_query_plan(&document, None, Default::default()) - .unwrap(); - // Make sure `fragment F2` contains `...F1`. - insta::assert_snapshot!(plan, @r###" - QueryPlan { - Fetch(service: "accounts") { - { - userById(id: 1) { - ...F2 } - case2: userById(id: 2) { - ...F2 - } - } - - fragment F2 on User { - name - email - id } }, } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index c0d85c7b62..4a063565e1 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -34,6 +34,7 @@ fn some_name() { mod context; mod debug_max_evaluated_plans_configuration; mod defer; +mod entities; mod fetch_operation_names; mod field_merging_with_skip_and_include; mod fragment_autogeneration; @@ -44,8 +45,7 @@ mod interface_type_explosion; mod introspection_typename_handling; mod merged_abstract_types_handling; mod mutations; -mod named_fragments; -mod named_fragments_preservation; +mod named_fragments_expansion; mod overrides; mod provides; mod requires; diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/entities.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/entities.rs new file mode 100644 index 0000000000..53f50aad38 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/entities.rs @@ -0,0 +1,142 @@ +// TODO this test shows inefficient QP where we make multiple parallel +// fetches of the same entity from the same subgraph but for different paths +#[test] +fn inefficient_entity_fetches_to_same_subgraph() { + let planner = planner!( + Subgraph1: r#" + type V @shareable { + x: Int + } + + interface I { + v: V + } + + type Outer implements I @key(fields: "id") { + id: ID! + v: V + } + "#, + Subgraph2: r#" + type Query { + outer1: Outer + outer2: Outer + } + + type V @shareable { + x: Int + } + + interface I { + v: V + w: Int + } + + type Inner implements I { + v: V + w: Int + } + + type Outer @key(fields: "id") { + id: ID! + inner: Inner + w: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } + + fragment IFrag on I { + v { + x + } + w + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + id + w + inner { + v { + x + } + w + } + } + outer2 { + __typename + id + w + inner { + v { + x + } + w + } + } + } + }, + Parallel { + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + }, + }, + } + "# + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/fragment_autogeneration.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/fragment_autogeneration.rs index 03b43c6245..e80f57953f 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/fragment_autogeneration.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/fragment_autogeneration.rs @@ -1,5 +1,12 @@ use apollo_federation::query_plan::query_planner::QueryPlannerConfig; +fn generate_fragments_config() -> QueryPlannerConfig { + QueryPlannerConfig { + generate_query_fragments: true, + ..Default::default() + } +} + const SUBGRAPH: &str = r#" directive @custom on INLINE_FRAGMENT | FRAGMENT_SPREAD @@ -25,7 +32,7 @@ const SUBGRAPH: &str = r#" #[test] fn it_respects_generate_query_fragments_option() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: SUBGRAPH, ); assert_plan!( @@ -73,7 +80,7 @@ fn it_respects_generate_query_fragments_option() { #[test] fn it_handles_nested_fragment_generation() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: SUBGRAPH, ); assert_plan!( @@ -131,10 +138,250 @@ fn it_handles_nested_fragment_generation() { ); } +// TODO this test shows a clearly worse plan than reused fragments when fragments +// target concrete types +#[test] +fn it_handles_nested_fragment_generation_from_operation_with_fragments() { + let planner = planner!( + config = generate_fragments_config(), + Subgraph1: r#" + type Query { + a: Anything + } + + union Anything = A1 | A2 | A3 + + interface Foo { + foo: String + child: Foo + child2: Foo + } + + type A1 implements Foo { + foo: String + child: Foo + child2: Foo + } + + type A2 implements Foo { + foo: String + child: Foo + child2: Foo + } + + type A3 implements Foo { + foo: String + child: Foo + child2: Foo + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + a { + ... on A1 { + ...FooSelect + } + ... on A2 { + ...FooSelect + } + ... on A3 { + ...FooSelect + } + } + } + + fragment FooSelect on Foo { + __typename + foo + child { + ...FooChildSelect + } + child2 { + ...FooChildSelect + } + } + + fragment FooChildSelect on Foo { + __typename + foo + child { + child { + child { + foo + } + } + } + } + "#, + + // This is a test case that shows worse result + // QueryPlan { + // Fetch(service: "Subgraph1") { + // { + // a { + // __typename + // ... on A1 { + // ...FooSelect + // } + // ... on A2 { + // ...FooSelect + // } + // ... on A3 { + // ...FooSelect + // } + // } + // } + // + // fragment FooChildSelect on Foo { + // __typename + // foo + // child { + // __typename + // child { + // __typename + // child { + // __typename + // foo + // } + // } + // } + // } + // + // fragment FooSelect on Foo { + // __typename + // foo + // child { + // ...FooChildSelect + // } + // child2 { + // ...FooChildSelect + // } + // } + // }, + // } + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + a { + __typename + ..._generated_onA14_0 + ..._generated_onA24_0 + ..._generated_onA34_0 + } + } + + fragment _generated_onA14_0 on A1 { + __typename + foo + child { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + child2 { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + } + + fragment _generated_onA24_0 on A2 { + __typename + foo + child { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + child2 { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + } + + fragment _generated_onA34_0 on A3 { + __typename + foo + child { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + child2 { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + } + }, + } + "### + ); +} + #[test] fn it_handles_fragments_with_one_non_leaf_field() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: SUBGRAPH, ); @@ -183,7 +430,7 @@ fn it_handles_fragments_with_one_non_leaf_field() { #[test] fn it_migrates_skip_include() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: SUBGRAPH, ); assert_plan!( @@ -250,10 +497,11 @@ fn it_migrates_skip_include() { "### ); } + #[test] fn it_identifies_and_reuses_equivalent_fragments_that_arent_identical() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: SUBGRAPH, ); assert_plan!( @@ -301,7 +549,7 @@ fn it_identifies_and_reuses_equivalent_fragments_that_arent_identical() { #[test] fn fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: SUBGRAPH, ); assert_plan!( @@ -354,7 +602,7 @@ fn fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment #[test] fn same_as_js_router798() { let planner = planner!( - config = QueryPlannerConfig { generate_query_fragments: true, reuse_query_fragments: false, ..Default::default() }, + config = generate_fragments_config(), Subgraph1: r#" interface Interface { a: Int } type Y implements Interface { a: Int b: Int } @@ -398,6 +646,7 @@ fn same_as_js_router798() { #[test] fn works_with_key_chains() { let planner = planner!( + config = generate_fragments_config(), Subgraph1: r#" type Query { t: T @@ -471,10 +720,12 @@ fn works_with_key_chains() { } } => { - ... on T { - x - y - } + ..._generated_onT2_0 + } + + fragment _generated_onT2_0 on T { + x + y } }, }, @@ -483,3 +734,192 @@ fn works_with_key_chains() { "### ); } + +#[test] +fn another_mix_of_fragments_indirection_and_unions() { + // This tests that the issue reported on https://github.com/apollographql/router/issues/3172 is resolved. + let planner = planner!( + config = generate_fragments_config(), + Subgraph1: r#" + type Query { + owner: Owner! + } + + interface OItf { + id: ID! + v0: String! + } + + type Owner implements OItf { + id: ID! + v0: String! + u: [U] + } + + union U = T1 | T2 + + interface I { + id1: ID! + id2: ID! + } + + type T1 implements I { + id1: ID! + id2: ID! + owner: Owner! + } + + type T2 implements I { + id1: ID! + id2: ID! + } + "#, + ); + assert_plan!( + &planner, + r#" + { + owner { + u { + ... on I { + id1 + id2 + } + ...Fragment1 + ...Fragment2 + } + } + } + + fragment Fragment1 on T1 { + owner { + ... on Owner { + ...Fragment3 + } + } + } + + fragment Fragment2 on T2 { + ...Fragment4 + id1 + } + + fragment Fragment3 on OItf { + v0 + } + + fragment Fragment4 on I { + id1 + id2 + __typename + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + owner { + u { + __typename + ..._generated_onI3_0 + ..._generated_onT11_0 + ..._generated_onT23_0 + } + } + } + + fragment _generated_onI3_0 on I { + __typename + id1 + id2 + } + + fragment _generated_onT11_0 on T1 { + owner { + v0 + } + } + + fragment _generated_onT23_0 on T2 { + __typename + id1 + id2 + } + }, + } + "### + ); + + assert_plan!( + &planner, + r#" + { + owner { + u { + ... on I { + id1 + id2 + } + ...Fragment1 + ...Fragment2 + } + } + } + + fragment Fragment1 on T1 { + owner { + ... on Owner { + ...Fragment3 + } + } + } + + fragment Fragment2 on T2 { + ...Fragment4 + id1 + } + + fragment Fragment3 on OItf { + v0 + } + + fragment Fragment4 on I { + id1 + id2 + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + owner { + u { + __typename + ..._generated_onI3_0 + ..._generated_onT11_0 + ..._generated_onT22_0 + } + } + } + + fragment _generated_onI3_0 on I { + __typename + id1 + id2 + } + + fragment _generated_onT11_0 on T1 { + owner { + v0 + } + } + + fragment _generated_onT22_0 on T2 { + id1 + id2 + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs deleted file mode 100644 index 959069588c..0000000000 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs +++ /dev/null @@ -1,563 +0,0 @@ -use apollo_federation::query_plan::query_planner::QueryPlannerConfig; - -fn reuse_fragments_config() -> QueryPlannerConfig { - QueryPlannerConfig { - reuse_query_fragments: true, - ..Default::default() - } -} - -#[test] -fn handles_mix_of_fragments_indirection_and_unions() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - parent: Parent - } - - union CatOrPerson = Cat | Parent | Child - - type Parent { - childs: [Child] - } - - type Child { - id: ID! - } - - type Cat { - name: String - } - "#, - ); - assert_plan!( - &planner, - r#" - query { - parent { - ...F_indirection1_parent - } - } - - fragment F_indirection1_parent on Parent { - ...F_indirection2_catOrPerson - } - - fragment F_indirection2_catOrPerson on CatOrPerson { - ...F_catOrPerson - } - - fragment F_catOrPerson on CatOrPerson { - __typename - ... on Cat { - name - } - ... on Parent { - childs { - __typename - id - } - } - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - parent { - __typename - childs { - __typename - id - } - } - } - }, - } - "### - ); -} - -#[test] -fn another_mix_of_fragments_indirection_and_unions() { - // This tests that the issue reported on https://github.com/apollographql/router/issues/3172 is resolved. - - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - owner: Owner! - } - - interface OItf { - id: ID! - v0: String! - } - - type Owner implements OItf { - id: ID! - v0: String! - u: [U] - } - - union U = T1 | T2 - - interface I { - id1: ID! - id2: ID! - } - - type T1 implements I { - id1: ID! - id2: ID! - owner: Owner! - } - - type T2 implements I { - id1: ID! - id2: ID! - } - "#, - ); - assert_plan!( - &planner, - r#" - { - owner { - u { - ... on I { - id1 - id2 - } - ...Fragment1 - ...Fragment2 - } - } - } - - fragment Fragment1 on T1 { - owner { - ... on Owner { - ...Fragment3 - } - } - } - - fragment Fragment2 on T2 { - ...Fragment4 - id1 - } - - fragment Fragment3 on OItf { - v0 - } - - fragment Fragment4 on I { - id1 - id2 - __typename - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - owner { - u { - __typename - ...Fragment4 - ... on T1 { - owner { - v0 - } - } - ... on T2 { - ...Fragment4 - } - } - } - } - - fragment Fragment4 on I { - __typename - id1 - id2 - } - }, - } - "### - ); - - assert_plan!( - &planner, - r#" - { - owner { - u { - ... on I { - id1 - id2 - } - ...Fragment1 - ...Fragment2 - } - } - } - - fragment Fragment1 on T1 { - owner { - ... on Owner { - ...Fragment3 - } - } - } - - fragment Fragment2 on T2 { - ...Fragment4 - id1 - } - - fragment Fragment3 on OItf { - v0 - } - - fragment Fragment4 on I { - id1 - id2 - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - owner { - u { - __typename - ... on I { - __typename - ...Fragment4 - } - ... on T1 { - owner { - v0 - } - } - ... on T2 { - ...Fragment4 - } - } - } - } - - fragment Fragment4 on I { - id1 - id2 - } - }, - } - "### - ); -} - -#[test] -fn handles_fragments_with_interface_field_subtyping() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t1: T1! - } - - interface I { - id: ID! - other: I! - } - - type T1 implements I { - id: ID! - other: T1! - } - - type T2 implements I { - id: ID! - other: T2! - } - "#, - ); - assert_plan!( - &planner, - r#" - { - t1 { - ...Fragment1 - } - } - - fragment Fragment1 on I { - other { - ... on T1 { - id - } - ... on T2 { - id - } - } - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t1 { - other { - __typename - id - } - } - } - }, - } - "### - ); -} - -#[test] -fn can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t1: T - t2: T - } - - type T @key(fields: "id") { - id: ID! - v0: Int - v1: Int - v2: Int - } - "#, - Subgraph2: r#" - type T @key(fields: "id") { - id: ID! - v3: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - { - t1 { - ...allTFields - } - t2 { - ...allTFields - } - } - - fragment allTFields on T { - v0 - v1 - v2 - v3 - } - "#, - @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { - { - t1 { - __typename - ...allTFields - id - } - t2 { - __typename - ...allTFields - id - } - } - - fragment allTFields on T { - v0 - v1 - v2 - } - }, - Parallel { - Flatten(path: "t2") { - Fetch(service: "Subgraph2") { - { - ... on T { - __typename - id - } - } => - { - ... on T { - v3 - } - } - }, - }, - Flatten(path: "t1") { - Fetch(service: "Subgraph2") { - { - ... on T { - __typename - id - } - } => - { - ... on T { - v3 - } - } - }, - }, - }, - }, - } - "### - ); -} - -#[test] -fn can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t: T - } - - type T @key(fields: "id") { - id: ID! - } - "#, - Subgraph2: r#" - type T @key(fields: "id") { - id: ID! - u1: U - u2: U - } - - type U @key(fields: "id") { - id: ID! - v0: Int - v1: Int - } - "#, - Subgraph3: r#" - type U @key(fields: "id") { - id: ID! - v2: Int - v3: Int - } - "#, - ); - - assert_plan!( - &planner, - r#" - { - t { - u1 { - ...allUFields - } - u2 { - ...allUFields - } - } - } - - fragment allUFields on U { - v0 - v1 - v2 - v3 - } - "#, - @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { - { - t { - __typename - id - } - } - }, - Flatten(path: "t") { - Fetch(service: "Subgraph2") { - { - ... on T { - __typename - id - } - } => - { - ... on T { - u1 { - __typename - ...allUFields - id - } - u2 { - __typename - ...allUFields - id - } - } - } - - fragment allUFields on U { - v0 - v1 - } - }, - }, - Parallel { - Flatten(path: "t.u2") { - Fetch(service: "Subgraph3") { - { - ... on U { - __typename - id - } - } => - { - ... on U { - v2 - v3 - } - } - }, - }, - Flatten(path: "t.u1") { - Fetch(service: "Subgraph3") { - { - ... on U { - __typename - id - } - } => - { - ... on U { - v2 - v3 - } - } - }, - }, - }, - }, - } - "### - ); -} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_expansion.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_expansion.rs new file mode 100644 index 0000000000..5b68d3e059 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_expansion.rs @@ -0,0 +1,369 @@ +#[test] +fn handles_mix_of_fragments_indirection_and_unions() { + let planner = planner!( + Subgraph1: r#" + type Query { + parent: Parent + } + + union CatOrPerson = Cat | Parent | Child + + type Parent { + childs: [Child] + } + + type Child { + id: ID! + } + + type Cat { + name: String + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + parent { + ...F_indirection1_parent + } + } + + fragment F_indirection1_parent on Parent { + ...F_indirection2_catOrPerson + } + + fragment F_indirection2_catOrPerson on CatOrPerson { + ...F_catOrPerson + } + + fragment F_catOrPerson on CatOrPerson { + __typename + ... on Cat { + name + } + ... on Parent { + childs { + __typename + id + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + parent { + __typename + childs { + __typename + id + } + } + } + }, + } + "### + ); +} + +#[test] +fn handles_fragments_with_interface_field_subtyping() { + let planner = planner!( + Subgraph1: r#" + type Query { + t1: T1! + } + + interface I { + id: ID! + other: I! + } + + type T1 implements I { + id: ID! + other: T1! + } + + type T2 implements I { + id: ID! + other: T2! + } + "#, + ); + + assert_plan!( + &planner, + r#" + { + t1 { + ...Fragment1 + } + } + + fragment Fragment1 on I { + other { + ... on T1 { + id + } + ... on T2 { + id + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t1 { + other { + id + } + } + } + }, + } + "### + ); +} + +#[test] +fn it_preserves_directives() { + // (because used only once) + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + b: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query test($if: Boolean!) { + t { + id + ...OnT @include(if: $if) + } + } + + fragment OnT on T { + a + b + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + id + ... on T @include(if: $if) { + a + b + } + } + } + }, + } + "### + ); +} + +#[test] +fn it_preserves_directives_when_fragment_is_reused() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + b: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query test($test1: Boolean!, $test2: Boolean!) { + t { + id + ...OnT @include(if: $test1) + ...OnT @include(if: $test2) + } + } + + fragment OnT on T { + a + b + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + id + ... on T @include(if: $test1) { + a + b + } + ... on T @include(if: $test2) { + a + b + } + } + } + }, + } + "### + ); +} + +#[test] +fn it_preserves_directives_on_collapsed_fragments() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T { + id: ID! + t1: V + t2: V + } + + type V { + v1: Int + v2: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query($test: Boolean!) { + t { + ...OnT + } + } + + fragment OnT on T { + id + ...OnTInner @include(if: $test) + } + + fragment OnTInner on T { + t1 { + ...OnV + } + t2 { + ...OnV + } + } + + fragment OnV on V { + v1 + v2 + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + id + ... on T @include(if: $test) { + t1 { + v1 + v2 + } + t2 { + v1 + v2 + } + } + } + } + }, + } + "### + ); +} + +#[test] +fn it_expands_nested_fragments() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: V + b: V + } + + type V { + v1: Int + v2: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + ...OnT + } + } + + fragment OnT on T { + a { + ...OnV + } + b { + ...OnV + } + } + + fragment OnV on V { + v1 + v2 + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + a { + v1 + v2 + } + b { + v1 + v2 + } + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs deleted file mode 100644 index da90c3edb8..0000000000 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs +++ /dev/null @@ -1,1384 +0,0 @@ -use apollo_federation::query_plan::query_planner::QueryPlannerConfig; - -fn reuse_fragments_config() -> QueryPlannerConfig { - QueryPlannerConfig { - reuse_query_fragments: true, - ..Default::default() - } -} - -#[test] -fn it_works_with_nested_fragments_1() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - a: Anything - } - - union Anything = A1 | A2 | A3 - - interface Foo { - foo: String - child: Foo - child2: Foo - } - - type A1 implements Foo { - foo: String - child: Foo - child2: Foo - } - - type A2 implements Foo { - foo: String - child: Foo - child2: Foo - } - - type A3 implements Foo { - foo: String - child: Foo - child2: Foo - } - "#, - ); - assert_plan!( - &planner, - r#" - query { - a { - ... on A1 { - ...FooSelect - } - ... on A2 { - ...FooSelect - } - ... on A3 { - ...FooSelect - } - } - } - - fragment FooSelect on Foo { - __typename - foo - child { - ...FooChildSelect - } - child2 { - ...FooChildSelect - } - } - - fragment FooChildSelect on Foo { - __typename - foo - child { - child { - child { - foo - } - } - } - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - a { - __typename - ... on A1 { - ...FooSelect - } - ... on A2 { - ...FooSelect - } - ... on A3 { - ...FooSelect - } - } - } - - fragment FooChildSelect on Foo { - __typename - foo - child { - __typename - child { - __typename - child { - __typename - foo - } - } - } - } - - fragment FooSelect on Foo { - __typename - foo - child { - ...FooChildSelect - } - child2 { - ...FooChildSelect - } - } - }, - } - "### - ); -} - -#[test] -fn it_avoid_fragments_usable_only_once() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t: T - } - - type T @key(fields: "id") { - id: ID! - v1: V - } - - type V @shareable { - a: Int - b: Int - c: Int - } - "#, - Subgraph2: r#" - type T @key(fields: "id") { - id: ID! - v2: V - v3: V - } - - type V @shareable { - a: Int - b: Int - c: Int - } - "#, - ); - - // We use a fragment which does save some on the original query, but as each - // field gets to a different subgraph, the fragment would only be used one - // on each sub-fetch and we make sure the fragment is not used in that case. - assert_plan!( - &planner, - r#" - query { - t { - v1 { - ...OnV - } - v2 { - ...OnV - } - } - } - - fragment OnV on V { - a - b - c - } - "#, - @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { - { - t { - __typename - id - v1 { - a - b - c - } - } - } - }, - Flatten(path: "t") { - Fetch(service: "Subgraph2") { - { - ... on T { - __typename - id - } - } => - { - ... on T { - v2 { - a - b - c - } - } - } - }, - }, - }, - } - "### - ); - - // But double-check that if we query 2 fields from the same subgraph, then - // the fragment gets used now. - assert_plan!( - &planner, - r#" - query { - t { - v2 { - ...OnV - } - v3 { - ...OnV - } - } - } - - fragment OnV on V { - a - b - c - } - "#, - @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { - { - t { - __typename - id - } - } - }, - Flatten(path: "t") { - Fetch(service: "Subgraph2") { - { - ... on T { - __typename - id - } - } => - { - ... on T { - v2 { - ...OnV - } - v3 { - ...OnV - } - } - } - - fragment OnV on V { - a - b - c - } - }, - }, - }, - } - "### - ); -} - -mod respects_query_planner_option_reuse_query_fragments { - use super::*; - - const SUBGRAPH1: &str = r#" - type Query { - t: T - } - - type T { - a1: A - a2: A - } - - type A { - x: Int - y: Int - } - "#; - const QUERY: &str = r#" - query { - t { - a1 { - ...Selection - } - a2 { - ...Selection - } - } - } - - fragment Selection on A { - x - y - } - "#; - - #[test] - fn respects_query_planner_option_reuse_query_fragments_true() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: SUBGRAPH1, - ); - let query = QUERY; - - assert_plan!( - &planner, - query, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t { - a1 { - ...Selection - } - a2 { - ...Selection - } - } - } - - fragment Selection on A { - x - y - } - }, - } - "### - ); - } - - #[test] - fn respects_query_planner_option_reuse_query_fragments_false() { - let reuse_query_fragments = false; - let planner = planner!( - config = QueryPlannerConfig {reuse_query_fragments, ..Default::default()}, - Subgraph1: SUBGRAPH1, - ); - let query = QUERY; - - assert_plan!( - &planner, - query, - @r#" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t { - a1 { - x - y - } - a2 { - x - y - } - } - } - }, - } - "# - ); - } -} - -#[test] -fn it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t: T - } - - type T @key(fields: "id") { - id: ID! - a: V - b: V - } - - type V { - v1: Int - v2: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - { - t { - ...OnT - } - } - - fragment OnT on T { - a { - ...OnV - } - b { - ...OnV - } - } - - fragment OnV on V { - v1 - v2 - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t { - a { - ...OnV - } - b { - ...OnV - } - } - } - - fragment OnV on V { - v1 - v2 - } - }, - } - "### - ); -} - -#[test] -fn it_preserves_directives_when_fragment_not_used() { - // (because used only once) - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t: T - } - - type T @key(fields: "id") { - id: ID! - a: Int - b: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - query test($if: Boolean!) { - t { - id - ...OnT @include(if: $if) - } - } - - fragment OnT on T { - a - b - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t { - id - ... on T @include(if: $if) { - a - b - } - } - } - }, - } - "### - ); -} - -#[test] -fn it_preserves_directives_when_fragment_is_reused() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t: T - } - - type T @key(fields: "id") { - id: ID! - a: Int - b: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - query test($test1: Boolean!, $test2: Boolean!) { - t { - id - ...OnT @include(if: $test1) - ...OnT @include(if: $test2) - } - } - - fragment OnT on T { - a - b - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t { - id - ...OnT @include(if: $test1) - ...OnT @include(if: $test2) - } - } - - fragment OnT on T { - a - b - } - }, - } - "### - ); -} - -#[test] -fn it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph() { - // Slightly artificial example for simplicity, but this highlight the problem. - // In that example, the only queried subgraph is the first one (there is in fact - // no way to ever reach the 2nd one), so the plan should mostly simply forward - // the query to the 1st subgraph, but a subtlety is that the named fragment used - // in the query is *not* valid for Subgraph1, because it queries `b` on `I`, but - // there is no `I.b` in Subgraph1. - // So including the named fragment in the fetch would be erroneous: the subgraph - // server would reject it when validating the query, and we must make sure it - // is not reused. - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - i1: I - i2: I - } - - interface I { - a: Int - } - - type T implements I { - a: Int - b: Int - } - "#, - Subgraph2: r#" - interface I { - a: Int - b: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - query { - i1 { - ... on T { - ...Frag - } - } - i2 { - ... on T { - ...Frag - } - } - } - - fragment Frag on I { - b - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - i1 { - __typename - ... on T { - b - } - } - i2 { - __typename - ... on T { - b - } - } - } - }, - } - "### - ); -} - -#[test] -fn it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs() { - // This test is designed such that type `Outer` implements the interface `I` in `Subgraph1` - // but not in `Subgraph2`, yet `I` exists in `Subgraph2` (but only `Inner` implements it - // there). Further, the operations we test have a fragment on I (`IFrag` below) that is - // used "in the context of `Outer`" (at the top-level of fragment `OuterFrag`). - // - // What this all means is that `IFrag` can be rebased in `Subgraph2` "as is" because `I` - // exists there with all its fields, but as we rebase `OuterFrag` on `Subgraph2`, we - // cannot use `...IFrag` inside it (at the top-level), because `I` and `Outer` do - // no intersect in `Subgraph2` and this would be an invalid selection. - // - // Previous versions of the code were not handling this case and were error out by - // creating the invalid selection (#2721), and this test ensures this is fixed. - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type V @shareable { - x: Int - } - - interface I { - v: V - } - - type Outer implements I @key(fields: "id") { - id: ID! - v: V - } - "#, - Subgraph2: r#" - type Query { - outer1: Outer - outer2: Outer - } - - type V @shareable { - x: Int - } - - interface I { - v: V - w: Int - } - - type Inner implements I { - v: V - w: Int - } - - type Outer @key(fields: "id") { - id: ID! - inner: Inner - w: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - query { - outer1 { - ...OuterFrag - } - outer2 { - ...OuterFrag - } - } - - fragment OuterFrag on Outer { - ...IFrag - inner { - ...IFrag - } - } - - fragment IFrag on I { - v { - x - } - } - "#, - @r#" - QueryPlan { - Sequence { - Fetch(service: "Subgraph2") { - { - outer1 { - __typename - ...OuterFrag - id - } - outer2 { - __typename - ...OuterFrag - id - } - } - - fragment OuterFrag on Outer { - inner { - v { - x - } - } - } - }, - Parallel { - Flatten(path: "outer2") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v { - x - } - } - } - }, - }, - Flatten(path: "outer1") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v { - x - } - } - } - }, - }, - }, - }, - } - "# - ); - - // We very slighly modify the operation to add an artificial indirection within `IFrag`. - // This does not really change the query, and should result in the same plan, but - // ensure the code handle correctly such indirection. - assert_plan!( - &planner, - r#" - query { - outer1 { - ...OuterFrag - } - outer2 { - ...OuterFrag - } - } - - fragment OuterFrag on Outer { - ...IFrag - inner { - ...IFrag - } - } - - fragment IFrag on I { - ...IFragDelegate - } - - fragment IFragDelegate on I { - v { - x - } - } - "#, - @r#" - QueryPlan { - Sequence { - Fetch(service: "Subgraph2") { - { - outer1 { - __typename - ...OuterFrag - id - } - outer2 { - __typename - ...OuterFrag - id - } - } - - fragment OuterFrag on Outer { - inner { - v { - x - } - } - } - }, - Parallel { - Flatten(path: "outer2") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v { - x - } - } - } - }, - }, - Flatten(path: "outer1") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v { - x - } - } - } - }, - }, - }, - }, - } - "# - ); - - // The previous cases tests the cases where nothing in the `...IFrag` spread at the - // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But - // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not - // have `v` in particular), it does contains field `w` that `I` also have, so we - // add that field to `IFrag` and make sure we still correctly query that field. - - assert_plan!( - &planner, - r#" - query { - outer1 { - ...OuterFrag - } - outer2 { - ...OuterFrag - } - } - - fragment OuterFrag on Outer { - ...IFrag - inner { - ...IFrag - } - } - - fragment IFrag on I { - v { - x - } - w - } - "#, - @r#" - QueryPlan { - Sequence { - Fetch(service: "Subgraph2") { - { - outer1 { - __typename - ...OuterFrag - id - } - outer2 { - __typename - ...OuterFrag - id - } - } - - fragment OuterFrag on Outer { - w - inner { - v { - x - } - w - } - } - }, - Parallel { - Flatten(path: "outer2") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v { - x - } - } - } - }, - }, - Flatten(path: "outer1") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v { - x - } - } - } - }, - }, - }, - }, - } - "# - ); -} - -#[test] -fn it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs() { - // This test is similar to the subtyping case (it tests the same problems), but test the case - // of unions instead of interfaces. - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type V @shareable { - x: Int - } - - union U = Outer - - type Outer @key(fields: "id") { - id: ID! - v: Int - } - "#, - Subgraph2: r#" - type Query { - outer1: Outer - outer2: Outer - } - - union U = Inner - - type Inner { - v: Int - w: Int - } - - type Outer @key(fields: "id") { - id: ID! - inner: Inner - w: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - query { - outer1 { - ...OuterFrag - } - outer2 { - ...OuterFrag - } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { - ...UFrag - } - } - - fragment UFrag on U { - ... on Outer { - v - } - ... on Inner { - v - } - } - "#, - @r#" - QueryPlan { - Sequence { - Fetch(service: "Subgraph2") { - { - outer1 { - __typename - ...OuterFrag - id - } - outer2 { - __typename - ...OuterFrag - id - } - } - - fragment OuterFrag on Outer { - inner { - v - } - } - }, - Parallel { - Flatten(path: "outer2") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v - } - } - }, - }, - Flatten(path: "outer1") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v - } - } - }, - }, - }, - }, - } - "# - ); - - // We very slighly modify the operation to add an artificial indirection within `IFrag`. - // This does not really change the query, and should result in the same plan, but - // ensure the code handle correctly such indirection. - assert_plan!( - &planner, - r#" - query { - outer1 { - ...OuterFrag - } - outer2 { - ...OuterFrag - } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { - ...UFrag - } - } - - fragment UFrag on U { - ...UFragDelegate - } - - fragment UFragDelegate on U { - ... on Outer { - v - } - ... on Inner { - v - } - } - "#, - @r#" - QueryPlan { - Sequence { - Fetch(service: "Subgraph2") { - { - outer1 { - __typename - ...OuterFrag - id - } - outer2 { - __typename - ...OuterFrag - id - } - } - - fragment OuterFrag on Outer { - inner { - v - } - } - }, - Parallel { - Flatten(path: "outer2") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v - } - } - }, - }, - Flatten(path: "outer1") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v - } - } - }, - }, - }, - }, - } - "# - ); - - // The previous cases tests the cases where nothing in the `...IFrag` spread at the - // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But - // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not - // have `v` in particular), it does contains field `w` that `I` also have, so we - // add that field to `IFrag` and make sure we still correctly query that field. - assert_plan!( - &planner, - r#" - query { - outer1 { - ...OuterFrag - } - outer2 { - ...OuterFrag - } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { - ...UFrag - } - } - - fragment UFrag on U { - ... on Outer { - v - w - } - ... on Inner { - v - } - } - "#, - @r#" - QueryPlan { - Sequence { - Fetch(service: "Subgraph2") { - { - outer1 { - __typename - ...OuterFrag - id - } - outer2 { - __typename - ...OuterFrag - id - } - } - - fragment OuterFrag on Outer { - w - inner { - v - } - } - }, - Parallel { - Flatten(path: "outer2") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v - } - } - }, - }, - Flatten(path: "outer1") { - Fetch(service: "Subgraph1") { - { - ... on Outer { - __typename - id - } - } => - { - ... on Outer { - v - } - } - }, - }, - }, - }, - } - "# - ); -} - -#[test] -fn it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated() { - let planner = planner!( - config = reuse_fragments_config(), - Subgraph1: r#" - type Query { - t: T - } - - type T { - id: ID! - t1: V - t2: V - } - - type V { - v1: Int - v2: Int - } - "#, - ); - assert_plan!( - &planner, - r#" - query($test: Boolean!) { - t { - ...OnT @include(if: $test) - } - } - - fragment OnT on T { - t1 { - ...OnV - } - t2 { - ...OnV - } - } - - fragment OnV on V { - v1 - v2 - } - "#, - @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - t { - ... on T @include(if: $test) { - t1 { - ...OnV - } - t2 { - ...OnV - } - } - } - } - - fragment OnV on V { - v1 - v2 - } - }, - } - "### - ); -} diff --git a/apollo-federation/tests/query_plan/supergraphs/inefficient_entity_fetches_to_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/inefficient_entity_fetches_to_same_subgraph.graphql new file mode 100644 index 0000000000..8aaa3f274f --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/inefficient_entity_fetches_to_same_subgraph.graphql @@ -0,0 +1,97 @@ +# Composed from subgraphs with hash: b2221050efb89f6e4df71823675d2ea1fbe66a31 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +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 + +interface I + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + v: V + w: Int @join__field(graph: SUBGRAPH2) +} + +type Inner implements I + @join__implements(graph: SUBGRAPH2, interface: "I") + @join__type(graph: SUBGRAPH2) +{ + v: V + w: Int +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +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 Outer implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: V @join__field(graph: SUBGRAPH1) + inner: Inner @join__field(graph: SUBGRAPH2) + w: Int @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + outer1: Outer @join__field(graph: SUBGRAPH2) + outer2: Outer @join__field(graph: SUBGRAPH2) +} + +type V + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + x: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_expands_nested_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/it_expands_nested_fragments.graphql new file mode 100644 index 0000000000..0d1594dcca --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_expands_nested_fragments.graphql @@ -0,0 +1,75 @@ +# Composed from subgraphs with hash: af8642bd2cc335a2823e7c95f48ce005d3c809f0 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +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 + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +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: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + a: V + b: V +} + +type V + @join__type(graph: SUBGRAPH1) +{ + v1: Int + v2: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation_from_operation_with_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation_from_operation_with_fragments.graphql new file mode 100644 index 0000000000..bf45161fb0 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation_from_operation_with_fragments.graphql @@ -0,0 +1,102 @@ +# Composed from subgraphs with hash: 7cb80bbad99a03ca0bb30082bd6f9eb6f7c1beff +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +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 + +type A1 implements Foo + @join__implements(graph: SUBGRAPH1, interface: "Foo") + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +type A2 implements Foo + @join__implements(graph: SUBGRAPH1, interface: "Foo") + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +type A3 implements Foo + @join__implements(graph: SUBGRAPH1, interface: "Foo") + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +union Anything + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A1") + @join__unionMember(graph: SUBGRAPH1, member: "A2") + @join__unionMember(graph: SUBGRAPH1, member: "A3") + = A1 | A2 | A3 + +interface Foo + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +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: SUBGRAPH1) +{ + a: Anything +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives.graphql new file mode 100644 index 0000000000..95316d4353 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives.graphql @@ -0,0 +1,68 @@ +# Composed from subgraphs with hash: 136ac120ab3c0a9b8ea4cb22cb440886a1b4a961 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +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 + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +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: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + a: Int + b: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_on_collapsed_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_on_collapsed_fragments.graphql new file mode 100644 index 0000000000..7b9af26713 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_on_collapsed_fragments.graphql @@ -0,0 +1,75 @@ +# Composed from subgraphs with hash: fd162a5fc982fc2cd0a8d33e271831822b681137 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +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 + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +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: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1) +{ + id: ID! + t1: V + t2: V +} + +type V + @join__type(graph: SUBGRAPH1) +{ + v1: Int + v2: Int +} diff --git a/apollo-router/src/configuration/migrations/0031-reuse-query-fragments.yaml b/apollo-router/src/configuration/migrations/0031-reuse-query-fragments.yaml new file mode 100644 index 0000000000..97ea457053 --- /dev/null +++ b/apollo-router/src/configuration/migrations/0031-reuse-query-fragments.yaml @@ -0,0 +1,6 @@ +description: supergraph.experimental_reuse_query_fragments is deprecated +actions: + - type: log + level: warn + path: supergraph.experimental_reuse_query_fragments + log: "'supergraph.experimental_reuse_query_fragments' is not supported by the native query planner and this configuration option will be removed in the next release. Use 'supergraph.generate_query_fragments' instead." \ No newline at end of file diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 2df5ad8c62..e6892e0087 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -417,7 +417,6 @@ impl Configuration { &self, ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig { apollo_federation::query_plan::query_planner::QueryPlannerConfig { - reuse_query_fragments: self.supergraph.reuse_query_fragments.unwrap_or(true), subgraph_graphql_validation: false, generate_query_fragments: self.supergraph.generate_query_fragments, incremental_delivery: @@ -684,7 +683,8 @@ pub(crate) struct Supergraph { pub(crate) introspection: bool, /// Enable reuse of query fragments - /// Default: depends on the federation version + /// This feature is deprecated and will be removed in next release. + /// The config can only be set when the legacy query planner is explicitly enabled. #[serde(rename = "experimental_reuse_query_fragments")] pub(crate) reuse_query_fragments: Option, @@ -765,7 +765,8 @@ impl Supergraph { Some(false) } else { reuse_query_fragments } ), - generate_query_fragments: generate_query_fragments.unwrap_or_else(default_generate_query_fragments), + generate_query_fragments: generate_query_fragments + .unwrap_or_else(default_generate_query_fragments), early_cancel: early_cancel.unwrap_or_default(), experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(), } @@ -802,7 +803,8 @@ impl Supergraph { Some(false) } else { reuse_query_fragments } ), - generate_query_fragments: generate_query_fragments.unwrap_or_else(default_generate_query_fragments), + generate_query_fragments: generate_query_fragments + .unwrap_or_else(default_generate_query_fragments), early_cancel: early_cancel.unwrap_or_default(), experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(), } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 6dd57e693f..db8f3f8851 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -6644,7 +6644,7 @@ expression: "&schema" }, "experimental_reuse_query_fragments": { "default": null, - "description": "Enable reuse of query fragments Default: depends on the federation version", + "description": "Enable reuse of query fragments This feature is deprecated and will be removed in next release. The config can only be set when the legacy query planner is explicitly enabled.", "nullable": true, "type": "boolean" }, diff --git a/apollo-router/src/configuration/tests.rs b/apollo-router/src/configuration/tests.rs index 21cb5fdb50..4a93e496cc 100644 --- a/apollo-router/src/configuration/tests.rs +++ b/apollo-router/src/configuration/tests.rs @@ -1096,19 +1096,3 @@ fn find_struct_name(lines: &[&str], line_number: usize) -> Option { }) .next() } - -#[test] -fn it_prevents_reuse_and_generate_query_fragments_simultaneously() { - let conf = Configuration::builder() - .supergraph( - Supergraph::builder() - .generate_query_fragments(true) - .reuse_query_fragments(true) - .build(), - ) - .build() - .unwrap(); - - assert!(conf.supergraph.generate_query_fragments); - assert_eq!(conf.supergraph.reuse_query_fragments, Some(false)); -} diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index cc058b5555..98a8832347 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -167,10 +167,6 @@ impl PlannerMode { configuration: &Configuration, ) -> Result, ServiceBuildError> { let config = apollo_federation::query_plan::query_planner::QueryPlannerConfig { - reuse_query_fragments: configuration - .supergraph - .reuse_query_fragments - .unwrap_or(true), subgraph_graphql_validation: false, generate_query_fragments: configuration.supergraph.generate_query_fragments, incremental_delivery: diff --git a/apollo-router/src/services/supergraph/tests.rs b/apollo-router/src/services/supergraph/tests.rs index ac2dbab21a..059d7baa09 100644 --- a/apollo-router/src/services/supergraph/tests.rs +++ b/apollo-router/src/services/supergraph/tests.rs @@ -3342,119 +3342,6 @@ async fn interface_object_typename() { insta::assert_json_snapshot!(stream.next_response().await.unwrap()); } -#[tokio::test] -async fn fragment_reuse() { - const SCHEMA: &str = r#"schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) - { - query: Query - } - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - 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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - 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 @join__implements( graph: join__Graph! interface: String!) repeatable on OBJECT | INTERFACE - - scalar link__Import - - enum link__Purpose { - SECURITY - EXECUTION - } - scalar join__FieldSet - - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - - type Query - @join__type(graph: ORGA) - @join__type(graph: USER) - { - me: User @join__field(graph: USER) - } - - type User - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") - { - id: ID! - name: String - organizations: [Organization] @join__field(graph: ORGA) - } - type Organization - @join__type(graph: ORGA, key: "id") - { - id: ID - name: String @join__field(graph: ORGA) - }"#; - - let subgraphs = MockedSubgraphs([ - ("user", MockSubgraph::builder().with_json( - serde_json::json!{{ - "query":"query Query__user__0($a:Boolean!=true$b:Boolean!=true){me{name ...on User@include(if:$a){__typename id}...on User@include(if:$b){__typename id}}}", - "operationName": "Query__user__0" - }}, - serde_json::json!{{"data": {"me": { "name": "Ada", "__typename": "User", "id": "1" }}}} - ).build()), - ("orga", MockSubgraph::builder().with_json( - serde_json::json!{{ - "query":"query Query__orga__1($representations:[_Any!]!$a:Boolean!=true$b:Boolean!=true){_entities(representations:$representations){...F@include(if:$a)...F@include(if:$b)}}fragment F on User{organizations{id name}}", - "operationName": "Query__orga__1", - "variables":{"representations":[{"__typename":"User","id":"1"}]} - }}, - serde_json::json!{{"data": {"_entities": [{ "organizations": [{"id": "2", "name": "Apollo"}] }]}}} - ).build()) - ].into_iter().collect()); - - let service = TestHarness::builder() - .configuration_json(serde_json::json!({ - "include_subgraph_errors": { "all": true }, - "supergraph": { - "generate_query_fragments": false, - "experimental_reuse_query_fragments": true, - } - })) - .unwrap() - .schema(SCHEMA) - .extra_plugin(subgraphs) - .build_supergraph() - .await - .unwrap(); - - let request = supergraph::Request::fake_builder() - .query( - r#"query Query($a: Boolean! = true, $b: Boolean! = true) { - me { - name - ...F @include(if: $a) - ...F @include(if: $b) - } - } - fragment F on User { - organizations { - id - name - } - }"#, - ) - .build() - .unwrap(); - let response = service - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); - - insta::assert_json_snapshot!(response); -} - #[tokio::test] async fn abstract_types_in_requires() { let schema = r#"schema diff --git a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml b/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml index 14136a0268..6f24cebc9a 100644 --- a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml +++ b/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml @@ -8,4 +8,5 @@ supergraph: - redis://localhost:6379 ttl: 10s experimental_reuse_query_fragments: true - + generate_query_fragments: false +experimental_query_planner_mode: legacy \ No newline at end of file diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 34e6b11093..8ff18e9cd4 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -51,7 +51,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { } // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:cache:1:federation:v2.9.3:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; + let known_cache_key = "plan:cache:1:federation:v2.9.3:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:8f8ce6ad09f15c3d567a05f1c3d7230ab71b3366fcaebc9cc3bbfa356d55ac12"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -974,10 +974,21 @@ async fn connection_failure_blocks_startup() { #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_query_fragments() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0ade8e18db172d9d51b36a2112513c15032d103100644df418a50596de3adfba", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:b030b297e8cc0fb51de5b683162be9a4a5a0023844597253e580f99672bdf2b4", ) .await; } @@ -1007,7 +1018,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:066f41523274aed2428e0f08c9de077ee748a1d8470ec31edb5224030a198f3b", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:ab8143af84859ddbed87fc3ac3b1f9c1e2271ffc8e58b58a666619ffc90bfc29", ) .await; } @@ -1029,11 +1040,12 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:b31d320db1af4015998cc89027f0ede2305dcc61724365e9b76d4252f90c7677", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:285740e3d6ca7533144f54f8395204d7c19c44ed16e48f22a3ea41195d60180b", ) .await; } +// TODO drop this test once we remove the JS QP #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_reuse_query_fragments() { // If this test fails and the cache key format changed you'll need to update @@ -1051,7 +1063,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:d54414eeede3a1bf631d88a84a1e3a354683be87746e79a69769cf18d919cc01", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:9af18c8afd568c197050fc1a60c52a8c98656f1775016110516fabfbedc135fe", ) .await; } @@ -1076,7 +1088,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.clear_redis_cache().await; // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; + let starting_key = "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:8f8ce6ad09f15c3d567a05f1c3d7230ab71b3366fcaebc9cc3bbfa356d55ac12"; assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; diff --git a/docs/source/reference/router/configuration.mdx b/docs/source/reference/router/configuration.mdx index 144fc63edf..ebf01cc9cb 100644 --- a/docs/source/reference/router/configuration.mdx +++ b/docs/source/reference/router/configuration.mdx @@ -1257,37 +1257,37 @@ example: ``` - -### Fragment generation and reuse + +### Automatic fragment generation By default, the router compresses subgraph requests by generating fragment definitions based on the shape of the subgraph operation. In many cases this significantly reduces the size of the query sent to subgraphs. -The router also supports an experimental algorithm that attempts to reuse fragments -from the original operation while forming subgraph requests. This experimental feature -used to be enabled by default, but is still available to support subgraphs that rely -on the specific shape of fragments in an operation: +You can explicitly opt-out of this behavior by specifying: ```yaml supergraph: generate_query_fragments: false - experimental_reuse_query_fragments: true ``` -Note that `generate_query_fragments` and `experimental_reuse_query_fragments` are -mutually exclusive; if both are explicitly set to `true`, `generate_query_fragments` -will take precedence. - -In the future, the `generate_query_fragments` option will be the only option for handling fragments. +The legacy query planner still supports an experimental algorithm that attempts to +reuse fragments from the original operation while forming subgraph requests. The +legacy query planner has to be explicitly enabled. This experimental feature used to +be enabled by default, but is still available to support subgraphs that rely on the +specific shape of fragments in an operation: - - - +```yaml +supergraph: + generate_query_fragments: false + experimental_reuse_query_fragments: true +``` -In the future, the `generate_query_fragments` option will be the only option for handling fragments. +Note that `generate_query_fragments` and `experimental_reuse_query_fragments` are +mutually exclusive; if both are explicitly set to `true`, `generate_query_fragments` +will take precedence. From 47f7528b50b42ec6e44d55771a390e54d079298c Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Thu, 5 Dec 2024 14:28:07 +0000 Subject: [PATCH 065/112] rename metric --- apollo-router/src/plugins/fleet_detector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index da97ed1886..dc1f60cbd1 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -160,7 +160,7 @@ impl GaugeStore { let opts = opts.clone(); gauges.push( meter - .u64_observable_gauge("apollo.router.schema") + .u64_observable_gauge("apollo.router.instance.schema") .with_description("Details about the current in-use schema") .with_callback(move |gauge| { // NOTE: this is a fixed gauge. We only care about observing the included From 0d649c39dbb60c0a674e1c3fbbea35bdd25f71f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:24:42 +0200 Subject: [PATCH 066/112] chore(deps): update dependency slack to v5 (#6370) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ee71dfa04..90c33dcebe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2.1 # across projects. See https://circleci.com/orbs/ for more information. orbs: gh: circleci/github-cli@2.6.0 - slack: circleci/slack@4.15.0 + slack: circleci/slack@5.1.1 secops: apollo/circleci-secops-orb@2.0.7 executors: From 0ce2cf1e278323961e7124de32c6bab5a27af537 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:32:52 +0200 Subject: [PATCH 067/112] fix(deps): update dependency express to v4.20.0 [security] (#6021) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jesse Rosenberger --- .../datadog-subgraph/package-lock.json | 294 +++++++++-- .../tracing/jaeger-subgraph/package-lock.json | 485 ++++++++++++++---- .../tracing/zipkin-subgraph/package-lock.json | 485 ++++++++++++++---- 3 files changed, 993 insertions(+), 271 deletions(-) diff --git a/dockerfiles/tracing/datadog-subgraph/package-lock.json b/dockerfiles/tracing/datadog-subgraph/package-lock.json index 6bfbc35f9f..b2e4649c45 100644 --- a/dockerfiles/tracing/datadog-subgraph/package-lock.json +++ b/dockerfiles/tracing/datadog-subgraph/package-lock.json @@ -770,9 +770,10 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -782,7 +783,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -792,6 +793,21 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -801,12 +817,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -943,6 +966,23 @@ "ms": "2.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delay": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", @@ -1000,6 +1040,27 @@ "node": ">= 0.8" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1019,36 +1080,37 @@ "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1059,6 +1121,54 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -1106,19 +1216,43 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1133,21 +1267,26 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">= 0.4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -1159,6 +1298,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1166,6 +1306,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1325,9 +1477,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -1447,9 +1603,13 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1496,9 +1656,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" }, "node_modules/pprof-format": { "version": "2.1.0", @@ -1665,9 +1826,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "license": "MIT", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -1678,6 +1840,23 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1704,13 +1883,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" diff --git a/dockerfiles/tracing/jaeger-subgraph/package-lock.json b/dockerfiles/tracing/jaeger-subgraph/package-lock.json index 639115bb69..6918784db6 100644 --- a/dockerfiles/tracing/jaeger-subgraph/package-lock.json +++ b/dockerfiles/tracing/jaeger-subgraph/package-lock.json @@ -610,9 +610,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -622,7 +622,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -632,6 +632,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bufrw": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.3.0.tgz", @@ -655,12 +669,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -729,6 +749,22 @@ "ms": "2.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -776,6 +812,25 @@ "xtend": "~4.0.0" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -790,36 +845,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -830,6 +885,50 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/express/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -877,19 +976,40 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -917,21 +1037,24 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "function-bind": "^1.1.1" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">= 0.4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -950,6 +1073,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hexer": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/hexer/-/hexer-1.5.0.tgz", @@ -1065,9 +1199,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -1163,9 +1300,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1198,9 +1338,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/process": { "version": "0.10.1", @@ -1319,9 +1459,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -1332,6 +1472,22 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1350,13 +1506,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1990,9 +2150,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2002,10 +2162,20 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" + }, + "dependencies": { + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + } } }, "bufrw": { @@ -2025,12 +2195,15 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "combined-stream": { @@ -2081,6 +2254,16 @@ "ms": "2.0.0" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2115,6 +2298,19 @@ "xtend": "~4.0.0" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2126,41 +2322,80 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + } + } + } } }, "finalhandler": { @@ -2198,19 +2433,28 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "gopd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", + "requires": { + "get-intrinsic": "^1.2.4" } }, "graphql": { @@ -2226,24 +2470,35 @@ "tslib": "^2.1.0" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "function-bind": "^1.1.1" + "es-define-property": "^1.0.0" } }, "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", + "requires": { + "call-bind": "^1.0.7" + } }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "hexer": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/hexer/-/hexer-1.5.0.tgz", @@ -2328,9 +2583,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "methods": { "version": "1.1.2", @@ -2394,9 +2649,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" }, "on-finished": { "version": "2.4.1", @@ -2417,9 +2672,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "process": { "version": "0.10.1", @@ -2502,9 +2757,9 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -2512,6 +2767,19 @@ "send": "0.18.0" } }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2527,13 +2795,14 @@ } }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "statuses": { diff --git a/dockerfiles/tracing/zipkin-subgraph/package-lock.json b/dockerfiles/tracing/zipkin-subgraph/package-lock.json index c3dbbfe3e4..cba9a4fb28 100644 --- a/dockerfiles/tracing/zipkin-subgraph/package-lock.json +++ b/dockerfiles/tracing/zipkin-subgraph/package-lock.json @@ -631,9 +631,9 @@ "peer": true }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -643,7 +643,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -653,6 +653,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bufrw": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.3.0.tgz", @@ -676,12 +690,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -750,6 +770,22 @@ "ms": "2.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -797,6 +833,25 @@ "xtend": "~4.0.0" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -811,36 +866,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -851,6 +906,50 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/express/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -898,19 +997,40 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -938,21 +1058,24 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "function-bind": "^1.1.1" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">= 0.4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -971,6 +1094,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hexer": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/hexer/-/hexer-1.5.0.tgz", @@ -1092,9 +1226,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -1190,9 +1327,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1225,9 +1365,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/process": { "version": "0.10.1", @@ -1346,9 +1486,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -1359,6 +1499,22 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1377,13 +1533,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2055,9 +2215,9 @@ "peer": true }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2067,10 +2227,20 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" + }, + "dependencies": { + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + } } }, "bufrw": { @@ -2090,12 +2260,15 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "combined-stream": { @@ -2146,6 +2319,16 @@ "ms": "2.0.0" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2180,6 +2363,19 @@ "xtend": "~4.0.0" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2191,41 +2387,80 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + } + } + } } }, "finalhandler": { @@ -2263,19 +2498,28 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "gopd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", + "requires": { + "get-intrinsic": "^1.2.4" } }, "graphql": { @@ -2291,24 +2535,35 @@ "tslib": "^2.1.0" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "function-bind": "^1.1.1" + "es-define-property": "^1.0.0" } }, "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", + "requires": { + "call-bind": "^1.0.7" + } }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "hexer": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/hexer/-/hexer-1.5.0.tgz", @@ -2399,9 +2654,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "methods": { "version": "1.1.2", @@ -2465,9 +2720,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" }, "on-finished": { "version": "2.4.1", @@ -2488,9 +2743,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "process": { "version": "0.10.1", @@ -2573,9 +2828,9 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -2583,6 +2838,19 @@ "send": "0.18.0" } }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2598,13 +2866,14 @@ } }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "statuses": { From cfbe80f83a4770156bbda52309dbb2a9b1c8d222 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:24:26 +0200 Subject: [PATCH 068/112] chore(deps): update openzipkin/zipkin docker tag to v3.4.3 (#6405) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 90c33dcebe..30441767cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,7 +26,7 @@ executors: - image: cimg/base:stable - image: cimg/redis:7.4.1 - image: jaegertracing/all-in-one:1.54.0 - - image: openzipkin/zipkin:3.4.2 + - image: openzipkin/zipkin:3.4.3 - image: ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:v1.17.0 resource_class: xlarge environment: diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index c2b4b5c25c..e80af46dad 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -36,6 +36,6 @@ services: zipkin: container_name: zipkin - image: openzipkin/zipkin:3.4.2 + image: openzipkin/zipkin:3.4.3 ports: - 9411:9411 From c4a24d3b8716636048f4e07c6c845fa30ab7dbd1 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 24 Oct 2024 17:52:43 -0700 Subject: [PATCH 069/112] [FOUN-575] persisted queries have client names This depends on unreleased functionality of GraphOS. --- apollo-router/src/plugins/telemetry/mod.rs | 2 +- .../persisted_queries/manifest_poller.rs | 82 +++++++++++++++---- .../services/layers/persisted_queries/mod.rs | 8 +- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index da5361e1c1..49fcbdf935 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -171,7 +171,7 @@ pub(crate) mod tracing; pub(crate) mod utils; // Tracing consts -const CLIENT_NAME: &str = "apollo_telemetry::client_name"; +pub(crate) const CLIENT_NAME: &str = "apollo_telemetry::client_name"; const CLIENT_VERSION: &str = "apollo_telemetry::client_version"; const SUBGRAPH_FTV1: &str = "apollo_telemetry::subgraph_ftv1"; pub(crate) const STUDIO_EXCLUDE: &str = "apollo_telemetry::studio::exclude"; diff --git a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs index 7051ec97be..a51460b3ec 100644 --- a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs +++ b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs @@ -21,8 +21,14 @@ use crate::uplink::stream_from_uplink_transforming_new_response; use crate::uplink::UplinkConfig; use crate::Configuration; +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub(crate) struct FullPersistedQueryOperationId { + operation_id: String, + client_name: Option, +} + /// An in memory cache of persisted queries. -pub(crate) type PersistedQueryManifest = HashMap; +pub(crate) type PersistedQueryManifest = HashMap; /// How the router should respond to requests that are not resolved as the IDs /// of an operation in the manifest. (For the most part this means "requests @@ -212,7 +218,7 @@ impl PersistedQueryManifestPoller { if manifest_files.is_empty() { return Err("no local persisted query list files specified".into()); } - let mut manifest: HashMap = PersistedQueryManifest::new(); + let mut manifest = PersistedQueryManifest::new(); for local_pq_list in manifest_files { tracing::info!( @@ -250,7 +256,13 @@ impl PersistedQueryManifestPoller { } for operation in manifest_file.operations { - manifest.insert(operation.id, operation.body); + manifest.insert( + FullPersistedQueryOperationId { + operation_id: operation.id, + client_name: operation.client_name, + }, + operation.body, + ); } } @@ -343,15 +355,35 @@ impl PersistedQueryManifestPoller { } } - pub(crate) fn get_operation_body(&self, persisted_query_id: &str) -> Option { + pub(crate) fn get_operation_body( + &self, + persisted_query_id: &str, + client_name: Option, + ) -> Option { let state = self .state .read() .expect("could not acquire read lock on persisted query manifest state"); - state + if let Some(body) = state .persisted_query_manifest - .get(persisted_query_id) + .get(&FullPersistedQueryOperationId { + operation_id: persisted_query_id.to_string(), + client_name: client_name.clone(), + }) .cloned() + { + Some(body) + } else if client_name.is_some() { + state + .persisted_query_manifest + .get(&FullPersistedQueryOperationId { + operation_id: persisted_query_id.to_string(), + client_name: None, + }) + .cloned() + } else { + None + } } pub(crate) fn get_all_operations(&self) -> Vec { @@ -588,7 +620,13 @@ async fn add_chunk_to_operations( match fetch_chunk(http_client.clone(), chunk_url).await { Ok(chunk) => { for operation in chunk.operations { - operations.insert(operation.id, operation.body); + operations.insert( + FullPersistedQueryOperationId { + operation_id: operation.id, + client_name: operation.client_name, + }, + operation.body, + ); } return Ok(()); } @@ -674,9 +712,11 @@ pub(crate) struct SignedUrlChunk { /// A single operation containing an ID and a body, #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub(crate) struct Operation { pub(crate) id: String, pub(crate) body: String, + pub(crate) client_name: Option, } #[cfg(test)] @@ -701,7 +741,7 @@ mod tests { ) .await .unwrap(); - assert_eq!(manifest_manager.get_operation_body(&id), Some(body)) + assert_eq!(manifest_manager.get_operation_body(&id, None), Some(body)) } #[tokio::test(flavor = "multi_thread")] @@ -734,18 +774,26 @@ mod tests { ) .await .unwrap(); - assert_eq!(manifest_manager.get_operation_body(&id), Some(body)) + assert_eq!(manifest_manager.get_operation_body(&id, None), Some(body)) } #[test] fn safelist_body_normalization() { - let safelist = FreeformGraphQLSafelist::new(&PersistedQueryManifest::from([( - "valid-syntax".to_string(), - "fragment A on T { a } query SomeOp { ...A ...B } fragment,,, B on U{b c } # yeah" - .to_string(), - ), ( - "invalid-syntax".to_string(), - "}}}".to_string()), + let safelist = FreeformGraphQLSafelist::new(&PersistedQueryManifest::from([ + ( + FullPersistedQueryOperationId { + operation_id: "valid-syntax".to_string(), + client_name: None, + }, + "fragment A on T { a } query SomeOp { ...A ...B } fragment,,, B on U{b c } # yeah".to_string(), + ), + ( + FullPersistedQueryOperationId { + operation_id: "invalid-syntax".to_string(), + client_name: None, + }, + "}}}".to_string(), + ), ])); let is_allowed = |body: &str| -> bool { @@ -795,6 +843,6 @@ mod tests { ) .await .unwrap(); - assert_eq!(manifest_manager.get_operation_body(&id), Some(body)) + assert_eq!(manifest_manager.get_operation_body(&id, None), Some(body)) } } diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 48c4dcba1d..85d296f683 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -14,6 +14,7 @@ use tower::BoxError; use self::manifest_poller::FreeformGraphQLAction; use super::query_analysis::ParsedDocument; use crate::graphql::Error as GraphQLError; +use crate::plugins::telemetry::CLIENT_NAME; use crate::services::SupergraphRequest; use crate::services::SupergraphResponse; use crate::Configuration; @@ -110,9 +111,10 @@ impl PersistedQueryLayer { } else { // if there is no query, look up the persisted query in the manifest // and put the body on the `supergraph_request` - if let Some(persisted_query_body) = - manifest_poller.get_operation_body(persisted_query_id) - { + if let Some(persisted_query_body) = manifest_poller.get_operation_body( + persisted_query_id, + request.context.get(CLIENT_NAME).unwrap_or_default(), + ) { let body = request.supergraph_request.body_mut(); body.query = Some(persisted_query_body); body.extensions.remove("persistedQuery"); From d40678f9998cc0fed09eb34afb04636a01b2f2c8 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 26 Nov 2024 17:26:46 -0800 Subject: [PATCH 070/112] Get client name for more places; tests --- apollo-router/src/plugins/telemetry/apollo.rs | 4 +- .../persisted_queries/manifest_poller.rs | 12 +- .../services/layers/persisted_queries/mod.rs | 28 +++- .../test_harness/mocks/persisted_queries.rs | 17 +- apollo-router/tests/integration_tests.rs | 154 +++++++++++++++--- 5 files changed, 183 insertions(+), 32 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/apollo.rs b/apollo-router/src/plugins/telemetry/apollo.rs index 69780cbc9e..2fcdcc1dc7 100644 --- a/apollo-router/src/plugins/telemetry/apollo.rs +++ b/apollo-router/src/plugins/telemetry/apollo.rs @@ -67,7 +67,7 @@ pub(crate) struct Config { #[schemars(skip)] pub(crate) apollo_graph_ref: Option, - /// The name of the header to extract from requests when populating 'client nane' for traces and metrics in Apollo Studio. + /// The name of the header to extract from requests when populating 'client name' for traces and metrics in Apollo Studio. #[schemars(with = "Option", default = "client_name_header_default_str")] #[serde(deserialize_with = "deserialize_header_name")] pub(crate) client_name_header: HeaderName, @@ -176,7 +176,7 @@ fn otlp_endpoint_default() -> Url { Url::parse(OTLP_ENDPOINT_DEFAULT).expect("must be valid url") } -const fn client_name_header_default_str() -> &'static str { +pub(crate) const fn client_name_header_default_str() -> &'static str { "apollographql-client-name" } diff --git a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs index a51460b3ec..496c98d0a7 100644 --- a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs +++ b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs @@ -21,14 +21,18 @@ use crate::uplink::stream_from_uplink_transforming_new_response; use crate::uplink::UplinkConfig; use crate::Configuration; +/// The full identifier for an operation in a PQ list consists of an operation +/// ID and an optional client name. #[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub(crate) struct FullPersistedQueryOperationId { - operation_id: String, - client_name: Option, +pub struct FullPersistedQueryOperationId { + /// The operation ID (usually a hash). + pub operation_id: String, + /// The client name associated with the operation; if None, can be any client. + pub client_name: Option, } /// An in memory cache of persisted queries. -pub(crate) type PersistedQueryManifest = HashMap; +pub type PersistedQueryManifest = HashMap; /// How the router should respond to requests that are not resolved as the IDs /// of an operation in the manifest. (For the most part this means "requests diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 85d296f683..56d3308792 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -8,18 +8,22 @@ use http::header::CACHE_CONTROL; use http::HeaderValue; use http::StatusCode; use id_extractor::PersistedQueryIdExtractor; +pub use manifest_poller::FullPersistedQueryOperationId; +pub use manifest_poller::PersistedQueryManifest; pub(crate) use manifest_poller::PersistedQueryManifestPoller; use tower::BoxError; use self::manifest_poller::FreeformGraphQLAction; use super::query_analysis::ParsedDocument; use crate::graphql::Error as GraphQLError; +use crate::plugins::telemetry::apollo::client_name_header_default_str; use crate::plugins::telemetry::CLIENT_NAME; use crate::services::SupergraphRequest; use crate::services::SupergraphResponse; use crate::Configuration; const DONT_CACHE_RESPONSE_VALUE: &str = "private, no-cache, must-revalidate"; +const PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY: &str = "apollo_persisted_queries::client_name"; struct UsedQueryIdFromManifest; @@ -113,7 +117,29 @@ impl PersistedQueryLayer { // and put the body on the `supergraph_request` if let Some(persisted_query_body) = manifest_poller.get_operation_body( persisted_query_id, - request.context.get(CLIENT_NAME).unwrap_or_default(), + // Use the first one of these that exists: + // - The PQL-specific context name entry + // `apollo_persisted_queries::client_name` (which can be set + // by router_service plugins) + // - The same name used by telemetry if telemetry is enabled + // (ie, the value of the header named by + // `telemetry.apollo.client_name_header`, which defaults to + // `apollographql-client-name` by default) + // - The value in the `apollographql-client-name` header + // (whether or not telemetry is enabled) + request + .context + .get(PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY) + .unwrap_or_default() + .or_else(|| request.context.get(CLIENT_NAME).unwrap_or_default()) + .or_else(|| { + request + .supergraph_request + .headers() + .get(client_name_header_default_str()) + .map(|hv| hv.to_str().unwrap_or_default()) + .map(str::to_string) + }), ) { let body = request.supergraph_request.body_mut(); body.query = Some(persisted_query_body); diff --git a/apollo-router/src/test_harness/mocks/persisted_queries.rs b/apollo-router/src/test_harness/mocks/persisted_queries.rs index cdf8b0e5f9..3db8d53932 100644 --- a/apollo-router/src/test_harness/mocks/persisted_queries.rs +++ b/apollo-router/src/test_harness/mocks/persisted_queries.rs @@ -14,6 +14,8 @@ use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; +pub use crate::services::layers::persisted_queries::FullPersistedQueryOperationId; +pub use crate::services::layers::persisted_queries::PersistedQueryManifest; use crate::uplink::Endpoints; use crate::uplink::UplinkConfig; @@ -32,7 +34,7 @@ pub async fn mock_empty_pq_uplink() -> (UplinkMockGuard, UplinkConfig) { /// Mocks an uplink server with a persisted query list with a delay. pub async fn mock_pq_uplink_with_delay( - manifest: &HashMap, + manifest: &PersistedQueryManifest, delay: Duration, ) -> (UplinkMockGuard, UplinkConfig) { let (guard, url) = mock_pq_uplink_one_endpoint(manifest, Some(delay)).await; @@ -43,7 +45,7 @@ pub async fn mock_pq_uplink_with_delay( } /// Mocks an uplink server with a persisted query list containing operations passed to this function. -pub async fn mock_pq_uplink(manifest: &HashMap) -> (UplinkMockGuard, UplinkConfig) { +pub async fn mock_pq_uplink(manifest: &PersistedQueryManifest) -> (UplinkMockGuard, UplinkConfig) { let (guard, url) = mock_pq_uplink_one_endpoint(manifest, None).await; ( guard, @@ -58,22 +60,29 @@ pub struct UplinkMockGuard { } #[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] struct Operation { id: String, body: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + client_name: Option, } /// Mocks an uplink server; returns a single Url rather than a full UplinkConfig, so you /// can combine it with another one to test failover. pub async fn mock_pq_uplink_one_endpoint( - manifest: &HashMap, + manifest: &PersistedQueryManifest, delay: Option, ) -> (UplinkMockGuard, Url) { let operations: Vec = manifest // clone the manifest so the caller can still make assertions about it .clone() .drain() - .map(|(id, body)| Operation { id, body }) + .map(|(full_id, body)| Operation { + id: full_id.operation_id, + body, + client_name: full_id.client_name, + }) .collect(); let mock_gcs_server = MockServer::start().await; diff --git a/apollo-router/tests/integration_tests.rs b/apollo-router/tests/integration_tests.rs index 378a20ddd5..b944448145 100644 --- a/apollo-router/tests/integration_tests.rs +++ b/apollo-router/tests/integration_tests.rs @@ -8,6 +8,7 @@ use std::ffi::OsStr; use std::sync::Arc; use std::sync::Mutex; +use apollo_router::_private::create_test_service_factory_from_yaml; use apollo_router::graphql; use apollo_router::plugin::Plugin; use apollo_router::plugin::PluginInit; @@ -17,7 +18,6 @@ use apollo_router::services::supergraph; use apollo_router::test_harness::mocks::persisted_queries::*; use apollo_router::Configuration; use apollo_router::Context; -use apollo_router::_private::create_test_service_factory_from_yaml; use futures::StreamExt; use http::header::ACCEPT; use http::header::CONTENT_TYPE; @@ -407,33 +407,49 @@ async fn persisted_queries() { use serde_json::json; /// Construct a persisted query request from an ID. - fn pq_request(persisted_query_id: &str) -> router::Request { - supergraph::Request::fake_builder() - .extension( - "persistedQuery", - json!({ - "version": 1, - "sha256Hash": persisted_query_id - }), - ) + fn pq_request(persisted_query_id: &str, header: Option<(&str, &str)>) -> router::Request { + let mut builder = supergraph::Request::fake_builder().extension( + "persistedQuery", + json!({ + "version": 1, + "sha256Hash": persisted_query_id + }), + ); + if let Some((name, value)) = header { + builder = builder.header(name, value); + } + builder .build() .expect("expecting valid request") .try_into() .expect("could not convert supergraph::Request to router::Request") } - // set up a PQM with one query + // set up a PQM with one query and one client-specific query const PERSISTED_QUERY_ID: &str = "GetMyNameID"; const PERSISTED_QUERY_BODY: &str = "query GetMyName { me { name } }"; + const PERSISTED_QUERY_MOBILE_BODY: &str = "query GetMyName { me { aliased: name } }"; let expected_data = serde_json_bytes::json!({ "me": { "name": "Ada Lovelace" } }); + let expected_data_mobile = serde_json_bytes::json!({ + "me": { + "aliased": "Ada Lovelace" + } + }); - let (_mock_guard, uplink_config) = mock_pq_uplink( - &hashmap! { PERSISTED_QUERY_ID.to_string() => PERSISTED_QUERY_BODY.to_string() }, - ) + let (_mock_guard, uplink_config) = mock_pq_uplink(&hashmap! { + FullPersistedQueryOperationId { + operation_id: PERSISTED_QUERY_ID.to_string(), + client_name: None, + } => PERSISTED_QUERY_BODY.to_string(), + FullPersistedQueryOperationId { + operation_id: PERSISTED_QUERY_ID.to_string(), + client_name: Some("mobile".to_string()), + } => PERSISTED_QUERY_MOBILE_BODY.to_string(), + }) .await; let config = serde_json::json!({ @@ -450,12 +466,62 @@ async fn persisted_queries() { let (router, registry) = setup_router_and_registry_with_config(config).await.unwrap(); // Successfully run a persisted query. - let actual = query_with_router(router.clone(), pq_request(PERSISTED_QUERY_ID)).await; + let actual = query_with_router(router.clone(), pq_request(PERSISTED_QUERY_ID, None)).await; + assert!(actual.errors.is_empty()); + assert_eq!(actual.data.as_ref(), Some(&expected_data)); + assert_eq!( + registry.totals_reset(), + hashmap! {"accounts".to_string() => 1} + ); + + // Successfully run a persisted query with client name that has its own operation. + let actual = query_with_router( + router.clone(), + pq_request( + PERSISTED_QUERY_ID, + Some(("apollographql-client-name", "mobile")), + ), + ) + .await; + assert!(actual.errors.is_empty()); + assert_eq!(actual.data.as_ref(), Some(&expected_data_mobile)); + assert_eq!( + registry.totals_reset(), + hashmap! {"accounts".to_string() => 1} + ); + + // Successfully run a persisted query with client name that has its own operation, + // setting the client name via context in a plugin. + let actual = query_with_router( + router.clone(), + pq_request(PERSISTED_QUERY_ID, Some(("pq-client-name", "mobile"))), + ) + .await; + assert!(actual.errors.is_empty()); + assert_eq!(actual.data.as_ref(), Some(&expected_data_mobile)); + assert_eq!( + registry.totals_reset(), + hashmap! {"accounts".to_string() => 1} + ); + + // Successfully run a persisted query with random client name falling back + // to the version without client name. + let actual = query_with_router( + router.clone(), + pq_request( + PERSISTED_QUERY_ID, + Some(("apollographql-client-name", "something-not-mobile")), + ), + ) + .await; assert!(actual.errors.is_empty()); assert_eq!(actual.data.as_ref(), Some(&expected_data)); - assert_eq!(registry.totals(), hashmap! {"accounts".to_string() => 1}); + assert_eq!( + registry.totals_reset(), + hashmap! {"accounts".to_string() => 1} + ); - // Error on unpersisted query. + // Error on unknown persisted query ID. const UNKNOWN_QUERY_ID: &str = "unknown_query"; const UNPERSISTED_QUERY_BODY: &str = "query GetYourName { you: me { name } }"; let expected_data = serde_json_bytes::json!({ @@ -463,7 +529,7 @@ async fn persisted_queries() { "name": "Ada Lovelace" } }); - let actual = query_with_router(router.clone(), pq_request(UNKNOWN_QUERY_ID)).await; + let actual = query_with_router(router.clone(), pq_request(UNKNOWN_QUERY_ID, None)).await; assert_eq!( actual.errors, vec![apollo_router::graphql::Error::builder() @@ -474,7 +540,7 @@ async fn persisted_queries() { .build()] ); assert_eq!(actual.data, None); - assert_eq!(registry.totals(), hashmap! {"accounts".to_string() => 1}); + assert_eq!(registry.totals_reset(), hashmap! {}); // We didn't break normal GETs. let actual = query_with_router( @@ -490,7 +556,10 @@ async fn persisted_queries() { .await; assert!(actual.errors.is_empty()); assert_eq!(actual.data.as_ref(), Some(&expected_data)); - assert_eq!(registry.totals(), hashmap! {"accounts".to_string() => 2}); + assert_eq!( + registry.totals_reset(), + hashmap! {"accounts".to_string() => 1} + ); // We didn't break normal POSTs. let actual = query_with_router( @@ -506,7 +575,10 @@ async fn persisted_queries() { .await; assert!(actual.errors.is_empty()); assert_eq!(actual.data, Some(expected_data)); - assert_eq!(registry.totals(), hashmap! {"accounts".to_string() => 3}); + assert_eq!( + registry.totals_reset(), + hashmap! {"accounts".to_string() => 1} + ); // Proper error when sending malformed request body let actual = query_with_router( @@ -530,6 +602,7 @@ async fn persisted_queries() { actual.errors[0].extensions["code"], "INVALID_GRAPHQL_REQUEST" ); + assert_eq!(registry.totals_reset(), hashmap! {}); } #[tokio::test(flavor = "multi_thread")] @@ -1200,6 +1273,7 @@ async fn setup_router_and_registry_with_config( .configuration(Arc::new(config)) .schema(include_str!("fixtures/supergraph.graphql")) .extra_plugin(counting_registry.clone()) + .extra_plugin(PQClientNamePlugin) .build_router() .await?; Ok((router, counting_registry)) @@ -1264,6 +1338,15 @@ impl CountingServiceRegistry { fn totals(&self) -> HashMap { self.counts.lock().unwrap().clone() } + + /// Like `totals`, but clears the counters, so that each assertion is + /// independent of each other. + fn totals_reset(&self) -> HashMap { + let mut totals = self.counts.lock().unwrap(); + let ret = totals.clone(); + totals.clear(); + ret + } } #[async_trait::async_trait] @@ -1290,6 +1373,35 @@ impl Plugin for CountingServiceRegistry { } } +#[derive(Debug, Clone)] +struct PQClientNamePlugin; + +#[async_trait::async_trait] +impl Plugin for PQClientNamePlugin { + type Config = (); + + async fn new(_: PluginInit) -> Result { + unreachable!() + } + + fn router_service(&self, service: router::BoxService) -> router::BoxService { + service + .map_request(move |request: router::Request| { + if let Some(v) = request.router_request.headers().get("pq-client-name") { + request + .context + .insert( + "apollo_persisted_queries::client_name", + v.to_str().unwrap().to_string(), + ) + .unwrap(); + } + request + }) + .boxed() + } +} + #[tokio::test(flavor = "multi_thread")] async fn all_stock_router_example_yamls_are_valid() { let example_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../examples"); From 6bfdef9fbf64ee90e2e690c46fb4eb9d0d4ef14c Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 27 Nov 2024 15:01:03 -0800 Subject: [PATCH 071/112] checkpoint --- .../services/layers/persisted_queries/mod.rs | 18 +-- apollo-router/tests/samples/README.md | 2 +- .../persisted-queries/basic/README.md | 3 + .../basic/configuration.yaml | 12 ++ .../basic/persisted-query-manifest.json | 15 ++ .../persisted-queries/basic/plan.yaml | 129 ++++++++++++++++++ .../persisted-queries/basic/rhai/main.rhai | 7 + .../basic/supergraph.graphql | 124 +++++++++++++++++ apollo-router/tests/samples_tests.rs | 53 ++++++- 9 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 apollo-router/tests/samples/enterprise/persisted-queries/basic/README.md create mode 100644 apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml create mode 100644 apollo-router/tests/samples/enterprise/persisted-queries/basic/persisted-query-manifest.json create mode 100644 apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml create mode 100644 apollo-router/tests/samples/enterprise/persisted-queries/basic/rhai/main.rhai create mode 100644 apollo-router/tests/samples/enterprise/persisted-queries/basic/supergraph.graphql diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 56d3308792..80614663ce 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -16,7 +16,6 @@ use tower::BoxError; use self::manifest_poller::FreeformGraphQLAction; use super::query_analysis::ParsedDocument; use crate::graphql::Error as GraphQLError; -use crate::plugins::telemetry::apollo::client_name_header_default_str; use crate::plugins::telemetry::CLIENT_NAME; use crate::services::SupergraphRequest; use crate::services::SupergraphResponse; @@ -121,25 +120,14 @@ impl PersistedQueryLayer { // - The PQL-specific context name entry // `apollo_persisted_queries::client_name` (which can be set // by router_service plugins) - // - The same name used by telemetry if telemetry is enabled - // (ie, the value of the header named by - // `telemetry.apollo.client_name_header`, which defaults to - // `apollographql-client-name` by default) - // - The value in the `apollographql-client-name` header - // (whether or not telemetry is enabled) + // - The same name used by telemetry (ie, the value of the + // header named by `telemetry.apollo.client_name_header`, + // which defaults to `apollographql-client-name` by default) request .context .get(PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY) .unwrap_or_default() .or_else(|| request.context.get(CLIENT_NAME).unwrap_or_default()) - .or_else(|| { - request - .supergraph_request - .headers() - .get(client_name_header_default_str()) - .map(|hv| hv.to_str().unwrap_or_default()) - .map(str::to_string) - }), ) { let body = request.supergraph_request.body_mut(); body.query = Some(persisted_query_body); diff --git a/apollo-router/tests/samples/README.md b/apollo-router/tests/samples/README.md index c37c65e06b..b5c2f68114 100644 --- a/apollo-router/tests/samples/README.md +++ b/apollo-router/tests/samples/README.md @@ -4,7 +4,7 @@ This folder contains a series of Router integration tests that can be defined en ## How to write a test -One test is recognized as a folder containing a `plan.json` file. Any number of subfolders is accepted, and the test name will be the path to the test folder. If the folder contains a `README.md` file, it will be added to the captured output of the test, and displayed if the test failed. +One test is recognized as a folder containing a `plan.json` (or `plan.yaml`) file. Any number of subfolders is accepted, and the test name will be the path to the test folder. If the folder contains a `README.md` file, it will be added to the captured output of the test, and displayed if the test failed. The `plan.json` file contains a top level JSON object with an `actions` field, containing an array of possible actions, that will be executed one by one: diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/README.md b/apollo-router/tests/samples/enterprise/persisted-queries/basic/README.md new file mode 100644 index 0000000000..09cba4f996 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/README.md @@ -0,0 +1,3 @@ +# Persisted Queries + +This tests Persisted Query Lists: https://www.apollographql.com/docs/graphos/routing/security/persisted-queries diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml b/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml new file mode 100644 index 0000000000..dc60579e53 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml @@ -0,0 +1,12 @@ +persisted_queries: + enabled: true + experimental_local_manifests: + - tests/samples/enterprise/persisted-queries/basic/persisted-query-manifest.json +apq: + enabled: false +telemetry: + apollo: + client_name_header: custom-client-name +rhai: + scripts: "tests/samples/enterprise/persisted-queries/basic/rhai" + main: "main.rhai" diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/persisted-query-manifest.json b/apollo-router/tests/samples/enterprise/persisted-queries/basic/persisted-query-manifest.json new file mode 100644 index 0000000000..1659290d24 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/persisted-query-manifest.json @@ -0,0 +1,15 @@ +{ + "format": "apollo-persisted-query-manifest", + "version": 1, + "operations": [ + { + "id": "GetMyNameID", + "body": "query GetMyName { me { name } }" + }, + { + "id": "GetMyNameID", + "clientName": "mobile", + "body": "query GetMyName { me { mobileAlias: name } }" + } + ] +} diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml b/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml new file mode 100644 index 0000000000..c08b75aeb9 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml @@ -0,0 +1,129 @@ +enterprise: true + +actions: +- type: Start + schema_path: ./supergraph.graphql + configuration_path: ./configuration.yaml + subgraphs: + accounts: + requests: + - request: + body: + query: "query GetMyName__accounts__0{me{name}}" + response: + body: + data: + me: + name: "Ada Lovelace" + - request: + body: + query: "query GetMyName__accounts__0{me{mobileAlias:name}}" + response: + body: + data: + me: + mobileAlias: "Ada Lovelace" + - request: + body: + query: "query GetYourName__accounts__0{you:me{name}}" + response: + body: + data: + you: + name: "Ada Lovelace" + +# Successfully run a persisted query. +- type: Request + description: "Run a persisted query" + request: + extensions: + persistedQuery: + version: 1 + sha256Hash: "GetMyNameID" + expected_response: + data: + me: + name: "Ada Lovelace" + +# Successfully run a persisted query with client name that has its own +# operation, using the client name header configured in +# `telemetry.apollo.client_name_header`. +- type: Request + description: "Run a persisted query with client_name_header" + request: + extensions: + persistedQuery: + version: 1 + sha256Hash: "GetMyNameID" + headers: + custom-client-name: mobile + expected_response: + data: + me: + mobileAlias: "Ada Lovelace" + +# Successfully run a persisted query with client name that has its own +# operation, setting the client name via context in a Rhai plugin. +- type: Request + description: "Run a persisted query with plugin-set client name" + request: + extensions: + persistedQuery: + version: 1 + sha256Hash: "GetMyNameID" + headers: + plugin-client-name: mobile + expected_response: + data: + me: + mobileAlias: "Ada Lovelace" + +# Successfully run a persisted query with random client name falling back to the +# version without client name. +- type: Request + description: "Run a persisted query with fallback client name" + request: + extensions: + persistedQuery: + version: 1 + sha256Hash: "GetMyNameID" + headers: + custom-client-name: something-not-mobile + expected_response: + data: + me: + name: "Ada Lovelace" + +- type: Request + description: "Unknown persisted query ID" + request: + extensions: + persistedQuery: + version: 1 + sha256Hash: "unknown_query" + expected_response: + errors: + - message: "Persisted query 'unknown_query' not found in the persisted query list" + extensions: + code: PERSISTED_QUERY_NOT_IN_LIST + +- type: Request + description: "Normal non-PQ POSTs work" + request: + query: "query GetYourName { you: me { name } }" + expected_response: + data: + you: + name: "Ada Lovelace" + +- type: Request + description: "Proper error when sending malformed request body" + request: "" + expected_response: + errors: + - message: "Invalid GraphQL request" + extensions: + code: INVALID_GRAPHQL_REQUEST + details: 'failed to deserialize the request body into JSON: invalid type: string "", expected a GraphQL request at line 1 column 2' + +- type: Stop diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/rhai/main.rhai b/apollo-router/tests/samples/enterprise/persisted-queries/basic/rhai/main.rhai new file mode 100644 index 0000000000..b31e749479 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/rhai/main.rhai @@ -0,0 +1,7 @@ +fn router_service(service) { + service.map_request(|request| { + if (request.headers.contains("plugin-client-name")) { + request.context["apollo_persisted_queries::client_name"] = request.headers["plugin-client-name"]; + } + }); +} diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/supergraph.graphql b/apollo-router/tests/samples/enterprise/persisted-queries/basic/supergraph.graphql new file mode 100644 index 0000000000..c5a920730a --- /dev/null +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/supergraph.graphql @@ -0,0 +1,124 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + +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 +) 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 + +scalar join__FieldSet +scalar link__Import + +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/") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + author: User @join__field(graph: REVIEWS, provides: "username") + body: String @join__field(graph: REVIEWS) + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: [Review] @join__field(graph: REVIEWS) +} diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 8159a40179..112f5ebff2 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -56,7 +56,7 @@ fn lookup_dir( path.file_name().unwrap().to_str().unwrap() ); - if path.join("plan.json").exists() { + let plan: Option = if path.join("plan.json").exists() { let mut file = File::open(path.join("plan.json")).map_err(|e| { format!( "could not open file at path '{:?}': {e}", @@ -71,8 +71,8 @@ fn lookup_dir( ) })?; - let plan: Plan = match serde_json::from_str(&s) { - Ok(data) => data, + match serde_json::from_str(&s) { + Ok(data) => Some(data), Err(e) => { return Err(format!( "could not deserialize test plan at {}: {e}", @@ -80,8 +80,37 @@ fn lookup_dir( ) .into()); } - }; + } + } else if path.join("plan.yaml").exists() { + let mut file = File::open(path.join("plan.yaml")).map_err(|e| { + format!( + "could not open file at path '{:?}': {e}", + &path.join("plan.yaml") + ) + })?; + let mut s = String::new(); + file.read_to_string(&mut s).map_err(|e| { + format!( + "could not read file at path: '{:?}': {e}", + &path.join("plan.yaml") + ) + })?; + match serde_yaml::from_str(&s) { + Ok(data) => Some(data), + Err(e) => { + return Err(format!( + "could not deserialize test plan at {}: {e}", + path.display() + ) + .into()); + } + } + } else { + None + }; + + if let Some(plan) = plan { if plan.enterprise && !(std::env::var("TEST_APOLLO_KEY").is_ok() && std::env::var("TEST_APOLLO_GRAPH_REF").is_ok()) @@ -172,6 +201,7 @@ impl TestExecution { .await } Action::Request { + description, request, query_path, headers, @@ -179,6 +209,7 @@ impl TestExecution { expected_headers, } => { self.request( + description.clone(), request.clone(), query_path.as_deref(), headers, @@ -429,6 +460,7 @@ impl TestExecution { #[allow(clippy::too_many_arguments)] async fn request( &mut self, + description: Option, mut request: Value, query_path: Option<&str>, headers: &HashMap, @@ -456,6 +488,11 @@ impl TestExecution { } } + writeln!(out, "").unwrap(); + if let Some(description) = description { + writeln!(out, "description: {description}").unwrap(); + } + writeln!(out, "query: {}\n", serde_json::to_string(&request).unwrap()).unwrap(); writeln!(out, "header: {:?}\n", headers).unwrap(); @@ -660,6 +697,7 @@ fn check_path(path: &Path, out: &mut String) -> Result<(), Failed> { #[derive(Deserialize)] #[allow(dead_code)] +#[serde(deny_unknown_fields)] struct Plan { #[serde(default)] enterprise: bool, @@ -669,7 +707,7 @@ struct Plan { } #[derive(Deserialize)] -#[serde(tag = "type")] +#[serde(tag = "type", deny_unknown_fields)] enum Action { Start { schema_path: String, @@ -689,6 +727,7 @@ enum Action { update_url_overrides: bool, }, Request { + description: Option, request: Value, query_path: Option, #[serde(default)] @@ -705,17 +744,20 @@ enum Action { } #[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct Subgraph { requests: Vec, } #[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct SubgraphRequestMock { request: HttpRequest, response: HttpResponse, } #[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct HttpRequest { method: Option, path: Option, @@ -725,6 +767,7 @@ struct HttpRequest { } #[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct HttpResponse { status: Option, #[serde(default)] From e3fb191f89e2dbe2849c4f4cab474588826ff64d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 27 Nov 2024 15:09:51 -0800 Subject: [PATCH 072/112] remove old test --- apollo-router/tests/integration_tests.rs | 234 ----------------------- 1 file changed, 234 deletions(-) diff --git a/apollo-router/tests/integration_tests.rs b/apollo-router/tests/integration_tests.rs index b944448145..725bfea90f 100644 --- a/apollo-router/tests/integration_tests.rs +++ b/apollo-router/tests/integration_tests.rs @@ -401,210 +401,6 @@ async fn automated_persisted_queries() { assert_eq!(registry.totals(), expected_service_hits); } -#[tokio::test(flavor = "multi_thread")] -async fn persisted_queries() { - use hyper::header::HeaderValue; - use serde_json::json; - - /// Construct a persisted query request from an ID. - fn pq_request(persisted_query_id: &str, header: Option<(&str, &str)>) -> router::Request { - let mut builder = supergraph::Request::fake_builder().extension( - "persistedQuery", - json!({ - "version": 1, - "sha256Hash": persisted_query_id - }), - ); - if let Some((name, value)) = header { - builder = builder.header(name, value); - } - builder - .build() - .expect("expecting valid request") - .try_into() - .expect("could not convert supergraph::Request to router::Request") - } - - // set up a PQM with one query and one client-specific query - const PERSISTED_QUERY_ID: &str = "GetMyNameID"; - const PERSISTED_QUERY_BODY: &str = "query GetMyName { me { name } }"; - const PERSISTED_QUERY_MOBILE_BODY: &str = "query GetMyName { me { aliased: name } }"; - let expected_data = serde_json_bytes::json!({ - "me": { - "name": "Ada Lovelace" - } - }); - let expected_data_mobile = serde_json_bytes::json!({ - "me": { - "aliased": "Ada Lovelace" - } - }); - - let (_mock_guard, uplink_config) = mock_pq_uplink(&hashmap! { - FullPersistedQueryOperationId { - operation_id: PERSISTED_QUERY_ID.to_string(), - client_name: None, - } => PERSISTED_QUERY_BODY.to_string(), - FullPersistedQueryOperationId { - operation_id: PERSISTED_QUERY_ID.to_string(), - client_name: Some("mobile".to_string()), - } => PERSISTED_QUERY_MOBILE_BODY.to_string(), - }) - .await; - - let config = serde_json::json!({ - "persisted_queries": { - "enabled": true - }, - "apq": { - "enabled": false - } - }); - - let mut config: Configuration = serde_json::from_value(config).unwrap(); - config.uplink = Some(uplink_config); - let (router, registry) = setup_router_and_registry_with_config(config).await.unwrap(); - - // Successfully run a persisted query. - let actual = query_with_router(router.clone(), pq_request(PERSISTED_QUERY_ID, None)).await; - assert!(actual.errors.is_empty()); - assert_eq!(actual.data.as_ref(), Some(&expected_data)); - assert_eq!( - registry.totals_reset(), - hashmap! {"accounts".to_string() => 1} - ); - - // Successfully run a persisted query with client name that has its own operation. - let actual = query_with_router( - router.clone(), - pq_request( - PERSISTED_QUERY_ID, - Some(("apollographql-client-name", "mobile")), - ), - ) - .await; - assert!(actual.errors.is_empty()); - assert_eq!(actual.data.as_ref(), Some(&expected_data_mobile)); - assert_eq!( - registry.totals_reset(), - hashmap! {"accounts".to_string() => 1} - ); - - // Successfully run a persisted query with client name that has its own operation, - // setting the client name via context in a plugin. - let actual = query_with_router( - router.clone(), - pq_request(PERSISTED_QUERY_ID, Some(("pq-client-name", "mobile"))), - ) - .await; - assert!(actual.errors.is_empty()); - assert_eq!(actual.data.as_ref(), Some(&expected_data_mobile)); - assert_eq!( - registry.totals_reset(), - hashmap! {"accounts".to_string() => 1} - ); - - // Successfully run a persisted query with random client name falling back - // to the version without client name. - let actual = query_with_router( - router.clone(), - pq_request( - PERSISTED_QUERY_ID, - Some(("apollographql-client-name", "something-not-mobile")), - ), - ) - .await; - assert!(actual.errors.is_empty()); - assert_eq!(actual.data.as_ref(), Some(&expected_data)); - assert_eq!( - registry.totals_reset(), - hashmap! {"accounts".to_string() => 1} - ); - - // Error on unknown persisted query ID. - const UNKNOWN_QUERY_ID: &str = "unknown_query"; - const UNPERSISTED_QUERY_BODY: &str = "query GetYourName { you: me { name } }"; - let expected_data = serde_json_bytes::json!({ - "you": { - "name": "Ada Lovelace" - } - }); - let actual = query_with_router(router.clone(), pq_request(UNKNOWN_QUERY_ID, None)).await; - assert_eq!( - actual.errors, - vec![apollo_router::graphql::Error::builder() - .message(format!( - "Persisted query '{UNKNOWN_QUERY_ID}' not found in the persisted query list" - )) - .extension_code("PERSISTED_QUERY_NOT_IN_LIST") - .build()] - ); - assert_eq!(actual.data, None); - assert_eq!(registry.totals_reset(), hashmap! {}); - - // We didn't break normal GETs. - let actual = query_with_router( - router.clone(), - supergraph::Request::fake_builder() - .query(UNPERSISTED_QUERY_BODY) - .method(Method::GET) - .build() - .unwrap() - .try_into() - .unwrap(), - ) - .await; - assert!(actual.errors.is_empty()); - assert_eq!(actual.data.as_ref(), Some(&expected_data)); - assert_eq!( - registry.totals_reset(), - hashmap! {"accounts".to_string() => 1} - ); - - // We didn't break normal POSTs. - let actual = query_with_router( - router.clone(), - supergraph::Request::fake_builder() - .query(UNPERSISTED_QUERY_BODY) - .method(Method::POST) - .build() - .unwrap() - .try_into() - .unwrap(), - ) - .await; - assert!(actual.errors.is_empty()); - assert_eq!(actual.data, Some(expected_data)); - assert_eq!( - registry.totals_reset(), - hashmap! {"accounts".to_string() => 1} - ); - - // Proper error when sending malformed request body - let actual = query_with_router( - router.clone(), - http::Request::builder() - .uri("http://default") - .method(Method::POST) - .header( - CONTENT_TYPE, - HeaderValue::from_static(APPLICATION_JSON.essence_str()), - ) - .body(router::Body::empty()) - .unwrap() - .into(), - ) - .await; - assert_eq!(actual.errors.len(), 1); - - assert_eq!(actual.errors[0].message, "Invalid GraphQL request"); - assert_eq!( - actual.errors[0].extensions["code"], - "INVALID_GRAPHQL_REQUEST" - ); - assert_eq!(registry.totals_reset(), hashmap! {}); -} - #[tokio::test(flavor = "multi_thread")] async fn missing_variables() { let request = supergraph::Request::fake_builder() @@ -1273,7 +1069,6 @@ async fn setup_router_and_registry_with_config( .configuration(Arc::new(config)) .schema(include_str!("fixtures/supergraph.graphql")) .extra_plugin(counting_registry.clone()) - .extra_plugin(PQClientNamePlugin) .build_router() .await?; Ok((router, counting_registry)) @@ -1373,35 +1168,6 @@ impl Plugin for CountingServiceRegistry { } } -#[derive(Debug, Clone)] -struct PQClientNamePlugin; - -#[async_trait::async_trait] -impl Plugin for PQClientNamePlugin { - type Config = (); - - async fn new(_: PluginInit) -> Result { - unreachable!() - } - - fn router_service(&self, service: router::BoxService) -> router::BoxService { - service - .map_request(move |request: router::Request| { - if let Some(v) = request.router_request.headers().get("pq-client-name") { - request - .context - .insert( - "apollo_persisted_queries::client_name", - v.to_str().unwrap().to_string(), - ) - .unwrap(); - } - request - }) - .boxed() - } -} - #[tokio::test(flavor = "multi_thread")] async fn all_stock_router_example_yamls_are_valid() { let example_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../examples"); From 09f7715e8928ba77f3e89f21f1fa0b5490eb1e1a Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 27 Nov 2024 15:30:35 -0800 Subject: [PATCH 073/112] undo no-longer-necessary changes --- apollo-router/src/plugins/telemetry/apollo.rs | 2 +- apollo-router/tests/integration_tests.rs | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/apollo.rs b/apollo-router/src/plugins/telemetry/apollo.rs index 2fcdcc1dc7..89c1ecd8e8 100644 --- a/apollo-router/src/plugins/telemetry/apollo.rs +++ b/apollo-router/src/plugins/telemetry/apollo.rs @@ -176,7 +176,7 @@ fn otlp_endpoint_default() -> Url { Url::parse(OTLP_ENDPOINT_DEFAULT).expect("must be valid url") } -pub(crate) const fn client_name_header_default_str() -> &'static str { +const fn client_name_header_default_str() -> &'static str { "apollographql-client-name" } diff --git a/apollo-router/tests/integration_tests.rs b/apollo-router/tests/integration_tests.rs index 725bfea90f..4346ba2f71 100644 --- a/apollo-router/tests/integration_tests.rs +++ b/apollo-router/tests/integration_tests.rs @@ -1133,15 +1133,6 @@ impl CountingServiceRegistry { fn totals(&self) -> HashMap { self.counts.lock().unwrap().clone() } - - /// Like `totals`, but clears the counters, so that each assertion is - /// independent of each other. - fn totals_reset(&self) -> HashMap { - let mut totals = self.counts.lock().unwrap(); - let ret = totals.clone(); - totals.clear(); - ret - } } #[async_trait::async_trait] From 2b0befa2da0df3e10d8cc0dbe453869ea23cf6af Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 27 Nov 2024 15:37:49 -0800 Subject: [PATCH 074/112] Fix unit tests --- ...outer__configuration__tests__schema_generation.snap | 2 +- .../src/services/layers/persisted_queries/mod.rs | 10 ++++++++-- .../src/test_harness/mocks/persisted_queries.rs | 9 +++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index db8f3f8851..36d76b53ec 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -1747,7 +1747,7 @@ expression: "&schema" }, "client_name_header": { "default": "apollographql-client-name", - "description": "The name of the header to extract from requests when populating 'client nane' for traces and metrics in Apollo Studio.", + "description": "The name of the header to extract from requests when populating 'client name' for traces and metrics in Apollo Studio.", "nullable": true, "type": "string" }, diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 80614663ce..4079dd327f 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -706,11 +706,17 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn pq_layer_freeform_graphql_with_safelist() { let manifest = HashMap::from([( - "valid-syntax".to_string(), + FullPersistedQueryOperationId { + operation_id: "valid-syntax".to_string(), + client_name: None, + }, "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah" .to_string(), ), ( - "invalid-syntax".to_string(), + FullPersistedQueryOperationId { + operation_id: "invalid-syntax".to_string(), + client_name: None, + }, "}}}".to_string()), ]); diff --git a/apollo-router/src/test_harness/mocks/persisted_queries.rs b/apollo-router/src/test_harness/mocks/persisted_queries.rs index 3db8d53932..7d513305bf 100644 --- a/apollo-router/src/test_harness/mocks/persisted_queries.rs +++ b/apollo-router/src/test_harness/mocks/persisted_queries.rs @@ -20,10 +20,15 @@ use crate::uplink::Endpoints; use crate::uplink::UplinkConfig; /// Get a query ID, body, and a PQ manifest with that ID and body. -pub fn fake_manifest() -> (String, String, HashMap) { +pub fn fake_manifest() -> (String, String, PersistedQueryManifest) { let id = "1234".to_string(); let body = r#"query { typename }"#.to_string(); - let manifest = hashmap! { id.to_string() => body.to_string() }; + let manifest = hashmap! { + FullPersistedQueryOperationId { + operation_id: id.to_string(), + client_name: None, + } => body.to_string() + }; (id, body, manifest) } From 9506cd6ea226871da2dbdee16cf641dac3524c3e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 27 Nov 2024 15:40:17 -0800 Subject: [PATCH 075/112] carg fmt --- apollo-router/src/services/layers/persisted_queries/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 4079dd327f..6f38a38dc9 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -127,7 +127,7 @@ impl PersistedQueryLayer { .context .get(PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY) .unwrap_or_default() - .or_else(|| request.context.get(CLIENT_NAME).unwrap_or_default()) + .or_else(|| request.context.get(CLIENT_NAME).unwrap_or_default()), ) { let body = request.supergraph_request.body_mut(); body.query = Some(persisted_query_body); From f0d53cdec5d1d929431655de6b76d0fae7a65342 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 27 Nov 2024 15:43:34 -0800 Subject: [PATCH 076/112] lint --- apollo-router/tests/samples_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 112f5ebff2..98782558bb 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -488,7 +488,7 @@ impl TestExecution { } } - writeln!(out, "").unwrap(); + writeln!(out).unwrap(); if let Some(description) = description { writeln!(out, "description: {description}").unwrap(); } From 3b4e9ca9efb358590318850e6b93fbc4e1f6fd3a Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 2 Dec 2024 17:51:40 -0800 Subject: [PATCH 077/112] unit tests --- .../services/layers/persisted_queries/mod.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 6f38a38dc9..0208b2f878 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -405,6 +405,7 @@ mod tests { use std::collections::HashMap; use std::time::Duration; + use maplit::hashmap; use serde_json::json; use super::*; @@ -416,6 +417,7 @@ mod tests { use crate::services::layers::query_analysis::QueryAnalysisLayer; use crate::spec::Schema; use crate::test_harness::mocks::persisted_queries::*; + use crate::Context; #[tokio::test(flavor = "multi_thread")] async fn disabled_pq_layer_has_no_poller() { @@ -495,6 +497,84 @@ mod tests { assert_eq!(request.supergraph_request.body().query, Some(body)); } + #[tokio::test(flavor = "multi_thread")] + async fn enabled_pq_layer_with_client_names() { + let (_mock_guard, uplink_config) = mock_pq_uplink(&hashmap! { + FullPersistedQueryOperationId { + operation_id: "both-plain-and-cliented".to_string(), + client_name: None, + } => "query { bpac_no_client: __typename }".to_string(), + FullPersistedQueryOperationId { + operation_id: "both-plain-and-cliented".to_string(), + client_name: Some("web".to_string()), + } => "query { bpac_web_client: __typename }".to_string(), + FullPersistedQueryOperationId { + operation_id: "only-cliented".to_string(), + client_name: Some("web".to_string()), + } => "query { oc_web_client: __typename }".to_string(), + }) + .await; + + let pq_layer = PersistedQueryLayer::new( + &Configuration::fake_builder() + .persisted_query(PersistedQueries::builder().enabled(true).build()) + .uplink(uplink_config) + .build() + .unwrap(), + ) + .await + .unwrap(); + + let map_to_query = |operation_id: &str, client_name: Option<&str>| -> Option { + let context = Context::new(); + if let Some(client_name) = client_name { + context + .insert( + PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY, + client_name.to_string(), + ) + .unwrap(); + } + + let incoming_request = SupergraphRequest::fake_builder() + .extension( + "persistedQuery", + json!({"version": 1, "sha256Hash": operation_id.to_string()}), + ) + .context(context) + .build() + .unwrap(); + + pq_layer + .supergraph_request(incoming_request) + .ok() + .expect("pq layer returned response instead of putting the query on the request") + .supergraph_request + .body() + .query + .clone() + }; + + assert_eq!( + map_to_query("both-plain-and-cliented", None), + Some("query { bpac_no_client: __typename }".to_string()) + ); + assert_eq!( + map_to_query("both-plain-and-cliented", Some("not-web")), + Some("query { bpac_no_client: __typename }".to_string()) + ); + assert_eq!( + map_to_query("both-plain-and-cliented", Some("web")), + Some("query { bpac_web_client: __typename }".to_string()) + ); + assert_eq!( + map_to_query("only-cliented", Some("web")), + Some("query { oc_web_client: __typename }".to_string()) + ); + assert_eq!(map_to_query("only-cliented", None), None); + assert_eq!(map_to_query("only-cliented", Some("not-web")), None); + } + #[tokio::test(flavor = "multi_thread")] async fn pq_layer_passes_on_to_apq_layer_when_id_not_found() { let (_id, _body, manifest) = fake_manifest(); From 92172c9ad951ac58bbd7a20abea45f06952e8f50 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 3 Dec 2024 17:21:37 -0800 Subject: [PATCH 078/112] Docs and changeset --- .changesets/feat_glasser_pq_client_name.md | 13 +++++++++++++ .../routing/observability/client-awareness.mdx | 2 ++ docs/source/routing/security/persisted-queries.mdx | 14 ++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 .changesets/feat_glasser_pq_client_name.md diff --git a/.changesets/feat_glasser_pq_client_name.md b/.changesets/feat_glasser_pq_client_name.md new file mode 100644 index 0000000000..42157835d3 --- /dev/null +++ b/.changesets/feat_glasser_pq_client_name.md @@ -0,0 +1,13 @@ +### Client name support for Persisted Query Lists ([PR #6198](https://github.com/apollographql/router/pull/6198)) + +The persisted query manifest fetched from Uplink can now contain a `clientName` field in each operation. Two operations with the same `id` but different `clientName` are considered to be distinct operations (and may have distinct bodies). + +Router resolves the client name by taking the first of these which exists: +- Reading the `apollo_persisted_queries::client_name` context key (which may be set by a `router_service` plugin) +- Reading the HTTP header named by `telemetry.apollo.client_name_header` (which defaults to `apollographql-client-name`) + +If a client name can be resolved for a request, Router first tries to find a persisted query with the specified ID and the resolved client name. + +If there is no operation with that ID and client name, or if a client name cannot be resolved, Router tries to find a persisted query with the specified ID and no client name specified. (This means that existing PQ lists that do not contain client names will continue to work.) + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6198 \ No newline at end of file diff --git a/docs/source/routing/observability/client-awareness.mdx b/docs/source/routing/observability/client-awareness.mdx index f7660f85a8..c54a8b3607 100644 --- a/docs/source/routing/observability/client-awareness.mdx +++ b/docs/source/routing/observability/client-awareness.mdx @@ -8,6 +8,8 @@ import { Link } from "gatsby"; The GraphOS Router and Apollo Router Core support [client awareness](/graphos/metrics/client-awareness/) by default. If the client sets the headers `apollographql-client-name` and `apollographql-client-version` in its HTTP requests, GraphOS Studio can separate the metrics and operations per client. +This client name is also used by the [Persisted Queries](/docs/graphos/routing/security/persisted-queries.mdx) feature. + ## Overriding client awareness headers Different header names can be used by updating the configuration file. If those headers will be sent by a browser, they must be allowed in the [CORS (Cross Origin Resource Sharing) configuration](/router/configuration/cors), as follows: diff --git a/docs/source/routing/security/persisted-queries.mdx b/docs/source/routing/security/persisted-queries.mdx index 5befd7071c..3ff98eac18 100644 --- a/docs/source/routing/security/persisted-queries.mdx +++ b/docs/source/routing/security/persisted-queries.mdx @@ -138,6 +138,20 @@ To enable safelisting, you _must_ turn off [automatic persisted queries](/router +### Customization via request context + +GraphOS Router can be [customized](/graphos/routing/customization/overview) via several mechanisms such as [Rhai scripts](/graphos/routing/customization/rhai) and [coprocessors](/docs/graphos/routing/customization/coprocessor). These plugins can affect your router's persistent query processing by writing to the request context. + +#### `apollo_persisted_queries::client_name` + +When publishing operations to a PQL, you can specify a client name associated with the operation (by including a `clientName` field in the individual operation in your [manifest](/docs/graphos/platform/security/persisted-queries#per-operation-properties), or by including the `--for-client-name` option to `rover persisted-queries publish`). If an operation has a client name, it will only be executed by requests that specify that client name. (Your PQL can contain multiple operations with the same ID and different client names.) + +Your customization (Rhai script, coprocessor, etc) can examine a request during the [Router Service stage](/docs/graphos/routing/customization/overview#request-path) of the request path and set the `apollo_persisted_queries::client_name` value in the request context to the request's client name. + +If this context value is not set by a customization, your router will use the same client name used for [client awareness](/docs/graphos/routing/observability/client-awareness) in observability. This client name is read from an HTTP header specified by `telemetry.apollo.client_name_header`, or `apollographql-client-name` by default. + +If your request specifies an ID and a client name but there is no operation in the PQL with that ID and client name, your router will look to see if there is an operation with that ID and no client name specified, and use that if it finds it. + ## Limitations * **Unsupported with offline license**. An GraphOS Router using an [offline Enterprise license](/router/enterprise-features/#offline-enterprise-license) cannot use safelisting with persisted queries. The feature relies on Apollo Uplink to fetch persisted query manifests, so it doesn't work as designed when the router is disconnected from Uplink. From 54e79ca75078826fe747460293e1e97dc1e17d75 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 5 Dec 2024 14:12:36 -0800 Subject: [PATCH 079/112] update link in docs --- docs/source/routing/observability/client-awareness.mdx | 2 +- docs/source/routing/security/persisted-queries.mdx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/routing/observability/client-awareness.mdx b/docs/source/routing/observability/client-awareness.mdx index c54a8b3607..8b5f566aef 100644 --- a/docs/source/routing/observability/client-awareness.mdx +++ b/docs/source/routing/observability/client-awareness.mdx @@ -8,7 +8,7 @@ import { Link } from "gatsby"; The GraphOS Router and Apollo Router Core support [client awareness](/graphos/metrics/client-awareness/) by default. If the client sets the headers `apollographql-client-name` and `apollographql-client-version` in its HTTP requests, GraphOS Studio can separate the metrics and operations per client. -This client name is also used by the [Persisted Queries](/docs/graphos/routing/security/persisted-queries.mdx) feature. +This client name is also used by the [Persisted Queries](/graphos/routing/security/persisted-queries) feature. ## Overriding client awareness headers diff --git a/docs/source/routing/security/persisted-queries.mdx b/docs/source/routing/security/persisted-queries.mdx index 3ff98eac18..845e0aa75d 100644 --- a/docs/source/routing/security/persisted-queries.mdx +++ b/docs/source/routing/security/persisted-queries.mdx @@ -140,15 +140,15 @@ To enable safelisting, you _must_ turn off [automatic persisted queries](/router ### Customization via request context -GraphOS Router can be [customized](/graphos/routing/customization/overview) via several mechanisms such as [Rhai scripts](/graphos/routing/customization/rhai) and [coprocessors](/docs/graphos/routing/customization/coprocessor). These plugins can affect your router's persistent query processing by writing to the request context. +GraphOS Router can be [customized](/graphos/routing/customization/overview) via several mechanisms such as [Rhai scripts](/graphos/routing/customization/rhai) and [coprocessors](/graphos/routing/customization/coprocessor). These plugins can affect your router's persistent query processing by writing to the request context. #### `apollo_persisted_queries::client_name` -When publishing operations to a PQL, you can specify a client name associated with the operation (by including a `clientName` field in the individual operation in your [manifest](/docs/graphos/platform/security/persisted-queries#per-operation-properties), or by including the `--for-client-name` option to `rover persisted-queries publish`). If an operation has a client name, it will only be executed by requests that specify that client name. (Your PQL can contain multiple operations with the same ID and different client names.) +When publishing operations to a PQL, you can specify a client name associated with the operation (by including a `clientName` field in the individual operation in your [manifest](/graphos/platform/security/persisted-queries#per-operation-properties), or by including the `--for-client-name` option to `rover persisted-queries publish`). If an operation has a client name, it will only be executed by requests that specify that client name. (Your PQL can contain multiple operations with the same ID and different client names.) -Your customization (Rhai script, coprocessor, etc) can examine a request during the [Router Service stage](/docs/graphos/routing/customization/overview#request-path) of the request path and set the `apollo_persisted_queries::client_name` value in the request context to the request's client name. +Your customization (Rhai script, coprocessor, etc) can examine a request during the [Router Service stage](/graphos/routing/customization/overview#request-path) of the request path and set the `apollo_persisted_queries::client_name` value in the request context to the request's client name. -If this context value is not set by a customization, your router will use the same client name used for [client awareness](/docs/graphos/routing/observability/client-awareness) in observability. This client name is read from an HTTP header specified by `telemetry.apollo.client_name_header`, or `apollographql-client-name` by default. +If this context value is not set by a customization, your router will use the same client name used for [client awareness](/graphos/routing/observability/client-awareness) in observability. This client name is read from an HTTP header specified by `telemetry.apollo.client_name_header`, or `apollographql-client-name` by default. If your request specifies an ID and a client name but there is no operation in the PQL with that ID and client name, your router will look to see if there is an operation with that ID and no client name specified, and use that if it finds it. From 8b1fe2a9261be31d0a808eea05de6f010a9b7ec2 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 4 Dec 2024 17:09:00 -0800 Subject: [PATCH 080/112] [ROUTER-890] Ability to skip safelisting enforcement via plugin If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. (This does not affect the logging of unknown operations by the `persisted_queries.log_unknown` option.) In cases where an operation would have been denied but is allowed due to the context key existing, the attribute `persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value true. This PR improves the testing of that metric as well. When writing the tests, I discovered that the `persisted_queries.safelist.rejected.unknown` attribute had its value set to `false` when the operation is denied but not logged, and to `true` when denied and logged. (You can also tell whether it is logged via the `persisted_queries.logged` attribute.) This dated to the creation of this metric in #3609 and seems to be a mistake. This PR normalizes this attribute to always be `true` if it is set. The metric was described as unstable when released in v1.28.1, so this seems reasonable. --- ...glasser_pq_metric_attribute_consistency.md | 9 + .../feat_glasser_pq_safelist_override.md | 10 + apollo-router/src/metrics/mod.rs | 13 +- .../persisted_queries/manifest_poller.rs | 51 +-- .../services/layers/persisted_queries/mod.rs | 380 ++++++++++++------ ...l_with_safelist_log_unknown_true@logs.snap | 21 + dev-docs/logging.md | 4 +- dev-docs/metrics.md | 4 +- .../routing/security/persisted-queries.mdx | 14 +- 9 files changed, 346 insertions(+), 160 deletions(-) create mode 100644 .changesets/breaking_glasser_pq_metric_attribute_consistency.md create mode 100644 .changesets/feat_glasser_pq_safelist_override.md create mode 100644 apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap diff --git a/.changesets/breaking_glasser_pq_metric_attribute_consistency.md b/.changesets/breaking_glasser_pq_metric_attribute_consistency.md new file mode 100644 index 0000000000..87c320e4ad --- /dev/null +++ b/.changesets/breaking_glasser_pq_metric_attribute_consistency.md @@ -0,0 +1,9 @@ +### More consistent attributes on `apollo.router.operations.persisted_queries` metric ([PR #6403](https://github.com/apollographql/router/pull/6403)) + +Version 1.28.1 added several *unstable* metrics, including `apollo.router.operations.persisted_queries`. + +When an operation is rejected, Router includes a `persisted_queries.safelist.rejected.unknown` attribute on the metric. Previously, this attribute had the value `true` if the operation is logged (via `log_unknown`), and `false` if the operation is not logged. (The attribute is not included at all if the operation is not rejected.) This appears to have been a mistake, as you can also tell whether it is logged via the `persisted_queries.logged` attribute. + +Router now only sets this attribute to true, and never to false. This may be a breaking change for your use of metrics; note that these metrics should be treated as unstable and may change in the future. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 diff --git a/.changesets/feat_glasser_pq_safelist_override.md b/.changesets/feat_glasser_pq_safelist_override.md new file mode 100644 index 0000000000..316febbca9 --- /dev/null +++ b/.changesets/feat_glasser_pq_safelist_override.md @@ -0,0 +1,10 @@ +### Ability to skip Persisted Query List safelisting enforcement via plugin ([PR #6403](https://github.com/apollographql/router/pull/6403)) + +If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. + +(This does not affect the logging of unknown operations by the `persisted_queries.log_unknown` option.) + +In cases where an operation would have been denied but is allowed due to the context key existing, the attribute +`persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value true. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 \ No newline at end of file diff --git a/apollo-router/src/metrics/mod.rs b/apollo-router/src/metrics/mod.rs index 4b2aa0b888..1cb90bc2ae 100644 --- a/apollo-router/src/metrics/mod.rs +++ b/apollo-router/src/metrics/mod.rs @@ -525,9 +525,10 @@ pub(crate) fn meter_provider() -> AggregateMeterProvider { /// frobbles.color = "blue" /// ); /// // Count a thing with dynamic attributes: -/// let attributes = [ -/// opentelemetry::KeyValue::new("frobbles.color".to_string(), "blue".into()), -/// ]; +/// let attributes = vec![]; +/// if (frobbled) { +/// attributes.push(opentelemetry::KeyValue::new("frobbles.color".to_string(), "blue".into())); +/// } /// u64_counter!( /// "apollo.router.operations.frobbles", /// "The amount of frobbles we've operated on", @@ -939,6 +940,11 @@ macro_rules! assert_counter { assert_metric!(result, $name, Some($value.into()), None, &attributes); }; + ($name:literal, $value: expr, $attributes: expr) => { + let result = crate::metrics::collect_metrics().assert($name, crate::metrics::test_utils::MetricType::Counter, $value, $attributes); + assert_metric!(result, $name, Some($value.into()), None, &$attributes); + }; + ($name:literal, $value: expr) => { let result = crate::metrics::collect_metrics().assert($name, crate::metrics::test_utils::MetricType::Counter, $value, &[]); assert_metric!(result, $name, Some($value.into()), None, &[]); @@ -1209,6 +1215,7 @@ mod test { let attributes = vec![KeyValue::new("attr", "val")]; u64_counter!("test", "test description", 1, attributes); assert_counter!("test", 1, "attr" = "val"); + assert_counter!("test", 1, &attributes); } #[test] diff --git a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs index 496c98d0a7..611c68b832 100644 --- a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs +++ b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs @@ -34,6 +34,13 @@ pub struct FullPersistedQueryOperationId { /// An in memory cache of persisted queries. pub type PersistedQueryManifest = HashMap; +/// Describes whether the router should allow or deny a given request. +/// with an error, or allow it but log the operation as unknown. +pub(crate) struct FreeformGraphQLAction { + pub(crate) should_allow: bool, + pub(crate) should_log: bool, +} + /// How the router should respond to requests that are not resolved as the IDs /// of an operation in the manifest. (For the most part this means "requests /// sent as freeform GraphQL", though it also includes requests sent as an ID @@ -58,49 +65,43 @@ pub(crate) enum FreeformGraphQLBehavior { }, } -/// Describes what the router should do for a given request: allow it, deny it -/// with an error, or allow it but log the operation as unknown. -pub(crate) enum FreeformGraphQLAction { - Allow, - Deny, - AllowAndLog, - DenyAndLog, -} - impl FreeformGraphQLBehavior { fn action_for_freeform_graphql( &self, ast: Result<&ast::Document, &str>, ) -> FreeformGraphQLAction { match self { - FreeformGraphQLBehavior::AllowAll { .. } => FreeformGraphQLAction::Allow, + FreeformGraphQLBehavior::AllowAll { .. } => FreeformGraphQLAction { + should_allow: true, + should_log: false, + }, // Note that this branch doesn't get called in practice, because we catch // DenyAll at an earlier phase with never_allows_freeform_graphql. - FreeformGraphQLBehavior::DenyAll { log_unknown, .. } => { - if *log_unknown { - FreeformGraphQLAction::DenyAndLog - } else { - FreeformGraphQLAction::Deny - } - } + FreeformGraphQLBehavior::DenyAll { log_unknown, .. } => FreeformGraphQLAction { + should_allow: false, + should_log: *log_unknown, + }, FreeformGraphQLBehavior::AllowIfInSafelist { safelist, log_unknown, .. } => { if safelist.is_allowed(ast) { - FreeformGraphQLAction::Allow - } else if *log_unknown { - FreeformGraphQLAction::DenyAndLog + FreeformGraphQLAction { + should_allow: true, + should_log: false, + } } else { - FreeformGraphQLAction::Deny + FreeformGraphQLAction { + should_allow: false, + should_log: *log_unknown, + } } } FreeformGraphQLBehavior::LogUnlessInSafelist { safelist, .. } => { - if safelist.is_allowed(ast) { - FreeformGraphQLAction::Allow - } else { - FreeformGraphQLAction::AllowAndLog + FreeformGraphQLAction { + should_allow: true, + should_log: !safelist.is_allowed(ast), } } } diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 0208b2f878..ae849a6170 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -13,7 +13,6 @@ pub use manifest_poller::PersistedQueryManifest; pub(crate) use manifest_poller::PersistedQueryManifestPoller; use tower::BoxError; -use self::manifest_poller::FreeformGraphQLAction; use super::query_analysis::ParsedDocument; use crate::graphql::Error as GraphQLError; use crate::plugins::telemetry::CLIENT_NAME; @@ -23,6 +22,8 @@ use crate::Configuration; const DONT_CACHE_RESPONSE_VALUE: &str = "private, no-cache, must-revalidate"; const PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY: &str = "apollo_persisted_queries::client_name"; +const PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY: &str = + "apollo_persisted_queries::safelist::skip_enforcement"; struct UsedQueryIdFromManifest; @@ -34,6 +35,14 @@ pub(crate) struct PersistedQueryLayer { introspection_enabled: bool, } +fn skip_enforcement(request: &SupergraphRequest) -> bool { + request + .context + .get(PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY) + .unwrap_or_default() + .unwrap_or(false) +} + impl PersistedQueryLayer { /// Create a new [`PersistedQueryLayer`] from CLI options, YAML configuration, /// and optionally, an existing persisted query manifest poller. @@ -69,6 +78,9 @@ impl PersistedQueryLayer { manifest_poller, &persisted_query_id, ) + } else if skip_enforcement(&request) { + // A plugin told us to allow this, so let's skip to require_id check. + Ok(request) } else if let Some(log_unknown) = manifest_poller.never_allows_freeform_graphql() { // If we don't have an ID and we require an ID, return an error immediately, if log_unknown { @@ -229,46 +241,39 @@ impl PersistedQueryLayer { return Ok(request); } - match manifest_poller.action_for_freeform_graphql(Ok(&doc.ast)) { - FreeformGraphQLAction::Allow => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1 - ); - Ok(request) - } - FreeformGraphQLAction::Deny => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1, - persisted_queries.safelist.rejected.unknown = false - ); - Err(supergraph_err_operation_not_in_safelist(request)) - } - // Note that this might even include complaining about an operation that came via APQs. - FreeformGraphQLAction::AllowAndLog => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1, - persisted_queries.logged = true - ); - log_unknown_operation(operation_body); - Ok(request) - } - FreeformGraphQLAction::DenyAndLog => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1, - persisted_queries.safelist.rejected.unknown = true, - persisted_queries.logged = true - ); - log_unknown_operation(operation_body); - Err(supergraph_err_operation_not_in_safelist(request)) - } + let mut metric_attributes = vec![]; + let freeform_graphql_action = manifest_poller.action_for_freeform_graphql(Ok(&doc.ast)); + let skip_enforcement = skip_enforcement(&request); + let allow = skip_enforcement || freeform_graphql_action.should_allow; + if !allow { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.safelist.rejected.unknown".to_string(), + true, + )); + } else if !freeform_graphql_action.should_allow { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.safelist.enforcement_skipped".to_string(), + true, + )); + } + if freeform_graphql_action.should_log { + log_unknown_operation(operation_body); + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.logged".to_string(), + true, + )); + } + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1, + metric_attributes + ); + + if allow { + Ok(request) + } else { + Err(supergraph_err_operation_not_in_safelist(request)) } } @@ -370,9 +375,10 @@ fn supergraph_err_operation_not_in_safelist(request: SupergraphRequest) -> Super } fn graphql_err_pq_id_required() -> GraphQLError { - graphql_err("PERSISTED_QUERY_ID_REQUIRED", + graphql_err( + "PERSISTED_QUERY_ID_REQUIRED", "This endpoint does not allow freeform GraphQL requests; operations must be sent by ID in the persisted queries GraphQL extension.", - ) + ) } fn supergraph_err_pq_id_required(request: SupergraphRequest) -> SupergraphResponse { @@ -407,12 +413,15 @@ mod tests { use maplit::hashmap; use serde_json::json; + use tracing::instrument::WithSubscriber; use super::*; + use crate::assert_snapshot_subscriber; use crate::configuration::Apq; use crate::configuration::PersistedQueries; use crate::configuration::PersistedQueriesSafelist; use crate::configuration::Supergraph; + use crate::metrics::FutureMetricsExt; use crate::services::layers::persisted_queries::manifest_poller::FreeformGraphQLBehavior; use crate::services::layers::query_analysis::QueryAnalysisLayer; use crate::spec::Schema; @@ -722,9 +731,21 @@ mod tests { pq_layer: &PersistedQueryLayer, query_analysis_layer: &QueryAnalysisLayer, body: &str, + skip_enforcement: bool, ) -> SupergraphRequest { + let context = Context::new(); + if skip_enforcement { + context + .insert( + PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY, + true, + ) + .unwrap(); + } + let incoming_request = SupergraphRequest::fake_builder() .query(body) + .context(context) .build() .unwrap(); @@ -747,9 +768,11 @@ mod tests { pq_layer: &PersistedQueryLayer, query_analysis_layer: &QueryAnalysisLayer, body: &str, + log_unknown: bool, + counter_value: u64, ) { let request_with_analyzed_query = - run_first_two_layers(pq_layer, query_analysis_layer, body).await; + run_first_two_layers(pq_layer, query_analysis_layer, body, false).await; let mut supergraph_response = pq_layer .supergraph_request_with_analyzed_query(request_with_analyzed_query) @@ -766,119 +789,208 @@ mod tests { response.errors, vec![graphql_err_operation_not_in_safelist()] ); + let mut metric_attributes = vec![opentelemetry::KeyValue::new( + "persisted_queries.safelist.rejected.unknown".to_string(), + true, + )]; + if log_unknown { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.logged".to_string(), + true, + )); + } + assert_counter!( + "apollo.router.operations.persisted_queries", + counter_value, + &metric_attributes + ); } async fn allowed_by_safelist( pq_layer: &PersistedQueryLayer, query_analysis_layer: &QueryAnalysisLayer, body: &str, + log_unknown: bool, + skip_enforcement: bool, + counter_value: u64, ) { let request_with_analyzed_query = - run_first_two_layers(pq_layer, query_analysis_layer, body).await; + run_first_two_layers(pq_layer, query_analysis_layer, body, skip_enforcement).await; pq_layer .supergraph_request_with_analyzed_query(request_with_analyzed_query) .await .ok() .expect("pq layer second hook returned error response instead of returning a request"); - } - #[tokio::test(flavor = "multi_thread")] - async fn pq_layer_freeform_graphql_with_safelist() { - let manifest = HashMap::from([( - FullPersistedQueryOperationId { - operation_id: "valid-syntax".to_string(), - client_name: None, - }, - "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah" - .to_string(), - ), ( - FullPersistedQueryOperationId { - operation_id: "invalid-syntax".to_string(), - client_name: None, - }, - "}}}".to_string()), - ]); + let mut metric_attributes = vec![]; + if skip_enforcement { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.safelist.enforcement_skipped".to_string(), + true, + )); + if log_unknown { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.logged".to_string(), + true, + )); + } + } - let (_mock_guard, uplink_config) = mock_pq_uplink(&manifest).await; + assert_counter!( + "apollo.router.operations.persisted_queries", + counter_value, + &metric_attributes + ); + } - let config = Configuration::fake_builder() - .persisted_query( - PersistedQueries::builder() - .enabled(true) - .safelist(PersistedQueriesSafelist::builder().enabled(true).build()) - .build(), - ) - .uplink(uplink_config) - .apq(Apq::fake_builder().enabled(false).build()) - .supergraph(Supergraph::fake_builder().introspection(true).build()) - .build() - .unwrap(); + async fn pq_layer_freeform_graphql_with_safelist(log_unknown: bool) { + async move { + let manifest = HashMap::from([ + ( + FullPersistedQueryOperationId { + operation_id: "valid-syntax".to_string(), + client_name: None, + }, + "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah" + .to_string(), + ), + ( + FullPersistedQueryOperationId { + operation_id: "invalid-syntax".to_string(), + client_name: None, + }, + "}}}".to_string(), + ), + ]); - let pq_layer = PersistedQueryLayer::new(&config).await.unwrap(); + let (_mock_guard, uplink_config) = mock_pq_uplink(&manifest).await; - let schema = Arc::new( - Schema::parse( - include_str!("../../../testdata/supergraph.graphql"), - &Default::default(), - ) - .unwrap(), - ); + let config = Configuration::fake_builder() + .persisted_query( + PersistedQueries::builder() + .enabled(true) + .safelist(PersistedQueriesSafelist::builder().enabled(true).build()) + .log_unknown(log_unknown) + .build(), + ) + .uplink(uplink_config) + .apq(Apq::fake_builder().enabled(false).build()) + .supergraph(Supergraph::fake_builder().introspection(true).build()) + .build() + .unwrap(); - let query_analysis_layer = QueryAnalysisLayer::new(schema, Arc::new(config)).await; + let pq_layer = PersistedQueryLayer::new(&config).await.unwrap(); - // A random query is blocked. - denied_by_safelist( - &pq_layer, - &query_analysis_layer, - "query SomeQuery { me { id } }", - ) - .await; + let schema = Arc::new(Schema::parse(include_str!("../../../testdata/supergraph.graphql"), &Default::default()).unwrap()); - // The exact string from the manifest is allowed. - allowed_by_safelist( - &pq_layer, - &query_analysis_layer, - "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah", - ) - .await; + let query_analysis_layer = QueryAnalysisLayer::new(schema, Arc::new(config)).await; - // Reordering definitions and reformatting a bit matches. - allowed_by_safelist( + // A random query is blocked. + denied_by_safelist( &pq_layer, &query_analysis_layer, - "#comment\n fragment, B on Query , { me{name username} } query SomeOp { ...A ...B } fragment \nA on Query { me{ id} }" + "query SomeQuery { me { id } }", + log_unknown, + 1, ).await; - // Reordering fields does not match! - denied_by_safelist( + // But it is allowed with skip_enforcement set. + allowed_by_safelist( &pq_layer, &query_analysis_layer, - "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{username,name} } # yeah" + "query SomeQuery { me { id } }", + log_unknown, + true, + 1, ).await; - // Introspection queries are allowed (even using fragments and aliases), because - // introspection is enabled. - allowed_by_safelist( - &pq_layer, - &query_analysis_layer, - r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F }"#, - ).await; - - // Multiple spreads of the same fragment are also allowed - // (https://github.com/apollographql/apollo-rs/issues/613) - allowed_by_safelist( - &pq_layer, - &query_analysis_layer, - r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F ...F }"#, - ).await; - - // But adding any top-level non-introspection field is enough to make it not count as introspection. - denied_by_safelist( - &pq_layer, - &query_analysis_layer, - r#"fragment F on Query { __typename foo: __schema { __typename } me { id } } query Q { __type(name: "foo") { name } ...F }"#, - ).await; + // The exact string from the manifest is allowed. + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah", + log_unknown, + false, + 1, + ) + .await; + + // Reordering definitions and reformatting a bit matches. + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + "#comment\n fragment, B on Query , { me{name username} } query SomeOp { ...A ...B } fragment \nA on Query { me{ id} }", + log_unknown, + false, + 2, + ) + .await; + + // Reordering fields does not match! + denied_by_safelist( + &pq_layer, + &query_analysis_layer, + "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{username,name} } # yeah", + log_unknown, + 2, + ) + .await; + + // Introspection queries are allowed (even using fragments and aliases), because + // introspection is enabled. + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F }"#, + log_unknown, + false, + // Note that introspection queries don't actually interact with the PQ machinery enough + // to update this metric, for better or for worse. + 2, + ) + .await; + + // Multiple spreads of the same fragment are also allowed + // (https://github.com/apollographql/apollo-rs/issues/613) + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F ...F }"#, + log_unknown, + false, + // Note that introspection queries don't actually interact with the PQ machinery enough + // to update this metric, for better or for worse. + 2, + ) + .await; + + // But adding any top-level non-introspection field is enough to make it not count as introspection. + denied_by_safelist( + &pq_layer, + &query_analysis_layer, + r#"fragment F on Query { __typename foo: __schema { __typename } me { id } } query Q { __type(name: "foo") { name } ...F }"#, + log_unknown, + 3, + ) + .await; + } + .with_metrics() + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn pq_layer_freeform_graphql_with_safelist_log_unknown_false() { + pq_layer_freeform_graphql_with_safelist(false).await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn pq_layer_freeform_graphql_with_safelist_log_unknown_true() { + async { + pq_layer_freeform_graphql_with_safelist(true).await; + } + .with_subscriber(assert_snapshot_subscriber!()) + .await } #[tokio::test(flavor = "multi_thread")] @@ -1042,6 +1154,22 @@ mod tests { .await .expect("could not get response from pq layer"); assert_eq!(response.errors, vec![graphql_err_pq_id_required()]); + + // Try again skipping enforcement. + let context = Context::new(); + context + .insert( + PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY, + true, + ) + .unwrap(); + let incoming_request = SupergraphRequest::fake_builder() + .query("query { typename }") + .context(context) + .build() + .unwrap(); + assert!(incoming_request.supergraph_request.body().query.is_some()); + assert!(pq_layer.supergraph_request(incoming_request).is_ok()); } #[tokio::test(flavor = "multi_thread")] diff --git a/apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap b/apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap new file mode 100644 index 0000000000..f9a850f1c8 --- /dev/null +++ b/apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap @@ -0,0 +1,21 @@ +--- +source: apollo-router/src/services/layers/persisted_queries/mod.rs +expression: yaml +snapshot_kind: text +--- +- fields: + operation_body: "query SomeQuery { me { id } }" + level: WARN + message: unknown operation +- fields: + operation_body: "query SomeQuery { me { id } }" + level: WARN + message: unknown operation +- fields: + operation_body: "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{username,name} } # yeah" + level: WARN + message: unknown operation +- fields: + operation_body: "fragment F on Query { __typename foo: __schema { __typename } me { id } } query Q { __type(name: \"foo\") { name } ...F }" + level: WARN + message: unknown operation diff --git a/dev-docs/logging.md b/dev-docs/logging.md index abf7ef32c1..bb2517d74b 100644 --- a/dev-docs/logging.md +++ b/dev-docs/logging.md @@ -106,7 +106,7 @@ expression: yaml - fields: alg: ES256 reason: "invalid type: string \"Hmm\", expected a sequence" - index: 5 + index: 5 level: WARN message: "ignoring a key since it is not valid, enable debug logs to full content" ``` @@ -130,7 +130,7 @@ Use `with_subscriber` to attach a subscriber to an async block. ```rust #[tokio::test] async fn test_async() { - async{...}.with_subscriber(assert_snapshot_subscriber!()) + async{...}.with_subscriber(assert_snapshot_subscriber!()).await } ``` diff --git a/dev-docs/metrics.md b/dev-docs/metrics.md index 34530201ef..37b8431d71 100644 --- a/dev-docs/metrics.md +++ b/dev-docs/metrics.md @@ -136,7 +136,7 @@ Make sure to use `.with_metrics()` method on the async block to ensure that the // Multi-threaded runtime needs to use a tokio task local to avoid tests interfering with each other async { u64_counter!("test", "test description", 1, "attr" => "val"); - assert_counter!("test", 1, "attr" => "val"); + assert_counter!("test", 1, "attr" = "val"); } .with_metrics() .await; @@ -147,7 +147,7 @@ Make sure to use `.with_metrics()` method on the async block to ensure that the async { // It's a single threaded tokio runtime, so we can still use a thread local u64_counter!("test", "test description", 1, "attr" => "val"); - assert_counter!("test", 1, "attr" => "val"); + assert_counter!("test", 1, "attr" = "val"); } .with_metrics() .await; diff --git a/docs/source/routing/security/persisted-queries.mdx b/docs/source/routing/security/persisted-queries.mdx index 845e0aa75d..ea7d1e92b4 100644 --- a/docs/source/routing/security/persisted-queries.mdx +++ b/docs/source/routing/security/persisted-queries.mdx @@ -64,7 +64,7 @@ persisted_queries: log_unknown: true ``` -If used with the [`safelist`](#safelist) option, the router logs unregistered and rejected operations. With [`safelist.required_id`](#require_id) off, the only rejected operations are unregistered ones. If [`safelist.required_id`](#require_id) is turned on, operations can be rejected even when registered because they use operation IDs rather than operation strings. +If used with the [`safelist`](#safelist) option, the router logs unregistered and rejected operations. With [`safelist.require_id`](#require_id) off, the only rejected operations are unregistered ones. If [`safelist.require_id`](#require_id) is turned on, operations can be rejected even when registered because they use operation IDs rather than operation strings. #### `experimental_prewarm_query_plan_cache` @@ -114,7 +114,7 @@ To enable safelisting, you _must_ turn off [automatic persisted queries](/router -By default, the [`require_id`](#required_id) suboption is `false`, meaning the router accepts both operation IDs and operation strings as long as the operation is registered. +By default, the [`require_id`](#require_id) suboption is `false`, meaning the router accepts both operation IDs and operation strings as long as the operation is registered. #### `require_id` @@ -152,6 +152,16 @@ If this context value is not set by a customization, your router will use the sa If your request specifies an ID and a client name but there is no operation in the PQL with that ID and client name, your router will look to see if there is an operation with that ID and no client name specified, and use that if it finds it. +#### `apollo_persisted_queries::safelist::skip_enforcement` + +If safelisting is enabled, you can still opt out of safelist enforcement on a per-request basis. + +Your customization (Rhai script, coprocessor, etc) can examine a request during the [Router Service stage](/graphos/routing/customization/overview#request-path) of the request path and set the `apollo_persisted_queries::safelist::skip_enforcement` value in the request context to the boolean value `true`. + +For any request where you set this value, Router will skip safelist enforcement: requests with a full operation string will be allowed even if they are not in the safelist, and even if [`safelist.required_id`](#require_id) is enabled. + +This does not affect the behavior of the [`log_unknown` option](#log_unknown): unknown operations will still be logged if that option is set. + ## Limitations * **Unsupported with offline license**. An GraphOS Router using an [offline Enterprise license](/router/enterprise-features/#offline-enterprise-license) cannot use safelisting with persisted queries. The feature relies on Apollo Uplink to fetch persisted query manifests, so it doesn't work as designed when the router is disconnected from Uplink. From 78b9b167a4222fc50ddca1df174b9b8d6f216924 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 6 Dec 2024 10:15:38 +0100 Subject: [PATCH 081/112] Update schemaAwareHash in snapshot tests after merging main into dev * `main` has commit 043e62c95363b3cf442701b146869a81fb80a398 "Miscellaneous @context/@fromContext bugfixes (#6380)" * `dev` has commit 92e684745ce82e6519428993898a46a7579f84d3 "tests: Fix snapshots after #6205 was merged (#6367)" --- ...ntext__set_context_dependent_fetch_failure_rust_qp.snap | 4 ++-- .../set_context__set_context_list_of_lists_rust_qp.snap | 4 ++-- .../snapshots/set_context__set_context_list_rust_qp.snap | 4 ++-- .../set_context__set_context_no_typenames_rust_qp.snap | 4 ++-- .../tests/snapshots/set_context__set_context_rust_qp.snap | 4 ++-- .../set_context__set_context_type_mismatch_rust_qp.snap | 4 ++-- .../snapshots/set_context__set_context_union_rust_qp.snap | 6 +++--- ...ntext__set_context_unrelated_fetch_failure_rust_qp.snap | 7 +++---- .../set_context__set_context_with_null_rust_qp.snap | 4 ++-- .../type_conditions___test_type_conditions_disabled-2.snap | 4 ++-- .../type_conditions___test_type_conditions_enabled-2.snap | 6 +++--- ...type_conditions_enabled_generate_query_fragments-2.snap | 6 +++--- ...ions___test_type_conditions_enabled_list_of_list-2.snap | 6 +++--- ...est_type_conditions_enabled_list_of_list_of_list-2.snap | 6 +++--- ...e_conditions_enabled_shouldnt_make_article_fetch-2.snap | 6 +++--- 15 files changed, 37 insertions(+), 38 deletions(-) diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap index d8de0b56a4..8e555063f8 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap @@ -43,7 +43,7 @@ expression: response "operationKind": "query", "operationName": "Query_fetch_dependent_failure__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "a34fdce551a4d3b8e3d747620c2389d09e60b3b301afc8c959bd8ff37b2799c8", + "schemaAwareHash": "dfb0f7a17a089f11d0c95f8e9acb3a17fa4fb21216843913bc3a6c62ce2b7fbd", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -89,7 +89,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "d0c5e77f5153414f81fe851685f9df9a2f0f3b260929ebe252844fdb83df2aa0", + "schemaAwareHash": "f5ae7b50fe8d94eedfb385d91e561de06e3a3256fedca901c0b50ae689b5d630", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap index c28ed4e1cb..6dabc52d4a 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap @@ -44,7 +44,7 @@ expression: response "operationKind": "query", "operationName": "QueryLL__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "99adc22aaf3a356059916f2e81cb475d1ab0f35c618aec188365315ec7c1b190", + "schemaAwareHash": "560ba34c3cdda6c435aaab55e21528b252f44caabc6c082117e4e9fcc935af5f", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -90,7 +90,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "52bf35c65bc562f6b55c008311e28307b05eec4cd9f6ee0cbd9e375ac361d14e", + "schemaAwareHash": "b97924736c4f71e4b6e80e2a9e2661130363820bd3df5b2e38000be4a4fb47b5", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap index 38f083a2c5..2cee07ad33 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap @@ -40,7 +40,7 @@ expression: response "operationKind": "query", "operationName": "set_context_list_rust_qp__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "08ca351ae6b0b7073323e2fc82b39d4d7e36f85b0fedb9a39b3123fb8b346138", + "schemaAwareHash": "f89e82a2730898d4c37766615534224fe8f569b4786a3946e652572a1b99117d", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -86,7 +86,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "93cbeb9112a7d101127234e0874bc243d5e28833d20cdbee65479647d9ce408e", + "schemaAwareHash": "57d42b319499942de11c8eaf8bedb3618079a21fb01792b1a0a1ca8a1157d04c", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap index cd75b198c4..d110b1b332 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap @@ -32,7 +32,7 @@ expression: response "operationKind": "query", "operationName": "set_context_no_typenames_rust_qp__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "ffb1965aacb1975285b1391e5c36540e193c3ff181e9afbc60ab6acd770351c9", + "schemaAwareHash": "9b5e7d0de84a6e670d5235682e776eb0ebcd753c955403c7159adea338813a93", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -78,7 +78,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "2295ba6079ae261664bd0b31d88aea65365eb43f390be2d407f298e00305b448", + "schemaAwareHash": "0b5a9920448d114be6250a0b85f3092f5dfbce80dc89e26880fb28a5ea684d3b", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap index f114331c67..7e7088d73f 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap @@ -34,7 +34,7 @@ expression: response "operationKind": "query", "operationName": "set_context_rust_qp__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "1bf26b6306e97cbce50243fe3de6103ca3aca7338f893c0eddeb61e03f3f102f", + "schemaAwareHash": "d9094fb75802583731265ab088bd914c2c10ad3d2f7e835cbe13d58811ab797f", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -80,7 +80,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "894a9777780202b6851ce5c7dda110647c9a688dbca7fd0ae0464ecfe2003b74", + "schemaAwareHash": "971f94cb09cb7a1a5564661ae4345da5e709f3ae16b81215db80ae61a740e8d2", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap index c371ae6c6b..abacde1e41 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap @@ -32,7 +32,7 @@ expression: response "operationKind": "query", "operationName": "Query_type_mismatch__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "7235a2342601274677b87b87b9772b51e7a38f4ec96309a82f6669cd3f185d12", + "schemaAwareHash": "0ca90df94a895d97f403b550a05e72808aee80cbfc6f2f3aea8d32ae0d73d2cd", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -78,7 +78,7 @@ expression: response "typeCondition": "U" } ], - "schemaAwareHash": "0d0587044ff993407d5657e09fe5a5ded13a6db70ce5733062e90dee6c610cd4", + "schemaAwareHash": "8ea1a4dce3d934c98814d56884f9e7dad9045562a072216ea903570e01c04680", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" diff --git a/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap index d21e96abb4..c19a7222e8 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap @@ -31,7 +31,7 @@ expression: response "operationKind": "query", "operationName": "QueryUnion__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "e2ac947e685882790c24243e1312a112923780c6dc9cb18e587f55e1728c5a18", + "schemaAwareHash": "967a248e156212f72f5abb27c01fe3d5e8bb4db154e0f7015db551ee0fe46877", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -80,7 +80,7 @@ expression: response "typeCondition": "V" } ], - "schemaAwareHash": "a4707e1dbe9c5c20130adc188662691c3d601a2f1ff6ddd499301f45c62f009c", + "schemaAwareHash": "77647255b7cbdfcb4a47ab2492c0c5252c4f6d06dd4008b104d8770a584a1e32", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_1" @@ -133,7 +133,7 @@ expression: response "typeCondition": "V" } ], - "schemaAwareHash": "a4d03082699251d18ad67b92c96a9c33f0c70af8713d10f1b40514cc6b369e33", + "schemaAwareHash": "4a608fbd27c2498c1f31bf737143990d8a2f31e72682542d3169fe2fac6d5834", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_1" diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap index 5effdbec4d..77341c5367 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap @@ -1,7 +1,6 @@ --- source: apollo-router/tests/set_context.rs expression: response -snapshot_kind: text --- { "data": null, @@ -38,7 +37,7 @@ snapshot_kind: text "operationKind": "query", "operationName": "Query_fetch_failure__Subgraph1__0", "outputRewrites": null, - "schemaAwareHash": "cb2490c26be37beda781fc72a6632c104b5e5c57cfabb5061c7f1429582382d9", + "schemaAwareHash": "03786f26d73a1ad1bfa3fed40f657316857018dc1105b2da578904373b7e1882", "serviceName": "Subgraph1", "variableUsages": [] }, @@ -87,7 +86,7 @@ snapshot_kind: text "typeCondition": "U" } ], - "schemaAwareHash": "6679327c6be3db6836531c0d50d583ad66b151113a833f666dcb9ea14af4c807", + "schemaAwareHash": "f615a413abdf99efaf7e760e1246371aa5dd0f2330820cf295335ed48cc077ed", "serviceName": "Subgraph1", "variableUsages": [ "contextualArgument_1_0" @@ -130,7 +129,7 @@ snapshot_kind: text "typeCondition": "U" } ], - "schemaAwareHash": "cde44282bd7d30b098be84c3f00790e23bbdd635f44454abd7c22b283adba034", + "schemaAwareHash": "0e71b293b5c0b6143252865b2c97338cd72a558897b0a478dd0cd8b027f9a5a3", "serviceName": "Subgraph2", "variableUsages": [] }, diff --git a/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap index 8614668d4c..fcd539eeb1 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap @@ -29,7 +29,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "232e6f39e34056c6f8add0806d1dd6e2c6219fc9d451182da0188bd0348163d8", + "schemaAwareHash": "5a33cc9574d930882310fe1f9ddae8f262a448a50ac9a899e71896a339fa0f85", "authorization": { "is_authenticated": false, "scopes": [], @@ -81,7 +81,7 @@ expression: response "renameKeyTo": "contextualArgument_1_0" } ], - "schemaAwareHash": "9973ece3eaba0503afb6325c9f7afcfb007b8ef43b55ded3c1bf5be39c89d301", + "schemaAwareHash": "4f6eeca0e601bbf183b759aa785df84eb0c435a266a36568238e8d721dc8fc3c", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap index 69a3e9e530..4d61ae95b4 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "08dd5362fc4c5c7e878861f83452d62b7281b556a214a4470940cd31d78254c8", + "schemaAwareHash": "1d2d8b1ab80b4b1293e9753a915997835e6ff5bc54ba4c9b400abe7fa4661386", "authorization": { "is_authenticated": false, "scopes": [], @@ -137,7 +137,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "f31b2449c96808b927a60495a2f0d50233f41965d72af96b49f8759f101c1184", + "schemaAwareHash": "66a2bd39c499f1edd8c3ec1bfbc170cb995c6f9e23427b5486b633decd2da08b", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap index 67a3ceb6f7..c89cab9c7d 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "08dd5362fc4c5c7e878861f83452d62b7281b556a214a4470940cd31d78254c8", + "schemaAwareHash": "1d2d8b1ab80b4b1293e9753a915997835e6ff5bc54ba4c9b400abe7fa4661386", "authorization": { "is_authenticated": false, "scopes": [], @@ -140,7 +140,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -199,7 +199,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap index 67a3ceb6f7..c89cab9c7d 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "08dd5362fc4c5c7e878861f83452d62b7281b556a214a4470940cd31d78254c8", + "schemaAwareHash": "1d2d8b1ab80b4b1293e9753a915997835e6ff5bc54ba4c9b400abe7fa4661386", "authorization": { "is_authenticated": false, "scopes": [], @@ -140,7 +140,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -199,7 +199,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap index a0d484f417..371fd3496e 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "61632fc2833efa5fa0c26669f3b8f33e9d872b21d819c0e6749337743de088ed", + "schemaAwareHash": "b74616ae898acf3abefb83e24bde5faf0de0f9475d703b105b60c18c7372ab13", "authorization": { "is_authenticated": false, "scopes": [], @@ -203,7 +203,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -263,7 +263,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap index 45dfb6808f..354bd034a9 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap @@ -145,7 +145,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a5ca7a9623bbc93a0f7d1614e865a98cf376c8084fb2868702bdc38aed9bcde2", + "schemaAwareHash": "dc1df8e8d701876c6ea7d25bbeab92a5629a82e55660ccc48fc37e12d5157efa", "authorization": { "is_authenticated": false, "scopes": [], @@ -208,7 +208,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39b0b589769858e72a545b4eda572dad33f38dd8f70c39c8165d49c6a5c0ab3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -269,7 +269,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "4f18d534604c5aba52dcb0a30973a79fc311a6940a56a29acb826e6f5084da26", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap index 5bebfc6359..8811454f74 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap @@ -54,7 +54,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "130217826766119286dfcc6c95317df332634acf49fcdc5408c6c4d1b0669517", + "schemaAwareHash": "446c1a72168f736a89e4f56799333e05b26092d36fc55e22c2e92828061c787b", "authorization": { "is_authenticated": false, "scopes": [], @@ -115,7 +115,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a331620be2259b8a89122f1c41feb3abdfbd782ebbc2a1e17e3de128c4bb5cb5", + "schemaAwareHash": "f9052a9ce97a084006a1f2054b7e0fba8734f24bb53cf0f7e0ba573c7e709b98", "authorization": { "is_authenticated": false, "scopes": [], @@ -174,7 +174,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "115c6ba516a8368e9b331769942157a1e438d3f79b60690de68378da16bde989", + "schemaAwareHash": "027cac0584184439636aea68757da18f3e0e18142948e3b8625724f93e8720fc", "authorization": { "is_authenticated": false, "scopes": [], From d169b2f1676067286b9c1c12514d274be09b1a8d Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Fri, 6 Dec 2024 15:46:44 +0100 Subject: [PATCH 082/112] Remove catch_unwind wrapper around the rust query planner (#6397) This PR removes catch_unwind around the invocation of the rust query planner. As part of this change, I have changed all applicable `panic!()`s in `apollo-federation`, as well as remove adding a backtrace capture to `SingleFederationErrors`. ### Details #### `panic()!` at the `apollo-federation` There were several panic!() marcos still in use in `apollo-federation`. Instead of panicking, these functions now return the expected FederationError. If you're searching in the code, you'll still see a few panics. They are in tests, in macros or function annotated with `#[cfg(test)]`, or in [unused] composition part of the codebase. More specifically: * in internally used [CLI](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/cli/src/main.rs#L214) * in tests: * [operation tests](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/src/operation/tests/mod.rs) * [build query graph tests](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/src/query_graph/build_query_graph.rs#L2448) * [api schema tests](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/tests/api_schema.rs) * [build query plan support tests](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/tests/query_plan/build_query_plan_support.rs) * [context build query plan test](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs) * [fetch operation name tests](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs) * test helpers annotated with `#[cfg(test)]`: * [connectors' json selection test helper](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/src/sources/connect/json_selection/apply_to.rs#L116) * [connectors selection! macro](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/src/sources/connect/json_selection/helpers.rs#L10) * [argument merge strategies](https://github.com/apollographql/router/blob/85f99f19e2a2907bd5597687773d68d63644a0c6/apollo-federation/src/schema/argument_composition_strategies.rs) used solely in composition #### Removing backtrace capture We were adding backtrace captures to `SingleFederationErrors` in order to help with debugging unexpected errors while were still developing the rust query planner. Capturing backtraces is quite often [a slow and memory intensive process](https://doc.rust-lang.org/stable/std/backtrace/struct.Backtrace.html#method.capture), so we want to remove this additional functionality for rust query planner's GA. Since we now have a lot of confidence in this implementation and have not faced a panic across millions of operations that were tested and compared, this is a safe thing to remove. **Note:** The compliance CI is failing at the moment, and will be fixed after #6395 is merged --- .../maint_lrlna_remove_catch_unwind.md | 15 +++++ apollo-federation/src/error/mod.rs | 28 ++------- .../src/query_plan/fetch_dependency_graph.rs | 14 ++--- .../src/query_plan/query_planner.rs | 5 +- .../connect/json_selection/apply_to.rs | 2 + apollo-router/src/executable.rs | 16 ++--- .../src/query_planner/bridge_query_planner.rs | 5 +- .../src/query_planner/dual_query_planner.rs | 58 ++++++------------- 8 files changed, 54 insertions(+), 89 deletions(-) create mode 100644 .changesets/maint_lrlna_remove_catch_unwind.md diff --git a/.changesets/maint_lrlna_remove_catch_unwind.md b/.changesets/maint_lrlna_remove_catch_unwind.md new file mode 100644 index 0000000000..0475d08cc0 --- /dev/null +++ b/.changesets/maint_lrlna_remove_catch_unwind.md @@ -0,0 +1,15 @@ + +### Remove catch_unwind wrapper around the native query planner ([PR #6397](https://github.com/apollographql/router/pull/6397)) + +As part of internal maintenance of the query planner, we are removing the +catch_unwind wrapper around the native query planner. This wrapper was used as +an extra safeguard for potential panics the native planner could produce. The +native query planner no longer has any code paths that could panic. We have also +not witnessed a panic in the last four months, having processed 560 million real +user operations through the native planner. + +This maintenance work also removes backtrace capture for federation errors which +was used for debugging and is no longer necessary as we have the confidence in +the native planner's implementation. + +By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6397 diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 8d1ceb156b..db775b92d5 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -1,4 +1,3 @@ -use std::backtrace::Backtrace; use std::cmp::Ordering; use std::fmt::Display; use std::fmt::Formatter; @@ -523,8 +522,8 @@ pub struct MultipleFederationErrors { impl MultipleFederationErrors { pub fn push(&mut self, error: FederationError) { match error { - FederationError::SingleFederationError { inner, .. } => { - self.errors.push(inner); + FederationError::SingleFederationError(error) => { + self.errors.push(error); } FederationError::MultipleFederationErrors(errors) => { self.errors.extend(errors.errors); @@ -591,22 +590,14 @@ impl Display for AggregateFederationError { } } -/// Work around thiserror, which when an error field has a type named `Backtrace` -/// "helpfully" implements `Error::provides` even though that API is not stable yet: -/// -type ThiserrorTrustMeThisIsTotallyNotABacktrace = Backtrace; - // PORT_NOTE: Often times, JS functions would either throw/return a GraphQLError, return a vector // of GraphQLErrors, or take a vector of GraphQLErrors and group them together under an // AggregateGraphQLError which itself would have a specific error message and code, and throw that. // We represent all these cases with an enum, and delegate to the members. #[derive(thiserror::Error)] pub enum FederationError { - #[error("{inner}")] - SingleFederationError { - inner: SingleFederationError, - trace: ThiserrorTrustMeThisIsTotallyNotABacktrace, - }, + #[error(transparent)] + SingleFederationError(#[from] SingleFederationError), #[error(transparent)] MultipleFederationErrors(#[from] MultipleFederationErrors), #[error(transparent)] @@ -616,22 +607,13 @@ pub enum FederationError { impl std::fmt::Debug for FederationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::SingleFederationError { inner, trace } => write!(f, "{inner}\n{trace}"), + Self::SingleFederationError(inner) => std::fmt::Debug::fmt(inner, f), Self::MultipleFederationErrors(inner) => std::fmt::Debug::fmt(inner, f), Self::AggregateFederationError(inner) => std::fmt::Debug::fmt(inner, f), } } } -impl From for FederationError { - fn from(inner: SingleFederationError) -> Self { - Self::SingleFederationError { - inner, - trace: Backtrace::capture(), - } - } -} - impl From for FederationError { fn from(value: DiagnosticList) -> Self { SingleFederationError::from(value).into() diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index c7ee7e8e22..14e259aa68 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -32,6 +32,7 @@ use crate::bail; use crate::display_helpers::DisplayOption; use crate::error::FederationError; use crate::error::SingleFederationError; +use crate::internal_error; use crate::link::graphql_definition::DeferDirectiveArguments; use crate::operation::ArgumentList; use crate::operation::ContainmentOptions; @@ -1709,7 +1710,7 @@ impl FetchDependencyGraph { children.push(child_index); } else { let Some(child_defer_ref) = &child.defer_ref else { - panic!( + bail!( "{} has defer_ref `{}`, so its child {} cannot have a top-level defer_ref.", node.display(node_index), DisplayOption(node.defer_ref.as_ref()), @@ -2666,12 +2667,11 @@ impl FetchDependencyGraphNode { }; let operation = operation_compression.compress(operation)?; let operation_document = operation.try_into().map_err(|err| match err { - FederationError::SingleFederationError { - inner: SingleFederationError::InvalidGraphQL { diagnostics }, - .. - } => FederationError::internal(format!( + FederationError::SingleFederationError(SingleFederationError::InvalidGraphQL { + diagnostics, + }) => internal_error!( "Query planning produced an invalid subgraph operation.\n{diagnostics}" - )), + ), _ => err, })?; @@ -3392,7 +3392,7 @@ impl DeferTracking { if let Some(parent_ref) = &defer_context.current_defer_ref { let Some(parent_info) = self.deferred.get_mut(parent_ref) else { - panic!("Cannot find info for parent {parent_ref} or {label}"); + bail!("Cannot find info for parent {parent_ref} or {label}") }; parent_info.deferred.insert(label.clone()); diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index 0c94adde1d..abf514382f 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -14,6 +14,7 @@ use tracing::trace; use super::fetch_dependency_graph::FetchIdGenerator; use super::ConditionNode; +use crate::bail; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::operation::normalize_operation; @@ -432,10 +433,10 @@ impl QueryPlanner { .root_kinds_to_nodes()? .get(&normalized_operation.root_kind) else { - panic!( + bail!( "Shouldn't have a {0} operation if the subgraphs don't have a {0} root", normalized_operation.root_kind - ); + ) }; let operation_compression = if self.config.generate_query_fragments { diff --git a/apollo-federation/src/sources/connect/json_selection/apply_to.rs b/apollo-federation/src/sources/connect/json_selection/apply_to.rs index 08cb5d0fcb..5c76c91414 100644 --- a/apollo-federation/src/sources/connect/json_selection/apply_to.rs +++ b/apollo-federation/src/sources/connect/json_selection/apply_to.rs @@ -92,6 +92,8 @@ impl ApplyToError { })) } + // This macro is useful for tests, but it absolutely should never be used with + // dynamic input at runtime, since it panics for any input that's not JSON. #[cfg(test)] fn from_json(json: &JSON) -> Self { if let JSON::Object(error) = json { diff --git a/apollo-router/src/executable.rs b/apollo-router/src/executable.rs index a67e294a7f..afe70ff552 100644 --- a/apollo-router/src/executable.rs +++ b/apollo-router/src/executable.rs @@ -1,6 +1,5 @@ //! Main entry point for CLI command to start server. -use std::cell::Cell; use std::env; use std::fmt::Debug; use std::net::SocketAddr; @@ -751,18 +750,11 @@ fn setup_panic_handler() { } else { tracing::error!("{}", e) } - if !USING_CATCH_UNWIND.get() { - // Once we've panic'ed the behaviour of the router is non-deterministic - // We've logged out the panic details. Terminate with an error code - std::process::exit(1); - } - })); -} -// TODO: once the Rust query planner does not use `todo!()` anymore, -// remove this and the use of `catch_unwind` to call it. -thread_local! { - pub(crate) static USING_CATCH_UNWIND: Cell = const { Cell::new(false) }; + // Once we've panic'ed the behaviour of the router is non-deterministic + // We've logged out the panic details. Terminate with an error code + std::process::exit(1); + })); } static COPIED: AtomicBool = AtomicBool::new(false); diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index dac65efe45..4c820299f5 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -170,10 +170,7 @@ impl PlannerMode { let result = QueryPlanner::new(schema.federation_supergraph(), config); match &result { - Err(FederationError::SingleFederationError { - inner: error, - trace: _, - }) => match error { + Err(FederationError::SingleFederationError(error)) => match error { SingleFederationError::UnsupportedFederationVersion { .. } => { metric_rust_qp_init(Some(UNSUPPORTED_FED1)); } diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 9e5250a447..831bdebe45 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -15,6 +15,7 @@ use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; use apollo_compiler::Node; +use apollo_federation::error::FederationError; use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use apollo_federation::query_plan::QueryPlan; @@ -24,7 +25,6 @@ use super::fetch::SubgraphOperation; use super::subscription::SubscriptionNode; use super::FlattenNode; use crate::error::format_bridge_errors; -use crate::executable::USING_CATCH_UNWIND; use crate::query_planner::bridge_query_planner::metric_query_planning_plan_duration; use crate::query_planner::bridge_query_planner::JS_QP_MODE; use crate::query_planner::bridge_query_planner::RUST_QP_MODE; @@ -80,47 +80,23 @@ impl BothModeComparisonJob { } fn execute(self) { - // TODO: once the Rust query planner does not use `todo!()` anymore, - // remove `USING_CATCH_UNWIND` and this use of `catch_unwind`. - let rust_result = std::panic::catch_unwind(|| { - let name = self - .operation_name - .clone() - .map(Name::try_from) - .transpose()?; - USING_CATCH_UNWIND.set(true); - - let start = Instant::now(); - - // No question mark operator or macro from here … - let result = + let start = Instant::now(); + + let rust_result = self + .operation_name + .as_deref() + .map(|n| Name::new(n).map_err(FederationError::from)) + .transpose() + .and_then(|operation| { self.rust_planner - .build_query_plan(&self.document, name, self.plan_options); - - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); - - metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); - metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, self.js_duration); - - // … to here, so the thread can only eiher reach here or panic. - // We unset USING_CATCH_UNWIND in both cases. - USING_CATCH_UNWIND.set(false); - result - }) - .unwrap_or_else(|panic| { - USING_CATCH_UNWIND.set(false); - Err(apollo_federation::error::FederationError::internal( - format!( - "query planner panicked: {}", - panic - .downcast_ref::() - .map(|s| s.as_str()) - .or_else(|| panic.downcast_ref::<&str>().copied()) - .unwrap_or_default() - ), - )) - }); + .build_query_plan(&self.document, operation, self.plan_options) + }); + + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); + + metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); + metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, self.js_duration); let name = self.operation_name.as_deref(); let operation_desc = if let Ok(operation) = self.document.operations.get(name) { From 799da709a9a7e482703f7698996203f1a33478be Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:12:30 -0600 Subject: [PATCH 083/112] Cleanup unused bypass QP functionality (#6399) Cleanup query planner code by removing unused bypass QP functionality. This was a gateway only feature that was never supported on the router. --- apollo-federation/cli/src/main.rs | 5 - apollo-federation/src/query_plan/mod.rs | 9 -- .../src/query_plan/query_planner.rs | 105 ------------------ apollo-router/tests/integration/redis.rs | 10 +- 4 files changed, 5 insertions(+), 124 deletions(-) diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index cfef296154..b8ce23f24e 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -35,10 +35,6 @@ struct QueryPlannerArgs { /// Set the `debug.paths_limit` option. #[arg(long)] paths_limit: Option, - /// If the supergraph only represents a single subgraph, pass through queries directly without - /// planning. - #[arg(long, default_value_t = false)] - single_subgraph_passthrough: bool, } /// CLI arguments. See @@ -112,7 +108,6 @@ impl QueryPlannerArgs { config.debug.max_evaluated_plans = max_evaluated_plans; } config.debug.paths_limit = self.paths_limit; - config.debug.bypass_planner_for_single_subgraph = self.single_subgraph_passthrough; } } diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index 41733af9b9..ce7cfae3bd 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -266,12 +266,3 @@ pub enum QueryPathElement { #[serde(serialize_with = "crate::utils::serde_bridge::serialize_exe_inline_fragment")] InlineFragment(executable::InlineFragment), } - -impl QueryPlan { - fn new(node: impl Into, statistics: QueryPlanningStatistics) -> Self { - Self { - node: Some(node.into()), - statistics, - } - } -} diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index abf514382f..badfd4cd89 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -35,7 +35,6 @@ use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToQ use crate::query_plan::query_planning_traversal::BestQueryPlanInfo; use crate::query_plan::query_planning_traversal::QueryPlanningParameters; use crate::query_plan::query_planning_traversal::QueryPlanningTraversal; -use crate::query_plan::FetchNode; use crate::query_plan::PlanNode; use crate::query_plan::QueryPlan; use crate::query_plan::SequenceNode; @@ -121,11 +120,6 @@ pub struct QueryPlanIncrementalDeliveryConfig { #[derive(Debug, Clone, Hash, Serialize)] pub struct QueryPlannerDebugConfig { - /// If used and the supergraph is built from a single subgraph, then user queries do not go - /// through the normal query planning and instead a fetch to the one subgraph is built directly - /// from the input query. - pub bypass_planner_for_single_subgraph: bool, - /// Query planning is an exploratory process. Depending on the specificities and feature used by /// subgraphs, there could exist may different theoretical valid (if not always efficient) plans /// for a given query, and at a high level, the query planner generates those possible choices, @@ -163,7 +157,6 @@ pub struct QueryPlannerDebugConfig { impl Default for QueryPlannerDebugConfig { fn default() -> Self { Self { - bypass_planner_for_single_subgraph: false, max_evaluated_plans: NonZeroU32::new(10_000).unwrap(), paths_limit: None, } @@ -176,15 +169,6 @@ pub struct QueryPlanningStatistics { pub evaluated_plan_count: Cell, } -impl QueryPlannerConfig { - /// Panics if options are used together in unsupported ways. - fn assert_valid(&self) { - if self.incremental_delivery.enable_defer { - assert!(!self.debug.bypass_planner_for_single_subgraph, "Cannot use the `debug.bypass_planner_for_single_subgraph` query planner option when @defer support is enabled"); - } - } -} - #[derive(Debug, Default, Clone)] pub struct QueryPlanOptions { /// A set of labels which will be used _during query planning_ to @@ -231,8 +215,6 @@ impl QueryPlanner { supergraph: &Supergraph, config: QueryPlannerConfig, ) -> Result { - config.assert_valid(); - let supergraph_schema = supergraph.schema.clone(); let api_schema = supergraph.to_api_schema(ApiSchemaOptions { include_defer: config.incremental_delivery.enable_defer, @@ -357,32 +339,6 @@ impl QueryPlanner { let statistics = QueryPlanningStatistics::default(); - if self.config.debug.bypass_planner_for_single_subgraph { - let mut subgraphs = self.federated_query_graph.subgraphs(); - if let (Some((subgraph_name, _subgraph_schema)), None) = - (subgraphs.next(), subgraphs.next()) - { - let node = FetchNode { - subgraph_name: subgraph_name.clone(), - operation_document: document.clone(), - operation_name: operation.name.clone(), - operation_kind: operation.operation_type, - id: None, - variable_usages: operation - .variables - .iter() - .map(|var| var.name.clone()) - .collect(), - requires: Default::default(), - input_rewrites: Default::default(), - output_rewrites: Default::default(), - context_rewrites: Default::default(), - }; - - return Ok(QueryPlan::new(node, statistics)); - } - } - let normalized_operation = normalize_operation( operation, NamedFragments::new(&document.fragments, &self.api_schema), @@ -1200,67 +1156,6 @@ type User "###); } - #[test] - fn bypass_planner_for_single_subgraph() { - let a = Subgraph::parse_and_expand( - "A", - "https://A", - r#" - type Query { - a: A - } - type A { - b: B - } - type B { - x: Int - y: String - } - "#, - ) - .unwrap(); - let subgraphs = vec![&a]; - let supergraph = Supergraph::compose(subgraphs).unwrap(); - let api_schema = supergraph.to_api_schema(Default::default()).unwrap(); - - let document = ExecutableDocument::parse_and_validate( - api_schema.schema(), - r#" - { - a { - b { - x - y - } - } - } - "#, - "", - ) - .unwrap(); - - let mut config = QueryPlannerConfig::default(); - config.debug.bypass_planner_for_single_subgraph = true; - let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner - .build_query_plan(&document, None, Default::default()) - .unwrap(); - insta::assert_snapshot!(plan, @r###" - QueryPlan { - Fetch(service: "A") { - { - a { - b { - x - y - } - } - } - }, - } - "###); - } - #[test] fn test_optimize_no_fragments_generated() { let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap(); diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 8ff18e9cd4..5f41d8e546 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -51,7 +51,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { } // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:cache:1:federation:v2.9.3:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:8f8ce6ad09f15c3d567a05f1c3d7230ab71b3366fcaebc9cc3bbfa356d55ac12"; + let known_cache_key = "plan:cache:1:federation:v2.9.3:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -988,7 +988,7 @@ async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:b030b297e8cc0fb51de5b683162be9a4a5a0023844597253e580f99672bdf2b4", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:a55ce3338ce6d5b78566be89cc6a7ad3fe8a7eeb38229d14ddf647edef84e545", ) .await; } @@ -1018,7 +1018,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:ab8143af84859ddbed87fc3ac3b1f9c1e2271ffc8e58b58a666619ffc90bfc29", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0497501d3d01d05ad142938e7b8d8e7ea13e648aabbbedb47f6291ca8b3e536d", ) .await; } @@ -1040,7 +1040,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:285740e3d6ca7533144f54f8395204d7c19c44ed16e48f22a3ea41195d60180b", + "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:662f5041882b3f621aeb7bad8e18818173eb077dc4343e16f3a34d2b6b6e4e59", ) .await; } @@ -1088,7 +1088,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.clear_redis_cache().await; // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:8f8ce6ad09f15c3d567a05f1c3d7230ab71b3366fcaebc9cc3bbfa356d55ac12"; + let starting_key = "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c"; assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; From 9ef82967f5eaf2d910164a8a35ed5e8ede117d27 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:57:40 -0600 Subject: [PATCH 084/112] fix build after merges (#6413) --- apollo-router/src/configuration/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index b15f5df8e8..e0d6ed6a03 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -437,7 +437,6 @@ impl Configuration { }, type_conditioned_fetching: self.experimental_type_conditioned_fetching, debug: QueryPlannerDebugConfig { - bypass_planner_for_single_subgraph: false, max_evaluated_plans, paths_limit: self.supergraph.query_planning.experimental_paths_limit, }, From a201cf5ae6f38377069e54b7e379bb3e81d61159 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 6 Dec 2024 18:36:09 +0100 Subject: [PATCH 085/112] Distributed query plan cache keys include the Router version number (#6406) --- ...onfig_simon_router_version_in_cache_key.md | 7 ++++ apollo-router/build/main.rs | 32 ------------------- .../src/query_planner/bridge_query_planner.rs | 25 --------------- .../query_planner/caching_query_planner.rs | 9 ++---- apollo-router/tests/integration/redis.rs | 30 +++++++++++++---- 5 files changed, 34 insertions(+), 69 deletions(-) create mode 100644 .changesets/config_simon_router_version_in_cache_key.md diff --git a/.changesets/config_simon_router_version_in_cache_key.md b/.changesets/config_simon_router_version_in_cache_key.md new file mode 100644 index 0000000000..ce8b9a8690 --- /dev/null +++ b/.changesets/config_simon_router_version_in_cache_key.md @@ -0,0 +1,7 @@ +### Distributed query plan cache keys include the Router version number ([PR #6406](https://github.com/apollographql/router/pull/6406)) + +More often than not, an Apollo Router release may contain changes that affect what query plans are generated or how they’re represented. To avoid using outdated entries from distributed cache, the cache key includes a counter that was manually incremented with relevant data structure or algorithm changes. Instead the cache key now includes the Router version number, so that different versions will always use separate cache entries. + +If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), starting with this release and going forward you should anticipate additional cache regeneration cost when updating between any Router versions. + +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6406 diff --git a/apollo-router/build/main.rs b/apollo-router/build/main.rs index 763d894df0..b323c668bb 100644 --- a/apollo-router/build/main.rs +++ b/apollo-router/build/main.rs @@ -1,37 +1,5 @@ -use std::fs; -use std::path::PathBuf; - mod studio; fn main() -> Result<(), Box> { - let cargo_manifest: serde_json::Value = basic_toml::from_str( - &fs::read_to_string(PathBuf::from(&env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")) - .expect("could not read Cargo.toml"), - ) - .expect("could not parse Cargo.toml"); - - let router_bridge = cargo_manifest - .get("dependencies") - .expect("Cargo.toml does not contain dependencies") - .as_object() - .expect("Cargo.toml dependencies key is not an object") - .get("router-bridge") - .expect("Cargo.toml dependencies does not have an entry for router-bridge"); - let router_bridge_version = router_bridge - .as_str() - .or_else(|| { - router_bridge - .as_object() - .and_then(|o| o.get("version")) - .and_then(|version| version.as_str()) - }) - .expect("router-bridge does not have a version"); - - let mut it = router_bridge_version.split('+'); - let _ = it.next(); - let fed_version = it.next().expect("invalid router-bridge version format"); - - println!("cargo:rustc-env=FEDERATION_VERSION={fed_version}"); - studio::main() } diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 4c820299f5..b60d62f3f5 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -843,9 +843,6 @@ pub(crate) fn metric_rust_qp_init(init_error_kind: Option<&'static str>) { #[cfg(test)] mod tests { - use std::fs; - use std::path::PathBuf; - use serde_json::json; use test_log::test; use tower::Service; @@ -1389,28 +1386,6 @@ mod tests { .await } - #[test] - fn router_bridge_dependency_is_pinned() { - let cargo_manifest: serde_json::Value = basic_toml::from_str( - &fs::read_to_string(PathBuf::from(&env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")) - .expect("could not read Cargo.toml"), - ) - .expect("could not parse Cargo.toml"); - let router_bridge_version = cargo_manifest - .get("dependencies") - .expect("Cargo.toml does not contain dependencies") - .as_object() - .expect("Cargo.toml dependencies key is not an object") - .get("router-bridge") - .expect("Cargo.toml dependencies does not have an entry for router-bridge") - .as_str() - .unwrap_or_default(); - assert!( - router_bridge_version.contains('='), - "router-bridge in Cargo.toml is not pinned with a '=' prefix" - ); - } - #[tokio::test] async fn test_both_mode() { let mut harness = crate::TestHarness::builder() diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 209adcc856..7b67bf30ec 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -632,10 +632,7 @@ pub(crate) struct CachingQueryKey { pub(crate) config_mode: Arc, } -// Update this key every time the cache key or the query plan format has to change. -// When changed it MUST BE CALLED OUT PROMINENTLY IN THE CHANGELOG. -const CACHE_KEY_VERSION: usize = 1; -const FEDERATION_VERSION: &str = std::env!("FEDERATION_VERSION"); +const ROUTER_VERSION: &str = env!("CARGO_PKG_VERSION"); impl std::fmt::Display for CachingQueryKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -654,8 +651,8 @@ impl std::fmt::Display for CachingQueryKey { write!( f, - "plan:cache:{}:federation:{}:{}:opname:{}:metadata:{}", - CACHE_KEY_VERSION, FEDERATION_VERSION, self.hash, operation, metadata, + "plan:router:{}:{}:opname:{}:metadata:{}", + ROUTER_VERSION, self.hash, operation, metadata, ) } } diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 5f41d8e546..d7428ab4e4 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -51,7 +51,10 @@ async fn query_planner_cache() -> Result<(), BoxError> { } // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:cache:1:federation:v2.9.3:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c"; + let known_cache_key = &format!( + "plan:router:{}:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c", + env!("CARGO_PKG_VERSION") + ); let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -988,7 +991,10 @@ async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:a55ce3338ce6d5b78566be89cc6a7ad3fe8a7eeb38229d14ddf647edef84e545", + &format!( + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:a55ce3338ce6d5b78566be89cc6a7ad3fe8a7eeb38229d14ddf647edef84e545", + env!("CARGO_PKG_VERSION") + ), ) .await; } @@ -1018,7 +1024,10 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0497501d3d01d05ad142938e7b8d8e7ea13e648aabbbedb47f6291ca8b3e536d", + &format!( + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0497501d3d01d05ad142938e7b8d8e7ea13e648aabbbedb47f6291ca8b3e536d", + env!("CARGO_PKG_VERSION") + ), ) .await; } @@ -1040,7 +1049,10 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:662f5041882b3f621aeb7bad8e18818173eb077dc4343e16f3a34d2b6b6e4e59", + &format!( + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:662f5041882b3f621aeb7bad8e18818173eb077dc4343e16f3a34d2b6b6e4e59", + env!("CARGO_PKG_VERSION") + ), ) .await; } @@ -1063,7 +1075,10 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:9af18c8afd568c197050fc1a60c52a8c98656f1775016110516fabfbedc135fe", + &format!( + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:9af18c8afd568c197050fc1a60c52a8c98656f1775016110516fabfbedc135fe", + env!("CARGO_PKG_VERSION") + ), ) .await; } @@ -1088,7 +1103,10 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.clear_redis_cache().await; // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:cache:1:federation:v2.9.3:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c"; + let starting_key = &format!( + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c", + env!("CARGO_PKG_VERSION") + ); assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; From 32960788252de729ab36d8ea5e883e34370228c3 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Fri, 6 Dec 2024 13:18:27 -0800 Subject: [PATCH 086/112] refactor(dual-query-planner): moved semantic diff out into its own module (#6411) --- apollo-router/src/lib.rs | 6 +- .../src/query_planner/bridge_query_planner.rs | 28 - .../src/query_planner/dual_query_planner.rs | 1151 +--------------- apollo-router/src/query_planner/mod.rs | 1 + .../src/query_planner/plan_compare.rs | 1187 +++++++++++++++++ 5 files changed, 1193 insertions(+), 1180 deletions(-) create mode 100644 apollo-router/src/query_planner/plan_compare.rs diff --git a/apollo-router/src/lib.rs b/apollo-router/src/lib.rs index 6f30ab6239..cc39b79aea 100644 --- a/apollo-router/src/lib.rs +++ b/apollo-router/src/lib.rs @@ -116,10 +116,10 @@ pub mod _private { pub use crate::plugin::PluginFactory; pub use crate::plugin::PLUGINS; // For comparison/fuzzing - pub use crate::query_planner::bridge_query_planner::render_diff; pub use crate::query_planner::bridge_query_planner::QueryPlanResult; - pub use crate::query_planner::dual_query_planner::diff_plan; - pub use crate::query_planner::dual_query_planner::plan_matches; + pub use crate::query_planner::plan_compare::diff_plan; + pub use crate::query_planner::plan_compare::plan_matches; + pub use crate::query_planner::plan_compare::render_diff; // For tests pub use crate::router_factory::create_test_service_factory_from_yaml; } diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index b60d62f3f5..9259d409d9 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::fmt::Debug; -use std::fmt::Write; use std::ops::ControlFlow; use std::sync::Arc; use std::time::Instant; @@ -786,33 +785,6 @@ pub(super) struct QueryPlan { pub(super) node: Option>, } -// Note: Reexported under `apollo_router::_private` -pub fn render_diff(differences: &[diff::Result<&str>]) -> String { - let mut output = String::new(); - for diff_line in differences { - match diff_line { - diff::Result::Left(l) => { - let trimmed = l.trim(); - if !trimmed.starts_with('#') && !trimmed.is_empty() { - writeln!(&mut output, "-{l}").expect("write will never fail"); - } else { - writeln!(&mut output, " {l}").expect("write will never fail"); - } - } - diff::Result::Both(l, _) => { - writeln!(&mut output, " {l}").expect("write will never fail"); - } - diff::Result::Right(r) => { - let trimmed = r.trim(); - if trimmed != "---" && !trimmed.is_empty() { - writeln!(&mut output, "+{r}").expect("write will never fail"); - } - } - } - } - output -} - pub(crate) fn metric_query_planning_plan_duration(planner: &'static str, elapsed: f64) { f64_histogram!( "apollo.router.query_planning.plan.duration", diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 831bdebe45..3996d4f22c 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -1,40 +1,23 @@ //! Running two query planner implementations and comparing their results -use std::borrow::Borrow; -use std::collections::hash_map::HashMap; -use std::fmt::Write; -use std::hash::DefaultHasher; -use std::hash::Hash; -use std::hash::Hasher; use std::sync::Arc; use std::sync::OnceLock; use std::time::Instant; -use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; -use apollo_compiler::Node; use apollo_federation::error::FederationError; use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; -use apollo_federation::query_plan::QueryPlan; -use super::fetch::FetchNode; -use super::fetch::SubgraphOperation; -use super::subscription::SubscriptionNode; -use super::FlattenNode; use crate::error::format_bridge_errors; use crate::query_planner::bridge_query_planner::metric_query_planning_plan_duration; use crate::query_planner::bridge_query_planner::JS_QP_MODE; use crate::query_planner::bridge_query_planner::RUST_QP_MODE; use crate::query_planner::convert::convert_root_query_plan_node; -use crate::query_planner::render_diff; -use crate::query_planner::rewrites::DataRewrite; -use crate::query_planner::selection::Selection; -use crate::query_planner::DeferredNode; -use crate::query_planner::PlanNode; -use crate::query_planner::Primary; +use crate::query_planner::plan_compare::diff_plan; +use crate::query_planner::plan_compare::opt_plan_node_matches; use crate::query_planner::QueryPlanResult; /// Jobs are dropped if this many are already queued @@ -170,1136 +153,6 @@ pub(crate) fn metric_query_planning_plan_both_comparison_duration( ); } -// Specific comparison functions - -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, - variable_usages, - operation, - // ignored: - // reordered parallel fetches may have different names - operation_name: _, - operation_kind, - id, - input_rewrites, - output_rewrites, - context_rewrites, - // ignored - schema_aware_hash: _, - // ignored: - // when running in comparison mode, the rust plan node does not have - // the attached cache key metadata for authorisation, since the rust plan is - // not going to be the one being executed. - authorization: _, - } = this; - - check_match_eq!(*service_name, other.service_name); - check_match_eq!(*operation_kind, other.operation_kind); - check_match_eq!(*id, other.id); - check_match!(same_requires(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, -) -> Result<(), MatchFailure> { - let SubscriptionNode { - service_name, - variable_usages, - operation, - operation_name: _, // ignored (reordered parallel fetches may have different names) - operation_kind, - input_rewrites, - output_rewrites, - } = this; - check_match_eq!(*service_name, other.service_name); - check_match_eq!(*operation_kind, other.operation_kind); - 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)); - operation_matches(operation, &other.operation)?; - Ok(()) -} - -fn operation_matches( - this: &SubgraphOperation, - other: &SubgraphOperation, -) -> Result<(), MatchFailure> { - document_str_matches(this.as_serialized(), other.as_serialized()) -} - -// Compare operation document strings such as query or just selection set. -fn document_str_matches(this: &str, other: &str) -> Result<(), MatchFailure> { - let this_ast = match ast::Document::parse(this, "this_operation.graphql") { - Ok(document) => document, - Err(_) => { - return Err(MatchFailure::new( - "Failed to parse this operation".to_string(), - )); - } - }; - let other_ast = match ast::Document::parse(other, "other_operation.graphql") { - Ok(document) => document, - Err(_) => { - return Err(MatchFailure::new( - "Failed to parse other operation".to_string(), - )); - } - }; - same_ast_document(&this_ast, &other_ast) -} - -fn opt_document_string_matches( - this: &Option, - other: &Option, -) -> Result<(), MatchFailure> { - match (this, other) { - (None, None) => Ok(()), - (Some(this_sel), Some(other_sel)) => document_str_matches(this_sel, other_sel), - _ => Err(MatchFailure::new(format!( - "mismatched at opt_document_string_matches\nleft: {:?}\nright: {:?}", - this, other - ))), - } -} - -// The rest is calling the comparison functions above instead of `PartialEq`, -// but otherwise behave just like `PartialEq`: - -// Note: Reexported under `apollo_router::_private` -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) -} - -pub fn diff_plan(js_plan: &QueryPlanResult, rust_plan: &QueryPlan) -> String { - let js_root_node = &js_plan.query_plan.node; - let rust_root_node = convert_root_query_plan_node(rust_plan); - - match (js_root_node, rust_root_node) { - (None, None) => String::from(""), - (None, Some(rust)) => { - let rust = &format!("{rust:#?}"); - let differences = diff::lines("", rust); - render_diff(&differences) - } - (Some(js), None) => { - let js = &format!("{js:#?}"); - let differences = diff::lines(js, ""); - render_diff(&differences) - } - (Some(js), Some(rust)) => { - let rust = &format!("{rust:#?}"); - let js = &format!("{js:#?}"); - let differences = diff::lines(js, rust); - render_diff(&differences) - } - } -} - -fn opt_plan_node_matches( - this: &Option>, - other: &Option>, -) -> Result<(), MatchFailure> { - match (this, other) { - (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()), - } -} - -//================================================================================================== -// Vec comparison functions - -fn vec_matches(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { - this.len() == other.len() - && 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))) - })?; - Ok(()) -} - -fn vec_matches_sorted(this: &[T], other: &[T]) -> bool { - let mut this_sorted = this.to_owned(); - let mut other_sorted = other.to_owned(); - this_sorted.sort(); - other_sorted.sort(); - vec_matches(&this_sorted, &other_sorted, T::eq) -} - -fn vec_matches_sorted_by( - this: &[T], - other: &[T], - compare: impl Fn(&T, &T) -> std::cmp::Ordering, - item_matches: impl Fn(&T, &T) -> bool, -) -> bool { - let mut this_sorted = this.to_owned(); - let mut other_sorted = other.to_owned(); - this_sorted.sort_by(&compare); - other_sorted.sort_by(&compare); - vec_matches(&this_sorted, &other_sorted, item_matches) -} - -fn vec_matches_result_sorted_by( - this: &[T], - other: &[T], - compare: impl Fn(&T, &T) -> std::cmp::Ordering, - item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, -) -> Result<(), MatchFailure> { - check_match_eq!(this.len(), other.len()); - let mut this_sorted = this.to_owned(); - let mut other_sorted = other.to_owned(); - this_sorted.sort_by(&compare); - other_sorted.sort_by(&compare); - std::iter::zip(&this_sorted, &other_sorted) - .try_fold((), |_acc, (this, other)| item_matches(this, other))?; - Ok(()) -} - -// `this` vector includes `other` vector as a set -fn vec_includes_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { - other.iter().all(|other_node| { - this.iter() - .any(|this_node| item_matches(this_node, other_node)) - }) -} - -// performs a set comparison, ignoring order -fn vec_matches_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { - // Set-inclusion test in both directions - this.len() == other.len() - && vec_includes_as_set(this, other, &item_matches) - && vec_includes_as_set(other, this, &item_matches) -} - -// Forward/reverse mappings from one Vec items (indices) to another. -type VecMapping = (HashMap, HashMap); - -// performs a set comparison, ignoring order -// and returns a mapping from `this` to `other`. -fn vec_matches_as_set_with_mapping( - this: &[T], - other: &[T], - item_matches: impl Fn(&T, &T) -> bool, -) -> VecMapping { - // Set-inclusion test in both directions - // - record forward/reverse mapping from this items <-> other items for reporting mismatches - let mut forward_map: HashMap = HashMap::new(); - let mut reverse_map: HashMap = HashMap::new(); - for (this_pos, this_node) in this.iter().enumerate() { - if let Some(other_pos) = other - .iter() - .position(|other_node| item_matches(this_node, other_node)) - { - forward_map.insert(this_pos, other_pos); - reverse_map.insert(other_pos, this_pos); - } - } - for (other_pos, other_node) in other.iter().enumerate() { - if reverse_map.contains_key(&other_pos) { - continue; - } - if let Some(this_pos) = this - .iter() - .position(|this_node| item_matches(this_node, other_node)) - { - forward_map.insert(this_pos, other_pos); - reverse_map.insert(other_pos, this_pos); - } - } - (forward_map, reverse_map) -} - -// Returns a formatted mismatch message and an optional pair of mismatched positions if the pair -// are the only remaining unmatched items. -fn format_mismatch_as_set( - this_len: usize, - other_len: usize, - forward_map: &HashMap, - reverse_map: &HashMap, -) -> Result<(String, Option<(usize, usize)>), std::fmt::Error> { - let mut ret = String::new(); - let buf = &mut ret; - write!(buf, "- mapping from left to right: [")?; - let mut this_missing_pos = None; - for this_pos in 0..this_len { - if this_pos != 0 { - write!(buf, ", ")?; - } - if let Some(other_pos) = forward_map.get(&this_pos) { - write!(buf, "{}", other_pos)?; - } else { - this_missing_pos = Some(this_pos); - write!(buf, "?")?; - } - } - writeln!(buf, "]")?; - - write!(buf, "- left-over on the right: [")?; - let mut other_missing_count = 0; - let mut other_missing_pos = None; - for other_pos in 0..other_len { - if reverse_map.get(&other_pos).is_none() { - if other_missing_count != 0 { - write!(buf, ", ")?; - } - other_missing_count += 1; - other_missing_pos = Some(other_pos); - write!(buf, "{}", other_pos)?; - } - } - write!(buf, "]")?; - let unmatched_pair = if let (Some(this_missing_pos), Some(other_missing_pos)) = - (this_missing_pos, other_missing_pos) - { - if this_len == 1 + forward_map.len() && other_len == 1 + reverse_map.len() { - // Special case: There are only one missing item on each side. They are supposed to - // match each other. - Some((this_missing_pos, other_missing_pos)) - } else { - None - } - } else { - None - }; - Ok((ret, unmatched_pair)) -} - -fn vec_matches_result_as_set( - this: &[T], - other: &[T], - item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, -) -> Result { - // Set-inclusion test in both directions - // - record forward/reverse mapping from this items <-> other items for reporting mismatches - let (forward_map, reverse_map) = - vec_matches_as_set_with_mapping(this, other, |a, b| item_matches(a, b).is_ok()); - if forward_map.len() == this.len() && reverse_map.len() == other.len() { - Ok((forward_map, reverse_map)) - } else { - // report mismatch - let Ok((message, unmatched_pair)) = - format_mismatch_as_set(this.len(), other.len(), &forward_map, &reverse_map) - else { - // Exception: Unable to format mismatch report => fallback to most generic message - return Err(MatchFailure::new( - "mismatch at vec_matches_result_as_set (failed to format mismatched sets)" - .to_string(), - )); - }; - if let Some(unmatched_pair) = unmatched_pair { - // found a unique pair to report => use that pair's error message - let Err(err) = item_matches(&this[unmatched_pair.0], &other[unmatched_pair.1]) else { - // Exception: Unable to format unique pair mismatch error => fallback to overall report - return Err(MatchFailure::new(format!( - "mismatched sets (failed to format unique pair mismatch error):\n{}", - message - ))); - }; - Err(err.add_description(&format!( - "under a sole unmatched pair ({} -> {}) in a set comparison", - unmatched_pair.0, unmatched_pair.1 - ))) - } else { - Err(MatchFailure::new(format!("mismatched sets:\n{}", message))) - } - } -} - -//================================================================================================== -// PlanNode comparison functions - -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_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_result_as_set(this, other, plan_node_matches) - .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::Defer { primary, deferred }, - PlanNode::Defer { - primary: other_primary, - deferred: other_deferred, - }, - ) => { - defer_primary_node_matches(primary, other_primary)?; - vec_matches_result(deferred, other_deferred, deferred_node_matches)?; - } - ( - PlanNode::Subscription { primary, rest }, - PlanNode::Subscription { - primary: other_primary, - rest: other_rest, - }, - ) => { - subscription_primary_matches(primary, other_primary)?; - opt_plan_node_matches(rest, other_rest) - .map_err(|err| err.add_description("under Subscription"))?; - } - ( - PlanNode::Condition { - condition, - if_clause, - else_clause, - }, - PlanNode::Condition { - condition: other_condition, - if_clause: other_if_clause, - 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)"))?; - } - _ => { - return Err(MatchFailure::new(format!( - "mismatched plan node types\nleft: {:?}\nright: {:?}", - this, other - ))) - } - }; - Ok(()) -} - -fn defer_primary_node_matches(this: &Primary, other: &Primary) -> Result<(), MatchFailure> { - let Primary { subselection, node } = this; - opt_document_string_matches(subselection, &other.subselection) - .map_err(|err| err.add_description("under defer primary subselection"))?; - opt_plan_node_matches(node, &other.node) - .map_err(|err| err.add_description("under defer primary plan node")) -} - -fn deferred_node_matches(this: &DeferredNode, other: &DeferredNode) -> Result<(), MatchFailure> { - let DeferredNode { - depends, - label, - query_path, - subselection, - node, - } = this; - - check_match_eq!(*depends, other.depends); - check_match_eq!(*label, other.label); - check_match_eq!(*query_path, other.query_path); - opt_document_string_matches(subselection, &other.subselection) - .map_err(|err| err.add_description("under deferred subselection"))?; - opt_plan_node_matches(node, &other.node) - .map_err(|err| err.add_description("under deferred node")) -} - -fn flatten_node_matches(this: &FlattenNode, other: &FlattenNode) -> Result<(), MatchFailure> { - let FlattenNode { path, node } = this; - check_match_eq!(*path, other.path); - plan_node_matches(node, &other.node) -} - -// Copied and modified from `apollo_federation::operation::SelectionKey` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum SelectionKey { - Field { - /// The field alias (if specified) or field name in the resulting selection set. - response_name: Name, - directives: ast::DirectiveList, - }, - FragmentSpread { - /// The name of the fragment. - fragment_name: Name, - directives: ast::DirectiveList, - }, - InlineFragment { - /// The optional type condition of the fragment. - type_condition: Option, - directives: ast::DirectiveList, - }, -} - -fn get_selection_key(selection: &Selection) -> SelectionKey { - match selection { - Selection::Field(field) => SelectionKey::Field { - response_name: field.response_name().clone(), - directives: Default::default(), - }, - Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { - type_condition: fragment.type_condition.clone(), - directives: Default::default(), - }, - } -} - -fn hash_value(x: &T) -> u64 { - let mut hasher = DefaultHasher::new(); - x.hash(&mut hasher); - hasher.finish() -} - -fn hash_selection_key(selection: &Selection) -> u64 { - hash_value(&get_selection_key(selection)) -} - -// Note: This `Selection` struct is a limited version used for the `requires` field. -fn same_selection(x: &Selection, y: &Selection) -> bool { - match (x, y) { - (Selection::Field(x), Selection::Field(y)) => { - x.name == y.name - && x.alias == y.alias - && match (&x.selections, &y.selections) { - (Some(x), Some(y)) => same_selection_set_sorted(x, y), - (None, None) => true, - _ => false, - } - } - (Selection::InlineFragment(x), Selection::InlineFragment(y)) => { - x.type_condition == y.type_condition - && same_selection_set_sorted(&x.selections, &y.selections) - } - _ => false, - } -} - -fn same_selection_set_sorted(x: &[Selection], y: &[Selection]) -> bool { - fn sorted_by_selection_key(s: &[Selection]) -> Vec<&Selection> { - let mut sorted: Vec<&Selection> = s.iter().collect(); - sorted.sort_by_key(|x| hash_selection_key(x)); - sorted - } - - if x.len() != y.len() { - return false; - } - sorted_by_selection_key(x) - .into_iter() - .zip(sorted_by_selection_key(y)) - .all(|(x, y)| same_selection(x, y)) -} - -fn same_requires(x: &[Selection], y: &[Selection]) -> bool { - vec_matches_as_set(x, y, same_selection) -} - -fn same_rewrites(x: &Option>, y: &Option>) -> bool { - match (x, y) { - (None, None) => true, - (Some(x), Some(y)) => vec_matches_as_set(x, y, |a, b| a == b), - _ => false, - } -} - -//================================================================================================== -// AST comparison functions - -fn same_ast_document(x: &ast::Document, y: &ast::Document) -> Result<(), MatchFailure> { - fn split_definitions( - doc: &ast::Document, - ) -> ( - Vec<&ast::OperationDefinition>, - Vec<&ast::FragmentDefinition>, - Vec<&ast::Definition>, - ) { - let mut operations: Vec<&ast::OperationDefinition> = Vec::new(); - let mut fragments: Vec<&ast::FragmentDefinition> = Vec::new(); - let mut others: Vec<&ast::Definition> = Vec::new(); - for def in doc.definitions.iter() { - match def { - ast::Definition::OperationDefinition(op) => operations.push(op), - ast::Definition::FragmentDefinition(frag) => fragments.push(frag), - _ => others.push(def), - } - } - (operations, fragments, others) - } - - let (x_ops, x_frags, x_others) = split_definitions(x); - let (y_ops, y_frags, y_others) = split_definitions(y); - - debug_assert!(x_others.is_empty(), "Unexpected definition types"); - debug_assert!(y_others.is_empty(), "Unexpected definition types"); - debug_assert!( - x_ops.len() == y_ops.len(), - "Different number of operation definitions" - ); - - check_match_eq!(x_frags.len(), y_frags.len()); - let mut fragment_map: HashMap = HashMap::new(); - // Assumption: x_frags and y_frags are topologically sorted. - // Thus, we can build the fragment name mapping in a single pass and compare - // fragment definitions using the mapping at the same time, since earlier fragments - // will never reference later fragments. - x_frags.iter().try_fold((), |_, x_frag| { - let y_frag = y_frags - .iter() - .find(|y_frag| same_ast_fragment_definition(x_frag, y_frag, &fragment_map).is_ok()); - if let Some(y_frag) = y_frag { - if x_frag.name != y_frag.name { - // record it only if they are not identical - fragment_map.insert(x_frag.name.clone(), y_frag.name.clone()); - } - Ok(()) - } else { - Err(MatchFailure::new(format!( - "mismatch: no matching fragment definition for {}", - x_frag.name - ))) - } - })?; - - 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, &fragment_map) - .map_err(|err| err.add_description("under operation definition")) - })?; - Ok(()) -} - -fn same_ast_operation_definition( - x: &ast::OperationDefinition, - y: &ast::OperationDefinition, - fragment_map: &HashMap, -) -> Result<(), MatchFailure> { - // Note: Operation names are ignored, since parallel fetches may have different names. - check_match_eq!(x.operation_type, y.operation_type); - vec_matches_result_sorted_by( - &x.variables, - &y.variables, - |a, b| a.name.cmp(&b.name), - |a, b| same_variable_definition(a, b), - ) - .map_err(|err| err.add_description("under Variable definition"))?; - check_match_eq!(x.directives, y.directives); - check_match!(same_ast_selection_set_sorted( - &x.selection_set, - &y.selection_set, - fragment_map, - )); - Ok(()) -} - -// `x` may be coerced to `y`. -// - `x` should be a value from JS QP. -// - `y` should be a value from Rust QP. -// - Assume: x and y are already checked not equal. -// Due to coercion differences, we need to compare AST values with special cases. -fn ast_value_maybe_coerced_to(x: &ast::Value, y: &ast::Value) -> bool { - match (x, y) { - // Special case 1: JS QP may convert an enum value into string. - // - In this case, compare them as strings. - (ast::Value::String(ref x), ast::Value::Enum(ref y)) => { - if x == y.as_str() { - return true; - } - } - - // Special case 2: Rust QP expands a object value by filling in its - // default field values. - // - If the Rust QP object value subsumes the JS QP object value, consider it a match. - // - Assuming the Rust QP object value has only default field values. - // - Warning: This is an unsound heuristic. - (ast::Value::Object(ref x), ast::Value::Object(ref y)) => { - if vec_includes_as_set(y, x, |(yy_name, yy_val), (xx_name, xx_val)| { - xx_name == yy_name - && (xx_val == yy_val || ast_value_maybe_coerced_to(xx_val, yy_val)) - }) { - return true; - } - } - - // Special case 3: JS QP may convert string to int for custom scalars, while Rust doesn't. - // - Note: This conversion seems a bit difficult to implement in the `apollo-federation`'s - // `coerce_value` function, since IntValue's constructor is private to the crate. - (ast::Value::Int(ref x), ast::Value::String(ref y)) => { - if x.as_str() == y { - return true; - } - } - - // Recurse into list items. - (ast::Value::List(ref x), ast::Value::List(ref y)) => { - if vec_matches(x, y, |xx, yy| { - xx == yy || ast_value_maybe_coerced_to(xx, yy) - }) { - return true; - } - } - - _ => {} // otherwise, fall through - } - false -} - -// Use this function, instead of `VariableDefinition`'s `PartialEq` implementation, -// due to known differences. -fn same_variable_definition( - x: &ast::VariableDefinition, - y: &ast::VariableDefinition, -) -> Result<(), MatchFailure> { - check_match_eq!(x.name, y.name); - check_match_eq!(x.ty, y.ty); - if x.default_value != y.default_value { - if let (Some(x), Some(y)) = (&x.default_value, &y.default_value) { - if ast_value_maybe_coerced_to(x, y) { - return Ok(()); - } - } - - return Err(MatchFailure::new(format!( - "mismatch between default values:\nleft: {:?}\nright: {:?}", - x.default_value, y.default_value - ))); - } - check_match_eq!(x.directives, y.directives); - Ok(()) -} - -fn same_ast_fragment_definition( - x: &ast::FragmentDefinition, - y: &ast::FragmentDefinition, - fragment_map: &HashMap, -) -> Result<(), MatchFailure> { - // Note: Fragment names at definitions are ignored. - 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, - fragment_map, - )); - Ok(()) -} - -fn same_ast_argument_value(x: &ast::Value, y: &ast::Value) -> bool { - x == y || ast_value_maybe_coerced_to(x, y) -} - -fn same_ast_argument(x: &ast::Argument, y: &ast::Argument) -> bool { - x.name == y.name && same_ast_argument_value(&x.value, &y.value) -} - -fn same_ast_arguments(x: &[Node], y: &[Node]) -> bool { - vec_matches_sorted_by( - x, - y, - |a, b| a.name.cmp(&b.name), - |a, b| same_ast_argument(a, b), - ) -} - -fn same_directives(x: &ast::DirectiveList, y: &ast::DirectiveList) -> bool { - vec_matches_sorted_by( - x, - y, - |a, b| a.name.cmp(&b.name), - |a, b| a.name == b.name && same_ast_arguments(&a.arguments, &b.arguments), - ) -} - -fn get_ast_selection_key( - selection: &ast::Selection, - fragment_map: &HashMap, -) -> SelectionKey { - match selection { - ast::Selection::Field(field) => SelectionKey::Field { - response_name: field.response_name().clone(), - directives: field.directives.clone(), - }, - ast::Selection::FragmentSpread(fragment) => SelectionKey::FragmentSpread { - fragment_name: fragment_map - .get(&fragment.fragment_name) - .unwrap_or(&fragment.fragment_name) - .clone(), - directives: fragment.directives.clone(), - }, - ast::Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { - type_condition: fragment.type_condition.clone(), - directives: fragment.directives.clone(), - }, - } -} - -fn same_ast_selection( - x: &ast::Selection, - y: &ast::Selection, - fragment_map: &HashMap, -) -> bool { - match (x, y) { - (ast::Selection::Field(x), ast::Selection::Field(y)) => { - x.name == y.name - && x.alias == y.alias - && same_ast_arguments(&x.arguments, &y.arguments) - && same_directives(&x.directives, &y.directives) - && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) - } - (ast::Selection::FragmentSpread(x), ast::Selection::FragmentSpread(y)) => { - let mapped_fragment_name = fragment_map - .get(&x.fragment_name) - .unwrap_or(&x.fragment_name); - *mapped_fragment_name == y.fragment_name - && same_directives(&x.directives, &y.directives) - } - (ast::Selection::InlineFragment(x), ast::Selection::InlineFragment(y)) => { - x.type_condition == y.type_condition - && same_directives(&x.directives, &y.directives) - && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) - } - _ => false, - } -} - -fn hash_ast_selection_key(selection: &ast::Selection, fragment_map: &HashMap) -> u64 { - hash_value(&get_ast_selection_key(selection, fragment_map)) -} - -// Selections are sorted and compared after renaming x's fragment spreads according to the -// fragment_map. -fn same_ast_selection_set_sorted( - x: &[ast::Selection], - y: &[ast::Selection], - fragment_map: &HashMap, -) -> bool { - fn sorted_by_selection_key<'a>( - s: &'a [ast::Selection], - fragment_map: &HashMap, - ) -> Vec<&'a ast::Selection> { - let mut sorted: Vec<&ast::Selection> = s.iter().collect(); - sorted.sort_by_key(|x| hash_ast_selection_key(x, fragment_map)); - sorted - } - - if x.len() != y.len() { - return false; - } - let x_sorted = sorted_by_selection_key(x, fragment_map); // Map fragment spreads - let y_sorted = sorted_by_selection_key(y, &Default::default()); // Don't map fragment spreads - x_sorted - .into_iter() - .zip(y_sorted) - .all(|(x, y)| same_ast_selection(x, y, fragment_map)) -} - -#[cfg(test)] -mod ast_comparison_tests { - use super::*; - - #[test] - fn test_query_variable_decl_order() { - let op_x = r#"query($qv2: String!, $qv1: Int!) { x(arg1: $qv1, arg2: $qv2) }"#; - 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).is_ok()); - } - - #[test] - fn test_query_variable_decl_enum_value_coercion() { - // Note: JS QP converts enum default values into strings. - let op_x = r#"query($qv1: E! = "default_value") { x(arg1: $qv1) }"#; - let op_y = r#"query($qv1: E! = default_value) { x(arg1: $qv1) }"#; - 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).is_ok()); - } - - #[test] - fn test_query_variable_decl_object_value_coercion_empty_case() { - // Note: Rust QP expands empty object default values by filling in its default field - // values. - let op_x = r#"query($qv1: T! = {}) { x(arg1: $qv1) }"#; - let op_y = - r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; - 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).is_ok()); - } - - #[test] - fn test_query_variable_decl_object_value_coercion_non_empty_case() { - // Note: Rust QP expands an object default values by filling in its default field values. - let op_x = r#"query($qv1: T! = {field1: true}) { x(arg1: $qv1) }"#; - let op_y = - r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; - 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).is_ok()); - } - - #[test] - fn test_query_variable_decl_list_of_object_value_coercion() { - // Testing a combination of list and object value coercion. - let op_x = r#"query($qv1: [T!]! = [{}]) { x(arg1: $qv1) }"#; - let op_y = - r#"query($qv1: [T!]! = [{field1: true, field2: "default_value"}]) { x(arg1: $qv1) }"#; - 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).is_ok()); - } - - #[test] - fn test_entities_selection_order() { - let op_x = r#" - query subgraph1__1($representations: [_Any!]!) { - _entities(representations: $representations) { x { w } y } - } - "#; - let op_y = r#" - query subgraph1__1($representations: [_Any!]!) { - _entities(representations: $representations) { y x { 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).is_ok()); - } - - #[test] - fn test_top_level_selection_order() { - let op_x = r#"{ x { w z } y }"#; - 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).is_ok()); - } - - #[test] - fn test_fragment_definition_order() { - let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; - 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).is_ok()); - } - - #[test] - fn test_selection_argument_is_compared() { - let op_x = r#"{ x(arg1: "one") }"#; - let op_y = r#"{ x(arg1: "two") }"#; - 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).is_err()); - } - - #[test] - fn test_selection_argument_order() { - let op_x = r#"{ x(arg1: "one", arg2: "two") }"#; - let op_y = r#"{ x(arg2: "two", arg1: "one") }"#; - 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).is_ok()); - } - - #[test] - fn test_selection_directive_order() { - let op_x = r#"{ x @include(if:true) @skip(if:false) }"#; - let op_y = r#"{ x @skip(if:false) @include(if:true) }"#; - 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).is_ok()); - } - - #[test] - fn test_string_to_id_coercion_difference() { - // JS QP coerces strings into integer for ID type, while Rust QP doesn't. - // This tests a special case that same_ast_document accepts this difference. - let op_x = r#"{ x(id: 123) }"#; - let op_y = r#"{ x(id: "123") }"#; - 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).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names() { - let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; - let op_y = r#"{ q { ...g1 ...g2 } } fragment g1 on T { x y } fragment g2 on T { w z }"#; - 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).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names_nested_1() { - // Nested fragments have the same name, only top-level fragments have different names. - let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; - let op_y = r#"{ q { ...g2 } } fragment f1 on T { x y } fragment g2 on T { z ...f1 }"#; - 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).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names_nested_2() { - // Nested fragments have different names. - let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; - let op_y = r#"{ q { ...g2 } } fragment g1 on T { x y } fragment g2 on T { z ...g1 }"#; - 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).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names_nested_3() { - // Nested fragments have different names. - // Also, fragment definitions are in different order. - let op_x = r#"{ q { ...f2 ...f3 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 } fragment f3 on T { w } "#; - let op_y = r#"{ q { ...g2 ...g3 } } fragment g1 on T { x y } fragment g2 on T { w } fragment g3 on T { z ...g1 }"#; - 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).is_ok()); - } -} - -#[cfg(test)] -mod qp_selection_comparison_tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_requires_comparison_with_same_selection_key() { - let requires_json = json!([ - { - "kind": "InlineFragment", - "typeCondition": "T", - "selections": [ - { - "kind": "Field", - "name": "id", - }, - ] - }, - { - "kind": "InlineFragment", - "typeCondition": "T", - "selections": [ - { - "kind": "Field", - "name": "id", - }, - { - "kind": "Field", - "name": "job", - } - ] - }, - ]); - - // The only difference between requires1 and requires2 is the order of selections. - // But, their items all have the same SelectionKey. - let requires1: Vec = serde_json::from_value(requires_json).unwrap(); - let requires2: Vec = requires1.iter().rev().cloned().collect(); - - // `same_selection_set_sorted` fails to match, since it doesn't account for - // two items with the same SelectionKey but in different order. - assert!(!same_selection_set_sorted(&requires1, &requires2)); - // `same_requires` should succeed. - assert!(same_requires(&requires1, &requires2)); - } -} - #[cfg(test)] mod tests { use std::time::Instant; diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs index ad23fbb80e..fb912e1d16 100644 --- a/apollo-router/src/query_planner/mod.rs +++ b/apollo-router/src/query_planner/mod.rs @@ -19,6 +19,7 @@ mod execution; pub(crate) mod fetch; mod labeler; mod plan; +pub(crate) mod plan_compare; pub(crate) mod rewrites; mod selection; mod subgraph_context; diff --git a/apollo-router/src/query_planner/plan_compare.rs b/apollo-router/src/query_planner/plan_compare.rs new file mode 100644 index 0000000000..c9df821102 --- /dev/null +++ b/apollo-router/src/query_planner/plan_compare.rs @@ -0,0 +1,1187 @@ +// Semantic comparison of JS and Rust query plans + +use std::borrow::Borrow; +use std::collections::hash_map::HashMap; +use std::fmt::Write; +use std::hash::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; + +use apollo_compiler::ast; +use apollo_compiler::Name; +use apollo_compiler::Node; +use apollo_federation::query_plan::QueryPlan as NativeQueryPlan; + +use super::convert::convert_root_query_plan_node; +use super::fetch::FetchNode; +use super::fetch::SubgraphOperation; +use super::rewrites::DataRewrite; +use super::selection::Selection; +use super::subscription::SubscriptionNode; +use super::DeferredNode; +use super::FlattenNode; +use super::PlanNode; +use super::Primary; +use super::QueryPlanResult; + +//================================================================================================== +// Public interface + +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)); + } + }; +} + +// Note: Reexported under `apollo_router::_private` +pub fn plan_matches( + js_plan: &QueryPlanResult, + rust_plan: &NativeQueryPlan, +) -> 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) +} + +// Note: Reexported under `apollo_router::_private` +pub fn diff_plan(js_plan: &QueryPlanResult, rust_plan: &NativeQueryPlan) -> String { + let js_root_node = &js_plan.query_plan.node; + let rust_root_node = convert_root_query_plan_node(rust_plan); + + match (js_root_node, rust_root_node) { + (None, None) => String::from(""), + (None, Some(rust)) => { + let rust = &format!("{rust:#?}"); + let differences = diff::lines("", rust); + render_diff(&differences) + } + (Some(js), None) => { + let js = &format!("{js:#?}"); + let differences = diff::lines(js, ""); + render_diff(&differences) + } + (Some(js), Some(rust)) => { + let rust = &format!("{rust:#?}"); + let js = &format!("{js:#?}"); + let differences = diff::lines(js, rust); + render_diff(&differences) + } + } +} + +// Note: Reexported under `apollo_router::_private` +pub fn render_diff(differences: &[diff::Result<&str>]) -> String { + let mut output = String::new(); + for diff_line in differences { + match diff_line { + diff::Result::Left(l) => { + let trimmed = l.trim(); + if !trimmed.starts_with('#') && !trimmed.is_empty() { + writeln!(&mut output, "-{l}").expect("write will never fail"); + } else { + writeln!(&mut output, " {l}").expect("write will never fail"); + } + } + diff::Result::Both(l, _) => { + writeln!(&mut output, " {l}").expect("write will never fail"); + } + diff::Result::Right(r) => { + let trimmed = r.trim(); + if trimmed != "---" && !trimmed.is_empty() { + writeln!(&mut output, "+{r}").expect("write will never fail"); + } + } + } + } + output +} + +//================================================================================================== +// Vec comparison functions + +fn vec_matches(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { + this.len() == other.len() + && 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))) + })?; + Ok(()) +} + +fn vec_matches_sorted(this: &[T], other: &[T]) -> bool { + let mut this_sorted = this.to_owned(); + let mut other_sorted = other.to_owned(); + this_sorted.sort(); + other_sorted.sort(); + vec_matches(&this_sorted, &other_sorted, T::eq) +} + +fn vec_matches_sorted_by( + this: &[T], + other: &[T], + compare: impl Fn(&T, &T) -> std::cmp::Ordering, + item_matches: impl Fn(&T, &T) -> bool, +) -> bool { + let mut this_sorted = this.to_owned(); + let mut other_sorted = other.to_owned(); + this_sorted.sort_by(&compare); + other_sorted.sort_by(&compare); + vec_matches(&this_sorted, &other_sorted, item_matches) +} + +fn vec_matches_result_sorted_by( + this: &[T], + other: &[T], + compare: impl Fn(&T, &T) -> std::cmp::Ordering, + item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, +) -> Result<(), MatchFailure> { + check_match_eq!(this.len(), other.len()); + let mut this_sorted = this.to_owned(); + let mut other_sorted = other.to_owned(); + this_sorted.sort_by(&compare); + other_sorted.sort_by(&compare); + std::iter::zip(&this_sorted, &other_sorted) + .try_fold((), |_acc, (this, other)| item_matches(this, other))?; + Ok(()) +} + +// `this` vector includes `other` vector as a set +fn vec_includes_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { + other.iter().all(|other_node| { + this.iter() + .any(|this_node| item_matches(this_node, other_node)) + }) +} + +// performs a set comparison, ignoring order +fn vec_matches_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { + // Set-inclusion test in both directions + this.len() == other.len() + && vec_includes_as_set(this, other, &item_matches) + && vec_includes_as_set(other, this, &item_matches) +} + +// Forward/reverse mappings from one Vec items (indices) to another. +type VecMapping = (HashMap, HashMap); + +// performs a set comparison, ignoring order +// and returns a mapping from `this` to `other`. +fn vec_matches_as_set_with_mapping( + this: &[T], + other: &[T], + item_matches: impl Fn(&T, &T) -> bool, +) -> VecMapping { + // Set-inclusion test in both directions + // - record forward/reverse mapping from this items <-> other items for reporting mismatches + let mut forward_map: HashMap = HashMap::new(); + let mut reverse_map: HashMap = HashMap::new(); + for (this_pos, this_node) in this.iter().enumerate() { + if let Some(other_pos) = other + .iter() + .position(|other_node| item_matches(this_node, other_node)) + { + forward_map.insert(this_pos, other_pos); + reverse_map.insert(other_pos, this_pos); + } + } + for (other_pos, other_node) in other.iter().enumerate() { + if reverse_map.contains_key(&other_pos) { + continue; + } + if let Some(this_pos) = this + .iter() + .position(|this_node| item_matches(this_node, other_node)) + { + forward_map.insert(this_pos, other_pos); + reverse_map.insert(other_pos, this_pos); + } + } + (forward_map, reverse_map) +} + +// Returns a formatted mismatch message and an optional pair of mismatched positions if the pair +// are the only remaining unmatched items. +fn format_mismatch_as_set( + this_len: usize, + other_len: usize, + forward_map: &HashMap, + reverse_map: &HashMap, +) -> Result<(String, Option<(usize, usize)>), std::fmt::Error> { + let mut ret = String::new(); + let buf = &mut ret; + write!(buf, "- mapping from left to right: [")?; + let mut this_missing_pos = None; + for this_pos in 0..this_len { + if this_pos != 0 { + write!(buf, ", ")?; + } + if let Some(other_pos) = forward_map.get(&this_pos) { + write!(buf, "{}", other_pos)?; + } else { + this_missing_pos = Some(this_pos); + write!(buf, "?")?; + } + } + writeln!(buf, "]")?; + + write!(buf, "- left-over on the right: [")?; + let mut other_missing_count = 0; + let mut other_missing_pos = None; + for other_pos in 0..other_len { + if reverse_map.get(&other_pos).is_none() { + if other_missing_count != 0 { + write!(buf, ", ")?; + } + other_missing_count += 1; + other_missing_pos = Some(other_pos); + write!(buf, "{}", other_pos)?; + } + } + write!(buf, "]")?; + let unmatched_pair = if let (Some(this_missing_pos), Some(other_missing_pos)) = + (this_missing_pos, other_missing_pos) + { + if this_len == 1 + forward_map.len() && other_len == 1 + reverse_map.len() { + // Special case: There are only one missing item on each side. They are supposed to + // match each other. + Some((this_missing_pos, other_missing_pos)) + } else { + None + } + } else { + None + }; + Ok((ret, unmatched_pair)) +} + +fn vec_matches_result_as_set( + this: &[T], + other: &[T], + item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, +) -> Result { + // Set-inclusion test in both directions + // - record forward/reverse mapping from this items <-> other items for reporting mismatches + let (forward_map, reverse_map) = + vec_matches_as_set_with_mapping(this, other, |a, b| item_matches(a, b).is_ok()); + if forward_map.len() == this.len() && reverse_map.len() == other.len() { + Ok((forward_map, reverse_map)) + } else { + // report mismatch + let Ok((message, unmatched_pair)) = + format_mismatch_as_set(this.len(), other.len(), &forward_map, &reverse_map) + else { + // Exception: Unable to format mismatch report => fallback to most generic message + return Err(MatchFailure::new( + "mismatch at vec_matches_result_as_set (failed to format mismatched sets)" + .to_string(), + )); + }; + if let Some(unmatched_pair) = unmatched_pair { + // found a unique pair to report => use that pair's error message + let Err(err) = item_matches(&this[unmatched_pair.0], &other[unmatched_pair.1]) else { + // Exception: Unable to format unique pair mismatch error => fallback to overall report + return Err(MatchFailure::new(format!( + "mismatched sets (failed to format unique pair mismatch error):\n{}", + message + ))); + }; + Err(err.add_description(&format!( + "under a sole unmatched pair ({} -> {}) in a set comparison", + unmatched_pair.0, unmatched_pair.1 + ))) + } else { + Err(MatchFailure::new(format!("mismatched sets:\n{}", message))) + } + } +} + +//================================================================================================== +// PlanNode comparison functions + +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_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_result_as_set(this, other, plan_node_matches) + .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::Defer { primary, deferred }, + PlanNode::Defer { + primary: other_primary, + deferred: other_deferred, + }, + ) => { + defer_primary_node_matches(primary, other_primary)?; + vec_matches_result(deferred, other_deferred, deferred_node_matches)?; + } + ( + PlanNode::Subscription { primary, rest }, + PlanNode::Subscription { + primary: other_primary, + rest: other_rest, + }, + ) => { + subscription_primary_matches(primary, other_primary)?; + opt_plan_node_matches(rest, other_rest) + .map_err(|err| err.add_description("under Subscription"))?; + } + ( + PlanNode::Condition { + condition, + if_clause, + else_clause, + }, + PlanNode::Condition { + condition: other_condition, + if_clause: other_if_clause, + 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)"))?; + } + _ => { + return Err(MatchFailure::new(format!( + "mismatched plan node types\nleft: {:?}\nright: {:?}", + this, other + ))) + } + }; + Ok(()) +} + +pub(crate) fn opt_plan_node_matches( + this: &Option>, + other: &Option>, +) -> Result<(), MatchFailure> { + match (this, other) { + (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()), + } +} + +fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> Result<(), MatchFailure> { + let FetchNode { + service_name, + requires, + variable_usages, + operation, + // ignored: + // reordered parallel fetches may have different names + operation_name: _, + operation_kind, + id, + input_rewrites, + output_rewrites, + context_rewrites, + // ignored + schema_aware_hash: _, + // ignored: + // when running in comparison mode, the rust plan node does not have + // the attached cache key metadata for authorisation, since the rust plan is + // not going to be the one being executed. + authorization: _, + } = this; + + check_match_eq!(*service_name, other.service_name); + check_match_eq!(*operation_kind, other.operation_kind); + check_match_eq!(*id, other.id); + check_match!(same_requires(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, +) -> Result<(), MatchFailure> { + let SubscriptionNode { + service_name, + variable_usages, + operation, + operation_name: _, // ignored (reordered parallel fetches may have different names) + operation_kind, + input_rewrites, + output_rewrites, + } = this; + check_match_eq!(*service_name, other.service_name); + check_match_eq!(*operation_kind, other.operation_kind); + 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)); + operation_matches(operation, &other.operation)?; + Ok(()) +} + +fn defer_primary_node_matches(this: &Primary, other: &Primary) -> Result<(), MatchFailure> { + let Primary { subselection, node } = this; + opt_document_string_matches(subselection, &other.subselection) + .map_err(|err| err.add_description("under defer primary subselection"))?; + opt_plan_node_matches(node, &other.node) + .map_err(|err| err.add_description("under defer primary plan node")) +} + +fn deferred_node_matches(this: &DeferredNode, other: &DeferredNode) -> Result<(), MatchFailure> { + let DeferredNode { + depends, + label, + query_path, + subselection, + node, + } = this; + + check_match_eq!(*depends, other.depends); + check_match_eq!(*label, other.label); + check_match_eq!(*query_path, other.query_path); + opt_document_string_matches(subselection, &other.subselection) + .map_err(|err| err.add_description("under deferred subselection"))?; + opt_plan_node_matches(node, &other.node) + .map_err(|err| err.add_description("under deferred node")) +} + +fn flatten_node_matches(this: &FlattenNode, other: &FlattenNode) -> Result<(), MatchFailure> { + let FlattenNode { path, node } = this; + check_match_eq!(*path, other.path); + plan_node_matches(node, &other.node) +} + +// Copied and modified from `apollo_federation::operation::SelectionKey` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum SelectionKey { + Field { + /// The field alias (if specified) or field name in the resulting selection set. + response_name: Name, + directives: ast::DirectiveList, + }, + FragmentSpread { + /// The name of the fragment. + fragment_name: Name, + directives: ast::DirectiveList, + }, + InlineFragment { + /// The optional type condition of the fragment. + type_condition: Option, + directives: ast::DirectiveList, + }, +} + +fn get_selection_key(selection: &Selection) -> SelectionKey { + match selection { + Selection::Field(field) => SelectionKey::Field { + response_name: field.response_name().clone(), + directives: Default::default(), + }, + Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { + type_condition: fragment.type_condition.clone(), + directives: Default::default(), + }, + } +} + +fn hash_value(x: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + x.hash(&mut hasher); + hasher.finish() +} + +fn hash_selection_key(selection: &Selection) -> u64 { + hash_value(&get_selection_key(selection)) +} + +// Note: This `Selection` struct is a limited version used for the `requires` field. +fn same_selection(x: &Selection, y: &Selection) -> bool { + match (x, y) { + (Selection::Field(x), Selection::Field(y)) => { + x.name == y.name + && x.alias == y.alias + && match (&x.selections, &y.selections) { + (Some(x), Some(y)) => same_selection_set_sorted(x, y), + (None, None) => true, + _ => false, + } + } + (Selection::InlineFragment(x), Selection::InlineFragment(y)) => { + x.type_condition == y.type_condition + && same_selection_set_sorted(&x.selections, &y.selections) + } + _ => false, + } +} + +fn same_selection_set_sorted(x: &[Selection], y: &[Selection]) -> bool { + fn sorted_by_selection_key(s: &[Selection]) -> Vec<&Selection> { + let mut sorted: Vec<&Selection> = s.iter().collect(); + sorted.sort_by_key(|x| hash_selection_key(x)); + sorted + } + + if x.len() != y.len() { + return false; + } + sorted_by_selection_key(x) + .into_iter() + .zip(sorted_by_selection_key(y)) + .all(|(x, y)| same_selection(x, y)) +} + +fn same_requires(x: &[Selection], y: &[Selection]) -> bool { + vec_matches_as_set(x, y, same_selection) +} + +fn same_rewrites(x: &Option>, y: &Option>) -> bool { + match (x, y) { + (None, None) => true, + (Some(x), Some(y)) => vec_matches_as_set(x, y, |a, b| a == b), + _ => false, + } +} + +fn operation_matches( + this: &SubgraphOperation, + other: &SubgraphOperation, +) -> Result<(), MatchFailure> { + document_str_matches(this.as_serialized(), other.as_serialized()) +} + +// Compare operation document strings such as query or just selection set. +fn document_str_matches(this: &str, other: &str) -> Result<(), MatchFailure> { + let this_ast = match ast::Document::parse(this, "this_operation.graphql") { + Ok(document) => document, + Err(_) => { + return Err(MatchFailure::new( + "Failed to parse this operation".to_string(), + )); + } + }; + let other_ast = match ast::Document::parse(other, "other_operation.graphql") { + Ok(document) => document, + Err(_) => { + return Err(MatchFailure::new( + "Failed to parse other operation".to_string(), + )); + } + }; + same_ast_document(&this_ast, &other_ast) +} + +fn opt_document_string_matches( + this: &Option, + other: &Option, +) -> Result<(), MatchFailure> { + match (this, other) { + (None, None) => Ok(()), + (Some(this_sel), Some(other_sel)) => document_str_matches(this_sel, other_sel), + _ => Err(MatchFailure::new(format!( + "mismatched at opt_document_string_matches\nleft: {:?}\nright: {:?}", + this, other + ))), + } +} + +//================================================================================================== +// AST comparison functions + +fn same_ast_document(x: &ast::Document, y: &ast::Document) -> Result<(), MatchFailure> { + fn split_definitions( + doc: &ast::Document, + ) -> ( + Vec<&ast::OperationDefinition>, + Vec<&ast::FragmentDefinition>, + Vec<&ast::Definition>, + ) { + let mut operations: Vec<&ast::OperationDefinition> = Vec::new(); + let mut fragments: Vec<&ast::FragmentDefinition> = Vec::new(); + let mut others: Vec<&ast::Definition> = Vec::new(); + for def in doc.definitions.iter() { + match def { + ast::Definition::OperationDefinition(op) => operations.push(op), + ast::Definition::FragmentDefinition(frag) => fragments.push(frag), + _ => others.push(def), + } + } + (operations, fragments, others) + } + + let (x_ops, x_frags, x_others) = split_definitions(x); + let (y_ops, y_frags, y_others) = split_definitions(y); + + debug_assert!(x_others.is_empty(), "Unexpected definition types"); + debug_assert!(y_others.is_empty(), "Unexpected definition types"); + debug_assert!( + x_ops.len() == y_ops.len(), + "Different number of operation definitions" + ); + + check_match_eq!(x_frags.len(), y_frags.len()); + let mut fragment_map: HashMap = HashMap::new(); + // Assumption: x_frags and y_frags are topologically sorted. + // Thus, we can build the fragment name mapping in a single pass and compare + // fragment definitions using the mapping at the same time, since earlier fragments + // will never reference later fragments. + x_frags.iter().try_fold((), |_, x_frag| { + let y_frag = y_frags + .iter() + .find(|y_frag| same_ast_fragment_definition(x_frag, y_frag, &fragment_map).is_ok()); + if let Some(y_frag) = y_frag { + if x_frag.name != y_frag.name { + // record it only if they are not identical + fragment_map.insert(x_frag.name.clone(), y_frag.name.clone()); + } + Ok(()) + } else { + Err(MatchFailure::new(format!( + "mismatch: no matching fragment definition for {}", + x_frag.name + ))) + } + })?; + + 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, &fragment_map) + .map_err(|err| err.add_description("under operation definition")) + })?; + Ok(()) +} + +fn same_ast_operation_definition( + x: &ast::OperationDefinition, + y: &ast::OperationDefinition, + fragment_map: &HashMap, +) -> Result<(), MatchFailure> { + // Note: Operation names are ignored, since parallel fetches may have different names. + check_match_eq!(x.operation_type, y.operation_type); + vec_matches_result_sorted_by( + &x.variables, + &y.variables, + |a, b| a.name.cmp(&b.name), + |a, b| same_variable_definition(a, b), + ) + .map_err(|err| err.add_description("under Variable definition"))?; + check_match_eq!(x.directives, y.directives); + check_match!(same_ast_selection_set_sorted( + &x.selection_set, + &y.selection_set, + fragment_map, + )); + Ok(()) +} + +// `x` may be coerced to `y`. +// - `x` should be a value from JS QP. +// - `y` should be a value from Rust QP. +// - Assume: x and y are already checked not equal. +// Due to coercion differences, we need to compare AST values with special cases. +fn ast_value_maybe_coerced_to(x: &ast::Value, y: &ast::Value) -> bool { + match (x, y) { + // Special case 1: JS QP may convert an enum value into string. + // - In this case, compare them as strings. + (ast::Value::String(ref x), ast::Value::Enum(ref y)) => { + if x == y.as_str() { + return true; + } + } + + // Special case 2: Rust QP expands a object value by filling in its + // default field values. + // - If the Rust QP object value subsumes the JS QP object value, consider it a match. + // - Assuming the Rust QP object value has only default field values. + // - Warning: This is an unsound heuristic. + (ast::Value::Object(ref x), ast::Value::Object(ref y)) => { + if vec_includes_as_set(y, x, |(yy_name, yy_val), (xx_name, xx_val)| { + xx_name == yy_name + && (xx_val == yy_val || ast_value_maybe_coerced_to(xx_val, yy_val)) + }) { + return true; + } + } + + // Special case 3: JS QP may convert string to int for custom scalars, while Rust doesn't. + // - Note: This conversion seems a bit difficult to implement in the `apollo-federation`'s + // `coerce_value` function, since IntValue's constructor is private to the crate. + (ast::Value::Int(ref x), ast::Value::String(ref y)) => { + if x.as_str() == y { + return true; + } + } + + // Recurse into list items. + (ast::Value::List(ref x), ast::Value::List(ref y)) => { + if vec_matches(x, y, |xx, yy| { + xx == yy || ast_value_maybe_coerced_to(xx, yy) + }) { + return true; + } + } + + _ => {} // otherwise, fall through + } + false +} + +// Use this function, instead of `VariableDefinition`'s `PartialEq` implementation, +// due to known differences. +fn same_variable_definition( + x: &ast::VariableDefinition, + y: &ast::VariableDefinition, +) -> Result<(), MatchFailure> { + check_match_eq!(x.name, y.name); + check_match_eq!(x.ty, y.ty); + if x.default_value != y.default_value { + if let (Some(x), Some(y)) = (&x.default_value, &y.default_value) { + if ast_value_maybe_coerced_to(x, y) { + return Ok(()); + } + } + + return Err(MatchFailure::new(format!( + "mismatch between default values:\nleft: {:?}\nright: {:?}", + x.default_value, y.default_value + ))); + } + check_match_eq!(x.directives, y.directives); + Ok(()) +} + +fn same_ast_fragment_definition( + x: &ast::FragmentDefinition, + y: &ast::FragmentDefinition, + fragment_map: &HashMap, +) -> Result<(), MatchFailure> { + // Note: Fragment names at definitions are ignored. + 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, + fragment_map, + )); + Ok(()) +} + +fn same_ast_argument_value(x: &ast::Value, y: &ast::Value) -> bool { + x == y || ast_value_maybe_coerced_to(x, y) +} + +fn same_ast_argument(x: &ast::Argument, y: &ast::Argument) -> bool { + x.name == y.name && same_ast_argument_value(&x.value, &y.value) +} + +fn same_ast_arguments(x: &[Node], y: &[Node]) -> bool { + vec_matches_sorted_by( + x, + y, + |a, b| a.name.cmp(&b.name), + |a, b| same_ast_argument(a, b), + ) +} + +fn same_directives(x: &ast::DirectiveList, y: &ast::DirectiveList) -> bool { + vec_matches_sorted_by( + x, + y, + |a, b| a.name.cmp(&b.name), + |a, b| a.name == b.name && same_ast_arguments(&a.arguments, &b.arguments), + ) +} + +fn get_ast_selection_key( + selection: &ast::Selection, + fragment_map: &HashMap, +) -> SelectionKey { + match selection { + ast::Selection::Field(field) => SelectionKey::Field { + response_name: field.response_name().clone(), + directives: field.directives.clone(), + }, + ast::Selection::FragmentSpread(fragment) => SelectionKey::FragmentSpread { + fragment_name: fragment_map + .get(&fragment.fragment_name) + .unwrap_or(&fragment.fragment_name) + .clone(), + directives: fragment.directives.clone(), + }, + ast::Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { + type_condition: fragment.type_condition.clone(), + directives: fragment.directives.clone(), + }, + } +} + +fn same_ast_selection( + x: &ast::Selection, + y: &ast::Selection, + fragment_map: &HashMap, +) -> bool { + match (x, y) { + (ast::Selection::Field(x), ast::Selection::Field(y)) => { + x.name == y.name + && x.alias == y.alias + && same_ast_arguments(&x.arguments, &y.arguments) + && same_directives(&x.directives, &y.directives) + && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) + } + (ast::Selection::FragmentSpread(x), ast::Selection::FragmentSpread(y)) => { + let mapped_fragment_name = fragment_map + .get(&x.fragment_name) + .unwrap_or(&x.fragment_name); + *mapped_fragment_name == y.fragment_name + && same_directives(&x.directives, &y.directives) + } + (ast::Selection::InlineFragment(x), ast::Selection::InlineFragment(y)) => { + x.type_condition == y.type_condition + && same_directives(&x.directives, &y.directives) + && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) + } + _ => false, + } +} + +fn hash_ast_selection_key(selection: &ast::Selection, fragment_map: &HashMap) -> u64 { + hash_value(&get_ast_selection_key(selection, fragment_map)) +} + +// Selections are sorted and compared after renaming x's fragment spreads according to the +// fragment_map. +fn same_ast_selection_set_sorted( + x: &[ast::Selection], + y: &[ast::Selection], + fragment_map: &HashMap, +) -> bool { + fn sorted_by_selection_key<'a>( + s: &'a [ast::Selection], + fragment_map: &HashMap, + ) -> Vec<&'a ast::Selection> { + let mut sorted: Vec<&ast::Selection> = s.iter().collect(); + sorted.sort_by_key(|x| hash_ast_selection_key(x, fragment_map)); + sorted + } + + if x.len() != y.len() { + return false; + } + let x_sorted = sorted_by_selection_key(x, fragment_map); // Map fragment spreads + let y_sorted = sorted_by_selection_key(y, &Default::default()); // Don't map fragment spreads + x_sorted + .into_iter() + .zip(y_sorted) + .all(|(x, y)| same_ast_selection(x, y, fragment_map)) +} + +//================================================================================================== +// Unit tests + +#[cfg(test)] +mod ast_comparison_tests { + use super::*; + + #[test] + fn test_query_variable_decl_order() { + let op_x = r#"query($qv2: String!, $qv1: Int!) { x(arg1: $qv1, arg2: $qv2) }"#; + 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).is_ok()); + } + + #[test] + fn test_query_variable_decl_enum_value_coercion() { + // Note: JS QP converts enum default values into strings. + let op_x = r#"query($qv1: E! = "default_value") { x(arg1: $qv1) }"#; + let op_y = r#"query($qv1: E! = default_value) { x(arg1: $qv1) }"#; + 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).is_ok()); + } + + #[test] + fn test_query_variable_decl_object_value_coercion_empty_case() { + // Note: Rust QP expands empty object default values by filling in its default field + // values. + let op_x = r#"query($qv1: T! = {}) { x(arg1: $qv1) }"#; + let op_y = + r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; + 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).is_ok()); + } + + #[test] + fn test_query_variable_decl_object_value_coercion_non_empty_case() { + // Note: Rust QP expands an object default values by filling in its default field values. + let op_x = r#"query($qv1: T! = {field1: true}) { x(arg1: $qv1) }"#; + let op_y = + r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; + 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).is_ok()); + } + + #[test] + fn test_query_variable_decl_list_of_object_value_coercion() { + // Testing a combination of list and object value coercion. + let op_x = r#"query($qv1: [T!]! = [{}]) { x(arg1: $qv1) }"#; + let op_y = + r#"query($qv1: [T!]! = [{field1: true, field2: "default_value"}]) { x(arg1: $qv1) }"#; + 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).is_ok()); + } + + #[test] + fn test_entities_selection_order() { + let op_x = r#" + query subgraph1__1($representations: [_Any!]!) { + _entities(representations: $representations) { x { w } y } + } + "#; + let op_y = r#" + query subgraph1__1($representations: [_Any!]!) { + _entities(representations: $representations) { y x { 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).is_ok()); + } + + #[test] + fn test_top_level_selection_order() { + let op_x = r#"{ x { w z } y }"#; + 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).is_ok()); + } + + #[test] + fn test_fragment_definition_order() { + let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; + 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).is_ok()); + } + + #[test] + fn test_selection_argument_is_compared() { + let op_x = r#"{ x(arg1: "one") }"#; + let op_y = r#"{ x(arg1: "two") }"#; + 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).is_err()); + } + + #[test] + fn test_selection_argument_order() { + let op_x = r#"{ x(arg1: "one", arg2: "two") }"#; + let op_y = r#"{ x(arg2: "two", arg1: "one") }"#; + 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).is_ok()); + } + + #[test] + fn test_selection_directive_order() { + let op_x = r#"{ x @include(if:true) @skip(if:false) }"#; + let op_y = r#"{ x @skip(if:false) @include(if:true) }"#; + 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).is_ok()); + } + + #[test] + fn test_string_to_id_coercion_difference() { + // JS QP coerces strings into integer for ID type, while Rust QP doesn't. + // This tests a special case that same_ast_document accepts this difference. + let op_x = r#"{ x(id: 123) }"#; + let op_y = r#"{ x(id: "123") }"#; + 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).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names() { + let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; + let op_y = r#"{ q { ...g1 ...g2 } } fragment g1 on T { x y } fragment g2 on T { w z }"#; + 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).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names_nested_1() { + // Nested fragments have the same name, only top-level fragments have different names. + let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; + let op_y = r#"{ q { ...g2 } } fragment f1 on T { x y } fragment g2 on T { z ...f1 }"#; + 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).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names_nested_2() { + // Nested fragments have different names. + let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; + let op_y = r#"{ q { ...g2 } } fragment g1 on T { x y } fragment g2 on T { z ...g1 }"#; + 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).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names_nested_3() { + // Nested fragments have different names. + // Also, fragment definitions are in different order. + let op_x = r#"{ q { ...f2 ...f3 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 } fragment f3 on T { w } "#; + let op_y = r#"{ q { ...g2 ...g3 } } fragment g1 on T { x y } fragment g2 on T { w } fragment g3 on T { z ...g1 }"#; + 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).is_ok()); + } +} + +#[cfg(test)] +mod qp_selection_comparison_tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_requires_comparison_with_same_selection_key() { + let requires_json = json!([ + { + "kind": "InlineFragment", + "typeCondition": "T", + "selections": [ + { + "kind": "Field", + "name": "id", + }, + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "T", + "selections": [ + { + "kind": "Field", + "name": "id", + }, + { + "kind": "Field", + "name": "job", + } + ] + }, + ]); + + // The only difference between requires1 and requires2 is the order of selections. + // But, their items all have the same SelectionKey. + let requires1: Vec = serde_json::from_value(requires_json).unwrap(); + let requires2: Vec = requires1.iter().rev().cloned().collect(); + + // `same_selection_set_sorted` fails to match, since it doesn't account for + // two items with the same SelectionKey but in different order. + assert!(!same_selection_set_sorted(&requires1, &requires2)); + // `same_requires` should succeed. + assert!(same_requires(&requires1, &requires2)); + } +} From b998167fe11a24813ea112a8220df079189ee99f Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Sat, 7 Dec 2024 02:47:03 +0100 Subject: [PATCH 087/112] Use the new query planner by default (#6360) Co-authored-by: Iryna Shestak --- apollo-router/src/batching.rs | 4 +- apollo-router/src/configuration/metrics.rs | 1 + apollo-router/src/configuration/mod.rs | 5 ++- ...cs__test__experimental_mode_metrics_2.snap | 2 +- ...nfiguration__tests__schema_generation.snap | 7 ++++ ...uter__plugins__cache__tests__insert-5.snap | 4 +- ...router__plugins__cache__tests__insert.snap | 4 +- ...ter__plugins__cache__tests__no_data-3.snap | 5 +-- ...outer__plugins__cache__tests__no_data.snap | 4 +- ...ter__plugins__cache__tests__private-3.snap | 5 +-- ...ter__plugins__cache__tests__private-5.snap | 5 +-- ...outer__plugins__cache__tests__private.snap | 5 +-- ...dden_field_yields_expected_query_plan.snap | 4 +- ...dden_field_yields_expected_query_plan.snap | 8 ++-- ...y_plan__tests__it_expose_query_plan-2.snap | 38 +++++++++---------- ...ery_plan__tests__it_expose_query_plan.snap | 38 +++++++++---------- .../src/query_planner/bridge_query_planner.rs | 21 +++++++--- .../query_planner/caching_query_planner.rs | 6 +++ ...sts__escaped_quotes_in_string_literal.snap | 4 +- apollo-router/src/services/router/tests.rs | 2 +- apollo-router/tests/integration/batching.rs | 14 +++---- .../tests/integration/file_upload.rs | 6 +-- .../tests/integration/query_planner.rs | 22 +++++++++++ apollo-router/tests/integration/redis.rs | 22 +++++------ ...tegration__redis__query_planner_cache.snap | 4 +- apollo-router/tests/samples/README.md | 23 ++++++++++- .../samples/basic/interface-object/plan.json | 10 ++--- .../tests/samples/core/defer/plan.json | 6 +-- .../tests/samples/core/query1/plan.json | 4 +- .../enterprise/entity-cache/defer/plan.json | 6 +-- .../invalidation-entity-key/plan.json | 13 ++++--- .../invalidation-subgraph-name/plan.json | 6 +-- .../invalidation-subgraph-type/plan.json | 10 ++++- .../enterprise/entity-cache/private/plan.json | 4 +- .../basic/configuration.yaml | 2 + .../persisted-queries/basic/plan.yaml | 6 +-- .../enterprise/query-planning-redis/plan.json | 4 +- apollo-router/tests/samples_tests.rs | 37 ++++++++++-------- apollo-router/tests/set_context.rs | 1 + 39 files changed, 227 insertions(+), 145 deletions(-) diff --git a/apollo-router/src/batching.rs b/apollo-router/src/batching.rs index a66aca8d87..daa7884d99 100644 --- a/apollo-router/src/batching.rs +++ b/apollo-router/src/batching.rs @@ -741,7 +741,7 @@ mod tests { // Extract info about this operation let (subgraph, count): (String, usize) = { - let re = regex::Regex::new(r"entry([AB])\(count:([0-9]+)\)").unwrap(); + let re = regex::Regex::new(r"entry([AB])\(count: ?([0-9]+)\)").unwrap(); let captures = re.captures(requests[0].query.as_ref().unwrap()).unwrap(); (captures[1].to_string(), captures[2].parse().unwrap()) @@ -757,7 +757,7 @@ mod tests { assert_eq!( request.query, Some(format!( - "query op{index}__{}__0{{entry{}(count:{count}){{index}}}}", + "query op{index}__{}__0 {{ entry{}(count: {count}) {{ index }} }}", subgraph.to_lowercase(), subgraph )) diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index 6ea8121a69..7241950fd7 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -565,6 +565,7 @@ impl InstrumentData { super::QueryPlannerMode::Both => "both", super::QueryPlannerMode::BothBestEffort => "both_best_effort", super::QueryPlannerMode::New => "new", + super::QueryPlannerMode::NewBestEffort => "new_best_effort", }; self.data.insert( diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index e0d6ed6a03..8bdc97b52a 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -224,8 +224,11 @@ pub(crate) enum QueryPlannerMode { /// Falls back to `legacy` with a warning /// if the the new planner does not support the schema /// (such as using legacy Apollo Federation 1) - #[default] BothBestEffort, + /// Use the new Rust-based implementation but fall back to the legacy one + /// for supergraph schemas composed with legacy Apollo Federation 1. + #[default] + NewBestEffort, } impl<'de> serde::Deserialize<'de> for Configuration { diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap index 43cb1b8568..e976e8391d 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap @@ -7,4 +7,4 @@ expression: "&metrics.non_zero()" datapoints: - value: 1 attributes: - mode: both_best_effort + mode: new_best_effort diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 36d76b53ec..4ae8457f17 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -4483,6 +4483,13 @@ expression: "&schema" "both_best_effort" ], "type": "string" + }, + { + "description": "Use the new Rust-based implementation but fall back to the legacy one for supergraph schemas composed with legacy Apollo Federation 1.", + "enum": [ + "new_best_effort" + ], + "type": "string" } ] }, diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap index 19faf7f003..ed078770d9 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap @@ -4,12 +4,12 @@ expression: cache_keys --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:ab9056ba140750aa8fe58360172b450fa717e7ea177e4a3c9426fe1291a88da2:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "public" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:user:type:Query:hash:0d4d253b049bbea514a54a892902fa4b9b658aedc9b8f2a1308323cdeef3c0ca:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "public" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap index cffe19303d..a49a81580f 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap @@ -4,12 +4,12 @@ expression: cache_keys --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:ab9056ba140750aa8fe58360172b450fa717e7ea177e4a3c9426fe1291a88da2:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "public" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:user:type:Query:hash:0d4d253b049bbea514a54a892902fa4b9b658aedc9b8f2a1308323cdeef3c0ca:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "public" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap index 473208cb48..fbe291783c 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap @@ -1,16 +1,15 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys -snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:4913f52405bb614177e7c718d43da695c2f0e7411707c2f77f1c62380153c8d8:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "[REDACTED]" }, { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4913f52405bb614177e7c718d43da695c2f0e7411707c2f77f1c62380153c8d8:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "cached", "cache_control": "[REDACTED]" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap index 1c84fc3dab..d32bd8453c 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap @@ -4,12 +4,12 @@ expression: cache_keys --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:4913f52405bb614177e7c718d43da695c2f0e7411707c2f77f1c62380153c8d8:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "[REDACTED]" }, { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:392508655d0b54b6c8e439eabbe356c4d54a9fdc323ac9b93e48efbc29581170:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4913f52405bb614177e7c718d43da695c2f0e7411707c2f77f1c62380153c8d8:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "[REDACTED]" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap index 3d98d643ef..76fd27f7fa 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap @@ -1,16 +1,15 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys -snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:ab9056ba140750aa8fe58360172b450fa717e7ea177e4a3c9426fe1291a88da2:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:user:type:Query:hash:0d4d253b049bbea514a54a892902fa4b9b658aedc9b8f2a1308323cdeef3c0ca:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap index 3d98d643ef..76fd27f7fa 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap @@ -1,16 +1,15 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys -snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:ab9056ba140750aa8fe58360172b450fa717e7ea177e4a3c9426fe1291a88da2:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:user:type:Query:hash:0d4d253b049bbea514a54a892902fa4b9b658aedc9b8f2a1308323cdeef3c0ca:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "cached", "cache_control": "private" } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap index afe8ae3850..65da6044f9 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap @@ -1,16 +1,15 @@ --- source: apollo-router/src/plugins/cache/tests.rs expression: cache_keys -snapshot_kind: text --- [ { - "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:3dc2642699f191f49fb769eb467c38e806266f6b1aa2f71b633acdeea0a6784e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:ab9056ba140750aa8fe58360172b450fa717e7ea177e4a3c9426fe1291a88da2:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", "status": "new", "cache_control": "private" }, { - "key": "version:1.0:subgraph:user:type:Query:hash:9ddb01f2d3c4613e328614598b1a3f5ee8833afd5b52c1157aec7a251bcfa4cd:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "version:1.0:subgraph:user:type:Query:hash:0d4d253b049bbea514a54a892902fa4b9b658aedc9b8f2a1308323cdeef3c0ca:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "status": "new", "cache_control": "private" } diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap index 9ae3534578..de68830bfe 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap @@ -12,14 +12,14 @@ expression: query_plan "kind": "Fetch", "serviceName": "Subgraph2", "variableUsages": [], - "operation": "{percent0{foo}}", + "operation": "{ percent0 { foo } }", "operationName": null, "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "cdab250089cc24ee95f749d187d2f936878348e62061b85fe6d1dccb9f4c26a1", + "schemaAwareHash": "343157a7d5b7929ebdc0c17cbf0f23c8d3cf0c93a820856d3a189521cc2f24a2", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap index 22296803a2..2967a7d6f7 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap @@ -17,14 +17,14 @@ expression: query_plan "kind": "Fetch", "serviceName": "Subgraph1", "variableUsages": [], - "operation": "{percent100{__typename id}}", + "operation": "{ percent100 { __typename id } }", "operationName": null, "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "113c32833cf7c2bb4324b08c068eb6613ebe0f77efaf3098ae6f0ed7b2df11de", + "schemaAwareHash": "df2a0633d70ab97805722bae920647da51b7eb821b06d8a2499683c5c7024316", "authorization": { "is_authenticated": false, "scopes": [], @@ -56,14 +56,14 @@ expression: query_plan } ], "variableUsages": [], - "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on T{foo}}}", + "operation": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on T { foo } } }", "operationName": null, "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7ef79e08871a3e122407b86d57079c3607cfd26a1993e2c239603a39d04d1bd8", + "schemaAwareHash": "56ac7a7cc11b7f293acbdaf0327cb2b676415eab8343e9259322a1609c90455e", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap index f2a3c247a1..b093e7be08 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap @@ -62,14 +62,14 @@ expression: "serde_json::to_value(response).unwrap()" "variableUsages": [ "first" ], - "operation": "query TopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}", + "operation": "query TopProducts__products__0($first: Int) { topProducts(first: $first) { __typename upc name } }", "operationName": "TopProducts__products__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "2504c66db02c170d0040785e0ac3455155db03beddcb7cc4d16f08ca02201fac", + "schemaAwareHash": "45b4beebcbf1df72ab950db7bd278417712b1aa39119317f44ad5b425bdb6997", "authorization": { "is_authenticated": false, "scopes": [], @@ -102,14 +102,14 @@ expression: "serde_json::to_value(response).unwrap()" } ], "variableUsages": [], - "operation": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{id product{__typename upc}author{__typename id}}}}}", + "operation": "query TopProducts__reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { reviews { id product { __typename upc } author { __typename id } } } } }", "operationName": "TopProducts__reviews__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "0f8abdb350d59e86567b72717be114a465c59ac4e6027d7179de6448b0fbc5a4", + "schemaAwareHash": "645f3f8763133d2376e33ab3d1145be7ded0ccc8e94e20aba1fbaa34a51633da", "authorization": { "is_authenticated": false, "scopes": [], @@ -127,15 +127,15 @@ expression: "serde_json::to_value(response).unwrap()" "@", "reviews", "@", - "product" + "author" ], "node": { "kind": "Fetch", - "serviceName": "products", + "serviceName": "accounts", "requires": [ { "kind": "InlineFragment", - "typeCondition": "Product", + "typeCondition": "User", "selections": [ { "kind": "Field", @@ -143,20 +143,20 @@ expression: "serde_json::to_value(response).unwrap()" }, { "kind": "Field", - "name": "upc" + "name": "id" } ] } ], "variableUsages": [], - "operation": "query TopProducts__products__2($representations:[_Any!]!){_entities(representations:$representations){...on Product{name}}}", - "operationName": "TopProducts__products__2", + "operation": "query TopProducts__accounts__2($representations: [_Any!]!) { _entities(representations: $representations) { ... on User { name } } }", + "operationName": "TopProducts__accounts__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a1e19c2c170464974293f946f09116216e71424a821908beb0062091475dad11", + "schemaAwareHash": "a79f69245d777abc4afbd7d0a8fc434137fa4fd1079ef082edf4c7746b5a0fcd", "authorization": { "is_authenticated": false, "scopes": [], @@ -171,15 +171,15 @@ expression: "serde_json::to_value(response).unwrap()" "@", "reviews", "@", - "author" + "product" ], "node": { "kind": "Fetch", - "serviceName": "accounts", + "serviceName": "products", "requires": [ { "kind": "InlineFragment", - "typeCondition": "User", + "typeCondition": "Product", "selections": [ { "kind": "Field", @@ -187,20 +187,20 @@ expression: "serde_json::to_value(response).unwrap()" }, { "kind": "Field", - "name": "id" + "name": "upc" } ] } ], "variableUsages": [], - "operation": "query TopProducts__accounts__3($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", - "operationName": "TopProducts__accounts__3", + "operation": "query TopProducts__products__3($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { name } } }", + "operationName": "TopProducts__products__3", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "466ef25e373cf367e18ed264f76c7c4b2f27fe2ef105cb39d8528320addaedc7", + "schemaAwareHash": "5ad94764f288a41312e07745510bf5dade2b63fb82c3d896f7d00408dbbe5cce", "authorization": { "is_authenticated": false, "scopes": [], @@ -213,7 +213,7 @@ expression: "serde_json::to_value(response).unwrap()" ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"products\") {\n {\n topProducts(first: $first) {\n __typename\n upc\n name\n }\n }\n },\n Flatten(path: \"topProducts.@\") {\n Fetch(service: \"reviews\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n reviews {\n id\n product {\n __typename\n upc\n }\n author {\n __typename\n id\n }\n }\n }\n }\n },\n },\n Parallel {\n Flatten(path: \"topProducts.@.reviews.@.product\") {\n Fetch(service: \"products\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n name\n }\n }\n },\n },\n Flatten(path: \"topProducts.@.reviews.@.author\") {\n Fetch(service: \"accounts\") {\n {\n ... on User {\n __typename\n id\n }\n } =>\n {\n ... on User {\n name\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"products\") {\n {\n topProducts(first: $first) {\n __typename\n upc\n name\n }\n }\n },\n Flatten(path: \"topProducts.@\") {\n Fetch(service: \"reviews\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n reviews {\n id\n product {\n __typename\n upc\n }\n author {\n __typename\n id\n }\n }\n }\n }\n },\n },\n Parallel {\n Flatten(path: \"topProducts.@.reviews.@.author\") {\n Fetch(service: \"accounts\") {\n {\n ... on User {\n __typename\n id\n }\n } =>\n {\n ... on User {\n name\n }\n }\n },\n },\n Flatten(path: \"topProducts.@.reviews.@.product\") {\n Fetch(service: \"products\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n name\n }\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap index f2a3c247a1..b093e7be08 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap @@ -62,14 +62,14 @@ expression: "serde_json::to_value(response).unwrap()" "variableUsages": [ "first" ], - "operation": "query TopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}", + "operation": "query TopProducts__products__0($first: Int) { topProducts(first: $first) { __typename upc name } }", "operationName": "TopProducts__products__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "2504c66db02c170d0040785e0ac3455155db03beddcb7cc4d16f08ca02201fac", + "schemaAwareHash": "45b4beebcbf1df72ab950db7bd278417712b1aa39119317f44ad5b425bdb6997", "authorization": { "is_authenticated": false, "scopes": [], @@ -102,14 +102,14 @@ expression: "serde_json::to_value(response).unwrap()" } ], "variableUsages": [], - "operation": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{id product{__typename upc}author{__typename id}}}}}", + "operation": "query TopProducts__reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { reviews { id product { __typename upc } author { __typename id } } } } }", "operationName": "TopProducts__reviews__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "0f8abdb350d59e86567b72717be114a465c59ac4e6027d7179de6448b0fbc5a4", + "schemaAwareHash": "645f3f8763133d2376e33ab3d1145be7ded0ccc8e94e20aba1fbaa34a51633da", "authorization": { "is_authenticated": false, "scopes": [], @@ -127,15 +127,15 @@ expression: "serde_json::to_value(response).unwrap()" "@", "reviews", "@", - "product" + "author" ], "node": { "kind": "Fetch", - "serviceName": "products", + "serviceName": "accounts", "requires": [ { "kind": "InlineFragment", - "typeCondition": "Product", + "typeCondition": "User", "selections": [ { "kind": "Field", @@ -143,20 +143,20 @@ expression: "serde_json::to_value(response).unwrap()" }, { "kind": "Field", - "name": "upc" + "name": "id" } ] } ], "variableUsages": [], - "operation": "query TopProducts__products__2($representations:[_Any!]!){_entities(representations:$representations){...on Product{name}}}", - "operationName": "TopProducts__products__2", + "operation": "query TopProducts__accounts__2($representations: [_Any!]!) { _entities(representations: $representations) { ... on User { name } } }", + "operationName": "TopProducts__accounts__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a1e19c2c170464974293f946f09116216e71424a821908beb0062091475dad11", + "schemaAwareHash": "a79f69245d777abc4afbd7d0a8fc434137fa4fd1079ef082edf4c7746b5a0fcd", "authorization": { "is_authenticated": false, "scopes": [], @@ -171,15 +171,15 @@ expression: "serde_json::to_value(response).unwrap()" "@", "reviews", "@", - "author" + "product" ], "node": { "kind": "Fetch", - "serviceName": "accounts", + "serviceName": "products", "requires": [ { "kind": "InlineFragment", - "typeCondition": "User", + "typeCondition": "Product", "selections": [ { "kind": "Field", @@ -187,20 +187,20 @@ expression: "serde_json::to_value(response).unwrap()" }, { "kind": "Field", - "name": "id" + "name": "upc" } ] } ], "variableUsages": [], - "operation": "query TopProducts__accounts__3($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", - "operationName": "TopProducts__accounts__3", + "operation": "query TopProducts__products__3($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { name } } }", + "operationName": "TopProducts__products__3", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "466ef25e373cf367e18ed264f76c7c4b2f27fe2ef105cb39d8528320addaedc7", + "schemaAwareHash": "5ad94764f288a41312e07745510bf5dade2b63fb82c3d896f7d00408dbbe5cce", "authorization": { "is_authenticated": false, "scopes": [], @@ -213,7 +213,7 @@ expression: "serde_json::to_value(response).unwrap()" ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"products\") {\n {\n topProducts(first: $first) {\n __typename\n upc\n name\n }\n }\n },\n Flatten(path: \"topProducts.@\") {\n Fetch(service: \"reviews\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n reviews {\n id\n product {\n __typename\n upc\n }\n author {\n __typename\n id\n }\n }\n }\n }\n },\n },\n Parallel {\n Flatten(path: \"topProducts.@.reviews.@.product\") {\n Fetch(service: \"products\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n name\n }\n }\n },\n },\n Flatten(path: \"topProducts.@.reviews.@.author\") {\n Fetch(service: \"accounts\") {\n {\n ... on User {\n __typename\n id\n }\n } =>\n {\n ... on User {\n name\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"products\") {\n {\n topProducts(first: $first) {\n __typename\n upc\n name\n }\n }\n },\n Flatten(path: \"topProducts.@\") {\n Fetch(service: \"reviews\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n reviews {\n id\n product {\n __typename\n upc\n }\n author {\n __typename\n id\n }\n }\n }\n }\n },\n },\n Parallel {\n Flatten(path: \"topProducts.@.reviews.@.author\") {\n Fetch(service: \"accounts\") {\n {\n ... on User {\n __typename\n id\n }\n } =>\n {\n ... on User {\n name\n }\n }\n },\n },\n Flatten(path: \"topProducts.@.reviews.@.product\") {\n Fetch(service: \"products\") {\n {\n ... on Product {\n __typename\n upc\n }\n } =>\n {\n ... on Product {\n name\n }\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 9259d409d9..09f50945ca 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -139,6 +139,13 @@ impl PlannerMode { Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) } } + QueryPlannerMode::NewBestEffort => { + if let Some(rust) = rust_planner { + Self::Rust(rust) + } else { + Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) + } + } }) } @@ -151,13 +158,15 @@ impl PlannerMode { QueryPlannerMode::New | QueryPlannerMode::Both => { Ok(Some(Self::rust(schema, configuration)?)) } - QueryPlannerMode::BothBestEffort => match Self::rust(schema, configuration) { - Ok(planner) => Ok(Some(planner)), - Err(error) => { - tracing::info!("Falling back to the legacy query planner: {error}"); - Ok(None) + QueryPlannerMode::BothBestEffort | QueryPlannerMode::NewBestEffort => { + match Self::rust(schema, configuration) { + Ok(planner) => Ok(Some(planner)), + Err(error) => { + tracing::info!("Falling back to the legacy query planner: {error}"); + Ok(None) + } } - }, + } } } diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 7b67bf30ec..7b1f8c5279 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -145,6 +145,12 @@ where ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) .hash(&mut hasher); } + crate::configuration::QueryPlannerMode::NewBestEffort => { + "PLANNER-NEW-BEST-EFFORT".hash(&mut hasher); + ConfigMode::Js(Arc::new(configuration.js_query_planner_config())).hash(&mut hasher); + ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) + .hash(&mut hasher); + } }; let config_mode_hash = Arc::new(QueryHash(hasher.finalize())); diff --git a/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap b/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap index 4c8165c12c..c4471110ea 100644 --- a/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap +++ b/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap @@ -35,13 +35,13 @@ expression: "(graphql_response, &subgraph_query_log)" ( "products", Some( - "query TopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}", + "query TopProducts__products__0($first: Int) { topProducts(first: $first) { __typename upc name } }", ), ), ( "reviews", Some( - "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviewsForAuthor(authorID:\"\\\"1\\\"\"){body}}", + "query TopProducts__reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onProduct1_0 } } fragment _generated_onProduct1_0 on Product { reviewsForAuthor(authorID: \"\\\"1\\\"\") { body } }", ), ), ], diff --git a/apollo-router/src/services/router/tests.rs b/apollo-router/src/services/router/tests.rs index 94caafd006..59737d44f3 100644 --- a/apollo-router/src/services/router/tests.rs +++ b/apollo-router/src/services/router/tests.rs @@ -569,5 +569,5 @@ async fn escaped_quotes_in_string_literal() { let subgraph_query = subgraph_query_log[1].1.as_ref().unwrap(); // The string literal made it through unchanged: - assert!(subgraph_query.contains(r#"reviewsForAuthor(authorID:"\"1\"")"#)); + assert!(subgraph_query.contains(r#"reviewsForAuthor(authorID: "\"1\"")"#)); } diff --git a/apollo-router/tests/integration/batching.rs b/apollo-router/tests/integration/batching.rs index 7998a21528..15dfd38de2 100644 --- a/apollo-router/tests/integration/batching.rs +++ b/apollo-router/tests/integration/batching.rs @@ -445,7 +445,7 @@ async fn it_handles_single_request_cancelled_by_rhai() -> Result<(), BoxError> { assert_eq!( request.query, Some(format!( - "query op{index}__b__0{{entryB(count:{REQUEST_COUNT}){{index}}}}", + "query op{index}__b__0 {{ entryB(count: {REQUEST_COUNT}) {{ index }} }}", )) ); } @@ -683,7 +683,7 @@ async fn it_handles_single_request_cancelled_by_coprocessor() -> Result<(), BoxE assert_eq!( request.query, Some(format!( - "query op{index}__a__0{{entryA(count:{REQUEST_COUNT}){{index}}}}", + "query op{index}__a__0 {{ entryA(count: {REQUEST_COUNT}) {{ index }} }}", )) ); } @@ -785,7 +785,7 @@ async fn it_handles_single_invalid_graphql() -> Result<(), BoxError> { assert_eq!( request.query, Some(format!( - "query op{index}__a__0{{entryA(count:{REQUEST_COUNT}){{index}}}}", + "query op{index}__a__0 {{ entryA(count: {REQUEST_COUNT}) {{ index }} }}", )) ); } @@ -927,7 +927,7 @@ mod helper { // Extract info about this operation let (subgraph, count): (String, usize) = { - let re = regex::Regex::new(r"entry([AB])\(count:([0-9]+)\)").unwrap(); + let re = regex::Regex::new(r"entry([AB])\(count: ?([0-9]+)\)").unwrap(); let captures = re.captures(requests[0].query.as_ref().unwrap()).unwrap(); (captures[1].to_string(), captures[2].parse().unwrap()) @@ -943,7 +943,7 @@ mod helper { assert_eq!( request.query, Some(format!( - "query op{index}__{}__0{{entry{}(count:{count}){{index}}}}", + "query op{index}__{}__0 {{ entry{}(count: {count}) {{ index }} }}", subgraph.to_lowercase(), subgraph )) @@ -971,7 +971,7 @@ mod helper { // Extract info about this operation let (subgraph, count): (String, usize) = { - let re = regex::Regex::new(r"entry([AB])\(count:([0-9]+)\)").unwrap(); + let re = regex::Regex::new(r"entry([AB])\(count: ?([0-9]+)\)").unwrap(); let captures = re.captures(requests[0].query.as_ref().unwrap()).unwrap(); (captures[1].to_string(), captures[2].parse().unwrap()) @@ -1010,7 +1010,7 @@ mod helper { // Extract info about this operation let (_, count): (String, usize) = { - let re = regex::Regex::new(r"entry([AB])\(count:([0-9]+)\)").unwrap(); + let re = regex::Regex::new(r"entry([AB])\(count: ?([0-9]+)\)").unwrap(); let captures = re.captures(requests[0].query.as_ref().unwrap()).unwrap(); (captures[1].to_string(), captures[2].parse().unwrap()) diff --git a/apollo-router/tests/integration/file_upload.rs b/apollo-router/tests/integration/file_upload.rs index fd272f9d28..d179f39b66 100644 --- a/apollo-router/tests/integration/file_upload.rs +++ b/apollo-router/tests/integration/file_upload.rs @@ -68,14 +68,14 @@ async fn it_uploads_file_to_subgraph() -> Result<(), BoxError> { assert_eq!(operations_field.name(), Some("operations")); let operations: helper::Operation = serde_json::from_slice(&operations_field.bytes().await.unwrap()).unwrap(); - insta::assert_json_snapshot!(operations, @r#" + insta::assert_json_snapshot!(operations, @r###" { - "query": "mutation SomeMutation__uploads__0($file:Upload){file:singleUpload(file:$file){filename body}}", + "query": "mutation SomeMutation__uploads__0($file: Upload) { file: singleUpload(file: $file) { filename body } }", "variables": { "file": null } } - "#); + "###); let map_field = multipart .next_field() diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 5bf0ca799c..d2834bfd84 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -10,6 +10,7 @@ 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"; +const NEW_BEST_EFFORT_QP: &str = "experimental_query_planner_mode: new_best_effort"; #[tokio::test(flavor = "multi_thread")] async fn fed1_schema_with_legacy_qp() { @@ -81,6 +82,27 @@ async fn fed1_schema_with_both_best_effort_qp() { router.graceful_shutdown().await; } +#[tokio::test(flavor = "multi_thread")] +async fn fed1_schema_with_new_best_effort_qp() { + let mut router = IntegrationTest::builder() + .config(NEW_BEST_EFFORT_QP) + .supergraph("../examples/graphql/supergraph-fed1.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}"); diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index d7428ab4e4..f5e3da3438 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -52,7 +52,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. let known_cache_key = &format!( - "plan:router:{}:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c", + "plan:router:{}:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:924b36a9ae6af4ff198220b1302b14b6329c4beb7c022fd31d6fef82eaad7ccb", env!("CARGO_PKG_VERSION") ); @@ -453,13 +453,13 @@ async fn entity_cache_basic() -> Result<(), BoxError> { // if this is failing due to a cache key change, hook up redis-cli with the MONITOR command to see the keys being set let s:String = client - .get("version:1.0:subgraph:products:type:Query:hash:ff69b4487720d4776dd85eef89ca7a077bbed2f37bbcec6252905cc701415728:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:products:type:Query:hash:5e8ac155fe1fb5b3b69292f89b7df818a39d88a3bf77031a6bd60c22eeb4b242:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); insta::assert_json_snapshot!(v.as_object().unwrap().get("data").unwrap()); - let s: String = client.get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:ea227d6d9f1e595fe4aa16ab7f90e7d3b7676bb065cd836d960e99e1edf94bef:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c").await.unwrap(); + let s: String = client.get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:50354623eb0a347d47a62f002fae74c0f579ee693af1fdb9a1e4744b4723dd2c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c").await.unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); insta::assert_json_snapshot!(v.as_object().unwrap().get("data").unwrap()); @@ -571,7 +571,7 @@ async fn entity_cache_basic() -> Result<(), BoxError> { insta::assert_json_snapshot!(response); let s:String = client - .get("version:1.0:subgraph:reviews:type:Product:entity:d9a4cd73308dd13ca136390c10340823f94c335b9da198d2339c886c738abf0d:hash:ea227d6d9f1e595fe4aa16ab7f90e7d3b7676bb065cd836d960e99e1edf94bef:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:reviews:type:Product:entity:d9a4cd73308dd13ca136390c10340823f94c335b9da198d2339c886c738abf0d:hash:50354623eb0a347d47a62f002fae74c0f579ee693af1fdb9a1e4744b4723dd2c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -800,7 +800,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { insta::assert_json_snapshot!(response); let s:String = client - .get("version:1.0:subgraph:products:type:Query:hash:ff69b4487720d4776dd85eef89ca7a077bbed2f37bbcec6252905cc701415728:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:products:type:Query:hash:5e8ac155fe1fb5b3b69292f89b7df818a39d88a3bf77031a6bd60c22eeb4b242:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -821,7 +821,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { ); let s: String = client - .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:ea227d6d9f1e595fe4aa16ab7f90e7d3b7676bb065cd836d960e99e1edf94bef:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:50354623eb0a347d47a62f002fae74c0f579ee693af1fdb9a1e4744b4723dd2c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -865,7 +865,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { insta::assert_json_snapshot!(response); let s:String = client - .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:00a80cad9114a41b85ea6df444a905f65e12ed82aba261d1716c71863608da35:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") + .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:2253830e3b366dcfdfa4e1acf6afa9e05d3c80ff50171243768a3e416536c89b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c") .await .unwrap(); let v: Value = serde_json::from_str(&s).unwrap(); @@ -992,7 +992,7 @@ async fn query_planner_redis_update_query_fragments() { // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:a55ce3338ce6d5b78566be89cc6a7ad3fe8a7eeb38229d14ddf647edef84e545", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:28acec2bebc3922cd261ed3c8a13b26d53b49e891797a199e3e1ce8089e813e6", env!("CARGO_PKG_VERSION") ), ) @@ -1025,7 +1025,7 @@ async fn query_planner_redis_update_defer() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0497501d3d01d05ad142938e7b8d8e7ea13e648aabbbedb47f6291ca8b3e536d", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:af3139ddd647c755d2eab5e6a177dc443030a528db278c19ad7b45c5c0324378", env!("CARGO_PKG_VERSION") ), ) @@ -1050,7 +1050,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:662f5041882b3f621aeb7bad8e18818173eb077dc4343e16f3a34d2b6b6e4e59", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:8b87abe2e45d38df4712af966aa540f33dbab6fc2868a409f2dbb6a5a4fb2d08", env!("CARGO_PKG_VERSION") ), ) @@ -1104,7 +1104,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key // If the tests above are failing, this is the key that needs to be changed first. let starting_key = &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:81b9c296d8ebc8adf4575b83a8d296621fd76de6d12d8c91f4552eda02d1dd9c", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:924b36a9ae6af4ff198220b1302b14b6329c4beb7c022fd31d6fef82eaad7ccb", env!("CARGO_PKG_VERSION") ); assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap index 077d5130ee..7f7fd862db 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner_cache.snap @@ -6,14 +6,14 @@ expression: query_plan "kind": "Fetch", "serviceName": "products", "variableUsages": [], - "operation": "{topProducts{name name2:name}}", + "operation": "{ topProducts { name name2: name } }", "operationName": null, "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3981edbf89170585b228f36239d5a9fac84d78945994c2add49de1cefca874ba", + "schemaAwareHash": "b86f4d9d705538498ec90551f9d90f9eee4386be36ad087638932dad3f44bf66", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/tests/samples/README.md b/apollo-router/tests/samples/README.md index b5c2f68114..28aa1a149c 100644 --- a/apollo-router/tests/samples/README.md +++ b/apollo-router/tests/samples/README.md @@ -119,4 +119,25 @@ Stops the Router. If the Router does not stop correctly, then this action will f { "type": "Stop" } -``` \ No newline at end of file +``` + +## Troubleshooting + +### Query planning related + +When execution does something unexpected, checking the generated query plan can help. +Make sure the YAML Router configuration enables the _expose query plan_ plugin: + +```yaml +plugins: + experimental.expose_query_plan: true +``` + +In a `"type": "Request"` step of `plan.json`, temporarily add the header to ask +for the response to include `extensions.apolloQueryPlan`: + +```json +"headers": { + "Apollo-Expose-Query-Plan": "true" +}, +``` diff --git a/apollo-router/tests/samples/basic/interface-object/plan.json b/apollo-router/tests/samples/basic/interface-object/plan.json index 91a5690a0c..f50fbc4589 100644 --- a/apollo-router/tests/samples/basic/interface-object/plan.json +++ b/apollo-router/tests/samples/basic/interface-object/plan.json @@ -10,7 +10,7 @@ { "request": { "body": { - "query": "query TestItf__accounts__0{i{__typename id x ...on A{a}...on B{b}}}", + "query": "query TestItf__accounts__0 { i { __typename id x ... on A { a } ... on B { b } } }", "operationName": "TestItf__accounts__0" } }, @@ -55,7 +55,7 @@ { "request": { "body": { - "query": "query TestItf__products__1($representations:[_Any!]!){_entities(representations:$representations){...on I{y}}}", + "query": "query TestItf__products__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on I { y } } }", "operationName": "TestItf__products__1", "variables": { "representations": [ @@ -143,7 +143,7 @@ { "request": { "body": { - "query": "query TestItf2__accounts__0{req{__typename id i{__typename id x}}}", + "query": "query TestItf2__accounts__0 { req { __typename id i { __typename id x } } }", "operationName": "TestItf2__accounts__0" } }, @@ -170,7 +170,7 @@ { "request": { "body": { - "query": "query TestItf2__products__1($representations:[_Any!]!){_entities(representations:$representations){...on I{y}}}", + "query": "query TestItf2__products__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on I { y } } }", "operationName": "TestItf2__products__1", "variables": { "representations": [ @@ -201,7 +201,7 @@ { "request": { "body": { - "query": "query TestItf2__reviews__2($representations:[_Any!]!){_entities(representations:$representations){...on C{c}}}", + "query": "query TestItf2__reviews__2($representations: [_Any!]!) { _entities(representations: $representations) { ... on C { c } } }", "operationName": "TestItf2__reviews__2", "variables": { "representations": [ diff --git a/apollo-router/tests/samples/core/defer/plan.json b/apollo-router/tests/samples/core/defer/plan.json index 72dd5efed0..5ea73b76e1 100644 --- a/apollo-router/tests/samples/core/defer/plan.json +++ b/apollo-router/tests/samples/core/defer/plan.json @@ -10,7 +10,7 @@ { "request": { "body": { - "query": "{me{__typename name id}}" + "query": "{ me { __typename name id } }" } }, "response": { @@ -32,7 +32,7 @@ { "request": { "body": { - "query": "query($representations:[_Any!]!){_entities(representations:$representations){..._generated_onUser1_0}}fragment _generated_onUser1_0 on User{reviews{body}}", + "query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onUser1_0 } } fragment _generated_onUser1_0 on User { reviews { body } }", "variables": { "representations": [ { @@ -103,4 +103,4 @@ "type": "Stop" } ] -} +} \ No newline at end of file diff --git a/apollo-router/tests/samples/core/query1/plan.json b/apollo-router/tests/samples/core/query1/plan.json index d8fe2c400d..d451236d33 100644 --- a/apollo-router/tests/samples/core/query1/plan.json +++ b/apollo-router/tests/samples/core/query1/plan.json @@ -9,7 +9,7 @@ "requests": [ { "request": { - "body": {"query":"{me{name}}"} + "body": {"query":"{ me { name } }"} }, "response": { "body": {"data": { "me": { "name": "test" } } } @@ -17,7 +17,7 @@ }, { "request": { - "body": {"query":"{me{nom:name}}"} + "body": {"query":"{ me { nom: name } }"} }, "response": { "body": {"data": { "me": { "nom": "test" } } } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json index 83a777d329..eb223d567a 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json @@ -12,7 +12,7 @@ { "request": { "body": { - "query": "query CacheDefer__cache_defer_accounts__0{me{__typename name id}}", + "query": "query CacheDefer__cache_defer_accounts__0 { me { __typename name id } }", "operationName": "CacheDefer__cache_defer_accounts__0" } }, @@ -39,7 +39,7 @@ { "request": { "body": { - "query": "query CacheDefer__cache_defer_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onUser1_0}}fragment _generated_onUser1_0 on User{reviews{body}}", + "query": "query CacheDefer__cache_defer_reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onUser1_0 } } fragment _generated_onUser1_0 on User { reviews { body } }", "operationName": "CacheDefer__cache_defer_reviews__1", "variables": { "representations": [ @@ -110,4 +110,4 @@ "type": "Stop" } ] -} +} \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json index c8588ed2b7..fd55c50c8e 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json @@ -12,7 +12,7 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_products__0{topProducts{__typename upc}}", + "query":"query InvalidationEntityKey__invalidation_entity_key_products__0 { topProducts { __typename upc } }", "operationName": "InvalidationEntityKey__invalidation_entity_key_products__0" } }, @@ -31,7 +31,7 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{body}}", + "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onProduct1_0 } } fragment _generated_onProduct1_0 on Product { reviews { body } }", "operationName": "InvalidationEntityKey__invalidation_entity_key_reviews__1", "variables":{"representations":[{"upc":"0","__typename":"Product"},{"upc":"1","__typename":"Product"}]} } @@ -90,7 +90,8 @@ { "request": { "body": { - "query":"mutation InvalidationEntityKey__invalidation_entity_key_reviews__0{invalidateProductReview}" + "query":"mutation InvalidationEntityKey__invalidation_entity_key_reviews__0 { invalidateProductReview }", + "operationName": "InvalidationEntityKey__invalidation_entity_key_reviews__0" } }, "response": { @@ -115,7 +116,8 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{body}}", + "query": "query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onProduct1_0 } } fragment _generated_onProduct1_0 on Product { reviews { body } }", + "operationName": "InvalidationEntityKey__invalidation_entity_key_reviews__1", "variables":{"representations":[{"upc":"1","__typename":"Product"}]} } }, @@ -202,7 +204,8 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{body}}", + "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onProduct1_0 } } fragment _generated_onProduct1_0 on Product { reviews { body } }", + "operationName": "InvalidationEntityKey__invalidation_entity_key_reviews__1", "variables":{"representations":[{"upc":"1","__typename":"Product"}]} } }, diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/plan.json index 9bbbd1d90c..b60d008b38 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/plan.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/plan.json @@ -11,7 +11,7 @@ "requests": [ { "request": { - "body": {"query":"query InvalidationSubgraphName__invalidation_subgraph_name_accounts__0{me{name}}"} + "body": {"query":"query InvalidationSubgraphName__invalidation_subgraph_name_accounts__0 { me { name } }"} }, "response": { "headers": { @@ -45,7 +45,7 @@ "requests": [ { "request": { - "body": {"query":"mutation InvalidationSubgraphName__invalidation_subgraph_name_accounts__0{updateMyAccount{name}}"} + "body": {"query":"mutation InvalidationSubgraphName__invalidation_subgraph_name_accounts__0 { updateMyAccount { name } }"} }, "response": { "headers": { @@ -99,7 +99,7 @@ "requests": [ { "request": { - "body": {"query":"query InvalidationSubgraphName__invalidation_subgraph_name_accounts__0{me{name}}"} + "body": {"query":"query InvalidationSubgraphName__invalidation_subgraph_name_accounts__0 { me { name } }"} }, "response": { "headers": { diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/plan.json index 72e39a7b80..5c2a63dd6d 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/plan.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/plan.json @@ -11,7 +11,10 @@ "requests": [ { "request": { - "body": {"query":"query InvalidationSubgraphType__invalidation_subgraph_type_accounts__0{me{name id}}","operationName":"InvalidationSubgraphType__invalidation_subgraph_type_accounts__0"} + "body": { + "query": "query InvalidationSubgraphType__invalidation_subgraph_type_accounts__0 { me { name id } }", + "operationName": "InvalidationSubgraphType__invalidation_subgraph_type_accounts__0" + } }, "response": { "headers": { @@ -83,7 +86,10 @@ "requests": [ { "request": { - "body": {"query":"query InvalidationSubgraphType__invalidation_subgraph_type_accounts__0{me{name id}}", "operationName":"InvalidationSubgraphType__invalidation_subgraph_type_accounts__0"} + "body": { + "query": "query InvalidationSubgraphType__invalidation_subgraph_type_accounts__0 { me { name id } }", + "operationName": "InvalidationSubgraphType__invalidation_subgraph_type_accounts__0" + } }, "response": { "headers": { diff --git a/apollo-router/tests/samples/enterprise/entity-cache/private/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/private/plan.json index b466291766..ecb3b32e71 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/private/plan.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/private/plan.json @@ -11,7 +11,7 @@ "requests": [ { "request": { - "body": {"query":"query private__accounts__0{me{name}}"} + "body": {"query":"query private__accounts__0 { me { name } }"} }, "response": { "headers": { @@ -48,7 +48,7 @@ "requests": [ { "request": { - "body": {"query":"query private__accounts__0{me{name}}"} + "body": {"query":"query private__accounts__0 { me { name } }"} }, "response": { "headers": { diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml b/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml index dc60579e53..8d7c0e2439 100644 --- a/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/configuration.yaml @@ -10,3 +10,5 @@ telemetry: rhai: scripts: "tests/samples/enterprise/persisted-queries/basic/rhai" main: "main.rhai" +include_subgraph_errors: + all: true diff --git a/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml b/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml index c08b75aeb9..82c7a680fc 100644 --- a/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml +++ b/apollo-router/tests/samples/enterprise/persisted-queries/basic/plan.yaml @@ -9,7 +9,7 @@ actions: requests: - request: body: - query: "query GetMyName__accounts__0{me{name}}" + query: "query GetMyName__accounts__0 { me { name } }" response: body: data: @@ -17,7 +17,7 @@ actions: name: "Ada Lovelace" - request: body: - query: "query GetMyName__accounts__0{me{mobileAlias:name}}" + query: "query GetMyName__accounts__0 { me { mobileAlias: name } }" response: body: data: @@ -25,7 +25,7 @@ actions: mobileAlias: "Ada Lovelace" - request: body: - query: "query GetYourName__accounts__0{you:me{name}}" + query: "query GetYourName__accounts__0 { you: me { name } }" response: body: data: diff --git a/apollo-router/tests/samples/enterprise/query-planning-redis/plan.json b/apollo-router/tests/samples/enterprise/query-planning-redis/plan.json index a864862620..c25a95d031 100644 --- a/apollo-router/tests/samples/enterprise/query-planning-redis/plan.json +++ b/apollo-router/tests/samples/enterprise/query-planning-redis/plan.json @@ -11,7 +11,7 @@ "requests": [ { "request": { - "body": {"query":"{me{name}}"} + "body": {"query":"{ me { name } }"} }, "response": { "body": {"data": { "me": { "name": "test" } } } @@ -19,7 +19,7 @@ }, { "request": { - "body": {"query":"{me{nom:name}}"} + "body": {"query":"{ me { nom: name } }"} }, "response": { "body": {"data": { "me": { "nom": "test" } } } diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 98782558bb..7f06f1d5cc 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -519,6 +519,7 @@ impl TestExecution { } } if failed { + self.print_received_requests(out).await; let f: Failed = out.clone().into(); return Err(f); } @@ -597,22 +598,7 @@ impl TestExecution { }; if expected_response != &graphql_response { - if let Some(requests) = self - .subgraphs_server - .as_ref() - .unwrap() - .received_requests() - .await - { - writeln!(out, "subgraphs received requests:").unwrap(); - for request in requests { - writeln!(out, "\tmethod: {}", request.method).unwrap(); - writeln!(out, "\tpath: {}", request.url).unwrap(); - writeln!(out, "\t{}\n", std::str::from_utf8(&request.body).unwrap()).unwrap(); - } - } else { - writeln!(out, "subgraphs received no requests").unwrap(); - } + self.print_received_requests(out).await; writeln!(out, "assertion `left == right` failed").unwrap(); writeln!( @@ -633,6 +619,25 @@ impl TestExecution { Ok(()) } + async fn print_received_requests(&mut self, out: &mut String) { + if let Some(requests) = self + .subgraphs_server + .as_ref() + .unwrap() + .received_requests() + .await + { + writeln!(out, "subgraphs received requests:").unwrap(); + for request in requests { + writeln!(out, "\tmethod: {}", request.method).unwrap(); + writeln!(out, "\tpath: {}", request.url).unwrap(); + writeln!(out, "\t{}\n", std::str::from_utf8(&request.body).unwrap()).unwrap(); + } + } else { + writeln!(out, "subgraphs received no requests").unwrap(); + } + } + async fn endpoint_request( &mut self, url: &url::Url, diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs index dc3f366fcf..96086bbe4c 100644 --- a/apollo-router/tests/set_context.rs +++ b/apollo-router/tests/set_context.rs @@ -51,6 +51,7 @@ fn get_configuration(rust_qp: bool) -> serde_json::Value { }}; } json! {{ + "experimental_query_planner_mode": "legacy", "experimental_type_conditioned_fetching": true, // will make debugging easier "plugins": { From c2745a93a318251e329cd219f234257603634f97 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Mon, 9 Dec 2024 12:46:53 +0100 Subject: [PATCH 088/112] plugins/fleet_detector: detect request/response sizes --- apollo-router/src/metrics/filter.rs | 2 +- apollo-router/src/plugins/fleet_detector.rs | 131 ++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/metrics/filter.rs b/apollo-router/src/metrics/filter.rs index 59bd991966..6f30335794 100644 --- a/apollo-router/src/metrics/filter.rs +++ b/apollo-router/src/metrics/filter.rs @@ -105,7 +105,7 @@ impl FilterMeterProvider { FilterMeterProvider::builder() .delegate(delegate) .deny( - Regex::new(r"apollo\.router\.(config|entities|instance)(\..*|$)") + Regex::new(r"apollo\.router\.(config|entities|instance|operations\.(fetch|request_size|response_size))(\..*|$)") .expect("regex should have been valid"), ) .build() diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 7b2882770b..2b7b36414a 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -6,6 +6,7 @@ use std::sync::Mutex; use std::time::Duration; use std::time::Instant; +use futures::StreamExt; use opentelemetry::metrics::MeterProvider; use opentelemetry_api::metrics::ObservableGauge; use opentelemetry_api::metrics::Unit; @@ -13,13 +14,19 @@ use opentelemetry_api::KeyValue; use schemars::JsonSchema; use serde::Deserialize; use sysinfo::System; +use tower::util::BoxService; use tower::BoxError; +use tower::ServiceExt as _; use tracing::debug; use crate::executable::APOLLO_TELEMETRY_DISABLED; use crate::metrics::meter_provider; use crate::plugin::PluginInit; use crate::plugin::PluginPrivate; +use crate::services::http::HttpRequest; +use crate::services::http::HttpResponse; +use crate::services::router; +use crate::services::router::body::RouterBody; const REFRESH_INTERVAL: Duration = Duration::from_secs(60); const COMPUTE_DETECTOR_THRESHOLD: u16 = 24576; @@ -171,6 +178,7 @@ impl GaugeStore { #[derive(Default)] struct FleetDetector { + enabled: bool, gauge_store: Mutex, } #[async_trait::async_trait] @@ -187,6 +195,7 @@ impl PluginPrivate for FleetDetector { } Ok(FleetDetector { + enabled: true, gauge_store: Mutex::new(GaugeStore::Pending), }) } @@ -197,6 +206,128 @@ impl PluginPrivate for FleetDetector { *store = GaugeStore::active(); } } + + fn router_service(&self, service: router::BoxService) -> router::BoxService { + if !self.enabled { + return service; + } + + service + // Count the number of request bytes from clients to the router + .map_request(move |req: router::Request| router::Request { + router_request: req.router_request.map(move |body| { + router::Body::wrap_stream(body.map(move |res| { + res.map(move |bytes| { + u64_counter!( + "apollo.router.operations.request_size", + "Total number of request bytes from clients", + bytes.len() as u64 + ); + bytes + }) + })) + }), + context: req.context, + }) + // Count the number of response bytes from the router to clients + .map_response(move |res: router::Response| router::Response { + response: res.response.map(move |body| { + router::Body::wrap_stream(body.map(move |res| { + res.map(move |bytes| { + u64_counter!( + "apollo.router.operations.response_size", + "Total number of response bytes to clients", + bytes.len() as u64 + ); + bytes + }) + })) + }), + context: res.context, + }) + .boxed() + } + + fn http_client_service( + &self, + subgraph_name: &str, + service: BoxService, + ) -> BoxService { + if !self.enabled { + return service; + } + let sn_req = Arc::new(subgraph_name.to_string()); + let sn_res = sn_req.clone(); + service + // Count the number of bytes per subgraph fetch request + .map_request(move |req: HttpRequest| { + let sn = sn_req.clone(); + HttpRequest { + http_request: req.http_request.map(move |body| { + let sn = sn.clone(); + RouterBody::wrap_stream(body.map(move |res| { + let sn = sn.clone(); + res.map(move |bytes| { + u64_counter!( + "apollo.router.operations.fetch.request_size", + "Total number of request bytes for subgraph fetches", + bytes.len() as u64, + subgraph.service.name = sn.to_string() + ); + bytes + }) + })) + }), + context: req.context, + } + }) + // Count the number of fetches, and the number of bytes per subgraph fetch response + .map_result(move |res| { + let sn = sn_res.clone(); + match res { + Ok(res) => { + u64_counter!( + "apollo.router.operations.fetch", + "Number of subgraph fetches", + 1u64, + subgraph.service.name = sn.to_string(), + http.response.status_code = res.http_response.status().as_u16() as i64 + ); + let sn = sn_res.clone(); + Ok(HttpResponse { + http_response: res.http_response.map(move |body| { + let sn = sn.clone(); + RouterBody::wrap_stream(body.map(move |res| { + let sn = sn.clone(); + res.map(move |bytes| { + u64_counter!( + "apollo.router.operations.fetch.response_size", + "Total number of response bytes for subgraph fetches", + bytes.len() as u64, + subgraph.service.name = sn.to_string() + ); + bytes + }) + })) + }), + context: res.context, + }) + } + Err(err) => { + // On fetch errors, report the status code as 0 + u64_counter!( + "apollo.router.operations.fetch", + "Number of subgraph fetches", + 1u64, + subgraph.service.name = sn.to_string(), + http.response.status_code = 0i64 + ); + Err(err) + } + } + }) + .boxed() + } } #[cfg(not(target_os = "linux"))] From 7d93ccc27cc7d1c02d8b8fee2531a663c8b9759a Mon Sep 17 00:00:00 2001 From: Gary Pennington Date: Mon, 9 Dec 2024 14:50:28 +0000 Subject: [PATCH 089/112] Make changes to address new RUSTSEC details RUSTSEC-2024-0421 identifies issues in the idna crate at versions < 1.0.3. We are not affected by the vulnerability, since the router is only using the crate to resolve subgraph addresses from a trusted source. We'll ignore the advisory. When we can update the hickory crate to a version using idna >=1.0.3, we'll remove the deny override. Additionally: Update the url crate to 2.5.4 anyway, so it's using idna 1.0.3 --- Cargo.lock | 1466 ++++++++++++++++++++++---------------- apollo-router/Cargo.toml | 2 +- deny.toml | 2 + 3 files changed, 841 insertions(+), 629 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f55501cd1c..2697654939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,18 +38,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -119,9 +119,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -134,43 +134,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "apollo-compiler" @@ -181,11 +181,11 @@ dependencies = [ "ahash", "apollo-parser", "ariadne", - "indexmap 2.2.6", + "indexmap 2.7.0", "rowan", "serde", "serde_json_bytes", - "thiserror", + "thiserror 1.0.69", "triomphe", "typed-arena", "uuid", @@ -211,7 +211,7 @@ dependencies = [ "either", "hashbrown 0.15.2", "hex", - "indexmap 2.2.6", + "indexmap 2.7.0", "insta", "itertools 0.13.0", "lazy_static", @@ -226,7 +226,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "tempfile", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "url", @@ -252,7 +252,7 @@ checksum = "b64257011a999f2e22275cf7a118f651e58dc9170e11b775d435de768fad0387" dependencies = [ "memchr", "rowan", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -314,7 +314,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyperlocal", - "indexmap 2.2.6", + "indexmap 2.7.0", "insta", "itertools 0.13.0", "itoa", @@ -392,7 +392,7 @@ dependencies = [ "sysinfo", "tempfile", "test-log", - "thiserror", + "thiserror 1.0.69", "tikv-jemallocator", "time", "tokio", @@ -410,7 +410,7 @@ dependencies = [ "tracing-core", "tracing-futures", "tracing-opentelemetry", - "tracing-serde", + "tracing-serde 0.1.3", "tracing-subscriber", "tracing-test", "uname", @@ -481,16 +481,16 @@ dependencies = [ "apollo-compiler", "apollo-parser", "arbitrary", - "indexmap 2.2.6", + "indexmap 2.7.0", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -508,16 +508,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44055e597c674aef7cb903b2b9f6e4cba1277ed0d2d61dae7cd52d7ffa81f8e2" dependencies = [ "concolor", - "unicode-width", + "unicode-width 0.1.14", "yansi", ] -[[package]] -name = "ascii" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - [[package]] name = "ascii_utils" version = "0.9.3" @@ -575,11 +569,11 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ - "brotli 6.0.0", + "brotli 7.0.0", "flate2", "futures-core", "memchr", @@ -589,14 +583,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", + "fastrand 2.3.0", + "futures-lite 2.5.0", "slab", ] @@ -608,10 +602,10 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "once_cell", ] @@ -633,7 +627,7 @@ dependencies = [ "futures-util", "handlebars 4.5.0", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.7.0", "mime", "multer", "num-traits", @@ -645,7 +639,7 @@ dependencies = [ "serde_urlencoded", "static_assertions", "tempfile", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -680,7 +674,7 @@ dependencies = [ "quote", "strum 0.25.0", "syn 2.0.90", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -702,57 +696,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323a5143f5bdd2030f45e3f2e0c821c9b1d36e79cf382129c64299c50a7f3750" dependencies = [ "bytes", - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_json", ] [[package]] name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ - "async-lock 3.4.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "parking", - "polling 3.7.2", - "rustix 0.38.34", + "polling", + "rustix", "slab", "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", + "windows-sys 0.59.0", ] [[package]] @@ -768,55 +733,57 @@ dependencies = [ [[package]] name = "async-process" -version = "1.8.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", + "async-channel 2.3.1", + "async-io", + "async-lock", "async-signal", + "async-task", "blocking", "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.34", - "windows-sys 0.48.0", + "event-listener 5.3.1", + "futures-lite 2.5.0", + "rustix", + "tracing", ] [[package]] name = "async-signal" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "async-std" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-channel 1.9.0", "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", + "async-io", + "async-lock", "async-process", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 1.13.0", + "futures-lite 2.5.0", "gloo-timers", "kv-log-macro", "log", @@ -830,9 +797,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -841,9 +808,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", @@ -858,9 +825,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -875,9 +842,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auth-git2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51bd0e4592409df8631ca807716dc1e5caafae5d01ce0157c966c71c7e49c3c" +checksum = "3810b5af212b013fe7302b12d86616c6c39a48e18f2e4b812a5a9e5710213791" dependencies = [ "dirs", "git2", @@ -886,15 +853,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a" +checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" dependencies = [ "aws-credential-types", "aws-runtime", @@ -909,10 +876,9 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", - "fastrand 2.1.0", + "fastrand 2.3.0", "hex", "http 0.2.12", - "hyper", "ring", "time", "tokio", @@ -923,9 +889,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -935,19 +901,20 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.4.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6" +checksum = "b5ac934720fbb46206292d2c75b57e67acfc56fe7dfd34fb9a02334af08409ea" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-http", + "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "fastrand 2.1.0", + "fastrand 2.3.0", "http 0.2.12", "http-body 0.4.6", "once_cell", @@ -1026,9 +993,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -1039,7 +1006,7 @@ dependencies = [ "hex", "hmac", "http 0.2.12", - "http 1.1.0", + "http 1.2.0", "once_cell", "percent-encoding", "sha2", @@ -1060,9 +1027,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.9" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -1099,16 +1066,16 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.6.3" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225" +checksum = "9f20685047ca9d6f17b994a07f629c813f08b5bce65523e47124879e60103d45" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "fastrand 2.1.0", + "fastrand 2.3.0", "h2", "http 0.2.12", "http-body 0.4.6", @@ -1126,15 +1093,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.7.2" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" dependencies = [ "aws-smithy-async", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.1.0", + "http 1.2.0", "pin-project-lite", "tokio", "tracing", @@ -1143,16 +1110,16 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.4" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273dcdfd762fae3e1650b8024624e7cd50e484e37abdab73a7a706188ad34543" +checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.1.0", + "http 1.2.0", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1169,9 +1136,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" dependencies = [ "xmlparser", ] @@ -1186,7 +1153,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "rustc_version 0.4.0", + "rustc_version 0.4.1", "tracing", ] @@ -1245,17 +1212,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -1361,15 +1328,15 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "piper", ] [[package]] name = "bloomfilter" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0bdbcf2078e0ba8a74e1fe0cf36f54054a04485759b61dfd60b174658e9607" +checksum = "c541c70a910b485670304fd420f0eab8f7bde68439db6a8d98819c3d2774d7e2" dependencies = [ "bit-vec 0.7.0", "getrandom 0.2.15", @@ -1389,9 +1356,9 @@ dependencies = [ [[package]] name = "brotli" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1420,9 +1387,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "serde", @@ -1439,7 +1406,7 @@ dependencies = [ "quote", "str_inflector", "syn 2.0.90", - "thiserror", + "thiserror 1.0.69", "try_match", ] @@ -1463,9 +1430,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -1531,12 +1498,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.5" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -1547,9 +1515,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1598,9 +1566,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.9" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -1608,9 +1576,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -1620,9 +1588,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1632,36 +1600,33 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" -version = "3.8.1" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "ascii", - "byteorder", - "either", + "bytes", "memchr", - "unreachable", ] [[package]] @@ -1702,7 +1667,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.14", "windows-sys 0.52.0", ] @@ -1802,9 +1767,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "countme" @@ -1814,9 +1779,9 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -2106,7 +2071,7 @@ dependencies = [ "strum_macros 0.25.3", "syn 1.0.109", "syn 2.0.90", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2180,9 +2145,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", @@ -2198,7 +2163,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version 0.4.1", "syn 2.0.90", ] @@ -2227,7 +2192,7 @@ dependencies = [ "console", "shell-words", "tempfile", - "thiserror", + "thiserror 1.0.69", "zeroize", ] @@ -2257,7 +2222,7 @@ checksum = "c3ca7fa3ba397980657070e679f412acddb7a372f1793ff68ef0bbe708680f0f" dependencies = [ "regex", "sha2", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -2301,9 +2266,9 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dw" @@ -2389,11 +2354,11 @@ dependencies = [ [[package]] name = "enum-as-inner" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.90", @@ -2449,12 +2414,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2469,17 +2434,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "5.3.1" @@ -2493,9 +2447,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener 5.3.1", "pin-project-lite", @@ -2562,9 +2516,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ff" @@ -2578,14 +2532,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] [[package]] @@ -2596,9 +2550,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "libz-ng-sys", @@ -2725,7 +2679,7 @@ dependencies = [ "rustls-native-certs", "rustls-webpki", "semver 1.0.23", - "socket2 0.5.7", + "socket2", "tokio", "tokio-rustls", "tokio-stream", @@ -2755,9 +2709,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -2770,9 +2724,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2780,15 +2734,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -2798,9 +2752,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -2819,11 +2773,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ - "fastrand 2.1.0", + "fastrand 2.3.0", "futures-core", "futures-io", "parking", @@ -2832,9 +2786,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -2843,21 +2797,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-test" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce388237b32ac42eca0df1ba55ed3bbda4eaf005d7d4b5dbc0b20ab962928ac9" +checksum = "5961fb6311645f46e2cdc2964a8bfae6743fd72315eaec181a71ae3eb2467113" dependencies = [ "futures-core", "futures-executor", @@ -2867,7 +2821,6 @@ dependencies = [ "futures-task", "futures-util", "pin-project", - "pin-utils", ] [[package]] @@ -2878,9 +2831,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -2942,9 +2895,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git2" @@ -2969,22 +2922,22 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", @@ -3003,12 +2956,12 @@ dependencies = [ [[package]] name = "graphql-parser" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" +checksum = "7a818c0d883d7c0801df27be910917750932be279c7bc82dc541b8769425f409" dependencies = [ "combine", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3073,7 +3026,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -3101,7 +3054,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3115,7 +3068,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3129,10 +3082,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -3244,7 +3193,7 @@ dependencies = [ "ipnet", "once_cell", "rand 0.8.5", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -3267,7 +3216,7 @@ dependencies = [ "rand 0.8.5", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -3314,9 +3263,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -3341,7 +3290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -3352,7 +3301,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] @@ -3396,9 +3345,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -3439,7 +3388,7 @@ dependencies = [ "itoa", "pin-project-lite", "smallvec", - "socket2 0.5.7", + "socket2", "tokio", "tower-service", "tracing", @@ -3510,6 +3459,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3528,12 +3595,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -3554,26 +3632,26 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -3604,9 +3682,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.39.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" dependencies = [ "console", "lazy_static", @@ -3642,24 +3720,13 @@ dependencies = [ "ghost", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.7", + "socket2", "widestring", "windows-sys 0.48.0", "winreg", @@ -3667,26 +3734,26 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "iso8601" @@ -3726,25 +3793,26 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -3758,7 +3826,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3887,19 +3955,18 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.167" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -3924,6 +3991,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall", ] [[package]] @@ -3954,9 +4022,9 @@ dependencies = [ [[package]] name = "libz-ng-sys" -version = "1.1.15" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" +checksum = "8f0f7295a34685977acb2e8cc8b08ee4a8dffd6cf278eeccddbe1ed55ba815d5" dependencies = [ "cmake", "libc", @@ -3964,9 +4032,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.18" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" dependencies = [ "cc", "libc", @@ -3982,18 +4050,18 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linkme" -version = "0.3.27" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb76662d78edc9f9bf56360d6919bdacc8b7761227727e5082f128eeb90bbf5" +checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae" dependencies = [ "linkme-impl", ] [[package]] name = "linkme-impl" -version = "0.3.27" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda" +checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9" dependencies = [ "proc-macro2", "quote", @@ -4002,15 +4070,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.4.14" +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -4034,11 +4102,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -4138,11 +4206,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -4164,14 +4232,25 @@ dependencies = [ ] [[package]] -name = "mockall" -version = "0.13.0" +name = "mio" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "cfg-if", - "downcast", - "fragile", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", "mockall_derive", "predicates", "predicates-tree", @@ -4179,9 +4258,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" dependencies = [ "cfg-if", "proc-macro2", @@ -4203,7 +4282,7 @@ dependencies = [ "log", "memchr", "mime", - "spin", + "spin 0.9.8", "version_check", ] @@ -4231,6 +4310,15 @@ dependencies = [ "serde", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +dependencies = [ + "spin 0.5.2", +] + [[package]] name = "nom" version = "7.1.3" @@ -4253,7 +4341,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -4399,18 +4487,21 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.36.1" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +dependencies = [ + "portable-atomic", +] [[package]] name = "oorandom" @@ -4438,18 +4529,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -4479,7 +4570,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror", + "thiserror 1.0.69", "urlencoding", ] @@ -4509,7 +4600,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "reqwest", "rmp", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -4561,7 +4652,7 @@ dependencies = [ "opentelemetry_sdk 0.20.0", "prost 0.11.9", "reqwest", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.9.2", ] @@ -4644,7 +4735,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "typed-builder", ] @@ -4660,7 +4751,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror", + "thiserror 1.0.69", "urlencoding", ] @@ -4683,7 +4774,7 @@ dependencies = [ "rand 0.8.5", "regex", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", ] @@ -4702,10 +4793,10 @@ dependencies = [ "glob", "once_cell", "opentelemetry 0.22.0", - "ordered-float 4.2.1", + "ordered-float 4.5.0", "percent-encoding", "rand 0.8.5", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4734,9 +4825,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.2.1" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" +checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" dependencies = [ "num-traits", ] @@ -4767,9 +4858,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -4789,7 +4880,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -4827,20 +4918,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.11" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.6", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ "pest", "pest_generator", @@ -4848,9 +4939,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ "pest", "pest_meta", @@ -4861,9 +4952,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", @@ -4877,25 +4968,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_derive", ] [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", @@ -4904,9 +4995,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -4916,12 +5007,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand 2.3.0", "futures-io", ] @@ -4937,15 +5028,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plotters" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -4956,15 +5047,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] @@ -4982,40 +5073,24 @@ dependencies = [ [[package]] name = "polling" -version = "2.8.0" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -5025,9 +5100,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "predicates" @@ -5041,15 +5119,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" dependencies = [ "predicates-core", "termtree", @@ -5105,7 +5183,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5219,7 +5297,7 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "typetag", ] @@ -5237,9 +5315,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -5351,31 +5429,22 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5386,7 +5455,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -5401,9 +5470,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -5502,13 +5571,14 @@ dependencies = [ [[package]] name = "rhai" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61797318be89b1a268a018a92a7657096d83f3ecb31418b9e9c16dcbb043b702" +checksum = "8867cfc57aaf2320b60ec0f4d55603ac950ce852e6ab6b9109aa3d626a4dd7ea" dependencies = [ "ahash", "bitflags 2.6.0", "instant", + "no-std-compat", "num-traits", "once_cell", "rhai_codegen", @@ -5599,7 +5669,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin", + "spin 0.9.8", "untrusted", "windows-sys 0.52.0", ] @@ -5643,7 +5713,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tower", "tower-service", @@ -5756,38 +5826,24 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver 1.0.23", ] [[package]] name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.34" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "linux-raw-sys", + "windows-sys 0.59.0", ] [[package]] @@ -5835,9 +5891,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -5856,20 +5912,20 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.4" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af" +checksum = "66b202022bb57c049555430e11fc22fea12909276a80a4c3d368da36ac1d88ed" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5915,9 +5971,9 @@ dependencies = [ [[package]] name = "sdd" -version = "1.6.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb0dde0ccd15e337a3cf738a9a38115c6d8e74795d074e73973dad3d229a897" +checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" [[package]] name = "sec1" @@ -5948,9 +6004,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -6015,7 +6071,7 @@ dependencies = [ "quote", "regex", "syn 2.0.90", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6031,12 +6087,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "itoa", + "memchr", "ryu", "serde", ] @@ -6049,7 +6106,7 @@ checksum = "0ecd92a088fb2500b2f146c9ddc5da9950bb7264d3f00932cd2a6fb369c26c46" dependencies = [ "ahash", "bytes", - "indexmap 2.2.6", + "indexmap 2.7.0", "jsonpath-rust", "regex", "serde", @@ -6074,14 +6131,14 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -6110,7 +6167,7 @@ dependencies = [ "serde", "serde_bytes", "smallvec", - "thiserror", + "thiserror 1.0.69", "v8", ] @@ -6128,9 +6185,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ "futures", "log", @@ -6142,9 +6199,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", @@ -6197,6 +6254,12 @@ dependencies = [ "dirs", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -6218,9 +6281,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" [[package]] name = "simple_asn1" @@ -6230,7 +6293,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -6272,19 +6335,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.10" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -6306,6 +6359,12 @@ dependencies = [ "url", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -6437,6 +6496,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "sys-info" version = "0.9.1" @@ -6449,9 +6519,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" dependencies = [ "core-foundation-sys", "libc", @@ -6484,14 +6554,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", - "fastrand 2.1.0", - "rustix 0.38.34", - "windows-sys 0.52.0", + "fastrand 2.3.0", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -6557,18 +6628,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +dependencies = [ + "thiserror-impl 2.0.6", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", @@ -6648,9 +6739,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -6671,9 +6762,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -6688,6 +6779,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -6715,21 +6816,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -6744,9 +6844,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -6765,9 +6865,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -6805,9 +6905,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -6820,21 +6920,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.16", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -6845,22 +6945,22 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.20", ] [[package]] @@ -6975,15 +7075,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower-test" @@ -7001,9 +7101,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -7013,9 +7113,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -7024,9 +7124,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -7092,11 +7192,21 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term 0.46.0", @@ -7110,7 +7220,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log 0.2.0", - "tracing-serde", + "tracing-serde 0.2.0", ] [[package]] @@ -7136,9 +7246,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" dependencies = [ "serde", "stable_deref_trait", @@ -7152,18 +7262,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "try_match" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ae3c1941e8859e30d28e572683fbfa89ae5330748b45139aedf488389e2be4" +checksum = "b065c869a3f832418e279aa4c1d7088f9d5d323bde15a60a08e20c2cd4549082" dependencies = [ "try_match_inner", ] [[package]] name = "try_match_inner" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0a91713132798caecb23c977488945566875e7b61b902fb111979871cbff34e" +checksum = "b9c81686f7ab4065ccac3df7a910c4249f8c0f3fb70421d6ddec19b9311f63f9" dependencies = [ "proc-macro2", "quote", @@ -7185,7 +7295,7 @@ dependencies = [ "rand 0.8.5", "rustls", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -7239,9 +7349,9 @@ dependencies = [ [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uname" @@ -7295,54 +7405,48 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-id" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] -name = "unreachable" -version = "1.0.0" +name = "unicode-width" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -dependencies = [ - "void", -] +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" @@ -7352,12 +7456,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -7387,6 +7491,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -7424,9 +7540,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" [[package]] name = "vcpkg" @@ -7436,15 +7552,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "void" -version = "1.0.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vsimd" @@ -7491,23 +7601,23 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.90", @@ -7516,21 +7626,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7538,9 +7649,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -7551,15 +7662,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -7570,9 +7681,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -7593,7 +7714,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.34", + "rustix", ] [[package]] @@ -7620,11 +7741,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7786,6 +7907,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -7975,9 +8105,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -8016,19 +8146,31 @@ dependencies = [ [[package]] name = "wmi" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70df482bbec7017ce4132154233642de658000b24b805345572036782a66ad55" +checksum = "dc47c0776cc6c00d2f7a874a0c846d94d45535936e5a1187693a24f23b4dd701" dependencies = [ "chrono", "futures", "log", "serde", - "thiserror", + "thiserror 2.0.6", "windows 0.58.0", "windows-core 0.58.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wsl" version = "0.1.0" @@ -8056,12 +8198,37 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -8076,12 +8243,55 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "zstd" version = "0.13.2" @@ -8093,18 +8303,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 61cfb736ab..b2b139e9d7 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -241,7 +241,7 @@ tracing = "0.1.40" tracing-core = "0.1.32" tracing-futures = { version = "0.2.5", features = ["futures-03"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } -url = { version = "2.5.2", features = ["serde"] } +url = { version = "2.5.4", features = ["serde"] } urlencoding = "2.1.3" uuid = { version = "1.9.1", features = ["serde", "v4"] } yaml-rust = "0.4.5" diff --git a/deny.toml b/deny.toml index 55cd3fb0f0..8e79104c33 100644 --- a/deny.toml +++ b/deny.toml @@ -30,6 +30,7 @@ git-fetch-with-cli = true ignore = [ "RUSTSEC-2023-0071", "RUSTSEC-2024-0376", # we do not use tonic::transport::Server + "RUSTSEC-2024-0421" # we only resolve trusted subgraphs ] # This section is considered when running `cargo deny check licenses` @@ -54,6 +55,7 @@ allow = [ "MPL-2.0", "Elastic-2.0", "Unicode-DFS-2016", + "Unicode-3.0", "Zlib" ] copyleft = "warn" From e7d1e281b6178e5c69d3d9ea47bb90de88466fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Mon, 9 Dec 2024 15:14:37 +0000 Subject: [PATCH 090/112] Add doc comment to `services::external` (#6345) --- apollo-router/src/services/external.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/services/external.rs b/apollo-router/src/services/external.rs index c356ddd39c..58bc24832d 100644 --- a/apollo-router/src/services/external.rs +++ b/apollo-router/src/services/external.rs @@ -1,4 +1,5 @@ -#![allow(missing_docs)] // FIXME +//! Structures for externalised data, communicating the state of the router pipeline at the +//! different stages. use std::collections::HashMap; use std::fmt::Debug; From 7955b24d4008c273935918e2a873e92cdb9639d1 Mon Sep 17 00:00:00 2001 From: Gary Pennington Date: Mon, 9 Dec 2024 15:44:42 +0000 Subject: [PATCH 091/112] Try to fix the Cargo.lock file I may have updated too much in the lock file, so try to recover it. --- Cargo.lock | 1199 +++++++++++++++++++++++++++------------------------- 1 file changed, 618 insertions(+), 581 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2697654939..b6258b95be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,18 +38,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] -name = "adler2" -version = "2.0.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.21" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -119,9 +119,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", @@ -134,43 +134,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "apollo-compiler" @@ -181,11 +181,11 @@ dependencies = [ "ahash", "apollo-parser", "ariadne", - "indexmap 2.7.0", + "indexmap 2.2.6", "rowan", "serde", "serde_json_bytes", - "thiserror 1.0.69", + "thiserror", "triomphe", "typed-arena", "uuid", @@ -211,7 +211,7 @@ dependencies = [ "either", "hashbrown 0.15.2", "hex", - "indexmap 2.7.0", + "indexmap 2.2.6", "insta", "itertools 0.13.0", "lazy_static", @@ -226,7 +226,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "tempfile", - "thiserror 1.0.69", + "thiserror", "time", "tracing", "url", @@ -252,7 +252,7 @@ checksum = "b64257011a999f2e22275cf7a118f651e58dc9170e11b775d435de768fad0387" dependencies = [ "memchr", "rowan", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -314,7 +314,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyperlocal", - "indexmap 2.7.0", + "indexmap 2.2.6", "insta", "itertools 0.13.0", "itoa", @@ -392,7 +392,7 @@ dependencies = [ "sysinfo", "tempfile", "test-log", - "thiserror 1.0.69", + "thiserror", "tikv-jemallocator", "time", "tokio", @@ -410,7 +410,7 @@ dependencies = [ "tracing-core", "tracing-futures", "tracing-opentelemetry", - "tracing-serde 0.1.3", + "tracing-serde", "tracing-subscriber", "tracing-test", "uname", @@ -481,16 +481,16 @@ dependencies = [ "apollo-compiler", "apollo-parser", "arbitrary", - "indexmap 2.7.0", + "indexmap 2.2.6", "once_cell", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" dependencies = [ "derive_arbitrary", ] @@ -508,10 +508,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44055e597c674aef7cb903b2b9f6e4cba1277ed0d2d61dae7cd52d7ffa81f8e2" dependencies = [ "concolor", - "unicode-width 0.1.14", + "unicode-width", "yansi", ] +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + [[package]] name = "ascii_utils" version = "0.9.3" @@ -569,11 +575,11 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.18" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ - "brotli 7.0.0", + "brotli 6.0.0", "flate2", "futures-core", "memchr", @@ -583,14 +589,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.3.0", - "futures-lite 2.5.0", + "fastrand 2.1.0", + "futures-lite 2.3.0", "slab", ] @@ -602,10 +608,10 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io", - "async-lock", + "async-io 2.3.3", + "async-lock 3.4.0", "blocking", - "futures-lite 2.5.0", + "futures-lite 2.3.0", "once_cell", ] @@ -627,7 +633,7 @@ dependencies = [ "futures-util", "handlebars 4.5.0", "http 0.2.12", - "indexmap 2.7.0", + "indexmap 2.2.6", "mime", "multer", "num-traits", @@ -639,7 +645,7 @@ dependencies = [ "serde_urlencoded", "static_assertions", "tempfile", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -674,7 +680,7 @@ dependencies = [ "quote", "strum 0.25.0", "syn 2.0.90", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -696,28 +702,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323a5143f5bdd2030f45e3f2e0c821c9b1d36e79cf382129c64299c50a7f3750" dependencies = [ "bytes", - "indexmap 2.7.0", + "indexmap 2.2.6", "serde", "serde_json", ] [[package]] name = "async-io" -version = "2.4.0" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ - "async-lock", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.3.0", "parking", - "polling", - "rustix", + "polling 3.7.2", + "rustix 0.38.34", "slab", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", ] [[package]] @@ -733,57 +768,55 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-channel 2.3.1", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "async-signal", - "async-task", "blocking", "cfg-if", - "event-listener 5.3.1", - "futures-lite 2.5.0", - "rustix", - "tracing", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", + "windows-sys 0.48.0", ] [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" dependencies = [ - "async-io", - "async-lock", + "async-io 2.3.3", + "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.34", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "async-std" -version = "1.13.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-channel 1.9.0", "async-global-executor", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "async-process", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 2.5.0", + "futures-lite 1.13.0", "gloo-timers", "kv-log-macro", "log", @@ -797,9 +830,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", @@ -808,9 +841,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", @@ -825,9 +858,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", @@ -842,9 +875,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auth-git2" -version = "0.5.5" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3810b5af212b013fe7302b12d86616c6c39a48e18f2e4b812a5a9e5710213791" +checksum = "e51bd0e4592409df8631ca807716dc1e5caafae5d01ce0157c966c71c7e49c3c" dependencies = [ "dirs", "git2", @@ -853,15 +886,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-config" -version = "1.5.5" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" +checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a" dependencies = [ "aws-credential-types", "aws-runtime", @@ -876,9 +909,10 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", - "fastrand 2.3.0", + "fastrand 2.1.0", "hex", "http 0.2.12", + "hyper", "ring", "time", "tokio", @@ -889,9 +923,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -901,20 +935,19 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.4.4" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ac934720fbb46206292d2c75b57e67acfc56fe7dfd34fb9a02334af08409ea" +checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-http", - "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "fastrand 2.3.0", + "fastrand 2.1.0", "http 0.2.12", "http-body 0.4.6", "once_cell", @@ -993,9 +1026,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.6" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" +checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -1006,7 +1039,7 @@ dependencies = [ "hex", "hmac", "http 0.2.12", - "http 1.2.0", + "http 1.1.0", "once_cell", "percent-encoding", "sha2", @@ -1027,9 +1060,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.11" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -1066,16 +1099,16 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.4" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f20685047ca9d6f17b994a07f629c813f08b5bce65523e47124879e60103d45" +checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "fastrand 2.3.0", + "fastrand 2.1.0", "h2", "http 0.2.12", "http-body 0.4.6", @@ -1093,15 +1126,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.7.3" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" dependencies = [ "aws-smithy-async", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.2.0", + "http 1.1.0", "pin-project-lite", "tokio", "tracing", @@ -1110,16 +1143,16 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.9" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" +checksum = "273dcdfd762fae3e1650b8024624e7cd50e484e37abdab73a7a706188ad34543" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.2.0", + "http 1.1.0", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1136,9 +1169,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" dependencies = [ "xmlparser", ] @@ -1153,7 +1186,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "rustc_version 0.4.1", + "rustc_version 0.4.0", "tracing", ] @@ -1212,17 +1245,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", ] [[package]] @@ -1328,15 +1361,15 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.3.0", "piper", ] [[package]] name = "bloomfilter" -version = "1.0.16" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c541c70a910b485670304fd420f0eab8f7bde68439db6a8d98819c3d2774d7e2" +checksum = "bc0bdbcf2078e0ba8a74e1fe0cf36f54054a04485759b61dfd60b174658e9607" dependencies = [ "bit-vec 0.7.0", "getrandom 0.2.15", @@ -1356,9 +1389,9 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1387,9 +1420,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "serde", @@ -1406,7 +1439,7 @@ dependencies = [ "quote", "str_inflector", "syn 2.0.90", - "thiserror 1.0.69", + "thiserror", "try_match", ] @@ -1430,9 +1463,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" dependencies = [ "serde", ] @@ -1498,13 +1531,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" dependencies = [ "jobserver", "libc", - "shlex", ] [[package]] @@ -1515,9 +1547,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1566,9 +1598,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -1576,9 +1608,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -1588,9 +1620,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1600,33 +1632,36 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "cmake" -version = "0.1.52" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" -version = "4.6.7" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" dependencies = [ - "bytes", + "ascii", + "byteorder", + "either", "memchr", + "unreachable", ] [[package]] @@ -1667,7 +1702,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width 0.1.14", + "unicode-width", "windows-sys 0.52.0", ] @@ -1767,9 +1802,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "countme" @@ -1779,9 +1814,9 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -2071,7 +2106,7 @@ dependencies = [ "strum_macros 0.25.3", "syn 1.0.109", "syn 2.0.90", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -2145,9 +2180,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", @@ -2163,7 +2198,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version 0.4.0", "syn 2.0.90", ] @@ -2192,7 +2227,7 @@ dependencies = [ "console", "shell-words", "tempfile", - "thiserror 1.0.69", + "thiserror", "zeroize", ] @@ -2222,7 +2257,7 @@ checksum = "c3ca7fa3ba397980657070e679f412acddb7a372f1793ff68ef0bbe708680f0f" dependencies = [ "regex", "sha2", - "thiserror 1.0.69", + "thiserror", "walkdir", ] @@ -2266,9 +2301,9 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dunce" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "dw" @@ -2354,11 +2389,11 @@ dependencies = [ [[package]] name = "enum-as-inner" -version = "0.6.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.90", @@ -2414,12 +2449,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2434,6 +2469,17 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -2447,9 +2493,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener 5.3.1", "pin-project-lite", @@ -2516,9 +2562,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "ff" @@ -2532,14 +2578,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.59.0", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] @@ -2550,9 +2596,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.35" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "libz-ng-sys", @@ -2679,7 +2725,7 @@ dependencies = [ "rustls-native-certs", "rustls-webpki", "semver 1.0.23", - "socket2", + "socket2 0.5.7", "tokio", "tokio-rustls", "tokio-stream", @@ -2709,9 +2755,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -2724,9 +2770,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -2734,15 +2780,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -2752,9 +2798,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -2773,11 +2819,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.3.0", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -2786,9 +2832,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -2797,21 +2843,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-test" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961fb6311645f46e2cdc2964a8bfae6743fd72315eaec181a71ae3eb2467113" +checksum = "ce388237b32ac42eca0df1ba55ed3bbda4eaf005d7d4b5dbc0b20ab962928ac9" dependencies = [ "futures-core", "futures-executor", @@ -2821,6 +2867,7 @@ dependencies = [ "futures-task", "futures-util", "pin-project", + "pin-utils", ] [[package]] @@ -2831,9 +2878,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -2895,9 +2942,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "git2" @@ -2922,22 +2969,22 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.15" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", + "regex-automata 0.4.8", "regex-syntax 0.8.5", ] [[package]] name = "gloo-timers" -version = "0.3.0" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" dependencies = [ "futures-channel", "futures-core", @@ -2956,12 +3003,12 @@ dependencies = [ [[package]] name = "graphql-parser" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a818c0d883d7c0801df27be910917750932be279c7bc82dc541b8769425f409" +checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" dependencies = [ "combine", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -3026,7 +3073,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -3054,7 +3101,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -3068,7 +3115,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -3082,6 +3129,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -3193,7 +3244,7 @@ dependencies = [ "ipnet", "once_cell", "rand 0.8.5", - "thiserror 1.0.69", + "thiserror", "tinyvec", "tokio", "tracing", @@ -3216,7 +3267,7 @@ dependencies = [ "rand 0.8.5", "resolv-conf", "smallvec", - "thiserror 1.0.69", + "thiserror", "tokio", "tracing", ] @@ -3263,9 +3314,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -3290,7 +3341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.1.0", ] [[package]] @@ -3301,7 +3352,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.2.0", + "http 1.1.0", "http-body 1.0.1", "pin-project-lite", ] @@ -3345,9 +3396,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -3388,7 +3439,7 @@ dependencies = [ "itoa", "pin-project-lite", "smallvec", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -3632,26 +3683,26 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.14.5", "serde", ] [[package]] name = "indicatif" -version = "0.17.9" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", + "instant", "number_prefix", "portable-atomic", - "unicode-width 0.2.0", - "web-time", + "unicode-width", ] [[package]] @@ -3682,9 +3733,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.41.1" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" dependencies = [ "console", "lazy_static", @@ -3720,13 +3771,24 @@ dependencies = [ "ghost", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.7", "widestring", "windows-sys 0.48.0", "winreg", @@ -3734,26 +3796,26 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi 0.3.9", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "iso8601" @@ -3793,26 +3855,25 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ - "once_cell", "wasm-bindgen", ] @@ -3826,7 +3887,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -3955,18 +4016,19 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libfuzzer-sys" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" dependencies = [ "arbitrary", "cc", + "once_cell", ] [[package]] @@ -3991,7 +4053,6 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall", ] [[package]] @@ -4022,9 +4083,9 @@ dependencies = [ [[package]] name = "libz-ng-sys" -version = "1.1.20" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0f7295a34685977acb2e8cc8b08ee4a8dffd6cf278eeccddbe1ed55ba815d5" +checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" dependencies = [ "cmake", "libc", @@ -4032,9 +4093,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.20" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "libc", @@ -4050,24 +4111,30 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linkme" -version = "0.3.31" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae" +checksum = "ccb76662d78edc9f9bf56360d6919bdacc8b7761227727e5082f128eeb90bbf5" dependencies = [ "linkme-impl", ] [[package]] name = "linkme-impl" -version = "0.3.31" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9" +checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -4102,11 +4169,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.14.5", ] [[package]] @@ -4206,11 +4273,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ - "adler2", + "adler", ] [[package]] @@ -4231,22 +4298,11 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - [[package]] name = "mockall" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" dependencies = [ "cfg-if", "downcast", @@ -4258,9 +4314,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" dependencies = [ "cfg-if", "proc-macro2", @@ -4282,7 +4338,7 @@ dependencies = [ "log", "memchr", "mime", - "spin 0.9.8", + "spin", "version_check", ] @@ -4310,15 +4366,6 @@ dependencies = [ "serde", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" -dependencies = [ - "spin 0.5.2", -] - [[package]] name = "nom" version = "7.1.3" @@ -4341,7 +4388,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio 0.8.11", + "mio", "walkdir", "windows-sys 0.48.0", ] @@ -4487,21 +4534,18 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.36.5" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -dependencies = [ - "portable-atomic", -] +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" @@ -4529,18 +4573,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.4.1+3.4.0" +version = "300.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -4570,7 +4614,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror 1.0.69", + "thiserror", "urlencoding", ] @@ -4600,7 +4644,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "reqwest", "rmp", - "thiserror 1.0.69", + "thiserror", "url", ] @@ -4652,7 +4696,7 @@ dependencies = [ "opentelemetry_sdk 0.20.0", "prost 0.11.9", "reqwest", - "thiserror 1.0.69", + "thiserror", "tokio", "tonic 0.9.2", ] @@ -4735,7 +4779,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", "typed-builder", ] @@ -4751,7 +4795,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror 1.0.69", + "thiserror", "urlencoding", ] @@ -4774,7 +4818,7 @@ dependencies = [ "rand 0.8.5", "regex", "serde_json", - "thiserror 1.0.69", + "thiserror", "tokio", "tokio-stream", ] @@ -4793,10 +4837,10 @@ dependencies = [ "glob", "once_cell", "opentelemetry 0.22.0", - "ordered-float 4.5.0", + "ordered-float 4.2.1", "percent-encoding", "rand 0.8.5", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -4825,9 +4869,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.5.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" dependencies = [ "num-traits", ] @@ -4858,9 +4902,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -4880,7 +4924,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.3", "smallvec", "windows-targets 0.52.6", ] @@ -4918,20 +4962,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" dependencies = [ "memchr", - "thiserror 2.0.6", + "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" dependencies = [ "pest", "pest_generator", @@ -4939,9 +4983,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" dependencies = [ "pest", "pest_meta", @@ -4952,9 +4996,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" dependencies = [ "once_cell", "pest", @@ -4968,25 +5012,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.7.0", + "indexmap 2.2.6", "serde", "serde_derive", ] [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -4995,9 +5039,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -5007,12 +5051,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", - "fastrand 2.3.0", + "fastrand 2.1.0", "futures-io", ] @@ -5028,15 +5072,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.7" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -5047,15 +5091,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.7" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.7" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -5073,24 +5117,40 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.4" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix", + "rustix 0.38.34", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" [[package]] name = "powerfmt" @@ -5100,12 +5160,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" @@ -5119,15 +5176,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.8" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" -version = "1.0.11" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", @@ -5183,7 +5240,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -5297,7 +5354,7 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", "typetag", ] @@ -5315,9 +5372,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -5429,22 +5486,31 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -5455,7 +5521,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.8", "regex-syntax 0.8.5", ] @@ -5470,9 +5536,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -5571,14 +5637,13 @@ dependencies = [ [[package]] name = "rhai" -version = "1.20.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8867cfc57aaf2320b60ec0f4d55603ac950ce852e6ab6b9109aa3d626a4dd7ea" +checksum = "61797318be89b1a268a018a92a7657096d83f3ecb31418b9e9c16dcbb043b702" dependencies = [ "ahash", "bitflags 2.6.0", "instant", - "no-std-compat", "num-traits", "once_cell", "rhai_codegen", @@ -5669,7 +5734,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -5713,7 +5778,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", "tokio", "tower", "tower-service", @@ -5826,24 +5891,38 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver 1.0.23", ] [[package]] name = "rustix" -version = "0.38.42" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.59.0", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", ] [[package]] @@ -5891,9 +5970,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -5912,20 +5991,20 @@ dependencies = [ [[package]] name = "scc" -version = "2.2.5" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b202022bb57c049555430e11fc22fea12909276a80a4c3d368da36ac1d88ed" +checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5971,9 +6050,9 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" +checksum = "8eb0dde0ccd15e337a3cf738a9a38115c6d8e74795d074e73973dad3d229a897" [[package]] name = "sec1" @@ -6004,9 +6083,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -6071,7 +6150,7 @@ dependencies = [ "quote", "regex", "syn 2.0.90", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -6087,13 +6166,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.2.6", "itoa", - "memchr", "ryu", "serde", ] @@ -6106,7 +6184,7 @@ checksum = "0ecd92a088fb2500b2f146c9ddc5da9950bb7264d3f00932cd2a6fb369c26c46" dependencies = [ "ahash", "bytes", - "indexmap 2.7.0", + "indexmap 2.2.6", "jsonpath-rust", "regex", "serde", @@ -6131,14 +6209,14 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -6167,7 +6245,7 @@ dependencies = [ "serde", "serde_bytes", "smallvec", - "thiserror 1.0.69", + "thiserror", "v8", ] @@ -6185,9 +6263,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ "futures", "log", @@ -6199,9 +6277,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", @@ -6254,12 +6332,6 @@ dependencies = [ "dirs", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -6281,9 +6353,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.6.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "simple_asn1" @@ -6293,7 +6365,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror 1.0.69", + "thiserror", "time", ] @@ -6335,9 +6407,19 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -6359,12 +6441,6 @@ dependencies = [ "url", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -6519,9 +6595,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" dependencies = [ "core-foundation-sys", "libc", @@ -6554,15 +6630,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.3.0", - "once_cell", - "rustix", - "windows-sys 0.59.0", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", ] [[package]] @@ -6628,38 +6703,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" -dependencies = [ - "thiserror-impl 2.0.6", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -6739,9 +6794,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -6762,9 +6817,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -6816,20 +6871,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.3", + "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -6844,9 +6900,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -6865,9 +6921,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -6905,9 +6961,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -6920,21 +6976,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.22", + "toml_edit 0.22.16", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -6945,22 +7001,22 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.20", + "winnow 0.6.13", ] [[package]] @@ -7075,15 +7131,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tower-test" @@ -7101,9 +7157,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -7113,9 +7169,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -7124,9 +7180,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -7192,21 +7248,11 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term 0.46.0", @@ -7220,7 +7266,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log 0.2.0", - "tracing-serde 0.2.0", + "tracing-serde", ] [[package]] @@ -7246,9 +7292,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.14" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" dependencies = [ "serde", "stable_deref_trait", @@ -7262,18 +7308,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "try_match" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b065c869a3f832418e279aa4c1d7088f9d5d323bde15a60a08e20c2cd4549082" +checksum = "61ae3c1941e8859e30d28e572683fbfa89ae5330748b45139aedf488389e2be4" dependencies = [ "try_match_inner", ] [[package]] name = "try_match_inner" -version = "0.5.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c81686f7ab4065ccac3df7a910c4249f8c0f3fb70421d6ddec19b9311f63f9" +checksum = "b0a91713132798caecb23c977488945566875e7b61b902fb111979871cbff34e" dependencies = [ "proc-macro2", "quote", @@ -7295,7 +7341,7 @@ dependencies = [ "rand 0.8.5", "rustls", "sha1", - "thiserror 1.0.69", + "thiserror", "url", "utf-8", ] @@ -7349,9 +7395,9 @@ dependencies = [ [[package]] name = "ucd-trie" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "uname" @@ -7405,48 +7451,54 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-id" -version = "0.3.5" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" +checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] -name = "unicode-width" -version = "0.2.0" +name = "unreachable" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] [[package]] name = "untrusted" @@ -7540,9 +7592,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" @@ -7552,9 +7604,15 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vsimd" @@ -7601,23 +7659,23 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", - "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", + "once_cell", "proc-macro2", "quote", "syn 2.0.90", @@ -7626,22 +7684,21 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7649,9 +7706,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -7662,15 +7719,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -7681,19 +7738,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -7714,7 +7761,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.34", ] [[package]] @@ -7741,11 +7788,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7907,15 +7954,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-targets" version = "0.42.2" @@ -8105,9 +8143,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -8146,15 +8184,15 @@ dependencies = [ [[package]] name = "wmi" -version = "0.14.2" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc47c0776cc6c00d2f7a874a0c846d94d45535936e5a1187693a24f23b4dd701" +checksum = "70df482bbec7017ce4132154233642de658000b24b805345572036782a66ad55" dependencies = [ "chrono", "futures", "log", "serde", - "thiserror 2.0.6", + "thiserror", "windows 0.58.0", "windows-core 0.58.0", ] @@ -8228,7 +8266,6 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive", ] @@ -8303,18 +8340,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.1" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", From 3c62d5760ca370f9fca03857b43dc9061909bdce Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 9 Dec 2024 09:57:38 -0700 Subject: [PATCH 092/112] docs: combine client awareness and enforcement docs (#6414) Co-authored-by: Edward Huang --- .../observability/client-awareness.mdx | 28 ---------- .../observability/client-id-enforcement.mdx | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 34 deletions(-) delete mode 100644 docs/source/routing/observability/client-awareness.mdx diff --git a/docs/source/routing/observability/client-awareness.mdx b/docs/source/routing/observability/client-awareness.mdx deleted file mode 100644 index 8b5f566aef..0000000000 --- a/docs/source/routing/observability/client-awareness.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Client Awareness -subtitle: Configure client awareness in the router -description: Configure client awareness in the Apollo GraphOS Router or Apollo Router Core to separate the metrics and operations per client. ---- - -import { Link } from "gatsby"; - -The GraphOS Router and Apollo Router Core support [client awareness](/graphos/metrics/client-awareness/) by default. If the client sets the headers `apollographql-client-name` and `apollographql-client-version` in its HTTP requests, GraphOS Studio can separate the metrics and operations per client. - -This client name is also used by the [Persisted Queries](/graphos/routing/security/persisted-queries) feature. - -## Overriding client awareness headers - -Different header names can be used by updating the configuration file. If those headers will be sent by a browser, they must be allowed in the [CORS (Cross Origin Resource Sharing) configuration](/router/configuration/cors), as follows: - -```yaml title="router.yaml" -telemetry: - apollo: - # defaults to apollographql-client-name - client_name_header: MyClientHeaderName - # defaults to apollographql-client-version - client_version_header: MyClientHeaderVersion -cors: - # The headers to allow. - # (Defaults to [ Content-Type ], which is required for GraphOS Studio) - allow_headers: [ Content-Type, MyClientHeaderName, MyClientHeaderVersion] -``` diff --git a/docs/source/routing/observability/client-id-enforcement.mdx b/docs/source/routing/observability/client-id-enforcement.mdx index 474405547e..15d63fc691 100644 --- a/docs/source/routing/observability/client-id-enforcement.mdx +++ b/docs/source/routing/observability/client-id-enforcement.mdx @@ -1,5 +1,5 @@ --- -title: Client ID Enforcement +title: Client Awareness and Enforcement subtitle: Require client details and operation names to help monitor schema usage description: Improve GraphQL operation monitoring by tagging operations with with client details. See code examples for Apollo GraphOS Router and Apollo Server. published: 2022-05-31 @@ -7,19 +7,61 @@ id: TN0001 tags: [server, observability, router] redirectFrom: - /technotes/TN0001-client-id-enforcement/ + - /graphos/routing/observability/client-awareness --- -As part of GraphOS Studio metrics reporting, servers can [tag reported operations with the requesting client's name and version](/graphos/metrics/client-awareness). This helps graph maintainers understand which clients are using which fields in the schema. +Metrics about GraphQL schema usage are more insightful when information about clients using the schema is available. Understanding client usage can help you reshape your schema to serve clients more efficiently. +As part of GraphOS Studio metrics reporting, servers can [tag reported operations with the requesting client's name and version](/graphos/metrics/client-awareness). +This **client awareness** helps graph maintainers understand which clients are using which fields in the schema. -Clients can (and should) also [name their GraphQL operations](/react/data/operation-best-practices/#name-all-operations), which provides more context around how and where data is being used. +Apollo's GraphOS Router and Apollo Server can enable client awareness by requiring metadata about requesting clients. +The router supports client awareness by default. If the client sets its name and version with the headers `apollographql-client-name` and `apollographql-client-version` in its HTTP requests, GraphOS Studio can separate the metrics and operations per client. -Together, these pieces of information help teams monitor their graph and make changes to it safely. We strongly encourage that your GraphQL gateway require client details and operation names from all requesting clients. + + +The client name is also used by the persisted queries feature. + + + + +Clients should [name their GraphQL operations](/react/data/operation-best-practices/#name-all-operations) to provide more context around how and where data is being used. + +## Why enforce client reporting? + +Client metadata enables better insights into schema usage, such as: + +- **Identifying which clients use which fields**: This facilitates usage monitoring and safe deprecation of fields. +- **Understanding traffic patterns**: This helps optimize schema design based on real-world client behavior. +- **Improving operation-level observability**: This provides details for debugging and performance improvements. + +Apollo strongly recommends requiring client name, client version, and operation names in all incoming GraphQL requests. ## Enforcing in GraphOS Router -The GraphOS Router supports client awareness by default if the client sets the `apollographql-client-name` and `apollographql-client-id` in their requests. These values can be overridden using the [router configuration file](/router/managed-federation/client-awareness/) directly. +The GraphOS Router supports client awareness by default if the client sets the `apollographql-client-name` and `apollographql-client-id` in their requests. +These values can be overridden using the [router configuration file](/router/managed-federation/client-awareness/) directly. +You can use a Rhai script to _enforce_ that clients include metadata. + +### Customizing client awareness headers + +If headers with customized names need to be sent by a browser, they must be allowed in the [CORS (Cross Origin Resource Sharing) configuration](/router/configuration/cors), as follows: + +```yaml title="router.yaml" +telemetry: + apollo: + # defaults to apollographql-client-name + client_name_header: MyClientHeaderName + # defaults to apollographql-client-version + client_version_header: MyClientHeaderVersion +cors: + # The headers to allow. + # (Defaults to [ Content-Type ], which is required for GraphOS Studio) + allow_headers: [ Content-Type, MyClientHeaderName, MyClientHeaderVersion] +``` + +### Enforcing via Rhai script -Client headers can also be enforced using a [Rhai script](/graphos/routing/customization/rhai) on every incoming request. +Client headers can be enforced using a [Rhai script](/graphos/routing/customization/rhai) on every incoming request. ```rhai title="client-id.rhai" fn supergraph_service(service) { From cf7a42df45cc8b69aadf25dbcc4f18611c66beb9 Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Mon, 9 Dec 2024 11:49:01 -0800 Subject: [PATCH 093/112] Ensure cost directives are picked up when not explicitly imported (#6328) Co-authored-by: Iryna Shestak --- .../fix_tninesling_cost_name_handling.md | 22 ++ .../src/link/cost_spec_definition.rs | 264 ++++++++++++++---- .../src/link/federation_spec_definition.rs | 13 - apollo-federation/src/link/mod.rs | 2 +- apollo-federation/src/link/spec_definition.rs | 11 - apollo-federation/src/supergraph/mod.rs | 171 +++--------- apollo-federation/src/supergraph/schema.rs | 41 --- .../cost_calculator/directives.rs | 197 ++----------- .../fixtures/custom_cost_schema.graphql | 12 +- .../demand_control/cost_calculator/schema.rs | 76 +++-- .../cost_calculator/static_cost.rs | 11 +- .../src/plugins/demand_control/mod.rs | 14 + 12 files changed, 366 insertions(+), 468 deletions(-) create mode 100644 .changesets/fix_tninesling_cost_name_handling.md diff --git a/.changesets/fix_tninesling_cost_name_handling.md b/.changesets/fix_tninesling_cost_name_handling.md new file mode 100644 index 0000000000..fe911c43ed --- /dev/null +++ b/.changesets/fix_tninesling_cost_name_handling.md @@ -0,0 +1,22 @@ +### Ensure cost directives are picked up when not explicitly imported ([PR #6328](https://github.com/apollographql/router/pull/6328)) + +With the recent composition changes, importing `@cost` results in a supergraph schema with the cost specification import at the top. The `@cost` directive itself is not explicitly imported, as it's expected to be available as the default export from the cost link. In contrast, uses of `@listSize` to translate to an explicit import in the supergraph. + +Old SDL link + +``` +@link( + url: "https://specs.apollo.dev/cost/v0.1" + import: ["@cost", "@listSize"] +) +``` + +New SDL link + +``` +@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) +``` + +Instead of using the directive names from the import list in the link, the directive names now come from `SpecDefinition::directive_name_in_schema`, which is equivalent to the change we made on the composition side. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6328 diff --git a/apollo-federation/src/link/cost_spec_definition.rs b/apollo-federation/src/link/cost_spec_definition.rs index 9e2aed07cc..bd0894c392 100644 --- a/apollo-federation/src/link/cost_spec_definition.rs +++ b/apollo-federation/src/link/cost_spec_definition.rs @@ -1,16 +1,20 @@ +use std::collections::HashSet; + use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; -use apollo_compiler::collections::IndexMap; +use apollo_compiler::ast::DirectiveList; +use apollo_compiler::ast::FieldDefinition; +use apollo_compiler::ast::InputValueDefinition; use apollo_compiler::name; use apollo_compiler::schema::Component; -use apollo_compiler::schema::EnumType; -use apollo_compiler::schema::ObjectType; -use apollo_compiler::schema::ScalarType; +use apollo_compiler::schema::ExtendedType; use apollo_compiler::Name; use apollo_compiler::Node; use lazy_static::lazy_static; use crate::error::FederationError; +use crate::internal_error; +use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph; use crate::link::spec::Identity; use crate::link::spec::Url; use crate::link::spec::Version; @@ -21,14 +25,17 @@ use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::position::ScalarTypeDefinitionPosition; use crate::schema::FederationSchema; -pub(crate) const COST_DIRECTIVE_NAME_IN_SPEC: Name = name!("cost"); -pub(crate) const COST_DIRECTIVE_NAME_DEFAULT: Name = name!("federation__cost"); - -pub(crate) const LIST_SIZE_DIRECTIVE_NAME_IN_SPEC: Name = name!("listSize"); -pub(crate) const LIST_SIZE_DIRECTIVE_NAME_DEFAULT: Name = name!("federation__listSize"); +const COST_DIRECTIVE_NAME: Name = name!("cost"); +const COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME: Name = name!("weight"); +const LIST_SIZE_DIRECTIVE_NAME: Name = name!("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"); #[derive(Clone)] -pub(crate) struct CostSpecDefinition { +pub struct CostSpecDefinition { url: Url, minimum_federation_version: Option, } @@ -36,27 +43,24 @@ pub(crate) struct CostSpecDefinition { macro_rules! propagate_demand_control_directives { ($func_name:ident, $directives_ty:ty, $wrap_ty:expr) => { pub(crate) fn $func_name( - &self, - subgraph_schema: &FederationSchema, + supergraph_schema: &FederationSchema, source: &$directives_ty, + subgraph_schema: &FederationSchema, dest: &mut $directives_ty, - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { - let cost_directive_name = original_directive_names.get(&COST_DIRECTIVE_NAME_IN_SPEC); - let cost_directive = cost_directive_name.and_then(|name| source.get(name.as_str())); + let cost_directive = Self::cost_directive_name(supergraph_schema)? + .and_then(|name| source.get(name.as_str())); if let Some(cost_directive) = cost_directive { - dest.push($wrap_ty(self.cost_directive( + dest.push($wrap_ty(Self::cost_directive( subgraph_schema, cost_directive.arguments.clone(), )?)); } - let list_size_directive_name = - original_directive_names.get(&LIST_SIZE_DIRECTIVE_NAME_IN_SPEC); - let list_size_directive = - list_size_directive_name.and_then(|name| source.get(name.as_str())); + let list_size_directive = Self::list_size_directive_name(supergraph_schema)? + .and_then(|name| source.get(name.as_str())); if let Some(list_size_directive) = list_size_directive { - dest.push($wrap_ty(self.list_size_directive( + dest.push($wrap_ty(Self::list_size_directive( subgraph_schema, list_size_directive.arguments.clone(), )?)); @@ -68,34 +72,31 @@ macro_rules! propagate_demand_control_directives { } macro_rules! propagate_demand_control_directives_to_position { - ($func_name:ident, $source_ty:ty, $dest_ty:ty) => { + ($func_name:ident, $source_ty:ty, $pos_ty:ty) => { pub(crate) fn $func_name( - &self, + supergraph_schema: &FederationSchema, subgraph_schema: &mut FederationSchema, - source: &Node<$source_ty>, - dest: &$dest_ty, - original_directive_names: &IndexMap, + pos: &$pos_ty, ) -> Result<(), FederationError> { - let cost_directive_name = original_directive_names.get(&COST_DIRECTIVE_NAME_IN_SPEC); - let cost_directive = - cost_directive_name.and_then(|name| source.directives.get(name.as_str())); + let source = pos.get(supergraph_schema.schema())?; + let cost_directive = Self::cost_directive_name(supergraph_schema)? + .and_then(|name| source.directives.get(name.as_str())); if let Some(cost_directive) = cost_directive { - dest.insert_directive( + pos.insert_directive( subgraph_schema, - Component::from( - self.cost_directive(subgraph_schema, cost_directive.arguments.clone())?, - ), + Component::from(Self::cost_directive( + subgraph_schema, + cost_directive.arguments.clone(), + )?), )?; } - let list_size_directive_name = - original_directive_names.get(&LIST_SIZE_DIRECTIVE_NAME_IN_SPEC); - let list_size_directive = - list_size_directive_name.and_then(|name| source.directives.get(name.as_str())); + let list_size_directive = Self::list_size_directive_name(supergraph_schema)? + .and_then(|name| source.directives.get(name.as_str())); if let Some(list_size_directive) = list_size_directive { - dest.insert_directive( + pos.insert_directive( subgraph_schema, - Component::from(self.list_size_directive( + Component::from(Self::list_size_directive( subgraph_schema, list_size_directive.arguments.clone(), )?), @@ -119,35 +120,32 @@ impl CostSpecDefinition { } pub(crate) fn cost_directive( - &self, schema: &FederationSchema, arguments: Vec>, ) -> Result { - let name = self - .directive_name_in_schema(schema, &COST_DIRECTIVE_NAME_IN_SPEC)? - .unwrap_or(COST_DIRECTIVE_NAME_DEFAULT); + let name = Self::cost_directive_name(schema)?.ok_or_else(|| { + internal_error!("The \"@cost\" directive is undefined in the target schema") + })?; Ok(Directive { name, arguments }) } pub(crate) fn list_size_directive( - &self, schema: &FederationSchema, arguments: Vec>, ) -> Result { - let name = self - .directive_name_in_schema(schema, &LIST_SIZE_DIRECTIVE_NAME_IN_SPEC)? - .unwrap_or(LIST_SIZE_DIRECTIVE_NAME_DEFAULT); + let name = Self::list_size_directive_name(schema)?.ok_or_else(|| { + internal_error!("The \"@listSize\" directive is undefined in the target schema") + })?; Ok(Directive { name, arguments }) } propagate_demand_control_directives!( propagate_demand_control_directives, - apollo_compiler::ast::DirectiveList, + DirectiveList, Node::new ); - propagate_demand_control_directives_to_position!( propagate_demand_control_directives_for_enum, EnumType, @@ -163,6 +161,81 @@ impl CostSpecDefinition { ScalarType, ScalarTypeDefinitionPosition ); + + fn for_federation_schema(schema: &FederationSchema) -> Option<&'static Self> { + let link = schema + .metadata()? + .for_identity(&Identity::cost_identity())?; + COST_VERSIONS.find(&link.url.version) + } + + /// Returns the name of the `@cost` directive in the given schema, accounting for import aliases or specification name + /// prefixes such as `@federation__cost`. This checks the linked cost specification, if there is one, and falls back + /// to the federation spec. + fn cost_directive_name(schema: &FederationSchema) -> Result, FederationError> { + if let Some(spec) = Self::for_federation_schema(schema) { + spec.directive_name_in_schema(schema, &COST_DIRECTIVE_NAME) + } else if let Ok(fed_spec) = get_federation_spec_definition_from_subgraph(schema) { + fed_spec.directive_name_in_schema(schema, &COST_DIRECTIVE_NAME) + } else { + Ok(None) + } + } + + /// Returns the name of the `@listSize` directive in the given schema, accounting for import aliases or specification name + /// prefixes such as `@federation__listSize`. This checks the linked cost specification, if there is one, and falls back + /// to the federation spec. + fn list_size_directive_name( + schema: &FederationSchema, + ) -> Result, FederationError> { + if let Some(spec) = Self::for_federation_schema(schema) { + spec.directive_name_in_schema(schema, &LIST_SIZE_DIRECTIVE_NAME) + } else if let Ok(fed_spec) = get_federation_spec_definition_from_subgraph(schema) { + fed_spec.directive_name_in_schema(schema, &LIST_SIZE_DIRECTIVE_NAME) + } else { + Ok(None) + } + } + + pub fn cost_directive_from_argument( + schema: &FederationSchema, + argument: &InputValueDefinition, + ty: &ExtendedType, + ) -> Result, FederationError> { + let directive_name = Self::cost_directive_name(schema)?; + if let Some(name) = directive_name.as_ref() { + Ok(CostDirective::from_directives(name, &argument.directives) + .or(CostDirective::from_schema_directives(name, ty.directives()))) + } else { + Ok(None) + } + } + + pub fn cost_directive_from_field( + schema: &FederationSchema, + field: &FieldDefinition, + ty: &ExtendedType, + ) -> Result, FederationError> { + let directive_name = Self::cost_directive_name(schema)?; + if let Some(name) = directive_name.as_ref() { + Ok(CostDirective::from_directives(name, &field.directives) + .or(CostDirective::from_schema_directives(name, ty.directives()))) + } else { + Ok(None) + } + } + + pub fn list_size_directive_from_field_definition( + schema: &FederationSchema, + field: &FieldDefinition, + ) -> Result, FederationError> { + let directive_name = Self::list_size_directive_name(schema)?; + if let Some(name) = directive_name.as_ref() { + Ok(ListSizeDirective::from_field_definition(name, field)) + } else { + Ok(None) + } + } } impl SpecDefinition for CostSpecDefinition { @@ -185,3 +258,96 @@ lazy_static! { definitions }; } + +pub struct CostDirective { + weight: i32, +} + +impl CostDirective { + pub fn weight(&self) -> f64 { + self.weight as f64 + } + + fn from_directives(directive_name: &Name, directives: &DirectiveList) -> Option { + directives + .get(directive_name)? + .specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)? + .to_i32() + .map(|weight| Self { weight }) + } + + fn from_schema_directives( + directive_name: &Name, + directives: &apollo_compiler::schema::DirectiveList, + ) -> Option { + directives + .get(directive_name)? + .specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)? + .to_i32() + .map(|weight| Self { weight }) + } +} + +pub struct ListSizeDirective { + pub assumed_size: Option, + pub slicing_argument_names: Option>, + pub sized_fields: Option>, + pub require_one_slicing_argument: bool, +} + +impl ListSizeDirective { + pub fn from_field_definition( + directive_name: &Name, + definition: &FieldDefinition, + ) -> Option { + let directive = definition.directives.get(directive_name)?; + let assumed_size = Self::assumed_size(directive); + let slicing_argument_names = Self::slicing_argument_names(directive); + let sized_fields = Self::sized_fields(directive); + let require_one_slicing_argument = + Self::require_one_slicing_argument(directive).unwrap_or(true); + + Some(Self { + assumed_size, + slicing_argument_names, + sized_fields, + require_one_slicing_argument, + }) + } + + fn assumed_size(directive: &Directive) -> Option { + directive + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME)? + .to_i32() + } + + fn slicing_argument_names(directive: &Directive) -> Option> { + let names = directive + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME)? + .as_list()? + .iter() + .flat_map(|arg| arg.as_str()) + .map(String::from) + .collect(); + Some(names) + } + + fn sized_fields(directive: &Directive) -> Option> { + let fields = directive + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME)? + .as_list()? + .iter() + .flat_map(|arg| arg.as_str()) + .map(String::from) + .collect(); + Some(fields) + } + + fn require_one_slicing_argument(directive: &Directive) -> Option { + directive + .specified_argument_by_name( + &LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME, + )? + .to_bool() + } +} diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 5e3e09512c..e89ce9f7ff 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -14,8 +14,6 @@ use crate::error::SingleFederationError; use crate::link::argument::directive_optional_boolean_argument; use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; -use crate::link::cost_spec_definition::CostSpecDefinition; -use crate::link::cost_spec_definition::COST_VERSIONS; use crate::link::spec::Identity; use crate::link::spec::Url; use crate::link::spec::Version; @@ -539,17 +537,6 @@ impl FederationSpecDefinition { )?, }) } - - pub(crate) fn get_cost_spec_definition( - &self, - schema: &FederationSchema, - ) -> Option<&'static CostSpecDefinition> { - schema - .metadata() - .and_then(|metadata| metadata.for_identity(&Identity::cost_identity())) - .and_then(|link| COST_VERSIONS.find(&link.url.version)) - .or_else(|| COST_VERSIONS.find_for_federation_version(self.version())) - } } impl SpecDefinition for FederationSpecDefinition { diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 971428257d..1f59986d93 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -23,7 +23,7 @@ use crate::link::spec::Url; pub(crate) mod argument; pub(crate) mod context_spec_definition; -pub(crate) mod cost_spec_definition; +pub mod cost_spec_definition; pub mod database; pub(crate) mod federation_spec_definition; pub(crate) mod graphql_definition; diff --git a/apollo-federation/src/link/spec_definition.rs b/apollo-federation/src/link/spec_definition.rs index 1fb084afe5..5826f8f4d9 100644 --- a/apollo-federation/src/link/spec_definition.rs +++ b/apollo-federation/src/link/spec_definition.rs @@ -182,17 +182,6 @@ impl SpecDefinitions { self.definitions.get(requested) } - pub(crate) fn find_for_federation_version(&self, federation_version: &Version) -> Option<&T> { - for definition in self.definitions.values() { - if let Some(minimum_federation_version) = definition.minimum_federation_version() { - if minimum_federation_version >= federation_version { - return Some(definition); - } - } - } - None - } - pub(crate) fn versions(&self) -> Keys { self.definitions.keys() } diff --git a/apollo-federation/src/supergraph/mod.rs b/apollo-federation/src/supergraph/mod.rs index a5547c27e6..d6d05a5d68 100644 --- a/apollo-federation/src/supergraph/mod.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -38,7 +38,6 @@ 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; @@ -265,8 +264,6 @@ fn extract_subgraphs_from_fed_2_supergraph( context_spec_definition: Option<&'static ContextSpecDefinition>, filtered_types: &Vec, ) -> Result<(), FederationError> { - let original_directive_names = get_apollo_directive_names(supergraph_schema)?; - let TypeInfos { object_types, interface_types, @@ -281,7 +278,6 @@ fn extract_subgraphs_from_fed_2_supergraph( join_spec_definition, context_spec_definition, filtered_types, - &original_directive_names, )?; extract_object_type_content( @@ -291,7 +287,6 @@ fn extract_subgraphs_from_fed_2_supergraph( federation_spec_definitions, join_spec_definition, &object_types, - &original_directive_names, )?; extract_interface_type_content( supergraph_schema, @@ -300,7 +295,6 @@ fn extract_subgraphs_from_fed_2_supergraph( federation_spec_definitions, join_spec_definition, &interface_types, - &original_directive_names, )?; extract_union_type_content( supergraph_schema, @@ -313,19 +307,15 @@ fn extract_subgraphs_from_fed_2_supergraph( supergraph_schema, subgraphs, graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, join_spec_definition, &enum_types, - &original_directive_names, )?; extract_input_object_type_content( supergraph_schema, subgraphs, graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, join_spec_definition, &input_object_types, - &original_directive_names, )?; extract_join_directives( @@ -404,7 +394,6 @@ fn add_all_empty_subgraph_types( join_spec_definition: &'static JoinSpecDefinition, context_spec_definition: Option<&'static ContextSpecDefinition>, filtered_types: &Vec, - original_directive_names: &IndexMap, ) -> Result { let type_directive_definition = join_spec_definition.type_directive_definition(supergraph_schema)?; @@ -434,12 +423,6 @@ fn add_all_empty_subgraph_types( graph_enum_value_name_to_subgraph_name, &type_directive_application.graph, )?; - let federation_spec_definition = federation_spec_definitions - .get(&type_directive_application.graph) - .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { - message: "Subgraph unexpectedly does not use federation spec" - .to_owned(), - })?; pos.pre_insert(&mut subgraph.schema)?; pos.insert( @@ -451,16 +434,11 @@ fn add_all_empty_subgraph_types( }), )?; - if let Some(cost_spec_definition) = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema) - { - cost_spec_definition.propagate_demand_control_directives_for_scalar( - &mut subgraph.schema, - pos.get(supergraph_schema.schema())?, - pos, - original_directive_names, - )?; - } + CostSpecDefinition::propagate_demand_control_directives_for_scalar( + supergraph_schema, + &mut subgraph.schema, + pos, + )?; } None } @@ -740,7 +718,6 @@ fn extract_object_type_content( federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, info: &[TypeInfo], - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { let field_directive_definition = join_spec_definition.field_directive_definition(supergraph_schema)?; @@ -796,21 +773,12 @@ fn extract_object_type_content( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; - let federation_spec_definition = federation_spec_definitions - .get(graph_enum_value) - .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { - message: "Subgraph unexpectedly does not use federation spec".to_owned(), - })?; - if let Some(cost_spec_definition) = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema) - { - cost_spec_definition.propagate_demand_control_directives_for_object( - &mut subgraph.schema, - type_, - &pos, - original_directive_names, - )?; - } + + CostSpecDefinition::propagate_demand_control_directives_for_object( + supergraph_schema, + &mut subgraph.schema, + &pos, + )?; } for (field_name, field) in type_.fields.iter() { @@ -836,17 +804,14 @@ fn extract_object_type_content( message: "Subgraph unexpectedly does not use federation spec" .to_owned(), })?; - let cost_spec_definition = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema); add_subgraph_field( field_pos.clone().into(), field, + supergraph_schema, subgraph, federation_spec_definition, is_shareable, None, - cost_spec_definition, - original_directive_names, )?; } } else { @@ -877,8 +842,6 @@ fn extract_object_type_content( message: "Subgraph unexpectedly does not use federation spec" .to_owned(), })?; - let cost_spec_definition = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema); if !subgraph_info.contains_key(graph_enum_value) { return Err( SingleFederationError::InvalidFederationSupergraph { @@ -894,12 +857,11 @@ fn extract_object_type_content( add_subgraph_field( field_pos.clone().into(), field, + supergraph_schema, subgraph, federation_spec_definition, is_shareable, Some(field_directive_application), - cost_spec_definition, - original_directive_names, )?; } } @@ -916,7 +878,6 @@ fn extract_interface_type_content( federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, info: &[TypeInfo], - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { let field_directive_definition = join_spec_definition.field_directive_definition(supergraph_schema)?; @@ -1039,17 +1000,14 @@ fn extract_interface_type_content( message: "Subgraph unexpectedly does not use federation spec" .to_owned(), })?; - let cost_spec_definition = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema); add_subgraph_field( pos.field(field_name.clone()), field, + supergraph_schema, subgraph, federation_spec_definition, false, None, - cost_spec_definition, - original_directive_names, )?; } } else { @@ -1073,8 +1031,6 @@ fn extract_interface_type_content( message: "Subgraph unexpectedly does not use federation spec" .to_owned(), })?; - let cost_spec_definition = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema); if !subgraph_info.contains_key(graph_enum_value) { return Err( SingleFederationError::InvalidFederationSupergraph { @@ -1090,12 +1046,11 @@ fn extract_interface_type_content( add_subgraph_field( pos.field(field_name.clone()), field, + supergraph_schema, subgraph, federation_spec_definition, false, Some(field_directive_application), - cost_spec_definition, - original_directive_names, )?; } } @@ -1201,10 +1156,8 @@ fn extract_enum_type_content( supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, graph_enum_value_name_to_subgraph_name: &IndexMap>, - federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, info: &[TypeInfo], - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { // This was added in join 0.3, so it can genuinely be None. let enum_value_directive_definition = @@ -1226,21 +1179,12 @@ fn extract_enum_type_content( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; - let federation_spec_definition = federation_spec_definitions - .get(graph_enum_value) - .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { - message: "Subgraph unexpectedly does not use federation spec".to_owned(), - })?; - if let Some(cost_spec_definition) = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema) - { - cost_spec_definition.propagate_demand_control_directives_for_enum( - &mut subgraph.schema, - type_, - &pos, - original_directive_names, - )?; - } + + CostSpecDefinition::propagate_demand_control_directives_for_enum( + supergraph_schema, + &mut subgraph.schema, + &pos, + )?; } for (value_name, value) in type_.values.iter() { @@ -1310,10 +1254,8 @@ fn extract_input_object_type_content( supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, graph_enum_value_name_to_subgraph_name: &IndexMap>, - federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, info: &[TypeInfo], - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { let field_directive_definition = join_spec_definition.field_directive_definition(supergraph_schema)?; @@ -1345,21 +1287,12 @@ fn extract_input_object_type_content( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; - let federation_spec_definition = federation_spec_definitions - .get(graph_enum_value) - .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { - message: "Subgraph unexpectedly does not use federation spec" - .to_owned(), - })?; - let cost_spec_definition = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema); add_subgraph_input_field( input_field_pos.clone(), input_field, + supergraph_schema, subgraph, None, - cost_spec_definition, - original_directive_names, )?; } } else { @@ -1375,14 +1308,6 @@ fn extract_input_object_type_content( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; - let federation_spec_definition = federation_spec_definitions - .get(graph_enum_value) - .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { - message: "Subgraph unexpectedly does not use federation spec" - .to_owned(), - })?; - let cost_spec_definition = - federation_spec_definition.get_cost_spec_definition(&subgraph.schema); if !subgraph_info.contains_key(graph_enum_value) { return Err( SingleFederationError::InvalidFederationSupergraph { @@ -1398,10 +1323,9 @@ fn extract_input_object_type_content( add_subgraph_input_field( input_field_pos.clone(), input_field, + supergraph_schema, subgraph, Some(field_directive_application), - cost_spec_definition, - original_directive_names, )?; } } @@ -1415,12 +1339,11 @@ fn extract_input_object_type_content( fn add_subgraph_field( object_or_interface_field_definition_position: ObjectOrInterfaceFieldDefinitionPosition, field: &FieldDefinition, + supergraph_schema: &FederationSchema, subgraph: &mut FederationSubgraph, federation_spec_definition: &'static FederationSpecDefinition, is_shareable: bool, field_directive_application: Option<&FieldDirectiveArguments>, - cost_spec_definition: Option<&'static CostSpecDefinition>, - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { let field_directive_application = field_directive_application.unwrap_or_else(|| &FieldDirectiveArguments { @@ -1456,14 +1379,13 @@ fn add_subgraph_field( default_value: argument.default_value.clone(), directives: Default::default(), }; - if let Some(cost_spec_definition) = cost_spec_definition { - cost_spec_definition.propagate_demand_control_directives( - &subgraph.schema, - &argument.directives, - &mut destination_argument.directives, - original_directive_names, - )?; - } + + CostSpecDefinition::propagate_demand_control_directives( + supergraph_schema, + &argument.directives, + &subgraph.schema, + &mut destination_argument.directives, + )?; subgraph_field .arguments @@ -1509,14 +1431,12 @@ fn add_subgraph_field( )); } - if let Some(cost_spec_definition) = cost_spec_definition { - cost_spec_definition.propagate_demand_control_directives( - &subgraph.schema, - &field.directives, - &mut subgraph_field.directives, - original_directive_names, - )?; - } + CostSpecDefinition::propagate_demand_control_directives( + supergraph_schema, + &field.directives, + &subgraph.schema, + &mut subgraph_field.directives, + )?; if let Some(context_arguments) = &field_directive_application.context_arguments { for args in context_arguments { @@ -1563,10 +1483,9 @@ fn add_subgraph_field( fn add_subgraph_input_field( input_object_field_definition_position: InputObjectFieldDefinitionPosition, input_field: &InputValueDefinition, + supergraph_schema: &FederationSchema, subgraph: &mut FederationSubgraph, field_directive_application: Option<&FieldDirectiveArguments>, - cost_spec_definition: Option<&'static CostSpecDefinition>, - original_directive_names: &IndexMap, ) -> Result<(), FederationError> { let field_directive_application = field_directive_application.unwrap_or_else(|| &FieldDirectiveArguments { @@ -1592,14 +1511,12 @@ fn add_subgraph_input_field( directives: Default::default(), }; - if let Some(cost_spec_definition) = cost_spec_definition { - cost_spec_definition.propagate_demand_control_directives( - &subgraph.schema, - &input_field.directives, - &mut subgraph_input_field.directives, - original_directive_names, - )?; - } + CostSpecDefinition::propagate_demand_control_directives( + supergraph_schema, + &input_field.directives, + &subgraph.schema, + &mut subgraph_input_field.directives, + )?; input_object_field_definition_position .insert(&mut subgraph.schema, Component::from(subgraph_input_field))?; diff --git a/apollo-federation/src/supergraph/schema.rs b/apollo-federation/src/supergraph/schema.rs index 46aa19618e..700a52e0e4 100644 --- a/apollo-federation/src/supergraph/schema.rs +++ b/apollo-federation/src/supergraph/schema.rs @@ -1,49 +1,8 @@ -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(); 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 cf819478e1..0b3c6738ae 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs @@ -1,114 +1,20 @@ 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 apollo_federation::link::cost_spec_definition::ListSizeDirective as ParsedListSizeDirective; use tower::BoxError; use crate::json_ext::Object; use crate::json_ext::ValueExt; use crate::plugins::demand_control::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.specified_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.specified_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, } @@ -134,86 +40,13 @@ pub(in crate::plugins::demand_control) struct ListSizeDirective<'schema> { } 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 - .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME) - .and_then(|arg| arg.to_i32()); - let slicing_argument_names = directive - .specified_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 - .specified_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 - .specified_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_and_variables( - &self, + pub(in crate::plugins::demand_control) fn new( + parsed: &'schema ParsedListSizeDirective, field: &Field, variables: &Object, - ) -> Result { + ) -> Result { let mut slicing_arguments: HashMap<&str, i32> = HashMap::new(); - if let Some(slicing_argument_names) = self.slicing_argument_names.as_ref() { + if let Some(slicing_argument_names) = parsed.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()) { @@ -240,7 +73,7 @@ impl DefinitionListSizeDirective { } } - if self.require_one_slicing_argument && slicing_arguments.len() != 1 { + if parsed.require_one_slicing_argument && slicing_arguments.len() != 1 { return Err(DemandControlError::QueryParseFailure(format!( "Exactly one slicing argument is required, but found {}", slicing_arguments.len() @@ -252,16 +85,28 @@ impl DefinitionListSizeDirective { .values() .max() .cloned() - .or(self.assumed_size); + .or(parsed.assumed_size); - Ok(ListSizeDirective { + Ok(Self { expected_size, - sized_fields: self + sized_fields: parsed .sized_fields .as_ref() .map(|set| set.iter().map(|s| s.as_str()).collect()), }) } + + 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 + } + } } pub(in crate::plugins::demand_control) struct RequiresDirective { 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 index d966512be1..02184164a9 100644 --- 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 @@ -1,10 +1,7 @@ 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"] - ) { + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) { query: Query } @@ -12,13 +9,6 @@ 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! diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs b/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs index 6a46ee9fe9..d59243f4d5 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/schema.rs @@ -3,20 +3,21 @@ use std::sync::Arc; use ahash::HashMap; use ahash::HashMapExt; +use apollo_compiler::ast::InputValueDefinition; use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Schema; +use apollo_federation::link::cost_spec_definition::CostDirective; +use apollo_federation::link::cost_spec_definition::CostSpecDefinition; +use apollo_federation::link::cost_spec_definition::ListSizeDirective; +use apollo_federation::schema::ValidFederationSchema; -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>, + inner: ValidFederationSchema, type_field_cost_directives: HashMap>, type_field_list_size_directives: HashMap>, type_field_requires_directives: HashMap>, @@ -24,8 +25,7 @@ pub(crate) struct DemandControlledSchema { impl DemandControlledSchema { pub(crate) fn new(schema: Arc>) -> Result { - let directive_name_map = get_apollo_directive_names(&schema); - + let fed_schema = ValidFederationSchema::new((*schema).clone())?; let mut type_field_cost_directives: HashMap> = HashMap::new(); let mut type_field_list_size_directives: HashMap> = @@ -55,17 +55,20 @@ impl DemandControlledSchema { )) })?; - if let Some(cost_directive) = - CostDirective::from_field(&directive_name_map, field_definition) - .or(CostDirective::from_type(&directive_name_map, field_type)) - { + if let Some(cost_directive) = CostSpecDefinition::cost_directive_from_field( + &fed_schema, + field_definition, + 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, - )? { + if let Some(list_size_directive) = + CostSpecDefinition::list_size_directive_from_field_definition( + &fed_schema, + field_definition, + )? + { field_list_size_directives .insert(field_name.clone(), list_size_directive); } @@ -90,17 +93,20 @@ impl DemandControlledSchema { )) })?; - if let Some(cost_directive) = - CostDirective::from_field(&directive_name_map, field_definition) - .or(CostDirective::from_type(&directive_name_map, field_type)) - { + if let Some(cost_directive) = CostSpecDefinition::cost_directive_from_field( + &fed_schema, + field_definition, + 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, - )? { + if let Some(list_size_directive) = + CostSpecDefinition::list_size_directive_from_field_definition( + &fed_schema, + field_definition, + )? + { field_list_size_directives .insert(field_name.clone(), list_size_directive); } @@ -122,18 +128,13 @@ impl DemandControlledSchema { } Ok(Self { - directive_name_map, - inner: schema, + inner: fed_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, @@ -163,11 +164,24 @@ impl DemandControlledSchema { .get(type_name)? .get(field_name) } + + pub(in crate::plugins::demand_control) fn argument_cost_directive( + &self, + definition: &InputValueDefinition, + ty: &ExtendedType, + ) -> Option { + // For now, we ignore FederationError and return None because this should not block the whole scoring + // process at runtime. Later, this should be pushed into the constructor and propagate any federation + // errors encountered when parsing. + CostSpecDefinition::cost_directive_from_argument(&self.inner, definition, ty) + .ok() + .flatten() + } } impl AsRef> for DemandControlledSchema { fn as_ref(&self) -> &Valid { - &self.inner + self.inner.schema() } } @@ -175,6 +189,6 @@ impl Deref for DemandControlledSchema { type Target = Schema; fn deref(&self) -> &Self::Target { - &self.inner + self.inner.schema() } } 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 397f33e9b6..ca7217ba7f 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 @@ -22,7 +22,6 @@ use super::DemandControlError; use crate::graphql::Response; use crate::graphql::ResponseVisitor; use crate::json_ext::Object; -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::DeferredNode; @@ -59,9 +58,7 @@ fn score_argument( argument_definition.ty.inner_named_type() )) })?; - let cost_directive = - CostDirective::from_argument(schema.directive_name_map(), argument_definition) - .or(CostDirective::from_type(schema.directive_name_map(), ty)); + let cost_directive = schema.argument_cost_directive(argument_definition, ty); match (argument, ty) { (_, ExtendedType::Interface(_)) @@ -124,9 +121,7 @@ fn score_variable( argument_definition.ty.inner_named_type() )) })?; - let cost_directive = - CostDirective::from_argument(schema.directive_name_map(), argument_definition) - .or(CostDirective::from_type(schema.directive_name_map(), ty)); + let cost_directive = schema.argument_cost_directive(argument_definition, ty); match (variable, ty) { (_, ExtendedType::Interface(_)) @@ -221,7 +216,7 @@ impl StaticCostCalculator { .schema .type_field_list_size_directive(parent_type, &field.name) { - Some(dir) => dir.with_field_and_variables(field, ctx.variables).map(Some), + Some(dir) => ListSizeDirective::new(dir, field, ctx.variables).map(Some), None => Ok(None), }?; let instance_count = if !field.ty().is_list() { diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index 5e3ba587f8..8e8d419ab8 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -11,6 +11,7 @@ use apollo_compiler::schema::FieldLookupError; use apollo_compiler::validation::Valid; use apollo_compiler::validation::WithErrors; use apollo_compiler::ExecutableDocument; +use apollo_federation::error::FederationError; use displaydoc::Display; use futures::future::Either; use futures::stream; @@ -123,6 +124,8 @@ pub(crate) enum DemandControlError { SubgraphOperationNotInitialized(crate::query_planner::fetch::SubgraphOperationNotInitialized), /// {0} ContextSerializationError(String), + /// {0} + FederationError(FederationError), } impl IntoGraphQLErrors for DemandControlError { @@ -163,6 +166,10 @@ impl IntoGraphQLErrors for DemandControlError { .extension_code(self.code()) .message(self.to_string()) .build()]), + DemandControlError::FederationError(_) => Ok(vec![graphql::Error::builder() + .extension_code(self.code()) + .message(self.to_string()) + .build()]), } } } @@ -175,6 +182,7 @@ impl DemandControlError { DemandControlError::QueryParseFailure(_) => "COST_QUERY_PARSE_FAILURE", DemandControlError::SubgraphOperationNotInitialized(e) => e.code(), DemandControlError::ContextSerializationError(_) => "COST_CONTEXT_SERIALIZATION_ERROR", + DemandControlError::FederationError(_) => "FEDERATION_ERROR", } } } @@ -201,6 +209,12 @@ impl<'a> From> for DemandControlError { } } +impl From for DemandControlError { + fn from(value: FederationError) -> Self { + DemandControlError::FederationError(value) + } +} + #[derive(Clone)] pub(crate) struct DemandControlContext { pub(crate) strategy: Strategy, From 89432b62b57686956d9fb86ee4d57d12434a0397 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Mon, 9 Dec 2024 12:53:25 -0800 Subject: [PATCH 094/112] Improve error messaging when QP encounters unmergeable fields (#6420) This PR updates the error messaging when query planner encounters unmergeable fields (indicating a known bug in query planner), and increments an internal metric when this error kind is encountered. Note that query planner does not check all such cases of this, as doing so is computationally expensive. The bug itself is not fixed in this PR, as it requires substantial effort to fix. --- apollo-federation/src/error/mod.rs | 4 ++++ apollo-federation/src/operation/merging.rs | 24 ++++++++++++++----- .../src/query_planner/bridge_query_planner.rs | 15 ++++++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index db775b92d5..0e7ae8e4c5 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -125,6 +125,9 @@ pub enum SingleFederationError { #[error("An internal error has occurred, please report this bug to Apollo. Details: {0}")] #[allow(private_interfaces)] // users should not inspect this. InternalRebaseError(#[from] crate::operation::RebaseError), + // This is a known bug that will take time to fix, and does not require reporting. + #[error("{message}")] + InternalUnmergeableFields { message: String }, #[error("{diagnostics}")] InvalidGraphQL { diagnostics: DiagnosticList }, #[error(transparent)] @@ -301,6 +304,7 @@ impl SingleFederationError { match self { SingleFederationError::Internal { .. } => ErrorCode::Internal, SingleFederationError::InternalRebaseError { .. } => ErrorCode::Internal, + SingleFederationError::InternalUnmergeableFields { .. } => ErrorCode::Internal, SingleFederationError::InvalidGraphQL { .. } | SingleFederationError::InvalidGraphQLName(_) => ErrorCode::InvalidGraphQL, SingleFederationError::InvalidSubgraph { .. } => ErrorCode::InvalidGraphQL, diff --git a/apollo-federation/src/operation/merging.rs b/apollo-federation/src/operation/merging.rs index 6b8e89193c..79ebec9fe3 100644 --- a/apollo-federation/src/operation/merging.rs +++ b/apollo-federation/src/operation/merging.rs @@ -18,6 +18,7 @@ use super::SelectionValue; use crate::bail; use crate::ensure; use crate::error::FederationError; +use crate::error::SingleFederationError; impl<'a> FieldSelectionValue<'a> { /// Merges the given field selections into this one. @@ -42,12 +43,23 @@ impl<'a> FieldSelectionValue<'a> { other_field.schema == self_field.schema, "Cannot merge field selections from different schemas", ); - ensure!( - other_field.field_position == self_field.field_position, - "Cannot merge field selection for field \"{}\" into a field selection for field \"{}\"", - other_field.field_position, - self_field.field_position, - ); + if other_field.field_position != self_field.field_position { + return Err(SingleFederationError::InternalUnmergeableFields { + message: format!( + "Cannot merge field selection for field \"{}\" into a field selection for \ + field \"{}\". This is a known query planning bug in the old Javascript \ + query planner that was silently ignored. The Rust-native query planner \ + does not address this bug at this time, but in some cases does catch when \ + this bug occurs. If you're seeing this message, this bug was likely \ + triggered by one of the field selections mentioned previously having an \ + alias that was the same name as the field in the other field selection. \ + The recommended workaround is to change this alias to a different one in \ + your operation.", + other_field.field_position, self_field.field_position, + ), + } + .into()); + } if self.get().selection_set.is_some() { let Some(other_selection_set) = &other.selection_set else { bail!( diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 09f50945ca..4e8143d3fb 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -269,8 +269,19 @@ impl PlannerMode { operation, query_plan_options, ) - }) - .map_err(|e| QueryPlannerError::FederationError(e.to_string())); + }); + if let Err(FederationError::SingleFederationError( + SingleFederationError::InternalUnmergeableFields { .. }, + )) = &result + { + u64_counter!( + "apollo.router.operations.query_planner.unmergeable_fields", + "Query planner caught attempting to merge unmergeable fields", + 1 + ); + } + let result = + result.map_err(|e| QueryPlannerError::FederationError(e.to_string())); let elapsed = start.elapsed().as_secs_f64(); metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); From 5d8751360c25971edfad7c125ca796586d173496 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Tue, 10 Dec 2024 10:38:02 +0100 Subject: [PATCH 095/112] chore: address feedback --- apollo-router/src/plugins/fleet_detector.rs | 36 +++++++++------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 2b7b36414a..2150ad903e 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -216,15 +216,14 @@ impl PluginPrivate for FleetDetector { // Count the number of request bytes from clients to the router .map_request(move |req: router::Request| router::Request { router_request: req.router_request.map(move |body| { - router::Body::wrap_stream(body.map(move |res| { - res.map(move |bytes| { + router::Body::wrap_stream(body.inspect(|res| { + if let Ok(bytes) = res { u64_counter!( "apollo.router.operations.request_size", "Total number of request bytes from clients", bytes.len() as u64 ); - bytes - }) + } })) }), context: req.context, @@ -232,15 +231,14 @@ impl PluginPrivate for FleetDetector { // Count the number of response bytes from the router to clients .map_response(move |res: router::Response| router::Response { response: res.response.map(move |body| { - router::Body::wrap_stream(body.map(move |res| { - res.map(move |bytes| { + router::Body::wrap_stream(body.inspect(|res| { + if let Ok(bytes) = res { u64_counter!( "apollo.router.operations.response_size", "Total number of response bytes to clients", bytes.len() as u64 ); - bytes - }) + } })) }), context: res.context, @@ -265,17 +263,16 @@ impl PluginPrivate for FleetDetector { HttpRequest { http_request: req.http_request.map(move |body| { let sn = sn.clone(); - RouterBody::wrap_stream(body.map(move |res| { - let sn = sn.clone(); - res.map(move |bytes| { + RouterBody::wrap_stream(body.inspect(move |res| { + if let Ok(bytes) = res { + let sn = sn.clone(); u64_counter!( "apollo.router.operations.fetch.request_size", "Total number of request bytes for subgraph fetches", bytes.len() as u64, subgraph.service.name = sn.to_string() ); - bytes - }) + } })) }), context: req.context, @@ -291,36 +288,35 @@ impl PluginPrivate for FleetDetector { "Number of subgraph fetches", 1u64, subgraph.service.name = sn.to_string(), + client_error = false, http.response.status_code = res.http_response.status().as_u16() as i64 ); let sn = sn_res.clone(); Ok(HttpResponse { http_response: res.http_response.map(move |body| { let sn = sn.clone(); - RouterBody::wrap_stream(body.map(move |res| { - let sn = sn.clone(); - res.map(move |bytes| { + RouterBody::wrap_stream(body.inspect(move |res| { + if let Ok(bytes) = res { + let sn = sn.clone(); u64_counter!( "apollo.router.operations.fetch.response_size", "Total number of response bytes for subgraph fetches", bytes.len() as u64, subgraph.service.name = sn.to_string() ); - bytes - }) + } })) }), context: res.context, }) } Err(err) => { - // On fetch errors, report the status code as 0 u64_counter!( "apollo.router.operations.fetch", "Number of subgraph fetches", 1u64, subgraph.service.name = sn.to_string(), - http.response.status_code = 0i64 + client_error = true ); Err(err) } From 5f72471d05deb6379610d1f661b2d84b4c65e10a Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Tue, 10 Dec 2024 14:53:14 +0100 Subject: [PATCH 096/112] fix cargo about (#6423) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> Co-authored-by: Gary Pennington --- .circleci/config.yml | 5 +- about.toml | 1 + licenses.html | 959 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 926 insertions(+), 39 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1977ae676c..051f824bd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -376,9 +376,12 @@ commands: - run: name: Install cargo deny, about, edit command: | + # Until we are able to update rustc to at least 1.81.0, + # we need special handling of the cargo-about command. + rustup install 1.83.0 + cargo +1.83.0 install --locked --version 0.6.6 cargo-about if [[ ! -f "$HOME/.cargo/bin/cargo-deny$EXECUTABLE_SUFFIX" ]]; then cargo install --locked --version 0.14.21 cargo-deny - cargo install --locked --version 0.6.1 cargo-about cargo install --locked --version 0.12.2 cargo-edit cargo install --locked --version 0.12.0 cargo-fuzz fi diff --git a/about.toml b/about.toml index 094647afae..23c6c3ef58 100644 --- a/about.toml +++ b/about.toml @@ -9,6 +9,7 @@ accepted = [ "LicenseRef-ring", "MIT", "MPL-2.0", + "Unicode-3.0", "Unicode-DFS-2016", "Zlib" ] diff --git a/licenses.html b/licenses.html index ee38302396..67a2901d9a 100644 --- a/licenses.html +++ b/licenses.html @@ -44,11 +44,12 @@