use std::collections::HashSet;
use crate::content::node::NodeState;
use crate::content::provider::SegmentProvider;
use crate::error::{Error, Result};
use crate::journal::read_journal;
use crate::packed_records::PackedRecordSet;
use crate::progress::{DiscardedProgress, ProgressObserver, Step, StrideCounter, WorkUnit};
use crate::segment::record::RecordIdentifier;
use crate::store::{ArchiveSet, open_all_archives_with_progress};
mod path;
mod subtree;
#[cfg(test)]
mod test_support;
pub(crate) use path::*;
pub use subtree::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathVerdict {
pub path: String,
pub latest_good_revision: Option<String>,
pub latest_good_timestamp_milliseconds: Option<i64>,
pub newest_failure: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsistencyReport {
pub checked_revisions: usize,
pub checkpoints: Vec<String>,
pub head_paths: Vec<PathVerdict>,
pub checkpoint_paths: Vec<(String, Vec<PathVerdict>)>,
pub overall_revision: Option<String>,
}
impl ConsistencyReport {
#[must_use]
pub fn has_good_revision(&self) -> bool {
self.head_paths
.iter()
.chain(
self.checkpoint_paths
.iter()
.flat_map(|(_, verdicts)| verdicts.iter()),
)
.any(|verdict| verdict.latest_good_revision.is_some())
}
}
pub(crate) enum PathRoot {
Head,
Checkpoint(String),
}
pub(crate) struct PathToCheck {
pub(crate) root: PathRoot,
pub(crate) path: String,
pub(crate) verdict: PathVerdict,
pub(crate) corrupt_paths: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryCheck {
RecordsOnly,
EveryBlock,
}
pub fn check_consistency(
directory: &std::path::Path,
filter_paths: &[String],
binary_check: BinaryCheck,
revision_limit: usize,
) -> Result<ConsistencyReport> {
check_consistency_with_progress(
directory,
filter_paths,
binary_check,
revision_limit,
&mut DiscardedProgress,
)
}
pub fn check_consistency_with_progress(
directory: &std::path::Path,
filter_paths: &[String],
binary_check: BinaryCheck,
revision_limit: usize,
observer: &mut dyn ProgressObserver,
) -> Result<ConsistencyReport> {
let archives = open_all_archives_with_progress(directory, observer)?;
let provider = ArchiveSet::new(archives);
let journal_entries = read_journal(&directory.join("journal.log"))?;
let base_paths: Vec<String> = if filter_paths.is_empty() {
vec!["/".to_owned()]
} else {
filter_paths.to_vec()
};
let checkpoints = current_head_checkpoint_names(&provider, &journal_entries)?;
let mut paths_to_check = build_paths_to_check(&base_paths, &checkpoints);
let mut checked_revisions = 0usize;
let mut overall_revision = None;
let examinable = examinable_revisions(journal_entries.len(), revision_limit);
for entry in &journal_entries {
checked_revisions += 1;
let position = checked_revisions.min(examinable);
let description = format!("checking revision {position} of {examinable}");
observer.step_began(&Step::new(&description, WorkUnit::Nodes));
let mut nodes = VerifiedNodeCount::new(observer);
let Some(head) = entry.record_identifier() else {
nodes.finish();
observer.step_ended();
continue;
};
if !provider_contains(&provider, head) {
nodes.finish();
observer.step_ended();
continue;
}
let super_root = NodeState::new(&provider, head);
let mut all_pinned = true;
let mut verified_this_revision = PackedRecordSet::new();
for path_to_check in &mut paths_to_check {
if path_to_check.verdict.latest_good_revision.is_some() {
continue;
}
match check_one_path(
&provider,
&super_root,
path_to_check,
binary_check,
&mut verified_this_revision,
&mut nodes,
) {
Ok(()) => {
path_to_check.verdict.latest_good_revision = Some(entry.revision_text.clone());
path_to_check.verdict.latest_good_timestamp_milliseconds =
Some(entry.timestamp_milliseconds);
}
Err(reason) => {
all_pinned = false;
if path_to_check.verdict.newest_failure.is_none() {
path_to_check.verdict.newest_failure = Some(reason);
}
}
}
}
nodes.finish();
observer.step_ended();
if all_pinned {
overall_revision = Some(entry.revision_text.clone());
break;
}
if checked_revisions == revision_limit {
break;
}
}
let mut head_paths = Vec::new();
let mut checkpoint_paths: Vec<(String, Vec<PathVerdict>)> = checkpoints
.iter()
.map(|name| (name.clone(), Vec::new()))
.collect();
for path_to_check in paths_to_check {
match &path_to_check.root {
PathRoot::Head => head_paths.push(path_to_check.verdict),
PathRoot::Checkpoint(name) => {
if let Some((_, verdicts)) = checkpoint_paths
.iter_mut()
.find(|(checkpoint, _)| checkpoint == name)
{
verdicts.push(path_to_check.verdict);
}
}
}
}
Ok(ConsistencyReport {
checked_revisions,
checkpoints,
head_paths,
checkpoint_paths,
overall_revision,
})
}
pub(crate) fn examinable_revisions(journal_length: usize, revision_limit: usize) -> usize {
if revision_limit == 0 {
journal_length
} else {
journal_length.min(revision_limit)
}
}
#[cfg(test)]
pub(crate) mod examinable_revision_tests {
use super::examinable_revisions;
#[test]
fn a_limit_bounds_the_declared_total() {
assert_eq!(examinable_revisions(5_000, 2), 2);
assert_eq!(examinable_revisions(2, 5_000), 2);
assert_eq!(
examinable_revisions(5_000, 0),
5_000,
"a zero limit means unlimited, as it does in Java"
);
assert_eq!(examinable_revisions(5_000, usize::MAX), 5_000);
assert_eq!(examinable_revisions(0, 3), 0);
}
}
pub(crate) fn build_paths_to_check(
base_paths: &[String],
checkpoints: &[String],
) -> Vec<PathToCheck> {
let unchecked_verdict = |path: &String| PathVerdict {
path: path.clone(),
latest_good_revision: None,
latest_good_timestamp_milliseconds: None,
newest_failure: None,
};
let mut paths_to_check: Vec<PathToCheck> = Vec::new();
for path in base_paths {
paths_to_check.push(PathToCheck {
root: PathRoot::Head,
path: path.clone(),
verdict: unchecked_verdict(path),
corrupt_paths: Vec::new(),
});
}
for checkpoint in checkpoints {
for path in base_paths {
paths_to_check.push(PathToCheck {
root: PathRoot::Checkpoint(checkpoint.clone()),
path: path.clone(),
verdict: unchecked_verdict(path),
corrupt_paths: Vec::new(),
});
}
}
paths_to_check
}
pub(crate) fn provider_contains(provider: &ArchiveSet, head: RecordIdentifier) -> bool {
provider.segment(head.segment).is_ok()
}
pub(crate) fn current_head_checkpoint_names(
provider: &ArchiveSet,
journal_entries: &[crate::journal::JournalEntry],
) -> Result<Vec<String>> {
let Some(head) = journal_entries
.iter()
.filter_map(crate::journal::JournalEntry::record_identifier)
.find(|identifier| provider_contains(provider, *identifier))
else {
return Ok(Vec::new());
};
let super_root = NodeState::new(provider, head);
match super_root.child_node("checkpoints")? {
None => Ok(Vec::new()),
Some(checkpoints) => Ok(checkpoints
.child_node_entries()?
.into_iter()
.map(|(name, _)| name)
.collect()),
}
}
#[cfg(test)]
mod tests {
use super::{BinaryCheck, check_consistency};
use crate::tooling::check::test_support::{TestDirectory, write_content_revision};
use crate::writer::store_writer::WritableRepository;
#[test]
fn pins_each_path_to_the_newest_consistent_revision() {
let directory = TestDirectory::new("consistent");
write_content_revision(&directory.path, "first");
write_content_revision(&directory.path, "second");
let report = check_consistency(
&directory.path,
&["/content".to_owned()],
BinaryCheck::EveryBlock,
100,
)
.expect("check");
assert!(report.has_good_revision(), "a consistent revision exists");
assert_eq!(report.head_paths.len(), 1);
let verdict = &report.head_paths[0];
assert_eq!(verdict.path, "/content");
assert!(verdict.latest_good_revision.is_some());
assert!(verdict.newest_failure.is_none());
assert_eq!(
report.overall_revision, verdict.latest_good_revision,
"with one path, overall is that path's revision"
);
assert!(report.checked_revisions >= 1);
}
#[test]
fn a_missing_path_is_reported_without_a_good_revision() {
let directory = TestDirectory::new("missing-path");
write_content_revision(&directory.path, "only");
let report = check_consistency(
&directory.path,
&["/nonexistent".to_owned()],
BinaryCheck::RecordsOnly,
100,
)
.expect("check");
assert!(!report.has_good_revision());
assert!(report.overall_revision.is_none());
let verdict = &report.head_paths[0];
assert!(verdict.latest_good_revision.is_none());
assert_eq!(
verdict.newest_failure.as_deref(),
Some("path does not exist")
);
}
#[test]
fn a_good_path_succeeds_even_when_another_never_verifies() {
let directory = TestDirectory::new("partial-good");
write_content_revision(&directory.path, "content");
let report = check_consistency(
&directory.path,
&["/content".to_owned(), "/nonexistent".to_owned()],
BinaryCheck::RecordsOnly,
100,
)
.expect("check");
assert!(report.has_good_revision());
assert!(
report.overall_revision.is_none(),
"overall requires every path to verify"
);
assert!(report.head_paths[0].latest_good_revision.is_some());
assert!(report.head_paths[1].latest_good_revision.is_none());
}
#[test]
fn the_root_path_checks_the_whole_content_tree_and_checkpoints() {
let directory = TestDirectory::new("root-path");
write_content_revision(&directory.path, "content");
{
let store = WritableRepository::open(&directory.path).expect("open");
crate::writer::commit::create_checkpoint(&store, 10_000_000, &[]).expect("checkpoint");
store.close().expect("close");
}
let report =
check_consistency(&directory.path, &[], BinaryCheck::EveryBlock, 100).expect("check");
assert!(report.has_good_revision());
assert_eq!(report.checkpoints.len(), 1, "the checkpoint is expanded");
assert_eq!(report.head_paths[0].path, "/");
assert!(report.head_paths[0].latest_good_revision.is_some());
let (_, checkpoint_verdicts) = &report.checkpoint_paths[0];
assert!(
checkpoint_verdicts[0].latest_good_revision.is_some(),
"the checkpoint's root snapshot verifies"
);
assert!(
report.overall_revision.is_some(),
"every path verified, so an overall revision exists"
);
}
}