use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::vec::Vec;
use crate::wal::{
calculate_crc32,
validate_record_type,
CompressionType,
CorruptionError,
Error,
IOError,
RecordType,
Result,
BLOCK_SIZE,
HEADER_SIZE as WAL_RECORD_HEADER_SIZE,
};
pub trait Reporter {
fn corruption(&mut self, bytes: usize, reason: &str, log_number: u64);
fn old_log_record(&mut self, bytes: usize);
}
pub(crate) struct Reader {
file: File,
buffer: Vec<u8>,
buffer_offset: usize,
end_of_buffer_offset: usize,
eof: bool,
read_error: bool,
rec: Vec<u8>,
cur_rec_type: RecordType,
err: Option<Error>,
reporter: Option<Box<dyn Reporter>>,
log_number: u64,
compression_type: CompressionType,
compression_type_record_read: bool,
}
impl Reader {
pub(crate) fn new(file: File) -> Self {
Self::with_options(file, None, 0)
}
#[allow(dead_code)]
pub(crate) fn with_options(
file: File,
reporter: Option<Box<dyn Reporter>>,
log_number: u64,
) -> Self {
Reader {
file,
buffer: Vec::with_capacity(BLOCK_SIZE),
buffer_offset: 0,
end_of_buffer_offset: 0,
eof: false,
read_error: false,
rec: Vec::new(),
cur_rec_type: RecordType::Empty,
err: None,
reporter,
log_number,
compression_type: CompressionType::None,
compression_type_record_read: false,
}
}
#[cfg(test)]
pub(crate) fn get_compression_type(&self) -> CompressionType {
self.compression_type
}
#[allow(dead_code)]
fn report_corruption(&mut self, bytes: usize, reason: &str) {
if let Some(ref mut reporter) = self.reporter {
reporter.corruption(bytes, reason, self.log_number);
}
}
#[allow(dead_code)]
fn report_old_log_record(&mut self, bytes: usize) {
if let Some(ref mut reporter) = self.reporter {
reporter.old_log_record(bytes);
}
}
fn buffer_remaining(&self) -> usize {
self.buffer.len().saturating_sub(self.buffer_offset)
}
fn read_more(&mut self) -> Result<bool> {
if self.eof || self.read_error {
return Ok(false);
}
self.buffer.clear();
self.buffer.resize(BLOCK_SIZE, 0);
match self.file.read(&mut self.buffer) {
Ok(0) => {
self.eof = true;
self.buffer.clear();
Ok(false)
}
Ok(n) => {
self.buffer.truncate(n);
self.buffer_offset = 0;
self.end_of_buffer_offset += n;
if n < BLOCK_SIZE {
self.eof = true;
}
Ok(true)
}
Err(e) => {
self.read_error = true;
self.buffer.clear();
Err(Error::IO(IOError::new(e.kind(), &e.to_string())))
}
}
}
fn parse_header(&mut self) -> (u32, u16, u8) {
let header = &self.buffer[self.buffer_offset..self.buffer_offset + WAL_RECORD_HEADER_SIZE];
let crc = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
let length = u16::from_be_bytes([header[4], header[5]]);
let record_type = header[6];
self.buffer_offset += WAL_RECORD_HEADER_SIZE;
(crc, length, record_type)
}
pub(crate) fn read(&mut self) -> Result<(&[u8], u64)> {
if let Some(err) = &self.err {
return Err(err.clone());
}
match self.next() {
Ok(_) => {
let offset = self.end_of_buffer_offset - self.buffer_remaining();
Ok((&self.rec, offset as u64))
}
Err(e) => {
if let Error::IO(io_err) = &e {
if io_err.kind() == io::ErrorKind::UnexpectedEof {
self.err = Some(e.clone());
return Err(e);
}
}
let error_offset = self.end_of_buffer_offset - self.buffer_remaining();
let file_size = self.file.seek(SeekFrom::End(0)).unwrap_or(0);
let block_number = error_offset / BLOCK_SIZE;
let offset_within_block = error_offset % BLOCK_SIZE;
log::error!(
"WAL corruption detected: \
log_number={}, \
error_offset={} (0x{:X}), \
file_size={} bytes, \
block_number={}, \
offset_within_block={}, \
buffer_offset={}, \
buffer_len={}, \
end_of_buffer_offset={}, \
eof={}, \
compression={:?}, \
accumulated_record_len={}, \
current_record_type={:?}, \
error=\"{}\"",
self.log_number,
error_offset,
error_offset,
file_size,
block_number,
offset_within_block,
self.buffer_offset,
self.buffer.len(),
self.end_of_buffer_offset,
self.eof,
self.compression_type,
self.rec.len(),
self.cur_rec_type,
e
);
let corruption_err = Error::Corruption(CorruptionError::new(
io::ErrorKind::Other,
e.to_string().as_str(),
self.log_number,
error_offset as u64,
));
self.report_corruption(self.rec.len(), &e.to_string());
self.err = Some(corruption_err.clone());
Err(corruption_err)
}
}
}
fn next(&mut self) -> Result<()> {
self.rec.clear();
let mut fragment_index = 0;
loop {
if self.buffer_remaining() < WAL_RECORD_HEADER_SIZE {
if !self.read_more()? {
return Err(Error::IO(IOError::new(
io::ErrorKind::UnexpectedEof,
"reached end of file",
)));
}
continue;
}
let (crc, length, type_byte) = self.parse_header();
self.cur_rec_type = RecordType::from_u8(type_byte)?;
if self.cur_rec_type == RecordType::Empty {
let remaining = self.buffer_remaining();
if remaining > 0 {
let zeros = &self.buffer[self.buffer_offset..self.buffer_offset + remaining];
if !zeros.iter().all(|&c| c == 0) {
return Err(Error::IO(IOError::new(
io::ErrorKind::Other,
"non-zero byte in padding area",
)));
}
}
self.buffer_offset = self.buffer.len();
continue;
}
if self.cur_rec_type == RecordType::SetCompressionType {
if (length as usize) > self.buffer_remaining() {
return Err(Error::IO(IOError::new(
io::ErrorKind::Other,
"truncated compression type record",
)));
}
if length > 0 {
let compression_byte = self.buffer[self.buffer_offset];
self.buffer_offset += length as usize;
self.compression_type = CompressionType::from_u8(compression_byte)?;
self.compression_type_record_read = true;
}
continue; }
validate_record_type(self.cur_rec_type, fragment_index)?;
if (length as usize) > self.buffer_remaining() {
return Err(Error::IO(IOError::new(
io::ErrorKind::Other,
"bad record length - exceeds block boundary",
)));
}
let record_data =
&self.buffer[self.buffer_offset..self.buffer_offset + length as usize];
let calculated_crc = calculate_crc32(&[type_byte], record_data);
if calculated_crc != crc {
return Err(Error::IO(IOError::new(io::ErrorKind::Other, "checksum mismatch")));
}
self.rec.extend_from_slice(record_data);
self.buffer_offset += length as usize;
if self.cur_rec_type == RecordType::Last || self.cur_rec_type == RecordType::Full {
break;
}
fragment_index += 1;
}
if self.compression_type == CompressionType::Lz4 && !self.rec.is_empty() {
self.rec = lz4_flex::decompress_size_prepended(&self.rec).map_err(|e| {
Error::IO(IOError::new(
io::ErrorKind::InvalidData,
&format!("LZ4 decompression failed: {}", e),
))
})?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::{BufReader, Read, Write};
use std::vec::Vec;
use tempdir::TempDir;
use test_log::test;
use super::*;
use crate::wal::manager::Wal;
use crate::wal::{Options, SegmentRef};
#[test]
fn bufreader_eof_and_error() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&file_path).expect("should create file");
file.write_all(b"Hello, World!").expect("should write data");
drop(file);
let file = File::open(&file_path).expect("should open file");
let mut buf_reader = BufReader::new(file);
let mut read_buffer = [0u8; 5];
let bytes_read = buf_reader.read(&mut read_buffer).expect("should read");
assert_eq!(&read_buffer[..bytes_read], b"Hello");
let mut read_buffer = [0u8; 10];
let bytes_read = buf_reader.read(&mut read_buffer).expect("should read");
assert_eq!(bytes_read, 8); assert_eq!(&read_buffer[..bytes_read], b", World!");
let mut read_buffer = [0u8; 1000];
let bytes_read = buf_reader.read(&mut read_buffer).expect("should read");
assert_eq!(bytes_read, 0); }
#[test]
fn reader_basic() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(&[1, 2, 3, 4]).expect("should append");
wal.append(&[5, 6]).expect("should append");
wal.append(&[7, 8, 9]).expect("should append");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read");
assert_eq!(data, vec![1, 2, 3, 4]);
let (data, _) = reader.read().expect("should read");
assert_eq!(data, vec![5, 6]);
let (data, _) = reader.read().expect("should read");
assert_eq!(data, vec![7, 8, 9]);
}
#[test]
fn reader_with_large_data() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default().with_max_file_size(4096 * 10);
let mut a = Wal::open(temp_dir.path(), opts).expect("should create aol");
let record_size = 4;
let num_records = 10000;
for _ in 0..num_records {
let data: Vec<u8> = (0..record_size).map(|i| (i & 0xFF) as u8).collect();
let r = a.append(&data);
assert!(r.is_ok());
}
a.sync().expect("should sync");
a.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let mut i = 0;
for segment in segments {
let file = File::open(&segment.file_path).expect("should open file");
let mut reader = Reader::new(file);
while let Ok((data, _)) = reader.read() {
assert_eq!(data, vec![0, 1, 2, 3]);
i += 1;
}
}
assert_eq!(i, num_records);
}
#[test]
fn reader_block_boundary_padding() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let record_size = 68;
let records_to_write = 1000;
for i in 0..records_to_write {
let data: Vec<u8> = (0..record_size).map(|j| ((i + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let mut count = 0;
while let Ok((data, _)) = reader.read() {
assert_eq!(data.len(), record_size, "Record {} has wrong size", count);
count += 1;
}
assert_eq!(count, records_to_write, "Should read all {} records", records_to_write);
}
#[test]
fn reader_record_spanning_blocks() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let large_record_size = BLOCK_SIZE * 2 + 1000;
let large_data: Vec<u8> = (0..large_record_size).map(|i| (i & 0xFF) as u8).collect();
wal.append(&large_data).expect("should append large record");
wal.append(&[1, 2, 3, 4]).expect("should append small record");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read large record");
assert_eq!(data.len(), large_record_size);
assert_eq!(data, large_data);
let (data, _) = reader.read().expect("should read small record");
assert_eq!(data, vec![1, 2, 3, 4]);
}
#[test]
fn reader_offset_from_first_read() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"foo").expect("should append");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, offset) = reader.read().expect("should read");
assert_eq!(data, b"foo");
assert_eq!(
offset,
(WAL_RECORD_HEADER_SIZE + 3) as u64,
"First read offset should be precise"
);
}
#[test]
fn reader_offsets_increasing() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"aaa").expect("should append"); wal.append(b"bbbbb").expect("should append"); wal.append(b"c").expect("should append"); wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (_, offset1) = reader.read().expect("should read 1");
let (_, offset2) = reader.read().expect("should read 2");
let (_, offset3) = reader.read().expect("should read 3");
assert_eq!(offset1, 10, "First record ends at 10");
assert_eq!(offset2, 22, "Second record ends at 22");
assert_eq!(offset3, 30, "Third record ends at 30");
assert!(offset1 < offset2, "Offsets must increase");
assert!(offset2 < offset3, "Offsets must increase");
}
#[test]
fn reader_offset_partial_block() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let record_size = 100;
for i in 0..5 {
let data: Vec<u8> = (0..record_size).map(|j| ((i + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let mut last_offset = 0u64;
let mut count = 0;
while let Ok((data, offset)) = reader.read() {
assert_eq!(data.len(), record_size);
assert!(offset > last_offset, "Offset {} should be > previous {}", offset, last_offset);
let expected = ((count + 1) * (WAL_RECORD_HEADER_SIZE + record_size)) as u64;
assert_eq!(offset, expected, "Record {} offset mismatch", count);
last_offset = offset;
count += 1;
}
assert_eq!(count, 5);
}
#[test]
fn reader_corrupted_compressed_data() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let compression_type = 5u8; let compression_data = [1u8]; let crc1 = calc_crc(compression_type, &compression_data);
write_raw_record(&mut file, crc1, 1, compression_type, &compression_data);
let record_type = 1u8; let garbage_compressed = [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]; let crc2 = calc_crc(record_type, &garbage_compressed);
write_raw_record(
&mut file,
crc2,
garbage_compressed.len() as u16,
record_type,
&garbage_compressed,
);
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Should detect corrupted compressed data");
}
#[test]
fn writer_block_boundary_exact_fit() {
use crate::wal::{BufferedFileWriter, CompressionType};
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
let file = File::create(&file_path).expect("should create file");
let buffered_writer = BufferedFileWriter::new(file, BLOCK_SIZE);
let mut writer =
crate::wal::writer::Writer::new(buffered_writer, false, CompressionType::None, 0);
let exact_fit_size = BLOCK_SIZE - WAL_RECORD_HEADER_SIZE;
let data = vec![b'X'; exact_fit_size];
writer.add_record(&data).expect("should write exact fit record");
writer.add_record(b"next").expect("should write next record");
writer.close().expect("should close");
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let (read_data, offset1) = reader.read().expect("should read first");
assert_eq!(read_data.len(), exact_fit_size);
assert_eq!(offset1, BLOCK_SIZE as u64, "First record fills entire block");
let (read_data, offset2) = reader.read().expect("should read second");
assert_eq!(read_data, b"next");
assert_eq!(offset2, (BLOCK_SIZE + WAL_RECORD_HEADER_SIZE + 4) as u64);
}
fn write_raw_record(file: &mut File, crc: u32, length: u16, record_type: u8, data: &[u8]) {
file.write_all(&crc.to_be_bytes()).unwrap();
file.write_all(&length.to_be_bytes()).unwrap();
file.write_all(&[record_type]).unwrap();
file.write_all(data).unwrap();
}
fn calc_crc(record_type: u8, data: &[u8]) -> u32 {
use crate::wal::calculate_crc32;
calculate_crc32(&[record_type], data)
}
fn big_string(partial: &str, n: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(n);
let partial_bytes = partial.as_bytes();
while result.len() < n {
let remaining = n - result.len();
let to_copy = std::cmp::min(remaining, partial_bytes.len());
result.extend_from_slice(&partial_bytes[..to_copy]);
}
result
}
fn number_string(n: i32) -> String {
format!("{}.", n)
}
fn random_skewed_string(i: i32, max_len: usize) -> Vec<u8> {
let skewed_len = ((i as usize * 17) % max_len).max(1);
big_string(&number_string(i), skewed_len)
}
fn set_byte(data: &mut [u8], offset: usize, value: u8) {
data[offset] = value;
}
fn fix_checksum(data: &mut [u8], header_offset: usize, payload_len: usize) {
use crate::wal::calculate_crc32;
let type_byte = data[header_offset + 6];
let payload_start = header_offset + WAL_RECORD_HEADER_SIZE;
let payload_end = payload_start + payload_len;
let crc = calculate_crc32(&[type_byte], &data[payload_start..payload_end]);
data[header_offset..header_offset + 4].copy_from_slice(&crc.to_be_bytes());
}
#[test]
fn empty() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
File::create(&file_path).expect("should create file");
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Empty WAL should return EOF");
}
#[test]
fn read_write_multiple_records() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"foo").expect("should append foo");
wal.append(b"bar").expect("should append bar");
wal.append(b"baz").expect("should append baz");
wal.append(b"xxxx").expect("should append xxxx");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, b"foo");
let (data, _) = reader.read().expect("should read bar");
assert_eq!(data, b"bar");
let (data, _) = reader.read().expect("should read baz");
assert_eq!(data, b"baz");
let (data, _) = reader.read().expect("should read xxxx");
assert_eq!(data, b"xxxx");
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn many_blocks() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let num_records = 10_000;
for i in 0..num_records {
let data = number_string(i);
wal.append(data.as_bytes()).expect("should append");
}
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
for i in 0..num_records {
let expected = number_string(i);
let (data, _) = reader.read().unwrap_or_else(|_| panic!("should read record {}", i));
assert_eq!(data, expected.as_bytes(), "Record {} mismatch", i);
}
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn fragmentation() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let small = b"small".to_vec();
let medium = big_string("medium", 50000);
let large = big_string("large", 100000);
wal.append(&small).expect("should append small");
wal.append(&medium).expect("should append medium");
wal.append(&large).expect("should append large");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read small");
assert_eq!(data, small);
let (data, _) = reader.read().expect("should read medium");
assert_eq!(data, medium);
let (data, _) = reader.read().expect("should read large");
assert_eq!(data, large);
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn random_read() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let num_records = 500;
let max_len = 1 << 17;
let mut expected_records = Vec::new();
for i in 0..num_records {
let data = random_skewed_string(i, max_len);
expected_records.push(data.clone());
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
for (i, expected) in expected_records.iter().enumerate() {
let (data, _) = reader.read().unwrap_or_else(|_| panic!("should read record {}", i));
assert_eq!(data, expected, "Record {} mismatch", i);
}
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn marginal_trailer() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let n = BLOCK_SIZE - 2 * WAL_RECORD_HEADER_SIZE;
let foo_data = big_string("foo", n);
wal.append(&foo_data).expect("should append foo");
wal.append(b"x").expect("should append small"); wal.append(b"bar").expect("should append bar");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, foo_data);
let (data, _) = reader.read().expect("should read small");
assert_eq!(data, b"x");
let (data, _) = reader.read().expect("should read bar");
assert_eq!(data, b"bar");
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn marginal_trailer2() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let n = BLOCK_SIZE - 2 * WAL_RECORD_HEADER_SIZE;
let foo_data = big_string("foo", n);
wal.append(&foo_data).expect("should append foo");
wal.append(b"bar").expect("should append bar");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, foo_data);
let (data, _) = reader.read().expect("should read bar");
assert_eq!(data, b"bar");
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn short_trailer() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let n = BLOCK_SIZE - 2 * WAL_RECORD_HEADER_SIZE + 4;
let foo_data = big_string("foo", n);
wal.append(&foo_data).expect("should append foo");
wal.append(b"x").expect("should append small"); wal.append(b"bar").expect("should append bar");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, foo_data);
let (data, _) = reader.read().expect("should read small");
assert_eq!(data, b"x");
let (data, _) = reader.read().expect("should read bar");
assert_eq!(data, b"bar");
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn aligned_eof() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let n = BLOCK_SIZE - 2 * WAL_RECORD_HEADER_SIZE + 4;
let foo_data = big_string("foo", n);
wal.append(&foo_data).expect("should append foo");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, foo_data);
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn bad_record_type() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let data = b"foo";
let invalid_type = 100u8; let crc = calc_crc(invalid_type, data);
write_raw_record(&mut file, crc, data.len() as u16, invalid_type, data);
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Should detect bad record type");
}
#[test]
fn truncated_trailing_record() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let data = b"foo";
let record_type = 1u8; let crc = calc_crc(record_type, data);
write_raw_record(&mut file, crc, data.len() as u16, record_type, data);
file.write_all(&[0u8; 3]).unwrap();
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, b"foo");
assert!(reader.read().is_err(), "Should return EOF on truncated record");
}
#[test]
fn bad_length() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let data = b"foo";
let record_type = 1u8; let crc = calc_crc(record_type, data);
write_raw_record(&mut file, crc, 100, record_type, data);
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Should detect bad length");
}
#[test]
fn checksum_mismatch() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let data = b"foooooo";
let record_type = 1u8; let crc = calc_crc(record_type, data);
let bad_crc = crc.wrapping_add(14);
write_raw_record(&mut file, bad_crc, data.len() as u16, record_type, data);
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Should detect checksum mismatch");
}
#[test]
fn error_joins_records() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let foo_data = big_string("foo", BLOCK_SIZE);
let bar_data = big_string("bar", BLOCK_SIZE);
wal.append(&foo_data).expect("should append foo");
wal.append(&bar_data).expect("should append bar");
wal.append(b"correct").expect("should append correct");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file_path = &segments[0].file_path;
let mut data = std::fs::read(file_path).expect("should read file");
for offset in BLOCK_SIZE..(2 * BLOCK_SIZE) {
if offset < data.len() {
data[offset] = b'x';
}
}
std::fs::write(file_path, &data).expect("should write file");
let file = File::open(file_path).expect("should open file");
let mut reader = Reader::new(file);
let mut records_read: Vec<Vec<u8>> = Vec::new();
while let Ok((data, _)) = reader.read() {
records_read.push(data.to_vec());
}
for (i, record) in records_read.iter().enumerate() {
let has_foo_pattern = record.windows(3).any(|w| w == b"foo");
let has_bar_pattern = record.windows(3).any(|w| w == b"bar");
let is_foo_record = record.len() == BLOCK_SIZE && has_foo_pattern && !has_bar_pattern;
let is_bar_record = record.len() == BLOCK_SIZE && has_bar_pattern && !has_foo_pattern;
let is_correct_record = record == b"correct";
assert!(
!(has_foo_pattern && has_bar_pattern),
"BUG: Record {} appears to be a corrupted join of foo+bar fragments (len={})",
i,
record.len()
);
assert!(
is_foo_record || is_bar_record || is_correct_record || record.is_empty(),
"Record {} is unexpected data (len={}, has_foo={}, has_bar={})",
i,
record.len(),
has_foo_pattern,
has_bar_pattern
);
}
let found_correct = records_read.iter().any(|r| r == b"correct");
eprintln!(
"error_joins_records: Read {} records, found_correct={}",
records_read.len(),
found_correct
);
}
#[test]
fn unexpected_middle_type() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let before_data = b"before";
let full_type = 1u8;
let crc1 = calc_crc(full_type, before_data);
write_raw_record(&mut file, crc1, before_data.len() as u16, full_type, before_data);
let middle_data = b"invalid_middle";
let middle_type = 3u8;
let crc2 = calc_crc(middle_type, middle_data);
write_raw_record(&mut file, crc2, middle_data.len() as u16, middle_type, middle_data);
let after_data = b"after";
let crc3 = calc_crc(full_type, after_data);
write_raw_record(&mut file, crc3, after_data.len() as u16, full_type, after_data);
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read first record");
assert_eq!(data, b"before", "First record should be 'before'");
let result = reader.read();
assert!(result.is_err(), "Should detect unexpected Middle type");
}
#[test]
fn unexpected_last_type() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let file_path = temp_dir.path().join("test.wal");
{
let mut file = File::create(&file_path).expect("should create file");
let before_data = b"before";
let full_type = 1u8;
let crc1 = calc_crc(full_type, before_data);
write_raw_record(&mut file, crc1, before_data.len() as u16, full_type, before_data);
let last_data = b"invalid_last";
let last_type = 4u8;
let crc2 = calc_crc(last_type, last_data);
write_raw_record(&mut file, crc2, last_data.len() as u16, last_type, last_data);
let after_data = b"after";
let crc3 = calc_crc(full_type, after_data);
write_raw_record(&mut file, crc3, after_data.len() as u16, full_type, after_data);
}
let file = File::open(&file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read first record");
assert_eq!(data, b"before", "First record should be 'before'");
let result = reader.read();
assert!(result.is_err(), "Should detect unexpected Last type");
}
#[test]
fn unexpected_full_type() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"corrupted").expect("should append corrupted");
wal.append(b"second").expect("should append second");
wal.append(b"third").expect("should append third");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file_path = &segments[0].file_path;
let mut data = std::fs::read(file_path).expect("should read file");
let first_type = 2u8; set_byte(&mut data, 6, first_type);
fix_checksum(&mut data, 0, 9); std::fs::write(file_path, &data).expect("should write file");
let file = File::open(file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Should detect unexpected Full after First");
}
#[test]
fn missing_last() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"complete_record").expect("should append first");
let large_data = big_string("bar", BLOCK_SIZE * 2);
wal.append(&large_data).expect("should append large");
wal.append(b"after_large").expect("should append after");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file_path = &segments[0].file_path;
let file_data = std::fs::read(file_path).expect("should read file");
let truncated_len = BLOCK_SIZE + BLOCK_SIZE / 2; let truncated_len = truncated_len.min(file_data.len() - 100); std::fs::write(file_path, &file_data[..truncated_len]).expect("should write file");
let file = File::open(file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read first complete record");
assert_eq!(data, b"complete_record", "First record should be readable");
let result = reader.read();
assert!(result.is_err(), "Should return error for missing Last fragment");
}
#[test]
fn partial_last() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"complete_record").expect("should append first");
let large_data = big_string("bar", BLOCK_SIZE * 2);
wal.append(&large_data).expect("should append large");
wal.append(b"after_large").expect("should append after");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file_path = &segments[0].file_path;
let file_data = std::fs::read(file_path).expect("should read file");
let original_len = file_data.len();
let truncated_len = original_len.saturating_sub(20);
std::fs::write(file_path, &file_data[..truncated_len]).expect("should write file");
let file = File::open(file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read first complete record");
assert_eq!(data, b"complete_record", "First record should be readable");
let result = reader.read();
match result {
Ok((data, _)) => {
assert_eq!(data, large_data, "Large record should match if complete");
let result3 = reader.read();
assert!(result3.is_err(), "Should fail after truncated point");
}
Err(_) => {
}
}
}
#[test]
fn compression_empty() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default().with_compression(CompressionType::Lz4);
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let result = reader.read();
assert!(result.is_err(), "Empty compressed WAL should return EOF");
}
#[test]
fn compression_read_write() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default().with_compression(CompressionType::Lz4);
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
wal.append(b"foo").expect("should append foo");
wal.append(b"bar").expect("should append bar");
wal.append(b"baz").expect("should append baz");
wal.append(b"xxxx").expect("should append xxxx");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read foo");
assert_eq!(data, b"foo");
let (data, _) = reader.read().expect("should read bar");
assert_eq!(data, b"bar");
let (data, _) = reader.read().expect("should read baz");
assert_eq!(data, b"baz");
let (data, _) = reader.read().expect("should read xxxx");
assert_eq!(data, b"xxxx");
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn compression_fragmentation() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default().with_compression(CompressionType::Lz4);
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let small = b"small".to_vec();
let medium = big_string("medium", 3 * BLOCK_SIZE / 2); let large = big_string("large", 3 * BLOCK_SIZE);
wal.append(&small).expect("should append small");
wal.append(&medium).expect("should append medium");
wal.append(&large).expect("should append large");
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let (data, _) = reader.read().expect("should read small");
assert_eq!(data, small);
let (data, _) = reader.read().expect("should read medium");
assert_eq!(data, medium);
let (data, _) = reader.read().expect("should read large");
assert_eq!(data, large);
assert!(reader.read().is_err(), "Should return EOF");
}
#[test]
fn multi_session_block_boundary_stress() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let record_size = 68;
let records_per_session = 100;
let num_sessions = 10;
for session in 0..num_sessions {
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
for i in 0..records_per_session {
let idx = session * records_per_session + i;
let data: Vec<u8> = (0..record_size).map(|j| ((idx + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
}
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let mut count = 0;
while let Ok((data, offset)) = reader.read() {
assert_eq!(
data.len(),
record_size,
"FALSE POSITIVE: Record {} has wrong size at offset {} (expected {}, got {})",
count,
offset,
record_size,
data.len()
);
count += 1;
}
assert_eq!(
count,
num_sessions * records_per_session,
"Should read ALL {} records without false corruption (got {})",
num_sessions * records_per_session,
count
);
}
#[test]
fn original_crud_bench_bug_reproduction() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let record_size = 68;
let target_file_size = 5 * 1024 * 1024; let records_per_session = 1000;
let mut total_records = 0;
let wal_path = temp_dir.path().join("00000000000000000000.wal");
loop {
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
for i in 0..records_per_session {
let data: Vec<u8> =
(0..record_size).map(|j| ((total_records + i + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
total_records += records_per_session;
let file_size = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
if file_size >= target_file_size as u64 {
break;
}
}
let file = File::open(&wal_path).expect("should open");
let mut reader = Reader::new(file);
let mut count = 0;
let mut last_offset = 0u64;
while let Ok((data, offset)) = reader.read() {
assert_eq!(
data.len(),
record_size,
"FALSE POSITIVE at record {} offset {} (last good: {})",
count,
offset,
last_offset
);
last_offset = offset;
count += 1;
}
assert_eq!(
count, total_records,
"Should read ALL {} records without false corruption (got {})",
total_records, count
);
}
#[test]
fn all_block_boundary_remainders() {
for target_remainder in 0..WAL_RECORD_HEADER_SIZE {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let base_record_size = 100;
let record_plus_header = WAL_RECORD_HEADER_SIZE + base_record_size;
let num_records = (BLOCK_SIZE * 3) / record_plus_header + 10;
let mut expected_data = Vec::new();
for i in 0..num_records {
let size = base_record_size + (i % 7);
let data: Vec<u8> = (0..size).map(|j| ((i + j) & 0xFF) as u8).collect();
expected_data.push(data.clone());
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
for (i, expected) in expected_data.iter().enumerate() {
let result = reader.read();
assert!(
result.is_ok(),
"FALSE POSITIVE: Record {} failed for remainder test {} - {:?}",
i,
target_remainder,
result.err()
);
let (data, _) = result.unwrap();
assert_eq!(
data, expected,
"Record {} data mismatch for remainder {}",
i, target_remainder
);
}
}
}
#[test]
fn fuzz_random_record_sizes_no_false_corruption() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let mut seed: u64 = 12345;
let mut next_random = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
seed
};
let mut expected_records = Vec::new();
for i in 0..500 {
let size = (next_random() % 50_000) as usize + 1;
let data: Vec<u8> = (0..size).map(|j| ((i + j) & 0xFF) as u8).collect();
expected_records.push(data.clone());
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
for (i, expected) in expected_records.iter().enumerate() {
let result = reader.read();
assert!(
result.is_ok(),
"FALSE POSITIVE: Record {} (size {}) should not be marked corrupt",
i,
expected.len()
);
let (data, _) = result.unwrap();
assert_eq!(data.len(), expected.len(), "Record {} size mismatch", i);
assert_eq!(data, expected, "Record {} data mismatch", i);
}
}
#[test]
fn mixed_fragmented_and_small_records() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let mut expected_records = Vec::new();
for session in 0..5 {
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let large_size = BLOCK_SIZE + 5000;
let large_data: Vec<u8> =
(0..large_size).map(|j| ((session * 1000 + j) & 0xFF) as u8).collect();
expected_records.push(large_data.clone());
wal.append(&large_data).expect("should append large");
for i in 0..10 {
let small_size = 50 + i * 10;
let small_data: Vec<u8> =
(0..small_size).map(|j| ((session * 100 + i + j) & 0xFF) as u8).collect();
expected_records.push(small_data.clone());
wal.append(&small_data).expect("should append small");
}
wal.close().expect("should close");
}
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
for (i, expected) in expected_records.iter().enumerate() {
let result = reader.read();
assert!(
result.is_ok(),
"FALSE POSITIVE: Record {} (size {}) failed - {:?}",
i,
expected.len(),
result.err()
);
let (data, _) = result.unwrap();
assert_eq!(data, expected, "Record {} mismatch", i);
}
}
#[test]
fn exact_block_alignment_stress() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let exact_fit_size = BLOCK_SIZE - WAL_RECORD_HEADER_SIZE;
let mut expected_records = Vec::new();
for session in 0..5 {
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let data: Vec<u8> = (0..exact_fit_size).map(|j| ((session + j) & 0xFF) as u8).collect();
expected_records.push(data.clone());
wal.append(&data).expect("should append exact fit");
let small_data: Vec<u8> = (0..100).map(|j| ((session * 10 + j) & 0xFF) as u8).collect();
expected_records.push(small_data.clone());
wal.append(&small_data).expect("should append small");
wal.close().expect("should close");
}
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
for (i, expected) in expected_records.iter().enumerate() {
let result = reader.read();
assert!(
result.is_ok(),
"FALSE POSITIVE: Record {} (size {}) failed at block alignment - {:?}",
i,
expected.len(),
result.err()
);
let (data, _) = result.unwrap();
assert_eq!(data, expected, "Record {} mismatch", i);
}
}
#[test]
fn compression_block_boundary_no_false_corruption() {
let temp_dir = TempDir::new("test").expect("should create temp dir");
let record_size = 68;
let records_per_session = 100;
let num_sessions = 5;
for session in 0..num_sessions {
let opts = Options::default().with_compression(CompressionType::Lz4);
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
for i in 0..records_per_session {
let idx = session * records_per_session + i;
let data: Vec<u8> = (0..record_size).map(|j| ((idx + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
}
wal.close().expect("should close");
}
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
let mut count = 0;
while let Ok((data, offset)) = reader.read() {
assert_eq!(
data.len(),
record_size,
"FALSE POSITIVE with compression: Record {} has wrong size at offset {}",
count,
offset
);
count += 1;
}
assert_eq!(
count,
num_sessions * records_per_session,
"Should read ALL {} compressed records without false corruption",
num_sessions * records_per_session
);
}
#[test]
fn resume_at_all_block_offsets() {
let test_offsets = [
0,
1,
6,
7,
8,
100,
1000,
BLOCK_SIZE / 4,
BLOCK_SIZE / 2,
BLOCK_SIZE - 100,
BLOCK_SIZE - 8,
BLOCK_SIZE - 7,
BLOCK_SIZE - 6,
BLOCK_SIZE - 1,
];
for &target_offset in &test_offsets {
let temp_dir = TempDir::new("test").expect("should create temp dir");
{
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
let mut current_offset = 0usize;
let mut record_num = 0;
while current_offset < target_offset.saturating_sub(100) {
let record_size = 50;
let data: Vec<u8> =
(0..record_size).map(|j| ((record_num + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
current_offset += WAL_RECORD_HEADER_SIZE + record_size;
record_num += 1;
}
wal.close().expect("should close");
}
let expected_second_session: Vec<Vec<u8>>;
{
let opts = Options::default();
let mut wal = Wal::open(temp_dir.path(), opts).expect("should create WAL");
expected_second_session = (0..10)
.map(|i| {
let data: Vec<u8> =
(0..68).map(|j| ((1000 + i + j) & 0xFF) as u8).collect();
wal.append(&data).expect("should append");
data
})
.collect();
wal.close().expect("should close");
}
let segments = SegmentRef::read_segments_from_directory(temp_dir.path(), Some("wal"))
.expect("should read segments");
let file = File::open(&segments[0].file_path).expect("should open file");
let mut reader = Reader::new(file);
while let Ok((data, _)) = reader.read() {
if data.len() == 68 && data[0] == ((1000) & 0xFF) as u8 {
for (i, expected) in expected_second_session.iter().skip(1).enumerate() {
let result = reader.read();
assert!(
result.is_ok(),
"FALSE POSITIVE at offset {}: Second session record {} failed",
target_offset,
i + 1
);
let (data, _) = result.unwrap();
assert_eq!(data, expected);
}
break;
}
}
}
}
}