Skip to main content

prolly/prolly/content_graph/
gc.rs

1use super::{walk_content_graph, ContentGraphLimits, TypedContentRoot};
2use crate::prolly::cid::Cid;
3use crate::prolly::error::Error;
4use crate::prolly::store::Store;
5use std::collections::HashSet;
6
7/// Global typed-content GC plan over an explicit candidate set.
8#[derive(Clone, Debug, Default, PartialEq, Eq)]
9pub struct ContentGcPlan {
10    pub live_cids: Vec<Cid>,
11    pub live_objects: usize,
12    pub live_bytes: usize,
13    pub candidate_objects: usize,
14    pub reclaimable_cids: Vec<Cid>,
15    pub reclaimable_bytes: usize,
16    pub missing_candidates: usize,
17}
18
19/// Applied typed-content GC result.
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct ContentGcSweep {
22    pub plan: ContentGcPlan,
23    pub deleted_objects: usize,
24    pub deleted_bytes: usize,
25}
26
27pub fn plan_content_gc<S: Store>(
28    store: &S,
29    retained_roots: &[TypedContentRoot],
30    candidates: &[Cid],
31    limits: &ContentGraphLimits,
32) -> Result<ContentGcPlan, Error> {
33    let walk = walk_content_graph(store, retained_roots, limits)?;
34    let mut live_cids: Vec<_> = walk
35        .objects
36        .iter()
37        .map(|object| object.root.cid.clone())
38        .collect();
39    live_cids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
40    let live: HashSet<_> = live_cids.iter().cloned().collect();
41    let mut unique_candidates = candidates.to_vec();
42    unique_candidates.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
43    unique_candidates.dedup();
44    let mut reclaimable_cids = Vec::new();
45    let mut reclaimable_bytes = 0usize;
46    let mut missing_candidates = 0usize;
47    for cid in &unique_candidates {
48        if live.contains(cid) {
49            continue;
50        }
51        match store
52            .get(cid.as_bytes())
53            .map_err(|error| Error::Store(Box::new(error)))?
54        {
55            Some(bytes) => {
56                let actual = Cid::from_bytes(&bytes);
57                if actual != *cid {
58                    return Err(Error::CidMismatch {
59                        expected: cid.clone(),
60                        actual,
61                    });
62                }
63                reclaimable_bytes += bytes.len();
64                reclaimable_cids.push(cid.clone());
65            }
66            None => missing_candidates += 1,
67        }
68    }
69    Ok(ContentGcPlan {
70        live_objects: live_cids.len(),
71        live_cids,
72        live_bytes: walk.total_bytes,
73        candidate_objects: unique_candidates.len(),
74        reclaimable_cids,
75        reclaimable_bytes,
76        missing_candidates,
77    })
78}
79
80pub fn sweep_content_gc<S: Store>(
81    store: &S,
82    retained_roots: &[TypedContentRoot],
83    candidates: &[Cid],
84    limits: &ContentGraphLimits,
85) -> Result<ContentGcSweep, Error> {
86    sweep_content_gc_with_invalidator(store, retained_roots, candidates, limits, |_| {})
87}
88
89/// Sweep unreachable candidates and invalidate process-local consumers after
90/// each successful deletion. Per-object notification keeps caches correct even
91/// when a later store deletion fails.
92pub fn sweep_content_gc_with_invalidator<S, F>(
93    store: &S,
94    retained_roots: &[TypedContentRoot],
95    candidates: &[Cid],
96    limits: &ContentGraphLimits,
97    mut invalidate: F,
98) -> Result<ContentGcSweep, Error>
99where
100    S: Store,
101    F: FnMut(&Cid),
102{
103    let plan = plan_content_gc(store, retained_roots, candidates, limits)?;
104    for cid in &plan.reclaimable_cids {
105        store
106            .delete(cid.as_bytes())
107            .map_err(|error| Error::Store(Box::new(error)))?;
108        invalidate(cid);
109    }
110    Ok(ContentGcSweep {
111        deleted_objects: plan.reclaimable_cids.len(),
112        deleted_bytes: plan.reclaimable_bytes,
113        plan,
114    })
115}