llm_config_cache/
l1.rs

1//! L1 in-memory cache with LRU eviction
2
3use crate::{CacheError, Result};
4use chrono::{DateTime, Utc};
5use llm_config_core::ConfigEntry;
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8
9/// Cached entry with metadata
10#[derive(Debug, Clone)]
11struct CachedEntry {
12    entry: ConfigEntry,
13    accessed_at: DateTime<Utc>,
14    access_count: u64,
15}
16
17/// L1 in-memory cache with LRU eviction policy
18pub struct L1Cache {
19    cache: Arc<RwLock<HashMap<String, CachedEntry>>>,
20    max_size: usize,
21    hit_count: Arc<RwLock<u64>>,
22    miss_count: Arc<RwLock<u64>>,
23}
24
25impl L1Cache {
26    /// Create a new L1 cache with specified max size
27    pub fn new(max_size: usize) -> Self {
28        Self {
29            cache: Arc::new(RwLock::new(HashMap::new())),
30            max_size,
31            hit_count: Arc::new(RwLock::new(0)),
32            miss_count: Arc::new(RwLock::new(0)),
33        }
34    }
35
36    /// Generate cache key from namespace, key, and environment
37    fn cache_key(namespace: &str, key: &str, env: &str) -> String {
38        format!("{}:{}:{}", namespace, key, env)
39    }
40
41    /// Get an entry from the cache
42    pub fn get(&self, namespace: &str, key: &str, env: &str) -> Result<ConfigEntry> {
43        let cache_key = Self::cache_key(namespace, key, env);
44
45        let mut cache = self.cache.write().unwrap();
46
47        if let Some(cached) = cache.get_mut(&cache_key) {
48            // Update access metadata
49            cached.accessed_at = Utc::now();
50            cached.access_count += 1;
51
52            // Increment hit counter
53            *self.hit_count.write().unwrap() += 1;
54
55            Ok(cached.entry.clone())
56        } else {
57            // Increment miss counter
58            *self.miss_count.write().unwrap() += 1;
59
60            Err(CacheError::CacheMiss(cache_key))
61        }
62    }
63
64    /// Put an entry into the cache
65    pub fn put(&self, entry: ConfigEntry) -> Result<()> {
66        let cache_key = Self::cache_key(&entry.namespace, &entry.key, &entry.environment.to_string());
67
68        let mut cache = self.cache.write().unwrap();
69
70        // Check if we need to evict
71        if cache.len() >= self.max_size && !cache.contains_key(&cache_key) {
72            self.evict_lru(&mut cache)?;
73        }
74
75        // Insert or update entry
76        cache.insert(
77            cache_key,
78            CachedEntry {
79                entry,
80                accessed_at: Utc::now(),
81                access_count: 1,
82            },
83        );
84
85        Ok(())
86    }
87
88    /// Evict the least recently used entry
89    fn evict_lru(&self, cache: &mut HashMap<String, CachedEntry>) -> Result<()> {
90        if cache.is_empty() {
91            return Ok(());
92        }
93
94        // Find LRU entry
95        let lru_key = cache
96            .iter()
97            .min_by_key(|(_, entry)| entry.accessed_at)
98            .map(|(k, _)| k.clone())
99            .ok_or_else(|| CacheError::Eviction("Failed to find LRU entry".to_string()))?;
100
101        cache.remove(&lru_key);
102        Ok(())
103    }
104
105    /// Invalidate a specific entry
106    pub fn invalidate(&self, namespace: &str, key: &str, env: &str) {
107        let cache_key = Self::cache_key(namespace, key, env);
108        let mut cache = self.cache.write().unwrap();
109        cache.remove(&cache_key);
110    }
111
112    /// Clear the entire cache
113    pub fn clear(&self) {
114        let mut cache = self.cache.write().unwrap();
115        cache.clear();
116        *self.hit_count.write().unwrap() = 0;
117        *self.miss_count.write().unwrap() = 0;
118    }
119
120    /// Get cache statistics
121    pub fn stats(&self) -> CacheStats {
122        let cache = self.cache.read().unwrap();
123        let hit_count = *self.hit_count.read().unwrap();
124        let miss_count = *self.miss_count.read().unwrap();
125
126        CacheStats {
127            size: cache.len(),
128            max_size: self.max_size,
129            hit_count,
130            miss_count,
131            hit_rate: if hit_count + miss_count > 0 {
132                hit_count as f64 / (hit_count + miss_count) as f64
133            } else {
134                0.0
135            },
136        }
137    }
138}
139
140/// Cache statistics
141#[derive(Debug, Clone)]
142pub struct CacheStats {
143    pub size: usize,
144    pub max_size: usize,
145    pub hit_count: u64,
146    pub miss_count: u64,
147    pub hit_rate: f64,
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use llm_config_core::{ConfigMetadata, ConfigValue, Environment};
154    use uuid::Uuid;
155
156    fn create_test_entry(namespace: &str, key: &str, env: Environment) -> ConfigEntry {
157        ConfigEntry {
158            id: Uuid::new_v4(),
159            namespace: namespace.to_string(),
160            key: key.to_string(),
161            value: ConfigValue::String("test-value".to_string()),
162            environment: env,
163            version: 1,
164            metadata: ConfigMetadata {
165                created_at: Utc::now(),
166                created_by: "test".to_string(),
167                updated_at: Utc::now(),
168                updated_by: "test".to_string(),
169                tags: vec![],
170                description: None,
171            },
172        }
173    }
174
175    #[test]
176    fn test_cache_creation() {
177        let cache = L1Cache::new(100);
178        let stats = cache.stats();
179        assert_eq!(stats.size, 0);
180        assert_eq!(stats.max_size, 100);
181    }
182
183    #[test]
184    fn test_put_and_get() {
185        let cache = L1Cache::new(100);
186        let entry = create_test_entry("ns", "key1", Environment::Development);
187
188        cache.put(entry.clone()).unwrap();
189
190        let retrieved = cache.get("ns", "key1", "development").unwrap();
191        assert_eq!(retrieved.id, entry.id);
192    }
193
194    #[test]
195    fn test_cache_miss() {
196        let cache = L1Cache::new(100);
197        let result = cache.get("ns", "nonexistent", "development");
198        assert!(result.is_err());
199    }
200
201    #[test]
202    fn test_lru_eviction() {
203        let cache = L1Cache::new(3);
204
205        // Fill cache
206        for i in 0..3 {
207            let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
208            cache.put(entry).unwrap();
209        }
210
211        // Access key1 to make it more recently used
212        cache.get("ns", "key1", "development").unwrap();
213
214        // Add new entry, should evict key0 (least recently used)
215        let entry = create_test_entry("ns", "key3", Environment::Development);
216        cache.put(entry).unwrap();
217
218        // key0 should be evicted
219        assert!(cache.get("ns", "key0", "development").is_err());
220
221        // key1, key2, key3 should still be present
222        assert!(cache.get("ns", "key1", "development").is_ok());
223        assert!(cache.get("ns", "key2", "development").is_ok());
224        assert!(cache.get("ns", "key3", "development").is_ok());
225    }
226
227    #[test]
228    fn test_invalidate() {
229        let cache = L1Cache::new(100);
230        let entry = create_test_entry("ns", "key1", Environment::Development);
231
232        cache.put(entry).unwrap();
233        assert!(cache.get("ns", "key1", "development").is_ok());
234
235        cache.invalidate("ns", "key1", "development");
236        assert!(cache.get("ns", "key1", "development").is_err());
237    }
238
239    #[test]
240    fn test_clear() {
241        let cache = L1Cache::new(100);
242
243        for i in 0..10 {
244            let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
245            cache.put(entry).unwrap();
246        }
247
248        assert_eq!(cache.stats().size, 10);
249
250        cache.clear();
251
252        assert_eq!(cache.stats().size, 0);
253    }
254
255    #[test]
256    fn test_cache_stats() {
257        let cache = L1Cache::new(100);
258        let entry = create_test_entry("ns", "key1", Environment::Development);
259
260        cache.put(entry).unwrap();
261
262        // Generate some hits and misses
263        cache.get("ns", "key1", "development").unwrap(); // hit
264        cache.get("ns", "key1", "development").unwrap(); // hit
265        let _ = cache.get("ns", "nonexistent", "development"); // miss
266
267        let stats = cache.stats();
268        assert_eq!(stats.hit_count, 2);
269        assert_eq!(stats.miss_count, 1);
270        assert!((stats.hit_rate - 0.666).abs() < 0.01);
271    }
272}