use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
use test_log::test;
use crate::test::recovery_test_helpers::RecoveryTestHelper;
use crate::TreeBuilder;
fn create_tree<F>(path: PathBuf, configure: F) -> crate::Tree
where
F: FnOnce(TreeBuilder) -> TreeBuilder,
{
let builder = TreeBuilder::new().with_path(path).with_max_memtable_size(1024 * 1024);
configure(builder).build().unwrap()
}
#[test(tokio::test)]
async fn test_basic_recovery() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
{
let tree = create_tree(path.clone(), |b| b);
let mut txn = tree.begin().unwrap();
txn.set(b"foo", b"v1").unwrap();
txn.commit().await.unwrap();
let mut txn = tree.begin().unwrap();
txn.set(b"baz", b"v5").unwrap();
txn.commit().await.unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
RecoveryTestHelper::verify_key(&tree, "foo", "v1").await;
RecoveryTestHelper::verify_key(&tree, "baz", "v5").await;
let mut txn = tree.begin().unwrap();
txn.set(b"bar", b"v2").unwrap();
txn.commit().await.unwrap();
let mut txn = tree.begin().unwrap();
txn.set(b"foo", b"v3").unwrap();
txn.commit().await.unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
RecoveryTestHelper::verify_key(&tree, "foo", "v3").await;
RecoveryTestHelper::verify_key(&tree, "bar", "v2").await;
RecoveryTestHelper::verify_key(&tree, "baz", "v5").await;
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_recover_with_existing_ssts() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let sst_dir = path.join("sstables");
{
let tree = create_tree(path.clone(), |b| {
b.with_max_memtable_size(64 * 1024) });
let mut txn = tree.begin().unwrap();
txn.set(b"foo", b"v1").unwrap();
txn.commit().await.unwrap();
let mut txn = tree.begin().unwrap();
txn.set(b"bar", b"v2").unwrap();
txn.commit().await.unwrap();
tree.flush().unwrap();
let sst_count_1 = RecoveryTestHelper::count_sst_files(&sst_dir);
assert_eq!(sst_count_1, 1, "Should have 1 SST after first flush");
let mut txn = tree.begin().unwrap();
txn.set(b"foo", b"v3").unwrap();
txn.commit().await.unwrap();
let mut txn = tree.begin().unwrap();
txn.set(b"bar", b"v4").unwrap();
txn.commit().await.unwrap();
tree.flush().unwrap();
let sst_count_2 = RecoveryTestHelper::count_sst_files(&sst_dir);
assert_eq!(sst_count_2, 2, "Should have 2 SSTs after second flush");
let mut txn = tree.begin().unwrap();
txn.set(b"big", b"large_value_not_flushed").unwrap();
txn.commit().await.unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
RecoveryTestHelper::verify_key(&tree, "foo", "v3").await;
RecoveryTestHelper::verify_key(&tree, "bar", "v4").await;
RecoveryTestHelper::verify_key(&tree, "big", "large_value_not_flushed").await;
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_recover_multiple_wals_without_flush() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let keys_to_write = 100;
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..keys_to_write {
let key = format!("key_{:05}", i);
let value = format!("value_{:05}", i);
let mut txn = tree.begin().unwrap();
txn.set(key.as_bytes(), value.as_bytes()).unwrap();
txn.commit().await.unwrap();
}
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..keys_to_write {
let key = format!("key_{:05}", i);
let expected_value = format!("value_{:05}", i);
RecoveryTestHelper::verify_key(&tree, &key, &expected_value).await;
}
let mut txn = tree.begin().unwrap();
txn.set(b"new_key", b"new_value").unwrap();
txn.commit().await.unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..keys_to_write {
let key = format!("key_{:05}", i);
let expected_value = format!("value_{:05}", i);
RecoveryTestHelper::verify_key(&tree, &key, &expected_value).await;
}
RecoveryTestHelper::verify_key(&tree, "new_key", "new_value").await;
tree.flush().unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..keys_to_write {
let key = format!("key_{:05}", i);
let expected_value = format!("value_{:05}", i);
RecoveryTestHelper::verify_key(&tree, &key, &expected_value).await;
}
RecoveryTestHelper::verify_key(&tree, "new_key", "new_value").await;
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_recover_with_large_wal() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let large_entry_count = 1000;
{
let tree = create_tree(path.clone(), |b| {
b.with_max_memtable_size(10 * 1024 * 1024) });
for i in 0..large_entry_count {
let key = format!("large_key_{:06}", i);
let value = format!("large_value_{:06}_with_padding", i);
let mut txn = tree.begin().unwrap();
txn.set(key.as_bytes(), value.as_bytes()).unwrap();
txn.commit().await.unwrap();
}
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..large_entry_count {
let key = format!("large_key_{:06}", i);
let txn = tree.begin().unwrap();
let result = txn.get(key.as_bytes()).unwrap();
assert!(result.is_some(), "Key {} should exist", key);
}
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_recovery_with_empty_wal() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
{
let tree = create_tree(path.clone(), |b| b);
let mut txn = tree.begin().unwrap();
txn.set(b"foo", b"v1").unwrap();
txn.commit().await.unwrap();
tree.flush().unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
RecoveryTestHelper::verify_key(&tree, "foo", "v1").await;
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_file_count_after_recovery() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let wal_dir = path.join("wal");
let sst_dir = path.join("sstables");
{
let tree = create_tree(path.clone(), |b| {
b.with_max_memtable_size(4 * 1024) });
for i in 0..150 {
let key = format!("key_{:04}", i);
let value = format!("value_{:04}", i);
let mut txn = tree.begin().unwrap();
txn.set(key.as_bytes(), value.as_bytes()).unwrap();
txn.commit().await.unwrap();
}
let sst_count = RecoveryTestHelper::count_sst_files(&sst_dir);
assert!(sst_count >= 1, "Should have created SST files");
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
let sst_count_after = RecoveryTestHelper::count_sst_files(&sst_dir);
let wal_count_after = RecoveryTestHelper::count_wal_files(&wal_dir);
assert!(sst_count_after >= 1, "SST files should exist after recovery");
assert!(wal_count_after >= 1, "WAL files should exist");
for i in 0..150 {
let key = format!("key_{:04}", i);
let txn = tree.begin().unwrap();
let result = txn.get(key.as_bytes()).unwrap();
assert!(result.is_some(), "Key {} should exist after recovery", key);
}
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_wal_cleanup_after_recovery_without_flush() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let wal_dir = path.join("wal");
{
let tree = create_tree(path.clone(), |b| b);
let mut txn = tree.begin().unwrap();
txn.set(b"foo", b"v1").unwrap();
txn.commit().await.unwrap();
tree.close().await.unwrap();
}
let wal_count_before_flush;
{
let tree = create_tree(path.clone(), |b| b);
wal_count_before_flush = RecoveryTestHelper::count_wal_files(&wal_dir);
assert!(wal_count_before_flush > 0, "WAL files should exist after recovery");
tree.flush().unwrap();
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
let wal_count_after_flush = RecoveryTestHelper::count_wal_files(&wal_dir);
assert!(
wal_count_after_flush <= wal_count_before_flush + 1,
"Old WALs should be cleaned up after flush"
);
RecoveryTestHelper::verify_key(&tree, "foo", "v1").await;
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_mixed_flushed_and_unflushed_wals() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
{
let tree = create_tree(path.clone(), |b| b.with_max_memtable_size(64 * 1024));
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("batch_a_{}", i).as_bytes(), b"value_a").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
let log_number_after_flush = RecoveryTestHelper::get_manifest_log_number(&tree);
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("batch_b_{}", i).as_bytes(), b"value_b").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("batch_c_{}", i).as_bytes(), b"value_c").unwrap();
txn.commit().await.unwrap();
}
tree.close().await.unwrap();
log::info!("Log number after flush: {}", log_number_after_flush);
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..10 {
let key = format!("batch_a_{}", i);
RecoveryTestHelper::verify_key(&tree, &key, "value_a").await;
}
for i in 0..10 {
let key = format!("batch_b_{}", i);
RecoveryTestHelper::verify_key(&tree, &key, "value_b").await;
}
for i in 0..10 {
let key = format!("batch_c_{}", i);
RecoveryTestHelper::verify_key(&tree, &key, "value_c").await;
}
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_orphaned_sst_doesnt_break_recovery() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let sst_dir = path.join("sstables");
{
let tree = create_tree(path.clone(), |b| b);
let mut txn = tree.begin().unwrap();
txn.set(b"real_key", b"real_value").unwrap();
txn.commit().await.unwrap();
tree.flush().unwrap();
tree.close().await.unwrap();
}
fs::create_dir_all(&sst_dir).ok();
let orphan_sst_path = sst_dir.join("99999999999999999999.sst");
fs::write(&orphan_sst_path, b"orphaned_data").unwrap();
{
let tree = create_tree(path.clone(), |b| b);
RecoveryTestHelper::verify_key(&tree, "real_key", "real_value").await;
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_manifest_log_number_progression() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
{
let tree = create_tree(path.clone(), |b| b.with_max_memtable_size(64 * 1024));
let log_num_initial = RecoveryTestHelper::get_manifest_log_number(&tree);
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("c1_key_{}", i).as_bytes(), b"cycle1").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
let log_num_after_flush = RecoveryTestHelper::get_manifest_log_number(&tree);
assert!(log_num_after_flush > log_num_initial, "Log number should advance after flush");
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
let log_num_before = RecoveryTestHelper::get_manifest_log_number(&tree);
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("c2_key_{}", i).as_bytes(), b"cycle2").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
let log_num_after = RecoveryTestHelper::get_manifest_log_number(&tree);
assert!(log_num_after > log_num_before, "Log number should advance again");
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
let log_num_before = RecoveryTestHelper::get_manifest_log_number(&tree);
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("c3_key_{}", i).as_bytes(), b"cycle3").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
let log_num_after = RecoveryTestHelper::get_manifest_log_number(&tree);
assert!(log_num_after > log_num_before, "Log number should advance with flush in cycle 3");
tree.close().await.unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..10 {
RecoveryTestHelper::verify_key(&tree, &format!("c1_key_{}", i), "cycle1").await;
RecoveryTestHelper::verify_key(&tree, &format!("c2_key_{}", i), "cycle2").await;
RecoveryTestHelper::verify_key(&tree, &format!("c3_key_{}", i), "cycle3").await;
}
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_recovery_with_no_wal_files() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let wal_dir = path.join("wal");
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..20 {
let mut txn = tree.begin().unwrap();
txn.set(format!("key_{}", i).as_bytes(), b"value").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
tree.close().await.unwrap();
}
if wal_dir.exists() {
for entry in fs::read_dir(&wal_dir).unwrap().flatten() {
if entry.path().extension().and_then(|s| s.to_str()) == Some("wal") {
fs::remove_file(entry.path()).ok();
}
}
}
let wal_count_after_delete = RecoveryTestHelper::count_wal_files(&wal_dir);
assert_eq!(wal_count_after_delete, 0, "All WAL files should be deleted");
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..20 {
let key = format!("key_{}", i);
RecoveryTestHelper::verify_key(&tree, &key, "value").await;
}
let mut txn = tree.begin().unwrap();
txn.set(b"new_key", b"new_value").unwrap();
txn.commit().await.unwrap();
let wal_count_after_write = RecoveryTestHelper::count_wal_files(&wal_dir);
assert!(wal_count_after_write > 0, "New WAL should be created");
tree.close().await.unwrap();
}
}
#[test(tokio::test)]
async fn test_corrupted_wal_with_valid_sst() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
let wal_dir = path.join("wal");
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..10 {
let mut txn = tree.begin().unwrap();
txn.set(format!("sst_key_{}", i).as_bytes(), b"safe_in_sst").unwrap();
txn.commit().await.unwrap();
}
tree.flush().unwrap();
for i in 0..5 {
let mut txn = tree.begin().unwrap();
txn.set(format!("wal_key_{}", i).as_bytes(), b"in_wal").unwrap();
txn.commit().await.unwrap();
}
tree.close().await.unwrap();
}
if let Some(wal_file) = fs::read_dir(&wal_dir)
.ok()
.and_then(|mut entries| entries.find_map(|e| e.ok()))
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("wal"))
{
let mut file = fs::OpenOptions::new().write(true).open(wal_file.path()).unwrap();
use std::io::{Seek, SeekFrom, Write};
file.seek(SeekFrom::Start(50)).unwrap();
file.write_all(&[0xFF, 0xAA, 0x55]).unwrap();
}
{
let tree = create_tree(path.clone(), |b| b);
for i in 0..10 {
let key = format!("sst_key_{}", i);
RecoveryTestHelper::verify_key(&tree, &key, "safe_in_sst").await;
}
tree.close().await.unwrap();
}
}