diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 58f2632cf6..d40cfe464c 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -292,6 +292,19 @@ By [@SimonSapin](https://github.com/SimonSapin) ## 🚀 Features +### Reduce initial memory footprint by lazily populating introspection query cache ([#1516](https://github.com/apollographql/router/issues/1516)) + +In an early alpha release of the Router, we only executed certain "known" introspection queries because of prior technical constraints that prohibited us from doing something more flexible. Because the set of introspection queries was "known", it made sense to cache them. + +As of https://github.com/apollographql/router/pull/802, this special-casing is (thankfully) no longer necessary and we no longer need to _know_ (and constrain!) the introspection queries that the Router supports. + +We could have kept caching those "known" queries, however we were finding that the resulting cache size was quite large and making the Router's minimum memory footprint larger than need be since we were caching many introspection results which the Router instance would never encounter. + +This change removes the cache entirely and allows introspection queries served by the Router to merely be lazily calculated and cached on-demand, thereby reducing the initial memory footprint. Disabling introspection entirely will prevent any use of this cache since no introspection will be possible. + +By [@o0Ignition0o](https://github.com/o0Ignition0o) + + ### Expose query plan in extensions for GraphQL response (experimental) ([PR #1470](https://github.com/apollographql/router/pull/1470)) Expose query plan in extensions for GraphQL response. Only experimental for now, no documentation available. diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index ff93cef21d..515a0307e9 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -1,106 +1,48 @@ +#[cfg(test)] use std::collections::HashMap; -use include_dir::include_dir; -use once_cell::sync::Lazy; use router_bridge::introspect; use router_bridge::introspect::IntrospectionError; +use crate::cache::storage::CacheStorage; use crate::graphql::Response; -use crate::*; - -/// KNOWN_INTROSPECTION_QUERIES we will serve through Introspection. -/// -/// If you would like to add one, put it in the "well_known_introspection_queries" folder. -static KNOWN_INTROSPECTION_QUERIES: Lazy> = Lazy::new(|| { - include_dir!("$CARGO_MANIFEST_DIR/well_known_introspection_queries") - .files() - .map(|file| { - file.contents_utf8() - .unwrap_or_else(|| { - panic!( - "contents of the file at path {} isn't valid utf8", - file.path().display() - ); - }) - .to_string() - }) - .collect() -}); + +const DEFAULT_INTROSPECTION_CACHE_CAPACITY: usize = 5; /// A cache containing our well known introspection queries. -#[derive(Debug)] pub(crate) struct Introspection { - cache: HashMap, + cache: CacheStorage, } impl Introspection { - #[cfg(test)] - pub(crate) fn from_cache(cache: HashMap) -> Self { - Self { cache } + pub(crate) async fn with_capacity(capacity: usize) -> Self { + Self { + cache: CacheStorage::new(capacity).await, + } } - /// Create a `Introspection` from a `Schema`. - /// - /// This function will populate a cache in a blocking manner. - /// This is why `Introspection` instanciation happens in a spawn_blocking task on the state_machine side. - pub(crate) fn from_schema(schema: &Schema) -> Self { - let span = tracing::trace_span!("introspection_population"); - let _guard = span.enter(); - - let cache = introspect::batch_introspect( - schema.as_string(), - KNOWN_INTROSPECTION_QUERIES.iter().cloned().collect(), - ) - .map_err(|deno_runtime_error| { - tracing::warn!( - "router-bridge returned a deno runtime error:\n{}", - deno_runtime_error - ); - }) - .and_then(|global_introspection_result| { - global_introspection_result - .map_err(|general_introspection_error| { - tracing::warn!( - "Introspection returned an error:\n{}", - general_introspection_error - ); - }) - .map(|responses| { - KNOWN_INTROSPECTION_QUERIES - .iter() - .zip(responses) - .filter_map(|(query, response)| match response.into_result() { - Ok(value) => { - let response = Response::builder().data(value).build(); - Some((query.into(), response)) - } - Err(graphql_errors) => { - for error in graphql_errors { - tracing::warn!( - "Introspection returned error:\n{}\n{}", - error, - query - ); - } - None - } - }) - .collect() - }) - }) - .unwrap_or_default(); - - Self { cache } + pub(crate) async fn new() -> Self { + Self::with_capacity(DEFAULT_INTROSPECTION_CACHE_CAPACITY).await + } + + #[cfg(test)] + pub(crate) async fn from_cache(cache: HashMap) -> Self { + let this = Self::with_capacity(cache.len()).await; + + for (query, response) in cache.into_iter() { + this.cache.insert(query, response).await; + } + this } /// Execute an introspection and cache the response. pub(crate) async fn execute( &self, schema_sdl: &str, - query: &str, + query: String, ) -> Result { - if let Some(response) = self.cache.get(query) { - return Ok(response.clone()); + if let Some(response) = self.cache.get(&query).await { + return Ok(response); } // Do the introspection query and cache it @@ -124,8 +66,11 @@ impl Introspection { ) .into(), })?; + let response = Response::builder().data(introspection_result).build(); + self.cache.insert(query, response.clone()).await; + Ok(response) } } @@ -135,48 +80,23 @@ mod introspection_tests { use super::*; #[tokio::test] - async fn test_plan() { + async fn test_plan_cache() { let query_to_test = "this is a test query"; let schema = " "; let expected_data = Response::builder().data(42).build(); - let cache = [(query_to_test.into(), expected_data.clone())] + let cache = [(query_to_test.to_string(), expected_data.clone())] .iter() .cloned() .collect(); - let introspection = Introspection::from_cache(cache); + let introspection = Introspection::from_cache(cache).await; assert_eq!( expected_data, - introspection.execute(schema, query_to_test).await.unwrap() - ); - } - - #[test] - fn test_known_introspection_queries() { - // This makes sure KNOWN_INTROSPECTION_QUERIES get created correctly - // and those queries don’t cause errors, - // thus preventing regressions if a wrong query is added - // to the `well_known_introspection_queries` folder - let config = Default::default(); - let schema = include_str!("query_planner/testdata/schema.graphql"); - let schema = Schema::parse(schema, &config).unwrap(); - assert_eq!( - Introspection::from_schema(&schema).cache.len(), - KNOWN_INTROSPECTION_QUERIES.len() + introspection + .execute(schema, query_to_test.to_string()) + .await + .unwrap() ); - - for (file, query) in include_dir!("$CARGO_MANIFEST_DIR/well_known_introspection_queries") - .files() - .zip(&*KNOWN_INTROSPECTION_QUERIES) - { - let result = Query::parse(query, &schema, &config); - assert!( - result.is_ok(), - "{}: {}", - file.path().display(), - result.unwrap_err() - ) - } } } diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 7fd9f1693b..af997a1fb5 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -24,7 +24,7 @@ use crate::*; pub(crate) static USAGE_REPORTING: &str = "apollo_telemetry::usage_reporting"; -#[derive(Debug, Clone)] +#[derive(Clone)] /// A query planner that calls out to the nodejs router-bridge query planner. /// /// No caching is performed. To cache, wrap in a [`CachingQueryPlanner`]. @@ -74,7 +74,7 @@ impl BridgeQueryPlanner { } } - async fn introspection(&self, query: &str) -> Result { + async fn introspection(&self, query: String) -> Result { match self.introspection.as_ref() { Some(introspection) => { let response = introspection @@ -175,7 +175,7 @@ impl BridgeQueryPlanner { let selections = self.parse_selections(key.0.clone()).await?; if selections.contains_introspection() { - return self.introspection(key.0.as_str()).await; + return self.introspection(key.0).await; } self.plan(key.0, key.1, key.2, selections).await @@ -201,7 +201,7 @@ mod tests { async fn test_plan() { let planner = BridgeQueryPlanner::new( Arc::new(example_schema()), - Some(Arc::new(Introspection::from_schema(&example_schema()))), + Some(Arc::new(Introspection::new().await)), Default::default(), ) .await @@ -228,7 +228,7 @@ mod tests { async fn test_plan_invalid_query() { let planner = BridgeQueryPlanner::new( Arc::new(example_schema()), - Some(Arc::new(Introspection::from_schema(&example_schema()))), + Some(Arc::new(Introspection::new().await)), Default::default(), ) .await @@ -272,7 +272,7 @@ mod tests { async fn empty_query_plan_should_be_a_planner_error() { let err = BridgeQueryPlanner::new( Arc::new(example_schema()), - Some(Arc::new(Introspection::from_schema(&example_schema()))), + Some(Arc::new(Introspection::new().await)), Default::default(), ) .await @@ -306,7 +306,7 @@ mod tests { async fn test_plan_error() { let planner = BridgeQueryPlanner::new( Arc::new(example_schema()), - Some(Arc::new(Introspection::from_schema(&example_schema()))), + Some(Arc::new(Introspection::new().await)), Default::default(), ) .await diff --git a/apollo-router/src/services/router_service.rs b/apollo-router/src/services/router_service.rs index 423c0aa520..056cebbb9c 100644 --- a/apollo-router/src/services/router_service.rs +++ b/apollo-router/src/services/router_service.rs @@ -307,15 +307,7 @@ impl PluggableRouterServiceBuilder { .unwrap_or(100); let introspection = if configuration.server.introspection { - // Introspection instantiation can potentially block for some time - // We don't need to use the api schema here because on the deno side we always convert to API schema - - let schema = self.schema.clone(); - Some(Arc::new( - tokio::task::spawn_blocking(move || Introspection::from_schema(&schema)) - .await - .expect("Introspection instantiation panicked"), - )) + Some(Arc::new(Introspection::new().await)) } else { None }; diff --git a/apollo-router/well_known_introspection_queries/altair.graphql b/apollo-router/well_known_introspection_queries/altair.graphql deleted file mode 100644 index f1d71706f5..0000000000 --- a/apollo-router/well_known_introspection_queries/altair.graphql +++ /dev/null @@ -1,98 +0,0 @@ - - query IntrospectionQuery { - __schema { - - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - - locations - args { - ...InputValue - } - } - } - } - - fragment FullType on __Type { - kind - name - description - - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - - fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue - - - } - - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } - } - \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/apollo-studio.graphql b/apollo-router/well_known_introspection_queries/apollo-studio.graphql deleted file mode 100644 index 7b18932f4b..0000000000 --- a/apollo-router/well_known_introspection_queries/apollo-studio.graphql +++ /dev/null @@ -1,96 +0,0 @@ - - query IntrospectionQuery { - __schema { - - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - - locations - args { - ...InputValue - } - } - } - } - - fragment FullType on __Type { - kind - name - description - - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - - fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue - } - - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } - } - \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/bananacakepop-phase1.graphql b/apollo-router/well_known_introspection_queries/bananacakepop-phase1.graphql deleted file mode 100644 index 0d10d06822..0000000000 --- a/apollo-router/well_known_introspection_queries/bananacakepop-phase1.graphql +++ /dev/null @@ -1,14 +0,0 @@ -query introspection_phase_1 { - schema: __type(name: "__Schema") { - name - fields { - name - } - } - directive: __type(name: "__Directive") { - name - fields { - name - } - } -} diff --git a/apollo-router/well_known_introspection_queries/bananacakepop-phase2.graphql b/apollo-router/well_known_introspection_queries/bananacakepop-phase2.graphql deleted file mode 100644 index b896bb2708..0000000000 --- a/apollo-router/well_known_introspection_queries/bananacakepop-phase2.graphql +++ /dev/null @@ -1,92 +0,0 @@ -query introspection_phase_2 { - __schema { - queryType { - name - } - mutationType { - name - } - subscriptionType { - name - } - types { - ...FullType - } - directives { - name - description - args { - ...InputValue - } - isRepeatable - locations - } - } -} - -fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } -} - -fragment InputValue on __InputValue { - name - description - type { - ...TypeRef - } - defaultValue -} - -fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } -} diff --git a/apollo-router/well_known_introspection_queries/graphql-playground.graphql b/apollo-router/well_known_introspection_queries/graphql-playground.graphql deleted file mode 100644 index d17da75f35..0000000000 --- a/apollo-router/well_known_introspection_queries/graphql-playground.graphql +++ /dev/null @@ -1,99 +0,0 @@ -query IntrospectionQuery { - __schema { - queryType { - name - } - mutationType { - name - } - subscriptionType { - name - } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } -} - -fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } -} - -fragment InputValue on __InputValue { - name - description - type { - ...TypeRef - } - defaultValue -} - -fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } -} diff --git a/apollo-router/well_known_introspection_queries/graphql_js_introspection_query.graphql b/apollo-router/well_known_introspection_queries/graphql_js_introspection_query.graphql deleted file mode 100644 index 949a298c3e..0000000000 --- a/apollo-router/well_known_introspection_queries/graphql_js_introspection_query.graphql +++ /dev/null @@ -1,98 +0,0 @@ - - query IntrospectionQuery { - __schema { - - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - - locations - args(includeDeprecated: true) { - ...InputValue - } - } - } - } - - fragment FullType on __Type { - kind - name - description - - fields(includeDeprecated: true) { - name - description - args(includeDeprecated: true) { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields(includeDeprecated: true) { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - - fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue - isDeprecated - deprecationReason - } - - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } - } - \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/graphql_js_introspection_query_with_description.graphql b/apollo-router/well_known_introspection_queries/graphql_js_introspection_query_with_description.graphql deleted file mode 100644 index f529df31e9..0000000000 --- a/apollo-router/well_known_introspection_queries/graphql_js_introspection_query_with_description.graphql +++ /dev/null @@ -1,89 +0,0 @@ - -query IntrospectionQuery { - __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } -} -fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } -} -fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue -} -fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/homepage.graphql b/apollo-router/well_known_introspection_queries/homepage.graphql deleted file mode 100644 index b4685e218b..0000000000 --- a/apollo-router/well_known_introspection_queries/homepage.graphql +++ /dev/null @@ -1 +0,0 @@ -query { __typename } \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/insomnia.graphql b/apollo-router/well_known_introspection_queries/insomnia.graphql deleted file mode 100644 index 83fdb54e14..0000000000 --- a/apollo-router/well_known_introspection_queries/insomnia.graphql +++ /dev/null @@ -1,93 +0,0 @@ - - query IntrospectionQuery { - __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } - } - - fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - - fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue - } - - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } - } - \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/npx_diagnose_endpoint1.graphql b/apollo-router/well_known_introspection_queries/npx_diagnose_endpoint1.graphql deleted file mode 100644 index f1606eef00..0000000000 --- a/apollo-router/well_known_introspection_queries/npx_diagnose_endpoint1.graphql +++ /dev/null @@ -1 +0,0 @@ -query Ping { __typename } \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/npx_diagnose_endpoint2.graphql b/apollo-router/well_known_introspection_queries/npx_diagnose_endpoint2.graphql deleted file mode 100644 index 34532200a6..0000000000 --- a/apollo-router/well_known_introspection_queries/npx_diagnose_endpoint2.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query MiniIntrospectionQuery { - __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - } - } - \ No newline at end of file diff --git a/apollo-router/well_known_introspection_queries/rover_graph_introspect.graphql b/apollo-router/well_known_introspection_queries/rover_graph_introspect.graphql deleted file mode 100644 index c1c64031a6..0000000000 --- a/apollo-router/well_known_introspection_queries/rover_graph_introspect.graphql +++ /dev/null @@ -1,99 +0,0 @@ -query GraphIntrospectQuery { - __schema { - queryType { - name - } - mutationType { - name - } - subscriptionType { - name - } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } -} - -fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } -} - -fragment InputValue on __InputValue { - name - description - type { - ...TypeRef - } - defaultValue -} - -fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } -} \ No newline at end of file