1use crate::{CacheError, Result};
4use chrono::{DateTime, Utc};
5use llm_config_core::ConfigEntry;
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8
9#[derive(Debug, Clone)]
11struct CachedEntry {
12 entry: ConfigEntry,
13 accessed_at: DateTime<Utc>,
14 access_count: u64,
15}
16
17pub 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 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 fn cache_key(namespace: &str, key: &str, env: &str) -> String {
38 format!("{}:{}:{}", namespace, key, env)
39 }
40
41 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 cached.accessed_at = Utc::now();
50 cached.access_count += 1;
51
52 *self.hit_count.write().unwrap() += 1;
54
55 Ok(cached.entry.clone())
56 } else {
57 *self.miss_count.write().unwrap() += 1;
59
60 Err(CacheError::CacheMiss(cache_key))
61 }
62 }
63
64 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 if cache.len() >= self.max_size && !cache.contains_key(&cache_key) {
72 self.evict_lru(&mut cache)?;
73 }
74
75 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 fn evict_lru(&self, cache: &mut HashMap<String, CachedEntry>) -> Result<()> {
90 if cache.is_empty() {
91 return Ok(());
92 }
93
94 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 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 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 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#[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 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 cache.get("ns", "key1", "development").unwrap();
213
214 let entry = create_test_entry("ns", "key3", Environment::Development);
216 cache.put(entry).unwrap();
217
218 assert!(cache.get("ns", "key0", "development").is_err());
220
221 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 cache.get("ns", "key1", "development").unwrap(); cache.get("ns", "key1", "development").unwrap(); let _ = cache.get("ns", "nonexistent", "development"); 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}