1use std::collections::HashMap;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum EvictionStrategy {
10 Lru,
12 Lfu,
15 Fifo,
17 SizePriority,
20}
21
22#[derive(Clone, Debug)]
24pub struct CacheEntry {
25 pub block_id: u64,
27 pub size_bytes: u64,
29 pub inserted_at_tick: u64,
31 pub last_accessed_tick: u64,
33 pub access_count: u64,
35}
36
37#[derive(Clone, Debug)]
39pub struct EvictionCandidate {
40 pub block_id: u64,
42 pub size_bytes: u64,
44 pub reason: EvictionStrategy,
46}
47
48#[derive(Clone, Debug, Default)]
50pub struct PolicyStats {
51 pub total_entries: usize,
53 pub total_size_bytes: u64,
55 pub total_evictions: u64,
57 pub hits: u64,
59 pub misses: u64,
61}
62
63pub struct StorageEvictionPolicy {
69 pub entries: HashMap<u64, CacheEntry>,
71 pub strategy: EvictionStrategy,
73 pub capacity_bytes: u64,
75 pub stats: PolicyStats,
77}
78
79impl StorageEvictionPolicy {
80 pub fn new(strategy: EvictionStrategy, capacity_bytes: u64) -> Self {
82 Self {
83 entries: HashMap::new(),
84 strategy,
85 capacity_bytes,
86 stats: PolicyStats::default(),
87 }
88 }
89
90 pub fn insert(&mut self, block_id: u64, size_bytes: u64, current_tick: u64) {
96 if let Some(existing) = self.entries.get_mut(&block_id) {
97 let old_size = existing.size_bytes;
99 existing.size_bytes = size_bytes;
100 existing.last_accessed_tick = current_tick;
101 self.stats.total_size_bytes = self
103 .stats
104 .total_size_bytes
105 .saturating_sub(old_size)
106 .saturating_add(size_bytes);
107 } else {
108 let entry = CacheEntry {
109 block_id,
110 size_bytes,
111 inserted_at_tick: current_tick,
112 last_accessed_tick: current_tick,
113 access_count: 0,
114 };
115 self.entries.insert(block_id, entry);
116 self.stats.total_size_bytes = self.stats.total_size_bytes.saturating_add(size_bytes);
117 self.stats.total_entries = self.entries.len();
118 }
119 self.stats.total_entries = self.entries.len();
120 }
121
122 pub fn access(&mut self, block_id: u64, current_tick: u64) -> bool {
127 if let Some(entry) = self.entries.get_mut(&block_id) {
128 entry.last_accessed_tick = current_tick;
129 entry.access_count = entry.access_count.saturating_add(1);
130 self.stats.hits = self.stats.hits.saturating_add(1);
131 true
132 } else {
133 self.stats.misses = self.stats.misses.saturating_add(1);
134 false
135 }
136 }
137
138 pub fn evict_to_fit(&mut self) -> Vec<EvictionCandidate> {
144 let mut evicted = Vec::new();
145
146 while self.stats.total_size_bytes > self.capacity_bytes {
147 if self.entries.is_empty() {
148 break;
149 }
150
151 let victim_id = self.select_victim();
152
153 if let Some(entry) = self.entries.remove(&victim_id) {
154 self.stats.total_size_bytes =
155 self.stats.total_size_bytes.saturating_sub(entry.size_bytes);
156 self.stats.total_evictions = self.stats.total_evictions.saturating_add(1);
157 self.stats.total_entries = self.entries.len();
158
159 evicted.push(EvictionCandidate {
160 block_id: entry.block_id,
161 size_bytes: entry.size_bytes,
162 reason: self.strategy,
163 });
164 }
165 }
166
167 evicted
168 }
169
170 fn select_victim(&self) -> u64 {
172 match self.strategy {
173 EvictionStrategy::Lru => {
174 self.entries
176 .values()
177 .min_by_key(|e| e.last_accessed_tick)
178 .map(|e| e.block_id)
179 .expect("entries is non-empty")
180 }
181 EvictionStrategy::Lfu => {
182 self.entries
184 .values()
185 .min_by_key(|e| (e.access_count, e.inserted_at_tick))
186 .map(|e| e.block_id)
187 .expect("entries is non-empty")
188 }
189 EvictionStrategy::Fifo => {
190 self.entries
192 .values()
193 .min_by_key(|e| e.inserted_at_tick)
194 .map(|e| e.block_id)
195 .expect("entries is non-empty")
196 }
197 EvictionStrategy::SizePriority => {
198 self.entries
200 .values()
201 .max_by_key(|e| (e.size_bytes, std::cmp::Reverse(e.block_id)))
202 .map(|e| e.block_id)
203 .expect("entries is non-empty")
204 }
205 }
206 }
207
208 pub fn remove(&mut self, block_id: u64) -> bool {
212 if let Some(entry) = self.entries.remove(&block_id) {
213 self.stats.total_size_bytes =
214 self.stats.total_size_bytes.saturating_sub(entry.size_bytes);
215 self.stats.total_entries = self.entries.len();
216 true
217 } else {
218 false
219 }
220 }
221
222 pub fn is_over_capacity(&self) -> bool {
225 self.stats.total_size_bytes > self.capacity_bytes
226 }
227
228 pub fn stats(&self) -> &PolicyStats {
230 &self.stats
231 }
232
233 pub fn set_strategy(&mut self, strategy: EvictionStrategy) {
235 self.strategy = strategy;
236 }
237}
238
239#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn policy(strategy: EvictionStrategy, cap: u64) -> StorageEvictionPolicy {
250 StorageEvictionPolicy::new(strategy, cap)
251 }
252
253 #[test]
256 fn test_insert_adds_entry_and_updates_size() {
257 let mut p = policy(EvictionStrategy::Lru, 1000);
258 p.insert(1, 100, 1);
259 assert_eq!(p.entries.len(), 1);
260 assert_eq!(p.stats().total_size_bytes, 100);
261 assert_eq!(p.stats().total_entries, 1);
262 }
263
264 #[test]
265 fn test_insert_multiple_entries_accumulates_size() {
266 let mut p = policy(EvictionStrategy::Lru, 1000);
267 p.insert(1, 100, 1);
268 p.insert(2, 200, 2);
269 p.insert(3, 300, 3);
270 assert_eq!(p.stats().total_size_bytes, 600);
271 assert_eq!(p.stats().total_entries, 3);
272 }
273
274 #[test]
275 fn test_insert_existing_block_updates_size_not_inserted_tick() {
276 let mut p = policy(EvictionStrategy::Lru, 1000);
277 p.insert(42, 100, 5);
278 let original_inserted_at = p.entries[&42].inserted_at_tick;
279 p.insert(42, 250, 10);
280 assert_eq!(p.entries[&42].inserted_at_tick, original_inserted_at);
281 assert_eq!(p.entries[&42].size_bytes, 250);
282 assert_eq!(p.stats().total_size_bytes, 250);
283 assert_eq!(p.stats().total_entries, 1);
284 }
285
286 #[test]
287 fn test_insert_existing_block_size_decreases_correctly() {
288 let mut p = policy(EvictionStrategy::Lru, 1000);
289 p.insert(1, 500, 1);
290 p.insert(2, 200, 2);
291 p.insert(1, 100, 3); assert_eq!(p.stats().total_size_bytes, 300);
293 }
294
295 #[test]
298 fn test_access_returns_true_for_existing_entry() {
299 let mut p = policy(EvictionStrategy::Lru, 1000);
300 p.insert(7, 50, 1);
301 assert!(p.access(7, 2));
302 }
303
304 #[test]
305 fn test_access_returns_false_for_missing_entry() {
306 let mut p = policy(EvictionStrategy::Lru, 1000);
307 assert!(!p.access(99, 1));
308 }
309
310 #[test]
311 fn test_access_updates_last_accessed_tick_and_count() {
312 let mut p = policy(EvictionStrategy::Lru, 1000);
313 p.insert(1, 100, 1);
314 p.access(1, 10);
315 p.access(1, 20);
316 let e = &p.entries[&1];
317 assert_eq!(e.last_accessed_tick, 20);
318 assert_eq!(e.access_count, 2);
319 }
320
321 #[test]
322 fn test_access_increments_hits() {
323 let mut p = policy(EvictionStrategy::Lru, 1000);
324 p.insert(1, 100, 1);
325 p.access(1, 2);
326 p.access(1, 3);
327 assert_eq!(p.stats().hits, 2);
328 assert_eq!(p.stats().misses, 0);
329 }
330
331 #[test]
332 fn test_access_increments_misses() {
333 let mut p = policy(EvictionStrategy::Lru, 1000);
334 p.access(999, 1);
335 p.access(998, 1);
336 assert_eq!(p.stats().misses, 2);
337 assert_eq!(p.stats().hits, 0);
338 }
339
340 #[test]
343 fn test_evict_lru_evicts_least_recently_used() {
344 let mut p = policy(EvictionStrategy::Lru, 200);
345 p.insert(1, 100, 1);
346 p.insert(2, 100, 2);
347 p.insert(3, 100, 3); p.access(1, 10);
350 let evicted = p.evict_to_fit();
352 assert_eq!(evicted.len(), 1);
353 assert_eq!(evicted[0].block_id, 2);
354 assert_eq!(evicted[0].reason, EvictionStrategy::Lru);
355 }
356
357 #[test]
358 fn test_evict_lru_multiple_rounds() {
359 let mut p = policy(EvictionStrategy::Lru, 100);
360 p.insert(1, 100, 1);
361 p.insert(2, 100, 2);
362 p.insert(3, 100, 3); let evicted = p.evict_to_fit();
364 assert_eq!(evicted.len(), 2);
365 let ids: Vec<u64> = evicted.iter().map(|c| c.block_id).collect();
366 assert!(ids.contains(&1));
367 assert!(ids.contains(&2));
368 }
369
370 #[test]
373 fn test_evict_lfu_evicts_least_frequently_used() {
374 let mut p = policy(EvictionStrategy::Lfu, 200);
375 p.insert(1, 100, 1);
376 p.insert(2, 100, 2);
377 p.insert(3, 100, 3); p.access(1, 5);
379 p.access(1, 6);
380 p.access(2, 7);
381 let evicted = p.evict_to_fit();
383 assert_eq!(evicted.len(), 1);
384 assert_eq!(evicted[0].block_id, 3);
385 assert_eq!(evicted[0].reason, EvictionStrategy::Lfu);
386 }
387
388 #[test]
389 fn test_evict_lfu_tie_broken_by_earliest_inserted() {
390 let mut p = policy(EvictionStrategy::Lfu, 100);
391 p.insert(1, 100, 1);
393 p.insert(2, 100, 2); let evicted = p.evict_to_fit();
395 assert_eq!(evicted.len(), 1);
396 assert_eq!(evicted[0].block_id, 1); }
398
399 #[test]
402 fn test_evict_fifo_evicts_oldest_first() {
403 let mut p = policy(EvictionStrategy::Fifo, 200);
404 p.insert(1, 100, 1);
405 p.insert(2, 100, 2);
406 p.insert(3, 100, 3); p.access(1, 10);
409 p.access(1, 11);
410 p.access(1, 12);
411 let evicted = p.evict_to_fit();
412 assert_eq!(evicted.len(), 1);
413 assert_eq!(evicted[0].block_id, 1); assert_eq!(evicted[0].reason, EvictionStrategy::Fifo);
415 }
416
417 #[test]
418 fn test_evict_fifo_multiple_entries() {
419 let mut p = policy(EvictionStrategy::Fifo, 50);
420 p.insert(10, 100, 5);
421 p.insert(20, 100, 3);
422 p.insert(30, 100, 7); let evicted = p.evict_to_fit();
424 assert_eq!(evicted[0].block_id, 20);
426 assert_eq!(evicted[1].block_id, 10);
427 }
428
429 #[test]
432 fn test_evict_size_priority_evicts_largest_first() {
433 let mut p = policy(EvictionStrategy::SizePriority, 200);
434 p.insert(1, 50, 1);
435 p.insert(2, 300, 2);
436 p.insert(3, 100, 3); let evicted = p.evict_to_fit();
438 assert_eq!(evicted[0].block_id, 2); assert_eq!(evicted[0].reason, EvictionStrategy::SizePriority);
440 }
441
442 #[test]
443 fn test_evict_size_priority_tie_smallest_block_id() {
444 let mut p = policy(EvictionStrategy::SizePriority, 100);
445 p.insert(5, 200, 1);
447 p.insert(3, 200, 2); let evicted = p.evict_to_fit();
449 assert_eq!(evicted[0].block_id, 3); }
451
452 #[test]
455 fn test_evict_to_fit_stops_when_under_capacity() {
456 let mut p = policy(EvictionStrategy::Lru, 250);
457 p.insert(1, 100, 1);
458 p.insert(2, 100, 2);
459 p.insert(3, 100, 3); let evicted = p.evict_to_fit();
461 assert_eq!(evicted.len(), 1);
462 assert!(p.stats().total_size_bytes <= 250);
463 }
464
465 #[test]
466 fn test_evict_to_fit_noop_when_under_capacity() {
467 let mut p = policy(EvictionStrategy::Lru, 1000);
468 p.insert(1, 100, 1);
469 let evicted = p.evict_to_fit();
470 assert!(evicted.is_empty());
471 assert_eq!(p.stats().total_evictions, 0);
472 }
473
474 #[test]
477 fn test_evict_increments_total_evictions() {
478 let mut p = policy(EvictionStrategy::Lru, 100);
479 p.insert(1, 100, 1);
480 p.insert(2, 100, 2);
481 p.insert(3, 100, 3); p.evict_to_fit();
483 assert_eq!(p.stats().total_evictions, 2);
484 }
485
486 #[test]
489 fn test_remove_existing_entry_returns_true_and_updates_size() {
490 let mut p = policy(EvictionStrategy::Lru, 1000);
491 p.insert(1, 400, 1);
492 p.insert(2, 200, 2);
493 assert!(p.remove(1));
494 assert_eq!(p.stats().total_size_bytes, 200);
495 assert_eq!(p.stats().total_entries, 1);
496 assert!(!p.entries.contains_key(&1));
497 }
498
499 #[test]
500 fn test_remove_nonexistent_entry_returns_false() {
501 let mut p = policy(EvictionStrategy::Lru, 1000);
502 assert!(!p.remove(999));
503 assert_eq!(p.stats().total_size_bytes, 0);
504 }
505
506 #[test]
509 fn test_is_over_capacity_true() {
510 let mut p = policy(EvictionStrategy::Lru, 50);
511 p.insert(1, 100, 1);
512 assert!(p.is_over_capacity());
513 }
514
515 #[test]
516 fn test_is_over_capacity_false() {
517 let mut p = policy(EvictionStrategy::Lru, 500);
518 p.insert(1, 100, 1);
519 assert!(!p.is_over_capacity());
520 }
521
522 #[test]
523 fn test_is_over_capacity_exactly_at_capacity() {
524 let mut p = policy(EvictionStrategy::Lru, 100);
525 p.insert(1, 100, 1);
526 assert!(!p.is_over_capacity()); }
528
529 #[test]
532 fn test_set_strategy_changes_policy() {
533 let mut p = policy(EvictionStrategy::Lru, 200);
534 assert_eq!(p.strategy, EvictionStrategy::Lru);
535 p.set_strategy(EvictionStrategy::Fifo);
536 assert_eq!(p.strategy, EvictionStrategy::Fifo);
537 }
538
539 #[test]
540 fn test_set_strategy_affects_subsequent_eviction() {
541 let mut p = policy(EvictionStrategy::Lru, 200);
542 p.insert(1, 100, 1);
543 p.insert(2, 100, 2);
544 p.insert(3, 100, 3); p.set_strategy(EvictionStrategy::Fifo);
547 let evicted = p.evict_to_fit();
548 assert_eq!(evicted[0].reason, EvictionStrategy::Fifo);
549 assert_eq!(evicted[0].block_id, 1); }
551
552 #[test]
555 fn test_eviction_candidate_reason_matches_strategy_lru() {
556 let mut p = policy(EvictionStrategy::Lru, 50);
557 p.insert(1, 100, 1);
558 let evicted = p.evict_to_fit();
559 assert_eq!(evicted[0].reason, EvictionStrategy::Lru);
560 }
561
562 #[test]
563 fn test_eviction_candidate_reason_matches_strategy_lfu() {
564 let mut p = policy(EvictionStrategy::Lfu, 50);
565 p.insert(1, 100, 1);
566 let evicted = p.evict_to_fit();
567 assert_eq!(evicted[0].reason, EvictionStrategy::Lfu);
568 }
569
570 #[test]
571 fn test_eviction_candidate_reason_matches_strategy_fifo() {
572 let mut p = policy(EvictionStrategy::Fifo, 50);
573 p.insert(1, 100, 1);
574 let evicted = p.evict_to_fit();
575 assert_eq!(evicted[0].reason, EvictionStrategy::Fifo);
576 }
577
578 #[test]
579 fn test_eviction_candidate_reason_matches_strategy_size_priority() {
580 let mut p = policy(EvictionStrategy::SizePriority, 50);
581 p.insert(1, 100, 1);
582 let evicted = p.evict_to_fit();
583 assert_eq!(evicted[0].reason, EvictionStrategy::SizePriority);
584 }
585
586 #[test]
589 fn test_stats_returns_current_state() {
590 let mut p = policy(EvictionStrategy::Lru, 1000);
591 p.insert(1, 300, 1);
592 p.insert(2, 200, 2);
593 p.access(1, 5);
594 p.access(99, 6); let s = p.stats();
596 assert_eq!(s.total_entries, 2);
597 assert_eq!(s.total_size_bytes, 500);
598 assert_eq!(s.hits, 1);
599 assert_eq!(s.misses, 1);
600 assert_eq!(s.total_evictions, 0);
601 }
602
603 #[test]
606 fn test_eviction_candidate_carries_correct_size() {
607 let mut p = policy(EvictionStrategy::SizePriority, 50);
608 p.insert(7, 777, 1);
609 let evicted = p.evict_to_fit();
610 assert_eq!(evicted[0].size_bytes, 777);
611 assert_eq!(evicted[0].block_id, 7);
612 }
613}