Skip to main content

running_process/cleanup/
prune.rs

1use 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/// Prune selection options.
9#[derive(Debug, Clone)]
10pub struct PruneOptions {
11    /// Only prune manifests inactive for at least this many seconds.
12    pub dormant_after_secs: Option<u64>,
13    /// Keep manifests with a current daemon.
14    pub keep_current: bool,
15    /// Keep the N most recently active manifests per service.
16    pub keep_last: Option<usize>,
17    /// Restrict to one service.
18    pub service: Option<String>,
19    /// Restrict to one version.
20    pub version: Option<String>,
21    /// Delete selected paths. False means dry-run.
22    pub confirm: bool,
23}
24
25/// Select and optionally delete prunable roots.
26pub 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}