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/// Type alias for the thread-safe cache implementation
163type SharedCache<V> = Arc<RwLock<Box<dyn Cache<CacheKey, CacheEntry<V>>>>>;
164
165/// Service managing cache operations and lifecycle
166#[derive(Debug)]
167pub struct CacheService<V>
168where
169    V: Clone + Send + Sync + std::fmt::Debug + 'static,
170{
171    /// Whether the cache is currently enabled
172    enabled: bool,
173    /// Time-to-live configuration for cache entries
174    ttl: Option<Duration>,
175    /// The underlying cache implementation
176    cache: SharedCache<V>,
177}
178
179impl<V> CacheService<V>
180where
181    V: Clone + Send + Sync + std::fmt::Debug + 'static,
182{
183    pub fn new(settings: CacheSettings) -> Self {
184        let (enabled, cache) = match settings.cache_type {
185            CacheType::Lru => {
186                let lru = crate::cache::lru::LruCacheImpl::new(settings.max_size);
187                (
188                    true,
189                    Box::new(lru) as Box<dyn Cache<CacheKey, CacheEntry<V>>>,
190                )
191            }
192            CacheType::InMemory => {
193                let mem = crate::cache::in_memory::InMemoryCache::new();
194                (
195                    true,
196                    Box::new(mem) as Box<dyn Cache<CacheKey, CacheEntry<V>>>,
197                )
198            }
199            CacheType::Disabled => {
200                let mem = crate::cache::in_memory::InMemoryCache::new();
201                (
202                    false,
203                    Box::new(mem) as Box<dyn Cache<CacheKey, CacheEntry<V>>>,
204                )
205            }
206        };
207
208        Self {
209            enabled,
210            ttl: settings.ttl,
211            cache: Arc::new(RwLock::new(cache)),
212        }
213    }
214
215    pub async fn get(&self, flag_key: &str, context: &EvaluationContext) -> Option<V> {
216        if !self.enabled {
217            return None;
218        }
219
220        let cache_key = CacheKey::new(flag_key, context);
221        let mut cache = self.cache.write().await;
222
223        if let Some(entry) = cache.get(&cache_key) {
224            if let Some(ttl) = self.ttl
225                && entry.created_at.elapsed() > ttl
226            {
227                cache.remove(&cache_key);
228                return None;
229            }
230            return Some(entry.value.clone());
231        }
232        None
233    }
234
235    pub async fn add(&self, flag_key: &str, context: &EvaluationContext, value: V) -> bool {
236        if !self.enabled {
237            return false;
238        }
239        let cache_key = CacheKey::new(flag_key, context);
240        let mut cache = self.cache.write().await;
241        let entry = CacheEntry {
242            value,
243            created_at: Instant::now(),
244        };
245        cache.add(cache_key, entry)
246    }
247
248    pub async fn purge(&self) {
249        if self.enabled {
250            let mut cache = self.cache.write().await;
251            cache.purge();
252        }
253    }
254
255    pub fn disable(&mut self) {
256        if self.enabled {
257            self.enabled = false;
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use test_log::test;
266
267    #[test(tokio::test)]
268    async fn test_cache_service_lru() {
269        let settings = CacheSettings {
270            cache_type: CacheType::Lru,
271            max_size: 2,
272            ttl: None,
273        };
274        let service = CacheService::<String>::new(settings);
275
276        let context1 = EvaluationContext::default()
277            .with_targeting_key("user1")
278            .with_custom_field("email", "test1@example.com");
279
280        let context2 = EvaluationContext::default()
281            .with_targeting_key("user2")
282            .with_custom_field("email", "test2@example.com");
283
284        service.add("key1", &context1, "value1".to_string()).await;
285        service.add("key1", &context2, "value2".to_string()).await;
286
287        assert_eq!(
288            service.get("key1", &context1).await,
289            Some("value1".to_string())
290        );
291        assert_eq!(
292            service.get("key1", &context2).await,
293            Some("value2".to_string())
294        );
295    }
296
297    #[test(tokio::test)]
298    async fn test_cache_service_ttl() {
299        let settings = CacheSettings {
300            cache_type: CacheType::InMemory,
301            max_size: 10,
302            ttl: Some(Duration::from_secs(1)),
303        };
304        let service = CacheService::<String>::new(settings);
305
306        let context = EvaluationContext::default()
307            .with_targeting_key("user1")
308            .with_custom_field("version", "1.0.0");
309
310        service.add("key1", &context, "value1".to_string()).await;
311        assert_eq!(
312            service.get("key1", &context).await,
313            Some("value1".to_string())
314        );
315
316        tokio::time::sleep(Duration::from_secs(2)).await;
317        assert_eq!(service.get("key1", &context).await, None);
318    }
319
320    #[test(tokio::test)]
321    async fn test_cache_service_disabled() {
322        let settings = CacheSettings {
323            cache_type: CacheType::Disabled,
324            max_size: 2,
325            ttl: None,
326        };
327        let service = CacheService::<String>::new(settings);
328
329        let context = EvaluationContext::default().with_targeting_key("user1");
330
331        service.add("key1", &context, "value1".to_string()).await;
332        assert_eq!(service.get("key1", &context).await, None);
333    }
334
335    #[test(tokio::test)]
336    async fn test_different_contexts_same_flag() {
337        let settings = CacheSettings {
338            cache_type: CacheType::InMemory,
339            max_size: 10,
340            ttl: None,
341        };
342        let service = CacheService::<String>::new(settings);
343
344        let context1 = EvaluationContext::default()
345            .with_targeting_key("user1")
346            .with_custom_field("email", "test1@example.com");
347
348        let context2 = EvaluationContext::default()
349            .with_targeting_key("user1")
350            .with_custom_field("email", "test2@example.com");
351
352        service
353            .add("feature-flag", &context1, "variant1".to_string())
354            .await;
355        service
356            .add("feature-flag", &context2, "variant2".to_string())
357            .await;
358
359        assert_eq!(
360            service.get("feature-flag", &context1).await,
361            Some("variant1".to_string())
362        );
363        assert_eq!(
364            service.get("feature-flag", &context2).await,
365            Some("variant2".to_string())
366        );
367    }
368}