open_feature_flagd/cache/
service.rs

1//! # Cache Service for Feature Flags
2//!
3//! Provides configurable caching functionality for feature flag values.
4//!
5//! ## Features
6//!
7//! * Thread-safe cache operations
8//! * Multiple cache implementations
9//! * TTL-based invalidation
10//! * Size-bounded caching
11//!
12//! ## Cache Types
13//!
14//! * [`CacheType::Lru`] - Least Recently Used cache
15//! * [`CacheType::InMemory`] - Simple in-memory cache
16//! * [`CacheType::Disabled`] - No caching
17//!
18//! ## Example
19//!
20//! ```rust
21//! use open_feature_flagd::cache::{CacheSettings, CacheType};
22//! use std::time::Duration;
23//!
24//! let settings = CacheSettings {
25//!     cache_type: CacheType::Lru,
26//!     max_size: 1000,
27//!     ttl: Some(Duration::from_secs(60)),
28//! };
29//! ```
30
31use open_feature::{EvaluationContext, EvaluationContextFieldValue};
32use std::hash::{DefaultHasher, Hash, Hasher};
33use std::sync::Arc;
34use std::time::{Duration, Instant};
35use tokio::sync::RwLock;
36
37#[derive(Debug, Clone)]
38pub enum CacheType {
39    Lru,
40    InMemory,
41    Disabled,
42}
43
44impl<'a> From<&'a str> for CacheType {
45    fn from(s: &'a str) -> Self {
46        match s.to_lowercase().as_str() {
47            "lru" => CacheType::Lru,
48            "mem" => CacheType::InMemory,
49            "disabled" => CacheType::Disabled,
50            _ => CacheType::Lru,
51        }
52    }
53}
54
55impl std::fmt::Display for CacheType {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            CacheType::Lru => write!(f, "lru"),
59            CacheType::InMemory => write!(f, "mem"),
60            CacheType::Disabled => write!(f, "disabled"),
61        }
62    }
63}
64
65/// Settings for configuring the cache behavior
66#[derive(Debug, Clone)]
67pub struct CacheSettings {
68    /// Type of cache to use (LRU, InMemory, or Disabled)
69    /// Default: LRU
70    pub cache_type: CacheType,
71    /// Maximum number of entries the cache can hold
72    /// Default: 1000
73    pub max_size: usize,
74    /// Optional time-to-live for cache entries
75    /// Default: 60 seconds
76    pub ttl: Option<Duration>,
77}
78
79impl Default for CacheSettings {
80    fn default() -> Self {
81        let cache_type = std::env::var("FLAGD_CACHE")
82            .map(|s| CacheType::from(s.as_str()))
83            .unwrap_or(CacheType::Lru);
84
85        let max_size = std::env::var("FLAGD_MAX_CACHE_SIZE")
86            .ok()
87            .and_then(|s| s.parse().ok())
88            .unwrap_or(1000);
89
90        // Java or Golang implementation does not use a default TTL, however
91        // if there is no TTL cache is never expired, resulting in flag resolution
92        // not updated. 60 second as a default is taken from gofeatureflag implementation.
93        let ttl = std::env::var("FLAGD_CACHE_TTL")
94            .ok()
95            .and_then(|s| s.parse().ok())
96            .map(Duration::from_secs)
97            .or_else(|| Some(Duration::from_secs(60)));
98
99        Self {
100            cache_type,
101            max_size,
102            ttl,
103        }
104    }
105}
106
107/// Entry in the cache with timestamp for TTL tracking
108#[derive(Debug)]
109struct CacheEntry<V>
110where
111    V: Clone + Send + Sync + std::fmt::Debug + 'static,
112{
113    value: V,
114    created_at: Instant,
115}
116
117/// Core trait defining cache behavior
118pub trait Cache<K, V>: Send + Sync + std::fmt::Debug {
119    /// Adds a new key-value pair to the cache
120    fn add(&mut self, key: K, value: V) -> bool;
121    /// Removes all entries from the cache
122    #[allow(dead_code)]
123    fn purge(&mut self);
124    /// Retrieves a value by key
125    fn get(&mut self, key: &K) -> Option<&V>;
126    /// Removes a specific key from the cache
127    fn remove(&mut self, key: &K) -> bool;
128}
129
130#[derive(Hash, Eq, PartialEq, Clone, Debug)]
131struct CacheKey {
132    flag_key: String,
133    context_hash: String,
134}
135
136impl CacheKey {
137    pub fn new(flag_key: &str, context: &EvaluationContext) -> Self {
138        let mut hasher = DefaultHasher::new();
139        // Hash targeting key if present
140        if let Some(key) = &context.targeting_key {
141            key.hash(&mut hasher);
142        }
143        // Hash custom fields
144        for (key, value) in &context.custom_fields {
145            key.hash(&mut hasher);
146            match value {
147                EvaluationContextFieldValue::String(s) => s.hash(&mut hasher),
148                EvaluationContextFieldValue::Bool(b) => b.hash(&mut hasher),
149                EvaluationContextFieldValue::Int(i) => i.hash(&mut hasher),
150                EvaluationContextFieldValue::Float(f) => f.to_bits().hash(&mut hasher),
151                EvaluationContextFieldValue::DateTime(dt) => dt.to_string().hash(&mut hasher),
152                EvaluationContextFieldValue::Struct(s) => format!("{:?}", s).hash(&mut hasher),
153            }
154        }
155        Self {
156            flag_key: flag_key.to_string(),
157            context_hash: hasher.finish().to_string(),
158        }
159    }
160}
161
162/// Service managing cache operations and lifecycle
163#[derive(Debug)]
164pub struct CacheService<V>
165where
166    V: Clone + Send + Sync + std::fmt::Debug + 'static,
167{
168    /// Whether the cache is currently enabled
169    enabled: bool,
170    /// Time-to-live configuration for cache entries
171    ttl: Option<Duration>,
172    /// The underlying cache implementation
173    cache: Arc<RwLock<Box<dyn Cache<CacheKey, CacheEntry<V>>>>>,
174}
175
176impl<V> CacheService<V>
177where
178    V: Clone + Send + Sync + std::fmt::Debug + 'static,
179{
180    pub fn new(settings: CacheSettings) -> Self {
181        let (enabled, cache) = match settings.cache_type {
182            CacheType::Lru => {
183                let lru = crate::cache::lru::LruCacheImpl::new(settings.max_size);
184                (
185                    true,
186                    Box::new(lru) as Box<dyn Cache<CacheKey, CacheEntry<V>>>,
187                )
188            }
189            CacheType::InMemory => {
190                let mem = crate::cache::in_memory::InMemoryCache::new();
191                (
192                    true,
193                    Box::new(mem) as Box<dyn Cache<CacheKey, CacheEntry<V>>>,
194                )
195            }
196            CacheType::Disabled => {
197                let mem = crate::cache::in_memory::InMemoryCache::new();
198                (
199                    false,
200                    Box::new(mem) as Box<dyn Cache<CacheKey, CacheEntry<V>>>,
201                )
202            }
203        };
204
205        Self {
206            enabled,
207            ttl: settings.ttl,
208            cache: Arc::new(RwLock::new(cache)),
209        }
210    }
211
212    pub async fn get(&self, flag_key: &str, context: &EvaluationContext) -> Option<V> {
213        if !self.enabled {
214            return None;
215        }
216
217        let cache_key = CacheKey::new(flag_key, context);
218        let mut cache = self.cache.write().await;
219
220        if let Some(entry) = cache.get(&cache_key) {
221            if let Some(ttl) = self.ttl {
222                if entry.created_at.elapsed() > ttl {
223                    cache.remove(&cache_key);
224                    return None;
225                }
226            }
227            return Some(entry.value.clone());
228        }
229        None
230    }
231
232    pub async fn add(&self, flag_key: &str, context: &EvaluationContext, value: V) -> bool {
233        if !self.enabled {
234            return false;
235        }
236        let cache_key = CacheKey::new(flag_key, context);
237        let mut cache = self.cache.write().await;
238        let entry = CacheEntry {
239            value,
240            created_at: Instant::now(),
241        };
242        cache.add(cache_key, entry)
243    }
244
245    pub fn disable(&mut self) {
246        if self.enabled {
247            self.enabled = false;
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use test_log::test;
256
257    #[test(tokio::test)]
258    async fn test_cache_service_lru() {
259        let settings = CacheSettings {
260            cache_type: CacheType::Lru,
261            max_size: 2,
262            ttl: None,
263        };
264        let service = CacheService::<String>::new(settings);
265
266        let context1 = EvaluationContext::default()
267            .with_targeting_key("user1")
268            .with_custom_field("email", "test1@example.com");
269
270        let context2 = EvaluationContext::default()
271            .with_targeting_key("user2")
272            .with_custom_field("email", "test2@example.com");
273
274        service.add("key1", &context1, "value1".to_string()).await;
275        service.add("key1", &context2, "value2".to_string()).await;
276
277        assert_eq!(
278            service.get("key1", &context1).await,
279            Some("value1".to_string())
280        );
281        assert_eq!(
282            service.get("key1", &context2).await,
283            Some("value2".to_string())
284        );
285    }
286
287    #[test(tokio::test)]
288    async fn test_cache_service_ttl() {
289        let settings = CacheSettings {
290            cache_type: CacheType::InMemory,
291            max_size: 10,
292            ttl: Some(Duration::from_secs(1)),
293        };
294        let service = CacheService::<String>::new(settings);
295
296        let context = EvaluationContext::default()
297            .with_targeting_key("user1")
298            .with_custom_field("version", "1.0.0");
299
300        service.add("key1", &context, "value1".to_string()).await;
301        assert_eq!(
302            service.get("key1", &context).await,
303            Some("value1".to_string())
304        );
305
306        tokio::time::sleep(Duration::from_secs(2)).await;
307        assert_eq!(service.get("key1", &context).await, None);
308    }
309
310    #[test(tokio::test)]
311    async fn test_cache_service_disabled() {
312        let settings = CacheSettings {
313            cache_type: CacheType::Disabled,
314            max_size: 2,
315            ttl: None,
316        };
317        let service = CacheService::<String>::new(settings);
318
319        let context = EvaluationContext::default().with_targeting_key("user1");
320
321        service.add("key1", &context, "value1".to_string()).await;
322        assert_eq!(service.get("key1", &context).await, None);
323    }
324
325    #[test(tokio::test)]
326    async fn test_different_contexts_same_flag() {
327        let settings = CacheSettings {
328            cache_type: CacheType::InMemory,
329            max_size: 10,
330            ttl: None,
331        };
332        let service = CacheService::<String>::new(settings);
333
334        let context1 = EvaluationContext::default()
335            .with_targeting_key("user1")
336            .with_custom_field("email", "test1@example.com");
337
338        let context2 = EvaluationContext::default()
339            .with_targeting_key("user1")
340            .with_custom_field("email", "test2@example.com");
341
342        service
343            .add("feature-flag", &context1, "variant1".to_string())
344            .await;
345        service
346            .add("feature-flag", &context2, "variant2".to_string())
347            .await;
348
349        assert_eq!(
350            service.get("feature-flag", &context1).await,
351            Some("variant1".to_string())
352        );
353        assert_eq!(
354            service.get("feature-flag", &context2).await,
355            Some("variant2".to_string())
356        );
357    }
358}