1pub mod cleanup;
7pub mod entry;
8pub mod key;
9pub mod storage;
10
11pub use cleanup::{CleanupManager, CleanupPolicy, CleanupStats, CleanupTrigger};
12pub use entry::{CacheEntry, CacheMetadata};
13pub use key::{hash_key, CACHE_VERSION};
14pub use storage::CacheStorage;
15
16use anyhow::Result;
17use std::path::Path;
18
19pub struct Cache {
21 storage: CacheStorage,
22 cleanup: CleanupManager,
23 enabled: bool,
24}
25
26impl Cache {
27 pub fn new<P: AsRef<Path>>(cache_dir: P) -> Result<Self> {
29 let cache_dir = cache_dir.as_ref().to_path_buf();
30 let storage = CacheStorage::new(&cache_dir)?;
31 let cleanup = CleanupManager::new(&cache_dir)?;
32
33 Ok(Self {
34 storage,
35 cleanup,
36 enabled: true,
37 })
38 }
39
40 pub fn with_cleanup_config<P: AsRef<Path>>(
42 cache_dir: P,
43 policy: CleanupPolicy,
44 trigger: CleanupTrigger,
45 ) -> Result<Self> {
46 let cache_dir = cache_dir.as_ref().to_path_buf();
47 let storage = CacheStorage::new(&cache_dir)?;
48 let cleanup = CleanupManager::with_config(&cache_dir, policy, trigger)?;
49
50 Ok(Self {
51 storage,
52 cleanup,
53 enabled: true,
54 })
55 }
56
57 pub fn disable(&mut self) {
59 self.enabled = false;
60 }
61
62 pub fn enable(&mut self) {
64 self.enabled = true;
65 }
66
67 pub fn is_enabled(&self) -> bool {
69 self.enabled
70 }
71
72 pub fn get(&self, namespace: &str, key: &str) -> Result<Option<String>> {
74 if !self.enabled {
75 return Ok(None);
76 }
77
78 log::debug!(
79 "Cache lookup: ns={}, key={}",
80 namespace,
81 &key[..key.len().min(8)]
82 );
83
84 if let Some(entry) = self.storage.get(namespace, key)? {
85 log::info!("Cache hit: {}", &key[..key.len().min(8)]);
86 Ok(Some(entry.value))
87 } else {
88 log::info!("Cache miss: {}", &key[..key.len().min(8)]);
89 Ok(None)
90 }
91 }
92
93 pub fn set(&self, namespace: &str, key: &str, value: &str, input_size: usize) -> Result<()> {
95 if !self.enabled {
96 return Ok(());
97 }
98
99 let entry = CacheEntry::new(
100 CACHE_VERSION.to_string(),
101 namespace.to_string(),
102 key.to_string(),
103 value.to_string(),
104 input_size,
105 );
106
107 self.storage.set(&entry)?;
108 log::info!(
109 "Cache stored: ns={}, key={}",
110 namespace,
111 &key[..key.len().min(8)]
112 );
113
114 Ok(())
115 }
116
117 pub fn should_cleanup_periodic(&self) -> Result<bool> {
119 self.cleanup.should_run_periodic_cleanup()
120 }
121
122 pub fn should_cleanup_size(&self) -> Result<bool> {
124 self.cleanup.is_over_size_limit()
125 }
126
127 pub fn cleanup_stale(&self) -> Result<CleanupStats> {
129 self.cleanup.cleanup_stale_entries()
130 }
131
132 pub fn cleanup_by_size(&self) -> Result<CleanupStats> {
134 self.cleanup.cleanup_by_size()
135 }
136
137 pub fn stats(&self) -> Result<CacheStats> {
139 let total_size = self.storage.total_size()?;
140 let entry_count = self.storage.entry_count()?;
141
142 Ok(CacheStats {
143 total_entries: entry_count,
144 total_size_bytes: total_size,
145 total_size_mb: total_size / 1_048_576,
146 })
147 }
148
149 pub fn clear_all(&self) -> Result<usize> {
151 self.storage.clear_all()
152 }
153
154 pub fn cache_dir(&self) -> &Path {
156 self.storage.cache_dir()
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct CacheStats {
163 pub total_entries: usize,
164 pub total_size_bytes: u64,
165 pub total_size_mb: u64,
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use tempfile::TempDir;
172
173 #[test]
174 fn test_cache_creation() {
175 let temp_dir = TempDir::new().unwrap();
176 let cache = Cache::new(temp_dir.path()).unwrap();
177 assert!(cache.is_enabled());
178 }
179
180 #[test]
181 fn test_cache_get_set() {
182 let temp_dir = TempDir::new().unwrap();
183 let cache = Cache::new(temp_dir.path()).unwrap();
184
185 let ns = "test-ns";
186 let key = "abc123def456";
187 let value = "test value";
188
189 let result = cache.get(ns, key).unwrap();
191 assert!(result.is_none());
192
193 cache.set(ns, key, value, 42).unwrap();
195
196 let result = cache.get(ns, key).unwrap();
198 assert_eq!(result, Some(value.to_string()));
199 }
200
201 #[test]
202 fn test_cache_disabled() {
203 let temp_dir = TempDir::new().unwrap();
204 let mut cache = Cache::new(temp_dir.path()).unwrap();
205
206 cache.disable();
207 assert!(!cache.is_enabled());
208
209 cache.set("ns", "key123", "value", 5).unwrap();
211
212 let result = cache.get("ns", "key123").unwrap();
214 assert!(result.is_none());
215
216 cache.enable();
218 assert!(cache.is_enabled());
219 }
220
221 #[test]
222 fn test_cache_stats() {
223 let temp_dir = TempDir::new().unwrap();
224 let cache = Cache::new(temp_dir.path()).unwrap();
225
226 cache.set("ns", "key1abc", "value1", 10).unwrap();
227 cache.set("ns", "key2def", "value2", 10).unwrap();
228
229 let stats = cache.stats().unwrap();
230 assert_eq!(stats.total_entries, 2);
231 assert!(stats.total_size_bytes > 0);
232 }
233
234 #[test]
235 fn test_cache_clear_all() {
236 let temp_dir = TempDir::new().unwrap();
237 let cache = Cache::new(temp_dir.path()).unwrap();
238
239 cache.set("ns", "key1abc", "value1", 10).unwrap();
240 cache.set("ns", "key2def", "value2", 10).unwrap();
241
242 let stats = cache.stats().unwrap();
243 assert_eq!(stats.total_entries, 2);
244
245 let removed = cache.clear_all().unwrap();
246 assert_eq!(removed, 2);
247
248 let stats = cache.stats().unwrap();
249 assert_eq!(stats.total_entries, 0);
250 }
251
252 #[test]
253 fn test_stats_mb_calculation() {
254 let temp_dir = TempDir::new().unwrap();
255 let cache = Cache::new(temp_dir.path()).unwrap();
256
257 let stats = cache.stats().unwrap();
258 assert_eq!(stats.total_size_bytes, 0);
259 assert_eq!(stats.total_size_mb, 0);
260
261 cache.set("ns", "key1abc", &"x".repeat(1000), 1000).unwrap();
262 let stats = cache.stats().unwrap();
263 assert!(stats.total_size_bytes > 0);
264 assert_eq!(stats.total_size_mb, stats.total_size_bytes / 1_048_576);
265 }
266
267 #[test]
268 fn test_should_cleanup_periodic_delegates() {
269 let temp_dir = TempDir::new().unwrap();
270 let cache = Cache::new(temp_dir.path()).unwrap();
271 let result = cache.should_cleanup_periodic().unwrap();
272 assert!(result);
273 }
274
275 #[test]
276 fn test_should_cleanup_size_delegates() {
277 let temp_dir = TempDir::new().unwrap();
278 let cache = Cache::new(temp_dir.path()).unwrap();
279 let result = cache.should_cleanup_size().unwrap();
280 assert!(!result);
281 }
282
283 #[test]
284 fn test_cleanup_stale_delegates() {
285 let temp_dir = TempDir::new().unwrap();
286 let cache = Cache::new(temp_dir.path()).unwrap();
287 let stats = cache.cleanup_stale().unwrap();
288 assert_eq!(stats.removed_count, 0);
289 assert_eq!(stats.freed_bytes, 0);
290 }
291
292 #[test]
293 fn test_cleanup_by_size_delegates() {
294 let temp_dir = TempDir::new().unwrap();
295 let cache = Cache::new(temp_dir.path()).unwrap();
296 let stats = cache.cleanup_by_size().unwrap();
297 assert_eq!(stats.removed_count, 0);
298 assert_eq!(stats.freed_bytes, 0);
299 }
300
301 #[test]
302 fn test_stats_mb_is_division_not_modulo() {
303 let temp_dir = TempDir::new().unwrap();
304 let cache = Cache::new(temp_dir.path()).unwrap();
305
306 cache.set("ns", "key1abc", &"x".repeat(1000), 1000).unwrap();
307 let stats = cache.stats().unwrap();
308
309 assert_eq!(stats.total_size_mb, 0);
310 assert!(stats.total_size_bytes > 0);
311 assert!(stats.total_size_mb < stats.total_size_bytes);
312 }
313
314 #[test]
315 fn test_with_cleanup_config() {
316 let temp_dir = TempDir::new().unwrap();
317 let policy = CleanupPolicy {
318 max_cache_size_mb: 100,
319 max_age_days: 30,
320 max_idle_days: 10,
321 remove_version_mismatch: true,
322 };
323 let trigger = CleanupTrigger::Manual;
324
325 let cache = Cache::with_cleanup_config(temp_dir.path(), policy, trigger).unwrap();
326 assert!(cache.is_enabled());
327 assert!(!cache.should_cleanup_periodic().unwrap());
328 }
329
330 #[test]
331 fn test_should_cleanup_size_with_data() {
332 let temp_dir = TempDir::new().unwrap();
333 let policy = CleanupPolicy::default();
334 let trigger = CleanupTrigger::OnSizeLimit { threshold_mb: 0 };
335
336 let cache = Cache::with_cleanup_config(temp_dir.path(), policy, trigger).unwrap();
337 cache.set("ns", "key1abc", "value", 5).unwrap();
338
339 assert!(cache.should_cleanup_size().unwrap());
340 }
341
342 #[test]
343 fn test_cleanup_stale_with_stale_data() {
344 let temp_dir = TempDir::new().unwrap();
345 let policy = CleanupPolicy {
346 max_cache_size_mb: 500,
347 max_age_days: 90,
348 max_idle_days: 30,
349 remove_version_mismatch: true,
350 };
351 let trigger = CleanupTrigger::Manual;
352
353 let cache = Cache::with_cleanup_config(temp_dir.path(), policy, trigger).unwrap();
354
355 let entry = CacheEntry::new(
356 "0.0.1".to_string(),
357 "ns".to_string(),
358 "stalekey".to_string(),
359 "resp".to_string(),
360 10,
361 );
362 let dir = temp_dir.path().join("ns").join("st");
363 std::fs::create_dir_all(&dir).unwrap();
364 std::fs::write(
365 dir.join("stalekey.json"),
366 serde_json::to_string(&entry).unwrap(),
367 )
368 .unwrap();
369
370 let stats = cache.cleanup_stale().unwrap();
371 assert_eq!(stats.removed_count, 1);
372 assert!(stats.freed_bytes > 0);
373 }
374
375 #[test]
376 fn test_cleanup_by_size_with_data() {
377 let temp_dir = TempDir::new().unwrap();
378 let policy = CleanupPolicy {
379 max_cache_size_mb: 0,
380 max_age_days: 90,
381 max_idle_days: 30,
382 remove_version_mismatch: true,
383 };
384 let trigger = CleanupTrigger::Manual;
385
386 let cache = Cache::with_cleanup_config(temp_dir.path(), policy, trigger).unwrap();
387
388 let entry = CacheEntry::new(
389 CACHE_VERSION.to_string(),
390 "ns".to_string(),
391 "sizekey".to_string(),
392 "resp".to_string(),
393 10,
394 );
395 let dir = temp_dir.path().join("ns").join("si");
396 std::fs::create_dir_all(&dir).unwrap();
397 std::fs::write(
398 dir.join("sizekey.json"),
399 serde_json::to_string(&entry).unwrap(),
400 )
401 .unwrap();
402
403 let stats = cache.cleanup_by_size().unwrap();
404 assert_eq!(stats.removed_count, 1);
405 assert!(stats.freed_bytes > 0);
406 }
407}