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
162#[derive(Debug)]
164pub struct CacheService<V>
165where
166 V: Clone + Send + Sync + std::fmt::Debug + 'static,
167{
168 enabled: bool,
170 ttl: Option<Duration>,
172 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}