1use crate::value::Value;
50use std::collections::HashMap;
51use std::sync::RwLock;
52use std::time::{Duration, Instant};
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct CacheKey {
66 pub table: String,
68 pub kind: CacheKeyKind,
70 pub identifier: String,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76pub enum CacheKeyKind {
77 ByPk,
79 ByQuery,
81 ByRelation,
83}
84
85impl CacheKey {
86 pub fn by_pk(table: impl Into<String>, pk: impl std::fmt::Display) -> Self {
88 Self {
89 table: table.into(),
90 kind: CacheKeyKind::ByPk,
91 identifier: pk.to_string(),
92 }
93 }
94
95 pub fn by_query(table: impl Into<String>, query_hash: impl std::fmt::Display) -> Self {
97 Self {
98 table: table.into(),
99 kind: CacheKeyKind::ByQuery,
100 identifier: query_hash.to_string(),
101 }
102 }
103
104 pub fn by_relation(table: impl Into<String>, relation: impl std::fmt::Display) -> Self {
106 Self {
107 table: table.into(),
108 kind: CacheKeyKind::ByRelation,
109 identifier: relation.to_string(),
110 }
111 }
112
113 pub fn to_string_key(&self) -> String {
115 let kind_str = match self.kind {
116 CacheKeyKind::ByPk => "pk",
117 CacheKeyKind::ByQuery => "q",
118 CacheKeyKind::ByRelation => "rel",
119 };
120 format!("l2:{}:{}:{}", self.table, kind_str, self.identifier)
121 }
122}
123
124impl std::fmt::Display for CacheKey {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.to_string_key())
127 }
128}
129
130#[derive(Debug, Clone, Default)]
136pub struct L2CacheStats {
137 pub hits: u64,
139 pub misses: u64,
141 pub sets: u64,
143 pub evictions: u64,
145 pub size: usize,
147}
148
149impl L2CacheStats {
150 pub fn total_lookups(&self) -> u64 {
152 self.hits + self.misses
153 }
154
155 pub fn hit_rate(&self) -> f64 {
157 let total = self.total_lookups();
158 if total == 0 {
159 0.0
160 } else {
161 self.hits as f64 / total as f64
162 }
163 }
164
165 pub fn miss_rate(&self) -> f64 {
167 1.0 - self.hit_rate()
168 }
169
170 pub fn merge(&mut self, other: &L2CacheStats) {
172 self.hits += other.hits;
173 self.misses += other.misses;
174 self.sets += other.sets;
175 self.evictions += other.evictions;
176 self.size += other.size;
177 }
178}
179
180#[derive(Debug, Clone)]
186struct CacheEntry {
187 value: Value,
189 expires_at: Option<Instant>,
191}
192
193impl CacheEntry {
194 fn new(value: Value, ttl: Option<Duration>) -> Self {
195 let expires_at = ttl.and_then(|d| {
198 if d == Duration::MAX {
199 None
200 } else {
201 Some(Instant::now() + d)
202 }
203 });
204 Self { value, expires_at }
205 }
206
207 fn is_expired(&self) -> bool {
208 self.expires_at
209 .map(|t| t <= Instant::now())
210 .unwrap_or(false)
211 }
212}
213
214pub struct L2Cache {
243 data: RwLock<HashMap<String, CacheEntry>>,
245 table_index: RwLock<HashMap<String, Vec<String>>>,
247 access_order: RwLock<Vec<String>>,
254 stats: RwLock<L2CacheStats>,
256 default_ttl: Option<Duration>,
258 max_size: usize,
260}
261
262impl Default for L2Cache {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268impl L2Cache {
269 pub fn new() -> Self {
271 Self {
272 data: RwLock::new(HashMap::new()),
273 table_index: RwLock::new(HashMap::new()),
274 access_order: RwLock::new(Vec::new()),
275 stats: RwLock::new(L2CacheStats::default()),
276 default_ttl: None,
277 max_size: 10_000,
278 }
279 }
280
281 pub fn with_default_ttl(mut self, ttl: Duration) -> Self {
283 self.default_ttl = Some(ttl);
284 self
285 }
286
287 pub fn with_max_size(mut self, max_size: usize) -> Self {
289 self.max_size = max_size;
290 self
291 }
292
293 pub fn put(&self, key: &CacheKey, value: Value, ttl: Option<Duration>) {
301 let actual_ttl = ttl.or(self.default_ttl);
302 let entry = CacheEntry::new(value, actual_ttl);
303 let key_str = key.to_string_key();
304
305 let is_new_key = {
307 let mut data = self.data.write().unwrap();
308 let exists = data.contains_key(&key_str);
309 if !exists && data.len() >= self.max_size {
310 let victim = {
312 let order = self.access_order.read().unwrap();
314 order
316 .iter()
317 .find(|k| data.get(*k).map(|e| e.is_expired()).unwrap_or(false))
318 .cloned()
319 .or_else(|| order.first().cloned())
320 };
321 if let Some(victim) = victim {
322 data.remove(&victim);
323 let mut order = self.access_order.write().unwrap();
325 order.retain(|k| k != &victim);
326 }
327 }
328 data.insert(key_str.clone(), entry);
329 !exists
330 };
331
332 {
334 let mut order = self.access_order.write().unwrap();
335 if is_new_key {
336 order.push(key_str.clone());
337 } else {
338 order.retain(|k| k != &key_str);
340 order.push(key_str.clone());
341 }
342 }
343
344 {
346 let mut idx = self.table_index.write().unwrap();
347 let keys = idx.entry(key.table.clone()).or_default();
348 if !keys.contains(&key_str) {
349 keys.push(key_str);
350 }
351 }
352
353 {
355 let mut stats = self.stats.write().unwrap();
356 stats.sets += 1;
357 }
358 }
359
360 pub fn get(&self, key: &CacheKey) -> Option<Value> {
364 let key_str = key.to_string_key();
365 let result = {
366 let data = self.data.read().ok()?;
367 if let Some(entry) = data.get(&key_str) {
368 if entry.is_expired() {
369 None
370 } else {
371 Some(entry.value.clone())
372 }
373 } else {
374 None
375 }
376 };
377
378 if result.is_some() {
380 let mut order = self.access_order.write().unwrap();
381 order.retain(|k| k != &key_str);
382 order.push(key_str);
383 }
384
385 if let Ok(mut stats) = self.stats.write() {
387 if result.is_some() {
388 stats.hits += 1;
389 } else {
390 stats.misses += 1;
391 }
392 }
393
394 result
395 }
396
397 pub fn invalidate(&self, key: &CacheKey) {
399 let key_str = key.to_string_key();
400 let removed = {
401 let mut data = self.data.write().unwrap();
402 data.remove(&key_str).is_some()
403 };
404 if removed {
405 let mut order = self.access_order.write().unwrap();
406 order.retain(|k| k != &key_str);
407 }
408 if removed {
409 let mut stats = self.stats.write().unwrap();
410 stats.evictions += 1;
411 }
412 }
413
414 pub fn invalidate_table(&self, table: &str) {
418 let keys_to_remove: Vec<String> = {
419 let idx = match self.table_index.read() {
420 Ok(i) => i,
421 Err(_) => return,
422 };
423 idx.get(table).cloned().unwrap_or_default()
424 };
425
426 let mut actually_removed: usize = 0;
427 {
428 let mut data = self.data.write().unwrap();
429 for k in &keys_to_remove {
430 if data.remove(k).is_some() {
431 actually_removed += 1;
432 }
433 }
434 }
435
436 if actually_removed > 0 {
437 let mut order = self.access_order.write().unwrap();
438 order.retain(|k| !keys_to_remove.contains(k));
439 }
440
441 if let Ok(mut idx) = self.table_index.write() {
442 idx.remove(table);
443 }
444 if actually_removed > 0 {
445 let mut stats = self.stats.write().unwrap();
446 stats.evictions += actually_removed as u64;
447 }
448 }
449
450 pub fn clear(&self) {
452 let removed = {
453 let mut data = self.data.write().unwrap();
454 let n = data.len();
455 data.clear();
456 n
457 };
458 if let Ok(mut order) = self.access_order.write() {
459 order.clear();
460 }
461 if let Ok(mut idx) = self.table_index.write() {
462 idx.clear();
463 }
464 if removed > 0 {
465 let mut stats = self.stats.write().unwrap();
466 stats.evictions += removed as u64;
467 stats.size = 0;
468 }
469 }
470
471 pub fn size(&self) -> usize {
473 self.data.read().map(|d| d.len()).unwrap_or(0)
474 }
475
476 pub fn stats(&self) -> L2CacheStats {
478 let mut s = self.stats.read().map(|s| s.clone()).unwrap_or_default();
479 s.size = self.size();
481 s
482 }
483
484 pub fn reset_stats(&self) {
486 if let Ok(mut stats) = self.stats.write() {
487 *stats = L2CacheStats::default();
488 }
489 }
490
491 pub fn contains(&self, key: &CacheKey) -> bool {
493 let key_str = key.to_string_key();
494 self.data
495 .read()
496 .map(|d| d.get(&key_str).map(|e| !e.is_expired()).unwrap_or(false))
497 .unwrap_or(false)
498 }
499
500 pub fn evict_expired(&self) -> usize {
502 let expired_keys: Vec<String> = {
503 let data = self.data.read().unwrap();
504 data.iter()
505 .filter(|(_, e)| e.is_expired())
506 .map(|(k, _)| k.clone())
507 .collect()
508 };
509
510 let mut removed = 0;
511 if !expired_keys.is_empty() {
512 let mut data = self.data.write().unwrap();
513 for k in &expired_keys {
514 if data.remove(k).is_some() {
515 removed += 1;
516 }
517 }
518 }
519
520 if removed > 0 {
521 let mut order = self.access_order.write().unwrap();
522 order.retain(|k| !expired_keys.contains(k));
523 let mut stats = self.stats.write().unwrap();
524 stats.evictions += removed as u64;
525 }
526 removed
527 }
528}
529
530#[cfg(test)]
535mod tests {
536 use super::*;
537 use crate::Value;
538 use std::thread;
539 use std::time::Duration;
540
541 #[test]
544 fn test_cache_key_by_pk() {
545 let key = CacheKey::by_pk("users", 1);
546 assert_eq!(key.table, "users");
547 assert_eq!(key.kind, CacheKeyKind::ByPk);
548 assert_eq!(key.identifier, "1");
549 assert_eq!(key.to_string_key(), "l2:users:pk:1");
550 }
551
552 #[test]
553 fn test_cache_key_by_query() {
554 let key = CacheKey::by_query("orders", "abc123");
555 assert_eq!(key.kind, CacheKeyKind::ByQuery);
556 assert_eq!(key.to_string_key(), "l2:orders:q:abc123");
557 }
558
559 #[test]
560 fn test_cache_key_by_relation() {
561 let key = CacheKey::by_relation("users", "posts:1");
562 assert_eq!(key.kind, CacheKeyKind::ByRelation);
563 assert_eq!(key.to_string_key(), "l2:users:rel:posts:1");
564 }
565
566 #[test]
567 fn test_cache_key_equality() {
568 let k1 = CacheKey::by_pk("users", 1);
569 let k2 = CacheKey::by_pk("users", 1);
570 let k3 = CacheKey::by_pk("users", 2);
571 assert_eq!(k1, k2);
572 assert_ne!(k1, k3);
573 }
574
575 #[test]
576 fn test_cache_key_display() {
577 let key = CacheKey::by_pk("users", 42);
578 assert_eq!(format!("{}", key), "l2:users:pk:42");
579 }
580
581 #[test]
584 fn test_stats_hit_rate_empty() {
585 let stats = L2CacheStats::default();
586 assert_eq!(stats.hit_rate(), 0.0);
587 assert_eq!(stats.total_lookups(), 0);
588 }
589
590 #[test]
591 fn test_stats_hit_rate_calculation() {
592 let stats = L2CacheStats {
593 hits: 80,
594 misses: 20,
595 ..Default::default()
596 };
597 assert_eq!(stats.total_lookups(), 100);
598 assert!((stats.hit_rate() - 0.8).abs() < 0.001);
599 assert!((stats.miss_rate() - 0.2).abs() < 0.001);
600 }
601
602 #[test]
603 fn test_stats_merge() {
604 let mut s1 = L2CacheStats {
605 hits: 10,
606 misses: 5,
607 sets: 15,
608 evictions: 2,
609 size: 100,
610 };
611 let s2 = L2CacheStats {
612 hits: 20,
613 misses: 10,
614 sets: 30,
615 evictions: 5,
616 size: 200,
617 };
618 s1.merge(&s2);
619 assert_eq!(s1.hits, 30);
620 assert_eq!(s1.misses, 15);
621 assert_eq!(s1.sets, 45);
622 assert_eq!(s1.evictions, 7);
623 assert_eq!(s1.size, 300);
624 }
625
626 #[test]
629 fn test_put_and_get() {
630 let cache = L2Cache::new();
631 let key = CacheKey::by_pk("users", 1);
632
633 cache.put(&key, Value::String("Alice".to_string()), None);
634 let val = cache.get(&key);
635 assert_eq!(val, Some(Value::String("Alice".to_string())));
636 }
637
638 #[test]
639 fn test_get_missing_returns_none() {
640 let cache = L2Cache::new();
641 let key = CacheKey::by_pk("users", 999);
642 assert_eq!(cache.get(&key), None);
643 }
644
645 #[test]
646 fn test_overwrite_existing_key() {
647 let cache = L2Cache::new();
648 let key = CacheKey::by_pk("users", 1);
649
650 cache.put(&key, Value::String("Alice".to_string()), None);
651 cache.put(&key, Value::String("Bob".to_string()), None);
652 assert_eq!(cache.get(&key), Some(Value::String("Bob".to_string())));
653 }
654
655 #[test]
656 fn test_invalidate_single_key() {
657 let cache = L2Cache::new();
658 let key = CacheKey::by_pk("users", 1);
659
660 cache.put(&key, Value::I64(42), None);
661 assert!(cache.get(&key).is_some());
662
663 cache.invalidate(&key);
664 assert!(cache.get(&key).is_none());
665 }
666
667 #[test]
670 fn test_invalidate_table_removes_all_entries_for_table() {
671 let cache = L2Cache::new();
672
673 let k1 = CacheKey::by_pk("users", 1);
674 let k2 = CacheKey::by_pk("users", 2);
675 let k3 = CacheKey::by_query("users", "hash1");
676 let k4 = CacheKey::by_pk("orders", 1); cache.put(&k1, Value::I64(1), None);
679 cache.put(&k2, Value::I64(2), None);
680 cache.put(&k3, Value::I64(3), None);
681 cache.put(&k4, Value::I64(4), None);
682
683 cache.invalidate_table("users");
684
685 assert!(cache.get(&k1).is_none());
687 assert!(cache.get(&k2).is_none());
688 assert!(cache.get(&k3).is_none());
689 assert!(cache.get(&k4).is_some());
691 }
692
693 #[test]
694 fn test_invalidate_table_no_op_for_unknown_table() {
695 let cache = L2Cache::new();
696 let k1 = CacheKey::by_pk("users", 1);
697 cache.put(&k1, Value::I64(1), None);
698
699 cache.invalidate_table("nonexistent");
700 assert!(cache.get(&k1).is_some());
701 }
702
703 #[test]
706 fn test_ttl_expiration() {
707 let cache = L2Cache::new();
708 let key = CacheKey::by_pk("users", 1);
709
710 cache.put(&key, Value::I64(42), Some(Duration::from_millis(50)));
711 assert!(cache.get(&key).is_some());
712
713 thread::sleep(Duration::from_millis(100));
715 assert!(cache.get(&key).is_none());
716 }
717
718 #[test]
719 fn test_default_ttl_applied_when_no_explicit_ttl() {
720 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
721 let key = CacheKey::by_pk("users", 1);
722
723 cache.put(&key, Value::I64(42), None); assert!(cache.get(&key).is_some());
725
726 thread::sleep(Duration::from_millis(100));
727 assert!(cache.get(&key).is_none());
728 }
729
730 #[test]
731 fn test_explicit_ttl_overrides_default() {
732 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
734 let key = CacheKey::by_pk("users", 1);
735
736 cache.put(&key, Value::I64(42), Some(Duration::MAX));
738
739 thread::sleep(Duration::from_millis(100));
741 assert!(cache.get(&key).is_some());
743 }
744
745 #[test]
746 fn test_none_ttl_uses_default_ttl() {
747 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
749 let key = CacheKey::by_pk("users", 1);
750
751 cache.put(&key, Value::I64(42), None);
752 assert!(cache.get(&key).is_some());
753
754 thread::sleep(Duration::from_millis(100));
755 assert!(cache.get(&key).is_none());
757 }
758
759 #[test]
762 fn test_stats_tracks_hits_and_misses() {
763 let cache = L2Cache::new();
764
765 let k1 = CacheKey::by_pk("users", 1);
766 let k2 = CacheKey::by_pk("users", 2);
767
768 cache.put(&k1, Value::I64(1), None);
769
770 cache.get(&k1);
772 cache.get(&k2);
774 cache.get(&k2);
775
776 let stats = cache.stats();
777 assert_eq!(stats.hits, 1);
778 assert_eq!(stats.misses, 2);
779 assert_eq!(stats.sets, 1);
780 }
781
782 #[test]
783 fn test_stats_tracks_evictions() {
784 let cache = L2Cache::new();
785 let k1 = CacheKey::by_pk("users", 1);
786 let k2 = CacheKey::by_pk("users", 2);
787
788 cache.put(&k1, Value::I64(1), None);
789 cache.put(&k2, Value::I64(2), None);
790
791 cache.invalidate(&k1); cache.invalidate_table("users"); let stats = cache.stats();
795 assert_eq!(stats.evictions, 2);
797 }
798
799 #[test]
800 fn test_stats_reset() {
801 let cache = L2Cache::new();
802 let k1 = CacheKey::by_pk("users", 1);
803
804 cache.put(&k1, Value::I64(1), None);
805 cache.get(&k1);
806 cache.get(&k1);
807
808 let stats_before = cache.stats();
809 assert!(stats_before.hits > 0);
810
811 cache.reset_stats();
812 let stats_after = cache.stats();
813 assert_eq!(stats_after.hits, 0);
814 assert_eq!(stats_after.misses, 0);
815 assert_eq!(stats_after.sets, 0);
816 }
817
818 #[test]
821 fn test_max_size_eviction() {
822 let cache = L2Cache::new().with_max_size(3);
823
824 for i in 0..5 {
825 let k = CacheKey::by_pk("users", i);
826 cache.put(&k, Value::I64(i), None);
827 }
828
829 let size = cache.size();
831 assert_eq!(
832 size, 3,
833 "size should be exactly max_size after LRU eviction, got {}",
834 size
835 );
836 }
837
838 #[test]
839 fn test_lru_eviction_order() {
840 let cache = L2Cache::new().with_max_size(3);
842
843 let k0 = CacheKey::by_pk("users", 0);
844 let k1 = CacheKey::by_pk("users", 1);
845 let k2 = CacheKey::by_pk("users", 2);
846 let k3 = CacheKey::by_pk("users", 3);
847
848 cache.put(&k0, Value::I64(0), None);
849 cache.put(&k1, Value::I64(1), None);
850 cache.put(&k2, Value::I64(2), None);
851
852 let _ = cache.get(&k0);
854
855 cache.put(&k3, Value::I64(3), None);
857
858 assert!(
859 cache.get(&k0).is_some(),
860 "k0 should survive (recently accessed)"
861 );
862 assert!(
863 cache.get(&k1).is_none(),
864 "k1 should be evicted (LRU victim)"
865 );
866 assert!(cache.get(&k2).is_some(), "k2 should survive");
867 assert!(
868 cache.get(&k3).is_some(),
869 "k3 should survive (just inserted)"
870 );
871 }
872
873 #[test]
874 fn test_clear_all() {
875 let cache = L2Cache::new();
876 cache.put(&CacheKey::by_pk("users", 1), Value::I64(1), None);
877 cache.put(&CacheKey::by_pk("users", 2), Value::I64(2), None);
878 cache.put(&CacheKey::by_pk("orders", 1), Value::I64(3), None);
879
880 assert_eq!(cache.size(), 3);
881 cache.clear();
882 assert_eq!(cache.size(), 0);
883 }
884
885 #[test]
888 fn test_contains_does_not_update_stats() {
889 let cache = L2Cache::new();
890 let k1 = CacheKey::by_pk("users", 1);
891 cache.put(&k1, Value::I64(1), None);
892
893 let exists = cache.contains(&k1);
894 assert!(exists);
895
896 let stats = cache.stats();
897 assert_eq!(stats.hits, 0);
898 assert_eq!(stats.misses, 0);
899 }
900
901 #[test]
902 fn test_contains_returns_false_for_missing() {
903 let cache = L2Cache::new();
904 let k = CacheKey::by_pk("users", 999);
905 assert!(!cache.contains(&k));
906 }
907
908 #[test]
909 fn test_contains_returns_false_for_expired() {
910 let cache = L2Cache::new();
911 let k = CacheKey::by_pk("users", 1);
912 cache.put(&k, Value::I64(1), Some(Duration::from_millis(10)));
913
914 thread::sleep(Duration::from_millis(50));
915 assert!(!cache.contains(&k));
916 }
917
918 #[test]
921 fn test_evict_expired_removes_only_expired_entries() {
922 let cache = L2Cache::new();
923
924 let k1 = CacheKey::by_pk("users", 1);
925 let k2 = CacheKey::by_pk("users", 2);
926
927 cache.put(&k1, Value::I64(1), Some(Duration::from_millis(10)));
928 cache.put(&k2, Value::I64(2), None); thread::sleep(Duration::from_millis(50));
931 let removed = cache.evict_expired();
932
933 assert_eq!(removed, 1);
934 assert!(cache.get(&k1).is_none());
935 assert!(cache.get(&k2).is_some());
936 }
937
938 #[test]
939 fn test_evict_expired_returns_zero_if_no_expired() {
940 let cache = L2Cache::new();
941 let k1 = CacheKey::by_pk("users", 1);
942 cache.put(&k1, Value::I64(1), None);
943
944 let removed = cache.evict_expired();
945 assert_eq!(removed, 0);
946 }
947
948 #[test]
951 fn test_concurrent_access() {
952 let cache = std::sync::Arc::new(L2Cache::new());
953 let mut handles = Vec::new();
954
955 for i in 0..4 {
957 let c = cache.clone();
958 handles.push(thread::spawn(move || {
959 for j in 0..10 {
960 let k = CacheKey::by_pk("users", i * 10 + j);
961 c.put(&k, Value::I64(i * 10 + j), None);
962 }
963 }));
964 }
965 for h in handles {
966 h.join().unwrap();
967 }
968
969 assert_eq!(cache.size(), 40);
970
971 let mut handles = Vec::new();
973 for i in 0..4 {
974 let c = cache.clone();
975 handles.push(thread::spawn(move || {
976 for j in 0..10 {
977 let k = CacheKey::by_pk("users", i * 10 + j);
978 let v = c.get(&k);
979 assert!(v.is_some());
980 }
981 }));
982 }
983 for h in handles {
984 h.join().unwrap();
985 }
986
987 let stats = cache.stats();
988 assert_eq!(stats.hits, 40);
989 }
990
991 #[test]
994 fn test_default() {
995 let cache = L2Cache::default();
996 assert_eq!(cache.size(), 0);
997 }
998
999 #[test]
1002 fn test_realistic_scenario() {
1003 let cache = L2Cache::new();
1004
1005 for i in 1..=5 {
1007 cache.put(
1008 &CacheKey::by_pk("users", i),
1009 Value::String(format!("user_{}", i)),
1010 None,
1011 );
1012 }
1013
1014 cache.put(
1016 &CacheKey::by_query("users", "active_users_hash"),
1017 Value::I64(5),
1018 None,
1019 );
1020
1021 for i in 1..=10 {
1023 let _ = cache.get(&CacheKey::by_pk("users", i));
1024 }
1025
1026 let stats = cache.stats();
1027 assert_eq!(stats.hits, 5); assert_eq!(stats.misses, 5); assert_eq!(stats.sets, 6); cache.invalidate_table("users");
1033
1034 cache.reset_stats();
1036 for i in 1..=5 {
1037 let _ = cache.get(&CacheKey::by_pk("users", i));
1038 }
1039 let stats2 = cache.stats();
1040 assert_eq!(stats2.hits, 0);
1041 assert_eq!(stats2.misses, 5);
1042 }
1043}