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