1use crate::{CacheError, Result};
4use llm_config_core::ConfigEntry;
5use std::collections::HashMap;
6use std::fs::{self, File};
7use std::io::{BufReader, BufWriter, Write};
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, RwLock};
10
11pub struct L2Cache {
13 cache_dir: PathBuf,
14 index: Arc<RwLock<HashMap<String, PathBuf>>>,
15}
16
17impl L2Cache {
18 pub fn new(cache_dir: impl AsRef<Path>) -> Result<Self> {
20 let cache_dir = cache_dir.as_ref();
21 fs::create_dir_all(cache_dir)?;
22
23 let cache = Self {
24 cache_dir: cache_dir.to_path_buf(),
25 index: Arc::new(RwLock::new(HashMap::new())),
26 };
27
28 cache.rebuild_index()?;
30
31 Ok(cache)
32 }
33
34 fn cache_key(namespace: &str, key: &str, env: &str) -> String {
36 format!("{}:{}:{}", namespace, key, env)
37 }
38
39 fn cache_file_path(&self, cache_key: &str) -> PathBuf {
41 let encoded = hex::encode(cache_key.as_bytes());
43 self.cache_dir.join(format!("{}.cache", encoded))
44 }
45
46 fn rebuild_index(&self) -> Result<()> {
48 let mut index = self.index.write().unwrap();
49 index.clear();
50
51 if !self.cache_dir.exists() {
52 return Ok(());
53 }
54
55 for entry in fs::read_dir(&self.cache_dir)? {
56 let entry = entry?;
57 let path = entry.path();
58
59 if path.extension().and_then(|s| s.to_str()) == Some("cache") {
60 if let Ok(file) = File::open(&path) {
62 let reader = BufReader::new(file);
63 if let Ok(cached_entry) = serde_json::from_reader::<_, ConfigEntry>(reader) {
64 let cache_key = Self::cache_key(
65 &cached_entry.namespace,
66 &cached_entry.key,
67 &cached_entry.environment.to_string(),
68 );
69 index.insert(cache_key, path);
70 }
71 }
72 }
73 }
74
75 Ok(())
76 }
77
78 pub fn get(&self, namespace: &str, key: &str, env: &str) -> Result<ConfigEntry> {
80 let cache_key = Self::cache_key(namespace, key, env);
81
82 let index = self.index.read().unwrap();
83
84 if let Some(path) = index.get(&cache_key) {
85 let file = File::open(path)?;
86 let reader = BufReader::new(file);
87 let entry = serde_json::from_reader(reader)
88 .map_err(|e| CacheError::Serialization(e.to_string()))?;
89 Ok(entry)
90 } else {
91 Err(CacheError::CacheMiss(cache_key))
92 }
93 }
94
95 pub fn put(&self, entry: &ConfigEntry) -> Result<()> {
97 let cache_key = Self::cache_key(&entry.namespace, &entry.key, &entry.environment.to_string());
98 let path = self.cache_file_path(&cache_key);
99
100 let temp_path = path.with_extension("tmp");
102 {
103 let file = File::create(&temp_path)?;
104 let mut writer = BufWriter::new(file);
105 serde_json::to_writer(&mut writer, entry)
106 .map_err(|e| CacheError::Serialization(e.to_string()))?;
107 writer.flush()?;
108 }
109
110 fs::rename(&temp_path, &path)?;
112
113 let mut index = self.index.write().unwrap();
115 index.insert(cache_key, path);
116
117 Ok(())
118 }
119
120 pub fn invalidate(&self, namespace: &str, key: &str, env: &str) -> Result<()> {
122 let cache_key = Self::cache_key(namespace, key, env);
123
124 let mut index = self.index.write().unwrap();
125
126 if let Some(path) = index.remove(&cache_key) {
127 let _ = fs::remove_file(path); }
129
130 Ok(())
131 }
132
133 pub fn clear(&self) -> Result<()> {
135 let mut index = self.index.write().unwrap();
136 index.clear();
137
138 if self.cache_dir.exists() {
140 for entry in fs::read_dir(&self.cache_dir)? {
141 let entry = entry?;
142 let path = entry.path();
143 if path.extension().and_then(|s| s.to_str()) == Some("cache") {
144 let _ = fs::remove_file(path);
145 }
146 }
147 }
148
149 Ok(())
150 }
151
152 pub fn size(&self) -> usize {
154 self.index.read().unwrap().len()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use llm_config_core::{ConfigMetadata, ConfigValue, Environment};
162 use tempfile::TempDir;
163 use uuid::Uuid;
164
165 fn create_test_entry(namespace: &str, key: &str, env: Environment) -> ConfigEntry {
166 ConfigEntry {
167 id: Uuid::new_v4(),
168 namespace: namespace.to_string(),
169 key: key.to_string(),
170 value: ConfigValue::String("test-value".to_string()),
171 environment: env,
172 version: 1,
173 metadata: ConfigMetadata {
174 created_at: chrono::Utc::now(),
175 created_by: "test".to_string(),
176 updated_at: chrono::Utc::now(),
177 updated_by: "test".to_string(),
178 tags: vec![],
179 description: None,
180 },
181 }
182 }
183
184 #[test]
185 fn test_l2_creation() {
186 let temp_dir = TempDir::new().unwrap();
187 let cache = L2Cache::new(temp_dir.path()).unwrap();
188 assert_eq!(cache.size(), 0);
189 }
190
191 #[test]
192 fn test_put_and_get() {
193 let temp_dir = TempDir::new().unwrap();
194 let cache = L2Cache::new(temp_dir.path()).unwrap();
195
196 let entry = create_test_entry("ns", "key1", Environment::Development);
197 cache.put(&entry).unwrap();
198
199 let retrieved = cache.get("ns", "key1", "development").unwrap();
200 assert_eq!(retrieved.id, entry.id);
201 }
202
203 #[test]
204 fn test_cache_miss() {
205 let temp_dir = TempDir::new().unwrap();
206 let cache = L2Cache::new(temp_dir.path()).unwrap();
207
208 let result = cache.get("ns", "nonexistent", "development");
209 assert!(result.is_err());
210 }
211
212 #[test]
213 fn test_persistence() {
214 let temp_dir = TempDir::new().unwrap();
215 let entry = create_test_entry("ns", "key1", Environment::Development);
216
217 {
219 let cache = L2Cache::new(temp_dir.path()).unwrap();
220 cache.put(&entry).unwrap();
221 }
222
223 {
225 let cache = L2Cache::new(temp_dir.path()).unwrap();
226 let retrieved = cache.get("ns", "key1", "development").unwrap();
227 assert_eq!(retrieved.id, entry.id);
228 }
229 }
230
231 #[test]
232 fn test_invalidate() {
233 let temp_dir = TempDir::new().unwrap();
234 let cache = L2Cache::new(temp_dir.path()).unwrap();
235
236 let entry = create_test_entry("ns", "key1", Environment::Development);
237 cache.put(&entry).unwrap();
238
239 assert!(cache.get("ns", "key1", "development").is_ok());
240
241 cache.invalidate("ns", "key1", "development").unwrap();
242
243 assert!(cache.get("ns", "key1", "development").is_err());
244 }
245
246 #[test]
247 fn test_clear() {
248 let temp_dir = TempDir::new().unwrap();
249 let cache = L2Cache::new(temp_dir.path()).unwrap();
250
251 for i in 0..10 {
252 let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
253 cache.put(&entry).unwrap();
254 }
255
256 assert_eq!(cache.size(), 10);
257
258 cache.clear().unwrap();
259
260 assert_eq!(cache.size(), 0);
261 }
262}