OXYGENHUB

Coverage / crates/oxy-core/src/repo/gc.rs 91.6%

131 of 143 lines run, 6 of 12 functions · run #302 on main · run missed

1
//! Garbage collection (spec §21): delete objects
2
//! nothing reachable refers to.
3
//!
4
//! Roots: the current view, the views of every operation newer than
5
//! `core.op-retention` (their working copies only within the shorter
6
//! `core.snapshot-retention`), reference attestations, and the policy; receipts
7
//! live as long as the changes of their state. Operation
8
//! objects are always kept so the log stays walkable, but an operation older
9
//! than the retention window no longer keeps its states alive, so undoing
10
//! that far back may fail once its objects are gone.
11
12
use super::*;
13
use crate::store::{PackReport, PruneReport};
14
use std::collections::HashSet;
15
16
#[derive(Debug, Clone, Default, PartialEq, Eq)]
17
pub struct GcReport {
18
    pub packed: PackReport,
19
    pub pruned: PruneReport,
20
    pub reachable: usize,
21
    /// Packs folded into the one a full repack writes.
22
    pub merged_packs: usize,
23
}
24
25
impl Repo {
26
    /// Prune unreachable objects older than the grace period (`now`
27
    /// ignores it), then repack everything into one pack (spec §13.4).
2815×
    pub fn gc(&self, now: bool) -> Result<GcReport> {
2983×
        self.gc_with(now, &mut |_| {})
3015×
    }
31
32
    /// [`Repo::gc`], reporting each phase and the packing's progress
33
    /// (oxygen#66), for a spinner or a log.
3416×
    pub fn gc_with(&self, now: bool, progress: crate::store::Progress) -> Result<GcReport> {
3516×
        self.gc_keeping(now, &[], progress)
3616×
    }
37
38
    /// [`Repo::gc_with`], keeping `extra` blobs and their chunks as well:
39
    /// objects a host stored for itself and names from outside the
40
    /// repository's history, such as a forge's job artifacts
41
    /// (oxygenhub#126).
4217×
    pub fn gc_keeping(
4317×
        &self,
4417×
        now: bool,
4517×
        extra: &[ObjectRef],
4617×
        progress: crate::store::Progress,
4717×
    ) -> Result<GcReport> {
4817×
        progress("finding what is reachable");
4917×
        let mut keep = self.reachable()?;
5017×
        for r in extra {
511×
            if !self.store.has(r) || !keep.insert(*r) {
520×
                continue;
531×
            }
541×
            if let Ok(body) = self.store.get_typed(r, ObjectType::Blob) {
551×
                if let Ok(blob) = Blob::decode(&body) {
561×
                    keep.extend(blob.chunks);
571×
                }
580×
            }
59
        }
6017×
        let grace = if now {
6115×
            std::time::Duration::ZERO
62
        } else {
632×
            std::time::Duration::from_secs(self.config.gc_grace_days * 86_400)
64
        };
6517×
        let pruned = self.store.prune_with(&keep, grace, progress)?;
66
        // Then everything left in one pack, compressed harder than a
67
        // commit's (spec §13.4), as `git gc` repacks.
6817×
        let (packed, merged_packs) = self
6917×
            .store
7017×
            .repack_all_with(crate::store::REPACK_ZSTD_LEVEL, progress)?;
7117×
        Ok(GcReport {
7217×
            packed,
7317×
            pruned,
7417×
            reachable: keep.len(),
7517×
            merged_packs,
7617×
        })
7717×
    }
78
79
    /// Every object that must survive garbage collection.
8017×
    pub fn reachable(&self) -> Result<HashSet<ObjectRef>> {
8117×
        let mut keep: HashSet<ObjectRef> = HashSet::new();
8217×
        let mut states: Vec<ObjectRef> = Vec::new();
8317×
        let mut changes: Vec<ChangeHash> = Vec::new();
8417×
        let mut attests: Vec<ObjectRef> = Vec::new();
8517×
        let mut policies: Vec<ObjectRef> = Vec::new();
86
87
        // Operations: always kept; views within retention are roots. Past
88
        // working copies only within the shorter `core.snapshot-retention`
89
        // (oxygen#98): a file only ever snapshotted, such as a build output
90
        // a status took in, goes once they expire.
9117×
        let now_ms = Hlc::now().wall_ms;
9217×
        let cutoff = now_ms.saturating_sub(self.config.op_retention_days * 86_400_000);
9317×
        let wc_cutoff = now_ms
9417×
            .saturating_sub(self.config.snapshot_retention_days * 86_400_000)
9517×
            .max(cutoff);
96118×
        for (i, (r, op)) in self.op_log()?.into_iter().enumerate() {
97118×
            keep.insert(r);
98118×
            if i == 0 || op.hlc.wall_ms >= cutoff {
9980×
                states.extend(op.view.bookmarks.values().copied());
10080×
                states.extend(op.view.remotes.values().flat_map(|b| b.values().copied()));
10180×
                policies.extend(op.view.policy);
10238×
            }
103118×
            if i == 0 || (self.config.snapshot_retention_days > 0 && op.hlc.wall_ms >= wc_cutoff) {
10477×
                changes.extend(op.view.workspaces.values().copied());
10577×
            }
106
        }
107
        // Checkpoints: the object, its payload blob, and the blob's chunks.
10817×
        for r in self.checkpoint_refs()? {
1092×
            if let Ok(bytes) = self.store.get_typed(&r, ObjectType::Checkpoint) {
1102×
                keep.insert(r);
1112×
                let cp = crate::objects::Checkpoint::decode(&bytes)?;
1122×
                keep.insert(cp.payload);
1132×
                if let Ok(b) = self.store.get_typed(&cp.payload, ObjectType::Blob) {
1142×
                    keep.extend(Blob::decode(&b)?.chunks);
1150×
                }
116
                // A version 2 payload's graph blobs and their chunks.
1172×
                for g in self.checkpoint_graph_blobs(&cp).unwrap_or_default() {
1181×
                    keep.insert(g);
1191×
                    if let Ok(b) = self.store.get_typed(&g, ObjectType::Blob) {
1201×
                        keep.extend(Blob::decode(&b)?.chunks);
1210×
                    }
122
                }
1230×
            }
124
        }
125
        // Tags: their states and attestations.
12617×
        let view_now = self.view()?;
12717×
        states.extend(view_now.tags.values().copied());
12817×
        for name in view_now.tags.keys() {
1293×
            if let Some((r, _)) = self.tag_attest(name)? {
1303×
                attests.push(r);
1313×
            }
132
        }
133
        // Reference attestations.
13417×
        for name in self.view()?.bookmarks.keys() {
13517×
            if let Some((r, _)) = self.latest_ref_attest(name)? {
1360×
                attests.push(r);
13717×
            }
138
        }
139
        // A historical view may name objects pruned under an earlier, shorter
140
        // retention; there is nothing to keep for those.
14199×
        for st in states {
14299×
            if keep.insert(st) {
14349×
                match self.store.get_state(&st) {
14446×
                    Ok(s) => changes.extend(s.heads),
1453×
                    Err(StoreError::NotFound(_)) => continue,
1460×
                    Err(e) => return Err(e.into()),
147
                }
14850×
            }
149
        }
15017×
        for p in policies {
1514×
            if keep.insert(p) {
1521×
                let bytes = match self.store.get_typed(&p, ObjectType::Policy) {
1531×
                    Ok(b) => b,
1540×
                    Err(StoreError::NotFound(_)) => continue,
1550×
                    Err(e) => return Err(e.into()),
156
                };
1571×
                let policy = crate::policy::Policy::decode(&bytes)?;
1581×
                for id in &policy.identities {
1591×
                    keep.insert(id.hash());
1601×
                }
1611×
                for sig in self.store.signatures(&p)? {
1621×
                    keep.insert(sig.key);
1631×
                }
1643×
            }
165
        }
166
        // Changes: their closure, identities, blobs, chunks, seals.
16717×
        let mut stack = changes;
168245×
        while let Some(h) = stack.pop() {
169228×
            if !keep.insert(h) {
170114×
                continue;
171114×
            }
172
            // Beyond the shallow boundary, or pruned under an earlier
173
            // retention, there is nothing to keep.
174114×
            let c = match self.store.get_change_parts(&h) {
175104×
                Ok((c, texts)) => {
176104×
                    keep.extend(texts);
177104×
                    c
178
                }
17910×
                Err(StoreError::NotFound(_)) => continue,
1800×
                Err(e) => return Err(e.into()),
181
            };
182104×
            stack.extend(c.deps.iter().copied());
183104×
            if !c.author.is_zero() {
18430×
                keep.insert(c.author);
18574×
            }
186104×
            for sig in self.store.signatures(&h)? {
18713×
                keep.insert(sig.key);
18813×
            }
189112×
            for e in &c.edits {
190112×
                if let Edit::Replace { old, new, .. } = e {
19118×
                    for b in [old, new].into_iter().flatten() {
19218×
                        if keep.insert(*b) {
19318×
                            if let Ok(bytes) = self.store.get_typed(b, ObjectType::Blob) {
19418×
                                keep.extend(Blob::decode(&bytes)?.chunks);
1950×
                            }
1960×
                        }
197
                    }
19894×
                }
199
            }
200104×
            attests.extend(self.store.seals_for(&h)?);
201
        }
20217×
        self.reachable_receipts(&mut keep)?;
20317×
        self.reachable_approvals(&mut keep)?;
20417×
        for a in attests {
2055×
            if keep.insert(a) {
2064×
                for sig in self.store.signatures(&a)? {
2074×
                    keep.insert(sig.key);
2084×
                }
2091×
            }
210
        }
21117×
        Ok(keep)
21217×
    }
213
}