gglib_core/domain/
slot_eviction.rs1use std::path::PathBuf;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SlotFileMeta {
13 pub path: PathBuf,
14 pub mtime_unix_secs: u64,
15 pub len_bytes: u64,
16}
17
18#[must_use]
25pub fn select_evictions(mut files: Vec<SlotFileMeta>, budget_bytes: u64) -> Vec<PathBuf> {
26 files.sort_by(|a, b| {
27 a.mtime_unix_secs
28 .cmp(&b.mtime_unix_secs)
29 .then_with(|| a.path.cmp(&b.path))
30 });
31
32 let total: u64 = files.iter().map(|f| f.len_bytes).sum();
33 let mut remaining = total;
34 let mut evicted = Vec::new();
35
36 for file in files {
37 if remaining <= budget_bytes {
38 break;
39 }
40 remaining = remaining.saturating_sub(file.len_bytes);
41 evicted.push(file.path);
42 }
43
44 evicted
45}
46
47pub const DISK_BUDGET_FRACTION_DIVISOR: u64 = 4;
52
53#[must_use]
62pub const fn compute_auto_disk_budget_bytes(available_bytes: u64, current_cache_bytes: u64) -> u64 {
63 available_bytes.saturating_add(current_cache_bytes) / DISK_BUDGET_FRACTION_DIVISOR
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 fn meta(path: &str, mtime: u64, len: u64) -> SlotFileMeta {
71 SlotFileMeta {
72 path: PathBuf::from(path),
73 mtime_unix_secs: mtime,
74 len_bytes: len,
75 }
76 }
77
78 #[test]
79 fn under_budget_evicts_nothing() {
80 let files = vec![meta("a.bin", 1, 100), meta("b.bin", 2, 100)];
81 assert!(select_evictions(files, 1000).is_empty());
82 }
83
84 #[test]
85 fn exact_budget_boundary_evicts_nothing() {
86 let files = vec![meta("a.bin", 1, 100), meta("b.bin", 2, 100)];
87 assert!(select_evictions(files, 200).is_empty());
88 }
89
90 #[test]
91 fn evicts_oldest_first_until_under_budget() {
92 let files = vec![
93 meta("oldest.bin", 1, 100),
94 meta("middle.bin", 2, 100),
95 meta("newest.bin", 3, 100),
96 ];
97 let evicted = select_evictions(files, 150);
99 assert_eq!(
100 evicted,
101 vec![PathBuf::from("oldest.bin"), PathBuf::from("middle.bin")]
102 );
103 }
104
105 #[test]
106 fn zero_budget_evicts_everything() {
107 let files = vec![meta("a.bin", 1, 100), meta("b.bin", 2, 100)];
108 let evicted = select_evictions(files, 0);
109 assert_eq!(evicted.len(), 2);
110 }
111
112 #[test]
113 fn single_file_larger_than_budget_is_evicted() {
114 let files = vec![meta("huge.bin", 1, 10_000)];
115 assert_eq!(
116 select_evictions(files, 100),
117 vec![PathBuf::from("huge.bin")]
118 );
119 }
120
121 #[test]
122 fn mtime_ties_break_on_path() {
123 let files = vec![
124 meta("z.bin", 5, 100),
125 meta("a.bin", 5, 100),
126 meta("m.bin", 5, 100),
127 ];
128 let evicted = select_evictions(files, 100);
130 assert_eq!(
131 evicted,
132 vec![PathBuf::from("a.bin"), PathBuf::from("m.bin")]
133 );
134 }
135
136 #[test]
137 fn auto_disk_budget_is_a_quarter_of_available_plus_cache() {
138 let available = 40 * 1024 * 1024 * 1024;
140 let cache = 8 * 1024 * 1024 * 1024;
141 assert_eq!(
142 compute_auto_disk_budget_bytes(available, cache),
143 12 * 1024 * 1024 * 1024
144 );
145 }
146
147 #[test]
148 fn auto_disk_budget_saturates_on_overflow() {
149 assert_eq!(
151 compute_auto_disk_budget_bytes(u64::MAX, u64::MAX),
152 u64::MAX / DISK_BUDGET_FRACTION_DIVISOR
153 );
154 }
155}