armature_cache/
tiered.rs

1//! Multi-tier caching (L1/L2 cache layers)
2
3use crate::error::CacheResult;
4use crate::traits::CacheStore;
5use async_trait::async_trait;
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::RwLock;
10
11/// Multi-tier cache with L1 (in-memory) and L2 (distributed) layers
12pub struct TieredCache<L1, L2>
13where
14    L1: CacheStore,
15    L2: CacheStore,
16{
17    /// L1 cache (fast, local)
18    l1: Arc<L1>,
19
20    /// L2 cache (slower, distributed)
21    l2: Arc<L2>,
22
23    /// Configuration
24    config: TieredCacheConfig,
25}
26
27/// Tiered cache configuration
28#[derive(Debug, Clone)]
29pub struct TieredCacheConfig {
30    /// Enable L1 cache
31    pub enable_l1: bool,
32
33    /// Enable L2 cache
34    pub enable_l2: bool,
35
36    /// Write-through to L2 on L1 set
37    pub write_through: bool,
38
39    /// Promote L2 hits to L1
40    pub promote_to_l1: bool,
41
42    /// L1 TTL multiplier (fraction of L2 TTL)
43    pub l1_ttl_fraction: f64,
44}
45
46impl Default for TieredCacheConfig {
47    fn default() -> Self {
48        Self {
49            enable_l1: true,
50            enable_l2: true,
51            write_through: true,
52            promote_to_l1: true,
53            l1_ttl_fraction: 0.25, // L1 lives 1/4 as long as L2
54        }
55    }
56}
57
58impl<L1, L2> TieredCache<L1, L2>
59where
60    L1: CacheStore,
61    L2: CacheStore,
62{
63    /// Create new tiered cache
64    ///
65    /// # Examples
66    ///
67    /// ```rust,ignore
68    /// use armature_cache::*;
69    ///
70    /// let l1 = Arc::new(InMemoryCache::new());
71    /// let l2 = Arc::new(RedisCache::new(config).await?);
72    /// let cache = TieredCache::new(l1, l2);
73    /// ```
74    pub fn new(l1: Arc<L1>, l2: Arc<L2>) -> Self {
75        Self::with_config(l1, l2, TieredCacheConfig::default())
76    }
77
78    /// Create with custom configuration
79    pub fn with_config(l1: Arc<L1>, l2: Arc<L2>, config: TieredCacheConfig) -> Self {
80        Self { l1, l2, config }
81    }
82
83    /// Get value from cache (checks L1 then L2)
84    pub async fn get(&self, key: &str) -> CacheResult<Option<String>> {
85        // Try L1 first
86        if self.config.enable_l1
87            && let Some(value) = self.l1.get_json(key).await?
88        {
89            return Ok(Some(value));
90        }
91
92        // Try L2
93        if self.config.enable_l2
94            && let Some(value) = self.l2.get_json(key).await?
95        {
96            // Promote to L1 if configured
97            if self.config.enable_l1 && self.config.promote_to_l1 {
98                // Use shorter TTL for L1
99                let l2_ttl = self.l2.ttl(key).await?;
100                let l1_ttl = l2_ttl.map(|ttl| {
101                    Duration::from_secs_f64(ttl.as_secs_f64() * self.config.l1_ttl_fraction)
102                });
103                let _ = self.l1.set_json(key, value.clone(), l1_ttl).await;
104            }
105            return Ok(Some(value));
106        }
107
108        Ok(None)
109    }
110
111    /// Set value in cache (writes to both L1 and L2)
112    pub async fn set(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
113        // Write to L2 first (source of truth)
114        if self.config.enable_l2 {
115            self.l2.set_json(key, value.clone(), ttl).await?;
116        }
117
118        // Write to L1 if write-through is enabled
119        if self.config.enable_l1 && (self.config.write_through || !self.config.enable_l2) {
120            let l1_ttl = ttl.map(|ttl| {
121                Duration::from_secs_f64(ttl.as_secs_f64() * self.config.l1_ttl_fraction)
122            });
123            self.l1.set_json(key, value, l1_ttl).await?;
124        }
125
126        Ok(())
127    }
128
129    /// Delete from both L1 and L2
130    pub async fn delete(&self, key: &str) -> CacheResult<()> {
131        if self.config.enable_l1 {
132            self.l1.delete(key).await?;
133        }
134        if self.config.enable_l2 {
135            self.l2.delete(key).await?;
136        }
137        Ok(())
138    }
139
140    /// Check if key exists (checks L1 then L2)
141    pub async fn exists(&self, key: &str) -> CacheResult<bool> {
142        if self.config.enable_l1 && self.l1.exists(key).await? {
143            return Ok(true);
144        }
145        if self.config.enable_l2 {
146            return self.l2.exists(key).await;
147        }
148        Ok(false)
149    }
150
151    /// Clear both L1 and L2
152    pub async fn clear(&self) -> CacheResult<()> {
153        if self.config.enable_l1 {
154            self.l1.clear().await?;
155        }
156        if self.config.enable_l2 {
157            self.l2.clear().await?;
158        }
159        Ok(())
160    }
161
162    /// Get cache statistics
163    pub async fn stats(&self) -> CacheStats {
164        CacheStats {
165            l1_enabled: self.config.enable_l1,
166            l2_enabled: self.config.enable_l2,
167            write_through: self.config.write_through,
168            promote_to_l1: self.config.promote_to_l1,
169        }
170    }
171}
172
173impl<L1, L2> Clone for TieredCache<L1, L2>
174where
175    L1: CacheStore,
176    L2: CacheStore,
177{
178    fn clone(&self) -> Self {
179        Self {
180            l1: self.l1.clone(),
181            l2: self.l2.clone(),
182            config: self.config.clone(),
183        }
184    }
185}
186
187/// Cache statistics
188#[derive(Debug, Clone)]
189pub struct CacheStats {
190    pub l1_enabled: bool,
191    pub l2_enabled: bool,
192    pub write_through: bool,
193    pub promote_to_l1: bool,
194}
195
196/// In-memory cache for L1 tier
197pub struct InMemoryCache {
198    data: Arc<RwLock<HashMap<String, CacheEntry>>>,
199}
200
201#[derive(Clone)]
202struct CacheEntry {
203    value: String,
204    expires_at: Option<tokio::time::Instant>,
205}
206
207impl InMemoryCache {
208    /// Create new in-memory cache
209    pub fn new() -> Self {
210        Self {
211            data: Arc::new(RwLock::new(HashMap::new())),
212        }
213    }
214
215    /// Clean up expired entries
216    #[allow(dead_code)]
217    async fn cleanup_expired(&self) {
218        let mut data = self.data.write().await;
219        let now = tokio::time::Instant::now();
220        data.retain(|_, entry| entry.expires_at.is_none_or(|exp| exp > now));
221    }
222}
223
224impl Default for InMemoryCache {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[async_trait]
231impl CacheStore for InMemoryCache {
232    async fn get_json(&self, key: &str) -> CacheResult<Option<String>> {
233        let data = self.data.read().await;
234        if let Some(entry) = data.get(key) {
235            if let Some(expires_at) = entry.expires_at
236                && tokio::time::Instant::now() > expires_at
237            {
238                return Ok(None); // Expired
239            }
240            Ok(Some(entry.value.clone()))
241        } else {
242            Ok(None)
243        }
244    }
245
246    async fn set_json(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
247        let expires_at = ttl.map(|d| tokio::time::Instant::now() + d);
248        let entry = CacheEntry { value, expires_at };
249        self.data.write().await.insert(key.to_string(), entry);
250        Ok(())
251    }
252
253    async fn delete(&self, key: &str) -> CacheResult<()> {
254        self.data.write().await.remove(key);
255        Ok(())
256    }
257
258    async fn exists(&self, key: &str) -> CacheResult<bool> {
259        self.get_json(key).await.map(|v| v.is_some())
260    }
261
262    async fn clear(&self) -> CacheResult<()> {
263        self.data.write().await.clear();
264        Ok(())
265    }
266
267    async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
268        let data = self.data.read().await;
269        if let Some(entry) = data.get(key) {
270            if let Some(expires_at) = entry.expires_at {
271                let now = tokio::time::Instant::now();
272                if expires_at > now {
273                    Ok(Some(expires_at - now))
274                } else {
275                    Ok(None)
276                }
277            } else {
278                Ok(None)
279            }
280        } else {
281            Ok(None)
282        }
283    }
284
285    async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()> {
286        let mut data = self.data.write().await;
287        if let Some(entry) = data.get_mut(key) {
288            entry.expires_at = Some(tokio::time::Instant::now() + ttl);
289        }
290        Ok(())
291    }
292
293    async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
294        let mut data = self.data.write().await;
295        let entry = data.entry(key.to_string()).or_insert_with(|| CacheEntry {
296            value: "0".to_string(),
297            expires_at: None,
298        });
299
300        let current: i64 = entry.value.parse().unwrap_or(0);
301        let new_value = current + delta;
302        entry.value = new_value.to_string();
303
304        Ok(new_value)
305    }
306
307    async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
308        self.increment(key, -delta).await
309    }
310}
311
312#[cfg(test)]
313mod tests_tiered {
314    use super::*;
315
316    #[tokio::test]
317    async fn test_tiered_cache() {
318        let l1 = Arc::new(InMemoryCache::new());
319        let l2 = Arc::new(InMemoryCache::new());
320        let cache = TieredCache::new(l1.clone(), l2.clone());
321
322        // Set value
323        cache.set("test", "value".to_string(), None).await.unwrap();
324
325        // Get from L1
326        let value = l1.get_json("test").await.unwrap();
327        assert!(value.is_some());
328
329        // Get from tiered cache
330        let value = cache.get("test").await.unwrap();
331        assert_eq!(value, Some("value".to_string()));
332
333        // Delete
334        cache.delete("test").await.unwrap();
335        let value = cache.get("test").await.unwrap();
336        assert_eq!(value, None);
337    }
338
339    #[tokio::test]
340    async fn test_l2_promotion() {
341        let l1 = Arc::new(InMemoryCache::new());
342        let l2 = Arc::new(InMemoryCache::new());
343        let cache = TieredCache::new(l1.clone(), l2.clone());
344
345        // Set in L2 only
346        l2.set_json("key", "value".to_string(), None).await.unwrap();
347
348        // Get from tiered cache (should promote to L1)
349        let value = cache.get("key").await.unwrap();
350        assert_eq!(value, Some("value".to_string()));
351
352        // Check L1 was populated
353        let l1_value = l1.get_json("key").await.unwrap();
354        assert!(l1_value.is_some());
355    }
356}