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