use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WalOperation {
WriteShadowFile { path: PathBuf, content: String },
WriteMetaLog { path: PathBuf, content: String },
DeleteFile { path: PathBuf },
RenameFile { from: PathBuf, to: PathBuf },
CreateDirectory { path: PathBuf },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WalEntry {
pub operation: WalOperation,
pub sequence: u64,
}
pub struct WriteAheadLog {
path: PathBuf,
sequence: u64,
in_transaction: bool,
}
impl WriteAheadLog {
pub fn new(wlk_root: &Path) -> Result<Self> {
let path = wlk_root.join("wal");
let sequence = if path.exists() {
Self::read_last_sequence(&path)?
} else {
0
};
Ok(WriteAheadLog {
path,
sequence,
in_transaction: false,
})
}
pub fn begin(&mut self) -> Result<()> {
if self.in_transaction {
return Err(anyhow::anyhow!("Transaction already in progress"));
}
self.in_transaction = true;
Ok(())
}
pub fn append(&mut self, operation: WalOperation) -> Result<()> {
if !self.in_transaction {
return Err(anyhow::anyhow!("No transaction in progress"));
}
self.sequence += 1;
let entry = WalEntry {
operation,
sequence: self.sequence,
};
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.context("Failed to open WAL file")?;
let json = serde_json::to_string(&entry).context("Failed to serialize WAL entry")?;
writeln!(file, "{}", json).context("Failed to write to WAL")?;
file.sync_all().context("Failed to sync WAL to disk")?;
Ok(())
}
pub fn commit(&mut self) -> Result<()> {
if !self.in_transaction {
return Err(anyhow::anyhow!("No transaction in progress"));
}
let operations = self.read_all().context("Failed to read WAL entries")?;
for entry in operations {
self.execute_operation(entry.operation).context(format!(
"Failed to execute WAL operation {}",
entry.sequence
))?;
}
std::fs::remove_file(&self.path).context("Failed to remove WAL file")?;
self.sequence = 0;
self.in_transaction = false;
Ok(())
}
pub fn rollback(&mut self) -> Result<()> {
if !self.in_transaction {
return Err(anyhow::anyhow!("No transaction in progress"));
}
if self.path.exists() {
std::fs::remove_file(&self.path).context("Failed to remove WAL file")?;
}
self.sequence = 0;
self.in_transaction = false;
Ok(())
}
pub fn recover(&mut self) -> Result<()> {
if !self.path.exists() {
return Ok(());
}
let operations = self
.read_all()
.context("Failed to read WAL entries for recovery")?;
for entry in operations {
let _ = self.execute_operation(entry.operation);
}
std::fs::remove_file(&self.path).context("Failed to remove WAL file after recovery")?;
self.sequence = 0;
Ok(())
}
fn execute_operation(&self, op: WalOperation) -> Result<()> {
match op {
WalOperation::WriteShadowFile { path, content } => {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create parent directory")?;
}
std::fs::write(&path, content)
.context(format!("Failed to write shadow file to {:?}", path))?;
}
WalOperation::WriteMetaLog { path, content } => {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create parent directory")?;
}
std::fs::write(&path, content)
.context(format!("Failed to write metalog to {:?}", path))?;
}
WalOperation::DeleteFile { path } => {
if path.exists() {
std::fs::remove_file(&path)
.context(format!("Failed to delete file {:?}", path))?;
}
}
WalOperation::RenameFile { from, to } => {
if from.exists() {
if let Some(parent) = to.parent() {
std::fs::create_dir_all(parent)
.context("Failed to create destination directory")?;
}
std::fs::rename(&from, &to)
.context(format!("Failed to rename {:?} to {:?}", from, to))?;
}
}
WalOperation::CreateDirectory { path } => {
std::fs::create_dir_all(&path)
.context(format!("Failed to create directory {:?}", path))?;
}
}
Ok(())
}
fn read_all(&self) -> Result<Vec<WalEntry>> {
if !self.path.exists() {
return Ok(vec![]);
}
let file = File::open(&self.path).context("Failed to open WAL file")?;
let reader = BufReader::new(file);
let mut entries = vec![];
for line in reader.lines() {
let line = line.context("Failed to read line from WAL")?;
let entry: WalEntry =
serde_json::from_str(&line).context("Failed to deserialize WAL entry")?;
entries.push(entry);
}
Ok(entries)
}
fn read_last_sequence(path: &PathBuf) -> Result<u64> {
let entries = Self::read_all_static(path)?;
Ok(entries.last().map(|e| e.sequence).unwrap_or(0))
}
fn read_all_static(path: &PathBuf) -> Result<Vec<WalEntry>> {
if !path.exists() {
return Ok(vec![]);
}
let file = File::open(path).context("Failed to open WAL file")?;
let reader = BufReader::new(file);
let mut entries = vec![];
for line in reader.lines() {
let line = line.context("Failed to read line from WAL")?;
let entry: WalEntry =
serde_json::from_str(&line).context("Failed to deserialize WAL entry")?;
entries.push(entry);
}
Ok(entries)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_wal_basic_transaction() {
let temp_dir = TempDir::new().unwrap();
let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
wal.begin().unwrap();
let test_file = temp_dir.path().join("test.txt");
wal.append(WalOperation::WriteShadowFile {
path: test_file.clone(),
content: "test content".to_string(),
})
.unwrap();
assert!(!test_file.exists());
wal.commit().unwrap();
assert!(test_file.exists());
assert_eq!(std::fs::read_to_string(&test_file).unwrap(), "test content");
}
#[test]
fn test_wal_rollback() {
let temp_dir = TempDir::new().unwrap();
let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
wal.begin().unwrap();
let test_file = temp_dir.path().join("test.txt");
wal.append(WalOperation::WriteShadowFile {
path: test_file.clone(),
content: "test content".to_string(),
})
.unwrap();
wal.rollback().unwrap();
assert!(!test_file.exists());
}
#[test]
fn test_wal_multiple_operations() {
let temp_dir = TempDir::new().unwrap();
let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
wal.begin().unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
wal.append(WalOperation::WriteShadowFile {
path: file1.clone(),
content: "content 1".to_string(),
})
.unwrap();
wal.append(WalOperation::WriteShadowFile {
path: file2.clone(),
content: "content 2".to_string(),
})
.unwrap();
wal.commit().unwrap();
assert!(file1.exists());
assert!(file2.exists());
assert_eq!(std::fs::read_to_string(&file1).unwrap(), "content 1");
assert_eq!(std::fs::read_to_string(&file2).unwrap(), "content 2");
}
#[test]
fn test_wal_recovery() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
{
let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
wal.begin().unwrap();
wal.append(WalOperation::WriteShadowFile {
path: test_file.clone(),
content: "recovered content".to_string(),
})
.unwrap();
}
let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
wal.recover().unwrap();
assert!(test_file.exists());
assert_eq!(
std::fs::read_to_string(&test_file).unwrap(),
"recovered content"
);
}
}