1use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum CacheTier {
16 Hot,
18 Warm,
20 Cold,
22}
23
24impl CacheTier {
25 #[must_use]
27 pub fn priority(&self) -> u8 {
28 match self {
29 CacheTier::Hot => 3,
30 CacheTier::Warm => 2,
31 CacheTier::Cold => 1,
32 }
33 }
34
35 fn demoted(self) -> Self {
37 match self {
38 CacheTier::Hot => CacheTier::Warm,
39 CacheTier::Warm => CacheTier::Cold,
40 CacheTier::Cold => CacheTier::Cold,
41 }
42 }
43
44 #[allow(dead_code)]
46 fn promoted(self) -> Self {
47 match self {
48 CacheTier::Cold => CacheTier::Warm,
49 CacheTier::Warm => CacheTier::Hot,
50 CacheTier::Hot => CacheTier::Hot,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum EvictionPolicy {
62 LRU,
64 LFU,
67 TwoQ,
69 ARC,
71}
72
73#[derive(Debug, Clone)]
82pub struct BcmCacheConfig {
83 pub max_hot_bytes: u64,
85 pub max_warm_bytes: u64,
87 pub max_cold_bytes: u64,
89 pub hot_threshold: u64,
91 pub warm_threshold: u64,
93 pub eviction_policy: EvictionPolicy,
95}
96
97impl Default for BcmCacheConfig {
98 fn default() -> Self {
99 Self {
100 max_hot_bytes: 64 * 1024 * 1024, max_warm_bytes: 128 * 1024 * 1024, max_cold_bytes: 256 * 1024 * 1024, hot_threshold: 10,
104 warm_threshold: 3,
105 eviction_policy: EvictionPolicy::LRU,
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
118pub struct BcmCachedBlock {
119 pub cid: String,
121 pub data: Vec<u8>,
123 pub tier: CacheTier,
125 pub inserted_at: u64,
127 pub last_accessed: u64,
129 pub access_count: u64,
131 pub pinned: bool,
133}
134
135impl BcmCachedBlock {
136 fn new(cid: String, data: Vec<u8>, now: u64) -> Self {
137 Self {
138 cid,
139 data,
140 tier: CacheTier::Cold,
141 inserted_at: now,
142 last_accessed: now,
143 access_count: 0,
144 pinned: false,
145 }
146 }
147
148 #[inline]
150 pub fn byte_size(&self) -> u64 {
151 self.data.len() as u64
152 }
153}
154
155#[derive(Debug, Clone, Default)]
163pub struct BcmCacheStats {
164 pub hot_count: usize,
165 pub warm_count: usize,
166 pub cold_count: usize,
167 pub hot_bytes: u64,
168 pub warm_bytes: u64,
169 pub cold_bytes: u64,
170 pub pinned_count: usize,
171 pub evictions: u64,
172 pub promotions: u64,
173 pub demotions: u64,
174}
175
176fn choose_victim(
183 tier_map: &HashMap<String, BcmCachedBlock>,
184 policy: EvictionPolicy,
185) -> Option<String> {
186 let candidates: Vec<&BcmCachedBlock> = tier_map.values().filter(|b| !b.pinned).collect();
187 if candidates.is_empty() {
188 return None;
189 }
190
191 let victim = match policy {
192 EvictionPolicy::LFU => candidates.iter().min_by(|a, b| {
193 a.access_count
194 .cmp(&b.access_count)
195 .then_with(|| a.last_accessed.cmp(&b.last_accessed))
196 }),
197 EvictionPolicy::LRU | EvictionPolicy::TwoQ | EvictionPolicy::ARC => {
199 candidates.iter().min_by_key(|b| b.last_accessed)
200 }
201 };
202
203 victim.map(|b| b.cid.clone())
204}
205
206fn tier_bytes(tier_map: &HashMap<String, BcmCachedBlock>) -> u64 {
208 tier_map.values().map(|b| b.byte_size()).sum()
209}
210
211#[derive(Debug)]
221pub struct BlockCacheManager {
222 pub config: BcmCacheConfig,
224
225 pub hot: HashMap<String, BcmCachedBlock>,
227 pub warm: HashMap<String, BcmCachedBlock>,
228 pub cold: HashMap<String, BcmCachedBlock>,
229
230 evictions: u64,
232 promotions: u64,
233 demotions: u64,
234}
235
236impl BlockCacheManager {
237 #[must_use]
241 pub fn new(config: BcmCacheConfig) -> Self {
242 Self {
243 config,
244 hot: HashMap::new(),
245 warm: HashMap::new(),
246 cold: HashMap::new(),
247 evictions: 0,
248 promotions: 0,
249 demotions: 0,
250 }
251 }
252
253 pub fn insert(&mut self, cid: String, data: Vec<u8>, now: u64) -> bool {
260 if self.contains(&cid) {
261 return false;
262 }
263
264 let needed = data.len() as u64;
265
266 let cold_used = tier_bytes(&self.cold);
268 if cold_used + needed > self.config.max_cold_bytes {
269 let to_free = (cold_used + needed).saturating_sub(self.config.max_cold_bytes);
270 self.evict_to_fit(CacheTier::Cold, to_free, now);
271 }
272
273 let block = BcmCachedBlock::new(cid.clone(), data, now);
274 self.cold.insert(cid, block);
275 true
276 }
277
278 pub fn get(&mut self, cid: &str, now: u64) -> Option<&[u8]> {
285 let found_tier = if let Some(block) = self.hot.get_mut(cid) {
287 block.access_count = block.access_count.saturating_add(1);
288 block.last_accessed = now;
289 Some(CacheTier::Hot)
290 } else if let Some(block) = self.warm.get_mut(cid) {
291 block.access_count = block.access_count.saturating_add(1);
292 block.last_accessed = now;
293 Some(CacheTier::Warm)
294 } else if let Some(block) = self.cold.get_mut(cid) {
295 block.access_count = block.access_count.saturating_add(1);
296 block.last_accessed = now;
297 Some(CacheTier::Cold)
298 } else {
299 None
300 };
301
302 found_tier?;
303
304 let cid_owned = cid.to_owned();
306 self.promote(&cid_owned, now);
307
308 if let Some(block) = self.hot.get(cid) {
310 return Some(&block.data);
311 }
312 if let Some(block) = self.warm.get(cid) {
313 return Some(&block.data);
314 }
315 if let Some(block) = self.cold.get(cid) {
316 return Some(&block.data);
317 }
318 None
319 }
320
321 pub fn pin(&mut self, cid: &str) -> bool {
325 if let Some(b) = self.hot.get_mut(cid) {
326 b.pinned = true;
327 return true;
328 }
329 if let Some(b) = self.warm.get_mut(cid) {
330 b.pinned = true;
331 return true;
332 }
333 if let Some(b) = self.cold.get_mut(cid) {
334 b.pinned = true;
335 return true;
336 }
337 false
338 }
339
340 pub fn unpin(&mut self, cid: &str) -> bool {
342 if let Some(b) = self.hot.get_mut(cid) {
343 b.pinned = false;
344 return true;
345 }
346 if let Some(b) = self.warm.get_mut(cid) {
347 b.pinned = false;
348 return true;
349 }
350 if let Some(b) = self.cold.get_mut(cid) {
351 b.pinned = false;
352 return true;
353 }
354 false
355 }
356
357 pub fn evict_to_fit(&mut self, tier: CacheTier, needed_bytes: u64, now: u64) -> u64 {
364 let _ = now;
366 let mut freed: u64 = 0;
367 let policy = self.config.eviction_policy;
368
369 let tier_map = match tier {
370 CacheTier::Hot => &mut self.hot,
371 CacheTier::Warm => &mut self.warm,
372 CacheTier::Cold => &mut self.cold,
373 };
374
375 while freed < needed_bytes {
376 match choose_victim(tier_map, policy) {
377 None => break,
378 Some(victim_cid) => {
379 if let Some(block) = tier_map.remove(&victim_cid) {
380 freed += block.byte_size();
381 self.evictions += 1;
382 }
383 }
384 }
385 }
386 freed
387 }
388
389 pub fn promote(&mut self, cid: &str, now: u64) {
398 let (current_tier, access_count) = {
400 if let Some(b) = self.hot.get(cid) {
401 (CacheTier::Hot, b.access_count)
402 } else if let Some(b) = self.warm.get(cid) {
403 (CacheTier::Warm, b.access_count)
404 } else if let Some(b) = self.cold.get(cid) {
405 (CacheTier::Cold, b.access_count)
406 } else {
407 return; }
409 };
410
411 let target_tier = {
412 if access_count >= self.config.hot_threshold {
413 CacheTier::Hot
414 } else if access_count >= self.config.warm_threshold {
415 CacheTier::Warm
416 } else {
417 return;
419 }
420 };
421
422 if target_tier == current_tier || (target_tier as u8) <= (current_tier as u8) {
423 if current_tier == target_tier {
425 return;
426 }
427 if target_tier.priority() <= current_tier.priority() {
429 return;
430 }
431 }
432
433 self.move_block(cid, current_tier, target_tier, now, true);
435 }
436
437 pub fn demote(&mut self, cid: &str, now: u64) {
440 let current_tier = {
441 if self.hot.contains_key(cid) {
442 CacheTier::Hot
443 } else if self.warm.contains_key(cid) {
444 CacheTier::Warm
445 } else {
446 return;
448 }
449 };
450
451 let target_tier = current_tier.demoted();
452 if target_tier == current_tier {
453 return;
454 }
455
456 self.move_block(cid, current_tier, target_tier, now, false);
457 }
458
459 fn move_block(
463 &mut self,
464 cid: &str,
465 from: CacheTier,
466 to: CacheTier,
467 now: u64,
468 is_promotion: bool,
469 ) {
470 let mut block = match from {
472 CacheTier::Hot => self.hot.remove(cid),
473 CacheTier::Warm => self.warm.remove(cid),
474 CacheTier::Cold => self.cold.remove(cid),
475 };
476
477 let block = match block.as_mut() {
478 Some(b) => b,
479 None => return,
480 };
481
482 block.tier = to;
483 block.last_accessed = now;
484
485 let block_size = block.byte_size();
487 let max_dest = match to {
488 CacheTier::Hot => self.config.max_hot_bytes,
489 CacheTier::Warm => self.config.max_warm_bytes,
490 CacheTier::Cold => self.config.max_cold_bytes,
491 };
492 let dest_used = match to {
493 CacheTier::Hot => tier_bytes(&self.hot),
494 CacheTier::Warm => tier_bytes(&self.warm),
495 CacheTier::Cold => tier_bytes(&self.cold),
496 };
497 if dest_used + block_size > max_dest {
498 let to_free = (dest_used + block_size).saturating_sub(max_dest);
499 self.evict_to_fit(to, to_free, now);
500 }
501
502 let block = block.clone();
503 match to {
504 CacheTier::Hot => {
505 self.hot.insert(cid.to_owned(), block);
506 }
507 CacheTier::Warm => {
508 self.warm.insert(cid.to_owned(), block);
509 }
510 CacheTier::Cold => {
511 self.cold.insert(cid.to_owned(), block);
512 }
513 }
514
515 if is_promotion {
516 self.promotions += 1;
517 } else {
518 self.demotions += 1;
519 }
520 }
521
522 #[must_use]
526 pub fn total_hot_bytes(&self) -> u64 {
527 tier_bytes(&self.hot)
528 }
529
530 #[must_use]
532 pub fn total_warm_bytes(&self) -> u64 {
533 tier_bytes(&self.warm)
534 }
535
536 #[must_use]
538 pub fn total_cold_bytes(&self) -> u64 {
539 tier_bytes(&self.cold)
540 }
541
542 #[must_use]
544 pub fn total_bytes(&self) -> u64 {
545 self.total_hot_bytes() + self.total_warm_bytes() + self.total_cold_bytes()
546 }
547
548 #[must_use]
552 pub fn len(&self) -> usize {
553 self.hot.len() + self.warm.len() + self.cold.len()
554 }
555
556 #[must_use]
558 pub fn is_empty(&self) -> bool {
559 self.len() == 0
560 }
561
562 pub fn remove(&mut self, cid: &str) -> bool {
568 let is_pinned = self.hot.get(cid).is_some_and(|b| b.pinned)
570 || self.warm.get(cid).is_some_and(|b| b.pinned)
571 || self.cold.get(cid).is_some_and(|b| b.pinned);
572
573 if is_pinned {
574 return false;
575 }
576
577 self.hot.remove(cid).is_some()
578 || self.warm.remove(cid).is_some()
579 || self.cold.remove(cid).is_some()
580 }
581
582 #[must_use]
586 pub fn contains(&self, cid: &str) -> bool {
587 self.hot.contains_key(cid) || self.warm.contains_key(cid) || self.cold.contains_key(cid)
588 }
589
590 #[must_use]
592 pub fn tier_of(&self, cid: &str) -> Option<CacheTier> {
593 if self.hot.contains_key(cid) {
594 Some(CacheTier::Hot)
595 } else if self.warm.contains_key(cid) {
596 Some(CacheTier::Warm)
597 } else if self.cold.contains_key(cid) {
598 Some(CacheTier::Cold)
599 } else {
600 None
601 }
602 }
603
604 #[must_use]
608 pub fn stats(&self) -> BcmCacheStats {
609 let pinned_count = self.hot.values().filter(|b| b.pinned).count()
610 + self.warm.values().filter(|b| b.pinned).count()
611 + self.cold.values().filter(|b| b.pinned).count();
612
613 BcmCacheStats {
614 hot_count: self.hot.len(),
615 warm_count: self.warm.len(),
616 cold_count: self.cold.len(),
617 hot_bytes: self.total_hot_bytes(),
618 warm_bytes: self.total_warm_bytes(),
619 cold_bytes: self.total_cold_bytes(),
620 pinned_count,
621 evictions: self.evictions,
622 promotions: self.promotions,
623 demotions: self.demotions,
624 }
625 }
626}
627
628impl From<CacheTier> for u8 {
633 fn from(tier: CacheTier) -> u8 {
634 tier.priority()
635 }
636}
637
638#[cfg(test)]
643mod tests {
644 use super::{
645 choose_victim, tier_bytes, BcmCacheConfig, BcmCacheStats, BcmCachedBlock,
646 BlockCacheManager, CacheTier, EvictionPolicy,
647 };
648 use std::collections::HashMap;
649
650 fn default_config() -> BcmCacheConfig {
653 BcmCacheConfig {
654 max_hot_bytes: 1024,
655 max_warm_bytes: 2048,
656 max_cold_bytes: 4096,
657 hot_threshold: 5,
658 warm_threshold: 2,
659 eviction_policy: EvictionPolicy::LRU,
660 }
661 }
662
663 fn make_manager() -> BlockCacheManager {
664 BlockCacheManager::new(default_config())
665 }
666
667 fn make_block(cid: &str, size: usize, now: u64) -> BcmCachedBlock {
668 BcmCachedBlock::new(cid.to_owned(), vec![0u8; size], now)
669 }
670
671 #[test]
674 fn test_tier_priority() {
675 assert_eq!(CacheTier::Hot.priority(), 3);
676 assert_eq!(CacheTier::Warm.priority(), 2);
677 assert_eq!(CacheTier::Cold.priority(), 1);
678 }
679
680 #[test]
681 fn test_tier_promoted() {
682 assert_eq!(CacheTier::Cold.promoted(), CacheTier::Warm);
683 assert_eq!(CacheTier::Warm.promoted(), CacheTier::Hot);
684 assert_eq!(CacheTier::Hot.promoted(), CacheTier::Hot);
685 }
686
687 #[test]
688 fn test_tier_demoted() {
689 assert_eq!(CacheTier::Hot.demoted(), CacheTier::Warm);
690 assert_eq!(CacheTier::Warm.demoted(), CacheTier::Cold);
691 assert_eq!(CacheTier::Cold.demoted(), CacheTier::Cold);
692 }
693
694 #[test]
695 fn test_tier_from_u8_priority() {
696 let hot: u8 = CacheTier::Hot.into();
697 let warm: u8 = CacheTier::Warm.into();
698 let cold: u8 = CacheTier::Cold.into();
699 assert!(hot > warm && warm > cold);
700 }
701
702 #[test]
705 fn test_cached_block_initial_state() {
706 let block = make_block("cid1", 128, 1000);
707 assert_eq!(block.cid, "cid1");
708 assert_eq!(block.data.len(), 128);
709 assert_eq!(block.tier, CacheTier::Cold);
710 assert_eq!(block.inserted_at, 1000);
711 assert_eq!(block.last_accessed, 1000);
712 assert_eq!(block.access_count, 0);
713 assert!(!block.pinned);
714 }
715
716 #[test]
717 fn test_cached_block_byte_size() {
718 let block = make_block("cid2", 64, 0);
719 assert_eq!(block.byte_size(), 64);
720 }
721
722 #[test]
725 fn test_tier_bytes_empty() {
726 let map: HashMap<String, BcmCachedBlock> = HashMap::new();
727 assert_eq!(tier_bytes(&map), 0);
728 }
729
730 #[test]
731 fn test_tier_bytes_sum() {
732 let mut map = HashMap::new();
733 map.insert("a".to_owned(), make_block("a", 100, 0));
734 map.insert("b".to_owned(), make_block("b", 200, 0));
735 assert_eq!(tier_bytes(&map), 300);
736 }
737
738 #[test]
741 fn test_choose_victim_empty() {
742 let map: HashMap<String, BcmCachedBlock> = HashMap::new();
743 assert!(choose_victim(&map, EvictionPolicy::LRU).is_none());
744 }
745
746 #[test]
747 fn test_choose_victim_all_pinned() {
748 let mut map = HashMap::new();
749 let mut b = make_block("cid1", 10, 0);
750 b.pinned = true;
751 map.insert("cid1".to_owned(), b);
752 assert!(choose_victim(&map, EvictionPolicy::LRU).is_none());
753 }
754
755 #[test]
756 fn test_choose_victim_lru_selects_oldest() {
757 let mut map = HashMap::new();
758 let mut b1 = make_block("cid_old", 10, 0);
759 b1.last_accessed = 1;
760 let mut b2 = make_block("cid_new", 10, 0);
761 b2.last_accessed = 100;
762 map.insert("cid_old".to_owned(), b1);
763 map.insert("cid_new".to_owned(), b2);
764 let victim = choose_victim(&map, EvictionPolicy::LRU).unwrap();
765 assert_eq!(victim, "cid_old");
766 }
767
768 #[test]
769 fn test_choose_victim_lfu_selects_least_frequent() {
770 let mut map = HashMap::new();
771 let mut b1 = make_block("cid_rare", 10, 0);
772 b1.access_count = 1;
773 b1.last_accessed = 50;
774 let mut b2 = make_block("cid_freq", 10, 0);
775 b2.access_count = 99;
776 b2.last_accessed = 10;
777 map.insert("cid_rare".to_owned(), b1);
778 map.insert("cid_freq".to_owned(), b2);
779 let victim = choose_victim(&map, EvictionPolicy::LFU).unwrap();
780 assert_eq!(victim, "cid_rare");
781 }
782
783 #[test]
784 fn test_choose_victim_lfu_tiebreak_by_last_accessed() {
785 let mut map = HashMap::new();
786 let mut b1 = make_block("cid_a", 10, 0);
787 b1.access_count = 3;
788 b1.last_accessed = 5;
789 let mut b2 = make_block("cid_b", 10, 0);
790 b2.access_count = 3;
791 b2.last_accessed = 10;
792 map.insert("cid_a".to_owned(), b1);
793 map.insert("cid_b".to_owned(), b2);
794 let victim = choose_victim(&map, EvictionPolicy::LFU).unwrap();
795 assert_eq!(victim, "cid_a");
796 }
797
798 #[test]
801 fn test_new_empty() {
802 let mgr = make_manager();
803 assert!(mgr.is_empty());
804 assert_eq!(mgr.len(), 0);
805 }
806
807 #[test]
808 fn test_insert_basic() {
809 let mut mgr = make_manager();
810 assert!(mgr.insert("cid1".to_owned(), vec![1u8; 64], 100));
811 assert!(!mgr.is_empty());
812 assert_eq!(mgr.len(), 1);
813 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
814 }
815
816 #[test]
817 fn test_insert_duplicate_returns_false() {
818 let mut mgr = make_manager();
819 assert!(mgr.insert("cid1".to_owned(), vec![0u8; 64], 100));
820 assert!(!mgr.insert("cid1".to_owned(), vec![1u8; 64], 101));
821 }
822
823 #[test]
824 fn test_insert_multiple() {
825 let mut mgr = make_manager();
826 for i in 0..5u64 {
827 assert!(mgr.insert(format!("cid{i}"), vec![0u8; 50], i));
828 }
829 assert_eq!(mgr.len(), 5);
830 }
831
832 #[test]
833 fn test_contains() {
834 let mut mgr = make_manager();
835 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
836 assert!(mgr.contains("cid1"));
837 assert!(!mgr.contains("cid_missing"));
838 }
839
840 #[test]
841 fn test_get_returns_data() {
842 let mut mgr = make_manager();
843 let data = vec![42u8; 64];
844 mgr.insert("cid1".to_owned(), data.clone(), 0);
845 let retrieved = mgr.get("cid1", 1);
846 assert_eq!(retrieved, Some(data.as_slice()));
847 }
848
849 #[test]
850 fn test_get_missing() {
851 let mut mgr = make_manager();
852 assert!(mgr.get("cid_missing", 0).is_none());
853 }
854
855 #[test]
856 fn test_get_updates_access_count() {
857 let mut mgr = make_manager();
858 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
859 mgr.get("cid1", 1);
860 let block = mgr.cold.get("cid1").unwrap();
863 assert_eq!(block.access_count, 1);
864 }
865
866 #[test]
867 fn test_get_updates_last_accessed() {
868 let mut mgr = make_manager();
869 mgr.insert("cid1".to_owned(), vec![0u8; 10], 100);
870 mgr.get("cid1", 200);
871 let ts = mgr
873 .cold
874 .get("cid1")
875 .or_else(|| mgr.warm.get("cid1"))
876 .or_else(|| mgr.hot.get("cid1"))
877 .map(|b| b.last_accessed)
878 .unwrap_or(0);
879 assert_eq!(ts, 200);
880 }
881
882 #[test]
885 fn test_promote_cold_to_warm() {
886 let mut mgr = make_manager();
887 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
888 for i in 1..=2u64 {
890 mgr.get("cid1", i);
891 }
892 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Warm));
893 assert_eq!(mgr.stats().promotions, 1);
894 }
895
896 #[test]
897 fn test_promote_warm_to_hot() {
898 let mut mgr = make_manager();
899 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
900 for i in 1..=5u64 {
902 mgr.get("cid1", i);
903 }
904 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Hot));
905 let s = mgr.stats();
906 assert!(s.promotions >= 2);
908 }
909
910 #[test]
911 fn test_no_promotion_below_warm_threshold() {
912 let mut mgr = make_manager();
913 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
914 mgr.get("cid1", 1);
916 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
917 assert_eq!(mgr.stats().promotions, 0);
918 }
919
920 #[test]
923 fn test_demote_hot_to_warm() {
924 let mut mgr = make_manager();
925 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
926 for i in 1..=5u64 {
928 mgr.get("cid1", i);
929 }
930 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Hot));
931 mgr.demote("cid1", 10);
932 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Warm));
933 assert_eq!(mgr.stats().demotions, 1);
934 }
935
936 #[test]
937 fn test_demote_warm_to_cold() {
938 let mut mgr = make_manager();
939 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
940 for i in 1..=2u64 {
941 mgr.get("cid1", i);
942 }
943 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Warm));
944 mgr.demote("cid1", 10);
945 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
946 }
947
948 #[test]
949 fn test_demote_cold_is_noop() {
950 let mut mgr = make_manager();
951 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
952 mgr.demote("cid1", 1);
953 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
954 assert_eq!(mgr.stats().demotions, 0);
955 }
956
957 #[test]
958 fn test_demote_missing_is_noop() {
959 let mut mgr = make_manager();
960 mgr.demote("cid_missing", 0);
961 assert_eq!(mgr.stats().demotions, 0);
962 }
963
964 #[test]
967 fn test_pin_and_unpin() {
968 let mut mgr = make_manager();
969 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
970 assert!(mgr.pin("cid1"));
971 assert_eq!(mgr.stats().pinned_count, 1);
972 assert!(mgr.unpin("cid1"));
973 assert_eq!(mgr.stats().pinned_count, 0);
974 }
975
976 #[test]
977 fn test_pin_missing() {
978 let mut mgr = make_manager();
979 assert!(!mgr.pin("cid_missing"));
980 }
981
982 #[test]
983 fn test_unpin_missing() {
984 let mut mgr = make_manager();
985 assert!(!mgr.unpin("cid_missing"));
986 }
987
988 #[test]
989 fn test_pinned_block_not_evicted() {
990 let mut mgr = make_manager();
991 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
992 mgr.pin("cid1");
993 let freed = mgr.evict_to_fit(CacheTier::Cold, 10, 1);
995 assert_eq!(freed, 0);
996 assert!(mgr.contains("cid1"));
997 }
998
999 #[test]
1000 fn test_remove_unpinned() {
1001 let mut mgr = make_manager();
1002 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
1003 assert!(mgr.remove("cid1"));
1004 assert!(!mgr.contains("cid1"));
1005 }
1006
1007 #[test]
1008 fn test_remove_pinned_fails() {
1009 let mut mgr = make_manager();
1010 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
1011 mgr.pin("cid1");
1012 assert!(!mgr.remove("cid1"));
1013 assert!(mgr.contains("cid1"));
1014 }
1015
1016 #[test]
1017 fn test_remove_missing() {
1018 let mut mgr = make_manager();
1019 assert!(!mgr.remove("cid_missing"));
1020 }
1021
1022 #[test]
1025 fn test_total_bytes() {
1026 let mut mgr = make_manager();
1027 mgr.insert("cid1".to_owned(), vec![0u8; 100], 0);
1028 assert_eq!(mgr.total_bytes(), 100);
1029 assert_eq!(mgr.total_cold_bytes(), 100);
1030 assert_eq!(mgr.total_hot_bytes(), 0);
1031 assert_eq!(mgr.total_warm_bytes(), 0);
1032 }
1033
1034 #[test]
1035 fn test_bytes_after_promotion() {
1036 let mut mgr = make_manager();
1037 mgr.insert("cid1".to_owned(), vec![0u8; 100], 0);
1038 for i in 1..=2u64 {
1039 mgr.get("cid1", i);
1040 }
1041 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Warm));
1042 assert_eq!(mgr.total_warm_bytes(), 100);
1043 assert_eq!(mgr.total_cold_bytes(), 0);
1044 assert_eq!(mgr.total_bytes(), 100);
1045 }
1046
1047 #[test]
1050 fn test_evict_to_fit_frees_bytes() {
1051 let mut mgr = make_manager();
1052 mgr.insert("cid1".to_owned(), vec![0u8; 100], 0);
1053 mgr.insert("cid2".to_owned(), vec![0u8; 100], 1);
1054 let freed = mgr.evict_to_fit(CacheTier::Cold, 100, 2);
1055 assert!(freed >= 100);
1056 }
1057
1058 #[test]
1059 fn test_evict_empty_tier_returns_zero() {
1060 let mut mgr = make_manager();
1061 let freed = mgr.evict_to_fit(CacheTier::Hot, 100, 0);
1062 assert_eq!(freed, 0);
1063 }
1064
1065 #[test]
1066 fn test_auto_eviction_on_cold_overflow() {
1067 let mut mgr = make_manager();
1069 for i in 0..50u64 {
1071 mgr.insert(format!("cid{i}"), vec![0u8; 100], i);
1072 }
1073 assert!(
1074 mgr.total_cold_bytes() <= 4096,
1075 "Cold tier exceeded capacity"
1076 );
1077 }
1078
1079 #[test]
1080 fn test_evictions_counter() {
1081 let mut mgr = make_manager();
1082 for i in 0..50u64 {
1083 mgr.insert(format!("cid{i}"), vec![0u8; 100], i);
1084 }
1085 assert!(mgr.stats().evictions > 0);
1086 }
1087
1088 #[test]
1091 fn test_stats_initial() {
1092 let mgr = make_manager();
1093 let s = mgr.stats();
1094 assert_eq!(s.hot_count, 0);
1095 assert_eq!(s.warm_count, 0);
1096 assert_eq!(s.cold_count, 0);
1097 assert_eq!(s.evictions, 0);
1098 assert_eq!(s.promotions, 0);
1099 assert_eq!(s.demotions, 0);
1100 }
1101
1102 #[test]
1103 fn test_stats_counts() {
1104 let mut mgr = make_manager();
1105 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
1106 mgr.insert("cid2".to_owned(), vec![0u8; 10], 1);
1107 let s = mgr.stats();
1108 assert_eq!(s.cold_count, 2);
1109 }
1110
1111 #[test]
1112 fn test_stats_default() {
1113 let s = BcmCacheStats::default();
1114 assert_eq!(s.evictions, 0);
1115 assert_eq!(s.hot_count, 0);
1116 }
1117
1118 #[test]
1121 fn test_tier_of_missing() {
1122 let mgr = make_manager();
1123 assert!(mgr.tier_of("missing").is_none());
1124 }
1125
1126 #[test]
1127 fn test_tier_of_cold() {
1128 let mut mgr = make_manager();
1129 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
1130 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
1131 }
1132
1133 #[test]
1136 fn test_lfu_evicts_least_frequent() {
1137 let config = BcmCacheConfig {
1138 max_cold_bytes: 200,
1139 eviction_policy: EvictionPolicy::LFU,
1140 ..default_config()
1141 };
1142 let mut mgr = BlockCacheManager::new(config);
1143
1144 mgr.insert("cid_rare".to_owned(), vec![0u8; 100], 0);
1145 mgr.get("cid_rare", 1);
1147
1148 mgr.insert("cid_freq".to_owned(), vec![0u8; 100], 2);
1149 mgr.insert("cid_new".to_owned(), vec![0u8; 100], 3);
1157
1158 assert!(
1160 !mgr.contains("cid_freq"),
1161 "cid_freq should have been evicted by LFU"
1162 );
1163 assert!(mgr.contains("cid_rare"), "cid_rare should still be present");
1164 }
1165
1166 #[test]
1169 fn test_twoq_behaves_like_lru() {
1170 let config = BcmCacheConfig {
1171 max_cold_bytes: 200,
1172 eviction_policy: EvictionPolicy::TwoQ,
1173 ..default_config()
1174 };
1175 let mut mgr = BlockCacheManager::new(config);
1176 mgr.insert("cid_old".to_owned(), vec![0u8; 100], 0);
1177 mgr.insert("cid_new".to_owned(), vec![0u8; 100], 10);
1178 mgr.insert("cid_extra".to_owned(), vec![0u8; 100], 20);
1180 assert!(!mgr.contains("cid_old"));
1182 }
1183
1184 #[test]
1185 fn test_arc_behaves_like_lru() {
1186 let config = BcmCacheConfig {
1187 max_cold_bytes: 200,
1188 eviction_policy: EvictionPolicy::ARC,
1189 ..default_config()
1190 };
1191 let mut mgr = BlockCacheManager::new(config);
1192 mgr.insert("cid_old".to_owned(), vec![0u8; 100], 0);
1193 mgr.insert("cid_new".to_owned(), vec![0u8; 100], 10);
1194 mgr.insert("cid_extra".to_owned(), vec![0u8; 100], 20);
1195 assert!(!mgr.contains("cid_old"));
1196 }
1197
1198 #[test]
1201 fn test_explicit_promote_missing_is_noop() {
1202 let mut mgr = make_manager();
1203 mgr.promote("cid_missing", 0); }
1205
1206 #[test]
1207 fn test_explicit_demote_on_hot_block() {
1208 let mut mgr = make_manager();
1209 mgr.insert("cid1".to_owned(), vec![0u8; 10], 0);
1210 for i in 1..=5u64 {
1211 mgr.get("cid1", i);
1212 }
1213 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Hot));
1214
1215 mgr.demote("cid1", 10);
1216 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Warm));
1217 mgr.demote("cid1", 11);
1218 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
1219 mgr.demote("cid1", 12);
1221 assert_eq!(mgr.tier_of("cid1"), Some(CacheTier::Cold));
1222 }
1223
1224 #[test]
1227 fn test_default_config_values() {
1228 let cfg = BcmCacheConfig::default();
1229 assert_eq!(cfg.max_hot_bytes, 64 * 1024 * 1024);
1230 assert_eq!(cfg.max_warm_bytes, 128 * 1024 * 1024);
1231 assert_eq!(cfg.max_cold_bytes, 256 * 1024 * 1024);
1232 assert_eq!(cfg.hot_threshold, 10);
1233 assert_eq!(cfg.warm_threshold, 3);
1234 }
1235
1236 #[test]
1239 fn test_large_insert_respects_cold_cap() {
1240 let mut mgr = make_manager(); for i in 0..100u64 {
1242 mgr.insert(format!("c{i}"), vec![9u8; 100], i);
1243 }
1244 assert!(
1245 mgr.total_cold_bytes() <= 4096 + 100,
1246 "cold bytes {} exceed cap",
1247 mgr.total_cold_bytes()
1248 );
1249 }
1250
1251 #[test]
1252 fn test_total_bytes_consistent() {
1253 let mut mgr = make_manager();
1254 for i in 0..10u64 {
1255 mgr.insert(format!("c{i}"), vec![0u8; 50], i);
1256 }
1257 let s = mgr.stats();
1258 assert_eq!(s.hot_bytes + s.warm_bytes + s.cold_bytes, mgr.total_bytes());
1259 }
1260}