use std::path::Path;
use crate::error::{Result, WalError};
use crate::reader::WalReader;
use crate::record::HEADER_SIZE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveryInfo {
pub last_lsn: u64,
pub record_count: u64,
pub end_offset: u64,
}
impl RecoveryInfo {
pub fn next_lsn(&self) -> u64 {
self.last_lsn + 1
}
}
pub fn recover(path: &Path) -> Result<RecoveryInfo> {
if !path.exists() {
return Ok(RecoveryInfo {
last_lsn: 0,
record_count: 0,
end_offset: 0,
});
}
let mut reader = WalReader::open(path)?;
let mut last_lsn = 0u64;
let mut record_count = 0u64;
let mut last_valid_offset = 0u64;
loop {
let offset_before = reader.offset();
match reader.next_record() {
Ok(Some(record)) => {
last_lsn = record.header.lsn;
record_count += 1;
last_valid_offset =
offset_before + HEADER_SIZE as u64 + record.header.payload_len as u64;
}
Ok(None) => {
break;
}
Err(e @ WalError::UnknownRequiredRecordType { .. }) => {
return Err(e);
}
Err(e) => return Err(e),
}
}
Ok(RecoveryInfo {
last_lsn,
record_count,
end_offset: last_valid_offset,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::RecordType;
use crate::writer::WalWriter;
#[test]
fn recover_empty_wal() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.wal");
{
let mut writer = WalWriter::open_without_direct_io(&path).unwrap();
writer.sync().unwrap();
}
let info = recover(&path).unwrap();
assert_eq!(info.last_lsn, 0);
assert_eq!(info.record_count, 0);
assert_eq!(info.next_lsn(), 1);
}
#[test]
fn recover_nonexistent_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nonexistent.wal");
let info = recover(&path).unwrap();
assert_eq!(info.last_lsn, 0);
assert_eq!(info.record_count, 0);
}
#[test]
fn recover_with_records() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.wal");
{
let mut writer = WalWriter::open_without_direct_io(&path).unwrap();
writer
.append(RecordType::Put as u16, 1, 0, b"first")
.unwrap();
writer
.append(RecordType::Put as u16, 1, 0, b"second")
.unwrap();
writer
.append(RecordType::Delete as u16, 2, 1, b"third")
.unwrap();
writer.sync().unwrap();
}
let info = recover(&path).unwrap();
assert_eq!(info.last_lsn, 3);
assert_eq!(info.record_count, 3);
assert_eq!(info.next_lsn(), 4);
assert!(info.end_offset > 0);
}
#[test]
fn recover_truncated_wal() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("truncated.wal");
{
let mut writer = WalWriter::open_without_direct_io(&path).unwrap();
writer
.append(RecordType::Put as u16, 1, 0, b"good")
.unwrap();
writer
.append(RecordType::Put as u16, 1, 0, b"also-good")
.unwrap();
writer.sync().unwrap();
}
{
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
file.write_all(b"GARBAGE_TORN_WRITE_PARTIAL").unwrap();
}
let info = recover(&path).unwrap();
assert_eq!(info.last_lsn, 2);
assert_eq!(info.record_count, 2);
assert_eq!(info.next_lsn(), 3);
}
}