1use std::collections::{HashMap, VecDeque};
12
13#[inline(always)]
17fn fnv1a_64(data: &[u8]) -> u64 {
18 let mut h: u64 = 14_695_981_039_346_656_037;
19 for &b in data {
20 h ^= b as u64;
21 h = h.wrapping_mul(1_099_511_628_211);
22 }
23 h
24}
25
26#[inline(always)]
28fn xorshift64(state: &mut u64) -> u64 {
29 let mut x = *state;
30 x ^= x << 13;
31 x ^= x >> 7;
32 x ^= x << 17;
33 *state = x;
34 x
35}
36
37fn now_secs() -> u64 {
39 std::time::SystemTime::now()
40 .duration_since(std::time::UNIX_EPOCH)
41 .map(|d| d.as_secs())
42 .unwrap_or(0)
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct Cac2Cid(pub [u8; 32]);
50
51impl Cac2Cid {
52 pub fn from_slice(s: &[u8]) -> Option<Self> {
54 if s.len() < 32 {
55 return None;
56 }
57 let mut arr = [0u8; 32];
58 arr.copy_from_slice(&s[..32]);
59 Some(Self(arr))
60 }
61
62 pub fn from_bytes(data: &[u8]) -> Self {
64 let h1 = fnv1a_64(data);
65 let h2 = fnv1a_64(&h1.to_le_bytes());
66 let h3 = fnv1a_64(&h2.to_le_bytes());
67 let h4 = fnv1a_64(&h3.to_le_bytes());
68 let mut arr = [0u8; 32];
69 arr[0..8].copy_from_slice(&h1.to_le_bytes());
70 arr[8..16].copy_from_slice(&h2.to_le_bytes());
71 arr[16..24].copy_from_slice(&h3.to_le_bytes());
72 arr[24..32].copy_from_slice(&h4.to_le_bytes());
73 Self(arr)
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Cac2Tier {
80 Hot,
82 Warm,
84 Evicted,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum Cac2EvictionReason {
91 LruEviction,
93 LfuEviction,
95 TtlExpiry,
97 ManualEviction,
99 CapacityPressure,
101}
102
103#[derive(Debug, Clone)]
105pub struct Cac2Entry {
106 pub cid: Cac2Cid,
108 pub data: Vec<u8>,
110 pub access_count: u32,
112 pub inserted_at: u64,
114 pub last_accessed: u64,
116 pub tier: Cac2Tier,
118}
119
120#[derive(Debug, Clone)]
122pub struct Cac2EvictionRecord {
123 pub ts: u64,
125 pub cid: Cac2Cid,
127 pub tier: Cac2Tier,
129 pub reason: Cac2EvictionReason,
131}
132
133#[derive(Debug, Clone)]
135pub struct Cac2CacheConfig {
136 pub hot_capacity: usize,
138 pub warm_capacity: usize,
140 pub bloom_size: usize,
142 pub hot_ttl_secs: u64,
144 pub warm_ttl_secs: u64,
146 pub admission_threshold: u32,
148}
149
150impl Default for Cac2CacheConfig {
151 fn default() -> Self {
152 Self {
153 hot_capacity: 512,
154 warm_capacity: 4096,
155 bloom_size: 1 << 16, hot_ttl_secs: 300,
157 warm_ttl_secs: 3600,
158 admission_threshold: 2,
159 }
160 }
161}
162
163#[derive(Debug, Clone, Default)]
165pub struct Cac2CacheStats {
166 pub hot_count: usize,
168 pub warm_count: usize,
170 pub hit_rate: f64,
172 pub miss_rate: f64,
174 pub eviction_count: u64,
176 pub bloom_false_positive_est: f64,
178}
179
180pub type Cac2ContentAddressedCacheV2 = ContentAddressedCacheV2;
184
185struct BloomFilter {
191 bits: Vec<u64>,
192 num_bits: u64,
194 set_count: u64,
196}
197
198impl BloomFilter {
199 fn new(num_bits: usize) -> Self {
200 let num_bits = num_bits.max(64);
201 let words = num_bits.div_ceil(64);
202 Self {
203 bits: vec![0u64; words],
204 num_bits: (words * 64) as u64,
205 set_count: 0,
206 }
207 }
208
209 fn probe_positions(&self, cid: &Cac2Cid) -> [u64; 3] {
211 let h0 = fnv1a_64(&cid.0);
212 let mut seed1 = h0 ^ 0xdeadbeef_cafebabe;
214 let h1 = xorshift64(&mut seed1);
215 let mut seed2 = h1 ^ 0x0123456789abcdef;
216 let h2 = xorshift64(&mut seed2);
217 [h0 % self.num_bits, h1 % self.num_bits, h2 % self.num_bits]
218 }
219
220 fn insert(&mut self, cid: &Cac2Cid) {
221 for pos in self.probe_positions(cid) {
222 let word = (pos / 64) as usize;
223 let bit = pos % 64;
224 if self.bits[word] & (1u64 << bit) == 0 {
225 self.bits[word] |= 1u64 << bit;
226 self.set_count += 1;
227 }
228 }
229 }
230
231 fn probably_contains(&self, cid: &Cac2Cid) -> bool {
232 for pos in self.probe_positions(cid) {
233 let word = (pos / 64) as usize;
234 let bit = pos % 64;
235 if self.bits[word] & (1u64 << bit) == 0 {
236 return false;
237 }
238 }
239 true
240 }
241
242 fn false_positive_estimate(&self) -> f64 {
245 let m = self.num_bits as f64;
246 let n = (self.set_count / 3) as f64; let k = 3.0_f64;
248 let inner = 1.0 - ((-k * n) / m).exp();
249 inner.powf(k)
250 }
251}
252
253struct LruList {
261 order: VecDeque<Cac2Cid>,
262}
263
264impl LruList {
265 fn new() -> Self {
266 Self {
267 order: VecDeque::new(),
268 }
269 }
270
271 fn touch(&mut self, cid: Cac2Cid) {
273 self.order.retain(|c| *c != cid);
274 self.order.push_front(cid);
275 }
276
277 fn evict_lru(&mut self) -> Option<Cac2Cid> {
279 self.order.pop_back()
280 }
281
282 fn remove(&mut self, cid: &Cac2Cid) {
284 self.order.retain(|c| c != cid);
285 }
286
287 #[allow(dead_code)]
288 fn len(&self) -> usize {
289 self.order.len()
290 }
291}
292
293pub struct ContentAddressedCacheV2 {
298 hot: HashMap<Cac2Cid, Cac2Entry>,
300 warm: HashMap<Cac2Cid, Cac2Entry>,
302 bloom: BloomFilter,
304 eviction_log: VecDeque<Cac2EvictionRecord>,
306 hot_lru: LruList,
308 config: Cac2CacheConfig,
310 hits: u64,
312 misses: u64,
314 total_evictions: u64,
316 #[allow(dead_code)]
318 rng_state: u64,
319}
320
321impl ContentAddressedCacheV2 {
322 pub fn new(config: Cac2CacheConfig) -> Self {
324 let bloom_size = config.bloom_size;
325 Self {
326 hot: HashMap::new(),
327 warm: HashMap::new(),
328 bloom: BloomFilter::new(bloom_size),
329 eviction_log: VecDeque::new(),
330 hot_lru: LruList::new(),
331 config,
332 hits: 0,
333 misses: 0,
334 total_evictions: 0,
335 rng_state: 0xcafe_babe_dead_beef,
336 }
337 }
338
339 pub fn default_config() -> Self {
341 Self::new(Cac2CacheConfig::default())
342 }
343
344 pub fn bloom_probably_contains(&self, cid: &Cac2Cid) -> bool {
348 self.bloom.probably_contains(cid)
349 }
350
351 pub fn insert(&mut self, cid: Cac2Cid, data: Vec<u8>) {
365 let now = now_secs();
366
367 if let Some(entry) = self.hot.get_mut(&cid) {
369 entry.data = data;
370 entry.last_accessed = now;
371 entry.access_count = entry.access_count.saturating_add(1);
372 self.hot_lru.touch(cid);
373 self.bloom.insert(&cid);
374 return;
375 }
376 if let Some(entry) = self.warm.get_mut(&cid) {
377 entry.data = data;
378 entry.last_accessed = now;
379 entry.access_count = entry.access_count.saturating_add(1);
380 self.bloom.insert(&cid);
381 return;
382 }
383
384 let seen_before = self.bloom.probably_contains(&cid);
386 self.bloom.insert(&cid);
387
388 if seen_before {
389 self.ensure_hot_capacity(now);
391 let entry = Cac2Entry {
392 cid,
393 data,
394 access_count: 1,
395 inserted_at: now,
396 last_accessed: now,
397 tier: Cac2Tier::Hot,
398 };
399 self.hot.insert(cid, entry);
400 self.hot_lru.touch(cid);
401 } else {
402 self.ensure_warm_capacity(now);
404 let entry = Cac2Entry {
405 cid,
406 data,
407 access_count: 0,
408 inserted_at: now,
409 last_accessed: now,
410 tier: Cac2Tier::Warm,
411 };
412 self.warm.insert(cid, entry);
413 }
414 }
415
416 pub fn get(&mut self, cid: &Cac2Cid) -> Option<&[u8]> {
423 let now = now_secs();
424
425 if let Some(entry) = self.hot.get_mut(cid) {
427 entry.access_count = entry.access_count.saturating_add(1);
428 entry.last_accessed = now;
429 self.hits += 1;
430 self.hot_lru.touch(*cid);
431 return self.hot.get(cid).map(|e| e.data.as_slice());
433 }
434
435 if self.warm.contains_key(cid) {
437 let threshold = self.config.admission_threshold;
438 if let Some(entry) = self.warm.get_mut(cid) {
440 entry.access_count = entry.access_count.saturating_add(1);
441 entry.last_accessed = now;
442 self.hits += 1;
443 }
444 let promote = self
445 .warm
446 .get(cid)
447 .map(|e| e.access_count > threshold)
448 .unwrap_or(false);
449 if promote {
450 self.promote_warm_to_hot(cid, now);
451 return self.hot.get(cid).map(|e| e.data.as_slice());
452 }
453 return self.warm.get(cid).map(|e| e.data.as_slice());
454 }
455
456 self.misses += 1;
457 None
458 }
459
460 pub fn evict(&mut self, cid: &Cac2Cid) {
464 let now = now_secs();
465 if self.hot.remove(cid).is_some() {
466 self.hot_lru.remove(cid);
467 self.log_eviction(now, *cid, Cac2Tier::Hot, Cac2EvictionReason::ManualEviction);
468 }
469 if self.warm.remove(cid).is_some() {
470 self.log_eviction(
471 now,
472 *cid,
473 Cac2Tier::Warm,
474 Cac2EvictionReason::ManualEviction,
475 );
476 }
477 }
478
479 pub fn expire_stale(&mut self, now_ts: u64) {
484 let hot_ttl = self.config.hot_ttl_secs;
485 let warm_ttl = self.config.warm_ttl_secs;
486
487 if hot_ttl > 0 {
488 let expired_hot: Vec<Cac2Cid> = self
489 .hot
490 .values()
491 .filter(|e| now_ts.saturating_sub(e.inserted_at) >= hot_ttl)
492 .map(|e| e.cid)
493 .collect();
494 for cid in expired_hot {
495 self.hot.remove(&cid);
496 self.hot_lru.remove(&cid);
497 self.log_eviction(now_ts, cid, Cac2Tier::Hot, Cac2EvictionReason::TtlExpiry);
498 }
499 }
500
501 if warm_ttl > 0 {
502 let expired_warm: Vec<Cac2Cid> = self
503 .warm
504 .values()
505 .filter(|e| now_ts.saturating_sub(e.inserted_at) >= warm_ttl)
506 .map(|e| e.cid)
507 .collect();
508 for cid in expired_warm {
509 self.warm.remove(&cid);
510 self.log_eviction(now_ts, cid, Cac2Tier::Warm, Cac2EvictionReason::TtlExpiry);
511 }
512 }
513 }
514
515 pub fn drain_warm_to_disk_simulation(&mut self) -> Vec<(Cac2Cid, Vec<u8>)> {
522 if self.warm.is_empty() {
523 return Vec::new();
524 }
525
526 let drain_count = ((self.warm.len() as f64) * 0.25).ceil() as usize;
527 if drain_count == 0 {
528 return Vec::new();
529 }
530
531 let now = now_secs();
532
533 let mut candidates: Vec<(u32, Cac2Cid)> = self
535 .warm
536 .values()
537 .map(|e| (e.access_count, e.cid))
538 .collect();
539 candidates.sort_unstable_by_key(|(count, _)| *count);
540
541 let to_drain: Vec<Cac2Cid> = candidates
542 .into_iter()
543 .take(drain_count)
544 .map(|(_, cid)| cid)
545 .collect();
546
547 let mut result = Vec::with_capacity(to_drain.len());
548 for cid in to_drain {
549 if let Some(entry) = self.warm.remove(&cid) {
550 self.log_eviction(now, cid, Cac2Tier::Warm, Cac2EvictionReason::LfuEviction);
551 result.push((cid, entry.data));
552 }
553 }
554 result
555 }
556
557 pub fn cache_stats(&self) -> Cac2CacheStats {
561 let total = self.hits + self.misses;
562 let (hit_rate, miss_rate) = if total == 0 {
563 (0.0, 0.0)
564 } else {
565 (
566 self.hits as f64 / total as f64,
567 self.misses as f64 / total as f64,
568 )
569 };
570 Cac2CacheStats {
571 hot_count: self.hot.len(),
572 warm_count: self.warm.len(),
573 hit_rate,
574 miss_rate,
575 eviction_count: self.total_evictions,
576 bloom_false_positive_est: self.bloom.false_positive_estimate(),
577 }
578 }
579
580 pub fn eviction_log(&self) -> &VecDeque<Cac2EvictionRecord> {
582 &self.eviction_log
583 }
584
585 pub fn config(&self) -> &Cac2CacheConfig {
587 &self.config
588 }
589
590 pub fn hot_len(&self) -> usize {
592 self.hot.len()
593 }
594
595 pub fn warm_len(&self) -> usize {
597 self.warm.len()
598 }
599
600 pub fn total_len(&self) -> usize {
602 self.hot.len() + self.warm.len()
603 }
604
605 pub fn is_empty(&self) -> bool {
607 self.hot.is_empty() && self.warm.is_empty()
608 }
609
610 pub fn contains(&self, cid: &Cac2Cid) -> bool {
612 self.hot.contains_key(cid) || self.warm.contains_key(cid)
613 }
614
615 fn ensure_hot_capacity(&mut self, now: u64) {
619 while self.hot.len() >= self.config.hot_capacity {
620 if let Some(lru_cid) = self.hot_lru.evict_lru() {
621 if let Some(mut entry) = self.hot.remove(&lru_cid) {
622 self.log_eviction(now, lru_cid, Cac2Tier::Hot, Cac2EvictionReason::LruEviction);
623 self.ensure_warm_capacity(now);
625 entry.tier = Cac2Tier::Warm;
626 self.warm.insert(lru_cid, entry);
627 }
628 } else {
629 break;
630 }
631 }
632 }
633
634 fn ensure_warm_capacity(&mut self, now: u64) {
636 while self.warm.len() >= self.config.warm_capacity {
637 let victim_cid = self
639 .warm
640 .values()
641 .min_by(|a, b| {
642 a.access_count
643 .cmp(&b.access_count)
644 .then_with(|| a.last_accessed.cmp(&b.last_accessed))
645 })
646 .map(|e| e.cid);
647
648 if let Some(cid) = victim_cid {
649 self.warm.remove(&cid);
650 self.log_eviction(now, cid, Cac2Tier::Warm, Cac2EvictionReason::LfuEviction);
651 } else {
652 break;
653 }
654 }
655 }
656
657 fn promote_warm_to_hot(&mut self, cid: &Cac2Cid, now: u64) {
659 if let Some(mut entry) = self.warm.remove(cid) {
660 self.ensure_hot_capacity(now);
661 entry.tier = Cac2Tier::Hot;
662 entry.last_accessed = now;
663 self.hot.insert(*cid, entry);
664 self.hot_lru.touch(*cid);
665 }
666 }
667
668 fn log_eviction(&mut self, ts: u64, cid: Cac2Cid, tier: Cac2Tier, reason: Cac2EvictionReason) {
670 const MAX_EVICTION_LOG: usize = 500;
671 if self.eviction_log.len() >= MAX_EVICTION_LOG {
672 self.eviction_log.pop_front();
673 }
674 self.eviction_log.push_back(Cac2EvictionRecord {
675 ts,
676 cid,
677 tier: Cac2Tier::Evicted,
678 reason,
679 });
680 let _ = tier;
684 self.total_evictions += 1;
685 }
686
687 #[cfg(test)]
689 fn next_rand(&mut self) -> u64 {
690 xorshift64(&mut self.rng_state)
691 }
692}
693
694#[cfg(test)]
697mod tests {
698 use super::*;
699
700 fn make_cid(seed: u8) -> Cac2Cid {
703 Cac2Cid([seed; 32])
704 }
705
706 fn make_cid_distinct(n: u64) -> Cac2Cid {
707 Cac2Cid::from_bytes(&n.to_le_bytes())
708 }
709
710 fn default_cache() -> ContentAddressedCacheV2 {
711 ContentAddressedCacheV2::new(Cac2CacheConfig {
712 hot_capacity: 4,
713 warm_capacity: 8,
714 bloom_size: 256,
715 hot_ttl_secs: 10,
716 warm_ttl_secs: 60,
717 admission_threshold: 2,
718 })
719 }
720
721 #[test]
724 fn test_cid_from_slice_ok() {
725 let data = [7u8; 32];
726 let cid = Cac2Cid::from_slice(&data);
727 assert!(cid.is_some());
728 assert_eq!(cid.unwrap().0, data);
729 }
730
731 #[test]
732 fn test_cid_from_slice_too_short() {
733 let short = [1u8; 10];
734 assert!(Cac2Cid::from_slice(&short).is_none());
735 }
736
737 #[test]
738 fn test_cid_from_bytes_deterministic() {
739 let a = Cac2Cid::from_bytes(b"hello");
740 let b = Cac2Cid::from_bytes(b"hello");
741 assert_eq!(a, b);
742 }
743
744 #[test]
745 fn test_cid_from_bytes_different_inputs() {
746 let a = Cac2Cid::from_bytes(b"foo");
747 let b = Cac2Cid::from_bytes(b"bar");
748 assert_ne!(a, b);
749 }
750
751 #[test]
752 fn test_cid_copy_semantics() {
753 let a = make_cid(1);
754 let b = a; assert_eq!(a, b);
756 }
757
758 #[test]
759 fn test_cid_hash_equality() {
760 let mut set = std::collections::HashSet::new();
761 set.insert(make_cid(42));
762 assert!(set.contains(&make_cid(42)));
763 assert!(!set.contains(&make_cid(43)));
764 }
765
766 #[test]
769 fn test_bloom_new_empty() {
770 let bf = BloomFilter::new(1024);
771 let cid = make_cid(1);
772 assert!(!bf.probably_contains(&cid));
773 }
774
775 #[test]
776 fn test_bloom_insert_then_contains() {
777 let mut bf = BloomFilter::new(1024);
778 let cid = make_cid(99);
779 bf.insert(&cid);
780 assert!(bf.probably_contains(&cid));
781 }
782
783 #[test]
784 fn test_bloom_multiple_cids() {
785 let mut bf = BloomFilter::new(8192);
786 for i in 0u8..50 {
787 let cid = make_cid(i);
788 bf.insert(&cid);
789 }
790 for i in 0u8..50 {
791 let cid = make_cid(i);
792 assert!(bf.probably_contains(&cid), "false negative for cid {i}");
793 }
794 }
795
796 #[test]
797 fn test_bloom_false_positive_estimate_increases_with_load() {
798 let mut bf = BloomFilter::new(256);
799 let fp0 = bf.false_positive_estimate();
800 for i in 0u64..20 {
801 bf.insert(&make_cid_distinct(i));
802 }
803 let fp20 = bf.false_positive_estimate();
804 assert!(fp20 > fp0, "FP rate should increase with insertions");
805 }
806
807 #[test]
808 fn test_bloom_no_false_negatives() {
809 let mut bf = BloomFilter::new(65536);
810 for i in 0u64..200 {
811 let cid = make_cid_distinct(i);
812 bf.insert(&cid);
813 assert!(bf.probably_contains(&cid), "false negative at i={i}");
814 }
815 }
816
817 #[test]
820 fn test_lru_touch_and_evict() {
821 let mut lru = LruList::new();
822 let a = make_cid(1);
823 let b = make_cid(2);
824 let c = make_cid(3);
825 lru.touch(a);
826 lru.touch(b);
827 lru.touch(c);
828 assert_eq!(lru.evict_lru(), Some(a));
830 }
831
832 #[test]
833 fn test_lru_touch_promotes_existing() {
834 let mut lru = LruList::new();
835 let a = make_cid(1);
836 let b = make_cid(2);
837 lru.touch(a);
838 lru.touch(b);
839 lru.touch(a); assert_eq!(lru.evict_lru(), Some(b));
841 }
842
843 #[test]
844 fn test_lru_remove_specific() {
845 let mut lru = LruList::new();
846 let a = make_cid(1);
847 let b = make_cid(2);
848 lru.touch(a);
849 lru.touch(b);
850 lru.remove(&a);
851 assert_eq!(lru.len(), 1);
852 assert_eq!(lru.evict_lru(), Some(b));
853 }
854
855 #[test]
856 fn test_lru_evict_from_empty() {
857 let mut lru = LruList::new();
858 assert_eq!(lru.evict_lru(), None);
859 }
860
861 #[test]
864 fn test_cache_new_empty() {
865 let cache = default_cache();
866 assert!(cache.is_empty());
867 assert_eq!(cache.total_len(), 0);
868 }
869
870 #[test]
871 fn test_cache_default_config() {
872 let cache = ContentAddressedCacheV2::default_config();
873 assert_eq!(cache.config().hot_capacity, 512);
874 }
875
876 #[test]
879 fn test_insert_and_get_warm() {
880 let mut cache = default_cache();
881 let cid = make_cid(1);
882 cache.insert(cid, b"hello".to_vec());
884 assert_eq!(cache.warm_len(), 1);
885 assert_eq!(cache.hot_len(), 0);
886 let val = cache.get(&cid);
887 assert_eq!(val, Some(b"hello".as_ref()));
888 }
889
890 #[test]
891 fn test_insert_twice_routes_to_hot() {
892 let mut cache = default_cache();
893 let cid = make_cid(10);
894 cache.insert(cid, b"v1".to_vec());
896 cache.warm.remove(&cid);
898 cache.insert(cid, b"v2".to_vec());
900 assert_eq!(cache.hot_len(), 1);
901 }
902
903 #[test]
904 fn test_get_miss_returns_none() {
905 let mut cache = default_cache();
906 let cid = make_cid(200);
907 assert_eq!(cache.get(&cid), None);
908 }
909
910 #[test]
911 fn test_get_increments_access_count() {
912 let mut cache = default_cache();
913 let cid = make_cid(5);
914 cache.insert(cid, b"data".to_vec());
915 let _ = cache.get(&cid);
917 let _ = cache.get(&cid);
918 let entry = cache.warm.get(&cid).or_else(|| cache.hot.get(&cid));
919 assert!(entry.map(|e| e.access_count).unwrap_or(0) >= 1);
920 }
921
922 #[test]
923 fn test_get_updates_last_accessed() {
924 let mut cache = default_cache();
925 let cid = make_cid(7);
926 cache.insert(cid, b"x".to_vec());
927 let before = cache.warm.get(&cid).map(|e| e.last_accessed).unwrap_or(0);
928 assert!(before > 0);
931 let _ = cache.get(&cid);
932 }
933
934 #[test]
937 fn test_bloom_admission_cold_goes_to_warm() {
938 let mut cache = default_cache();
939 let cid = Cac2Cid::from_bytes(b"unique_cold_key_abc");
940 cache.insert(cid, b"payload".to_vec());
941 assert_eq!(cache.warm_len(), 1);
942 assert_eq!(cache.hot_len(), 0);
943 }
944
945 #[test]
946 fn test_bloom_admission_warm_hit_goes_to_hot() {
947 let mut cache = default_cache();
948 let cid = Cac2Cid::from_bytes(b"repeated_key_xyz");
949 cache.insert(cid, b"first".to_vec());
951 assert_eq!(cache.warm_len(), 1, "should be in warm after cold insert");
952 cache.warm.remove(&cid);
955 cache.insert(cid, b"second".to_vec());
957 assert_eq!(cache.hot_len(), 1, "repeated CID should route to hot tier");
958 }
959
960 #[test]
961 fn test_bloom_probably_contains_after_insert() {
962 let mut cache = default_cache();
963 let cid = make_cid(33);
964 assert!(!cache.bloom_probably_contains(&cid));
965 cache.insert(cid, b"z".to_vec());
966 assert!(cache.bloom_probably_contains(&cid));
967 }
968
969 #[test]
972 fn test_promotion_on_repeated_get() {
973 let mut cache = default_cache();
975 let cid = Cac2Cid::from_bytes(b"promo_test");
976 cache.insert(cid, b"data".to_vec()); let _ = cache.get(&cid); let _ = cache.get(&cid); let _ = cache.get(&cid); assert_eq!(cache.hot_len(), 1, "entry should have been promoted to hot");
982 }
983
984 #[test]
985 fn test_no_promotion_below_threshold() {
986 let mut cache = default_cache(); let cid = Cac2Cid::from_bytes(b"no_promo");
988 cache.insert(cid, b"d".to_vec()); let _ = cache.get(&cid); assert_eq!(cache.hot_len(), 0, "should not yet be promoted");
991 }
992
993 #[test]
996 fn test_hot_lru_eviction_demotes_to_warm() {
997 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
999 hot_capacity: 3,
1000 warm_capacity: 10,
1001 bloom_size: 512,
1002 hot_ttl_secs: 0,
1003 warm_ttl_secs: 0,
1004 admission_threshold: 0,
1005 });
1006 for i in 0u8..4 {
1010 let cid = make_cid(i);
1011 cache.insert(cid, vec![i]); cache.insert(cid, vec![i, i]); }
1014 assert!(cache.hot_len() <= 3);
1016 assert!(cache.warm_len() >= 1);
1017 }
1018
1019 #[test]
1022 fn test_warm_lfu_eviction() {
1023 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1024 hot_capacity: 100,
1025 warm_capacity: 3,
1026 bloom_size: 512,
1027 hot_ttl_secs: 0,
1028 warm_ttl_secs: 0,
1029 admission_threshold: 100, });
1031
1032 let cid_a = make_cid(1);
1033 let cid_b = make_cid(2);
1034 let cid_c = make_cid(3);
1035 let cid_d = make_cid(4);
1036
1037 cache.insert(cid_a, b"A".to_vec());
1039 cache.insert(cid_b, b"B".to_vec());
1040 cache.insert(cid_c, b"C".to_vec());
1041
1042 let _ = cache.get(&cid_a);
1044 let _ = cache.get(&cid_a);
1045 let _ = cache.get(&cid_b);
1046
1047 cache.insert(cid_d, b"D".to_vec());
1049
1050 assert!(!cache.contains(&cid_c), "C should have been LFU-evicted");
1051 }
1052
1053 #[test]
1056 fn test_evict_removes_hot_entry() {
1057 let mut cache = default_cache();
1058 let cid = make_cid(20);
1059 cache.insert(cid, b"hot".to_vec()); cache.insert(cid, b"hot2".to_vec()); cache.evict(&cid);
1062 assert!(!cache.contains(&cid));
1063 }
1064
1065 #[test]
1066 fn test_evict_removes_warm_entry() {
1067 let mut cache = default_cache();
1068 let cid = make_cid(21);
1069 cache.insert(cid, b"warm".to_vec());
1070 cache.evict(&cid);
1071 assert!(!cache.contains(&cid));
1072 }
1073
1074 #[test]
1075 fn test_evict_nonexistent_is_noop() {
1076 let mut cache = default_cache();
1077 cache.evict(&make_cid(255)); }
1079
1080 #[test]
1083 fn test_expire_stale_removes_old_hot_entries() {
1084 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1085 hot_capacity: 10,
1086 warm_capacity: 10,
1087 bloom_size: 256,
1088 hot_ttl_secs: 5,
1089 warm_ttl_secs: 0,
1090 admission_threshold: 0,
1091 });
1092
1093 let cid = make_cid(50);
1094 cache.insert(cid, b"ttl".to_vec()); cache.insert(cid, b"ttl2".to_vec()); if let Some(entry) = cache.hot.get_mut(&cid) {
1099 entry.inserted_at = 0; }
1101
1102 let future_ts = 1_000_000u64;
1103 cache.expire_stale(future_ts);
1104 assert!(
1105 !cache.hot.contains_key(&cid),
1106 "hot entry should have expired"
1107 );
1108 }
1109
1110 #[test]
1111 fn test_expire_stale_removes_old_warm_entries() {
1112 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1113 hot_capacity: 10,
1114 warm_capacity: 10,
1115 bloom_size: 256,
1116 hot_ttl_secs: 0,
1117 warm_ttl_secs: 30,
1118 admission_threshold: 100,
1119 });
1120
1121 let cid = make_cid(51);
1122 cache.insert(cid, b"warm_old".to_vec());
1123
1124 if let Some(entry) = cache.warm.get_mut(&cid) {
1125 entry.inserted_at = 0;
1126 }
1127
1128 cache.expire_stale(1_000_000);
1129 assert!(!cache.warm.contains_key(&cid));
1130 }
1131
1132 #[test]
1133 fn test_expire_stale_keeps_fresh_entries() {
1134 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1135 hot_capacity: 10,
1136 warm_capacity: 10,
1137 bloom_size: 256,
1138 hot_ttl_secs: 300,
1139 warm_ttl_secs: 300,
1140 admission_threshold: 100,
1141 });
1142
1143 let cid = make_cid(52);
1144 cache.insert(cid, b"fresh".to_vec());
1145
1146 let now = cache
1148 .warm
1149 .get(&cid)
1150 .map(|e| e.inserted_at)
1151 .unwrap_or(now_secs());
1152 cache.expire_stale(now);
1153 assert!(cache.contains(&cid), "fresh entry should survive TTL sweep");
1154 }
1155
1156 #[test]
1157 fn test_expire_stale_zero_ttl_skips() {
1158 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1159 hot_ttl_secs: 0,
1160 warm_ttl_secs: 0,
1161 ..Cac2CacheConfig::default()
1162 });
1163 let cid = make_cid(53);
1164 cache.insert(cid, b"forever".to_vec());
1165 if let Some(e) = cache.warm.get_mut(&cid) {
1166 e.inserted_at = 0;
1167 }
1168 cache.expire_stale(u64::MAX);
1169 assert!(cache.contains(&cid));
1170 }
1171
1172 #[test]
1175 fn test_drain_warm_empty_returns_empty() {
1176 let mut cache = default_cache();
1177 let drained = cache.drain_warm_to_disk_simulation();
1178 assert!(drained.is_empty());
1179 }
1180
1181 #[test]
1182 fn test_drain_warm_drains_25_percent() {
1183 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1184 hot_capacity: 100,
1185 warm_capacity: 1000,
1186 bloom_size: 8192,
1187 hot_ttl_secs: 0,
1188 warm_ttl_secs: 0,
1189 admission_threshold: 1000,
1190 });
1191 for i in 0u64..20 {
1192 cache.insert(make_cid_distinct(i), vec![i as u8]);
1193 }
1194 let before = cache.warm_len();
1195 let drained = cache.drain_warm_to_disk_simulation();
1196 let expected = ((before as f64) * 0.25).ceil() as usize;
1197 assert_eq!(drained.len(), expected);
1198 assert_eq!(cache.warm_len(), before - expected);
1199 }
1200
1201 #[test]
1202 fn test_drain_warm_returns_lowest_frequency() {
1203 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1204 hot_capacity: 100,
1205 warm_capacity: 1000,
1206 bloom_size: 8192,
1207 hot_ttl_secs: 0,
1208 warm_ttl_secs: 0,
1209 admission_threshold: 1000,
1210 });
1211 let high_freq = make_cid(101);
1212 let low_freq = make_cid(102);
1213
1214 cache.insert(high_freq, b"hf".to_vec());
1215 cache.insert(low_freq, b"lf".to_vec());
1216
1217 for _ in 0..10 {
1219 let _ = cache.get(&high_freq);
1220 }
1221
1222 let drained = cache.drain_warm_to_disk_simulation();
1223 let drained_cids: Vec<Cac2Cid> = drained.iter().map(|(c, _)| *c).collect();
1224 assert!(
1225 drained_cids.contains(&low_freq),
1226 "low_freq should be drained first"
1227 );
1228 }
1229
1230 #[test]
1233 fn test_stats_initial_zero() {
1234 let cache = default_cache();
1235 let stats = cache.cache_stats();
1236 assert_eq!(stats.hot_count, 0);
1237 assert_eq!(stats.warm_count, 0);
1238 assert_eq!(stats.hit_rate, 0.0);
1239 assert_eq!(stats.miss_rate, 0.0);
1240 assert_eq!(stats.eviction_count, 0);
1241 }
1242
1243 #[test]
1244 fn test_stats_hit_rate() {
1245 let mut cache = default_cache();
1246 let cid = make_cid(60);
1247 cache.insert(cid, b"stat".to_vec());
1248 let _ = cache.get(&cid); let _ = cache.get(&make_cid(61)); let stats = cache.cache_stats();
1251 assert!((stats.hit_rate - 0.5).abs() < 1e-9);
1252 assert!((stats.miss_rate - 0.5).abs() < 1e-9);
1253 }
1254
1255 #[test]
1256 fn test_stats_eviction_count() {
1257 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1258 hot_capacity: 2,
1259 warm_capacity: 2,
1260 bloom_size: 256,
1261 hot_ttl_secs: 0,
1262 warm_ttl_secs: 0,
1263 admission_threshold: 0,
1264 });
1265 for i in 0u8..10 {
1267 let cid = make_cid(i);
1268 cache.insert(cid, vec![i]);
1269 cache.insert(cid, vec![i]); }
1271 assert!(cache.cache_stats().eviction_count > 0);
1272 }
1273
1274 #[test]
1275 fn test_stats_bloom_fpe_non_negative() {
1276 let cache = default_cache();
1277 assert!(cache.cache_stats().bloom_false_positive_est >= 0.0);
1278 }
1279
1280 #[test]
1283 fn test_eviction_log_bounded_500() {
1284 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1285 hot_capacity: 1,
1286 warm_capacity: 1,
1287 bloom_size: 256,
1288 hot_ttl_secs: 0,
1289 warm_ttl_secs: 0,
1290 admission_threshold: 0,
1291 });
1292 for i in 0u64..600 {
1294 let cid = make_cid_distinct(i);
1295 cache.insert(cid, vec![0u8]);
1296 cache.insert(cid, vec![1u8]);
1297 }
1298 assert!(
1299 cache.eviction_log().len() <= 500,
1300 "eviction log must be bounded at 500"
1301 );
1302 }
1303
1304 #[test]
1305 fn test_eviction_log_records_reason() {
1306 let mut cache = default_cache();
1307 let cid = make_cid(80);
1308 cache.insert(cid, b"manual".to_vec());
1309 cache.evict(&cid);
1310 let found = cache
1311 .eviction_log()
1312 .iter()
1313 .any(|r| r.reason == Cac2EvictionReason::ManualEviction);
1314 assert!(found, "manual eviction should appear in log");
1315 }
1316
1317 #[test]
1318 fn test_eviction_log_ttl_reason() {
1319 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1320 hot_capacity: 10,
1321 warm_capacity: 10,
1322 bloom_size: 256,
1323 hot_ttl_secs: 1,
1324 warm_ttl_secs: 1,
1325 admission_threshold: 100,
1326 });
1327 let cid = make_cid(81);
1328 cache.insert(cid, b"stale".to_vec());
1329 if let Some(e) = cache.warm.get_mut(&cid) {
1330 e.inserted_at = 0;
1331 }
1332 cache.expire_stale(1_000_000);
1333 let found = cache
1334 .eviction_log()
1335 .iter()
1336 .any(|r| r.reason == Cac2EvictionReason::TtlExpiry);
1337 assert!(found);
1338 }
1339
1340 #[test]
1343 fn test_contains_hot() {
1344 let mut cache = default_cache();
1345 let cid = make_cid(90);
1346 cache.insert(cid, b"h".to_vec());
1347 cache.insert(cid, b"h2".to_vec()); assert!(cache.contains(&cid));
1349 }
1350
1351 #[test]
1352 fn test_contains_warm() {
1353 let mut cache = default_cache();
1354 let cid = make_cid(91);
1355 cache.insert(cid, b"w".to_vec());
1356 assert!(cache.contains(&cid));
1357 }
1358
1359 #[test]
1360 fn test_is_empty_after_evict_all() {
1361 let mut cache = default_cache();
1362 let c1 = make_cid(92);
1363 let c2 = make_cid(93);
1364 cache.insert(c1, b"a".to_vec());
1365 cache.insert(c2, b"b".to_vec());
1366 cache.evict(&c1);
1367 cache.evict(&c2);
1368 assert!(cache.is_empty());
1369 }
1370
1371 #[test]
1374 fn test_xorshift64_non_zero() {
1375 let mut state = 12345u64;
1376 let val = xorshift64(&mut state);
1377 assert_ne!(val, 0);
1378 assert_ne!(state, 12345);
1379 }
1380
1381 #[test]
1382 fn test_cache_prng_accessor() {
1383 let mut cache = default_cache();
1384 let a = cache.next_rand();
1385 let b = cache.next_rand();
1386 assert_ne!(a, b, "consecutive PRNG values should differ");
1387 }
1388
1389 #[test]
1392 fn test_fnv1a_deterministic() {
1393 assert_eq!(fnv1a_64(b"hello"), fnv1a_64(b"hello"));
1394 }
1395
1396 #[test]
1397 fn test_fnv1a_distinct_inputs() {
1398 assert_ne!(fnv1a_64(b"a"), fnv1a_64(b"b"));
1399 }
1400
1401 #[test]
1402 fn test_fnv1a_empty_input() {
1403 assert_eq!(fnv1a_64(b""), 14_695_981_039_346_656_037);
1405 }
1406
1407 #[test]
1410 fn test_insert_updates_data_in_place_warm() {
1411 let mut cache = default_cache();
1412 let cid = make_cid(110);
1413 cache.insert(cid, b"v1".to_vec());
1414 cache.insert(cid, b"v2".to_vec()); let val = cache.get(&cid);
1417 assert_eq!(val, Some(b"v2".as_ref()));
1418 }
1419
1420 #[test]
1423 fn test_hot_len_warm_len_total_len() {
1424 let mut cache = default_cache();
1425 let cid_a = make_cid(120);
1426 let cid_b = make_cid(121);
1427 cache.insert(cid_a, b"a".to_vec()); cache.insert(cid_b, b"b".to_vec()); assert_eq!(cache.total_len(), 2);
1430 assert_eq!(cache.warm_len(), 2);
1431 assert_eq!(cache.hot_len(), 0);
1432 }
1433
1434 #[test]
1435 fn test_total_len_after_eviction() {
1436 let mut cache = default_cache();
1437 let cid = make_cid(122);
1438 cache.insert(cid, b"z".to_vec());
1439 assert_eq!(cache.total_len(), 1);
1440 cache.evict(&cid);
1441 assert_eq!(cache.total_len(), 0);
1442 }
1443
1444 #[test]
1447 fn test_zero_capacity_warm_still_works() {
1448 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1450 hot_capacity: 1,
1451 warm_capacity: 1,
1452 bloom_size: 128,
1453 hot_ttl_secs: 0,
1454 warm_ttl_secs: 0,
1455 admission_threshold: 999,
1456 });
1457 let c1 = make_cid(0);
1458 let c2 = make_cid(1);
1459 cache.insert(c1, b"first".to_vec());
1460 cache.insert(c2, b"second".to_vec()); assert!(cache.warm_len() <= 1);
1462 }
1463
1464 #[test]
1465 fn test_large_data_payload() {
1466 let mut cache = default_cache();
1467 let cid = make_cid(130);
1468 let big_data = vec![0xABu8; 1024 * 1024]; cache.insert(cid, big_data.clone());
1470 let retrieved = cache.get(&cid);
1471 assert_eq!(retrieved.map(|d| d.len()), Some(1024 * 1024));
1472 }
1473
1474 #[test]
1475 fn test_get_miss_increments_miss_counter() {
1476 let mut cache = default_cache();
1477 let _ = cache.get(&make_cid(200));
1478 let _ = cache.get(&make_cid(201));
1479 let stats = cache.cache_stats();
1480 assert!((stats.miss_rate - 1.0).abs() < 1e-9);
1481 }
1482
1483 #[test]
1484 fn test_expire_stale_multiple_entries() {
1485 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1486 hot_capacity: 100,
1487 warm_capacity: 100,
1488 bloom_size: 1024,
1489 hot_ttl_secs: 10,
1490 warm_ttl_secs: 10,
1491 admission_threshold: 100,
1492 });
1493 for i in 0u8..10 {
1494 let cid = make_cid(i);
1495 cache.insert(cid, vec![i]);
1496 }
1497 for entry in cache.warm.values_mut() {
1499 entry.inserted_at = 0;
1500 }
1501 cache.expire_stale(1_000_000);
1502 assert_eq!(cache.warm_len(), 0);
1503 }
1504
1505 #[test]
1506 fn test_drain_warm_one_entry() {
1507 let mut cache = ContentAddressedCacheV2::new(Cac2CacheConfig {
1508 hot_capacity: 100,
1509 warm_capacity: 100,
1510 bloom_size: 256,
1511 hot_ttl_secs: 0,
1512 warm_ttl_secs: 0,
1513 admission_threshold: 1000,
1514 });
1515 let cid = make_cid(140);
1516 cache.insert(cid, b"only".to_vec());
1517 let drained = cache.drain_warm_to_disk_simulation();
1518 assert_eq!(drained.len(), 1);
1520 assert!(cache.warm.is_empty());
1521 }
1522
1523 #[test]
1524 fn test_cid_equality_and_inequality() {
1525 let a = make_cid(0);
1526 let b = make_cid(0);
1527 let c = make_cid(1);
1528 assert_eq!(a, b);
1529 assert_ne!(a, c);
1530 }
1531
1532 #[test]
1533 fn test_tier_enum_debug() {
1534 let t = Cac2Tier::Hot;
1535 assert!(format!("{t:?}").contains("Hot"));
1536 }
1537
1538 #[test]
1539 fn test_eviction_reason_debug() {
1540 let r = Cac2EvictionReason::CapacityPressure;
1541 assert!(format!("{r:?}").contains("CapacityPressure"));
1542 }
1543}