reddb_server/storage/compact/
scrub.rs1use super::*;
2
3#[derive(Debug)]
5pub struct PagedTreeFinding {
6 pub collection: String,
7 pub tree: String,
8 pub root_page: u32,
9 pub reason: String,
10}
11
12#[derive(Debug, Default)]
14pub struct PagedTreeScrubReport {
15 pub trees_verified: u64,
16 pub records_verified: u64,
17 pub findings: Vec<PagedTreeFinding>,
18}
19
20#[derive(Debug)]
23pub struct PagedScrubReport {
24 pub trees: PagedTreeScrubReport,
25 pub assets_verified: bool,
26 pub unclassified_pages: u64,
28}
29
30pub fn scrub_paged(path: &Path) -> Result<PagedScrubReport> {
33 let canonical_path = path.canonicalize()?;
34 let path = canonical_path.as_path();
35 let source = Arc::new(Source::open(path)?);
36 super::salvage::verify_supported_source_with_audit(path, &source)?;
37 let (trees, catalog) = inspect_all_trees(&source)?;
38 if !trees.findings.is_empty() {
39 return Ok(PagedScrubReport {
40 trees,
41 assets_verified: false,
42 unclassified_pages: source.unclaimed_pages(),
43 });
44 }
45 let internal = super::internal_metadata::read(&source, &catalog)?;
46 let mut assets = super::assets::Assets::for_verification(&source);
47 assets.vault()?;
48 assets.physical()?;
49 assets.metadata(
50 path,
51 None,
52 internal.as_ref().map(|record| record.document.as_str()),
53 )?;
54 catalog.claim_retired(&source)?;
55 Ok(PagedScrubReport {
56 trees,
57 assets_verified: true,
58 unclassified_pages: source.unclaimed_pages(),
59 })
60}
61
62pub(super) fn verify_recovery_vector_pages(path: &Path) -> Result<()> {
63 let source = Arc::new(Source::open(path)?);
64 super::salvage::verify_supported_source(path, &source)?;
65 let (trees, catalog) = inspect_all_trees(&source)?;
66 ensure(
67 trees.findings.is_empty(),
68 "recovered tree verification failed",
69 )?;
70 let internal = super::internal_metadata::read(&source, &catalog)?;
71 let mut assets = super::assets::Assets::for_verification(&source);
72 assets.vault()?;
73 assets.physical()?;
74 assets.metadata(
75 path,
76 None,
77 internal.as_ref().map(|record| record.document.as_str()),
78 )?;
79 catalog.claim_retired(&source)?;
80 source.verify_unclaimed_vector_pages()
81}
82
83pub fn scrub_paged_trees(path: &Path) -> Result<PagedTreeScrubReport> {
87 let source = Arc::new(Source::open(path)?);
88 inspect_all_trees(&source).map(|(report, _)| report)
89}
90
91fn inspect_all_trees(source: &Arc<Source>) -> Result<(PagedTreeScrubReport, Catalog)> {
92 let catalog = Catalog::read(source)?;
93 ensure(
94 !source.header.requires_disk_residency() || catalog.resume.is_some(),
95 "prepared disk-resident file has no resume catalog",
96 )?;
97 let mut report = PagedTreeScrubReport::default();
98 for collection in &catalog.roots {
99 let state = catalog
100 .resume
101 .as_ref()
102 .and_then(|r| r.collections.get(&collection.collection));
103 if catalog.resume.is_some() {
104 ensure(state.is_some(), "collection missing from resume catalog")?;
105 }
106 if let Some(state) = state {
107 ensure(
108 state.root_page == collection.root_page,
109 "collection root differs from resume catalog",
110 )?;
111 }
112 inspect_tree(
113 source,
114 &mut report,
115 &collection.collection,
116 "collection",
117 collection.root_page,
118 state.map(|s| s.entities),
119 );
120 }
121 if let Some(resume) = &catalog.resume {
122 ensure(
123 resume.collections.len() == catalog.roots.len(),
124 "resume catalog has unknown collections",
125 )?;
126 for (name, state) in &resume.collections {
127 if let Some(root) = state.key_index_root {
128 inspect_tree(source, &mut report, name, "key index", root, None);
129 }
130 for (index, state) in &state.secondary_indices {
131 inspect_tree(
132 source,
133 &mut report,
134 name,
135 &format!("secondary index {index}"),
136 state.root_page,
137 None,
138 );
139 }
140 }
141 }
142 Ok((report, catalog))
143}
144
145fn inspect_tree(
146 source: &Arc<Source>,
147 report: &mut PagedTreeScrubReport,
148 collection: &str,
149 name: &str,
150 root: u32,
151 declared_count: Option<u64>,
152) {
153 let result = (|| -> Result<u64> {
154 if root == 0 {
155 ensure(
156 declared_count.unwrap_or(0) == 0,
157 "empty tree has nonzero declared count",
158 )?;
159 return Ok(0);
160 }
161 let tree = source.tree(root)?;
162 let (expected, _) = inventory_tree(source, &tree, true)?;
163 let mut cursor = tree.cursor_first()?;
164 let mut previous: Option<Vec<u8>> = None;
165 let mut count = 0u64;
166 while let Some((key, _value)) = cursor.next_with_budget(32 * 1024 * 1024)? {
167 ensure(
168 previous.as_ref().is_none_or(|old| old < &key),
169 "tree keys are not strictly ordered",
170 )?;
171 previous = Some(key);
172 count += 1;
173 ensure(
174 count <= expected,
175 "tree cursor exceeds structural inventory",
176 )?;
177 }
178 ensure(count == expected, "tree cursor omits owned records")?;
179 if let Some(declared) = declared_count {
180 ensure(
181 count == declared,
182 "collection count disagrees with resume catalog",
183 )?;
184 }
185 Ok(count)
186 })();
187 match result {
188 Ok(count) => {
189 report.trees_verified += 1;
190 report.records_verified += count;
191 }
192 Err(error) => report.findings.push(PagedTreeFinding {
193 collection: collection.to_owned(),
194 tree: name.to_owned(),
195 root_page: root,
196 reason: error.to_string(),
197 }),
198 }
199}