1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::Arc;
6use std::sync::RwLock;
7use std::time::Duration;
8
9use bytes::Bytes;
10
11use super::cached_file::CachedFile;
12use super::stats::CacheStats;
13
14#[derive(Debug, Clone)]
16pub struct CacheConfig {
17 pub max_entries: usize,
19 pub max_size: usize,
21 pub default_ttl: Duration,
23}
24
25impl Default for CacheConfig {
26 fn default() -> Self {
27 Self {
28 max_entries: 1000,
29 max_size: 100 * 1024 * 1024, default_ttl: Duration::from_secs(300), }
32 }
33}
34
35#[derive(Debug, Clone)]
37struct LruEntry {
38 file: CachedFile,
40}
41
42pub struct MemoryCache {
44 cache: RwLock<HashMap<PathBuf, LruEntry>>,
46 access_order: RwLock<Vec<PathBuf>>,
48 config: CacheConfig,
50 stats: Arc<CacheStats>,
52}
53
54impl MemoryCache {
55 pub fn new() -> Self {
57 Self::with_config(CacheConfig::default())
58 }
59
60 pub fn with_config(config: CacheConfig) -> Self {
62 Self {
63 cache: RwLock::new(HashMap::new()),
64 access_order: RwLock::new(Vec::new()),
65 config,
66 stats: Arc::new(CacheStats::new()),
67 }
68 }
69
70 pub fn get(&self, path: &PathBuf) -> Option<CachedFile> {
72 let cache = self.cache.read().unwrap();
73
74 if let Some(entry) = cache.get(path) {
75 if entry.file.is_expired() {
77 drop(cache);
78 self.stats.record_miss();
79 self.remove(path);
80 return None;
81 }
82
83 let file = entry.file.clone();
84 drop(cache);
85
86 self.touch(path);
88 self.stats.record_hit();
89
90 return Some(file);
91 }
92
93 self.stats.record_miss();
94 None
95 }
96
97 pub fn insert(&self, path: PathBuf, content: Bytes, mime_type: String, etag: String, last_modified: u64) {
99 self.insert_with_ttl(path, content, mime_type, etag, last_modified, self.config.default_ttl)
100 }
101
102 pub fn insert_with_ttl(
104 &self,
105 path: PathBuf,
106 content: Bytes,
107 mime_type: String,
108 etag: String,
109 last_modified: u64,
110 ttl: Duration,
111 ) {
112 let size = content.len();
113
114 self.evict_if_needed(size);
116
117 let file = CachedFile::new(content, mime_type, etag, last_modified, ttl);
118 let entry = LruEntry { file };
119
120 {
121 let mut cache = self.cache.write().unwrap();
122
123 if cache.contains_key(&path) {
125 if let Some(old) = cache.get(&path) {
126 self.stats.remove_entry(old.file.size);
127 }
128 cache.remove(&path);
129 self.remove_from_access_order(&path);
130 }
131
132 cache.insert(path.clone(), entry);
133 }
134
135 {
137 let mut order = self.access_order.write().unwrap();
138 order.push(path);
139 }
140
141 self.stats.add_entry(size);
142 }
143
144 pub fn remove(&self, path: &PathBuf) -> bool {
146 let mut cache = self.cache.write().unwrap();
147
148 if let Some(entry) = cache.remove(path) {
149 self.stats.remove_entry(entry.file.size);
150 drop(cache);
151 self.remove_from_access_order(path);
152 return true;
153 }
154
155 false
156 }
157
158 pub fn contains(&self, path: &PathBuf) -> bool {
160 let cache = self.cache.read().unwrap();
161 if let Some(entry) = cache.get(path) {
162 !entry.file.is_expired()
163 } else {
164 false
165 }
166 }
167
168 pub fn clear(&self) {
170 let mut cache = self.cache.write().unwrap();
171 let mut order = self.access_order.write().unwrap();
172
173 cache.clear();
174 order.clear();
175
176 self.stats.reset();
177 }
178
179 pub fn stats(&self) -> Arc<CacheStats> {
181 Arc::clone(&self.stats)
182 }
183
184 pub fn len(&self) -> usize {
186 self.cache.read().unwrap().len()
187 }
188
189 pub fn is_empty(&self) -> bool {
191 self.len() == 0
192 }
193
194 pub fn remove_expired(&self) -> usize {
196 let cache = self.cache.read().unwrap();
197 let expired: Vec<PathBuf> = cache
198 .iter()
199 .filter(|(_, entry)| entry.file.is_expired())
200 .map(|(path, _)| path.clone())
201 .collect();
202 drop(cache);
203
204 let count = expired.len();
205 for path in expired {
206 self.remove(&path);
207 self.stats.record_expired();
208 }
209
210 count
211 }
212
213 fn touch(&self, path: &PathBuf) {
215 self.remove_from_access_order(path);
216 let mut order = self.access_order.write().unwrap();
217 order.push(path.clone());
218 }
219
220 fn remove_from_access_order(&self, path: &PathBuf) {
222 let mut order = self.access_order.write().unwrap();
223 order.retain(|p| p != path);
224 }
225
226 fn evict_if_needed(&self, new_size: usize) {
228 while self.len() >= self.config.max_entries {
230 self.evict_one();
231 }
232
233 let current_size = self.stats.total_size() as usize;
235 while current_size + new_size > self.config.max_size && self.len() > 0 {
236 self.evict_one();
237 }
238 }
239
240 fn evict_one(&self) {
242 let path = {
243 let order = self.access_order.read().unwrap();
244 if order.is_empty() {
245 return;
246 }
247 order.first().unwrap().clone()
248 };
249
250 self.remove(&path);
251 self.stats.record_eviction();
252 }
253
254 pub fn config(&self) -> &CacheConfig {
256 &self.config
257 }
258}
259
260impl Default for MemoryCache {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn create_test_cache() -> MemoryCache {
271 MemoryCache::with_config(CacheConfig {
272 max_entries: 10,
273 max_size: 1024, default_ttl: Duration::from_secs(60),
275 })
276 }
277
278 fn make_path(s: &str) -> PathBuf {
279 PathBuf::from(s)
280 }
281
282 #[test]
283 fn test_cache_creation() {
284 let cache = MemoryCache::new();
285 assert!(cache.is_empty());
286 assert_eq!(cache.len(), 0);
287 }
288
289 #[test]
290 fn test_cache_with_config() {
291 let config = CacheConfig {
292 max_entries: 100,
293 max_size: 1024 * 1024,
294 default_ttl: Duration::from_secs(120),
295 };
296 let cache = MemoryCache::with_config(config);
297 assert_eq!(cache.config().max_entries, 100);
298 }
299
300 #[test]
301 fn test_insert_and_get() {
302 let cache = create_test_cache();
303 let path = make_path("/test.txt");
304
305 cache.insert(
306 path.clone(),
307 Bytes::from("Hello, World!"),
308 "text/plain".to_string(),
309 "\"etag1\"".to_string(),
310 1234567890,
311 );
312
313 assert_eq!(cache.len(), 1);
314
315 let cached = cache.get(&path).unwrap();
316 assert_eq!(cached.content, Bytes::from("Hello, World!"));
317 assert_eq!(cached.mime_type, "text/plain");
318 }
319
320 #[test]
321 fn test_get_nonexistent() {
322 let cache = create_test_cache();
323 let path = make_path("/nonexistent.txt");
324
325 let result = cache.get(&path);
326 assert!(result.is_none());
327 }
328
329 #[test]
330 fn test_remove() {
331 let cache = create_test_cache();
332 let path = make_path("/test.txt");
333
334 cache.insert(
335 path.clone(),
336 Bytes::from("test"),
337 "text/plain".to_string(),
338 "\"etag\"".to_string(),
339 0,
340 );
341
342 assert_eq!(cache.len(), 1);
343
344 let removed = cache.remove(&path);
345 assert!(removed);
346 assert!(cache.is_empty());
347 }
348
349 #[test]
350 fn test_remove_nonexistent() {
351 let cache = create_test_cache();
352 let path = make_path("/nonexistent.txt");
353
354 let removed = cache.remove(&path);
355 assert!(!removed);
356 }
357
358 #[test]
359 fn test_contains() {
360 let cache = create_test_cache();
361 let path = make_path("/test.txt");
362
363 assert!(!cache.contains(&path));
364
365 cache.insert(
366 path.clone(),
367 Bytes::from("test"),
368 "text/plain".to_string(),
369 "\"etag\"".to_string(),
370 0,
371 );
372
373 assert!(cache.contains(&path));
374 }
375
376 #[test]
377 fn test_clear() {
378 let cache = create_test_cache();
379
380 for i in 0..5 {
381 let path = make_path(&format!("/file{}.txt", i));
382 cache.insert(
383 path,
384 Bytes::from("test"),
385 "text/plain".to_string(),
386 "\"etag\"".to_string(),
387 0,
388 );
389 }
390
391 assert_eq!(cache.len(), 5);
392
393 cache.clear();
394
395 assert!(cache.is_empty());
396 }
397
398 #[test]
399 fn test_stats_hits_and_misses() {
400 let cache = create_test_cache();
401 let path = make_path("/test.txt");
402
403 cache.get(&path);
405 assert_eq!(cache.stats().misses(), 1);
406
407 cache.insert(
409 path.clone(),
410 Bytes::from("test"),
411 "text/plain".to_string(),
412 "\"etag\"".to_string(),
413 0,
414 );
415
416 cache.get(&path);
418 assert_eq!(cache.stats().hits(), 1);
419 assert_eq!(cache.stats().misses(), 1);
420 }
421
422 #[test]
423 fn test_lru_eviction_by_count() {
424 let cache = create_test_cache();
425
426 for i in 0..10 {
428 let path = make_path(&format!("/file{}.txt", i));
429 cache.insert(
430 path,
431 Bytes::from("x"),
432 "text/plain".to_string(),
433 format!("\"etag{}\"", i),
434 0,
435 );
436 }
437
438 assert_eq!(cache.len(), 10);
439
440 let new_path = make_path("/new.txt");
442 cache.insert(
443 new_path.clone(),
444 Bytes::from("y"),
445 "text/plain".to_string(),
446 "\"newetag\"".to_string(),
447 0,
448 );
449
450 assert_eq!(cache.len(), 10);
451
452 let first_path = make_path("/file0.txt");
454 assert!(!cache.contains(&first_path));
455
456 assert!(cache.contains(&new_path));
458
459 assert!(cache.stats().evictions() > 0);
461 }
462
463 #[test]
464 fn test_lru_access_order() {
465 let cache = create_test_cache();
466
467 let path1 = make_path("/file1.txt");
469 let path2 = make_path("/file2.txt");
470 let path3 = make_path("/file3.txt");
471
472 cache.insert(path1.clone(), Bytes::from("a"), "text/plain".to_string(), "\"e1\"".to_string(), 0);
473 cache.insert(path2.clone(), Bytes::from("b"), "text/plain".to_string(), "\"e2\"".to_string(), 0);
474 cache.insert(path3.clone(), Bytes::from("c"), "text/plain".to_string(), "\"e3\"".to_string(), 0);
475
476 cache.get(&path1);
478
479 for i in 4..=12 {
481 let path = make_path(&format!("/file{}.txt", i));
482 cache.insert(path, Bytes::from("x"), "text/plain".to_string(), format!("\"e{}\"", i), 0);
483 }
484
485 assert!(cache.contains(&path1));
487 }
488
489 #[test]
490 fn test_size_eviction() {
491 let config = CacheConfig {
492 max_entries: 100,
493 max_size: 10, default_ttl: Duration::from_secs(60),
495 };
496 let cache = MemoryCache::with_config(config);
497
498 let path1 = make_path("/file1.txt");
500 cache.insert(path1.clone(), Bytes::from("12345"), "text/plain".to_string(), "\"e1\"".to_string(), 0);
501
502 assert_eq!(cache.len(), 1);
503
504 let path2 = make_path("/file2.txt");
506 cache.insert(path2.clone(), Bytes::from("67890"), "text/plain".to_string(), "\"e2\"".to_string(), 0);
507
508 assert_eq!(cache.len(), 2);
510
511 let path3 = make_path("/file3.txt");
513 cache.insert(path3.clone(), Bytes::from("123456"), "text/plain".to_string(), "\"e3\"".to_string(), 0);
514
515 assert!(!cache.contains(&path1));
517 }
518
519 #[test]
520 fn test_expired_entry() {
521 let cache = create_test_cache();
522 let path = make_path("/test.txt");
523
524 cache.insert_with_ttl(
526 path.clone(),
527 Bytes::from("test"),
528 "text/plain".to_string(),
529 "\"etag\"".to_string(),
530 0,
531 Duration::from_millis(1),
532 );
533
534 std::thread::sleep(Duration::from_millis(10));
536
537 let result = cache.get(&path);
539 assert!(result.is_none());
540 }
541
542 #[test]
543 fn test_remove_expired() {
544 let cache = create_test_cache();
545
546 let path1 = make_path("/long.txt");
548 let path2 = make_path("/short.txt");
549
550 cache.insert_with_ttl(
551 path1.clone(),
552 Bytes::from("test1"),
553 "text/plain".to_string(),
554 "\"etag1\"".to_string(),
555 0,
556 Duration::from_secs(60),
557 );
558
559 cache.insert_with_ttl(
560 path2.clone(),
561 Bytes::from("test2"),
562 "text/plain".to_string(),
563 "\"etag2\"".to_string(),
564 0,
565 Duration::from_millis(1),
566 );
567
568 std::thread::sleep(Duration::from_millis(10));
570
571 let removed = cache.remove_expired();
573
574 assert_eq!(removed, 1);
575 assert!(cache.contains(&path1));
576 assert!(!cache.contains(&path2));
577 }
578
579 #[test]
580 fn test_update_existing_entry() {
581 let cache = create_test_cache();
582 let path = make_path("/test.txt");
583
584 cache.insert(
585 path.clone(),
586 Bytes::from("original"),
587 "text/plain".to_string(),
588 "\"etag1\"".to_string(),
589 0,
590 );
591
592 assert_eq!(cache.len(), 1);
593
594 cache.insert(
595 path.clone(),
596 Bytes::from("updated"),
597 "text/plain".to_string(),
598 "\"etag2\"".to_string(),
599 0,
600 );
601
602 assert_eq!(cache.len(), 1);
603
604 let cached = cache.get(&path).unwrap();
605 assert_eq!(cached.content, Bytes::from("updated"));
606 assert_eq!(cached.etag, "\"etag2\"");
607 }
608
609 #[test]
610 fn test_stats_evictions() {
611 let cache = create_test_cache();
612
613 for i in 0..10 {
615 let path = make_path(&format!("/file{}.txt", i));
616 cache.insert(
617 path,
618 Bytes::from("x"),
619 "text/plain".to_string(),
620 format!("\"etag{}\"", i),
621 0,
622 );
623 }
624
625 let path = make_path("/overflow.txt");
627 cache.insert(path, Bytes::from("y"), "text/plain".to_string(), "\"overflow\"".to_string(), 0);
628
629 assert!(cache.stats().evictions() > 0);
630 }
631
632 #[test]
633 fn test_concurrent_access() {
634 use std::thread;
635
636 let cache = Arc::new(create_test_cache());
637 let mut handles = vec![];
638
639 for i in 0..5 {
641 let cache_clone = Arc::clone(&cache);
642 handles.push(thread::spawn(move || {
643 let path = make_path(&format!("/file{}.txt", i));
644 cache_clone.insert(
645 path,
646 Bytes::from(format!("content{}", i)),
647 "text/plain".to_string(),
648 format!("\"etag{}\"", i),
649 0,
650 );
651 }));
652 }
653
654 for handle in handles {
655 handle.join().unwrap();
656 }
657
658 assert_eq!(cache.len(), 5);
659 }
660
661 #[test]
662 fn test_concurrent_reads() {
663 use std::thread;
664
665 let cache = Arc::new(create_test_cache());
666
667 let path = make_path("/test.txt");
669 cache.insert(
670 path.clone(),
671 Bytes::from("test content"),
672 "text/plain".to_string(),
673 "\"etag\"".to_string(),
674 0,
675 );
676
677 let mut handles = vec![];
678
679 for _ in 0..10 {
681 let cache_clone = Arc::clone(&cache);
682 let path_clone = path.clone();
683 handles.push(thread::spawn(move || {
684 cache_clone.get(&path_clone)
685 }));
686 }
687
688 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
689
690 assert!(results.iter().all(|r| r.is_some()));
692 assert_eq!(cache.stats().hits(), 10);
693 }
694
695 #[test]
696 fn test_stats_total_size() {
697 let cache = create_test_cache();
698
699 cache.insert(
700 make_path("/file1.txt"),
701 Bytes::from("12345"), "text/plain".to_string(),
703 "\"e1\"".to_string(),
704 0,
705 );
706
707 assert_eq!(cache.stats().total_size(), 5);
708
709 cache.insert(
710 make_path("/file2.txt"),
711 Bytes::from("67890"), "text/plain".to_string(),
713 "\"e2\"".to_string(),
714 0,
715 );
716
717 assert_eq!(cache.stats().total_size(), 10);
718 }
719
720 #[test]
721 fn test_hit_rate_calculation() {
722 let cache = create_test_cache();
723 let path = make_path("/test.txt");
724
725 cache.get(&path);
727 cache.get(&path);
728
729 cache.insert(
731 path.clone(),
732 Bytes::from("test"),
733 "text/plain".to_string(),
734 "\"etag\"".to_string(),
735 0,
736 );
737
738 cache.get(&path);
740 cache.get(&path);
741 cache.get(&path);
742
743 let expected = 3.0 / 5.0;
745 assert!((cache.stats().hit_rate() - expected).abs() < 0.001);
746 }
747}