1use std::collections::{BTreeMap, HashMap};
14use std::hash::Hash;
15
16use crate::resources::{ByteBudget, ByteReservation, ResourceError};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub struct CacheStats {
21 pub entries: u64,
23 pub bytes: u64,
25 pub hits: u64,
27 pub misses: u64,
29 pub evictions: u64,
31}
32
33struct Entry<V> {
34 value: V,
35 bytes: usize,
36 seq: u64,
37}
38
39pub struct LogicalCache<K, V> {
41 budget: ByteBudget,
42 reserved: ByteReservation,
43 entries: HashMap<K, Entry<V>>,
44 order: BTreeMap<u64, K>,
47 seq: u64,
48 hits: u64,
49 misses: u64,
50 evictions: u64,
51}
52
53impl<K, V> LogicalCache<K, V>
54where
55 K: Eq + Hash + Clone,
56{
57 pub fn new(budget: ByteBudget) -> Result<Self, ResourceError> {
64 let reserved = budget.try_reserve(0)?;
65 Ok(Self {
66 budget,
67 reserved,
68 entries: HashMap::new(),
69 order: BTreeMap::new(),
70 seq: 0,
71 hits: 0,
72 misses: 0,
73 evictions: 0,
74 })
75 }
76
77 fn next_seq(&mut self) -> u64 {
78 self.seq = self.seq.wrapping_add(1);
79 self.seq
80 }
81
82 #[must_use]
84 pub fn limit(&self) -> usize {
85 self.budget.snapshot().limit
86 }
87
88 pub fn get(&mut self, key: &K) -> Option<&V> {
90 let seq = self.next_seq();
91 let Some(entry) = self.entries.get_mut(key) else {
92 self.misses = self.misses.saturating_add(1);
93 return None;
94 };
95 self.hits = self.hits.saturating_add(1);
96 self.order.remove(&entry.seq);
97 entry.seq = seq;
98 self.order.insert(seq, key.clone());
99 self.entries.get(key).map(|entry| &entry.value)
100 }
101
102 pub fn insert(&mut self, key: K, value: V, bytes: usize) -> bool {
106 if bytes > self.limit() {
108 return false;
109 }
110 if let Some(old) = self.entries.remove(&key) {
111 self.order.remove(&old.seq);
112 let _ = self
113 .reserved
114 .resize(self.reserved.bytes().saturating_sub(old.bytes));
115 }
116 while self.reserved.bytes().saturating_add(bytes) > self.limit() {
117 if !self.evict_one() {
118 return false;
119 }
120 }
121 if self
122 .reserved
123 .resize(self.reserved.bytes().saturating_add(bytes))
124 .is_err()
125 {
126 return false;
127 }
128 let seq = self.next_seq();
129 self.order.insert(seq, key.clone());
130 self.entries.insert(key, Entry { value, bytes, seq });
131 true
132 }
133
134 fn evict_one(&mut self) -> bool {
136 let Some((&seq, key)) = self.order.iter().next() else {
137 return false;
138 };
139 let key = key.clone();
140 self.order.remove(&seq);
141 if let Some(entry) = self.entries.remove(&key) {
142 let _ = self
143 .reserved
144 .resize(self.reserved.bytes().saturating_sub(entry.bytes));
145 self.evictions = self.evictions.saturating_add(1);
146 return true;
147 }
148 false
149 }
150
151 pub fn retain(&mut self, mut keep: impl FnMut(&K, &V) -> bool) -> usize {
154 let doomed: Vec<K> = self
155 .entries
156 .iter()
157 .filter(|(key, entry)| !keep(key, &entry.value))
158 .map(|(key, _)| key.clone())
159 .collect();
160 for key in &doomed {
161 if let Some(entry) = self.entries.remove(key) {
162 self.order.remove(&entry.seq);
163 let _ = self
164 .reserved
165 .resize(self.reserved.bytes().saturating_sub(entry.bytes));
166 self.evictions = self.evictions.saturating_add(1);
167 }
168 }
169 doomed.len()
170 }
171
172 pub fn clear(&mut self) {
174 self.entries.clear();
175 self.order.clear();
176 let _ = self.reserved.resize(0);
177 }
178
179 #[must_use]
181 pub fn stats(&self) -> CacheStats {
182 CacheStats {
183 entries: self.entries.len() as u64,
184 bytes: self.reserved.bytes() as u64,
185 hits: self.hits,
186 misses: self.misses,
187 evictions: self.evictions,
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 fn cache(limit: usize) -> LogicalCache<u64, String> {
197 LogicalCache::new(ByteBudget::new("test-cache", limit).unwrap()).unwrap()
198 }
199
200 #[test]
201 fn evicts_least_recently_used_when_over_budget() {
202 let mut c = cache(100);
203 assert!(c.insert(1, "a".into(), 40));
204 assert!(c.insert(2, "b".into(), 40));
205 assert_eq!(c.get(&1).map(String::as_str), Some("a"));
207 assert!(c.insert(3, "c".into(), 40)); assert!(c.get(&2).is_none(), "LRU evicted");
209 assert_eq!(c.get(&1).map(String::as_str), Some("a"));
210 assert_eq!(c.get(&3).map(String::as_str), Some("c"));
211 let s = c.stats();
212 assert_eq!(s.entries, 2);
213 assert_eq!(s.bytes, 80);
214 assert_eq!(s.evictions, 1);
215 assert_eq!(s.hits, 3); assert_eq!(s.misses, 1);
217 }
218
219 #[test]
220 fn oversized_entry_is_not_cached_and_releases_nothing() {
221 let mut c = cache(100);
222 assert!(c.insert(1, "a".into(), 40));
223 assert!(!c.insert(2, "huge".into(), 500), "oversized rejected");
224 assert!(c.get(&2).is_none());
225 assert_eq!(c.stats().bytes, 40, "failed insert charges nothing");
226 assert_eq!(c.get(&1).map(String::as_str), Some("a"));
227 }
228
229 #[test]
230 fn replacing_releases_the_old_charge() {
231 let mut c = cache(100);
232 c.insert(1, "a".into(), 60);
233 c.insert(1, "bb".into(), 20);
234 let s = c.stats();
235 assert_eq!(s.entries, 1);
236 assert_eq!(s.bytes, 20);
237 assert_eq!(c.get(&1).map(String::as_str), Some("bb"));
238 }
239
240 #[test]
241 fn budget_too_small_to_hold_any_entry_caches_nothing() {
242 let mut c = cache(10);
243 assert!(!c.insert(1, "a".into(), 20));
244 assert!(c.get(&1).is_none());
245 assert_eq!(c.stats().bytes, 0);
246 }
247
248 #[test]
249 fn clear_releases_all_bytes() {
250 let mut c = cache(100);
251 c.insert(1, "a".into(), 40);
252 c.insert(2, "b".into(), 40);
253 c.clear();
254 assert_eq!(c.stats().entries, 0);
255 assert_eq!(c.stats().bytes, 0);
256 }
257}