Skip to main content

reddb_server/storage/compact/
scrub.rs

1use super::*;
2
3/// A failed tree is unverified; the number of affected records is unknown.
4#[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/// This is a tree-only diagnostic, not a complete store integrity check.
13#[derive(Debug, Default)]
14pub struct PagedTreeScrubReport {
15    pub trees_verified: u64,
16    pub records_verified: u64,
17    pub findings: Vec<PagedTreeFinding>,
18}
19
20/// A physical scrub of a supported, closed and checkpointed standalone store.
21/// If a tree fails, auxiliary checks are not considered complete.
22#[derive(Debug)]
23pub struct PagedScrubReport {
24    pub trees: PagedTreeScrubReport,
25    pub assets_verified: bool,
26    /// Allocated pages not identified as free or owned by a verified structure.
27    pub unclassified_pages: u64,
28}
29
30/// Verify the supported offline layout without writing to the source. Pending
31/// recovery, vaults without valid credentials and unsupported sidecars fail closed.
32pub 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
56/// Inspect trees in a closed, checkpointed paged store without writing to it.
57/// Vault, physical assets, logical index correctness and replica state are not
58/// verified. The same layout and WAL restrictions as offline compaction apply.
59pub 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}