use std::sync::Arc;
use tempdir::TempDir;
use test_log::test;
use crate::compaction::leveled::Strategy;
use crate::vfs::sync_tracker;
use crate::{Error, Options, Tree};
fn simulate_data_loss(path: &std::path::Path) {
std::fs::OpenOptions::new().write(true).open(path).unwrap().set_len(0).unwrap();
}
#[test(tokio::test)]
async fn power_loss_after_compaction_must_not_lose_synced_data() {
let temp_dir = TempDir::new("test").unwrap();
let path = temp_dir.path().to_path_buf();
let opts = Arc::new(Options {
path,
level0_max_files: 2,
flush_on_close: false,
..Default::default()
});
let tree = Tree::new(Arc::clone(&opts)).unwrap();
for batch in 0..2u32 {
let mut txn = tree.begin().unwrap();
for i in 0..100u32 {
let key = format!("key_{batch:02}_{i:03}");
let value = format!("value_{batch:02}_{i:03}");
txn.set(key.as_bytes(), value.as_bytes()).unwrap();
}
txn.commit().await.unwrap();
tree.flush().unwrap();
}
let l0_ids: Vec<u64> = {
let manifest = tree.core.inner.level_manifest.read().unwrap();
manifest.levels.get_levels()[0].tables.iter().map(|t| t.id).collect()
};
assert_eq!(l0_ids.len(), 2, "expected two L0 tables before compaction");
tree.compact(Arc::new(Strategy::from_options(Arc::clone(&opts)))).unwrap();
let output_id = {
let manifest = tree.core.inner.level_manifest.read().unwrap();
let levels = manifest.levels.get_levels();
assert!(levels[0].tables.is_empty(), "L0 should be empty after compaction");
assert_eq!(levels[1].tables.len(), 1, "L1 should hold the compaction output");
levels[1].tables[0].id
};
for id in &l0_ids {
assert!(
!opts.sstable_file_path(*id).exists(),
"compaction input table {id} should be deleted"
);
}
{
let batch = 2u32;
let mut txn = tree.begin().unwrap();
for i in 0..100u32 {
let key = format!("key_{batch:02}_{i:03}");
let value = format!("value_{batch:02}_{i:03}");
txn.set(key.as_bytes(), value.as_bytes()).unwrap();
}
txn.commit().await.unwrap();
tree.flush().unwrap();
}
let sibling_id = {
let manifest = tree.core.inner.level_manifest.read().unwrap();
let levels = manifest.levels.get_levels();
assert_eq!(levels[0].tables.len(), 1, "post-compaction flush should land in L0");
levels[0].tables[0].id
};
{
let mut lockfile = tree.core.inner.lockfile.lock().unwrap();
lockfile.release().unwrap();
}
drop(tree);
let mut truncated = Vec::new();
for entry in std::fs::read_dir(opts.sstable_dir()).unwrap() {
let sst_path = entry.unwrap().path();
if sst_path.extension().is_some_and(|ext| ext == "sst")
&& !sync_tracker::was_synced(&sst_path)
{
simulate_data_loss(&sst_path);
truncated.push(sst_path);
}
}
let sibling_size = std::fs::metadata(opts.sstable_file_path(sibling_id)).unwrap().len();
assert!(sibling_size > 0, "fsynced flush table {sibling_id} must survive the crash intact");
let tree = Tree::new(Arc::clone(&opts)).unwrap_or_else(|e| {
panic!(
"store failed to reopen after simulated power loss (compaction \
output table {output_id} was not fsynced before the manifest \
referenced it; truncated: {truncated:?}): {e}"
)
});
let txn = tree.begin().unwrap();
for batch in 0..3u32 {
for i in 0..100u32 {
let key = format!("key_{batch:02}_{i:03}");
let expected = format!("value_{batch:02}_{i:03}");
let got = txn.get(key.as_bytes()).unwrap();
assert_eq!(got, Some(expected.into_bytes()), "missing {key} after crash recovery");
}
}
}
#[test(tokio::test)]
async fn manifest_referencing_zero_byte_sst_fails_cleanly() {
let temp_dir = TempDir::new("test").unwrap();
let path = temp_dir.path().to_path_buf();
let opts = Arc::new(Options {
path,
..Default::default()
});
let table_id = {
let tree = Tree::new(Arc::clone(&opts)).unwrap();
let mut txn = tree.begin().unwrap();
txn.set(b"key", b"value").unwrap();
txn.commit().await.unwrap();
tree.flush().unwrap();
let table_id = {
let manifest = tree.core.inner.level_manifest.read().unwrap();
manifest.levels.get_levels()[0].tables[0].id
};
tree.close().await.unwrap();
table_id
};
simulate_data_loss(&opts.sstable_file_path(table_id));
match Tree::new(Arc::clone(&opts)) {
Ok(_) => panic!("open should fail when the manifest references a zero-byte SST"),
Err(Error::LoadManifestFail(msg)) => {
assert!(
msg.contains("file size is too small"),
"unexpected LoadManifestFail message: {msg}"
);
}
Err(other) => panic!("expected LoadManifestFail, got: {other}"),
}
}