llm_config_cache/
manager.rs

1//! Cache manager coordinating L1 and L2 caches
2
3use crate::{l1::L1Cache, l2::L2Cache, Result};
4use llm_config_core::ConfigEntry;
5use std::path::Path;
6use std::sync::Arc;
7
8/// Multi-tier cache manager
9pub struct CacheManager {
10    l1: Arc<L1Cache>,
11    l2: Arc<L2Cache>,
12}
13
14impl CacheManager {
15    /// Create a new cache manager
16    pub fn new(l1_size: usize, l2_dir: impl AsRef<Path>) -> Result<Self> {
17        Ok(Self {
18            l1: Arc::new(L1Cache::new(l1_size)),
19            l2: Arc::new(L2Cache::new(l2_dir)?),
20        })
21    }
22
23    /// Get an entry from the cache
24    ///
25    /// Search order:
26    /// 1. L1 cache (fastest)
27    /// 2. L2 cache (fast)
28    /// 3. Return cache miss
29    pub fn get(&self, namespace: &str, key: &str, env: &str) -> Result<ConfigEntry> {
30        // Try L1 first
31        if let Ok(entry) = self.l1.get(namespace, key, env) {
32            return Ok(entry);
33        }
34
35        // Try L2 if L1 miss
36        if let Ok(entry) = self.l2.get(namespace, key, env) {
37            // Promote to L1
38            self.l1.put(entry.clone())?;
39            return Ok(entry);
40        }
41
42        // Complete cache miss
43        Err(crate::CacheError::CacheMiss(format!(
44            "{}:{}:{}",
45            namespace, key, env
46        )))
47    }
48
49    /// Put an entry into the cache (both L1 and L2)
50    pub fn put(&self, entry: ConfigEntry) -> Result<()> {
51        // Write to both caches
52        self.l1.put(entry.clone())?;
53        self.l2.put(&entry)?;
54        Ok(())
55    }
56
57    /// Invalidate an entry from both caches
58    pub fn invalidate(&self, namespace: &str, key: &str, env: &str) -> Result<()> {
59        self.l1.invalidate(namespace, key, env);
60        self.l2.invalidate(namespace, key, env)?;
61        Ok(())
62    }
63
64    /// Clear both caches
65    pub fn clear(&self) -> Result<()> {
66        self.l1.clear();
67        self.l2.clear()?;
68        Ok(())
69    }
70
71    /// Get L1 cache statistics
72    pub fn l1_stats(&self) -> crate::l1::CacheStats {
73        self.l1.stats()
74    }
75
76    /// Get L2 cache size
77    pub fn l2_size(&self) -> usize {
78        self.l2.size()
79    }
80
81    /// Clear only L1 cache (for testing)
82    pub fn clear_l1(&self) {
83        self.l1.clear();
84    }
85
86    /// Clear only L2 cache (for testing)
87    pub fn clear_l2(&self) -> Result<()> {
88        self.l2.clear()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use llm_config_core::{ConfigMetadata, ConfigValue, Environment};
96    use tempfile::TempDir;
97    use uuid::Uuid;
98
99    fn create_test_entry(namespace: &str, key: &str, env: Environment) -> ConfigEntry {
100        ConfigEntry {
101            id: Uuid::new_v4(),
102            namespace: namespace.to_string(),
103            key: key.to_string(),
104            value: ConfigValue::String("test-value".to_string()),
105            environment: env,
106            version: 1,
107            metadata: ConfigMetadata {
108                created_at: chrono::Utc::now(),
109                created_by: "test".to_string(),
110                updated_at: chrono::Utc::now(),
111                updated_by: "test".to_string(),
112                tags: vec![],
113                description: None,
114            },
115        }
116    }
117
118    #[test]
119    fn test_manager_creation() {
120        let temp_dir = TempDir::new().unwrap();
121        let manager = CacheManager::new(100, temp_dir.path()).unwrap();
122        assert_eq!(manager.l1_stats().size, 0);
123        assert_eq!(manager.l2_size(), 0);
124    }
125
126    #[test]
127    fn test_put_and_get() {
128        let temp_dir = TempDir::new().unwrap();
129        let manager = CacheManager::new(100, temp_dir.path()).unwrap();
130
131        let entry = create_test_entry("ns", "key1", Environment::Development);
132        manager.put(entry.clone()).unwrap();
133
134        let retrieved = manager.get("ns", "key1", "development").unwrap();
135        assert_eq!(retrieved.id, entry.id);
136    }
137
138    #[test]
139    fn test_l1_to_l2_fallback() {
140        let temp_dir = TempDir::new().unwrap();
141        let manager = CacheManager::new(100, temp_dir.path()).unwrap();
142
143        let entry = create_test_entry("ns", "key1", Environment::Development);
144        manager.put(entry.clone()).unwrap();
145
146        // Clear L1 but L2 should still have it
147        manager.l1.clear();
148
149        // Should still retrieve from L2
150        let retrieved = manager.get("ns", "key1", "development").unwrap();
151        assert_eq!(retrieved.id, entry.id);
152
153        // L1 should now have it (promoted)
154        let stats = manager.l1_stats();
155        assert_eq!(stats.size, 1);
156    }
157
158    #[test]
159    fn test_invalidate() {
160        let temp_dir = TempDir::new().unwrap();
161        let manager = CacheManager::new(100, temp_dir.path()).unwrap();
162
163        let entry = create_test_entry("ns", "key1", Environment::Development);
164        manager.put(entry).unwrap();
165
166        manager.invalidate("ns", "key1", "development").unwrap();
167
168        assert!(manager.get("ns", "key1", "development").is_err());
169    }
170
171    #[test]
172    fn test_clear() {
173        let temp_dir = TempDir::new().unwrap();
174        let manager = CacheManager::new(100, temp_dir.path()).unwrap();
175
176        for i in 0..10 {
177            let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
178            manager.put(entry).unwrap();
179        }
180
181        assert_eq!(manager.l1_stats().size, 10);
182        assert_eq!(manager.l2_size(), 10);
183
184        manager.clear().unwrap();
185
186        assert_eq!(manager.l1_stats().size, 0);
187        assert_eq!(manager.l2_size(), 0);
188    }
189
190    #[test]
191    fn test_cache_promotion() {
192        let temp_dir = TempDir::new().unwrap();
193        let manager = CacheManager::new(2, temp_dir.path()).unwrap(); // Small L1 cache
194
195        // Add 3 entries
196        for i in 0..3 {
197            let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
198            manager.put(entry).unwrap();
199        }
200
201        // L1 can only hold 2 entries, L2 should have all 3
202        assert_eq!(manager.l1_stats().size, 2);
203        assert_eq!(manager.l2_size(), 3);
204
205        // Access an entry that was evicted from L1
206        manager.get("ns", "key0", "development").unwrap();
207
208        // It should be promoted back to L1
209        let stats = manager.l1_stats();
210        assert_eq!(stats.size, 2); // Still at capacity
211    }
212}