Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: introspection: don't prefill the introspection cache anymore. #1517

Merged
merged 3 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)


o0Ignition0o marked this conversation as resolved.
Show resolved Hide resolved
### 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