1use crate::ids::{GenerationId, Timestamp};
12use serde::{Deserialize, Serialize};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase", default)]
17pub struct GcBudget {
18 pub keep_generations: usize,
20 pub generations_permille: u32,
23 pub keep_emergency: usize,
26 pub retain_all: bool,
31}
32
33impl Default for GcBudget {
34 fn default() -> Self {
35 GcBudget { keep_generations: 10, generations_permille: 200, keep_emergency: 3, retain_all: false }
36 }
37}
38
39impl GcBudget {
40 pub fn max_bytes(&self, fs_bytes: u64) -> u64 {
42 (fs_bytes as u128 * self.generations_permille as u128 / 1000) as u64
43 }
44
45 pub fn protected(&self, on_disk: &[GenerationId], protect: &[GenerationId]) -> Vec<GenerationId> {
48 if self.retain_all { on_disk.to_vec() } else { protect.to_vec() }
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub struct GcPlan {
54 pub delete: Vec<GenerationId>,
56 pub kept: Vec<GenerationId>,
57 pub kept_bytes: u64,
59 pub max_bytes: u64,
60 pub shortfall: Option<u64>,
62}
63
64pub fn plan(
67 gens: &[GenerationId],
68 protect: &[GenerationId],
69 keep: usize,
70 max_bytes: u64,
71 mut measure: impl FnMut(&[GenerationId]) -> u64,
72) -> GcPlan {
73 let mut kept: Vec<GenerationId> = gens.to_vec();
74 kept.sort();
75 kept.dedup();
76 let mut delete = vec![];
77 let mut bytes = measure(&kept);
78 loop {
79 if kept.len() <= keep && bytes <= max_bytes {
80 break;
81 }
82 let Some(i) = kept.iter().position(|g| !protect.contains(g)) else { break };
83 delete.push(kept.remove(i));
84 bytes = measure(&kept);
85 }
86 let shortfall = (bytes > max_bytes).then(|| bytes - max_bytes);
87 GcPlan { delete, kept, kept_bytes: bytes, max_bytes, shortfall }
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92pub struct SpaceReport {
93 pub at: Timestamp,
94 pub generations: usize,
95 pub bytes: u64,
96 pub max_bytes: u64,
97 pub collected: usize,
98 pub shortfall: Option<u64>,
99}
100
101impl SpaceReport {
102 pub fn of(plan: &GcPlan, at: Timestamp) -> SpaceReport {
103 SpaceReport {
104 at,
105 generations: plan.kept.len(),
106 bytes: plan.kept_bytes,
107 max_bytes: plan.max_bytes,
108 collected: plan.delete.len(),
109 shortfall: plan.shortfall,
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn g(n: u64) -> GenerationId {
119 GenerationId(n)
120 }
121
122 const GB: u64 = 1 << 30;
123
124 fn size(gs: &[GenerationId]) -> u64 {
126 gs.len() as u64 * 2 * GB
127 }
128
129 #[test]
130 fn retain_all_collects_no_generation_and_reports_the_overrun() {
131 let gens: Vec<_> = (1..=13).map(g).collect();
132 let b = GcBudget { retain_all: true, ..GcBudget::default() };
133 let p = plan(&gens, &b.protected(&gens, &[g(13)]), b.keep_emergency, 7 * GB, size);
134 assert!(p.delete.is_empty());
135 assert_eq!(p.kept.len(), 13);
136 assert_eq!(p.shortfall, Some(26 * GB - 7 * GB));
137 }
138
139 #[test]
140 fn count_bound_collects_oldest_first() {
141 let gens: Vec<_> = (1..=13).map(g).collect();
142 let p = plan(&gens, &[g(2), g(13)], 10, u64::MAX, size);
143 assert_eq!(p.delete, vec![g(1), g(3), g(4)]);
144 assert_eq!(p.kept.len(), 10);
145 assert!(p.kept.contains(&g(2)), "protected stays even when oldest");
146 assert_eq!(p.shortfall, None);
147 }
148
149 #[test]
150 fn space_bound_collects_below_the_count() {
151 let gens: Vec<_> = (1..=6).map(g).collect();
152 let p = plan(&gens, &[g(6)], 10, 7 * GB, size);
153 assert_eq!(p.delete, vec![g(1), g(2), g(3)]);
154 assert_eq!(p.kept_bytes, 6 * GB);
155 assert_eq!(p.shortfall, None);
156 }
157
158 #[test]
159 fn unsatisfiable_budget_never_touches_the_protected_set() {
160 let gens: Vec<_> = (1..=5).map(g).collect();
161 let protect = [g(1), g(4), g(5)];
162 let p = plan(&gens, &protect, 10, GB, size);
163 assert_eq!(p.delete, vec![g(2), g(3)]);
164 assert_eq!(p.kept, protect.to_vec());
165 assert_eq!(p.shortfall, Some(5 * GB));
166 }
167
168 #[test]
169 fn within_both_bounds_collects_nothing() {
170 let gens: Vec<_> = (1..=4).map(g).collect();
171 let p = plan(&gens, &[], 10, 100 * GB, size);
172 assert!(p.delete.is_empty());
173 }
174
175 #[test]
176 fn budget_share() {
177 assert_eq!(GcBudget::default().max_bytes(100 * GB), 20 * GB);
178 }
179}