open_feature_flagd/cache/
service.rs1use 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#[derive(Debug, Clone)]
67pub struct CacheSettings {
68 pub cache_type: CacheType,
71 pub max_size: usize,
74 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 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#[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
117pub trait Cache<K, V>: Send + Sync + std::fmt::Debug {
119 fn add(&mut self, key: K, value: V) -> bool;
121 #[allow(dead_code)]
123 fn purge(&mut self);
124 fn get(&mut self, key: &K) -> Option<&V>;
126 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 if let Some(key) = &context.targeting_key {
141 key.hash(&mut hasher);
142 }
143 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
162type SharedCache<V> = Arc<RwLock<Box<dyn Cache<CacheKey, CacheEntry<V>>>>>;
164
165#[derive(Debug)]
167pub struct CacheService<V>
168where
169 V: Clone + Send + Sync + std::fmt::Debug + 'static,
170{
171 enabled: bool,
173 ttl: Option<Duration>,
175 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}