use super::{Operation, Transaction, TransactionId};
use crate::error::{DbError, DbResult};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Condvar, Mutex,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WalEntry {
Begin {
tx_id: TransactionId,
timestamp: u64,
},
Operation {
tx_id: TransactionId,
operation: Operation,
},
Commit {
tx_id: TransactionId,
timestamp: u64,
},
Abort {
tx_id: TransactionId,
timestamp: u64,
},
Checkpoint { timestamp: u64 },
}
impl WalEntry {
pub fn tx_id(&self) -> Option<TransactionId> {
match self {
WalEntry::Begin { tx_id, .. } => Some(*tx_id),
WalEntry::Operation { tx_id, .. } => Some(*tx_id),
WalEntry::Commit { tx_id, .. } => Some(*tx_id),
WalEntry::Abort { tx_id, .. } => Some(*tx_id),
WalEntry::Checkpoint { .. } => None,
}
}
}
pub struct WalWriter {
file: Arc<Mutex<File>>,
path: PathBuf,
buffer: Arc<Mutex<WalBuffer>>,
batch_size: usize,
pending_writes: Arc<AtomicUsize>,
sync_state: Arc<Mutex<SyncState>>,
sync_done: Arc<Condvar>,
}
struct WalBuffer {
entries: VecDeque<String>,
last_assigned: u64,
}
struct SyncState {
syncing: bool,
synced: u64,
}
impl WalWriter {
pub fn new<P: AsRef<Path>>(path: P) -> DbResult<Self> {
Self::with_batch_size(path, 1)
}
pub fn with_batch_size<P: AsRef<Path>>(path: P, batch_size: usize) -> DbResult<Self> {
let path = path.as_ref().to_path_buf();
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| DbError::InternalError(format!("Failed to open WAL: {}", e)))?;
Ok(Self {
file: Arc::new(Mutex::new(file)),
path,
buffer: Arc::new(Mutex::new(WalBuffer {
entries: VecDeque::new(),
last_assigned: 0,
})),
batch_size: batch_size.max(1),
pending_writes: Arc::new(AtomicUsize::new(0)),
sync_state: Arc::new(Mutex::new(SyncState {
syncing: false,
synced: 0,
})),
sync_done: Arc::new(Condvar::new()),
})
}
fn buffer_entry(&self, entry: &WalEntry) -> DbResult<u64> {
let json = serde_json::to_string(entry)
.map_err(|e| DbError::InternalError(format!("Failed to serialize WAL entry: {}", e)))?;
let mut buffer = self.buffer.lock().unwrap();
buffer.entries.push_back(json);
buffer.last_assigned += 1;
Ok(buffer.last_assigned)
}
pub fn write(&self, entry: &WalEntry) -> DbResult<()> {
self.buffer_entry(entry)?;
let pending = self.pending_writes.fetch_add(1, Ordering::SeqCst) + 1;
if pending >= self.batch_size {
self.flush()?;
}
Ok(())
}
pub fn flush(&self) -> DbResult<()> {
let target = {
let buffer = self.buffer.lock().unwrap();
if buffer.entries.is_empty() {
return Ok(());
}
buffer.last_assigned
};
self.sync_up_to(target)
}
fn sync_up_to(&self, target: u64) -> DbResult<()> {
let mut state = self.sync_state.lock().unwrap();
loop {
if state.synced >= target {
return Ok(());
}
if !state.syncing {
state.syncing = true;
drop(state);
let result = self.write_and_sync();
let mut state = self.sync_state.lock().unwrap();
state.syncing = false;
if let Ok(up_to) = &result {
state.synced = state.synced.max(*up_to);
}
self.sync_done.notify_all();
return result.map(|_| ());
}
state = self.sync_done.wait(state).unwrap();
}
}
fn write_and_sync(&self) -> DbResult<u64> {
let (entries, up_to) = {
let mut buffer = self.buffer.lock().unwrap();
let entries: Vec<String> = buffer.entries.drain(..).collect();
(entries, buffer.last_assigned)
};
if !entries.is_empty() {
let mut file = self.file.lock().unwrap();
for json in &entries {
writeln!(file, "{}", json).map_err(|e| {
DbError::InternalError(format!("Failed to write WAL entry: {}", e))
})?;
}
file.sync_all()
.map_err(|e| DbError::InternalError(format!("Failed to sync WAL: {}", e)))?;
}
self.pending_writes.store(0, Ordering::SeqCst);
Ok(up_to)
}
pub fn force_sync(&self) -> DbResult<()> {
self.flush()
}
pub fn write_begin(&self, tx_id: TransactionId) -> DbResult<()> {
self.write(&WalEntry::Begin {
tx_id,
timestamp: tx_id.as_u64(),
})
}
pub fn write_operation(&self, tx_id: TransactionId, operation: Operation) -> DbResult<()> {
self.write(&WalEntry::Operation { tx_id, operation })
}
pub fn write_commit(&self, tx_id: TransactionId) -> DbResult<()> {
let entry = WalEntry::Commit {
tx_id,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64,
};
let seq = self.buffer_entry(&entry)?;
self.sync_up_to(seq)
}
pub fn write_abort(&self, tx_id: TransactionId) -> DbResult<()> {
self.write(&WalEntry::Abort {
tx_id,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64,
})
}
pub fn write_checkpoint(&self) -> DbResult<()> {
let entry = WalEntry::Checkpoint {
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64,
};
let seq = self.buffer_entry(&entry)?;
self.sync_up_to(seq)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn set_batch_size(&self, size: usize) {
let _ = size;
}
}
pub struct WalReader {
path: PathBuf,
}
impl WalReader {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
pub fn read_all(&self) -> DbResult<Vec<WalEntry>> {
if !self.path.exists() {
return Ok(Vec::new());
}
let file = File::open(&self.path)
.map_err(|e| DbError::InternalError(format!("Failed to open WAL: {}", e)))?;
let reader = BufReader::new(file);
let mut entries = Vec::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line.map_err(|e| {
DbError::InternalError(format!("Failed to read WAL line {}: {}", line_num, e))
})?;
if line.trim().is_empty() {
continue;
}
let entry: WalEntry = serde_json::from_str(&line).map_err(|e| {
DbError::InternalError(format!(
"Failed to parse WAL entry at line {}: {}",
line_num, e
))
})?;
entries.push(entry);
}
Ok(entries)
}
pub fn replay(&self) -> DbResult<Vec<Transaction>> {
let entries = self.read_all()?;
let mut transactions = std::collections::HashMap::new();
let mut committed = Vec::new();
for entry in entries {
match entry {
WalEntry::Begin { tx_id, timestamp } => {
let mut tx = Transaction::new(super::IsolationLevel::ReadCommitted);
tx.id = tx_id;
tx.read_timestamp = timestamp;
transactions.insert(tx_id, tx);
}
WalEntry::Operation { tx_id, operation } => {
if let Some(tx) = transactions.get_mut(&tx_id) {
tx.add_operation(operation);
}
}
WalEntry::Commit { tx_id, .. } => {
if let Some(mut tx) = transactions.remove(&tx_id) {
tx.commit();
committed.push(tx);
}
}
WalEntry::Abort { tx_id, .. } => {
transactions.remove(&tx_id);
}
WalEntry::Checkpoint { .. } => {
}
}
}
Ok(committed)
}
}
pub fn truncate_wal<P: AsRef<Path>>(path: P) -> DbResult<()> {
let reader = WalReader::new(&path);
let entries = reader.read_all()?;
let last_checkpoint_idx = entries
.iter()
.enumerate()
.rev()
.find(|(_, e)| matches!(e, WalEntry::Checkpoint { .. }))
.map(|(idx, _)| idx);
if let Some(checkpoint_idx) = last_checkpoint_idx {
let entries_to_keep = &entries[checkpoint_idx + 1..];
let temp_path = path.as_ref().with_extension("wal.tmp");
let mut temp_file = File::create(&temp_path)
.map_err(|e| DbError::InternalError(format!("Failed to create temp WAL: {}", e)))?;
for entry in entries_to_keep {
let json = serde_json::to_string(entry)
.map_err(|e| DbError::InternalError(format!("Failed to serialize entry: {}", e)))?;
writeln!(temp_file, "{}", json)
.map_err(|e| DbError::InternalError(format!("Failed to write temp WAL: {}", e)))?;
}
temp_file
.sync_all()
.map_err(|e| DbError::InternalError(format!("Failed to sync temp WAL: {}", e)))?;
std::fs::rename(&temp_path, path.as_ref())
.map_err(|e| DbError::InternalError(format!("Failed to rename WAL: {}", e)))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::super::TransactionState;
use super::*;
use tempfile::tempdir;
#[test]
fn test_wal_write_and_read() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let writer = WalWriter::new(&wal_path).unwrap();
let tx_id = TransactionId::new();
writer.write_begin(tx_id).unwrap();
writer
.write_operation(
tx_id,
Operation::Insert {
database: "_system".to_string(),
collection: "users".to_string(),
key: "user1".to_string(),
data: serde_json::json!({"name": "Alice"}),
},
)
.unwrap();
writer.write_commit(tx_id).unwrap();
let reader = WalReader::new(&wal_path);
let entries = reader.read_all().unwrap();
assert_eq!(entries.len(), 3);
assert!(matches!(entries[0], WalEntry::Begin { .. }));
assert!(matches!(entries[1], WalEntry::Operation { .. }));
assert!(matches!(entries[2], WalEntry::Commit { .. }));
}
#[test]
fn test_wal_replay() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let writer = WalWriter::new(&wal_path).unwrap();
let tx_id = TransactionId::new();
writer.write_begin(tx_id).unwrap();
writer
.write_operation(
tx_id,
Operation::Insert {
database: "_system".to_string(),
collection: "users".to_string(),
key: "user1".to_string(),
data: serde_json::json!({"name": "Alice"}),
},
)
.unwrap();
writer.write_commit(tx_id).unwrap();
let reader = WalReader::new(&wal_path);
let committed = reader.replay().unwrap();
assert_eq!(committed.len(), 1);
assert_eq!(committed[0].id, tx_id);
assert_eq!(committed[0].state, TransactionState::Committed);
assert_eq!(committed[0].operations.len(), 1);
}
#[test]
fn test_wal_truncate() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let writer = WalWriter::new(&wal_path).unwrap();
let tx1 = TransactionId::new();
writer.write_begin(tx1).unwrap();
writer.write_commit(tx1).unwrap();
writer.write_checkpoint().unwrap();
let tx2 = TransactionId::new();
writer.write_begin(tx2).unwrap();
writer.write_commit(tx2).unwrap();
truncate_wal(&wal_path).unwrap();
let reader = WalReader::new(&wal_path);
let entries = reader.read_all().unwrap();
assert_eq!(entries.len(), 2);
}
#[test]
fn test_wal_concurrent_group_commit() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let writer = Arc::new(WalWriter::new(&wal_path).unwrap());
const THREADS: usize = 8;
const COMMITS_PER_THREAD: usize = 50;
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let writer = Arc::clone(&writer);
std::thread::spawn(move || {
for _ in 0..COMMITS_PER_THREAD {
let tx_id = TransactionId::new();
writer.write_begin(tx_id).unwrap();
writer.write_commit(tx_id).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let reader = WalReader::new(&wal_path);
let entries = reader.read_all().unwrap();
let commits = entries
.iter()
.filter(|e| matches!(e, WalEntry::Commit { .. }))
.count();
assert_eq!(commits, THREADS * COMMITS_PER_THREAD);
let committed = reader.replay().unwrap();
assert_eq!(committed.len(), THREADS * COMMITS_PER_THREAD);
}
}