use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::bplus_tree::storage::{crc32, DiskFile};
const WAL_MAGIC: &[u8; 8] = b"WAL00001";
const WAL_HEADER_SIZE: usize = 32;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum WalRecord {
Begin { xid: u64 },
Write {
xid: u64,
key: Vec<u8>,
value: Option<Vec<u8>>,
},
Commit { xid: u64 },
Abort { xid: u64 },
Checkpoint {
next_version: u64,
root_page_id: u64,
next_page_id: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalEntry {
pub lsn: u64,
pub record: WalRecord,
}
pub struct Wal {
file: DiskFile,
next_lsn: u64,
dirty: bool,
}
impl Wal {
pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
let mut file = DiskFile::open(path)?;
let next_lsn = if file.len()? == 0 {
Self::write_header(&mut file, 1)?;
1
} else {
let mut hdr = vec![0u8; WAL_HEADER_SIZE];
file.read_exact_at(0, &mut hdr)?;
if &hdr[0..8] != WAL_MAGIC {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"非法 WAL 文件魔数",
));
}
Self::scan_next_lsn(&mut file)?
};
Ok(Self {
file,
next_lsn,
dirty: false,
})
}
fn write_header(file: &mut DiskFile, next_lsn: u64) -> std::io::Result<()> {
let mut hdr = vec![0u8; WAL_HEADER_SIZE];
hdr[0..8].copy_from_slice(WAL_MAGIC);
hdr[8..16].copy_from_slice(&next_lsn.to_le_bytes());
file.write_all_at(0, &hdr)?;
file.sync()?;
Ok(())
}
fn scan_next_lsn(file: &mut DiskFile) -> std::io::Result<u64> {
let entries = Self::read_all_entries(file)?;
Ok(entries.last().map(|e| e.lsn + 1).unwrap_or(1))
}
pub fn append(&mut self, record: WalRecord) -> std::io::Result<u64> {
let lsn = self.next_lsn;
self.next_lsn += 1;
let entry = WalEntry { lsn, record };
let body = bincode::serialize(&entry).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
})?;
let checksum = crc32(&body);
let mut frame = Vec::with_capacity(8 + body.len());
frame.extend_from_slice(&(body.len() as u32).to_le_bytes());
frame.extend_from_slice(&checksum.to_le_bytes());
frame.extend_from_slice(&body);
self.file.append(&frame)?;
self.dirty = true;
Ok(lsn)
}
pub fn sync(&mut self) -> std::io::Result<()> {
let mut hdr = vec![0u8; WAL_HEADER_SIZE];
hdr[0..8].copy_from_slice(WAL_MAGIC);
hdr[8..16].copy_from_slice(&self.next_lsn.to_le_bytes());
self.file.write_all_at(0, &hdr)?;
self.file.sync()?;
Ok(())
}
pub fn read_all(&mut self) -> std::io::Result<Vec<WalEntry>> {
Self::read_all_entries(&mut self.file)
}
fn read_all_entries(file: &mut DiskFile) -> std::io::Result<Vec<WalEntry>> {
let len = file.len()?;
if len <= WAL_HEADER_SIZE as u64 {
return Ok(Vec::new());
}
let mut offset = WAL_HEADER_SIZE as u64;
let mut out = Vec::new();
while offset + 8 <= len {
let mut header = [0u8; 8];
if file.read_exact_at(offset, &mut header).is_err() {
break;
}
let body_len = u32::from_le_bytes(header[0..4].try_into().unwrap()) as u64;
let expect_crc = u32::from_le_bytes(header[4..8].try_into().unwrap());
if body_len == 0 || body_len > 16 * 1024 * 1024 {
break; }
if offset + 8 + body_len > len {
break; }
let mut body = vec![0u8; body_len as usize];
if file.read_exact_at(offset + 8, &mut body).is_err() {
break;
}
if crc32(&body) != expect_crc {
break; }
match bincode::deserialize::<WalEntry>(&body) {
Ok(entry) => out.push(entry),
Err(_) => break,
}
offset += 8 + body_len;
}
Ok(out)
}
pub fn truncate(&mut self) -> std::io::Result<()> {
self.file.set_len(WAL_HEADER_SIZE as u64)?;
Self::write_header(&mut self.file, self.next_lsn)?;
self.dirty = false;
Ok(())
}
pub fn next_lsn(&self) -> u64 {
self.next_lsn
}
pub fn path(&self) -> &Path {
self.file.path()
}
}
#[derive(Debug, Default)]
pub struct RecoveryPlan {
pub committed_writes: Vec<(u64, Vec<u8>, Option<Vec<u8>>)>, pub uncommitted_writes: Vec<(u64, Vec<u8>)>, pub max_xid: u64,
pub last_checkpoint: Option<WalRecord>,
}
pub fn analyze_recovery(entries: &[WalEntry]) -> RecoveryPlan {
use std::collections::{HashMap, HashSet};
let mut plan = RecoveryPlan::default();
let mut open_writes: HashMap<u64, Vec<(Vec<u8>, Option<Vec<u8>>)>> = HashMap::new();
let mut committed: HashSet<u64> = HashSet::new();
for e in entries {
match &e.record {
WalRecord::Begin { xid } => {
plan.max_xid = plan.max_xid.max(*xid);
open_writes.entry(*xid).or_default();
}
WalRecord::Write { xid, key, value } => {
plan.max_xid = plan.max_xid.max(*xid);
open_writes
.entry(*xid)
.or_default()
.push((key.clone(), value.clone()));
}
WalRecord::Commit { xid } => {
plan.max_xid = plan.max_xid.max(*xid);
if let Some(writes) = open_writes.remove(xid) {
for (k, v) in writes {
plan.committed_writes.push((*xid, k, v));
}
}
committed.insert(*xid);
}
WalRecord::Abort { xid } => {
plan.max_xid = plan.max_xid.max(*xid);
if let Some(writes) = open_writes.remove(xid) {
for (k, _) in writes {
plan.uncommitted_writes.push((*xid, k));
}
}
}
WalRecord::Checkpoint { .. } => {
plan.last_checkpoint = Some(e.record.clone());
}
}
}
for (xid, writes) in open_writes {
if !committed.contains(&xid) {
for (k, _) in writes {
plan.uncommitted_writes.push((xid, k));
}
}
}
plan
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_wal(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"wal_{tag}_{}_{}.wal",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
#[test]
fn test_wal_append_and_read() {
let path = tmp_wal("basic");
{
let mut wal = Wal::open(&path).unwrap();
wal.append(WalRecord::Begin { xid: 1 }).unwrap();
wal.append(WalRecord::Write {
xid: 1,
key: b"k".to_vec(),
value: Some(b"v".to_vec()),
})
.unwrap();
wal.append(WalRecord::Commit { xid: 1 }).unwrap();
wal.sync().unwrap();
}
{
let mut wal = Wal::open(&path).unwrap();
let entries = wal.read_all().unwrap();
assert_eq!(entries.len(), 3);
let plan = analyze_recovery(&entries);
assert_eq!(plan.committed_writes.len(), 1);
assert!(plan.uncommitted_writes.is_empty());
assert_eq!(plan.max_xid, 1);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_recovery_undo_uncommitted() {
let path = tmp_wal("undo");
{
let mut wal = Wal::open(&path).unwrap();
wal.append(WalRecord::Begin { xid: 1 }).unwrap();
wal.append(WalRecord::Write {
xid: 1,
key: b"a".to_vec(),
value: Some(b"1".to_vec()),
})
.unwrap();
wal.append(WalRecord::Commit { xid: 1 }).unwrap();
wal.append(WalRecord::Begin { xid: 2 }).unwrap();
wal.append(WalRecord::Write {
xid: 2,
key: b"b".to_vec(),
value: Some(b"2".to_vec()),
})
.unwrap();
wal.sync().unwrap();
}
{
let mut wal = Wal::open(&path).unwrap();
let plan = analyze_recovery(&wal.read_all().unwrap());
assert_eq!(plan.committed_writes.len(), 1);
assert_eq!(plan.uncommitted_writes.len(), 1);
assert_eq!(plan.uncommitted_writes[0].1, b"b");
}
let _ = std::fs::remove_file(&path);
}
}