Skip to main content

nv_redfish_bmc_http/
cache.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! CAR (Clock with Adaptive Replacement) Cache Implementation
17//!
18//! Based on "CAR: Clock with Adaptive Replacement" by Bansal & Modha
19//! USENIX Conference on File and Storage Technologies, 2004
20//!
21//! This implementation follows the exact pseudocode from the [paper](https://www.usenix.org/legacy/publications/library/proceedings/fast04/tech/full_papers/bansal/bansal.pdf).
22
23use std::any::Any;
24use std::collections::HashMap;
25use std::hash::Hash;
26
27/// Information about an evicted cache entry.
28///
29/// When an entry is evicted from the cache, this struct holds both the key
30/// and value of the evicted entry. This is particularly useful for cleaning
31/// up related resources (like ETags) when entries are evicted.
32#[derive(Debug)]
33pub struct Evicted<K, V> {
34    /// The key of the evicted entry
35    pub key: K,
36    /// The value of the evicted entry
37    pub value: V,
38}
39
40impl<K, V> Evicted<K, V> {
41    /// Create a new Evicted struct
42    const fn new(key: K, value: V) -> Self {
43        Self { key, value }
44    }
45}
46
47/// A cache entry with reference bit for clock algorithm
48#[derive(Debug)]
49struct CacheEntry<K, V> {
50    key: K,
51    value: V,
52    /// Reference bit: 0 or 1 as per pseudocode
53    ref_bit: bool,
54}
55
56impl<K, V> CacheEntry<K, V> {
57    const fn new(key: K, value: V) -> Self {
58        Self {
59            key,
60            value,
61            ref_bit: false, // Always start with ref_bit = 0
62        }
63    }
64}
65
66/// Node in the ghost list doubly-linked structure
67#[derive(Debug, Clone)]
68struct GhostNode<K> {
69    key: K,
70    prev: Option<usize>,
71    next: Option<usize>,
72}
73
74/// Intrusive doubly linked list for ghost entries (B1, B2)
75#[derive(Debug)]
76struct GhostList<K> {
77    entries: Vec<Option<GhostNode<K>>>,
78    head: Option<usize>, // LRU end
79    tail: Option<usize>, // MRU end
80    free_slots: Vec<usize>,
81    size: usize,
82}
83
84impl<K: Clone> GhostList<K> {
85    fn new(capacity: usize) -> Self {
86        Self {
87            entries: vec![None; capacity],
88            head: None,
89            tail: None,
90            free_slots: (0..capacity).rev().collect(),
91            size: 0,
92        }
93    }
94
95    /// Insert at tail (MRU position) - O(1)
96    /// Returns `(slot, evicted_key)` where `evicted_key` is Some if an item was evicted
97    fn insert_at_tail(&mut self, key: K) -> Option<(usize, Option<K>)> {
98        // If we're at capacity, remove LRU first
99        let evicted_key = if self.free_slots.is_empty() {
100            self.remove_lru()
101        } else {
102            None
103        };
104
105        let slot = self.free_slots.pop()?;
106        let new_node = GhostNode {
107            key,
108            prev: self.tail,
109            next: None,
110        };
111
112        if let Some(old_tail) = self.tail {
113            if let Some(ref mut old_tail_node) = self.entries[old_tail] {
114                old_tail_node.next = Some(slot);
115            }
116        } else {
117            self.head = Some(slot);
118        }
119
120        self.tail = Some(slot);
121        self.entries[slot] = Some(new_node);
122        self.size += 1;
123
124        Some((slot, evicted_key))
125    }
126
127    /// Remove LRU (head) entry - O(1)
128    fn remove_lru(&mut self) -> Option<K> {
129        let head_slot = self.head?;
130        let head_node = self.entries[head_slot].take()?;
131
132        self.free_slots.push(head_slot);
133        self.size -= 1;
134
135        if self.size == 0 {
136            self.head = None;
137            self.tail = None;
138        } else {
139            self.head = head_node.next;
140            if let Some(new_head) = self.head {
141                if let Some(ref mut new_head_node) = self.entries[new_head] {
142                    new_head_node.prev = None;
143                }
144            }
145        }
146
147        Some(head_node.key)
148    }
149
150    /// Remove specific slot - O(1)
151    fn remove(&mut self, slot: usize) -> bool {
152        let Some(node) = self.entries[slot].take() else {
153            return false;
154        };
155
156        self.free_slots.push(slot);
157        self.size -= 1;
158
159        if self.size == 0 {
160            self.head = None;
161            self.tail = None;
162        } else {
163            if let Some(prev_slot) = node.prev {
164                if let Some(ref mut prev_node) = self.entries[prev_slot] {
165                    prev_node.next = node.next;
166                }
167            } else {
168                self.head = node.next;
169            }
170
171            if let Some(next_slot) = node.next {
172                if let Some(ref mut next_node) = self.entries[next_slot] {
173                    next_node.prev = node.prev;
174                }
175            } else {
176                self.tail = node.prev;
177            }
178        }
179
180        true
181    }
182
183    const fn len(&self) -> usize {
184        self.size
185    }
186}
187
188/// Clock-based list for T1 and T2
189#[derive(Debug)]
190struct ClockList<K, V> {
191    entries: Vec<Option<CacheEntry<K, V>>>,
192    hand: usize, // Clock hand position
193    free_slots: Vec<usize>,
194    size: usize,
195}
196
197impl<K: Clone, V> ClockList<K, V> {
198    fn new(capacity: usize) -> Self {
199        let mut entries = Vec::with_capacity(capacity);
200        for _ in 0..capacity {
201            entries.push(None);
202        }
203        Self {
204            entries,
205            hand: 0,
206            free_slots: (0..capacity).rev().collect(),
207            size: 0,
208        }
209    }
210
211    /// Insert at tail (any available slot)
212    fn insert_at_tail(&mut self, key: K, value: V) -> Option<usize> {
213        let slot = self.free_slots.pop()?;
214        self.entries[slot] = Some(CacheEntry::new(key, value));
215        self.size += 1;
216        Some(slot)
217    }
218
219    /// Get head page for clock algorithm
220    fn get_head_page(&mut self) -> Option<&mut CacheEntry<K, V>> {
221        // Find the entry at the current hand position
222        let start_hand = self.hand;
223        loop {
224            if self.size == 0 {
225                return None;
226            }
227
228            if self.entries[self.hand].is_some() {
229                return self.entries[self.hand].as_mut();
230            }
231
232            self.advance_hand();
233
234            // Prevent infinite loop
235            if self.hand == start_hand {
236                return None;
237            }
238        }
239    }
240
241    /// Remove head page (at current hand position)
242    fn remove_head_page(&mut self) -> Option<CacheEntry<K, V>> {
243        let entry = self.entries[self.hand].take()?;
244        self.free_slots.push(self.hand);
245        self.size -= 1;
246        self.advance_hand();
247        Some(entry)
248    }
249
250    const fn advance_hand(&mut self) {
251        self.hand = (self.hand + 1) % self.entries.len();
252    }
253
254    fn get_mut(&mut self, slot: usize) -> Option<&mut CacheEntry<K, V>> {
255        self.entries.get_mut(slot)?.as_mut()
256    }
257
258    const fn len(&self) -> usize {
259        self.size
260    }
261}
262
263/// Location of a key in the cache system
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265enum Location {
266    T1(usize),
267    T2(usize),
268    B1(usize),
269    B2(usize),
270}
271
272/// CAR Cache implementation following the exact pseudocode
273pub struct CarCache<K, V> {
274    /// Cache capacity
275    c: usize,
276    /// Target size for T1 (adaptive parameter)
277    p: usize,
278
279    /// T1: Recent pages (short-term utility)
280    t1: ClockList<K, V>,
281    /// T2: Frequent pages (long-term utility)
282    t2: ClockList<K, V>,
283    /// B1: Ghost list for pages evicted from T1
284    b1: GhostList<K>,
285    /// B2: Ghost list for pages evicted from T2
286    b2: GhostList<K>,
287
288    /// Index to track key locations
289    index: HashMap<K, Location>,
290}
291
292impl<K, V> CarCache<K, V>
293where
294    K: Eq + Hash + Clone,
295{
296    /// Create new CAR cache with given capacity
297    #[must_use]
298    pub fn new(capacity: usize) -> Self {
299        Self {
300            c: capacity,
301            p: 0,
302            t1: ClockList::new(capacity),
303            t2: ClockList::new(capacity),
304            b1: GhostList::new(capacity),
305            b2: GhostList::new(capacity),
306            index: HashMap::new(),
307        }
308    }
309
310    /// Get value from cache
311    /// Returns Some(value) if found, None if not in cache
312    pub fn get(&mut self, key: &K) -> Option<&V> {
313        match self.index.get(key) {
314            Some(Location::T1(slot)) => {
315                // Line 1-2: if (x is in T1 ∪ T2) then Set the page reference bit for x to one
316                if let Some(entry) = self.t1.get_mut(*slot) {
317                    entry.ref_bit = true; // Line 2: Set the page reference bit for x to one
318                    Some(&entry.value)
319                } else {
320                    None
321                }
322            }
323            Some(Location::T2(slot)) => {
324                // Line 1-2: if (x is in T1 ∪ T2) then Set the page reference bit for x to one
325                if let Some(entry) = self.t2.get_mut(*slot) {
326                    entry.ref_bit = true; // Line 2: Set the page reference bit for x to one
327                    Some(&entry.value)
328                } else {
329                    None
330                }
331            }
332            _ => None, // Line 3: else /* cache miss */
333        }
334    }
335
336    /// Insert/update value in cache following the exact pseudocode
337    /// Returns `Option<Evicted<K, V>>` containing the evicted entry (key and value)
338    /// if an entry was evicted from the cache, or `None` if no eviction occurred.
339    pub fn put(&mut self, key: K, value: V) -> Option<Evicted<K, V>> {
340        // Check if it's a cache hit first
341        if let Some(location) = self.index.get(&key).copied() {
342            match location {
343                Location::T1(slot) | Location::T2(slot) => {
344                    // Cache hit - update value and set reference bit
345                    let entry = if matches!(location, Location::T1(_)) {
346                        self.t1.get_mut(slot)
347                    } else {
348                        self.t2.get_mut(slot)
349                    };
350
351                    if let Some(entry) = entry {
352                        entry.value = value;
353                    }
354
355                    // We are not removed anything, as we just updated value
356                    return None;
357                }
358                _ => {
359                    // Will handle B1/B2 hits below
360                }
361            }
362        }
363
364        let mut evicted = None;
365        // Line 3: else /* cache miss */
366        // Line 4: if (|T1| + |T2| = c) then
367        if self.t1.len() + self.t2.len() == self.c {
368            // Line 5: replace()
369            evicted = self.replace();
370
371            // Line 6: if ((x is not in B1 ∪ B2) and (|T1| + |B1| = c)) then
372            if !self.is_in_b1_or_b2(&key) && (self.t1.len() + self.b1.len() == self.c) {
373                // Line 7: Discard the LRU page in B1
374                if let Some(discarded_key) = self.b1.remove_lru() {
375                    self.index.remove(&discarded_key);
376                }
377            }
378            // Line 8: elseif ((|T1| + |T2| + |B1| + |B2| = 2c) and (x is not in B1 ∪ B2)) then
379            else if !self.is_in_b1_or_b2(&key)
380                && (self.t1.len() + self.t2.len() + self.b1.len() + self.b2.len() == 2 * self.c)
381            {
382                // Line 9: Discard the LRU page in B2
383                if let Some(discarded_key) = self.b2.remove_lru() {
384                    self.index.remove(&discarded_key);
385                }
386            }
387        }
388
389        match self.index.get(&key).copied() {
390            Some(Location::B1(slot)) => {
391                // Line 14: elseif (x is in B1) then
392                // Line 15: Adapt: Increase the target size for the list T1 as: p = min {p + max{1, |B2|/|B1|}, c}
393                let delta = if self.b1.len() > 0 {
394                    1.max(self.b2.len() / self.b1.len())
395                } else {
396                    1
397                };
398                self.p = (self.p + delta).min(self.c);
399
400                // Remove from B1
401                self.b1.remove(slot);
402
403                // Line 16: Move x at the tail of T2. Set the page reference bit of x to 0.
404                if let Some(t2_slot) = self.t2.insert_at_tail(key.clone(), value) {
405                    self.index.insert(key, Location::T2(t2_slot));
406                    // ref_bit is already 0 from CacheEntry::new()
407                }
408            }
409            Some(Location::B2(slot)) => {
410                // Line 17: else /* x must be in B2 */
411                // Line 18: Adapt: Decrease the target size for the list T1 as: p = max {p − max{1, |B1|/|B2|}, 0}
412                let delta = if self.b2.len() > 0 {
413                    1.max(self.b1.len() / self.b2.len())
414                } else {
415                    1
416                };
417                self.p = self.p.saturating_sub(delta);
418
419                // Remove from B2
420                self.b2.remove(slot);
421
422                // Line 19: Move x at the tail of T2. Set the page reference bit of x to 0.
423                if let Some(t2_slot) = self.t2.insert_at_tail(key.clone(), value) {
424                    self.index.insert(key, Location::T2(t2_slot));
425                }
426            }
427            None => {
428                // Line 12: if (x is not in B1 ∪ B2) then
429                // Line 13: Insert x at the tail of T1. Set the page reference bit of x to 0.
430                if let Some(t1_slot) = self.t1.insert_at_tail(key.clone(), value) {
431                    self.index.insert(key, Location::T1(t1_slot));
432                }
433            }
434            _ => {
435                // Should not happen - T1/T2 cases handled above
436            }
437        }
438        evicted.map(|e| Evicted::new(e.key, e.value))
439    }
440
441    /// Line 5: `replace()` - exact implementation of pseudocode
442    fn replace(&mut self) -> Option<CacheEntry<K, V>> {
443        // Line 23: repeat
444        loop {
445            // Line 24: if (|T1| >= max(1, p)) then
446            if self.t1.len() >= 1.max(self.p) {
447                if let Some(found) = self.try_replace_from_t1() {
448                    return Some(found);
449                }
450                self.t1.advance_hand();
451            } else {
452                // Line 31: else
453                if let Some(found) = self.try_replace_from_t2() {
454                    return Some(found);
455                }
456                self.t2.advance_hand();
457            }
458        }
459        // Line 39: until (found)
460    }
461
462    /// Try to replace from T1, returns the evicted entry if replacement was successful
463    fn try_replace_from_t1(&mut self) -> Option<CacheEntry<K, V>> {
464        if let Some(head_entry) = self.t1.get_head_page() {
465            // Line 25: if (the page reference bit of head page in T1 is 0) then
466            // ref_bit == false
467            #[allow(clippy::bool_comparison)] // Allow to match paper
468            if head_entry.ref_bit == false {
469                // Line 26: found = 1;
470                // Line 27: Demote the head page in T1 and make it the MRU page in B1
471                if let Some(entry) = self.t1.remove_head_page() {
472                    if let Some((b1_slot, evicted_key)) = self.b1.insert_at_tail(entry.key.clone())
473                    {
474                        // Clean up evicted key from index if any
475                        if let Some(evicted) = evicted_key {
476                            self.index.remove(&evicted);
477                        }
478                        self.index.insert(entry.key.clone(), Location::B1(b1_slot));
479                    } else {
480                        self.index.remove(&entry.key);
481                    }
482                    return Some(entry);
483                }
484            } else {
485                // Line 28-29: else Set the page reference bit of head page in T1 to 0, and make it the tail page in T2
486                head_entry.ref_bit = false; // Line 29: Set the page reference bit of head page in T1 to 0
487                if let Some(entry) = self.t1.remove_head_page() {
488                    if let Some(t2_slot) = self.t2.insert_at_tail(entry.key.clone(), entry.value) {
489                        self.index.insert(entry.key, Location::T2(t2_slot));
490                    }
491                }
492            }
493        }
494        None
495    }
496
497    /// Try to replace from T2, returns the evicted entry if replacement was successful
498    fn try_replace_from_t2(&mut self) -> Option<CacheEntry<K, V>> {
499        if let Some(head_entry) = self.t2.get_head_page() {
500            // Line 32: if (the page reference bit of head page in T2 is 0), then
501            // ref_bit == false
502            #[allow(clippy::bool_comparison)] // Allow to match paper
503            if head_entry.ref_bit == false {
504                // Line 33: found = 1;
505                // Line 34: Demote the head page in T2 and make it the MRU page in B2
506                if let Some(entry) = self.t2.remove_head_page() {
507                    if let Some((b2_slot, evicted_key)) = self.b2.insert_at_tail(entry.key.clone())
508                    {
509                        // Clean up evicted key from index if any
510                        if let Some(evicted) = evicted_key {
511                            self.index.remove(&evicted);
512                        }
513                        self.index.insert(entry.key.clone(), Location::B2(b2_slot));
514                    } else {
515                        self.index.remove(&entry.key);
516                    }
517                    return Some(entry);
518                }
519            } else {
520                // Line 35-36: else Set the page reference bit of head page in T2 to 0, and make it the tail page in T2
521                head_entry.ref_bit = false; // Line 36: Set the page reference bit of head page in T2 to 0
522                if let Some(entry) = self.t2.remove_head_page() {
523                    if let Some(t2_slot) = self.t2.insert_at_tail(entry.key.clone(), entry.value) {
524                        self.index.insert(entry.key, Location::T2(t2_slot));
525                    }
526                }
527            }
528        }
529        None
530    }
531
532    /// Helper function to check if key is in B1 or B2
533    fn is_in_b1_or_b2(&self, key: &K) -> bool {
534        matches!(self.index.get(key), Some(Location::B1(_) | Location::B2(_)))
535    }
536
537    /// Get current cache size (items in T1 + T2)
538    #[must_use]
539    pub const fn len(&self) -> usize {
540        self.t1.len() + self.t2.len()
541    }
542
543    /// Check if cache is empty
544    #[must_use]
545    pub const fn is_empty(&self) -> bool {
546        self.len() == 0
547    }
548
549    /// Get cache capacity
550    #[must_use]
551    pub const fn capacity(&self) -> usize {
552        self.c
553    }
554
555    /// Get current adaptation parameter
556    #[must_use]
557    pub const fn adaptation_parameter(&self) -> usize {
558        self.p
559    }
560}
561
562pub(crate) type TypeErasedCarCache<K> = CarCache<K, Box<dyn Any + Send + Sync>>;
563
564impl<K> TypeErasedCarCache<K>
565where
566    K: Eq + Hash + Clone,
567{
568    pub(crate) fn get_typed<T: 'static + Send + Sync>(&mut self, key: &K) -> Option<&T> {
569        self.get(key)?.downcast_ref::<T>()
570    }
571
572    /// Put a typed value into the cache and return the evicted key if any.
573    ///
574    /// Returns `Some(key)` if an entry was evicted from the cache, `None` otherwise.
575    pub(crate) fn put_typed<T: 'static + Send + Sync>(&mut self, key: K, value: T) -> Option<K> {
576        let evicted = self.put(key, Box::new(value) as Box<dyn Any + Send + Sync>);
577        evicted.map(|e| e.key)
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use std::sync::Arc;
584
585    use super::*;
586
587    #[derive(Debug, Clone)]
588    #[allow(dead_code)]
589    struct TypeA {
590        id: String,
591    }
592
593    #[derive(Debug, Clone)]
594    #[allow(dead_code)]
595    struct TypeB {
596        id: String,
597    }
598
599    fn fill_cache_with_invariant_check<K, V>(
600        cache: &mut CarCache<K, V>,
601        items: impl Iterator<Item = (K, V)>,
602    ) where
603        K: Eq + std::hash::Hash + Clone,
604    {
605        for (key, value) in items {
606            cache.put(key, value);
607            assert_car_invariants(cache);
608        }
609    }
610
611    fn access_items_with_invariant_check<K, V>(
612        cache: &mut CarCache<K, V>,
613        keys: impl Iterator<Item = K>,
614    ) where
615        K: Eq + std::hash::Hash + Clone,
616    {
617        for key in keys {
618            cache.get(&key);
619            assert_car_invariants(cache);
620        }
621    }
622
623    fn assert_car_invariants<K, V>(cache: &CarCache<K, V>)
624    where
625        K: Eq + std::hash::Hash + Clone,
626    {
627        let c = cache.capacity();
628        let t1_size = cache.t1.len();
629        let t2_size = cache.t2.len();
630        let b1_size = cache.b1.len();
631        let b2_size = cache.b2.len();
632        let p = cache.adaptation_parameter();
633
634        let state_info = format!(
635            "Cache state: T1={}, T2={}, B1={}, B2={}, c={}, p={}",
636            t1_size, t2_size, b1_size, b2_size, c, p
637        );
638
639        // I1: 0 ≤ |T1| + |T2| ≤ c
640        assert!(
641            t1_size + t2_size <= c,
642            "I1 violated: |T1| + |T2| = {} > c = {}. {}",
643            t1_size + t2_size,
644            c,
645            state_info
646        );
647
648        // I2: 0 ≤ |T1| + |B1| ≤ c
649        assert!(
650            t1_size + b1_size <= c,
651            "I2 violated: |T1| + |B1| = {} > c = {}. {}",
652            t1_size + b1_size,
653            c,
654            state_info
655        );
656
657        // I3: 0 ≤ |T2| + |B2| ≤ 2c
658        assert!(
659            t2_size + b2_size <= 2 * c,
660            "I3 violated: |T2| + |B2| = {} > 2c = {}. {}",
661            t2_size + b2_size,
662            2 * c,
663            state_info
664        );
665
666        // I4: 0 ≤ |T1| + |T2| + |B1| + |B2| ≤ 2c
667        assert!(
668            t1_size + t2_size + b1_size + b2_size <= 2 * c,
669            "I4 violated: |T1| + |T2| + |B1| + |B2| = {} > 2c = {}. {}",
670            t1_size + t2_size + b1_size + b2_size,
671            2 * c,
672            state_info
673        );
674
675        // I5: If |T1| + |T2| < c, then B1 ∪ B2 is empty
676        if t1_size + t2_size < c {
677            assert!(
678                b1_size == 0 && b2_size == 0,
679                "I5 violated: |T1| + |T2| = {} < c = {} but B1 or B2 not empty. {}",
680                t1_size + t2_size,
681                c,
682                state_info
683            );
684        }
685
686        // I6: If |T1| + |B1| + |T2| + |B2| ≥ c, then |T1| + |T2| = c
687        if t1_size + b1_size + t2_size + b2_size >= c {
688            assert!(
689                t1_size + t2_size == c,
690                "I6 violated: total directory size {} ≥ c = {} but |T1| + |T2| = {} ≠ c. {}",
691                t1_size + b1_size + t2_size + b2_size,
692                c,
693                t1_size + t2_size,
694                state_info
695            );
696        }
697
698        // I7: Once cache is full, it remains full
699        if t1_size + t2_size == c {
700            assert_eq!(
701                cache.len(),
702                c,
703                "I7: Cache should remain at capacity once full. {}",
704                state_info
705            );
706        }
707
708        assert!(
709            p <= c,
710            "Adaptation parameter p={} should not exceed capacity c={}. {}",
711            p,
712            c,
713            state_info
714        );
715        assert_eq!(
716            cache.len(),
717            t1_size + t2_size,
718            "Cache length mismatch. {}",
719            state_info
720        );
721    }
722
723    fn create_eviction_pressure(cache: &mut CarCache<String, i32>, rounds: i32) {
724        for round in 0..rounds {
725            cache.put(format!("b1_source_{}", round), round + 100);
726            assert_car_invariants(cache);
727
728            cache.put(format!("b2_source_{}", round), round + 200);
729            cache.get(&format!("b2_source_{}", round));
730            assert_car_invariants(cache);
731
732            cache.put(format!("pressure_{}", round), round + 300);
733            assert_car_invariants(cache);
734        }
735    }
736
737    fn promote_all_to_t2(cache: &mut CarCache<i32, i32>, range: std::ops::Range<i32>) {
738        for i in range.clone() {
739            cache.put(i, i);
740            cache.get(&i);
741            assert_car_invariants(cache);
742        }
743    }
744
745    fn create_t1_t2_mix(cache: &mut CarCache<String, i32>, prefix: &str, count: i32) {
746        fill_cache_with_invariant_check(
747            cache,
748            (0..count).map(|i| (format!("{}_{}", prefix, i), i)),
749        );
750        access_items_with_invariant_check(
751            cache,
752            (0..count / 2).map(|i| format!("{}_{}", prefix, i)),
753        );
754    }
755
756    fn verify_directory_state<K, V>(cache: &CarCache<K, V>) -> (usize, usize, usize, usize, usize)
757    where
758        K: Eq + std::hash::Hash + Clone,
759    {
760        let t1_size = cache.t1.len();
761        let t2_size = cache.t2.len();
762        let b1_size = cache.b1.len();
763        let b2_size = cache.b2.len();
764        let total = t1_size + t2_size + b1_size + b2_size;
765
766        (t1_size, t2_size, b1_size, b2_size, total)
767    }
768
769    fn create_ghost_hits(
770        cache: &mut CarCache<String, i32>,
771        prefix: &str,
772        range: std::ops::Range<i32>,
773        value_offset: i32,
774    ) {
775        for i in range {
776            cache.put(format!("{}_{}", prefix, i), i + value_offset);
777            assert_car_invariants(cache);
778        }
779    }
780
781    #[test]
782    fn test_ghost_list_basic_operations() {
783        let mut ghost_list = GhostList::new(3);
784
785        assert_eq!(ghost_list.len(), 0);
786        assert_eq!(ghost_list.remove_lru(), None);
787
788        let (_slot1, _) = ghost_list.insert_at_tail("a").unwrap();
789        assert_eq!(ghost_list.len(), 1);
790
791        let (slot2, _) = ghost_list.insert_at_tail("b").unwrap();
792        assert_eq!(ghost_list.len(), 2);
793
794        assert_eq!(ghost_list.remove_lru(), Some("a"));
795        assert_eq!(ghost_list.len(), 1);
796
797        assert!(ghost_list.remove(slot2));
798        assert_eq!(ghost_list.len(), 0);
799    }
800
801    #[test]
802    fn test_clock_list_basic_operations() {
803        let mut clock_list = ClockList::new(3);
804
805        assert_eq!(clock_list.len(), 0);
806        assert!(clock_list.get_head_page().is_none());
807
808        let slot1 = clock_list.insert_at_tail("a", 1).unwrap();
809        assert_eq!(clock_list.len(), 1);
810
811        let slot2 = clock_list.insert_at_tail("b", 2).unwrap();
812        assert_eq!(clock_list.len(), 2);
813
814        assert_eq!(clock_list.get_mut(slot1).unwrap().value, 1);
815        assert_eq!(clock_list.get_mut(slot2).unwrap().value, 2);
816
817        let entry = clock_list.get_mut(slot1).unwrap();
818        assert_eq!(entry.ref_bit, false);
819    }
820
821    #[test]
822    fn test_adaptation_parameter_increase_on_b1_hit() {
823        let mut cache = CarCache::new(4);
824
825        cache.put("a", 1);
826        cache.put("b", 2);
827        cache.put("c", 3);
828
829        let initial_p = cache.adaptation_parameter();
830        cache.get(&"a");
831
832        cache.put("e", 5);
833        cache.put("f", 6);
834
835        cache.put("c", 10);
836
837        assert!(cache.adaptation_parameter() > initial_p);
838        assert!(cache.adaptation_parameter() <= cache.capacity());
839    }
840
841    #[test]
842    fn test_adaptation_parameter_decrease_on_b2_hit() {
843        let mut cache = CarCache::new(4);
844
845        cache.put("a", 1);
846        cache.put("b", 2);
847        cache.put("c", 3);
848
849        cache.get(&"a");
850
851        cache.put("e", 5);
852        cache.put("f", 6);
853
854        cache.put("c", 10);
855
856        let p_before = cache.adaptation_parameter();
857
858        cache.put("f", 6);
859        cache.get(&"f");
860        cache.put("g", 7);
861        cache.get(&"g");
862        cache.put("x", 7);
863        cache.put("y", 7);
864        cache.put("z", 7);
865
866        cache.put("a", 10);
867
868        assert!(cache.adaptation_parameter() < p_before);
869    }
870
871    #[test]
872    fn test_clock_algorithm_reference_bit_behavior() {
873        let mut cache = CarCache::new(3);
874
875        cache.put("a", 1);
876        cache.put("b", 2);
877        cache.put("c", 3);
878
879        cache.get(&"a");
880
881        cache.put("d", 4);
882        cache.put("e", 5);
883
884        assert!(cache.get(&"a").is_some());
885        assert!(cache.len() <= 3);
886    }
887
888    #[test]
889    fn test_ghost_list_lru_behavior() {
890        let mut ghost_list = GhostList::new(3);
891
892        let _ = ghost_list.insert_at_tail("first");
893        let _ = ghost_list.insert_at_tail("second");
894        let _ = ghost_list.insert_at_tail("third");
895
896        assert_eq!(ghost_list.remove_lru(), Some("first"));
897        assert_eq!(ghost_list.remove_lru(), Some("second"));
898        assert_eq!(ghost_list.remove_lru(), Some("third"));
899        assert_eq!(ghost_list.remove_lru(), None);
900    }
901
902    #[test]
903    fn test_directory_replacement_constraints() {
904        let mut cache = CarCache::new(3);
905
906        cache.put("a", 1);
907        cache.put("b", 2);
908        cache.get(&"a");
909        cache.put("c", 3);
910        cache.get(&"c");
911        cache.put("d", 4);
912        cache.put("e", 5);
913
914        assert_eq!(cache.t1.len(), 1);
915        assert_eq!(cache.t2.len(), 2);
916    }
917
918    #[test]
919    fn test_large_cache_reference_bit_behavior() {
920        let mut cache = CarCache::new(1000);
921
922        for i in 0..800 {
923            cache.put(format!("frequent_{}", i), i);
924            cache.get(&format!("frequent_{}", i)); // Set reference bit
925        }
926
927        for i in 0..200 {
928            cache.put(format!("rare_{}", i), i);
929        }
930
931        for i in 0..400 {
932            cache.put(format!("new_{}", i), i);
933        }
934
935        let frequent_survivors = (0..800)
936            .filter(|&i| cache.get(&format!("frequent_{}", i)).is_some())
937            .count();
938
939        let rare_survivors = (0..200)
940            .filter(|&i| cache.get(&format!("rare_{}", i)).is_some())
941            .count();
942
943        assert!(frequent_survivors as f64 / 800.0 >= rare_survivors as f64 / 200.0);
944    }
945
946    #[test]
947    fn test_large_cache_scan_resistance() {
948        let mut cache = CarCache::new(1000);
949
950        let working_set: Vec<String> = (0..200).map(|i| format!("working_{}", i)).collect();
951        for key in &working_set {
952            cache.put(key.clone(), 1);
953            cache.get(key);
954        }
955
956        for i in 0..800 {
957            cache.put(format!("filler_{}", i), i);
958        }
959
960        for i in 0..500 {
961            cache.put(format!("scan_{}", i), i);
962        }
963
964        let survivors = working_set
965            .iter()
966            .filter(|key| cache.get(key).is_some())
967            .count();
968
969        assert_eq!(survivors, 200);
970        assert_eq!(cache.len(), cache.capacity());
971        assert!(cache.adaptation_parameter() <= cache.capacity());
972    }
973
974    #[test]
975    fn test_cache_adaptation_bounds() {
976        let mut cache = CarCache::new(10);
977        let mut p_values = Vec::new();
978
979        let working_set = (0..15).map(|i| format!("item_{}", i)).collect::<Vec<_>>();
980
981        for i in 0..8 {
982            cache.put(working_set[i].clone(), i);
983        }
984
985        for i in 0..4 {
986            cache.get(&working_set[i]);
987        }
988
989        p_values.push(cache.adaptation_parameter());
990        for cycle in 0..3 {
991            for (round, item) in working_set.iter().enumerate() {
992                cache.put(item.clone(), cycle * 100 + round);
993
994                let p_after = cache.adaptation_parameter();
995                p_values.push(p_after);
996
997                assert!(
998                    p_after <= cache.capacity(),
999                    "Adaptation parameter {} exceeds capacity {} at cycle {} round {}",
1000                    p_after,
1001                    cache.capacity(),
1002                    cycle,
1003                    round
1004                );
1005
1006                if round % 3 == 0 && round > 0 {
1007                    cache.get(&working_set[round - 1]);
1008                }
1009            }
1010        }
1011
1012        for (i, &p) in p_values.iter().enumerate() {
1013            assert!(
1014                p <= cache.capacity(),
1015                "p={} > c={} at step {}",
1016                p,
1017                cache.capacity(),
1018                i
1019            );
1020        }
1021
1022        let p_changed = p_values.iter().any(|&p| p != p_values[0]);
1023        assert!(
1024            p_changed,
1025            "NOTE: Adaptation parameter remained at {} (may need different workload)",
1026            p_values[0]
1027        );
1028        assert_eq!(cache.adaptation_parameter(), 5);
1029    }
1030
1031    #[test]
1032    fn test_put_return_values_eviction() {
1033        let mut cache = CarCache::new(3);
1034
1035        assert!(cache.put("a", 1).is_none());
1036        assert!(cache.put("b", 2).is_none());
1037        assert!(cache.put("c", 3).is_none());
1038
1039        // When eviction occurs, we get back the Evicted struct with key and value
1040        let evicted = cache.put("d", 4);
1041        assert!(evicted.is_some());
1042        let evicted = evicted.unwrap();
1043        assert_eq!(evicted.key, "a");
1044        assert_eq!(evicted.value, 1);
1045
1046        let evicted = cache.put("e", 5);
1047        assert!(evicted.is_some());
1048        let evicted = evicted.unwrap();
1049        assert_eq!(evicted.key, "b");
1050        assert_eq!(evicted.value, 2);
1051
1052        assert_eq!(cache.get(&"a"), None);
1053        assert_eq!(cache.get(&"b"), None);
1054        assert_eq!(cache.get(&"c"), Some(&3));
1055        assert_eq!(cache.get(&"d"), Some(&4));
1056        assert_eq!(cache.get(&"e"), Some(&5));
1057    }
1058
1059    #[test]
1060    fn test_put_return_values_t1_t2_eviction() {
1061        let mut cache = CarCache::new(4);
1062
1063        assert!(cache.put("t1_a", 1).is_none());
1064        assert!(cache.put("t1_b", 2).is_none());
1065
1066        cache.get(&"t1_a");
1067        cache.get(&"t1_b");
1068
1069        assert!(cache.put("t1_c", 3).is_none());
1070        assert!(cache.put("t1_d", 4).is_none());
1071
1072        let evicted = cache.put("new1", 10);
1073        assert!(evicted.is_some());
1074        assert_eq!(evicted.unwrap().value, 3);
1075    }
1076
1077    #[test]
1078    fn test_car_invariants_i3_stress() {
1079        let mut cache = CarCache::new(5);
1080
1081        promote_all_to_t2(&mut cache, 0..5);
1082
1083        for i in 5..20 {
1084            cache.put(i, i);
1085            assert_car_invariants(&cache);
1086            cache.get(&i);
1087            assert_car_invariants(&cache);
1088        }
1089
1090        fill_cache_with_invariant_check(&mut cache, (0..5).map(|i| (i, i + 100)));
1091
1092        let (_, t2_size, _, b2_size, _) = verify_directory_state(&cache);
1093        assert!(
1094            t2_size + b2_size > 0,
1095            "Should have some T2/B2 entries to test I3"
1096        );
1097    }
1098
1099    #[test]
1100    fn test_car_invariants_i4_maximum_directory() {
1101        let mut cache = CarCache::new(8);
1102
1103        create_t1_t2_mix(&mut cache, "t1", 8);
1104        create_eviction_pressure(&mut cache, 10);
1105        create_ghost_hits(&mut cache, "t1", 0..4, 1000);
1106
1107        let (_, _, _, _, total) = verify_directory_state(&cache);
1108        let max_allowed = 2 * cache.capacity();
1109
1110        assert!(
1111            total >= cache.capacity(),
1112            "Directory should be substantial for meaningful I4 test"
1113        );
1114        assert!(
1115            total <= max_allowed,
1116            "I4: Directory size {} should not exceed 2c={}",
1117            total,
1118            max_allowed
1119        );
1120    }
1121
1122    #[test]
1123    fn test_car_invariant_i6_directory_full_cache_full() {
1124        let mut cache = CarCache::new(6);
1125
1126        create_t1_t2_mix(&mut cache, "initial", 6);
1127
1128        for i in 6..15 {
1129            cache.put(format!("evict_{}", i), i);
1130            assert_car_invariants(&cache);
1131
1132            if i % 2 == 0 {
1133                cache.get(&format!("evict_{}", i));
1134                assert_car_invariants(&cache);
1135            }
1136        }
1137
1138        create_ghost_hits(&mut cache, "initial", 0..3, 1000);
1139
1140        let (t1_size, t2_size, _b1_size, _b2_size, total_dir) = verify_directory_state(&cache);
1141
1142        if total_dir >= cache.capacity() {
1143            assert_eq!(
1144                t1_size + t2_size,
1145                cache.capacity(),
1146                "I6: When directory size {} ≥ c={}, cache should be full but |T1|+|T2|={}",
1147                total_dir,
1148                cache.capacity(),
1149                t1_size + t2_size
1150            );
1151        } else {
1152            panic!(
1153                "Test setup failed: Directory size {} should be ≥ c={}",
1154                total_dir,
1155                cache.capacity()
1156            );
1157        }
1158    }
1159
1160    #[test]
1161    fn test_car_invariant_i7_cache_remains_full() {
1162        let mut cache = CarCache::new(8);
1163
1164        for i in 0..8 {
1165            cache.put(format!("fill_{}", i), i);
1166            assert_car_invariants(&cache);
1167        }
1168
1169        assert_eq!(cache.len(), cache.capacity(), "Cache should be at capacity");
1170
1171        for round in 0..20 {
1172            cache.put(format!("new_{}", round), round + 100);
1173            assert_car_invariants(&cache);
1174            assert_eq!(
1175                cache.len(),
1176                cache.capacity(),
1177                "I7: Cache should remain full after adding new item in round {}",
1178                round
1179            );
1180
1181            cache.get(&format!("new_{}", round));
1182            assert_car_invariants(&cache);
1183            assert_eq!(
1184                cache.len(),
1185                cache.capacity(),
1186                "I7: Cache should remain full after accessing item in round {}",
1187                round
1188            );
1189
1190            cache.put(format!("new_{}", round), round + 200);
1191            assert_car_invariants(&cache);
1192            assert_eq!(
1193                cache.len(),
1194                cache.capacity(),
1195                "I7: Cache should remain full after updating item in round {}",
1196                round
1197            );
1198
1199            if round > 5 {
1200                cache.put(format!("fill_{}", round % 8), round + 300);
1201                assert_car_invariants(&cache);
1202                assert_eq!(
1203                    cache.len(),
1204                    cache.capacity(),
1205                    "I7: Cache should remain full after B1/B2 hit in round {}",
1206                    round
1207                );
1208            }
1209        }
1210    }
1211
1212    #[test]
1213    fn test_put_typed_works_across_types() {
1214        let mut cache: TypeErasedCarCache<String> = CarCache::new(2);
1215
1216        let evicted_key = cache.put_typed("key1".to_string(), Arc::new(TypeA { id: "1".into() }));
1217        assert!(evicted_key.is_none());
1218
1219        let evicted_key = cache.put_typed("key2".to_string(), Arc::new(TypeA { id: "2".into() }));
1220        assert!(evicted_key.is_none());
1221
1222        let evicted_key = cache.put_typed("key3".to_string(), Arc::new(TypeB { id: "3".into() }));
1223
1224        assert!(evicted_key.is_some(),);
1225
1226        let evicted_key = evicted_key.unwrap();
1227        assert!(evicted_key == "key1" || evicted_key == "key2",);
1228
1229        let key_in_cache = cache.get_typed::<Arc<TypeA>>(&evicted_key).is_some();
1230        assert!(!key_in_cache,);
1231    }
1232}