diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 1508b75baa..2c01352247 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -188,7 +188,7 @@ There are situations where comments and whitespace are not preserved. This may b By [@bryncooke](https://github.com/bryncooke) in https://github.com/apollographql/router/pull/2116, https://github.com/apollographql/router/pull/2162 -### *Experimental* subgraph request retry ([Issue #338](https://github.com/apollographql/router/issues/338), [Issue #1956](https://github.com/apollographql/router/issues/1956)) +### *Experimental* 🥼 subgraph request retry ([Issue #338](https://github.com/apollographql/router/issues/338), [Issue #1956](https://github.com/apollographql/router/issues/1956)) Implements subgraph request retries, using Finagle's retry buckets algorithm: - it defines a minimal number of retries per second (`min_per_sec`, default is 10 retries per second), to @@ -213,6 +213,28 @@ traffic_shaping: By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2006 +### *Experimental* 🥼 Caching configuration ([Issue #2075](https://github.com/apollographql/router/issues/2075)) + +Split Redis cache configuration for APQ and query planning: + +```yaml +supergraph: + apq: + experimental_cache: + in_memory: + limit: 512 + redis: + urls: ["redis://..."] + query_planning: + experimental_cache: + in_memory: + limit: 512 + redis: + urls: ["redis://..."] +``` + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2155 + ## 🐛 Fixes ### Improve errors when subgraph returns non-GraphQL response with a non-2xx status code ([Issue #2117](https://github.com/apollographql/router/issues/2117)) diff --git a/apollo-router/src/axum_factory/axum_http_server_factory.rs b/apollo-router/src/axum_factory/axum_http_server_factory.rs index ad640703df..5fcf3a3758 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -163,7 +163,12 @@ impl HttpServerFactory for AxumHttpServerFactory { RF: SupergraphServiceFactory, { Box::pin(async move { - let apq = APQLayer::with_cache(DeduplicatingCache::new().await); + let apq = APQLayer::with_cache( + DeduplicatingCache::from_configuration( + &configuration.supergraph.apq.experimental_cache, + ) + .await, + ); let all_routers = make_axum_router(service_factory, &configuration, extra_endpoints, apq)?; diff --git a/apollo-router/src/cache/mod.rs b/apollo-router/src/cache/mod.rs index abca52b917..e98a664ed6 100644 --- a/apollo-router/src/cache/mod.rs +++ b/apollo-router/src/cache/mod.rs @@ -37,6 +37,17 @@ where } } + pub(crate) async fn from_configuration(config: &crate::configuration::Cache) -> Self { + Self::with_capacity( + config.in_memory.limit, + #[cfg(feature = "experimental_cache")] + config.redis.as_ref().map(|c| c.urls.clone()), + #[cfg(not(feature = "experimental_cache"))] + None, + ) + .await + } + pub(crate) async fn get(&self, key: &K) -> Entry { // waiting on a value from the cache is a potentially long(millisecond scale) task that // can involve a network call to an external database. To reduce the waiting time, we diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 94d00331ca..30ed5dab67 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -34,6 +34,7 @@ use serde_json::Map; use serde_json::Value; use thiserror::Error; +use crate::cache::DEFAULT_CACHE_CAPACITY; use crate::configuration::schema::Mode; use crate::executable::APOLLO_ROUTER_DEV_ENV; use crate::plugin::plugins; @@ -485,16 +486,19 @@ pub(crate) struct Supergraph { #[serde(default = "default_defer_support")] pub(crate) preview_defer_support: bool, - #[cfg(feature = "experimental_cache")] - /// URLs of Redis cache used for query planning - pub(crate) cache_redis_urls: Option>, + /// Configures automatic persisted queries + #[serde(default)] + pub(crate) apq: Apq, + + /// Query planning options + #[serde(default)] + pub(crate) query_planning: QueryPlanning, } fn default_defer_support() -> bool { true } -#[cfg(feature = "experimental_cache")] #[buildstructor::buildstructor] impl Supergraph { #[builder] @@ -503,19 +507,20 @@ impl Supergraph { path: Option, introspection: Option, preview_defer_support: Option, - cache_redis_urls: Option>, + apq: Option, + query_planning: Option, ) -> Self { Self { listen: listen.unwrap_or_else(default_graphql_listen), path: path.unwrap_or_else(default_graphql_path), introspection: introspection.unwrap_or_else(default_graphql_introspection), preview_defer_support: preview_defer_support.unwrap_or_else(default_defer_support), - cache_redis_urls, + apq: apq.unwrap_or_default(), + query_planning: query_planning.unwrap_or_default(), } } } -#[cfg(feature = "experimental_cache")] #[cfg(test)] #[buildstructor::buildstructor] impl Supergraph { @@ -525,75 +530,74 @@ impl Supergraph { path: Option, introspection: Option, preview_defer_support: Option, - cache_redis_urls: Option>, + apq: Option, + query_planning: Option, ) -> Self { Self { listen: listen.unwrap_or_else(test_listen), path: path.unwrap_or_else(default_graphql_path), introspection: introspection.unwrap_or_else(default_graphql_introspection), preview_defer_support: preview_defer_support.unwrap_or_else(default_defer_support), - cache_redis_urls, + apq: apq.unwrap_or_default(), + query_planning: query_planning.unwrap_or_default(), } } } -#[cfg(not(feature = "experimental_cache"))] -#[buildstructor::buildstructor] -impl Supergraph { - #[builder] - pub(crate) fn new( - listen: Option, - path: Option, - introspection: Option, - preview_defer_support: Option, - ) -> Self { - Self { - listen: listen.unwrap_or_else(default_graphql_listen), - path: path.unwrap_or_else(default_graphql_path), - introspection: introspection.unwrap_or_else(default_graphql_introspection), - preview_defer_support: preview_defer_support.unwrap_or_else(default_defer_support), - } +impl Default for Supergraph { + fn default() -> Self { + Self::builder().build() } } -#[cfg(not(feature = "experimental_cache"))] -#[cfg(test)] -#[buildstructor::buildstructor] -impl Supergraph { - #[builder] - pub(crate) fn fake_new( - listen: Option, - path: Option, - introspection: Option, - preview_defer_support: Option, - ) -> Self { - Self { - listen: listen.unwrap_or_else(test_listen), - path: path.unwrap_or_else(default_graphql_path), - introspection: introspection.unwrap_or_else(default_graphql_introspection), - preview_defer_support: preview_defer_support.unwrap_or_else(default_defer_support), - } - } +#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct Apq { + pub(crate) experimental_cache: Cache, } -impl Supergraph { +#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct QueryPlanning { + pub(crate) experimental_cache: Cache, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] + +pub(crate) struct Cache { + /// Configures the in memory cache (always active) + pub(crate) in_memory: InMemoryCache, #[cfg(feature = "experimental_cache")] - pub(crate) fn cache(&self) -> Option> { - self.cache_redis_urls.clone() - } + /// Configures and activates the Redis cache + pub(crate) redis: Option, +} - #[cfg(not(feature = "experimental_cache"))] - pub(crate) fn cache(&self) -> Option> { - None - } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +/// In memory cache configuration +pub(crate) struct InMemoryCache { + /// Number of entries in the Least Recently Used cache + pub(crate) limit: usize, } -impl Default for Supergraph { +impl Default for InMemoryCache { fn default() -> Self { - Self::builder().build() + Self { + limit: DEFAULT_CACHE_CAPACITY, + } } } +#[cfg(feature = "experimental_cache")] +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +/// Redis cache configuration +pub(crate) struct RedisCache { + /// List of URLs to the Redis cluster + pub(crate) urls: Vec, +} + /// Configuration options pertaining to the sandbox page. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] 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 2c8fd06bca..30557317e9 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 @@ -605,10 +605,66 @@ expression: "&schema" "listen": "127.0.0.1:4000", "path": "/", "introspection": false, - "preview_defer_support": true + "preview_defer_support": true, + "apq": { + "experimental_cache": { + "in_memory": { + "limit": 512 + } + } + }, + "query_planning": { + "experimental_cache": { + "in_memory": { + "limit": 512 + } + } + } }, "type": "object", "properties": { + "apq": { + "description": "Configures automatic persisted queries", + "default": { + "experimental_cache": { + "in_memory": { + "limit": 512 + } + } + }, + "type": "object", + "required": [ + "experimental_cache" + ], + "properties": { + "experimental_cache": { + "type": "object", + "required": [ + "in_memory" + ], + "properties": { + "in_memory": { + "description": "Configures the in memory cache (always active)", + "type": "object", + "required": [ + "limit" + ], + "properties": { + "limit": { + "description": "Number of entries in the Least Recently Used cache", + "type": "integer", + "format": "uint", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "introspection": { "description": "Enable introspection Default: false", "default": false, @@ -636,6 +692,48 @@ expression: "&schema" "preview_defer_support": { "default": true, "type": "boolean" + }, + "query_planning": { + "description": "Query planning options", + "default": { + "experimental_cache": { + "in_memory": { + "limit": 512 + } + } + }, + "type": "object", + "required": [ + "experimental_cache" + ], + "properties": { + "experimental_cache": { + "type": "object", + "required": [ + "in_memory" + ], + "properties": { + "in_memory": { + "description": "Configures the in memory cache (always active)", + "type": "object", + "required": [ + "limit" + ], + "properties": { + "limit": { + "description": "Number of entries in the Least Recently Used cache", + "type": "integer", + "format": "uint", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index 3075e4c3de..1dbbcc3770 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -19,24 +19,15 @@ pub(crate) struct Introspection { } impl Introspection { - pub(crate) async fn with_capacity( - configuration: &Configuration, - capacity: usize, - redis_urls: Option>, - ) -> Self { + pub(crate) async fn with_capacity(configuration: &Configuration, capacity: usize) -> Self { Self { - cache: CacheStorage::new(capacity, redis_urls).await, + cache: CacheStorage::new(capacity, None).await, defer_support: configuration.supergraph.preview_defer_support, } } pub(crate) async fn new(configuration: &Configuration) -> Self { - Self::with_capacity( - configuration, - DEFAULT_INTROSPECTION_CACHE_CAPACITY, - configuration.supergraph.cache(), - ) - .await + Self::with_capacity(configuration, DEFAULT_INTROSPECTION_CACHE_CAPACITY).await } #[cfg(test)] @@ -44,8 +35,7 @@ impl Introspection { configuration: &Configuration, cache: HashMap, ) -> Self { - let this = - Self::with_capacity(configuration, cache.len(), configuration.supergraph.cache()).await; + let this = Self::with_capacity(configuration, cache.len()).await; for (query, response) in cache.into_iter() { this.cache.insert(query, response).await; diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index bb5cfab671..da2ed9c789 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -35,11 +35,11 @@ where /// Creates a new query planner that caches the results of another [`QueryPlanner`]. pub(crate) async fn new( delegate: T, - plan_cache_limit: usize, schema_id: Option, - redis_urls: Option>, + config: &crate::configuration::QueryPlanning, ) -> CachingQueryPlanner { - let cache = Arc::new(DeduplicatingCache::with_capacity(plan_cache_limit, redis_urls).await); + let cache = + Arc::new(DeduplicatingCache::from_configuration(&config.experimental_cache).await); Self { cache, delegate, @@ -262,7 +262,12 @@ mod tests { planner }); - let mut planner = CachingQueryPlanner::new(delegate, 10, None, None).await; + let mut planner = CachingQueryPlanner::new( + delegate, + None, + &crate::configuration::QueryPlanning::default(), + ) + .await; for _ in 0..5 { assert!(planner @@ -318,7 +323,12 @@ mod tests { planner }); - let mut planner = CachingQueryPlanner::new(delegate, 10, None, None).await; + let mut planner = CachingQueryPlanner::new( + delegate, + None, + &crate::configuration::QueryPlanning::default(), + ) + .await; for _ in 0..5 { assert!(planner diff --git a/apollo-router/src/router.rs b/apollo-router/src/router.rs index 88d583f5c4..41352ca776 100644 --- a/apollo-router/src/router.rs +++ b/apollo-router/src/router.rs @@ -36,7 +36,6 @@ use self::Event::UpdateSchema; use crate::axum_factory::make_axum_router; use crate::axum_factory::AxumHttpServerFactory; use crate::axum_factory::ListenAddrAndRouter; -use crate::cache::DeduplicatingCache; use crate::configuration::Configuration; use crate::configuration::ListenAddr; use crate::plugin::DynPlugin; @@ -65,7 +64,7 @@ async fn make_transport_service( .create(configuration.clone(), schema, None, Some(extra_plugins)) .await?; - let apq = APQLayer::with_cache(DeduplicatingCache::new().await); + let apq = APQLayer::new().await; let web_endpoints = service_factory.web_endpoints(); let routers = make_axum_router(service_factory, &configuration, web_endpoints, apq)?; // FIXME: how should diff --git a/apollo-router/src/services/supergraph_service.rs b/apollo-router/src/services/supergraph_service.rs index c409359174..4018f25698 100644 --- a/apollo-router/src/services/supergraph_service.rs +++ b/apollo-router/src/services/supergraph_service.rs @@ -315,12 +315,6 @@ impl PluggableSupergraphServiceBuilder { let configuration = self.configuration.unwrap_or_default(); - let plan_cache_limit = std::env::var("ROUTER_PLAN_CACHE_LIMIT") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(100); - let redis_urls = configuration.supergraph.cache(); - let introspection = if configuration.supergraph.introspection { Some(Arc::new(Introspection::new(&configuration).await)) } else { @@ -329,14 +323,13 @@ impl PluggableSupergraphServiceBuilder { // QueryPlannerService takes an UnplannedRequest and outputs PlannedRequest let bridge_query_planner = - BridgeQueryPlanner::new(self.schema.clone(), introspection, configuration) + BridgeQueryPlanner::new(self.schema.clone(), introspection, configuration.clone()) .await .map_err(ServiceBuildError::QueryPlannerError)?; let query_planner_service = CachingQueryPlanner::new( bridge_query_planner, - plan_cache_limit, self.schema.schema_id.clone(), - redis_urls, + &configuration.supergraph.query_planning, ) .await; diff --git a/docs/source/configuration/caching.mdx b/docs/source/configuration/caching.mdx new file mode 100644 index 0000000000..5a31a430bb --- /dev/null +++ b/docs/source/configuration/caching.mdx @@ -0,0 +1,42 @@ +--- +title: Caching in the Apollo Router +--- + +The Apollo Router comes with an in-memory cache, used to store Automated Persisted Queries (APQ) and query plans. +This is a Least Recently Used (LRU) cache, that can be configured as follows: + +```yaml title="router.yaml" +supergraph: + apq: + experimental_cache: + in_memory: + limit: 512 + query_planning: + experimental_cache: + in_memory: + limit: 512 +``` + +Introspection responses are cached too, but that cache is not configurable for now. + +## Experimental Redis cache + +The Apollo Router has an experimental external storage cache, using Redis Cluster. It can be tested by building a custom Router binary, with the Cargo feature `experimental_cache`. + +This will activate a configuration option to connect to a Redis Cluster: + +```yaml +supergraph: + apq: + experimental_cache: + in_memory: + limit: 512 + redis: + urls: ["redis://..."] + query_planning: + experimental_cache: + in_memory: + limit: 512 + redis: + urls: ["redis://..."] +``` \ No newline at end of file diff --git a/docs/source/configuration/overview.mdx b/docs/source/configuration/overview.mdx index 2a3d8043a2..65e45000e3 100644 --- a/docs/source/configuration/overview.mdx +++ b/docs/source/configuration/overview.mdx @@ -368,7 +368,7 @@ See [Tracing in the Apollo Router](./tracing/). ### Automatic persisted queries (APQ) Automatic Persisted Queries (APQ) enable GraphQL clients to send a server the _hash_ of their query string, _instead of_ the query string itself. This can significantly reduce network usage for very large query strings. -The Apollo Router automatically supports APQ via its in-memory cache. **No configuration options are supported at this time.** Support for external data stores like Redis and Memcached will be supported in a future release. +The Apollo Router automatically supports APQ via its in-memory cache. See the [caching documentation](./caching) for related options. For more information on APQ, including client configuration, see [this article](/apollo-server/performance/apq/).