use std::fs;
use std::io::Write;
use tempfile::TempDir;
use test_log::test;
use crate::batch::Batch;
use crate::error::Error;
use crate::test::recovery_test_helpers::{CorruptionType, WalTestHelper};
use crate::wal::manager::Wal;
use crate::wal::recovery::replay_wal;
use crate::wal::Options;
const ARENA_SIZE: usize = 1024 * 1024;
#[test]
fn test_sequential_5_segments_recovery() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![100, 100, 100, 100, 100];
let final_seq = WalTestHelper::create_segments(wal_dir, &entries_per_segment, 1000);
assert_eq!(final_seq, 1500, "Final sequence should be 1500 (1000 + 500 entries)");
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(1499), "Max sequence should be 1499");
WalTestHelper::verify_total_entry_count(&memtables, 500);
let expected_keys = WalTestHelper::generate_expected_keys(0, 4, &entries_per_segment);
WalTestHelper::verify_entries_across_memtables(&memtables, &expected_keys);
}
#[test]
fn test_empty_segments_between_data() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![50, 0, 50, 0, 50];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 100);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some(), "Should have recovered data");
WalTestHelper::verify_total_entry_count(&memtables, 150);
let expected_keys = WalTestHelper::generate_expected_keys(0, 4, &entries_per_segment);
WalTestHelper::verify_entries_across_memtables(&memtables, &expected_keys);
}
#[test]
fn test_large_batches_multiple_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![1000, 1000, 1000, 1000, 1000];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 10000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(14999), "Max sequence should be 14999");
WalTestHelper::verify_total_entry_count(&memtables, 5000);
}
#[test]
fn test_variable_sized_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![10, 100, 1000, 50, 200];
let total_entries: usize = entries_per_segment.iter().sum();
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 5000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some(), "Should have recovered data");
WalTestHelper::verify_total_entry_count(&memtables, total_entries);
let expected_keys = WalTestHelper::generate_expected_keys(0, 4, &entries_per_segment);
WalTestHelper::verify_entries_across_memtables(&memtables, &expected_keys);
}
#[test]
fn test_100_segments_recovery() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let mut entries_per_segment = Vec::new();
let mut total_entries = 0;
for i in 0..100 {
let count = 10 + (i % 41); entries_per_segment.push(count);
total_entries += count;
}
let start = std::time::Instant::now();
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 100000);
let create_duration = start.elapsed();
let start = std::time::Instant::now();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
let replay_duration = start.elapsed();
assert!(max_seq_opt.is_some(), "Should have recovered data");
WalTestHelper::verify_total_entry_count(&memtables, total_entries);
log::info!(
"100 segments: create={:?}, replay={:?}, entries={}",
create_duration,
replay_duration,
total_entries
);
}
#[test]
fn test_single_large_vs_multiple_small_segments() {
let temp_dir1 = TempDir::new().unwrap();
let temp_dir2 = TempDir::new().unwrap();
fs::create_dir_all(temp_dir1.path()).unwrap();
fs::create_dir_all(temp_dir2.path()).unwrap();
let total_entries = 10000;
let start = std::time::Instant::now();
WalTestHelper::create_segments(temp_dir1.path(), &[total_entries], 0);
let single_create = start.elapsed();
let start = std::time::Instant::now();
let (max_seq1, memtables1) = replay_wal(temp_dir1.path(), 0, ARENA_SIZE).unwrap();
let single_replay = start.elapsed();
let entries_per_segment = vec![1000; 10];
let start = std::time::Instant::now();
WalTestHelper::create_segments(temp_dir2.path(), &entries_per_segment, 0);
let multi_create = start.elapsed();
let start = std::time::Instant::now();
let (max_seq2, memtables2) = replay_wal(temp_dir2.path(), 0, ARENA_SIZE).unwrap();
let multi_replay = start.elapsed();
assert_eq!(max_seq1, max_seq2);
WalTestHelper::verify_total_entry_count(&memtables1, total_entries);
WalTestHelper::verify_total_entry_count(&memtables2, total_entries);
log::info!("Single segment: create={:?}, replay={:?}", single_create, single_replay);
log::info!("Multiple segments: create={:?}, replay={:?}", multi_create, multi_replay);
}
#[test]
fn test_crash_immediately_after_rotation() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let mut batch = Batch::new(100);
for i in 0..50 {
batch
.set(
format!("key{}", i).as_bytes().to_vec(),
format!("value{}", i).as_bytes().to_vec(),
0,
)
.unwrap();
}
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch.encode().unwrap()).unwrap();
wal.rotate().unwrap();
wal.close().unwrap();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(149), "Should recover all 50 entries from segment 0");
WalTestHelper::verify_total_entry_count(&memtables, 50);
}
#[test]
fn test_crash_after_multiple_rapid_rotations() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
for i in 0..5 {
let mut batch = Batch::new(100 + i * 10);
batch
.set(
format!("key{}", i).as_bytes().to_vec(),
format!("val{}", i).as_bytes().to_vec(),
0,
)
.unwrap();
wal.append(&batch.encode().unwrap()).unwrap();
if i < 4 {
wal.rotate().unwrap();
}
}
wal.close().unwrap();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 5);
}
#[test]
fn test_crash_during_wal_write_mid_batch() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let mut batch1 = Batch::new(100);
for i in 0..10 {
batch1
.set(
format!("key{}", i).as_bytes().to_vec(),
format!("val{}", i).as_bytes().to_vec(),
0,
)
.unwrap();
}
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
let mut batch2 = Batch::new(200);
for i in 0..10 {
batch2
.set(
format!("key2_{}", i).as_bytes().to_vec(),
format!("val2_{}", i).as_bytes().to_vec(),
0,
)
.unwrap();
}
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
let segment_path = wal_dir.join("00000000000000000000.wal");
let file_size = fs::metadata(&segment_path).unwrap().len();
let truncate_point = file_size - 20; let file = fs::OpenOptions::new().write(true).open(&segment_path).unwrap();
file.set_len(truncate_point).unwrap();
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok((_, memtables)) => {
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(
entry_count >= 10,
"Should recover at least batch1 (10 entries), got {}",
entry_count
);
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_crash_with_unflushed_memtable() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![20, 30, 40];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 500);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 90);
}
#[test]
fn test_partial_batch_at_segment_end() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let mut batch = Batch::new(100);
for i in 0..20 {
batch
.set(
format!("key{}", i).as_bytes().to_vec(),
format!("val{}", i).as_bytes().to_vec(),
0,
)
.unwrap();
}
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch.encode().unwrap()).unwrap();
wal.close().unwrap();
let segment_path = wal_dir.join("00000000000000000000.wal");
let file = fs::OpenOptions::new().write(true).open(&segment_path).unwrap();
let current_size = file.metadata().unwrap().len();
file.set_len(current_size - 10).unwrap();
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok(_) => {
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_empty_segment_at_end() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![50, 50, 0];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 1000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 100);
}
#[test]
fn test_all_empty_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![0, 0, 0];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 1000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, None);
assert!(memtables.is_empty(), "Should have no memtables for empty segments");
assert_eq!(WalTestHelper::count_total_entries(&memtables), 0);
}
#[test]
fn test_rapid_rotation_with_minimal_data() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![1; 10];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 2000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(2009));
WalTestHelper::verify_total_entry_count(&memtables, 10);
}
#[test]
fn test_corruption_first_record_segment_0() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![30, 30, 30, 30, 30];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 1000);
WalTestHelper::corrupt_segment(wal_dir, 0, 0.0, CorruptionType::HeaderCorruption);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 0);
}
_ => panic!("Expected WalCorruption error in segment 0"),
}
}
#[test]
fn test_corruption_middle_segment_0() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![100, 50, 50];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 5000);
WalTestHelper::corrupt_segment(wal_dir, 0, 0.5, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 0);
}
_ => panic!("Expected WalCorruption error in segment 0"),
}
}
#[test]
fn test_corruption_end_segment_0() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![50, 30, 30];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 3000);
WalTestHelper::corrupt_segment(wal_dir, 0, 0.9, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 0);
}
_ => panic!("Expected WalCorruption error in segment 0"),
}
}
#[test]
fn test_corruption_beginning_middle_segment() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![40, 40, 40, 40];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 6000);
WalTestHelper::corrupt_segment(wal_dir, 2, 0.0, CorruptionType::HeaderCorruption);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 2);
}
_ => panic!("Expected WalCorruption error in segment 2"),
}
}
#[test]
fn test_corruption_middle_of_middle_segment() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![50, 50, 50, 50];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 7000);
WalTestHelper::corrupt_segment(wal_dir, 2, 0.5, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 2);
}
_ => panic!("Expected WalCorruption error in segment 2"),
}
}
#[test]
fn test_corruption_last_segment() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![25, 25, 25, 25, 25];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 8000);
WalTestHelper::corrupt_segment(wal_dir, 4, 0.1, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 4);
}
_ => panic!("Expected WalCorruption error in segment 4"),
}
}
#[test]
fn test_truncated_wal_file() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![30, 30, 30];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 9000);
WalTestHelper::corrupt_segment(wal_dir, 1, 0.6, CorruptionType::Truncate);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert!(segment_id >= 1, "Corruption should be in segment 1 or later");
}
Ok((_, memtables)) => {
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(entry_count >= 30, "Should have at least segment 0, got {}", entry_count);
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_random_byte_corruption() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![50];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 10000);
WalTestHelper::corrupt_segment(wal_dir, 0, 0.4, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok(_) => {
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_multiple_corruptions_stop_at_first() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![40, 40, 40, 40];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 11000);
WalTestHelper::corrupt_segment(wal_dir, 1, 0.5, CorruptionType::RandomBytes);
WalTestHelper::corrupt_segment(wal_dir, 3, 0.3, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 1, "Should stop at first corruption (segment 1)");
}
_ => panic!("Expected WalCorruption error in segment 1"),
}
}
#[test]
fn test_crc_mismatch() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![40];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 12000);
WalTestHelper::corrupt_segment(wal_dir, 0, 0.3, CorruptionType::CrcMismatch);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok((_, memtables)) => {
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(entry_count < 40, "Should have partial recovery");
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_corruption_in_wal_header() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![35, 35];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 13000);
WalTestHelper::corrupt_segment(wal_dir, 1, 0.0, CorruptionType::HeaderCorruption);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 1);
}
_ => panic!("Expected WalCorruption error in segment 1"),
}
}
#[test]
fn test_corruption_in_batch_data() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![30];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 14000);
WalTestHelper::corrupt_segment(wal_dir, 0, 0.5, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok(_) => {
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_completely_corrupted_file() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![25, 25, 25];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 15000);
let segment_path = wal_dir.join("00000000000000000001.wal");
let garbage: Vec<u8> = vec![0xDE; 1000]; fs::write(&segment_path, garbage).unwrap();
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 1);
}
_ => panic!("Expected WalCorruption error in segment 1"),
}
}
#[test]
fn test_tail_corruption_recovery() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![40];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 16000);
let segment_path = wal_dir.join("00000000000000000000.wal");
let mut file = fs::OpenOptions::new().append(true).open(&segment_path).unwrap();
file.write_all(b"CORRUPTED_TAIL_DATA_HERE").unwrap();
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok((_, memtables)) => {
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(
entry_count >= 30,
"Should recover most data despite tail corruption, got {}",
entry_count
);
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_contiguous_sequence_numbers() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![100, 100, 100];
let final_seq = WalTestHelper::create_segments(wal_dir, &entries_per_segment, 100);
assert_eq!(final_seq, 400, "Final sequence should be 400");
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(399), "Max sequence should be 399");
WalTestHelper::verify_total_entry_count(&memtables, 300);
}
#[test]
fn test_sequence_tracking_10_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![100; 10];
let final_seq = WalTestHelper::create_segments(wal_dir, &entries_per_segment, 1000);
assert_eq!(final_seq, 2000, "Final sequence should be 2000");
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(1999), "Max sequence should be 1999");
WalTestHelper::verify_total_entry_count(&memtables, 1000);
}
#[test]
fn test_sequence_after_corruption() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![101, 100];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 100);
WalTestHelper::corrupt_segment(wal_dir, 1, 0.5, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 1);
}
_ => panic!("Expected WalCorruption error in segment 1"),
}
}
#[test]
fn test_large_sequence_numbers() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![50, 50, 50];
let final_seq = WalTestHelper::create_segments(wal_dir, &entries_per_segment, 1_000_000);
assert_eq!(final_seq, 1_000_150);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, Some(1_000_149));
WalTestHelper::verify_total_entry_count(&memtables, 150);
}
#[test]
fn test_sequence_gaps_detection() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![30, 0, 30, 0, 30];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 5000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 90);
}
#[test]
fn test_skip_flushed_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![20, 20, 20, 20, 20];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 6000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 3, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 40);
}
#[test]
fn test_all_segments_already_flushed() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![15, 15, 15];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 7000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 3, ARENA_SIZE).unwrap();
assert_eq!(max_seq_opt, None);
assert!(memtables.is_empty(), "Should have no memtables when all segments already flushed");
assert_eq!(WalTestHelper::count_total_entries(&memtables), 0);
}
#[test]
fn test_min_wal_equals_first_segment() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![25, 25, 25];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 8000);
for i in 0..3 {
let old_path = wal_dir.join(format!("{:020}.wal", i));
let new_path = wal_dir.join(format!("{:020}.wal", i + 5));
fs::rename(old_path, new_path).unwrap();
}
let (max_seq_opt, memtables) = replay_wal(wal_dir, 5, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 75);
}
#[test]
fn test_mixed_flushed_unflushed() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![18, 18, 18, 18, 18, 18];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 9000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 3, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 54);
}
#[test]
fn test_recovery_min_wal_zero() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![22, 22, 22, 22, 22, 22];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 10000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 132);
}
#[test]
fn test_corruption_at_min_wal_boundary() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![24, 24, 24, 24, 24, 24];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 11000);
WalTestHelper::corrupt_segment(wal_dir, 3, 0.2, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 3, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 3);
}
_ => panic!("Expected WalCorruption error in segment 3"),
}
}
#[test]
fn test_boundary_with_empty_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![20, 0, 0, 20, 20];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 12000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 2, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 40);
}
#[test]
fn test_very_large_values_across_segments() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
let large_value = vec![b'X'; 10240];
for seg_idx in 0..5 {
let mut batch = Batch::new(13000 + seg_idx * 10);
for i in 0..5 {
let key = format!("large_seg{}_key{}", seg_idx, i);
batch.set(key.as_bytes().to_vec(), large_value.clone(), 0).unwrap();
}
wal.append(&batch.encode().unwrap()).unwrap();
if seg_idx < 4 {
wal.rotate().unwrap();
}
}
wal.close().unwrap();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 25);
}
#[test]
fn test_many_small_batches() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
let mut entry_count = 0;
for seg_idx in 0..10 {
for batch_idx in 0..100 {
let mut batch = Batch::new(14000 + entry_count);
let key = format!("s{}_b{}_k", seg_idx, batch_idx);
batch.set(key.as_bytes().to_vec(), b"value".to_vec(), 0).unwrap();
wal.append(&batch.encode().unwrap()).unwrap();
entry_count += 1;
}
if seg_idx < 9 {
wal.rotate().unwrap();
}
}
wal.close().unwrap();
let start = std::time::Instant::now();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
let duration = start.elapsed();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 1000);
log::info!("1000 small batches replay time: {:?}", duration);
}
#[test]
fn test_few_large_batches() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
for seg_idx in 0..5 {
let mut batch = Batch::new(15000 + seg_idx * 200);
for i in 0..200 {
let key = format!("seg{}_key{:04}", seg_idx, i);
batch.set(key.as_bytes().to_vec(), b"value".to_vec(), 0).unwrap();
}
wal.append(&batch.encode().unwrap()).unwrap();
if seg_idx < 4 {
wal.rotate().unwrap();
}
}
wal.close().unwrap();
let start = std::time::Instant::now();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
let duration = start.elapsed();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 1000);
log::info!("5 large batches replay time: {:?}", duration);
}
#[test]
fn test_recovery_performance_scaling() {
let segment_counts = vec![10, 20, 50];
let entries_per_segment = 50;
for &seg_count in &segment_counts {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let segments = vec![entries_per_segment; seg_count];
WalTestHelper::create_segments(wal_dir, &segments, 16000);
let start = std::time::Instant::now();
let (_max_seq, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
let duration = start.elapsed();
let total_entries = seg_count * entries_per_segment;
WalTestHelper::verify_total_entry_count(&memtables, total_entries);
log::info!("{} segments, {} entries: {:?}", seg_count, total_entries, duration);
}
}
#[test]
fn test_wal_segments_with_gaps() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![28, 28, 28, 28, 28];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 17000);
let segment2_path = wal_dir.join("00000000000000000002.wal");
fs::remove_file(segment2_path).ok();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 112);
}
#[test]
fn test_non_sequential_segment_ids_with_min_wal() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![26, 26, 26, 26];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 18000);
let new_ids = [0, 5, 10, 15];
for (old_id, new_id) in new_ids.iter().enumerate() {
let old_path = wal_dir.join(format!("{:020}.wal", old_id));
let new_path = wal_dir.join(format!("{:020}.wal", new_id));
fs::rename(old_path, new_path).unwrap();
}
let (max_seq_opt, memtables) = replay_wal(wal_dir, 5, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 78);
}
#[test]
fn test_zero_length_segment_file() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![32, 32, 32];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 19000);
let segment1_path = wal_dir.join("00000000000000000001.wal");
fs::write(&segment1_path, b"").unwrap();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 64);
}
#[test]
fn test_segment_larger_than_expected() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![5000];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 20000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 5000);
}
#[test]
fn test_recovery_with_readonly_segment() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![35, 35];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 21000);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let segment0_path = wal_dir.join("00000000000000000000.wal");
let mut perms = fs::metadata(&segment0_path).unwrap().permissions();
perms.set_mode(0o444); fs::set_permissions(&segment0_path, perms).unwrap();
}
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 70);
}
#[test]
fn test_only_wal_extension_processed() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![38, 38];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 22000);
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 76);
}
#[test]
fn test_symlink_to_wal_segment() {
#[cfg(unix)]
{
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![42];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 23000);
let original = wal_dir.join("00000000000000000000.wal");
let symlink = wal_dir.join("00000000000000000001.wal");
std::os::unix::fs::symlink(&original, &symlink).ok();
let (_result, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(entry_count >= 42, "Should process at least original segment");
}
}
#[test]
fn test_concurrent_wal_directory_modifications() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let entries_per_segment = vec![45, 45];
WalTestHelper::create_segments(wal_dir, &entries_per_segment, 24000);
fs::write(wal_dir.join("00000000000000000002.wal"), b"new_file").unwrap();
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 2, "Corruption should be in segment 2");
}
Ok((_, memtables)) => {
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(entry_count >= 90, "Should have recovered segments 0-1, got {}", entry_count);
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_compressed_wal_segments() {
use crate::wal::CompressionType;
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options {
compression_type: CompressionType::Lz4,
..Default::default()
};
let mut wal = Wal::open(wal_dir, opts).unwrap();
for seg_idx in 0..3 {
let mut batch = Batch::new(25000 + seg_idx * 50);
for i in 0..50 {
let key = format!("compressed_seg{}_k{}", seg_idx, i);
let value = format!("compressed_value_{}_{}", seg_idx, i);
batch.set(key.as_bytes().to_vec(), value.as_bytes().to_vec(), 0).unwrap();
}
wal.append(&batch.encode().unwrap()).unwrap();
if seg_idx < 2 {
wal.rotate().unwrap();
}
}
wal.close().unwrap();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 150);
}
#[test]
fn test_mixed_compression_segments() {
use crate::wal::CompressionType;
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
for seg_idx in 0..2 {
let mut batch = Batch::new(26000 + seg_idx * 40);
for i in 0..40 {
let key = format!("uncomp_seg{}_k{}", seg_idx, i);
batch.set(key.as_bytes().to_vec(), b"uncompressed_value".to_vec(), 0).unwrap();
}
wal.append(&batch.encode().unwrap()).unwrap();
wal.rotate().unwrap();
}
wal.close().unwrap();
let opts = Options {
compression_type: CompressionType::Lz4,
..Default::default()
};
let mut wal = Wal::open(wal_dir, opts).unwrap();
for seg_idx in 0..2 {
let mut batch = Batch::new(26080 + seg_idx * 40);
for i in 0..40 {
let key = format!("comp_seg{}_k{}", seg_idx, i);
batch.set(key.as_bytes().to_vec(), b"compressed_value".to_vec(), 0).unwrap();
}
wal.append(&batch.encode().unwrap()).unwrap();
if seg_idx < 1 {
wal.rotate().unwrap();
}
}
wal.close().unwrap();
let (max_seq_opt, memtables) = replay_wal(wal_dir, 0, ARENA_SIZE).unwrap();
assert!(max_seq_opt.is_some());
WalTestHelper::verify_total_entry_count(&memtables, 160);
}
#[test]
fn test_compressed_data_corruption() {
use crate::wal::CompressionType;
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let opts = Options {
compression_type: CompressionType::Lz4,
..Default::default()
};
let mut wal = Wal::open(wal_dir, opts).unwrap();
let mut batch = Batch::new(27000);
for i in 0..60 {
let key = format!("key{}", i);
batch.set(key.as_bytes().to_vec(), b"compressed_value_data".to_vec(), 0).unwrap();
}
wal.append(&batch.encode().unwrap()).unwrap();
wal.close().unwrap();
WalTestHelper::corrupt_segment(wal_dir, 0, 0.5, CorruptionType::RandomBytes);
let result = replay_wal(wal_dir, 0, ARENA_SIZE);
match result {
Err(Error::WalCorruption {
..
}) => {
}
Ok((_, memtables)) => {
let entry_count = WalTestHelper::count_total_entries(&memtables);
assert!(entry_count < 60, "Should detect corruption in compressed data");
}
Err(e) => panic!("Unexpected error: {}", e),
}
}