qualia_core_db/inference/runtime/artifacts/
cleanup.rs1use std::fs;
2use std::path::Path;
3use std::time::SystemTime;
4
5use super::budget::ArtifactError;
6use super::run_dir::RUN_MARKER_FILE;
7
8const RUN_PREFIX: &str = "qualia-inference-";
9const MARKER_CONTENT: &[u8] = b"qualia-inference-run-v1\n";
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct StaleCleanupReport {
13 pub candidates: u64,
14 pub removed_runs: u64,
15 pub removed_bytes: u64,
16 pub failures: u64,
17}
18
19fn directory_bytes(path: &Path) -> std::io::Result<u64> {
20 let mut total = 0u64;
21 for entry in fs::read_dir(path)? {
22 let entry = entry?;
23 let file_type = entry.file_type()?;
24 if file_type.is_symlink() {
25 continue;
26 }
27 if file_type.is_dir() {
28 total = total.saturating_add(directory_bytes(&entry.path())?);
29 } else if file_type.is_file() {
30 total = total.saturating_add(entry.metadata()?.len());
31 }
32 }
33 Ok(total)
34}
35
36pub fn cleanup_stale_runs(
41 parent: &Path,
42 modified_before: SystemTime,
43) -> Result<StaleCleanupReport, ArtifactError> {
44 let canonical_parent = parent.canonicalize()?;
45 let mut report = StaleCleanupReport::default();
46 for entry in fs::read_dir(&canonical_parent)? {
47 let Ok(entry) = entry else {
48 report.failures = report.failures.saturating_add(1);
49 continue;
50 };
51 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
52 continue;
53 };
54 if !name.starts_with(RUN_PREFIX) {
55 continue;
56 }
57 let Ok(file_type) = entry.file_type() else {
58 report.failures = report.failures.saturating_add(1);
59 continue;
60 };
61 if !file_type.is_dir() || file_type.is_symlink() {
62 continue;
63 }
64 report.candidates = report.candidates.saturating_add(1);
65 let path = entry.path();
66 let marker = path.join(RUN_MARKER_FILE);
67 if fs::read(&marker).ok().as_deref() != Some(MARKER_CONTENT) {
68 continue;
69 }
70 let Ok(metadata) = entry.metadata() else {
71 report.failures = report.failures.saturating_add(1);
72 continue;
73 };
74 if metadata
75 .modified()
76 .map_or(true, |time| time >= modified_before)
77 {
78 continue;
79 }
80 let Ok(canonical_path) = path.canonicalize() else {
81 report.failures = report.failures.saturating_add(1);
82 continue;
83 };
84 if canonical_path.parent() != Some(canonical_parent.as_path()) {
85 report.failures = report.failures.saturating_add(1);
86 continue;
87 }
88 let bytes = directory_bytes(&canonical_path).unwrap_or(0);
89 match fs::remove_dir_all(&canonical_path) {
90 Ok(()) => {
91 report.removed_runs = report.removed_runs.saturating_add(1);
92 report.removed_bytes = report.removed_bytes.saturating_add(bytes);
93 }
94 Err(_) => report.failures = report.failures.saturating_add(1),
95 }
96 }
97 Ok(report)
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn removes_only_marker_owned_prefixed_directories() {
106 let parent = tempfile::tempdir().unwrap();
107 let stale = parent.path().join("qualia-inference-test-stale");
108 let unmarked = parent.path().join("qualia-inference-test-unmarked");
109 let retained = parent.path().join("retained-evidence");
110 fs::create_dir_all(&stale).unwrap();
111 fs::create_dir_all(&unmarked).unwrap();
112 fs::create_dir_all(&retained).unwrap();
113 fs::write(stale.join(RUN_MARKER_FILE), MARKER_CONTENT).unwrap();
114 fs::write(stale.join("payload.bin"), [1u8; 8]).unwrap();
115 fs::write(retained.join(RUN_MARKER_FILE), MARKER_CONTENT).unwrap();
116
117 let report = cleanup_stale_runs(
118 parent.path(),
119 SystemTime::now() + std::time::Duration::from_secs(1),
120 )
121 .unwrap();
122 assert_eq!(report.removed_runs, 1);
123 assert!(report.removed_bytes >= 8);
124 assert!(!stale.exists());
125 assert!(unmarked.exists());
126 assert!(retained.exists());
127 }
128}