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
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
71/// Inspect trees in a closed, checkpointed paged store without writing to it.
72/// Vault, physical assets, logical index correctness and replica state are not
73/// verified. The same layout and WAL restrictions as offline compaction apply.
74pub 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}