use crate::{Error, Result, Record, Lsn};
use bytes::{BytesMut, BufMut};
use parking_lot::Mutex;
use std::fs::{File, OpenOptions};
use std::io::{Read, Write, Seek, SeekFrom};
use std::path::Path;
use std::sync::Arc;
const WAL_HEADER_SIZE: usize = 16;
const WAL_MAGIC: u32 = 0x57414C00; const RECORD_HEADER_SIZE: usize = 12;
pub struct Wal {
inner: Arc<Mutex<WalInner>>,
}
struct WalInner {
file: File,
next_lsn: Lsn,
pending: Vec<Record>,
}
impl Wal {
pub fn create(path: impl AsRef<Path>) -> Result<Self> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(path)?;
let mut header = BytesMut::with_capacity(WAL_HEADER_SIZE);
header.put_u32(WAL_MAGIC); header.put_u32_le(1); header.put_u64_le(0); file.write_all(&header)?;
file.sync_all()?;
Ok(Self {
inner: Arc::new(Mutex::new(WalInner {
file,
next_lsn: 1,
pending: Vec::new(),
})),
})
}
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path)?;
let mut header = [0u8; WAL_HEADER_SIZE];
file.read_exact(&mut header)?;
let magic = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
if magic != WAL_MAGIC {
return Err(Error::Corruption("Invalid WAL magic".to_string()));
}
file.seek(SeekFrom::Start(WAL_HEADER_SIZE as u64))?;
let mut max_lsn = 0u64;
loop {
let mut rec_header = [0u8; RECORD_HEADER_SIZE];
match file.read_exact(&mut rec_header) {
Ok(_) => {
let lsn = u64::from_le_bytes([
rec_header[0], rec_header[1], rec_header[2], rec_header[3],
rec_header[4], rec_header[5], rec_header[6], rec_header[7],
]);
let len = u32::from_le_bytes([
rec_header[8], rec_header[9], rec_header[10], rec_header[11],
]) as u64;
max_lsn = max_lsn.max(lsn);
file.seek(SeekFrom::Current(len as i64 + 4))?;
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
}
Ok(Self {
inner: Arc::new(Mutex::new(WalInner {
file,
next_lsn: max_lsn + 1,
pending: Vec::new(),
})),
})
}
pub fn append(&self, record: Record) -> Result<Lsn> {
let mut inner = self.inner.lock();
let lsn = inner.next_lsn;
inner.next_lsn += 1;
inner.pending.push(record);
Ok(lsn)
}
pub fn flush(&self) -> Result<()> {
let mut inner = self.inner.lock();
if inner.pending.is_empty() {
return Ok(());
}
inner.file.seek(SeekFrom::End(0))?;
let mut full_buf = BytesMut::new();
let base_lsn = inner.next_lsn - inner.pending.len() as u64;
for (i, record) in inner.pending.iter().enumerate() {
let lsn = base_lsn + i as u64;
let data = bincode::serialize(record)
.map_err(|e| Error::Internal(format!("Serialize error: {}", e)))?;
let crc = crc32fast::hash(&data);
full_buf.put_u64_le(lsn);
full_buf.put_u32_le(data.len() as u32);
full_buf.put_slice(&data);
full_buf.put_u32_le(crc);
}
inner.file.write_all(&full_buf)?;
inner.file.sync_all()?;
inner.pending.clear();
Ok(())
}
pub fn read_all(&self) -> Result<Vec<(Lsn, Record)>> {
let inner = self.inner.lock();
let mut file = inner.file.try_clone()?;
drop(inner);
file.seek(SeekFrom::Start(WAL_HEADER_SIZE as u64))?;
let mut records = Vec::new();
loop {
let mut rec_header = [0u8; RECORD_HEADER_SIZE];
match file.read_exact(&mut rec_header) {
Ok(_) => {
let lsn = u64::from_le_bytes([
rec_header[0], rec_header[1], rec_header[2], rec_header[3],
rec_header[4], rec_header[5], rec_header[6], rec_header[7],
]);
let len = u32::from_le_bytes([
rec_header[8], rec_header[9], rec_header[10], rec_header[11],
]) as usize;
let mut data = vec![0u8; len];
file.read_exact(&mut data)?;
let mut crc_bytes = [0u8; 4];
file.read_exact(&mut crc_bytes)?;
let expected_crc = u32::from_le_bytes(crc_bytes);
let actual_crc = crc32fast::hash(&data);
if expected_crc != actual_crc {
return Err(Error::ChecksumMismatch);
}
let record: Record = bincode::deserialize(&data)
.map_err(|e| Error::Corruption(format!("Deserialize error: {}", e)))?;
records.push((lsn, record));
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
}
Ok(records)
}
pub fn next_lsn(&self) -> Lsn {
self.inner.lock().next_lsn
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Key, Value};
use tempfile::TempDir;
use std::collections::HashMap;
#[test]
fn test_wal_create_and_write() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("wal.log");
let wal = Wal::create(path).unwrap();
let key = Key::new(b"user#123".to_vec());
let mut item = HashMap::new();
item.insert("name".to_string(), Value::string("Alice"));
let record = Record::put(key, item, 1);
let lsn = wal.append(record).unwrap();
assert_eq!(lsn, 1);
wal.flush().unwrap();
let records = wal.read_all().unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].0, 1);
}
#[test]
fn test_wal_reopen() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("wal.log");
{
let wal = Wal::create(&path).unwrap();
let key = Key::new(b"test".to_vec());
let item = HashMap::new();
let record = Record::put(key, item, 1);
wal.append(record).unwrap();
wal.flush().unwrap();
}
let wal = Wal::open(&path).unwrap();
assert_eq!(wal.next_lsn(), 2);
let records = wal.read_all().unwrap();
assert_eq!(records.len(), 1);
}
#[test]
fn test_wal_group_commit() {
let tmp = TempDir::new().unwrap();
let wal = Wal::create(tmp.path().join("wal.log")).unwrap();
for i in 0..10 {
let key = Key::new(format!("key{}", i).into_bytes());
let item = HashMap::new();
let record = Record::put(key, item, i);
wal.append(record).unwrap();
}
wal.flush().unwrap();
let records = wal.read_all().unwrap();
assert_eq!(records.len(), 10);
}
}