prolly/prolly/gc.rs
1//! Garbage-collection planning for immutable prolly trees.
2//!
3//! The core [`Store`](crate::Store) trait does not require key listing, so the
4//! generic GC API works from explicit root and candidate sets:
5//!
6//! - mark live nodes from the trees an application wants to retain;
7//! - plan which caller-supplied candidate CIDs are unreachable;
8//! - optionally sweep those unreachable candidates.
9
10use super::blob::BlobRef;
11use super::cid::Cid;
12
13/// Live node set discovered from one or more retained tree roots.
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
15pub struct GcReachability {
16 /// Reachable content-addressed node CIDs, sorted by CID bytes.
17 pub live_cids: Vec<Cid>,
18 /// Number of reachable nodes.
19 pub live_nodes: usize,
20 /// Serialized byte weight of reachable nodes as encoded by the current
21 /// node serializer.
22 pub live_bytes: usize,
23 /// Number of reachable leaf nodes.
24 pub leaf_nodes: usize,
25 /// Number of reachable internal nodes.
26 pub internal_nodes: usize,
27}
28
29impl GcReachability {
30 /// Return reachable node CIDs in stable byte order.
31 pub fn cids(&self) -> &[Cid] {
32 &self.live_cids
33 }
34
35 /// Whether `cid` is reachable from the retained roots.
36 pub fn contains(&self, cid: &Cid) -> bool {
37 self.live_cids.iter().any(|probe| probe == cid)
38 }
39
40 /// Consume this report and return the reachable node CIDs.
41 pub fn into_cids(self) -> Vec<Cid> {
42 self.live_cids
43 }
44}
45
46/// Dry-run garbage-collection plan for an explicit candidate set.
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct GcPlan {
49 /// Reachability report for the retained roots.
50 pub reachability: GcReachability,
51 /// Number of unique candidate CIDs inspected.
52 pub candidate_nodes: usize,
53 /// Unreachable candidate CIDs present in the store, sorted by CID bytes.
54 pub reclaimable_cids: Vec<Cid>,
55 /// Number of reclaimable candidate nodes.
56 pub reclaimable_nodes: usize,
57 /// Serialized bytes reclaimable from present unreachable candidates.
58 pub reclaimable_bytes: usize,
59 /// Candidate CIDs that were neither reachable nor present in the store.
60 pub missing_candidates: usize,
61}
62
63impl GcPlan {
64 /// Return reclaimable candidate CIDs in stable byte order.
65 pub fn reclaimable_cids(&self) -> &[Cid] {
66 &self.reclaimable_cids
67 }
68
69 /// Whether this plan would delete no nodes.
70 pub fn is_empty(&self) -> bool {
71 self.reclaimable_cids.is_empty()
72 }
73
74 /// Number of candidate nodes retained because they are still reachable.
75 pub fn retained_candidate_nodes(&self) -> usize {
76 self.candidate_nodes
77 .saturating_sub(self.reclaimable_nodes)
78 .saturating_sub(self.missing_candidates)
79 }
80}
81
82/// Result of sweeping a garbage-collection plan.
83#[derive(Clone, Debug, Default, PartialEq, Eq)]
84pub struct GcSweep {
85 /// Plan used to decide which candidates were unreachable.
86 pub plan: GcPlan,
87 /// Number of nodes deleted from the backing store.
88 pub deleted_nodes: usize,
89 /// Serialized bytes deleted from the backing store.
90 pub deleted_bytes: usize,
91}
92
93/// Live blob set discovered from one or more retained tree roots.
94#[derive(Clone, Debug, Default, PartialEq, Eq)]
95pub struct BlobGcReachability {
96 /// Reachable content-addressed blob references, sorted by CID bytes.
97 pub live_blobs: Vec<BlobRef>,
98 /// Number of unique reachable blobs.
99 pub live_blob_count: usize,
100 /// Total byte weight of unique reachable blobs.
101 pub live_blob_bytes: u64,
102 /// Number of reachable tree nodes scanned while marking blob references.
103 pub scanned_nodes: usize,
104 /// Number of reachable leaf values inspected while marking blob references.
105 pub scanned_values: usize,
106}
107
108impl BlobGcReachability {
109 /// Return reachable blob references in stable CID order.
110 pub fn blobs(&self) -> &[BlobRef] {
111 &self.live_blobs
112 }
113
114 /// Whether `reference` is reachable from the retained roots.
115 pub fn contains(&self, reference: &BlobRef) -> bool {
116 self.live_blobs
117 .iter()
118 .any(|probe| probe.cid == reference.cid)
119 }
120
121 /// Consume this report and return the reachable blob references.
122 pub fn into_blobs(self) -> Vec<BlobRef> {
123 self.live_blobs
124 }
125}
126
127/// Dry-run garbage-collection plan for offloaded blobs.
128#[derive(Clone, Debug, Default, PartialEq, Eq)]
129pub struct BlobGcPlan {
130 /// Reachability report for the retained roots.
131 pub reachability: BlobGcReachability,
132 /// Number of unique candidate blob CIDs inspected.
133 pub candidate_blobs: usize,
134 /// Unreachable candidate blobs present in the blob store, sorted by CID bytes.
135 pub reclaimable_blobs: Vec<BlobRef>,
136 /// Number of reclaimable candidate blobs.
137 pub reclaimable_blob_count: usize,
138 /// Bytes reclaimable from present unreachable candidates.
139 pub reclaimable_blob_bytes: u64,
140 /// Candidate blob CIDs that were neither reachable nor present in the blob
141 /// store.
142 pub missing_candidates: usize,
143}
144
145impl BlobGcPlan {
146 /// Return reclaimable blob references in stable CID order.
147 pub fn reclaimable_blobs(&self) -> &[BlobRef] {
148 &self.reclaimable_blobs
149 }
150
151 /// Whether this plan would delete no blobs.
152 pub fn is_empty(&self) -> bool {
153 self.reclaimable_blobs.is_empty()
154 }
155
156 /// Number of candidate blobs retained because they are still reachable.
157 pub fn retained_candidate_blobs(&self) -> usize {
158 self.candidate_blobs
159 .saturating_sub(self.reclaimable_blob_count)
160 .saturating_sub(self.missing_candidates)
161 }
162}
163
164/// Result of sweeping a blob garbage-collection plan.
165#[derive(Clone, Debug, Default, PartialEq, Eq)]
166pub struct BlobGcSweep {
167 /// Plan used to decide which candidate blobs were unreachable.
168 pub plan: BlobGcPlan,
169 /// Number of blobs deleted from the backing blob store.
170 pub deleted_blobs: usize,
171 /// Blob bytes deleted from the backing blob store.
172 pub deleted_blob_bytes: u64,
173}
174
175pub(crate) fn sort_cids(cids: &mut [Cid]) {
176 cids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
177}
178
179pub(crate) fn sort_blob_refs(blobs: &mut [BlobRef]) {
180 blobs.sort_by(|left, right| left.cid.as_bytes().cmp(right.cid.as_bytes()));
181}