1#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct GCCandidate {
33 pub cid: String,
35 pub size_bytes: u64,
37 pub ref_count: u32,
40 pub last_accessed_secs: u64,
42 pub pinned: bool,
44}
45
46#[derive(Debug, Clone, Default)]
49pub struct GCPlan {
50 pub candidates: Vec<GCCandidate>,
53 pub estimated_freed_bytes: u64,
55}
56
57impl GCPlan {
58 pub fn candidate_count(&self) -> usize {
60 self.candidates.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.candidates.is_empty()
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct GCConfig {
72 pub min_age_secs: u64,
75 pub target_free_bytes: u64,
78 pub max_candidates: usize,
81}
82
83impl Default for GCConfig {
84 fn default() -> Self {
85 Self {
86 min_age_secs: 3_600,
87 target_free_bytes: 1_073_741_824,
88 max_candidates: 1_000,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
96pub struct GCPlannerStats {
97 pub total_analyzed: usize,
99 pub pinned_skipped: usize,
101 pub referenced_skipped: usize,
103 pub too_young_skipped: usize,
105 pub selected_count: usize,
107 pub estimated_freed_bytes: u64,
109}
110
111#[derive(Debug, Clone)]
117pub struct StorageGCPlanner {
118 pub config: GCConfig,
120}
121
122impl StorageGCPlanner {
123 pub fn new(config: GCConfig) -> Self {
125 Self { config }
126 }
127
128 pub fn plan(&self, candidates: &[GCCandidate], now_secs: u64) -> (GCPlan, GCPlannerStats) {
142 let mut stats = GCPlannerStats {
143 total_analyzed: candidates.len(),
144 ..Default::default()
145 };
146
147 let mut eligible: Vec<&GCCandidate> = Vec::with_capacity(candidates.len());
148
149 for candidate in candidates {
150 if candidate.pinned {
151 stats.pinned_skipped += 1;
152 continue;
153 }
154 if candidate.ref_count > 0 {
155 stats.referenced_skipped += 1;
156 continue;
157 }
158 let age = now_secs.saturating_sub(candidate.last_accessed_secs);
161 if age < self.config.min_age_secs {
162 stats.too_young_skipped += 1;
163 continue;
164 }
165 eligible.push(candidate);
166 }
167
168 eligible.sort_by_key(|c| c.last_accessed_secs);
170
171 let mut plan_candidates: Vec<GCCandidate> = Vec::new();
172 let mut freed_bytes: u64 = 0;
173
174 for candidate in eligible {
175 if plan_candidates.len() >= self.config.max_candidates {
176 break;
177 }
178 plan_candidates.push(candidate.clone());
179 freed_bytes = freed_bytes.saturating_add(candidate.size_bytes);
180 if freed_bytes >= self.config.target_free_bytes {
181 break;
182 }
183 }
184
185 stats.selected_count = plan_candidates.len();
186 stats.estimated_freed_bytes = freed_bytes;
187
188 let plan = GCPlan {
189 candidates: plan_candidates,
190 estimated_freed_bytes: freed_bytes,
191 };
192
193 (plan, stats)
194 }
195
196 pub fn estimate_run_time_ms(&self, plan: &GCPlan) -> u64 {
202 let per_candidate = plan.candidate_count() as u64;
203 let per_data = plan.estimated_freed_bytes / 10_485_760;
204 per_candidate + per_data
205 }
206}
207
208#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn make_candidate(
217 cid: &str,
218 size_bytes: u64,
219 ref_count: u32,
220 last_accessed_secs: u64,
221 pinned: bool,
222 ) -> GCCandidate {
223 GCCandidate {
224 cid: cid.to_string(),
225 size_bytes,
226 ref_count,
227 last_accessed_secs,
228 pinned,
229 }
230 }
231
232 fn default_planner() -> StorageGCPlanner {
233 StorageGCPlanner::new(GCConfig::default())
234 }
235
236 #[test]
239 fn test_new_stores_config() {
240 let config = GCConfig {
241 min_age_secs: 7_200,
242 target_free_bytes: 512 * 1024 * 1024,
243 max_candidates: 500,
244 };
245 let planner = StorageGCPlanner::new(config.clone());
246 assert_eq!(planner.config.min_age_secs, 7_200);
247 assert_eq!(planner.config.target_free_bytes, 512 * 1024 * 1024);
248 assert_eq!(planner.config.max_candidates, 500);
249 }
250
251 #[test]
252 fn test_gc_config_default_values() {
253 let cfg = GCConfig::default();
254 assert_eq!(cfg.min_age_secs, 3_600);
255 assert_eq!(cfg.target_free_bytes, 1_073_741_824);
256 assert_eq!(cfg.max_candidates, 1_000);
257 }
258
259 #[test]
262 fn test_empty_input_produces_empty_plan() {
263 let planner = default_planner();
264 let (plan, stats) = planner.plan(&[], 10_000);
265 assert!(plan.is_empty());
266 assert_eq!(plan.candidate_count(), 0);
267 assert_eq!(plan.estimated_freed_bytes, 0);
268 assert_eq!(stats.total_analyzed, 0);
269 assert_eq!(stats.selected_count, 0);
270 }
271
272 #[test]
275 fn test_pinned_blocks_skipped() {
276 let planner = default_planner();
277 let candidates = vec![make_candidate("c1", 1024, 0, 0, true)];
278 let (plan, stats) = planner.plan(&candidates, 10_000);
279 assert!(plan.is_empty());
280 assert_eq!(stats.pinned_skipped, 1);
281 assert_eq!(stats.referenced_skipped, 0);
282 assert_eq!(stats.too_young_skipped, 0);
283 }
284
285 #[test]
286 fn test_referenced_blocks_skipped() {
287 let planner = default_planner();
288 let candidates = vec![make_candidate("c1", 1024, 3, 0, false)];
289 let (plan, stats) = planner.plan(&candidates, 10_000);
290 assert!(plan.is_empty());
291 assert_eq!(stats.referenced_skipped, 1);
292 assert_eq!(stats.pinned_skipped, 0);
293 assert_eq!(stats.too_young_skipped, 0);
294 }
295
296 #[test]
297 fn test_too_young_blocks_skipped() {
298 let planner = default_planner(); let candidates = vec![make_candidate("c1", 1024, 0, 7_000, false)];
301 let (plan, stats) = planner.plan(&candidates, 10_000);
302 assert!(plan.is_empty());
303 assert_eq!(stats.too_young_skipped, 1);
304 }
305
306 #[test]
307 fn test_exactly_at_min_age_is_skipped() {
308 let planner = default_planner(); let candidates = vec![make_candidate("c1", 1024, 0, 6_400, false)];
313 let (plan, stats) = planner.plan(&candidates, 10_000);
314 assert_eq!(
315 plan.candidate_count(),
316 1,
317 "block at exactly min_age must be included (skip condition is strictly less than)"
318 );
319 assert_eq!(stats.too_young_skipped, 0);
320 assert_eq!(stats.selected_count, 1);
321 }
322
323 #[test]
324 fn test_eligible_block_included() {
325 let planner = default_planner(); let candidates = vec![make_candidate("c1", 1024, 0, 6_000, false)];
328 let (plan, stats) = planner.plan(&candidates, 10_000);
329 assert_eq!(plan.candidate_count(), 1);
330 assert_eq!(stats.selected_count, 1);
331 assert_eq!(plan.candidates[0].cid, "c1");
332 }
333
334 #[test]
337 fn test_plan_sorted_oldest_first() {
338 let planner = default_planner();
339 let candidates = vec![
341 make_candidate("newer", 512, 0, 200, false),
342 make_candidate("oldest", 512, 0, 0, false),
343 make_candidate("middle", 512, 0, 100, false),
344 ];
345 let (plan, _) = planner.plan(&candidates, 10_000);
346 assert_eq!(plan.candidate_count(), 3);
347 assert_eq!(plan.candidates[0].cid, "oldest");
348 assert_eq!(plan.candidates[1].cid, "middle");
349 assert_eq!(plan.candidates[2].cid, "newer");
350 }
351
352 #[test]
355 fn test_target_free_bytes_stops_early() {
356 let config = GCConfig {
357 min_age_secs: 0,
358 target_free_bytes: 1_000,
359 max_candidates: 1_000,
360 };
361 let planner = StorageGCPlanner::new(config);
362 let candidates = vec![
363 make_candidate("c1", 600, 0, 0, false),
364 make_candidate("c2", 600, 0, 1, false),
365 make_candidate("c3", 600, 0, 2, false),
366 ];
367 let (plan, stats) = planner.plan(&candidates, 100);
368 assert_eq!(plan.candidate_count(), 2);
370 assert_eq!(stats.selected_count, 2);
371 assert!(plan.estimated_freed_bytes >= 1_000);
372 }
373
374 #[test]
375 fn test_max_candidates_stops_early() {
376 let config = GCConfig {
377 min_age_secs: 0,
378 target_free_bytes: u64::MAX,
379 max_candidates: 2,
380 };
381 let planner = StorageGCPlanner::new(config);
382 let candidates: Vec<GCCandidate> = (0..5_u64)
383 .map(|i| make_candidate(&format!("c{i}"), 100, 0, i, false))
384 .collect();
385 let (plan, stats) = planner.plan(&candidates, 1_000);
386 assert_eq!(plan.candidate_count(), 2);
387 assert_eq!(stats.selected_count, 2);
388 }
389
390 #[test]
393 fn test_estimated_freed_bytes_sum_correct() {
394 let config = GCConfig {
395 min_age_secs: 0,
396 target_free_bytes: u64::MAX,
397 max_candidates: 1_000,
398 };
399 let planner = StorageGCPlanner::new(config);
400 let candidates = vec![
401 make_candidate("c1", 1_000, 0, 0, false),
402 make_candidate("c2", 2_000, 0, 1, false),
403 make_candidate("c3", 3_000, 0, 2, false),
404 ];
405 let (plan, stats) = planner.plan(&candidates, 10_000);
406 assert_eq!(plan.estimated_freed_bytes, 6_000);
407 assert_eq!(stats.estimated_freed_bytes, 6_000);
408 }
409
410 #[test]
411 fn test_is_empty_true_when_no_candidates() {
412 let plan = GCPlan::default();
413 assert!(plan.is_empty());
414 }
415
416 #[test]
417 fn test_candidate_count_correct() {
418 let planner = StorageGCPlanner::new(GCConfig {
419 min_age_secs: 0,
420 target_free_bytes: u64::MAX,
421 max_candidates: 1_000,
422 });
423 let candidates: Vec<GCCandidate> = (0..7_u64)
424 .map(|i| make_candidate(&format!("c{i}"), 1, 0, i, false))
425 .collect();
426 let (plan, _) = planner.plan(&candidates, 1_000);
427 assert_eq!(plan.candidate_count(), 7);
428 }
429
430 #[test]
433 fn test_stats_total_analyzed_correct() {
434 let planner = default_planner();
435 let candidates: Vec<GCCandidate> = (0..10_u64)
436 .map(|i| make_candidate(&format!("c{i}"), 1, 0, 0, false))
437 .collect();
438 let (_, stats) = planner.plan(&candidates, 10_000);
439 assert_eq!(stats.total_analyzed, 10);
440 }
441
442 #[test]
443 fn test_stats_pinned_skipped_correct() {
444 let planner = default_planner();
445 let candidates = vec![
446 make_candidate("p1", 1, 0, 0, true),
447 make_candidate("p2", 1, 0, 0, true),
448 make_candidate("ok", 1, 0, 0, false),
449 ];
450 let (_, stats) = planner.plan(&candidates, 10_000);
451 assert_eq!(stats.pinned_skipped, 2);
452 }
453
454 #[test]
455 fn test_stats_referenced_skipped_correct() {
456 let planner = default_planner();
457 let candidates = vec![
458 make_candidate("r1", 1, 1, 0, false),
459 make_candidate("r2", 1, 5, 0, false),
460 make_candidate("ok", 1, 0, 0, false),
461 ];
462 let (_, stats) = planner.plan(&candidates, 10_000);
463 assert_eq!(stats.referenced_skipped, 2);
464 }
465
466 #[test]
467 fn test_stats_too_young_skipped_correct() {
468 let planner = default_planner(); let candidates = vec![
470 make_candidate("y1", 1, 0, 8_000, false),
472 make_candidate("y2", 1, 0, 7_000, false),
474 make_candidate("ok", 1, 0, 0, false),
476 ];
477 let (_, stats) = planner.plan(&candidates, 10_000);
478 assert_eq!(stats.too_young_skipped, 2);
479 }
480
481 #[test]
482 fn test_stats_selected_count_correct() {
483 let planner = StorageGCPlanner::new(GCConfig {
484 min_age_secs: 0,
485 target_free_bytes: u64::MAX,
486 max_candidates: 1_000,
487 });
488 let candidates: Vec<GCCandidate> = (0..6_u64)
489 .map(|i| make_candidate(&format!("c{i}"), 1, 0, i, false))
490 .collect();
491 let (_, stats) = planner.plan(&candidates, 1_000);
492 assert_eq!(stats.selected_count, 6);
493 }
494
495 #[test]
498 fn test_multiple_skip_reasons_combine_correctly() {
499 let planner = default_planner(); let candidates = vec![
501 make_candidate("pinned", 1, 0, 0, true), make_candidate("ref'd", 1, 2, 0, false), make_candidate("young", 1, 0, 8_000, false), make_candidate("eligible", 1_024, 0, 0, false), ];
506 let (plan, stats) = planner.plan(&candidates, 10_000);
507 assert_eq!(stats.total_analyzed, 4);
508 assert_eq!(stats.pinned_skipped, 1);
509 assert_eq!(stats.referenced_skipped, 1);
510 assert_eq!(stats.too_young_skipped, 1);
511 assert_eq!(stats.selected_count, 1);
512 assert_eq!(plan.candidate_count(), 1);
513 assert_eq!(plan.candidates[0].cid, "eligible");
514 }
515
516 #[test]
519 fn test_estimate_run_time_ms_basic() {
520 let planner = default_planner();
521 let plan = GCPlan {
523 candidates: vec![
524 make_candidate("c1", 10_485_760, 0, 0, false),
525 make_candidate("c2", 10_485_760, 0, 1, false),
526 make_candidate("c3", 10_485_760, 0, 2, false),
527 ],
528 estimated_freed_bytes: 31_457_280, };
530 let ms = planner.estimate_run_time_ms(&plan);
531 assert_eq!(ms, 6); }
533
534 #[test]
535 fn test_estimate_run_time_ms_empty_plan() {
536 let planner = default_planner();
537 let plan = GCPlan::default();
538 assert_eq!(planner.estimate_run_time_ms(&plan), 0);
539 }
540
541 #[test]
542 fn test_estimate_run_time_ms_large_data() {
543 let planner = default_planner();
544 let plan = GCPlan {
546 candidates: vec![make_candidate("big", 104_857_600, 0, 0, false)],
547 estimated_freed_bytes: 104_857_600,
548 };
549 let ms = planner.estimate_run_time_ms(&plan);
550 assert_eq!(ms, 11); }
552
553 #[test]
556 fn test_future_last_accessed_treated_as_too_young() {
557 let planner = default_planner();
559 let candidates = vec![make_candidate("future", 1_024, 0, 99_999, false)];
560 let (plan, stats) = planner.plan(&candidates, 10_000);
561 assert!(plan.is_empty());
562 assert_eq!(stats.too_young_skipped, 1);
563 }
564
565 #[test]
566 fn test_ref_count_one_still_skipped() {
567 let planner = default_planner();
568 let candidates = vec![make_candidate("one_ref", 512, 1, 0, false)];
569 let (plan, stats) = planner.plan(&candidates, 10_000);
570 assert!(plan.is_empty());
571 assert_eq!(stats.referenced_skipped, 1);
572 }
573}