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.version < f::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
95 || catalog.resume.is_some(),
96 "prepared disk-resident file has no resume catalog",
97 )?;
98 let mut report = PagedTreeScrubReport::default();
99 for collection in &catalog.roots {
100 let state = catalog
101 .resume
102 .as_ref()
103 .and_then(|r| r.collections.get(&collection.collection));
104 if catalog.resume.is_some() {
105 ensure(state.is_some(), "collection missing from resume catalog")?;
106 }
107 if let Some(state) = state {
108 ensure(
109 state.root_page == collection.root_page,
110 "collection root differs from resume catalog",
111 )?;
112 }
113 inspect_tree(
114 source,
115 &mut report,
116 &collection.collection,
117 "collection",
118 collection.root_page,
119 state.map(|s| s.entities),
120 );
121 }
122 if let Some(resume) = &catalog.resume {
123 ensure(
124 resume.collections.len() == catalog.roots.len(),
125 "resume catalog has unknown collections",
126 )?;
127 for (name, state) in &resume.collections {
128 if let Some(root) = state.key_index_root {
129 inspect_tree(source, &mut report, name, "key index", root, None);
130 }
131 for (index, state) in &state.secondary_indices {
132 inspect_tree(
133 source,
134 &mut report,
135 name,
136 &format!("secondary index {index}"),
137 state.root_page,
138 None,
139 );
140 }
141 }
142 }
143 Ok((report, catalog))
144}
145
146fn inspect_tree(
147 source: &Arc<Source>,
148 report: &mut PagedTreeScrubReport,
149 collection: &str,
150 name: &str,
151 root: u32,
152 declared_count: Option<u64>,
153) {
154 let result = (|| -> Result<u64> {
155 if root == 0 {
156 ensure(
157 declared_count.unwrap_or(0) == 0,
158 "empty tree has nonzero declared count",
159 )?;
160 return Ok(0);
161 }
162 let tree = BTree::with_root(Arc::clone(source), root);
163 let (expected, _) = inventory_tree(source, &tree, true)?;
164 let mut cursor = tree.cursor_first()?;
165 let mut previous: Option<Vec<u8>> = None;
166 let mut count = 0u64;
167 while let Some((key, _value)) = cursor.next_with_budget(32 * 1024 * 1024)? {
168 ensure(
169 previous.as_ref().is_none_or(|old| old < &key),
170 "tree keys are not strictly ordered",
171 )?;
172 previous = Some(key);
173 count += 1;
174 ensure(
175 count <= expected,
176 "tree cursor exceeds structural inventory",
177 )?;
178 }
179 ensure(count == expected, "tree cursor omits owned records")?;
180 if let Some(declared) = declared_count {
181 ensure(
182 count == declared,
183 "collection count disagrees with resume catalog",
184 )?;
185 }
186 Ok(count)
187 })();
188 match result {
189 Ok(count) => {
190 report.trees_verified += 1;
191 report.records_verified += count;
192 }
193 Err(error) => report.findings.push(PagedTreeFinding {
194 collection: collection.to_owned(),
195 tree: name.to_owned(),
196 root_page: root,
197 reason: error.to_string(),
198 }),
199 }
200}