From 883410fd6cac6105210982ce47b2722e87009005 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Thu, 2 Jul 2026 10:14:36 +0100 Subject: [PATCH] Make the user config cache expiry configurable The control-plane dataplane publisher rewrites UserConfig keys in Redis every 60s with a 70s TTL, but RedisUserConfigStore cached entries with the shared LRU_CACHE_EXPIRY_DURATION of 1 hour, hardcoded in const_values.rs. Any subject already in the cache kept serving stale config for up to an hour after a change, so newly created or modified virtual servers stayed unreachable through the dataplane until a restart. Reproduced in cf-integration: a new virtual server was present in Redis within one publish cycle while the dataplane kept answering -32002 No configuration for 148s+; restarting the dataplane made the same request succeed immediately. Add user_config_cache_expiry_seconds to the config surface (--user-config-cache-expiry-seconds / CONTEXTFORGE_GATEWAY_RS_USER_CONFIG_CACHE_EXPIRY_SECONDS), defaulting to 60 to match the publisher cadence. 0 disables caching entirely and reads Redis on every request, which is useful for tests that create virtual servers at runtime. Session stores keep the 1 hour expiry so session lifetimes are unaffected. Signed-off-by: lucarlig --- .../contextforge-gateway-rs-lib/src/common.rs | 7 ++++ crates/contextforge-gateway-rs-lib/src/lib.rs | 3 +- .../user_config_store/redis_config_store.rs | 33 ++++++++++--------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/crates/contextforge-gateway-rs-lib/src/common.rs b/crates/contextforge-gateway-rs-lib/src/common.rs index 8683c946..59f31549 100644 --- a/crates/contextforge-gateway-rs-lib/src/common.rs +++ b/crates/contextforge-gateway-rs-lib/src/common.rs @@ -225,6 +225,13 @@ pub struct Config { #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM_TRUST_BUNDLE")] pub upstream_trust_bundle: Option, + /// Expiry in seconds for the in-process user config cache in front of + /// Redis. The control-plane dataplane publisher rewrites UserConfig keys + /// every 60s, so this bounds how stale a subject's config can get. + /// 0 disables caching and reads Redis on every request. + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_USER_CONFIG_CACHE_EXPIRY_SECONDS", default_value_t = 60)] + pub user_config_cache_expiry_seconds: u64, + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_REDIS_HOSTNAME")] pub redis_address: String, #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_REDIS_PORT")] diff --git a/crates/contextforge-gateway-rs-lib/src/lib.rs b/crates/contextforge-gateway-rs-lib/src/lib.rs index d41b4976..e8c0751d 100644 --- a/crates/contextforge-gateway-rs-lib/src/lib.rs +++ b/crates/contextforge-gateway-rs-lib/src/lib.rs @@ -161,5 +161,6 @@ impl Gateway { pub async fn get_config_store(config: &Config) -> Result { let redis_config = RedisConfig::try_from(config)?; - RedisUserConfigStore::new(&RedisClient::try_from(redis_config)?).await + let cache_expiry = std::time::Duration::from_secs(config.user_config_cache_expiry_seconds); + RedisUserConfigStore::new(&RedisClient::try_from(redis_config)?, cache_expiry).await } diff --git a/crates/contextforge-gateway-rs-lib/src/user_config_store/redis_config_store.rs b/crates/contextforge-gateway-rs-lib/src/user_config_store/redis_config_store.rs index 60c19a4b..101884b5 100644 --- a/crates/contextforge-gateway-rs-lib/src/user_config_store/redis_config_store.rs +++ b/crates/contextforge-gateway-rs-lib/src/user_config_store/redis_config_store.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use contextforge_gateway_rs_apis::{User, user_store::UserConfig}; @@ -14,17 +14,18 @@ use tracing::{debug, warn}; use super::{ConfigStoreError, UserConfigStore}; use crate::{ common::RedisClient, - const_values::{LRU_CACHE_ENTRIES, LRU_CACHE_EXPIRY_DURATION, REDIS_RETRIES}, + const_values::{LRU_CACHE_ENTRIES, REDIS_RETRIES}, }; #[derive(Clone)] pub struct RedisUserConfigStore { connection: ConnectionManager, - cache: Arc>>, + /// `None` when caching is disabled (zero expiry): every request reads Redis. + cache: Option>>>, } impl RedisUserConfigStore { - pub async fn new(redis_client: &RedisClient) -> crate::Result { + pub async fn new(redis_client: &RedisClient, cache_expiry: Duration) -> crate::Result { Ok(Self { connection: redis_client .get_connection_manager_with_config( @@ -35,10 +36,9 @@ impl RedisUserConfigStore { warn!("RedisUserConfigStore::new - failed to create Redis user config connection error = {error}"); ConfigStoreError::InvalidConnection })?, - cache: Arc::new(Mutex::new(LruCache::with_expiry_duration_and_capacity( - LRU_CACHE_EXPIRY_DURATION, - LRU_CACHE_ENTRIES, - ))), + cache: (!cache_expiry.is_zero()).then(|| { + Arc::new(Mutex::new(LruCache::with_expiry_duration_and_capacity(cache_expiry, LRU_CACHE_ENTRIES))) + }), }) } } @@ -48,18 +48,17 @@ impl UserConfigStore for RedisUserConfigStore { async fn get_config<'a>(&self, user_key: &'a User) -> Result { let subject = user_key.key(); - { - let mut cache = self.cache.lock().await; - if let Some(user_config) = cache.get_mut(subject) { + if let Some(cache) = &self.cache { + if let Some(user_config) = cache.lock().await.get_mut(subject) { let virtual_hosts = user_config.virtual_hosts.len(); debug!( "RedisUserConfigStore::get_config - user config cache hit subject = {subject} virtual_hosts = {virtual_hosts}" ); return Ok(user_config.clone()); } - } - debug!("RedisUserConfigStore::get_config - user config cache miss subject = {subject}"); + debug!("RedisUserConfigStore::get_config - user config cache miss subject = {subject}"); + } let Ok(key) = rmp_serde::encode::to_vec::(user_key) else { warn!("RedisUserConfigStore::get_config - failed to encode Redis user config key subject = {subject}"); @@ -105,7 +104,9 @@ impl UserConfigStore for RedisUserConfigStore { "RedisUserConfigStore::get_config - decoded user config subject = {subject} virtual_hosts = {virtual_hosts}" ); - self.cache.lock().await.insert(subject.to_owned(), user_config.clone()); + if let Some(cache) = &self.cache { + cache.lock().await.insert(subject.to_owned(), user_config.clone()); + } Ok(user_config) } @@ -134,7 +135,9 @@ impl UserConfigStore for RedisUserConfigStore { debug!( "RedisUserConfigStore::set_config - wrote user config to Redis subject = {subject} bytes = {bytes} virtual_hosts = {virtual_hosts}" ); - self.cache.lock().await.insert(subject.to_owned(), config.clone()); + if let Some(cache) = &self.cache { + cache.lock().await.insert(subject.to_owned(), config.clone()); + } Ok(()) }, Err(error) => {