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 = 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 mut assets = super::assets::Assets::for_verification(&source);
46 assets.vault()?;
47 assets.physical()?;
48 assets.metadata(path, None)?;
49 Ok(PagedScrubReport {
50 trees,
51 assets_verified: true,
52 unclassified_pages: source.unclaimed_pages(),
53 })
54}
55
56pub(super) fn verify_recovery_vector_pages(path: &Path) -> Result<()> {
57 let source = Arc::new(Source::open(path)?);
58 super::salvage::verify_supported_source(path, &source)?;
59 let trees = inspect_all_trees(&source)?;
60 ensure(
61 trees.findings.is_empty(),
62 "recovered tree verification failed",
63 )?;
64 let mut assets = super::assets::Assets::for_verification(&source);
65 assets.vault()?;
66 assets.physical()?;
67 assets.metadata(path, None)?;
68 source.verify_unclaimed_vector_pages()
69}
70
71pub fn scrub_paged_trees(path: &Path) -> Result<PagedTreeScrubReport> {
75 let source = Arc::new(Source::open(path)?);
76 inspect_all_trees(&source)
77}
78
79fn inspect_all_trees(source: &Arc<Source>) -> Result<PagedTreeScrubReport> {
80 let catalog = Catalog::read(source)?;
81 ensure(
82 source.header.version < f::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
83 || catalog.resume.is_some(),
84 "prepared disk-resident file has no resume catalog",
85 )?;
86 let mut report = PagedTreeScrubReport::default();
87 for collection in &catalog.roots {
88 let state = catalog
89 .resume
90 .as_ref()
91 .and_then(|r| r.collections.get(&collection.collection));
92 if catalog.resume.is_some() {
93 ensure(state.is_some(), "collection missing from resume catalog")?;
94 }
95 if let Some(state) = state {
96 ensure(
97 state.root_page == collection.root_page,
98 "collection root differs from resume catalog",
99 )?;
100 }
101 inspect_tree(
102 source,
103 &mut report,
104 &collection.collection,
105 "collection",
106 collection.root_page,
107 state.map(|s| s.entities),
108 );
109 }
110 if let Some(resume) = &catalog.resume {
111 ensure(
112 resume.collections.len() == catalog.roots.len(),
113 "resume catalog has unknown collections",
114 )?;
115 for (name, state) in &resume.collections {
116 if let Some(root) = state.key_index_root {
117 inspect_tree(source, &mut report, name, "key index", root, None);
118 }
119 for (index, state) in &state.secondary_indices {
120 inspect_tree(
121 source,
122 &mut report,
123 name,
124 &format!("secondary index {index}"),
125 state.root_page,
126 None,
127 );
128 }
129 }
130 }
131 Ok(report)
132}
133
134fn inspect_tree(
135 source: &Arc<Source>,
136 report: &mut PagedTreeScrubReport,
137 collection: &str,
138 name: &str,
139 root: u32,
140 declared_count: Option<u64>,
141) {
142 let result = (|| -> Result<u64> {
143 if root == 0 {
144 ensure(
145 declared_count.unwrap_or(0) == 0,
146 "empty tree has nonzero declared count",
147 )?;
148 return Ok(0);
149 }
150 let tree = BTree::with_root(Arc::clone(source), root);
151 let (expected, _) = inventory_tree(source, &tree, true)?;
152 let mut cursor = tree.cursor_first()?;
153 let mut previous: Option<Vec<u8>> = None;
154 let mut count = 0u64;
155 while let Some((key, _value)) = cursor.next_with_budget(32 * 1024 * 1024)? {
156 ensure(
157 previous.as_ref().is_none_or(|old| old < &key),
158 "tree keys are not strictly ordered",
159 )?;
160 previous = Some(key);
161 count += 1;
162 ensure(
163 count <= expected,
164 "tree cursor exceeds structural inventory",
165 )?;
166 }
167 ensure(count == expected, "tree cursor omits owned records")?;
168 if let Some(declared) = declared_count {
169 ensure(
170 count == declared,
171 "collection count disagrees with resume catalog",
172 )?;
173 }
174 Ok(count)
175 })();
176 match result {
177 Ok(count) => {
178 report.trees_verified += 1;
179 report.records_verified += count;
180 }
181 Err(error) => report.findings.push(PagedTreeFinding {
182 collection: collection.to_owned(),
183 tree: name.to_owned(),
184 root_page: root,
185 reason: error.to_string(),
186 }),
187 }
188}