Skip to main content

oximedia_cache/
two_queue.rs

1//! 2Q (two-queue) eviction policy — a scan-resistant alternative to LRU.
2//!
3//! The 2Q algorithm maintains three queues:
4//!
5//! | Queue | Purpose |
6//! |-------|---------|
7//! | **A1in** | FIFO buffer for recently inserted items (first access) |
8//! | **A1out** | Ghost queue: tracks keys recently evicted from A1in |
9//! | **Am** | LRU queue for items accessed at least twice (promoted from A1in or A1out hit) |
10//!
11//! On a cache miss:
12//! - If the key is in A1out (ghost hit), it is admitted directly to Am.
13//! - Otherwise it enters A1in.
14//!
15//! On a cache hit:
16//! - If in Am, the entry is moved to the MRU position of Am.
17//! - If in A1in, no promotion happens yet (FIFO within A1in).
18//!
19//! This prevents sequential scans from polluting the long-term cache (Am).
20//!
21//! # Reference
22//!
23//! Theodore Johnson and Dennis Shasha, "2Q: A Low Overhead High Performance
24//! Buffer Management Replacement Algorithm," VLDB 1994.
25
26use std::collections::{HashMap, HashSet, VecDeque};
27
28// ── TwoQueueCache ───────────────────────────────────────────────────────────
29
30/// A 2Q cache with configurable total capacity and A1in/A1out sizing.
31///
32/// # Type parameters
33/// * `K` — key type (must be `Eq + Hash + Clone`).
34/// * `V` — value type.
35pub struct TwoQueueCache<K: Eq + std::hash::Hash + Clone, V> {
36    /// Maximum total entries (Am + A1in).
37    capacity: usize,
38    /// Target size for A1in (the FIFO admission buffer), typically ~25% of
39    /// capacity.
40    a1in_capacity: usize,
41    /// Target size for A1out (the ghost list), typically ~50% of capacity.
42    a1out_capacity: usize,
43
44    // ── A1in: FIFO buffer for first-access items ────────────────────────
45    /// Maps keys in A1in to their values.
46    a1in_map: HashMap<K, V>,
47    /// FIFO order of keys in A1in (front = oldest).
48    a1in_queue: VecDeque<K>,
49
50    // ── Am: LRU buffer for frequently accessed items ────────────────────
51    /// Maps keys in Am to their values.
52    am_map: HashMap<K, V>,
53    /// LRU order of keys in Am (front = LRU, back = MRU).
54    am_queue: VecDeque<K>,
55
56    // ── A1out: ghost list tracking recently evicted A1in keys ───────────
57    /// FIFO queue of ghost keys.
58    a1out_queue: VecDeque<K>,
59    /// Set of keys in A1out for O(1) membership tests.
60    a1out_set: HashSet<K>,
61
62    // ── Stats ───────────────────────────────────────────────────────────
63    hits: u64,
64    misses: u64,
65    evictions: u64,
66}
67
68/// Snapshot of 2Q cache statistics.
69#[derive(Debug, Clone)]
70pub struct TwoQueueStats {
71    /// Number of cache hits.
72    pub hits: u64,
73    /// Number of cache misses.
74    pub misses: u64,
75    /// Number of evictions.
76    pub evictions: u64,
77    /// Number of entries in A1in.
78    pub a1in_len: usize,
79    /// Number of entries in Am.
80    pub am_len: usize,
81    /// Number of ghost entries in A1out.
82    pub a1out_len: usize,
83    /// Total capacity.
84    pub capacity: usize,
85}
86
87impl<K: Eq + std::hash::Hash + Clone, V> TwoQueueCache<K, V> {
88    /// Create a new 2Q cache with the given capacity.
89    ///
90    /// A1in is sized to ~25% of capacity and A1out to ~50%.
91    pub fn new(capacity: usize) -> Self {
92        let cap = capacity.max(2);
93        let a1in_cap = (cap / 4).max(1);
94        let a1out_cap = (cap / 2).max(1);
95        Self {
96            capacity: cap,
97            a1in_capacity: a1in_cap,
98            a1out_capacity: a1out_cap,
99            a1in_map: HashMap::new(),
100            a1in_queue: VecDeque::new(),
101            am_map: HashMap::new(),
102            am_queue: VecDeque::new(),
103            a1out_queue: VecDeque::new(),
104            a1out_set: HashSet::new(),
105            hits: 0,
106            misses: 0,
107            evictions: 0,
108        }
109    }
110
111    /// Create a 2Q cache with explicit A1in and A1out sizing.
112    pub fn with_queue_sizes(capacity: usize, a1in_capacity: usize, a1out_capacity: usize) -> Self {
113        let cap = capacity.max(2);
114        Self {
115            capacity: cap,
116            a1in_capacity: a1in_capacity.max(1),
117            a1out_capacity: a1out_capacity.max(1),
118            a1in_map: HashMap::new(),
119            a1in_queue: VecDeque::new(),
120            am_map: HashMap::new(),
121            am_queue: VecDeque::new(),
122            a1out_queue: VecDeque::new(),
123            a1out_set: HashSet::new(),
124            hits: 0,
125            misses: 0,
126            evictions: 0,
127        }
128    }
129
130    /// Look up `key`.  On a hit in Am, promote to MRU.  On a hit in A1in,
131    /// the entry stays in place (FIFO semantics).
132    pub fn get(&mut self, key: &K) -> Option<&V> {
133        // Check Am first (more likely to hit).
134        if self.am_map.contains_key(key) {
135            self.hits += 1;
136            // Promote to MRU in Am.
137            self.am_queue.retain(|k| k != key);
138            self.am_queue.push_back(key.clone());
139            return self.am_map.get(key);
140        }
141        // Check A1in.
142        if self.a1in_map.contains_key(key) {
143            self.hits += 1;
144            // No promotion within A1in — FIFO semantics.
145            return self.a1in_map.get(key);
146        }
147        self.misses += 1;
148        None
149    }
150
151    /// Insert `(key, value)` into the cache.
152    ///
153    /// - If `key` is already in Am or A1in, its value is updated.
154    /// - If `key` is in A1out (ghost hit), it is admitted to Am.
155    /// - Otherwise it enters A1in.
156    pub fn insert(&mut self, key: K, value: V) {
157        // Already in Am → update value and promote.
158        if self.am_map.contains_key(&key) {
159            self.am_map.insert(key.clone(), value);
160            self.am_queue.retain(|k| k != &key);
161            self.am_queue.push_back(key);
162            return;
163        }
164        // Already in A1in → update value (no position change).
165        if let std::collections::hash_map::Entry::Occupied(mut e) = self.a1in_map.entry(key.clone())
166        {
167            e.insert(value);
168            return;
169        }
170        // Ghost hit in A1out → promote to Am.
171        if self.a1out_set.contains(&key) {
172            self.a1out_set.remove(&key);
173            self.a1out_queue.retain(|k| k != &key);
174            self.ensure_room_am();
175            self.am_map.insert(key.clone(), value);
176            self.am_queue.push_back(key);
177            return;
178        }
179        // Brand new key → insert into A1in.
180        self.ensure_room_a1in();
181        self.a1in_map.insert(key.clone(), value);
182        self.a1in_queue.push_back(key);
183    }
184
185    /// Remove `key` from the cache entirely (including ghost list).
186    pub fn remove(&mut self, key: &K) -> Option<V> {
187        // Check Am.
188        if let Some(v) = self.am_map.remove(key) {
189            self.am_queue.retain(|k| k != key);
190            return Some(v);
191        }
192        // Check A1in.
193        if let Some(v) = self.a1in_map.remove(key) {
194            self.a1in_queue.retain(|k| k != key);
195            return Some(v);
196        }
197        // Remove from ghost list too.
198        self.a1out_set.remove(key);
199        self.a1out_queue.retain(|k| k != key);
200        None
201    }
202
203    /// Returns `true` if the cache contains `key` (not counting ghosts).
204    pub fn contains(&self, key: &K) -> bool {
205        self.am_map.contains_key(key) || self.a1in_map.contains_key(key)
206    }
207
208    /// Total number of live entries.
209    pub fn len(&self) -> usize {
210        self.am_map.len() + self.a1in_map.len()
211    }
212
213    /// Returns `true` when the cache has no live entries.
214    pub fn is_empty(&self) -> bool {
215        self.len() == 0
216    }
217
218    /// Return a statistics snapshot.
219    pub fn stats(&self) -> TwoQueueStats {
220        TwoQueueStats {
221            hits: self.hits,
222            misses: self.misses,
223            evictions: self.evictions,
224            a1in_len: self.a1in_map.len(),
225            am_len: self.am_map.len(),
226            a1out_len: self.a1out_set.len(),
227            capacity: self.capacity,
228        }
229    }
230
231    /// Peek at `key` without updating access metadata or LRU order.
232    pub fn peek(&self, key: &K) -> Option<&V> {
233        if let Some(v) = self.am_map.get(key) {
234            return Some(v);
235        }
236        self.a1in_map.get(key)
237    }
238
239    /// Return a mutable reference to the value for `key` without changing
240    /// queue positions.
241    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
242        if let Some(v) = self.am_map.get_mut(key) {
243            self.hits += 1;
244            return Some(v);
245        }
246        if let Some(v) = self.a1in_map.get_mut(key) {
247            self.hits += 1;
248            return Some(v);
249        }
250        self.misses += 1;
251        None
252    }
253
254    /// Remove all entries, ghost list included.
255    pub fn clear(&mut self) {
256        self.a1in_map.clear();
257        self.a1in_queue.clear();
258        self.am_map.clear();
259        self.am_queue.clear();
260        self.a1out_queue.clear();
261        self.a1out_set.clear();
262    }
263
264    /// Return `true` if `key` is in the ghost list (A1out).
265    pub fn is_ghost(&self, key: &K) -> bool {
266        self.a1out_set.contains(key)
267    }
268
269    /// Return the number of entries in A1in.
270    pub fn a1in_len(&self) -> usize {
271        self.a1in_map.len()
272    }
273
274    /// Return the number of entries in Am.
275    pub fn am_len(&self) -> usize {
276        self.am_map.len()
277    }
278
279    /// Return the number of ghost entries in A1out.
280    pub fn a1out_len(&self) -> usize {
281        self.a1out_set.len()
282    }
283
284    // ── Internal helpers ────────────────────────────────────────────────────
285
286    /// Make room in A1in by evicting the oldest entry to A1out (ghost list).
287    fn ensure_room_a1in(&mut self) {
288        // Also respect total capacity.
289        while self.len() >= self.capacity {
290            self.evict_from_am_or_a1in();
291        }
292        while self.a1in_map.len() >= self.a1in_capacity {
293            self.evict_a1in_to_ghost();
294        }
295    }
296
297    /// Make room in Am.
298    fn ensure_room_am(&mut self) {
299        while self.len() >= self.capacity {
300            self.evict_from_am_or_a1in();
301        }
302    }
303
304    /// Evict the oldest entry from A1in, moving its key to A1out.
305    fn evict_a1in_to_ghost(&mut self) {
306        if let Some(key) = self.a1in_queue.pop_front() {
307            self.a1in_map.remove(&key);
308            self.evictions += 1;
309            // Add to ghost list.
310            self.a1out_queue.push_back(key.clone());
311            self.a1out_set.insert(key);
312            // Trim ghost list if over capacity.
313            while self.a1out_set.len() > self.a1out_capacity {
314                if let Some(ghost) = self.a1out_queue.pop_front() {
315                    self.a1out_set.remove(&ghost);
316                }
317            }
318        }
319    }
320
321    /// Evict from A1in (FIFO) or Am (LRU) to make room for total capacity.
322    ///
323    /// Per the 2Q paper, we prefer evicting from A1in first (since those are
324    /// one-access items) to protect the Am hot set.  This is what gives 2Q
325    /// its scan-resistance property.
326    fn evict_from_am_or_a1in(&mut self) {
327        if !self.a1in_map.is_empty() {
328            self.evict_a1in_to_ghost();
329        } else if let Some(key) = self.am_queue.pop_front() {
330            self.am_map.remove(&key);
331            self.evictions += 1;
332        }
333    }
334}
335
336// ── Tests ─────────────────────────────────────────────────────────────────────
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    // 1. Basic insert and get
343    #[test]
344    fn test_insert_and_get() {
345        let mut cache: TwoQueueCache<&str, i32> = TwoQueueCache::new(10);
346        cache.insert("a", 1);
347        cache.insert("b", 2);
348        assert_eq!(cache.get(&"a"), Some(&1));
349        assert_eq!(cache.get(&"b"), Some(&2));
350    }
351
352    // 2. Miss returns None
353    #[test]
354    fn test_miss() {
355        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
356        assert_eq!(cache.get(&99), None);
357    }
358
359    // 3. Contains
360    #[test]
361    fn test_contains() {
362        let mut cache: TwoQueueCache<&str, i32> = TwoQueueCache::new(10);
363        cache.insert("hello", 1);
364        assert!(cache.contains(&"hello"));
365        assert!(!cache.contains(&"world"));
366    }
367
368    // 4. Len and is_empty
369    #[test]
370    fn test_len_and_is_empty() {
371        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
372        assert!(cache.is_empty());
373        cache.insert(1, 10);
374        cache.insert(2, 20);
375        assert_eq!(cache.len(), 2);
376    }
377
378    // 5. Remove
379    #[test]
380    fn test_remove() {
381        let mut cache: TwoQueueCache<&str, i32> = TwoQueueCache::new(10);
382        cache.insert("x", 42);
383        let removed = cache.remove(&"x");
384        assert_eq!(removed, Some(42));
385        assert!(!cache.contains(&"x"));
386    }
387
388    // 6. Scan resistance: sequential scan does not pollute Am
389    #[test]
390    fn test_scan_resistance() {
391        // capacity=8, A1in=2, A1out=8 (generous ghost list).
392        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(8, 2, 8);
393        // Step 1: Insert 3 hot items → they enter A1in then flow to ghost
394        // as more items push them out.
395        cache.insert(1, 10);
396        cache.insert(2, 20);
397        cache.insert(3, 30); // A1in is full (cap=2), key 1 evicted to ghost.
398        cache.insert(4, 40); // key 2 evicted to ghost.
399        cache.insert(5, 50); // key 3 evicted to ghost.
400                             // Now keys 1,2,3 should be in A1out.
401        assert!(cache.is_ghost(&1), "key 1 should be in ghost");
402        assert!(cache.is_ghost(&2), "key 2 should be in ghost");
403        assert!(cache.is_ghost(&3), "key 3 should be in ghost");
404        // Step 2: Re-insert hot items → ghost hit → promoted to Am.
405        cache.insert(1, 100);
406        cache.insert(2, 200);
407        cache.insert(3, 300);
408        assert!(
409            cache.am_len() >= 3,
410            "Am should have 3 entries after ghost hits"
411        );
412        // Step 3: Flood with sequential scan.
413        for i in 1000..1050 {
414            cache.insert(i, i);
415        }
416        // Hot items in Am should survive the sequential scan.
417        assert!(
418            cache.am_len() >= 3,
419            "Am should still have entries after scan (got {})",
420            cache.am_len()
421        );
422        // Verify hot keys are still accessible.
423        for &k in &[1, 2, 3] {
424            assert!(cache.contains(&k), "hot key {k} should survive scan");
425        }
426    }
427
428    // 7. Ghost hit promotes to Am
429    #[test]
430    fn test_ghost_hit_promotes_to_am() {
431        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 2);
432        cache.insert(1, 10); // A1in
433        cache.insert(2, 20); // evicts 1 to ghost (A1in cap=1)
434                             // Now key 1 is in A1out.
435        cache.insert(1, 100); // ghost hit → Am
436        let s = cache.stats();
437        assert_eq!(s.am_len, 1, "ghost hit should put key in Am");
438        assert_eq!(cache.get(&1), Some(&100));
439    }
440
441    // 8. Stats counters
442    #[test]
443    fn test_stats() {
444        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
445        cache.insert(1, 10);
446        cache.get(&1); // hit
447        cache.get(&99); // miss
448        let s = cache.stats();
449        assert_eq!(s.hits, 1);
450        assert_eq!(s.misses, 1);
451    }
452
453    // 9. Update existing key in A1in
454    #[test]
455    fn test_update_in_a1in() {
456        let mut cache: TwoQueueCache<&str, i32> = TwoQueueCache::new(10);
457        cache.insert("k", 1);
458        cache.insert("k", 2);
459        assert_eq!(cache.get(&"k"), Some(&2));
460        assert_eq!(cache.len(), 1);
461    }
462
463    // 10. Update existing key in Am
464    #[test]
465    fn test_update_in_am() {
466        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 2);
467        cache.insert(1, 10);
468        cache.insert(2, 20); // evicts 1 to ghost
469        cache.insert(1, 100); // ghost hit → Am
470        cache.insert(1, 200); // update in Am
471        assert_eq!(cache.get(&1), Some(&200));
472    }
473
474    // 11. Capacity is respected
475    #[test]
476    fn test_capacity() {
477        let mut cache: TwoQueueCache<usize, usize> = TwoQueueCache::new(5);
478        for i in 0..100 {
479            cache.insert(i, i);
480        }
481        assert!(cache.len() <= 5, "len {} exceeds capacity 5", cache.len());
482    }
483
484    // 12. Remove absent key returns None
485    #[test]
486    fn test_remove_absent() {
487        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
488        assert_eq!(cache.remove(&42), None);
489    }
490
491    // 13. Custom queue sizes
492    #[test]
493    fn test_custom_queue_sizes() {
494        let cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(100, 10, 20);
495        let s = cache.stats();
496        assert_eq!(s.capacity, 100);
497    }
498
499    // 14. Am LRU eviction under Am pressure
500    #[test]
501    fn test_am_lru_eviction() {
502        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
503        // Build up Am entries via ghost hits.
504        for i in 0..10 {
505            cache.insert(i, i);
506        }
507        // Re-insert early keys that are now in ghost.
508        for i in 0..4 {
509            cache.insert(i, i * 100);
510        }
511        // Am should have at most capacity entries total.
512        assert!(cache.len() <= 4);
513    }
514
515    // 15. Hit in Am promotes to MRU
516    #[test]
517    fn test_am_hit_promotes_to_mru() {
518        // capacity=6, A1in=1, A1out=6 (large ghost to keep evicted keys).
519        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(6, 1, 6);
520        // Insert items — they enter A1in (cap=1), overflow to ghost.
521        for i in 0..6 {
522            cache.insert(i, i);
523        }
524        // Re-insert some items → ghost hit → promoted to Am.
525        for i in 0..4 {
526            cache.insert(i, i * 10);
527        }
528        assert!(cache.am_len() > 0, "Am should have entries");
529        // Access key 0 to promote to MRU within Am.
530        cache.get(&0);
531        // Key 0 should still be present.
532        let val = cache.get(&0);
533        assert!(val.is_some(), "key 0 should be promoted to MRU");
534    }
535
536    // ── Enhanced 2Q tests ───────────────────────────────────────────────────
537
538    // 16. peek does not update LRU order
539    #[test]
540    fn test_peek_no_side_effects() {
541        let mut cache: TwoQueueCache<&str, i32> = TwoQueueCache::new(10);
542        cache.insert("a", 1);
543        let val = cache.peek(&"a");
544        assert_eq!(val, Some(&1));
545        // Stats should not change from peek
546        let s = cache.stats();
547        assert_eq!(s.hits, 0);
548        assert_eq!(s.misses, 0);
549    }
550
551    // 17. peek absent key returns None
552    #[test]
553    fn test_peek_absent() {
554        let cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
555        assert_eq!(cache.peek(&99), None);
556    }
557
558    // 18. get_mut allows in-place mutation
559    #[test]
560    fn test_get_mut() {
561        let mut cache: TwoQueueCache<&str, i32> = TwoQueueCache::new(10);
562        cache.insert("a", 1);
563        if let Some(v) = cache.get_mut(&"a") {
564            *v = 42;
565        }
566        assert_eq!(cache.get(&"a"), Some(&42));
567    }
568
569    // 19. get_mut absent key records miss
570    #[test]
571    fn test_get_mut_absent() {
572        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
573        assert!(cache.get_mut(&99).is_none());
574        assert_eq!(cache.stats().misses, 1);
575    }
576
577    // 20. clear removes everything
578    #[test]
579    fn test_clear() {
580        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(10);
581        for i in 0..10 {
582            cache.insert(i, i);
583        }
584        cache.clear();
585        assert!(cache.is_empty());
586        assert_eq!(cache.len(), 0);
587        assert_eq!(cache.a1out_len(), 0);
588    }
589
590    // 21. is_ghost detects ghost entries
591    #[test]
592    fn test_is_ghost() {
593        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
594        cache.insert(1, 10); // A1in
595        cache.insert(2, 20); // evicts 1 to ghost (A1in cap=1)
596        assert!(cache.is_ghost(&1));
597        assert!(!cache.is_ghost(&2));
598        assert!(!cache.is_ghost(&99));
599    }
600
601    // 22. a1in_len / am_len / a1out_len
602    #[test]
603    fn test_queue_length_accessors() {
604        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
605        cache.insert(1, 10);
606        assert_eq!(cache.a1in_len(), 1);
607        assert_eq!(cache.am_len(), 0);
608        cache.insert(2, 20); // evicts 1 to ghost
609        assert_eq!(cache.a1in_len(), 1);
610        assert_eq!(cache.a1out_len(), 1);
611        // Ghost hit: re-insert 1 → Am
612        cache.insert(1, 100);
613        assert_eq!(cache.am_len(), 1);
614        assert_eq!(cache.a1out_len(), 0);
615    }
616
617    // 23. Large sequential workload maintains capacity
618    #[test]
619    fn test_large_sequential_workload() {
620        let cap = 20;
621        let mut cache: TwoQueueCache<usize, usize> = TwoQueueCache::new(cap);
622        for i in 0..1000 {
623            cache.insert(i, i * 2);
624        }
625        assert!(
626            cache.len() <= cap,
627            "cache exceeds capacity: {}",
628            cache.len()
629        );
630    }
631
632    // 24. Ghost list respects a1out_capacity
633    #[test]
634    fn test_ghost_list_bounded() {
635        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 3);
636        // Insert many items to fill ghost list
637        for i in 0..20 {
638            cache.insert(i, i);
639        }
640        assert!(cache.a1out_len() <= 3, "ghost list exceeds capacity");
641    }
642
643    // 25. peek on item in Am
644    #[test]
645    fn test_peek_am_item() {
646        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
647        cache.insert(1, 10);
648        cache.insert(2, 20); // evicts 1 to ghost
649        cache.insert(1, 100); // ghost hit → Am
650        assert_eq!(cache.peek(&1), Some(&100));
651    }
652
653    // 26. Remove from Am
654    #[test]
655    fn test_remove_from_am() {
656        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
657        cache.insert(1, 10);
658        cache.insert(2, 20); // evicts 1 to ghost
659        cache.insert(1, 100); // ghost hit → Am
660        let removed = cache.remove(&1);
661        assert_eq!(removed, Some(100));
662        assert!(!cache.contains(&1));
663    }
664
665    // 27. Remove also clears ghost entry
666    #[test]
667    fn test_remove_clears_ghost() {
668        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
669        cache.insert(1, 10);
670        cache.insert(2, 20); // evicts 1 to ghost
671        assert!(cache.is_ghost(&1));
672        cache.remove(&1); // should also clear ghost
673        assert!(!cache.is_ghost(&1));
674    }
675
676    // 28. Stats evictions counter
677    #[test]
678    fn test_stats_evictions() {
679        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::new(4);
680        for i in 0..10 {
681            cache.insert(i, i);
682        }
683        assert!(cache.stats().evictions > 0, "should have evictions");
684    }
685
686    // 29. Mixed Am and A1in access pattern
687    #[test]
688    fn test_mixed_access_pattern() {
689        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(6, 2, 4);
690        // Insert items
691        for i in 0..6 {
692            cache.insert(i, i * 10);
693        }
694        // Some items flow to ghost, re-insert for Am promotion
695        for i in 0..3 {
696            cache.insert(i, i * 100);
697        }
698        // Access items in Am
699        for i in 0..3 {
700            let val = cache.get(&i);
701            assert!(val.is_some(), "key {i} should be accessible");
702        }
703        assert!(cache.stats().hits >= 3);
704    }
705
706    // 30. get_mut on Am entry counts as hit
707    #[test]
708    fn test_get_mut_am_hit() {
709        let mut cache: TwoQueueCache<i32, i32> = TwoQueueCache::with_queue_sizes(4, 1, 4);
710        cache.insert(1, 10);
711        cache.insert(2, 20); // evicts 1 to ghost
712        cache.insert(1, 100); // ghost hit → Am
713        if let Some(v) = cache.get_mut(&1) {
714            *v = 999;
715        }
716        assert_eq!(cache.peek(&1), Some(&999));
717        assert!(cache.stats().hits >= 1);
718    }
719}