use super::{BoundedCache, HashSet, NodeState, RecordIdentifier, Result, SegmentProvider};
pub(crate) const RECOVERY_VISITED_BUDGET_BYTES: usize = 128 * 1024 * 1024;
pub(crate) fn is_fully_consistent(
provider: &dyn SegmentProvider,
record: RecordIdentifier,
corrupt_memory: &mut Vec<String>,
) -> bool {
let super_root = NodeState::new(provider, record);
let mut visited = BoundedCache::new(RECOVERY_VISITED_BUDGET_BYTES);
let Ok(Some(content_root)) = super_root.child_node("root") else {
return false;
};
if !every_corrupt_path_verifies(provider, &content_root, corrupt_memory) {
return false;
}
if let Err(corrupt_path) = verify_tree(provider, content_root.record_identifier(), &mut visited)
{
remember_corrupt(corrupt_memory, corrupt_path);
return false;
}
let Ok(Some(checkpoints)) = super_root.child_node("checkpoints") else {
return false;
};
let Ok(checkpoint_entries) = checkpoints.child_node_entries() else {
return false;
};
for (_, checkpoint) in checkpoint_entries {
let Ok(Some(snapshot_root)) = checkpoint.child_node("root") else {
return false;
};
if !every_corrupt_path_verifies(provider, &snapshot_root, corrupt_memory) {
return false;
}
if let Err(corrupt_path) =
verify_tree(provider, snapshot_root.record_identifier(), &mut visited)
{
remember_corrupt(corrupt_memory, corrupt_path);
return false;
}
}
true
}
pub(crate) fn every_corrupt_path_verifies(
provider: &dyn SegmentProvider,
tree_root: &NodeState<'_>,
corrupt_memory: &[String],
) -> bool {
for corrupt_path in corrupt_memory {
let Ok(Some(corrupt_node)) = resolve_descendant(tree_root, corrupt_path) else {
return false;
};
if check_node_shallow(provider, corrupt_node.record_identifier()).is_err() {
return false;
}
}
true
}
pub(crate) fn resolve_descendant<'provider>(
node: &NodeState<'provider>,
relative_path: &str,
) -> Result<Option<NodeState<'provider>>> {
let mut current = *node;
for name in relative_path
.split('/')
.filter(|segment| !segment.is_empty())
{
match current.child_node(name)? {
Some(child) => current = child,
None => return Ok(None),
}
}
Ok(Some(current))
}
pub(crate) fn remember_corrupt(corrupt_memory: &mut Vec<String>, corrupt_path: String) {
if !corrupt_memory.contains(&corrupt_path) {
corrupt_memory.push(corrupt_path);
}
}
pub(crate) fn check_node_shallow(
provider: &dyn SegmentProvider,
record: RecordIdentifier,
) -> Result<()> {
let node = NodeState::new(provider, record);
for property in node.properties()? {
match &property.values {
crate::content::node::PropertyValues::Single(value) => {
verify_inline_binary(provider, value)?;
}
crate::content::node::PropertyValues::Multiple(values) => {
for value in values {
verify_inline_binary(provider, value)?;
}
}
}
}
Ok(())
}
pub(crate) fn verify_tree(
provider: &dyn SegmentProvider,
record: RecordIdentifier,
visited: &mut BoundedCache<RecordIdentifier, ()>,
) -> std::result::Result<(), String> {
struct Frame {
record: RecordIdentifier,
pending_children: Vec<(String, RecordIdentifier)>,
parent_path_length: usize,
}
fn open(
provider: &dyn SegmentProvider,
record: RecordIdentifier,
visited: &BoundedCache<RecordIdentifier, ()>,
ancestors: &mut HashSet<RecordIdentifier>,
path: &str,
) -> std::result::Result<Option<Vec<(String, RecordIdentifier)>>, String> {
if ancestors.contains(&record) {
return Err(path.to_owned());
}
if visited.get(&record).is_some() {
return Ok(None);
}
ancestors.insert(record);
check_node_shallow(provider, record).map_err(|_| path.to_owned())?;
let node = NodeState::new(provider, record);
let children = node.child_node_entries().map_err(|_| path.to_owned())?;
let mut children: Vec<(String, RecordIdentifier)> = children
.into_iter()
.map(|(name, child)| (name, child.record_identifier()))
.collect();
children.reverse();
Ok(Some(children))
}
let mut ancestors = HashSet::new();
let mut path = String::new();
let Some(children) = open(provider, record, visited, &mut ancestors, &path)? else {
return Ok(());
};
let mut stack = vec![Frame {
record,
pending_children: children,
parent_path_length: 0,
}];
loop {
let next = stack
.last_mut()
.expect("the loop returns before the stack empties")
.pending_children
.pop();
if let Some((name, child)) = next {
let parent_path_length = path.len();
path.push('/');
path.push_str(&name);
match open(provider, child, visited, &mut ancestors, &path)? {
Some(children) => stack.push(Frame {
record: child,
pending_children: children,
parent_path_length,
}),
None => path.truncate(parent_path_length),
}
continue;
}
let finished = stack.pop().expect("a frame was just inspected");
ancestors.remove(&finished.record);
visited.insert(finished.record, ());
if stack.is_empty() {
return Ok(());
}
path.truncate(finished.parent_path_length);
}
}
pub(crate) fn verify_inline_binary(
provider: &dyn SegmentProvider,
value: &crate::content::property::PropertyValue,
) -> Result<()> {
use crate::content::property::PropertyValue;
use crate::content::value::BinaryValue;
if let PropertyValue::Binary(BinaryValue::Inline {
record_identifier, ..
}) = value
{
crate::content::value::verify_binary_content(provider, *record_identifier)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::store::Repository;
use crate::writer::backup::recover::recover_journal;
use crate::writer::backup::test_support::{TestDirectory, write_revision_with_children};
#[test]
fn recovery_picks_the_newest_revision_even_when_it_has_fewer_nodes() {
let directory = TestDirectory::new("recover-newest");
write_revision_with_children(&directory.path, 8); std::thread::sleep(std::time::Duration::from_millis(5));
write_revision_with_children(&directory.path, 2);
let newest_head = {
let repository = Repository::open(&directory.path).expect("reader");
repository.head_record_identifier()
};
std::fs::remove_file(directory.path.join("journal.log")).expect("remove journal");
let outcome = recover_journal(&directory.path).expect("recover");
assert_eq!(
outcome.recovered_head, newest_head,
"recovery returns the newest revision, not the larger older one"
);
let repository = Repository::open(&directory.path).expect("reader");
let content = repository
.node_at_path("/content")
.expect("resolve")
.expect("present");
assert_eq!(content.child_node_count().expect("count"), 2);
}
}