Skip to content

Commit

Permalink
feature: introspection: don't prefill the introspection cache anymore. (
Browse files Browse the repository at this point in the history
#1517)



fixes: #1516, #1466

Since @bnjjj s refactoring on introspection, we don't need to cache well known introspection queries.

Pros: Caching those queries and responses can lead to high memory footprints, especially on very large schemas.
Cons: The first introspection query will take a bit longer (~130ms on my machine) than the cached one (~2ms on my machine)

The default introspection cache size is now 5, and cannot be configured. 5 has been chosen because we do not expect local developers to run tools that need more than 5 different introspection queries with < 3ms latency requirements.
  • Loading branch information
o0Ignition0o authored Aug 17, 2022
1 parent b706f2a commit a366da9
Show file tree
Hide file tree
Showing 16 changed files with 55 additions and 918 deletions.
13 changes: 13 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
148 changes: 34 additions & 114 deletions apollo-router/src/introspection.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<String>> = 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<String, Response>,
cache: CacheStorage<String, Response>,
}

impl Introspection {
#[cfg(test)]
pub(crate) fn from_cache(cache: HashMap<String, Response>) -> 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<String, Response>) -> 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<Response, IntrospectionError> {
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
Expand All @@ -124,8 +66,11 @@ impl Introspection {
)
.into(),
})?;

let response = Response::builder().data(introspection_result).build();

self.cache.insert(query, response.clone()).await;

Ok(response)
}
}
Expand All @@ -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()
)
}
}
}
14 changes: 7 additions & 7 deletions apollo-router/src/query_planner/bridge_query_planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down Expand Up @@ -74,7 +74,7 @@ impl BridgeQueryPlanner {
}
}

async fn introspection(&self, query: &str) -> Result<QueryPlannerContent, QueryPlannerError> {
async fn introspection(&self, query: String) -> Result<QueryPlannerContent, QueryPlannerError> {
match self.introspection.as_ref() {
Some(introspection) => {
let response = introspection
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 1 addition & 9 deletions apollo-router/src/services/router_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
98 changes: 0 additions & 98 deletions apollo-router/well_known_introspection_queries/altair.graphql

This file was deleted.

Loading

0 comments on commit a366da9

Please sign in to comment.