running_process/cleanup/
prune.rs1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::broker::manifest;
5use crate::broker::protocol::CacheManifest;
6use crate::cleanup::{delete_path, now_unix_ms, root_is_prunable, CleanupAction, CleanupError};
7
8#[derive(Debug, Clone)]
10pub struct PruneOptions {
11 pub dormant_after_secs: Option<u64>,
13 pub keep_current: bool,
15 pub keep_last: Option<usize>,
17 pub service: Option<String>,
19 pub version: Option<String>,
21 pub confirm: bool,
23}
24
25pub fn run(
27 registry_dir: &Path,
28 options: &PruneOptions,
29) -> Result<Vec<CleanupAction>, CleanupError> {
30 let manifests = manifest::enumerate_central(registry_dir);
31 let keep_last = keep_last_keys(&manifests, options.keep_last);
32 let now_ms = now_unix_ms();
33 let mut actions = Vec::new();
34
35 for manifest in manifests {
36 if let Some(service) = &options.service {
37 if &manifest.service_name != service {
38 continue;
39 }
40 }
41 if let Some(version) = &options.version {
42 if &manifest.service_version != version {
43 continue;
44 }
45 }
46 let key = manifest_key(&manifest);
47 if keep_last.contains_key(&key) {
48 continue;
49 }
50 if options.keep_current && manifest.current_daemon.is_some() {
51 continue;
52 }
53 if let Some(dormant_after_secs) = options.dormant_after_secs {
54 let dormant_ms = dormant_after_secs.saturating_mul(1000);
55 if now_ms.saturating_sub(manifest.last_active_unix_ms) < dormant_ms {
56 continue;
57 }
58 }
59
60 for root in &manifest.roots {
61 let path = std::path::PathBuf::from(&root.path);
62 if !root_is_prunable(root) {
63 actions.push(CleanupAction {
64 service_name: manifest.service_name.clone(),
65 service_version: manifest.service_version.clone(),
66 path,
67 reason: "prune".to_string(),
68 deleted: false,
69 skipped: true,
70 skip_reason: Some("root disposition is not prunable".to_string()),
71 });
72 continue;
73 }
74 if options.confirm {
75 delete_path(&path)?;
76 }
77 actions.push(CleanupAction {
78 service_name: manifest.service_name.clone(),
79 service_version: manifest.service_version.clone(),
80 path,
81 reason: if options.confirm {
82 "prune-confirmed".to_string()
83 } else {
84 "prune-dry-run".to_string()
85 },
86 deleted: options.confirm,
87 skipped: false,
88 skip_reason: None,
89 });
90 }
91 }
92
93 Ok(actions)
94}
95
96fn keep_last_keys(manifests: &[CacheManifest], keep_last: Option<usize>) -> HashMap<String, ()> {
97 let Some(limit) = keep_last else {
98 return HashMap::new();
99 };
100 let mut by_service: HashMap<&str, Vec<&CacheManifest>> = HashMap::new();
101 for manifest in manifests {
102 by_service
103 .entry(&manifest.service_name)
104 .or_default()
105 .push(manifest);
106 }
107 let mut out = HashMap::new();
108 for manifests in by_service.values_mut() {
109 manifests.sort_by_key(|m| std::cmp::Reverse(m.last_active_unix_ms));
110 for manifest in manifests.iter().take(limit) {
111 out.insert(manifest_key(manifest), ());
112 }
113 }
114 out
115}
116
117fn manifest_key(manifest: &CacheManifest) -> String {
118 format!("{}@{}", manifest.service_name, manifest.service_version)
119}