//! Garbage collection (spec §21): delete objects //! nothing reachable refers to. //! //! Roots: the current view, the views of every operation newer than //! `core.op-retention` (their working copies only within the shorter //! `core.snapshot-retention`), reference attestations, and the policy; receipts //! live as long as the changes of their state. Operation //! objects are always kept so the log stays walkable, but an operation older //! than the retention window no longer keeps its states alive, so undoing //! that far back may fail once its objects are gone. use super::*; use crate::store::{PackReport, PruneReport}; use std::collections::HashSet; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct GcReport { pub packed: PackReport, pub pruned: PruneReport, pub reachable: usize, /// Packs folded into the one a full repack writes. pub merged_packs: usize, } impl Repo { /// Prune unreachable objects older than the grace period (`now` /// ignores it), then repack everything into one pack (spec §13.4). pub fn gc(&self, now: bool) -> Result { self.gc_with(now, &mut |_| {}) } /// [`Repo::gc`], reporting each phase and the packing's progress /// (oxygen#66), for a spinner or a log. pub fn gc_with(&self, now: bool, progress: crate::store::Progress) -> Result { self.gc_keeping(now, &[], progress) } /// [`Repo::gc_with`], keeping `extra` blobs and their chunks as well: /// objects a host stored for itself and names from outside the /// repository's history, such as a forge's job artifacts /// (oxygenhub#126). pub fn gc_keeping( &self, now: bool, extra: &[ObjectRef], progress: crate::store::Progress, ) -> Result { progress("finding what is reachable"); let mut keep = self.reachable()?; for r in extra { if !self.store.has(r) || !keep.insert(*r) { continue; } if let Ok(body) = self.store.get_typed(r, ObjectType::Blob) { if let Ok(blob) = Blob::decode(&body) { keep.extend(blob.chunks); } } } let grace = if now { std::time::Duration::ZERO } else { std::time::Duration::from_secs(self.config.gc_grace_days * 86_400) }; let pruned = self.store.prune_with(&keep, grace, progress)?; // Then everything left in one pack, compressed harder than a // commit's (spec §13.4), as `git gc` repacks. let (packed, merged_packs) = self .store .repack_all_with(crate::store::REPACK_ZSTD_LEVEL, progress)?; Ok(GcReport { packed, pruned, reachable: keep.len(), merged_packs, }) } /// Every object that must survive garbage collection. pub fn reachable(&self) -> Result> { let mut keep: HashSet = HashSet::new(); let mut states: Vec = Vec::new(); let mut changes: Vec = Vec::new(); let mut attests: Vec = Vec::new(); let mut policies: Vec = Vec::new(); // Operations: always kept; views within retention are roots. Past // working copies only within the shorter `core.snapshot-retention` // (oxygen#98): a file only ever snapshotted, such as a build output // a status took in, goes once they expire. let now_ms = Hlc::now().wall_ms; let cutoff = now_ms.saturating_sub(self.config.op_retention_days * 86_400_000); let wc_cutoff = now_ms .saturating_sub(self.config.snapshot_retention_days * 86_400_000) .max(cutoff); for (i, (r, op)) in self.op_log()?.into_iter().enumerate() { keep.insert(r); if i == 0 || op.hlc.wall_ms >= cutoff { states.extend(op.view.bookmarks.values().copied()); states.extend(op.view.remotes.values().flat_map(|b| b.values().copied())); policies.extend(op.view.policy); } if i == 0 || (self.config.snapshot_retention_days > 0 && op.hlc.wall_ms >= wc_cutoff) { changes.extend(op.view.workspaces.values().copied()); } } // Checkpoints: the object, its payload blob, and the blob's chunks. for r in self.checkpoint_refs()? { if let Ok(bytes) = self.store.get_typed(&r, ObjectType::Checkpoint) { keep.insert(r); let cp = crate::objects::Checkpoint::decode(&bytes)?; keep.insert(cp.payload); if let Ok(b) = self.store.get_typed(&cp.payload, ObjectType::Blob) { keep.extend(Blob::decode(&b)?.chunks); } // A version 2 payload's graph blobs and their chunks. for g in self.checkpoint_graph_blobs(&cp).unwrap_or_default() { keep.insert(g); if let Ok(b) = self.store.get_typed(&g, ObjectType::Blob) { keep.extend(Blob::decode(&b)?.chunks); } } } } // Tags: their states and attestations. let view_now = self.view()?; states.extend(view_now.tags.values().copied()); for name in view_now.tags.keys() { if let Some((r, _)) = self.tag_attest(name)? { attests.push(r); } } // Reference attestations. for name in self.view()?.bookmarks.keys() { if let Some((r, _)) = self.latest_ref_attest(name)? { attests.push(r); } } // A historical view may name objects pruned under an earlier, shorter // retention; there is nothing to keep for those. for st in states { if keep.insert(st) { match self.store.get_state(&st) { Ok(s) => changes.extend(s.heads), Err(StoreError::NotFound(_)) => continue, Err(e) => return Err(e.into()), } } } for p in policies { if keep.insert(p) { let bytes = match self.store.get_typed(&p, ObjectType::Policy) { Ok(b) => b, Err(StoreError::NotFound(_)) => continue, Err(e) => return Err(e.into()), }; let policy = crate::policy::Policy::decode(&bytes)?; for id in &policy.identities { keep.insert(id.hash()); } for sig in self.store.signatures(&p)? { keep.insert(sig.key); } } } // Changes: their closure, identities, blobs, chunks, seals. let mut stack = changes; while let Some(h) = stack.pop() { if !keep.insert(h) { continue; } // Beyond the shallow boundary, or pruned under an earlier // retention, there is nothing to keep. let c = match self.store.get_change_parts(&h) { Ok((c, texts)) => { keep.extend(texts); c } Err(StoreError::NotFound(_)) => continue, Err(e) => return Err(e.into()), }; stack.extend(c.deps.iter().copied()); if !c.author.is_zero() { keep.insert(c.author); } for sig in self.store.signatures(&h)? { keep.insert(sig.key); } for e in &c.edits { if let Edit::Replace { old, new, .. } = e { for b in [old, new].into_iter().flatten() { if keep.insert(*b) { if let Ok(bytes) = self.store.get_typed(b, ObjectType::Blob) { keep.extend(Blob::decode(&bytes)?.chunks); } } } } } attests.extend(self.store.seals_for(&h)?); } self.reachable_receipts(&mut keep)?; for a in attests { if keep.insert(a) { for sig in self.store.signatures(&a)? { keep.insert(sig.key); } } } Ok(keep) } }