use std::fs::File;
use std::path::Path;
use std::sync::Arc;
use crate::batch::Batch;
use crate::error::{Error, Result};
use crate::lsm::fsync_directory;
use crate::memtable::MemTable;
use crate::wal::reader::{Reader, Reporter};
use crate::wal::{get_segment_range, list_segment_ids, Error as WalError, SegmentRef};
pub struct DefaultReporter {
log_number: u64,
corruption_count: usize,
old_record_count: usize,
}
impl DefaultReporter {
pub fn new(log_number: u64) -> Self {
Self {
log_number,
corruption_count: 0,
old_record_count: 0,
}
}
}
impl Reporter for DefaultReporter {
fn corruption(&mut self, bytes: usize, reason: &str, log_number: u64) {
log::error!("Corruption in WAL {}: {} bytes lost - {}", log_number, bytes, reason);
self.corruption_count += 1;
}
fn old_log_record(&mut self, bytes: usize) {
log::warn!("Old log record encountered in WAL {}: {} bytes", self.log_number, bytes);
self.old_record_count += 1;
}
}
type ReplayResult = (Option<u64>, Vec<(Arc<MemTable>, u64)>);
pub(crate) fn replay_wal(
wal_dir: &Path,
min_wal_number: u64,
arena_size: usize,
) -> Result<ReplayResult> {
log::info!("Starting WAL recovery from directory: {:?}", wal_dir);
log::debug!(
"WAL recovery parameters: min_wal_number={}, arena_size={}",
min_wal_number,
arena_size
);
if !wal_dir.exists() {
log::debug!("WAL directory does not exist, skipping recovery");
return Ok((None, vec![]));
}
if list_segment_ids(wal_dir, Some("wal"))?.is_empty() {
log::debug!("No WAL segments found, skipping recovery");
return Ok((None, vec![]));
}
let (first, last) = match get_segment_range(wal_dir, Some("wal")) {
Ok(range) => range,
Err(WalError::IO(ref io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => {
log::debug!("WAL segment range not found, skipping recovery");
return Ok((None, vec![]));
}
Err(e) => return Err(e.into()),
};
if first > last {
log::debug!("No valid WAL segment range, skipping recovery");
return Ok((None, vec![]));
}
let start_segment = std::cmp::max(first, min_wal_number);
if start_segment > last {
log::info!(
"All WAL segments already flushed (last={:020}, min_log_number={:020})",
last,
min_wal_number
);
return Ok((None, vec![]));
}
log::info!("Replaying WAL segments #{:020} to #{:020}", start_segment, last);
let mut max_seq_num: u64 = 0;
let mut last_added_max_seq: u64 = 0; let mut total_batches_replayed = 0;
let mut segments_processed = 0;
let mut memtables: Vec<(Arc<MemTable>, u64)> = Vec::new();
let all_segments = SegmentRef::read_segments_from_directory(wal_dir, Some("wal"))?;
for segment_id in start_segment..=last {
let segment = match all_segments.iter().find(|seg| seg.id == segment_id) {
Some(seg) => seg,
None => {
log::warn!(
"WAL segment #{:020} not found in range [{:020}..{:020}], skipping.",
segment_id,
start_segment,
last
);
continue;
}
};
log::debug!("Processing WAL segment #{:020}", segment_id);
let mut current_memtable = Arc::new(MemTable::new(arena_size, last_added_max_seq));
let file = File::open(&segment.file_path)?;
let reporter = Box::new(DefaultReporter::new(segment_id));
let mut reader = Reader::with_options(file, Some(reporter), segment_id);
let mut batches_in_segment = 0;
let mut last_valid_offset = 0;
loop {
match reader.read() {
Ok((record_data, offset)) => {
last_valid_offset = offset as usize;
let batch = Batch::decode(record_data)?;
let batch_highest_seq_num = batch.get_highest_seq_num();
if batch_highest_seq_num > max_seq_num {
max_seq_num = batch_highest_seq_num;
}
batches_in_segment += 1;
log::debug!(
"Replayed batch from WAL #{:020}: seq_num={}, entries={}, offset={}",
segment_id,
batch_highest_seq_num,
batch.count(),
offset
);
match current_memtable.add(&batch) {
Ok(()) => {
if batch_highest_seq_num > last_added_max_seq {
last_added_max_seq = batch_highest_seq_num;
}
}
Err(Error::ArenaFull) => {
if current_memtable.is_empty() {
return Err(Error::Other(format!(
"Batch too large for memtable (batch size exceeds arena_size={})",
arena_size
)));
}
log::warn!(
"WAL segment #{:020} exceeds single memtable capacity, splitting",
segment_id
);
memtables.push((Arc::clone(¤t_memtable), segment_id));
current_memtable =
Arc::new(MemTable::new(arena_size, last_added_max_seq));
current_memtable.add(&batch)?;
if batch_highest_seq_num > last_added_max_seq {
last_added_max_seq = batch_highest_seq_num;
}
}
Err(e) => return Err(e),
}
}
Err(WalError::Corruption(err)) => {
log::error!(
"Corrupted WAL record detected in segment {:020} at offset {}: {}",
segment_id,
last_valid_offset,
err
);
return Err(Error::wal_corruption(
segment_id as usize,
last_valid_offset,
format!("Corrupted WAL record: {}", err),
));
}
Err(WalError::IO(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
break; }
Err(err) => return Err(err.into()),
}
}
if !current_memtable.is_empty() {
memtables.push((current_memtable, segment_id));
}
if batches_in_segment > 0 {
log::info!(
"Replayed {} batches from WAL segment #{:020}",
batches_in_segment,
segment_id
);
total_batches_replayed += batches_in_segment;
segments_processed += 1;
}
}
let result = if max_seq_num > 0 {
Some(max_seq_num)
} else {
None
};
log::info!(
"WAL recovery complete: {} batches across {} segments, {} memtables created, max_seq_num={:?}",
total_batches_replayed,
segments_processed,
memtables.len(),
result
);
Ok((result, memtables))
}
pub(crate) fn repair_corrupted_wal_segment(wal_dir: &Path, segment_id: usize) -> Result<()> {
use std::fs;
use crate::wal::manager::Wal;
use crate::wal::reader::Reader;
use crate::wal::Options;
let segment_path = wal_dir.join(format!("{segment_id:020}.wal"));
if !segment_path.exists() {
return Err(crate::error::Error::Other(format!(
"WAL segment {segment_id:020}.wal does not exist"
)));
}
let repair_dir = wal_dir.join("repair_temp");
fs::create_dir_all(&repair_dir)?;
let opts = Options::default();
let mut repair_wal = Wal::open(&repair_dir, opts)?;
let file = fs::File::open(&segment_path)?;
let mut reader = Reader::new(file);
let mut valid_batches_count = 0;
let mut repair_failed = false;
loop {
match reader.read() {
Ok((record_data, _offset)) => {
if let Err(e) = repair_wal.append(record_data) {
log::error!("Failed to write valid batch to repaired WAL: {e}");
repair_failed = true;
break;
}
valid_batches_count += 1;
}
Err(WalError::Corruption(err)) => {
log::error!(
"Stopped repair at corruption: {err}. Recovered {valid_batches_count} valid batches."
);
break;
}
Err(WalError::IO(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
log::info!(
"Repair completed successfully. Recovered {valid_batches_count} valid batches."
);
break;
}
Err(err) => {
log::error!("Unexpected error during repair: {err}");
repair_failed = true;
break;
}
}
}
repair_wal.close()?;
let temp_wal_path = repair_dir.join("00000000000000000000.wal");
if repair_failed {
fs::remove_dir_all(&repair_dir).ok();
return Err(crate::error::Error::Other(format!(
"Failed to repair WAL segment {segment_id}. Original segment unchanged."
)));
}
if valid_batches_count == 0 {
fs::remove_file(&segment_path)?;
fs::remove_dir_all(&repair_dir).ok();
log::info!("Deleted corrupted WAL segment {segment_id:020}.wal (no valid data)");
return Ok(());
}
fs::rename(&temp_wal_path, &segment_path)?;
fs::remove_dir_all(&repair_dir).ok();
fsync_directory(wal_dir)?;
log::info!(
"Successfully repaired WAL segment {segment_id} with {valid_batches_count} valid batches."
);
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::{Seek, Write};
use tempfile::TempDir;
use test_log::test;
use super::*;
use crate::wal::manager::Wal;
use crate::wal::Options;
use crate::{LSMIterator, WalRecoveryMode};
#[test]
fn test_replay_wal_sequence_number_tracking() {
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);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap(); batch1.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap(); batch1.set(b"key3".to_vec(), b"value3".to_vec(), 0).unwrap();
let mut batch2 = Batch::new(200);
batch2.set(b"key4".to_vec(), b"value4".to_vec(), 0).unwrap(); batch2.set(b"key5".to_vec(), b"value5".to_vec(), 0).unwrap(); batch2.delete(b"key6".to_vec(), 0).unwrap(); batch2.set(b"key7".to_vec(), b"value7".to_vec(), 0).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
wal.rotate().unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024; let (max_seq_num_opt, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(
max_seq_num_opt,
Some(203),
"WAL recovery should track highest sequence number (203) across all segments"
);
assert_eq!(memtables.len(), 2, "Should create one memtable per WAL segment");
let mut entry_count = 0;
for (memtable, _) in memtables {
let mut iter = memtable.iter();
while iter.valid() {
entry_count += 1;
iter.next().unwrap();
}
}
assert_eq!(
entry_count, 7,
"Memtables should contain all 7 entries from both WAL segments (6 sets + 1 delete)"
);
}
#[test]
fn test_replay_wal_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
let arena_size = 1024 * 1024; let (max_seq_num_opt, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(max_seq_num_opt, None, "Empty WAL directory should return None");
assert_eq!(memtables.len(), 0, "Empty WAL directory should return no memtables");
}
#[test]
fn test_replay_wal_single_entry_batches() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let mut batch1 = Batch::new(500);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
let mut batch2 = Batch::new(600);
batch2.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
let mut batch3 = Batch::new(700);
batch3.set(b"key3".to_vec(), b"value3".to_vec(), 0).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
wal.rotate().unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.rotate().unwrap();
wal.append(&batch3.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024; let (max_seq_num_opt, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(
max_seq_num_opt,
Some(700),
"Should replay all 3 segments and return highest sequence number (700)"
);
assert_eq!(memtables.len(), 3, "Should create one memtable per WAL segment");
let mut entry_count = 0;
for (memtable, _) in memtables {
let mut iter = memtable.iter();
while iter.valid() {
entry_count += 1;
iter.next().unwrap();
}
}
assert_eq!(
entry_count, 3,
"Memtables should contain all 3 entries from all 3 WAL segments"
);
}
#[test]
fn test_replay_wal_multiple_batches() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let mut batch1 = Batch::new(200); batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap(); batch1.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
let mut batch2 = Batch::new(300); batch2.set(b"key3".to_vec(), b"value3".to_vec(), 0).unwrap(); batch2.set(b"key4".to_vec(), b"value4".to_vec(), 0).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
wal.rotate().unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024; let (max_seq_num_opt, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(
max_seq_num_opt,
Some(301),
"WAL recovery should track highest sequence number (301) across all segments"
);
assert_eq!(memtables.len(), 2, "Should create one memtable per WAL segment");
let mut entry_count = 0;
for (memtable, _) in memtables {
let mut iter = memtable.iter();
while iter.valid() {
entry_count += 1;
iter.next().unwrap();
}
}
assert_eq!(entry_count, 4, "Memtables should contain all 4 entries from both WAL segments");
}
#[test]
fn test_repair_corrupted_wal_segment_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let wal_dir = temp_dir.path();
fs::create_dir_all(wal_dir).unwrap();
let result = repair_corrupted_wal_segment(wal_dir, 999);
assert!(result.is_err(), "Should fail when segment doesn't exist");
}
#[test]
fn test_post_corruption_replay() {
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 batch1 = Batch::new(100);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
batch1.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
wal.close().unwrap();
let segment_path = wal_dir.join("00000000000000000000.wal");
let mut file =
std::fs::OpenOptions::new().read(true).write(true).open(&segment_path).unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
let record_type = 3u8;
let length = 10u16; let data = vec![0xFF; 10]; let crc = crate::wal::calculate_crc32(&[record_type], &data);
file.write_all(&[record_type]).unwrap(); file.write_all(&length.to_be_bytes()).unwrap(); file.write_all(&crc.to_be_bytes()).unwrap(); file.write_all(&data).unwrap(); drop(file);
let (max_seq_num, memtable_opt) = crate::lsm::Core::replay_wal_with_repair(
wal_dir,
0,
"Test repair",
WalRecoveryMode::TolerateCorruptedWithRepair,
1024,
|_memtable, _wal_number| {
Ok(())
},
)
.unwrap();
assert_eq!(max_seq_num, None, "Should have recovered None when first record is corrupted");
assert!(memtable_opt.is_none(), "Should have no memtable when first record is corrupted");
}
#[test]
fn test_repair_with_three_batches_corrupt_third() {
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 batch1 = Batch::new(100);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
batch1.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
let mut batch2 = Batch::new(200);
batch2.set(b"key3".to_vec(), b"value3".to_vec(), 0).unwrap();
batch2.set(b"key4".to_vec(), b"value4".to_vec(), 0).unwrap();
let mut batch3 = Batch::new(300);
batch3.set(b"key5".to_vec(), b"value5".to_vec(), 0).unwrap();
batch3.set(b"key6".to_vec(), b"value6".to_vec(), 0).unwrap();
let encoded1 = batch1.encode().unwrap();
let encoded2 = batch2.encode().unwrap();
let encoded3 = batch3.encode().unwrap();
wal.append(&encoded1).unwrap();
wal.append(&encoded2).unwrap();
wal.append(&encoded3).unwrap();
wal.close().unwrap();
let segment_path = wal_dir.join("00000000000000000000.wal");
let mut file =
std::fs::OpenOptions::new().read(true).write(true).open(&segment_path).unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
let batch1_size = encoded1.len() + 7; let batch2_size = encoded2.len() + 7; let third_batch_offset = batch1_size as u64 + batch2_size as u64;
file.seek(std::io::SeekFrom::Start(third_batch_offset)).unwrap();
let record_type = 3u8;
let length = 10u16;
let data = vec![0xFF; 10];
let crc = crate::wal::calculate_crc32(&[record_type], &data);
file.write_all(&[record_type]).unwrap();
file.write_all(&length.to_be_bytes()).unwrap(); file.write_all(&crc.to_be_bytes()).unwrap(); file.write_all(&data).unwrap(); drop(file);
let (max_seq_num, memtable_opt) = crate::lsm::Core::replay_wal_with_repair(
wal_dir,
0,
"Test repair",
WalRecoveryMode::TolerateCorruptedWithRepair,
1024,
|_memtable, _wal_number| {
Ok(())
},
)
.unwrap();
assert_eq!(
max_seq_num,
Some(201),
"Should have recovered sequence numbers from first two batches only"
);
if let Some(memtable) = memtable_opt {
let mut entry_count = 0;
let mut iter = memtable.iter();
while iter.valid() {
entry_count += 1;
iter.next().unwrap();
}
assert_eq!(entry_count, 4, "Should have recovered 4 entries from first two batches");
} else {
panic!("Should have recovered a memtable");
}
}
#[test]
fn test_recovery_mode_tolerate_tail_corruption() {
use crate::batch::Batch;
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 batch1 = Batch::new(100);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
let mut batch2 = Batch::new(200);
batch2.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
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_DATA_AT_END").unwrap();
let arena_size = 1024 * 1024; let result = replay_wal(wal_dir, 0, arena_size);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 0, "Should detect corruption in segment 0");
}
_ => panic!("Expected WalCorruption error"),
}
}
#[test]
fn test_recovery_mode_skip_corrupted() {
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 batch = Batch::new(100);
batch.set(b"key".to_vec(), b"value".to_vec(), 0).unwrap();
wal.append(&batch.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024; let result = replay_wal(wal_dir, 0, arena_size);
assert!(result.is_ok());
}
#[test]
fn test_default_reporter_usage() {
use crate::batch::Batch;
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 batch = Batch::new(100);
batch.set(b"key".to_vec(), b"value".to_vec(), 0).unwrap();
wal.append(&batch.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024; let (seq_num_opt, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(seq_num_opt, Some(100));
assert_eq!(memtables.len(), 1, "Should create one memtable for single segment");
}
#[test]
fn test_multi_segment_recovery_after_crash() {
use crate::batch::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);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
batch1.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
let opts = Options::default();
let mut wal = Wal::open(wal_dir, opts).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
wal.rotate().unwrap();
let mut batch2 = Batch::new(200);
batch2.set(b"key3".to_vec(), b"value3".to_vec(), 0).unwrap();
batch2.set(b"key4".to_vec(), b"value4".to_vec(), 0).unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024; let (max_seq_num_opt, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(
max_seq_num_opt,
Some(201),
"Should replay both WAL segments and return max seq from segment 1"
);
assert_eq!(memtables.len(), 2, "Should create one memtable per WAL segment");
let mut entry_count = 0;
for (memtable, _) in memtables {
let mut iter = memtable.iter();
while iter.valid() {
entry_count += 1;
iter.next().unwrap();
}
}
assert_eq!(
entry_count, 4,
"Should recover all 4 entries from both WAL segments (2 from each)"
);
}
#[test]
fn test_corruption_stops_at_segment_n() {
use crate::batch::Batch;
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 batch0 = Batch::new(100);
batch0.set(b"key0".to_vec(), b"value0".to_vec(), 0).unwrap();
wal.append(&batch0.encode().unwrap()).unwrap();
wal.rotate().unwrap();
let mut batch1 = Batch::new(200);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
let encoded1 = batch1.encode().unwrap();
wal.append(&encoded1).unwrap();
wal.rotate().unwrap();
let mut batch2 = Batch::new(300);
batch2.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
let segment1_path = wal_dir.join("00000000000000000001.wal");
let mut file = fs::OpenOptions::new().read(true).write(true).open(&segment1_path).unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
let record_type = 3u8; let length = 10u16;
let data = vec![0xFF; 10];
let crc = crate::wal::calculate_crc32(&[record_type], &data);
file.write_all(&[record_type]).unwrap();
file.write_all(&length.to_be_bytes()).unwrap();
file.write_all(&crc.to_be_bytes()).unwrap();
file.write_all(&data).unwrap();
drop(file);
let arena_size = 1024 * 1024; let result = replay_wal(wal_dir, 0, arena_size);
match result {
Err(Error::WalCorruption {
segment_id,
..
}) => {
assert_eq!(segment_id, 1, "Corruption should be reported in segment 1");
}
_ => panic!("Expected WalCorruption error in segment 1"),
}
}
#[test]
fn test_multi_wal_recovery_creates_multiple_memtables() {
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 batch0 = Batch::new(100);
batch0.set(b"key0".to_vec(), b"value0".to_vec(), 0).unwrap();
wal.append(&batch0.encode().unwrap()).unwrap();
wal.rotate().unwrap();
let mut batch1 = Batch::new(200);
batch1.set(b"key1".to_vec(), b"value1".to_vec(), 0).unwrap();
wal.append(&batch1.encode().unwrap()).unwrap();
wal.rotate().unwrap();
let mut batch2 = Batch::new(300);
batch2.set(b"key2".to_vec(), b"value2".to_vec(), 0).unwrap();
wal.append(&batch2.encode().unwrap()).unwrap();
wal.close().unwrap();
let arena_size = 1024 * 1024;
let (max_seq, memtables) = replay_wal(wal_dir, 0, arena_size).unwrap();
assert_eq!(max_seq, Some(300), "Max seq should be from last batch");
assert_eq!(memtables.len(), 3, "Should create one memtable per WAL segment");
assert_eq!(memtables[0].1, 0, "First memtable should have WAL 0");
assert_eq!(memtables[1].1, 1, "Second memtable should have WAL 1");
assert_eq!(memtables[2].1, 2, "Third memtable should have WAL 2");
{
let mut iter = memtables[0].0.iter();
iter.seek_first().unwrap();
let mut count = 0;
while iter.valid() {
count += 1;
if !iter.next().unwrap() {
break;
}
}
assert_eq!(count, 1);
}
{
let mut iter = memtables[1].0.iter();
iter.seek_first().unwrap();
let mut count = 0;
while iter.valid() {
count += 1;
if !iter.next().unwrap() {
break;
}
}
assert_eq!(count, 1);
}
{
let mut iter = memtables[2].0.iter();
iter.seek_first().unwrap();
let mut count = 0;
while iter.valid() {
count += 1;
if !iter.next().unwrap() {
break;
}
}
assert_eq!(count, 1);
}
}
}