Skip to main content

oxihuman_core/
cache_policy.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Eviction policies for cache systems (LRU, FIFO, LFU scoring).
6
7/// Eviction policy kind.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PolicyKind {
11    Lru,
12    Fifo,
13    Lfu,
14}
15
16/// An entry tracked by the cache policy.
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19pub struct PolicyEntry {
20    pub key: String,
21    pub insert_order: u64,
22    pub last_access: u64,
23    pub access_count: u64,
24}
25
26/// Cache eviction policy tracker.
27#[allow(dead_code)]
28#[derive(Debug, Clone)]
29pub struct CachePolicy {
30    kind: PolicyKind,
31    entries: Vec<PolicyEntry>,
32    counter: u64,
33}
34
35#[allow(dead_code)]
36impl CachePolicy {
37    pub fn new(kind: PolicyKind) -> Self {
38        Self {
39            kind,
40            entries: Vec::new(),
41            counter: 0,
42        }
43    }
44
45    pub fn insert(&mut self, key: &str) {
46        self.counter += 1;
47        if let Some(e) = self.entries.iter_mut().find(|e| e.key == key) {
48            e.last_access = self.counter;
49            e.access_count += 1;
50        } else {
51            self.entries.push(PolicyEntry {
52                key: key.to_string(),
53                insert_order: self.counter,
54                last_access: self.counter,
55                access_count: 1,
56            });
57        }
58    }
59
60    pub fn touch(&mut self, key: &str) {
61        self.counter += 1;
62        if let Some(e) = self.entries.iter_mut().find(|e| e.key == key) {
63            e.last_access = self.counter;
64            e.access_count += 1;
65        }
66    }
67
68    pub fn evict_candidate(&self) -> Option<&str> {
69        if self.entries.is_empty() {
70            return None;
71        }
72        let best = match self.kind {
73            PolicyKind::Lru => self.entries.iter().min_by_key(|e| e.last_access),
74            PolicyKind::Fifo => self.entries.iter().min_by_key(|e| e.insert_order),
75            PolicyKind::Lfu => self.entries.iter().min_by_key(|e| e.access_count),
76        };
77        best.map(|e| e.key.as_str())
78    }
79
80    pub fn remove(&mut self, key: &str) {
81        self.entries.retain(|e| e.key != key);
82    }
83
84    pub fn len(&self) -> usize {
85        self.entries.len()
86    }
87
88    pub fn is_empty(&self) -> bool {
89        self.entries.is_empty()
90    }
91
92    pub fn kind(&self) -> PolicyKind {
93        self.kind
94    }
95
96    pub fn contains(&self, key: &str) -> bool {
97        self.entries.iter().any(|e| e.key == key)
98    }
99
100    pub fn clear(&mut self) {
101        self.entries.clear();
102        self.counter = 0;
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn new_policy_empty() {
112        let p = CachePolicy::new(PolicyKind::Lru);
113        assert!(p.is_empty());
114    }
115
116    #[test]
117    fn insert_adds_entry() {
118        let mut p = CachePolicy::new(PolicyKind::Lru);
119        p.insert("a");
120        assert_eq!(p.len(), 1);
121        assert!(p.contains("a"));
122    }
123
124    #[test]
125    fn lru_evicts_least_recently_used() {
126        let mut p = CachePolicy::new(PolicyKind::Lru);
127        p.insert("a");
128        p.insert("b");
129        p.touch("a");
130        assert_eq!(p.evict_candidate(), Some("b"));
131    }
132
133    #[test]
134    fn fifo_evicts_oldest() {
135        let mut p = CachePolicy::new(PolicyKind::Fifo);
136        p.insert("first");
137        p.insert("second");
138        assert_eq!(p.evict_candidate(), Some("first"));
139    }
140
141    #[test]
142    fn lfu_evicts_least_frequent() {
143        let mut p = CachePolicy::new(PolicyKind::Lfu);
144        p.insert("a");
145        p.insert("b");
146        p.touch("a");
147        p.touch("a");
148        assert_eq!(p.evict_candidate(), Some("b"));
149    }
150
151    #[test]
152    fn remove_entry() {
153        let mut p = CachePolicy::new(PolicyKind::Lru);
154        p.insert("x");
155        p.remove("x");
156        assert!(p.is_empty());
157    }
158
159    #[test]
160    fn clear_resets() {
161        let mut p = CachePolicy::new(PolicyKind::Lru);
162        p.insert("a");
163        p.insert("b");
164        p.clear();
165        assert!(p.is_empty());
166    }
167
168    #[test]
169    fn evict_candidate_empty() {
170        let p = CachePolicy::new(PolicyKind::Lru);
171        assert!(p.evict_candidate().is_none());
172    }
173
174    #[test]
175    fn kind_returns_policy() {
176        let p = CachePolicy::new(PolicyKind::Fifo);
177        assert_eq!(p.kind(), PolicyKind::Fifo);
178    }
179
180    #[test]
181    fn duplicate_insert_updates() {
182        let mut p = CachePolicy::new(PolicyKind::Lfu);
183        p.insert("a");
184        p.insert("a");
185        assert_eq!(p.len(), 1);
186    }
187}