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
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 committed Aug 16, 2022
1 parent 89b7a96 commit 6fbc9c2
Show file tree
Hide file tree
Showing 17 changed files with 49 additions and 917 deletions.
7 changes: 7 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ By [@SimonSapin](https://github.com/SimonSapin)

## 🚀 Features

### Don't prefill the introspection cache ([#1516](https://github.com/apollographql/router/issues/1516))

The introspection query cache won't be warmed up anymore. Which reduces the router's idle memory footprint.

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
2 changes: 1 addition & 1 deletion apollo-router/src/cache/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use tokio::sync::Mutex;
//
// this will be replaced by the multi level (in memory + redis/memcached) once we find
// a suitable implementation.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub(crate) struct CacheStorage<K: Hash + Eq + Send, V: Clone> {
inner: Arc<Mutex<LruCache<K, V>>>,
}
Expand Down
147 changes: 34 additions & 113 deletions apollo-router/src/introspection.rs
Original file line number Diff line number Diff line change
@@ -1,106 +1,49 @@
#[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 +67,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 +81,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()
)
}
}
}
12 changes: 6 additions & 6 deletions apollo-router/src/query_planner/bridge_query_planner.rs
Original file line number Diff line number Diff line change
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 6fbc9c2

Please sign in to comment.