use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::MemoryRecord;
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum WalOp {
Remember { record: Box<MemoryRecord> },
Forget { id: Uuid },
}
pub struct Wal {
path: PathBuf,
file: File,
appends: usize,
}
impl Wal {
pub fn open(dir: &Path) -> Result<(Self, Vec<WalOp>)> {
std::fs::create_dir_all(dir)
.with_context(|| format!("creating data dir {}", dir.display()))?;
let path = dir.join("memory.wal");
let mut ops = Vec::new();
if path.exists() {
let reader = BufReader::new(File::open(&path)?);
for (i, line) in reader.lines().enumerate() {
let line = line?;
if line.trim().is_empty() {
continue;
}
match serde_json::from_str::<WalOp>(&line) {
Ok(op) => ops.push(op),
Err(e) => {
eprintln!("memrust: skipping unreadable WAL line {}: {e}", i + 1);
}
}
}
}
let file = OpenOptions::new().create(true).append(true).open(&path)?;
Ok((
Self {
path,
file,
appends: 0,
},
ops,
))
}
pub fn append_batch(&mut self, ops: &[WalOp]) -> Result<()> {
if ops.is_empty() {
return Ok(());
}
let mut buf = String::new();
for op in ops {
buf.push_str(&serde_json::to_string(op)?);
buf.push('\n');
}
self.file.write_all(buf.as_bytes())?;
self.file.sync_data()?;
self.appends += ops.len();
Ok(())
}
pub fn append(&mut self, op: &WalOp) -> Result<()> {
let mut line = serde_json::to_string(op)?;
line.push('\n');
self.file.write_all(line.as_bytes())?;
self.file.sync_data()?;
self.appends += 1;
Ok(())
}
pub fn appends_since_checkpoint(&self) -> usize {
self.appends
}
pub fn truncate(&mut self) -> Result<()> {
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(&self.path)?;
file.sync_all()?;
self.file = OpenOptions::new().append(true).open(&self.path)?;
self.appends = 0;
Ok(())
}
}
const CHECKPOINT_FILE: &str = "checkpoint.json";
pub fn save_checkpoint<T: Serialize>(dir: &Path, state: &T) -> Result<()> {
let path = dir.join(CHECKPOINT_FILE);
let tmp = dir.join("checkpoint.json.tmp");
{
let mut out = BufWriter::new(File::create(&tmp)?);
serde_json::to_writer(&mut out, state).context("serializing checkpoint")?;
out.flush()?;
out.get_ref().sync_all()?;
}
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn load_checkpoint<T: DeserializeOwned>(dir: &Path) -> Result<Option<T>> {
let path = dir.join(CHECKPOINT_FILE);
if !path.exists() {
return Ok(None);
}
let reader = BufReader::new(File::open(&path)?);
let state = serde_json::from_reader(reader)
.with_context(|| format!("reading checkpoint {}", path.display()))?;
Ok(Some(state))
}