Skip to main content

oximedia_cache/
eviction.rs

1//! Pluggable eviction policy abstraction.
2//!
3//! This module defines an [`EvictionStrategy`] enum and a
4//! [`create_eviction_fn`] factory that returns a boxed eviction closure.
5//!
6//! Each closure accepts a mutable `Vec<(K, u64)>` (a vector of `(key, access_time)`
7//! pairs sorted or unsorted) and returns the key that should be evicted next.
8//! The vector is not modified by the closure — callers are responsible for
9//! removing the returned entry.
10//!
11//! # Policies
12//!
13//! | Policy | Behaviour |
14//! |--------|-----------|
15//! | `Lru`  | Evict the entry with the **smallest** (oldest) `access_time`. |
16//! | `Lfu`  | Evict the entry with the **smallest** `access_time` (access count used as proxy). |
17//! | `Fifo` | Evict the entry at **index 0** (oldest insertion position). |
18//!
19//! # Example
20//!
21//! ```
22//! use oximedia_cache::eviction::{EvictionStrategy, create_eviction_fn};
23//!
24//! let evict = create_eviction_fn(EvictionStrategy::Lru);
25//! let entries: Vec<(String, u64)> = vec![
26//!     ("frame-001".to_string(), 1000),
27//!     ("frame-002".to_string(), 500),
28//!     ("frame-003".to_string(), 2000),
29//! ];
30//! let to_evict = evict(&entries);
31//! assert_eq!(to_evict, Some("frame-002".to_string()));
32//! ```
33
34#![allow(dead_code)]
35
36// ---------------------------------------------------------------------------
37// EvictionStrategy
38// ---------------------------------------------------------------------------
39
40/// Discriminated union of the supported eviction strategies.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum EvictionStrategy {
43    /// Least Recently Used — evict the entry with the oldest access timestamp.
44    Lru,
45    /// Least Frequently Used — evict the entry with the lowest access count
46    /// (represented here as the smallest access timestamp; in a real system
47    /// a separate frequency counter would be maintained).
48    Lfu,
49    /// First In, First Out — evict the entry at position 0 in the slice.
50    Fifo,
51}
52
53// ---------------------------------------------------------------------------
54// Factory
55// ---------------------------------------------------------------------------
56
57/// Create a boxed eviction closure for the given `policy`.
58///
59/// The returned closure takes a `&Vec<(K, u64)>` and returns
60/// `Option<K>` — the key that should be removed.
61///
62/// Returns `None` when the entry list is empty.
63pub fn create_eviction_fn<K>(policy: EvictionStrategy) -> Box<dyn Fn(&Vec<(K, u64)>) -> Option<K>>
64where
65    K: Clone + Eq + 'static,
66{
67    match policy {
68        EvictionStrategy::Lru | EvictionStrategy::Lfu => Box::new(|entries: &Vec<(K, u64)>| {
69            entries
70                .iter()
71                .min_by_key(|(_, ts)| *ts)
72                .map(|(k, _)| k.clone())
73        }),
74        EvictionStrategy::Fifo => {
75            Box::new(|entries: &Vec<(K, u64)>| entries.first().map(|(k, _)| k.clone()))
76        }
77    }
78}
79
80// ---------------------------------------------------------------------------
81// EvictionChooser — concrete helper wrapping the closure
82// ---------------------------------------------------------------------------
83
84/// A convenience wrapper around an eviction closure.
85pub struct EvictionChooser<K: Clone + Eq + 'static> {
86    /// The active strategy.
87    pub strategy: EvictionStrategy,
88    evict: Box<dyn Fn(&Vec<(K, u64)>) -> Option<K>>,
89}
90
91impl<K: Clone + Eq + 'static> EvictionChooser<K> {
92    /// Create a new `EvictionChooser` for the given strategy.
93    #[must_use]
94    pub fn new(strategy: EvictionStrategy) -> Self {
95        Self {
96            strategy,
97            evict: create_eviction_fn(strategy),
98        }
99    }
100
101    /// Choose the next key to evict from `entries`.
102    #[must_use]
103    pub fn choose(&self, entries: &Vec<(K, u64)>) -> Option<K> {
104        (self.evict)(entries)
105    }
106}
107
108impl<K: Clone + Eq + std::fmt::Debug + 'static> std::fmt::Debug for EvictionChooser<K> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("EvictionChooser")
111            .field("strategy", &self.strategy)
112            .finish()
113    }
114}
115
116// ---------------------------------------------------------------------------
117// Tests
118// ---------------------------------------------------------------------------
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    type Entries = Vec<(String, u64)>;
125
126    fn e(key: &str, ts: u64) -> (String, u64) {
127        (key.to_string(), ts)
128    }
129
130    // ── LRU ──────────────────────────────────────────────────────────────────
131
132    #[test]
133    fn test_lru_evicts_oldest() {
134        let evict = create_eviction_fn::<String>(EvictionStrategy::Lru);
135        let entries: Entries = vec![e("b", 1000), e("c", 500), e("a", 2000)];
136        assert_eq!(evict(&entries), Some("c".to_string()));
137    }
138
139    #[test]
140    fn test_lru_single_entry() {
141        let evict = create_eviction_fn::<String>(EvictionStrategy::Lru);
142        let entries: Entries = vec![e("only", 999)];
143        assert_eq!(evict(&entries), Some("only".to_string()));
144    }
145
146    #[test]
147    fn test_lru_empty_returns_none() {
148        let evict = create_eviction_fn::<String>(EvictionStrategy::Lru);
149        let entries: Entries = vec![];
150        assert_eq!(evict(&entries), None);
151    }
152
153    // ── LFU ──────────────────────────────────────────────────────────────────
154
155    #[test]
156    fn test_lfu_evicts_lowest_count() {
157        // LFU uses access_time as proxy for frequency (lowest = least frequent)
158        let evict = create_eviction_fn::<String>(EvictionStrategy::Lfu);
159        let entries: Entries = vec![e("hot", 100), e("cold", 1), e("warm", 50)];
160        assert_eq!(evict(&entries), Some("cold".to_string()));
161    }
162
163    // ── FIFO ─────────────────────────────────────────────────────────────────
164
165    #[test]
166    fn test_fifo_evicts_first_inserted() {
167        let evict = create_eviction_fn::<String>(EvictionStrategy::Fifo);
168        let entries: Entries = vec![e("first", 9999), e("second", 1), e("third", 5000)];
169        // FIFO returns index 0 regardless of timestamp
170        assert_eq!(evict(&entries), Some("first".to_string()));
171    }
172
173    #[test]
174    fn test_fifo_empty_returns_none() {
175        let evict = create_eviction_fn::<String>(EvictionStrategy::Fifo);
176        let entries: Entries = vec![];
177        assert_eq!(evict(&entries), None);
178    }
179
180    // ── EvictionChooser ────────────────────────────────────────────────────────
181
182    #[test]
183    fn test_eviction_chooser_lru() {
184        let chooser: EvictionChooser<String> = EvictionChooser::new(EvictionStrategy::Lru);
185        let entries: Entries = vec![e("x", 300), e("y", 100), e("z", 200)];
186        assert_eq!(chooser.choose(&entries), Some("y".to_string()));
187    }
188
189    #[test]
190    fn test_eviction_chooser_fifo() {
191        let chooser: EvictionChooser<String> = EvictionChooser::new(EvictionStrategy::Fifo);
192        let entries: Entries = vec![e("alpha", 1), e("beta", 2)];
193        assert_eq!(chooser.choose(&entries), Some("alpha".to_string()));
194    }
195
196    #[test]
197    fn test_eviction_chooser_strategy_field() {
198        let chooser: EvictionChooser<String> = EvictionChooser::new(EvictionStrategy::Lfu);
199        assert_eq!(chooser.strategy, EvictionStrategy::Lfu);
200    }
201}