OXYGENHUB

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

150 of 162 lines run, 7 of 14 functions · run #383 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, RetiredReport};
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
    /// Packs and loose objects earlier repacks retired, deleted now that
24
    /// [`crate::store::RETIRED_FOR`] has passed (oxygenhub#249).
25
    pub retired: RetiredReport,
26
}
27
28
impl Repo {
29
    /// Delete what earlier repacks retired more than
30
    /// [`crate::store::RETIRED_FOR`] ago, prune unreachable objects older
31
    /// than the grace period (`now` ignores it, but not the retired
32
    /// files' hour), then repack everything into one pack (spec §13.4).
33
    /// The packs and loose objects this repack replaces are retired in
34
    /// turn, for the next collection. A store that is not this disk's
35
    /// directory then folds its signatures and index entries (§13.7).
3622×
    pub fn gc(&self, now: bool) -> Result<GcReport> {
37121×
        self.gc_with(now, &mut |_| {})
3822×
    }
39
40
    /// [`Repo::gc`], reporting each phase and the packing's progress
41
    /// (oxygen#66), for a spinner or a log.
4223×
    pub fn gc_with(&self, now: bool, progress: crate::store::Progress) -> Result<GcReport> {
4323×
        self.gc_keeping(now, &[], progress)
4423×
    }
45
46
    /// [`Repo::gc_with`], keeping `extra` blobs and their chunks as well:
47
    /// objects a host stored for itself and names from outside the
48
    /// repository's history, such as a forge's job artifacts
49
    /// (oxygenhub#126).
5024×
    pub fn gc_keeping(
5124×
        &self,
5224×
        now: bool,
5324×
        extra: &[ObjectRef],
5424×
        progress: crate::store::Progress,
5524×
    ) -> Result<GcReport> {
56
        // Retired files first: the readers that used them have had their
57
        // hour, and what replaced them is in the packs read from here on.
5824×
        let retired = self
5924×
            .store
6024×
            .expire_retired(crate::store::RETIRED_FOR, progress)?;
6124×
        progress("finding what is reachable");
6224×
        let mut keep = self.reachable()?;
6324×
        for r in extra {
641×
            if !self.store.has(r) || !keep.insert(*r) {
650×
                continue;
661×
            }
671×
            if let Ok(body) = self.store.get_typed(r, ObjectType::Blob) {
681×
                if let Ok(blob) = Blob::decode(&body) {
691×
                    keep.extend(blob.chunks);
701×
                }
710×
            }
72
        }
7324×
        let grace = if now {
7422×
            std::time::Duration::ZERO
75
        } else {
762×
            std::time::Duration::from_secs(self.config.gc_grace_days * 86_400)
77
        };
7824×
        let pruned = self.store.prune_with(&keep, grace, progress)?;
79
        // Then everything left in one pack, compressed harder than a
80
        // commit's (spec §13.4), as `git gc` repacks.
8124×
        let (packed, merged_packs) = self
8224×
            .store
8324×
            .repack_all_with(crate::store::REPACK_ZSTD_LEVEL, progress)?;
84
        // Last, on a store that is not this disk's directory, every
85
        // signature and index entry into one list (spec §13.7, #116).
8624×
        self.store.fold_entries(progress)?;
8724×
        Ok(GcReport {
8824×
            packed,
8924×
            pruned,
9024×
            reachable: keep.len(),
9124×
            merged_packs,
9224×
            retired,
9324×
        })
9424×
    }
95
96
    /// Every object that must survive garbage collection.
9724×
    pub fn reachable(&self) -> Result<HashSet<ObjectRef>> {
9824×
        let mut keep: HashSet<ObjectRef> = HashSet::new();
9924×
        let mut states: Vec<ObjectRef> = Vec::new();
10024×
        let mut changes: Vec<ChangeHash> = Vec::new();
10124×
        let mut attests: Vec<ObjectRef> = Vec::new();
10224×
        let mut policies: Vec<ObjectRef> = Vec::new();
103
104
        // Operations: always kept; views within retention are roots. Past
105
        // working copies only within the shorter `core.snapshot-retention`
106
        // (oxygen#98): a file only ever snapshotted, such as a build output
107
        // a status took in, goes once they expire.
10824×
        let now_ms = Hlc::now().wall_ms;
10924×
        let cutoff = now_ms.saturating_sub(self.config.op_retention_days * 86_400_000);
11024×
        let wc_cutoff = now_ms
11124×
            .saturating_sub(self.config.snapshot_retention_days * 86_400_000)
11224×
            .max(cutoff);
113154×
        for (i, (r, op)) in self.op_log()?.into_iter().enumerate() {
114154×
            keep.insert(r);
115154×
            if i == 0 || op.hlc.wall_ms >= cutoff {
116116×
                states.extend(op.view.bookmarks.values().copied());
117116×
                states.extend(op.view.remotes.values().flat_map(|b| b.values().copied()));
118116×
                policies.extend(op.view.policy);
11938×
            }
120154×
            if i == 0 || (self.config.snapshot_retention_days > 0 && op.hlc.wall_ms >= wc_cutoff) {
121113×
                changes.extend(op.view.workspaces.values().copied());
122113×
            }
123
        }
124
        // Checkpoints: the object, its payload blob, and the blob's chunks.
12524×
        for r in self.checkpoint_refs()? {
1262×
            if let Ok(bytes) = self.store.get_typed(&r, ObjectType::Checkpoint) {
1272×
                keep.insert(r);
1282×
                let cp = crate::objects::Checkpoint::decode(&bytes)?;
1292×
                keep.insert(cp.payload);
1302×
                if let Ok(b) = self.store.get_typed(&cp.payload, ObjectType::Blob) {
1312×
                    keep.extend(Blob::decode(&b)?.chunks);
1320×
                }
133
                // A version 2 payload's graph blobs and their chunks.
1342×
                for g in self.checkpoint_graph_blobs(&cp).unwrap_or_default() {
1351×
                    keep.insert(g);
1361×
                    if let Ok(b) = self.store.get_typed(&g, ObjectType::Blob) {
1371×
                        keep.extend(Blob::decode(&b)?.chunks);
1380×
                    }
139
                }
1400×
            }
141
        }
142
        // Tags: their states and attestations.
14324×
        let view_now = self.view()?;
14424×
        states.extend(view_now.tags.values().copied());
14524×
        for name in view_now.tags.keys() {
1464×
            if let Some((r, _)) = self.tag_attest(name)? {
147
                // With its issuer's acceptance (spec §14.7).
1484×
                if let Some(acc) = self.acceptance_of(&r)? {
1491×
                    attests.push(acc.attest.hash());
1503×
                }
1514×
                attests.push(r);
1520×
            }
153
        }
154
        // Reference attestations.
15524×
        for name in self.view()?.bookmarks.keys() {
15624×
            if let Some((r, _)) = self.latest_ref_attest(name)? {
157
                // With a host's acceptance of it (spec §14.7).
1582×
                if let Some(acc) = self.acceptance_of(&r)? {
1591×
                    attests.push(acc.attest.hash());
1601×
                }
1612×
                attests.push(r);
16222×
            }
163
        }
164
        // A historical view may name objects pruned under an earlier, shorter
165
        // retention; there is nothing to keep for those.
166136×
        for st in states {
167136×
            if keep.insert(st) {
16878×
                match self.store.get_state(&st) {
16975×
                    Ok(s) => changes.extend(s.heads),
1703×
                    Err(StoreError::NotFound(_)) => continue,
1710×
                    Err(e) => return Err(e.into()),
172
                }
17358×
            }
174
        }
17524×
        for p in policies {
17614×
            if keep.insert(p) {
1775×
                let bytes = match self.store.get_typed(&p, ObjectType::Policy) {
1785×
                    Ok(b) => b,
1790×
                    Err(StoreError::NotFound(_)) => continue,
1800×
                    Err(e) => return Err(e.into()),
181
                };
1825×
                let policy = crate::policy::Policy::decode(&bytes)?;
1837×
                for id in &policy.identities {
1847×
                    keep.insert(id.hash());
1857×
                }
1865×
                for sig in self.store.signatures(&p)? {
1875×
                    keep.insert(sig.key);
1885×
                }
1899×
            }
190
        }
191
        // Changes: their closure, identities, blobs, chunks, seals.
19224×
        let mut stack = changes;
193394×
        while let Some(h) = stack.pop() {
194370×
            if !keep.insert(h) {
195208×
                continue;
196162×
            }
197
            // Beyond the shallow boundary, or pruned under an earlier
198
            // retention, there is nothing to keep.
199162×
            let c = match self.store.get_change_parts(&h) {
200152×
                Ok((c, texts)) => {
201152×
                    keep.extend(texts);
202152×
                    c
203
                }
20410×
                Err(StoreError::NotFound(_)) => continue,
2050×
                Err(e) => return Err(e.into()),
206
            };
207152×
            stack.extend(c.deps.iter().copied());
208152×
            if !c.author.is_zero() {
20933×
                keep.insert(c.author);
210119×
            }
211152×
            for sig in self.store.signatures(&h)? {
21213×
                self.keep_signer(&mut keep, sig.key)?;
213
            }
214152×
            for e in &c.edits {
215134×
                if let Edit::Replace { old, new, .. } = e {
21618×
                    for b in [old, new].into_iter().flatten() {
21718×
                        if keep.insert(*b) {
21818×
                            if let Ok(bytes) = self.store.get_typed(b, ObjectType::Blob) {
21918×
                                keep.extend(Blob::decode(&bytes)?.chunks);
2200×
                            }
2210×
                        }
222
                    }
223116×
                }
224
            }
225152×
            attests.extend(self.store.seals_for(&h)?);
226
        }
22724×
        self.reachable_receipts(&mut keep)?;
22824×
        self.reachable_approvals(&mut keep)?;
22924×
        for a in attests {
23013×
            if keep.insert(a) {
23112×
                for sig in self.store.signatures(&a)? {
23212×
                    self.keep_signer(&mut keep, sig.key)?;
233
                }
2341×
            }
235
        }
23624×
        Ok(keep)
23724×
    }
238
239
    /// Keep a signer's identity and, for a short-lived key, its
240
    /// certificate and the certificate's issuer (spec §14.7).
24127×
    pub(crate) fn keep_signer(&self, keep: &mut HashSet<ObjectRef>, key: ObjectRef) -> Result<()> {
24227×
        keep.insert(key);
24327×
        if let Some(c) = self.store.certificate_of(&key)? {
2445×
            keep.insert(c);
2455×
            if let Ok(cert) = self.store.get_certificate(&c) {
2465×
                keep.insert(cert.cert.issuer);
2475×
            }
24822×
        }
24927×
        Ok(())
25027×
    }
251
}