#[cfg(test)]
mod tests {
use crate::store::Repository;
use crate::writer::compaction::CompactionKind;
use crate::writer::fault_injection::test_support::{
REINDEX_SCENARIO, TestDirectory, reindex_work_directory, run_crash_child, run_error_child,
write_flagged_index_fixture,
};
use crate::writer::index::{ReindexOptions, WorkDirectory, plan_reindex, reindex};
use crate::writer::maintenance::{CompactionOptions, MaintenanceTask, compact};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::Path;
const BEFORE_SPILL_CLEANUP: &str = "index-reindex.before-spill-cleanup";
const BEFORE_HEAD_PUBLISH: &str = "index-reindex.before-head-publish";
const AFTER_HEAD_PUBLISH_BEFORE_FLUSH: &str = "index-reindex.after-head-publish-before-flush";
const BEFORE_APPLIED_VERIFICATION: &str = "index-reindex.before-applied-verification";
fn store_files(directory: &Path) -> BTreeMap<OsString, Vec<u8>> {
std::fs::read_dir(directory)
.expect("read the store directory")
.map(|entry| {
let entry = entry.expect("directory entry");
(
entry.file_name(),
std::fs::read(entry.path()).expect("read the file"),
)
})
.collect()
}
fn digest_under(store: &Path, path: &str) -> Vec<String> {
let repository = Repository::open(store).expect("open the repository");
let mut rendered = Vec::new();
crate::tooling::digest::digest_repository_excluding(&repository, &[], &[], &mut rendered)
.expect("digest");
String::from_utf8(rendered)
.expect("UTF-8")
.lines()
.filter(|line| line.starts_with(path))
.map(str::to_owned)
.collect()
}
fn assert_the_original_store_survived(
store: &Path,
before: &BTreeMap<OsString, Vec<u8>>,
definition_before: &[String],
) {
let after = store_files(store);
for (name, bytes) in before {
if name == "repo.lock" {
continue;
}
assert_eq!(
after.get(name),
Some(bytes),
"{} must survive a pre-publication failure byte-identical",
Path::new(name).display()
);
}
assert_eq!(
digest_under(store, "/oak:index/title"),
definition_before,
"the definition must be exactly as the run found it"
);
}
fn assert_a_later_compaction_reclaims_the_orphans(
store: &Path,
before: &BTreeMap<OsString, Vec<u8>>,
) {
let orphans: Vec<OsString> = store_files(store)
.into_keys()
.filter(|name| {
!before.contains_key(name)
&& Path::new(name)
.extension()
.is_some_and(|extension| extension == "tar")
})
.collect();
assert!(
!orphans.is_empty(),
"the failed run appended no archive, so there is nothing to reclaim"
);
compact(
store,
CompactionOptions::default()
.with_tasks([MaintenanceTask::Segments])
.with_compaction(CompactionKind::Full),
)
.expect("a later compaction runs over the orphan output");
let after = store_files(store);
for orphan in &orphans {
assert!(
!after.contains_key(orphan),
"{} survived the compaction that should have retired it",
Path::new(orphan).display()
);
}
let repository = Repository::open(store).expect("the store reopens after compaction");
drop(repository);
}
fn assert_the_retry_refuses_the_residue(store: &Path) {
let work = reindex_work_directory(store);
let residue: Vec<_> = std::fs::read_dir(&work)
.expect("read the work directory")
.map(|entry| entry.expect("entry").file_name())
.collect();
assert!(
!residue.is_empty(),
"the death left no run subdirectory to retry against"
);
let options = ReindexOptions::new()
.with_work_directory(WorkDirectory::OperatorNamed(work.clone()))
.with_sort_budget_bytes(64);
let error = plan_reindex(store, &options)
.expect_err("an operator-named directory holding residue must be refused");
assert!(
error
.to_string()
.contains("left over from an earlier froe reindex"),
"the refusal names what it found: {error}"
);
for name in residue {
std::fs::remove_dir_all(work.join(name)).expect("clear the residue");
}
let outcome = reindex(store, options).expect("the retry runs");
assert!(outcome.moved_the_head(), "the retry published nothing");
assert!(
!digest_under(store, "/oak:index/title/:index").is_empty(),
"the retry did not rebuild the index"
);
}
#[test]
fn a_spill_failure_removes_the_run_subdirectory_and_leaves_the_store_unchanged() {
let directory = TestDirectory::new("reindex-spill-error");
let store = write_flagged_index_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/title");
run_error_child(&store, REINDEX_SCENARIO, BEFORE_SPILL_CLEANUP);
assert_the_original_store_survived(&store, &before, &definition_before);
let leftovers: Vec<_> = std::fs::read_dir(reindex_work_directory(&store))
.expect("read the work directory")
.map(|entry| entry.expect("entry").file_name())
.collect();
assert!(
leftovers.is_empty(),
"a returned error removes the run's subdirectory: {leftovers:?}"
);
}
#[test]
fn a_death_before_spill_cleanup_leaves_no_file_in_the_store() {
let directory = TestDirectory::new("reindex-spill-crash");
let store = write_flagged_index_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/title");
run_crash_child(&store, REINDEX_SCENARIO, BEFORE_SPILL_CLEANUP);
assert_the_original_store_survived(&store, &before, &definition_before);
assert_the_retry_refuses_the_residue(&store);
}
#[test]
fn an_error_before_head_publish_leaves_the_head_and_every_definition_as_they_were() {
let directory = TestDirectory::new("reindex-publish-error");
let store = write_flagged_index_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/title");
run_error_child(&store, REINDEX_SCENARIO, BEFORE_HEAD_PUBLISH);
assert_the_original_store_survived(&store, &before, &definition_before);
assert_a_later_compaction_reclaims_the_orphans(&store, &before);
}
#[test]
fn a_death_before_head_publish_leaves_the_head_resolving_the_old_records() {
let directory = TestDirectory::new("reindex-publish-crash");
let store = write_flagged_index_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/title");
run_crash_child(&store, REINDEX_SCENARIO, BEFORE_HEAD_PUBLISH);
assert_the_original_store_survived(&store, &before, &definition_before);
assert_the_retry_refuses_the_residue(&store);
}
#[test]
fn an_error_after_head_publish_before_flush_leaves_the_journal_naming_the_old_head() {
let directory = TestDirectory::new("reindex-flush-error");
let store = write_flagged_index_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/title");
run_error_child(&store, REINDEX_SCENARIO, AFTER_HEAD_PUBLISH_BEFORE_FLUSH);
assert_the_original_store_survived(&store, &before, &definition_before);
assert_a_later_compaction_reclaims_the_orphans(&store, &before);
}
#[test]
fn a_death_between_head_publish_and_flush_leaves_one_resolvable_head() {
let directory = TestDirectory::new("reindex-flush-crash");
let store = write_flagged_index_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/title");
run_crash_child(&store, REINDEX_SCENARIO, AFTER_HEAD_PUBLISH_BEFORE_FLUSH);
assert_the_original_store_survived(&store, &before, &definition_before);
assert_the_retry_refuses_the_residue(&store);
}
#[test]
fn a_failed_applied_state_verification_reports_rather_than_repairs() {
let directory = TestDirectory::new("reindex-applied");
let store = write_flagged_index_fixture(&directory.path);
let head_before = Repository::open(&store)
.expect("open")
.head_record_identifier();
run_error_child(&store, REINDEX_SCENARIO, BEFORE_APPLIED_VERIFICATION);
let repository = Repository::open(&store).expect("the store reopens");
assert_ne!(
repository.head_record_identifier(),
head_before,
"this boundary is after publication, so the head has moved"
);
drop(repository);
assert!(
!digest_under(&store, "/oak:index/title/:index").is_empty(),
"the rebuilt index is published and stays published"
);
assert!(
crate::tooling::check_consistency(
&store,
&["/".to_owned()],
crate::tooling::BinaryCheck::EveryBlock,
1,
)
.expect("check the store")
.has_good_revision()
);
}
}