//! Garbage collection (spec ยง21): pack loose objects, then delete objects //! nothing reachable refers to. //! //! Roots: the current view, the views of every operation newer than //! `core.op-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 one because there were more than `MAX_PACKS`. pub merged_packs: usize, } impl Repo { /// Pack loose objects and prune unreachable ones older than the grace /// period (`now` ignores the grace period). pub fn gc(&self, now: bool) -> Result { let packed = self.store.pack_loose()?; let keep = self.reachable()?; 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(&keep, grace)?; let merged_packs = self.store.merge_packs()?; 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. let cutoff = Hlc::now() .wall_ms .saturating_sub(self.config.op_retention_days * 86_400_000); 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())); changes.extend(op.view.workspaces.values().copied()); policies.extend(op.view.policy); } } // 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) } }