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}