use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::tree::Hash;
use crate::tree::node::HASH_SIZE;
use super::buffer::{Mutation, WalError};
use super::entry::WalEntry;
const FRAME_LEN_SIZE: usize = 4;
const MARKER_TAG_SIZE: usize = 1;
const MARKER_CHECKSUM_SIZE: usize = 4;
const TAG_TRUNCATION_MARKER: u8 = 0x03;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FsyncPolicy {
PerWrite,
Batched(usize),
CommitOnly,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WalFileContents {
entries: Vec<WalEntry>,
committed_root: Option<Hash>,
}
impl WalFileContents {
#[must_use]
pub fn entries(&self) -> &[WalEntry] {
&self.entries
}
#[must_use]
pub const fn committed_root(&self) -> Option<Hash> {
self.committed_root
}
}
#[derive(Debug)]
pub struct DurableWal {
path: PathBuf,
file: File,
policy: FsyncPolicy,
writes_since_sync: usize,
promise: Option<super::promise::PromiseRecord>,
}
impl DurableWal {
pub fn new<P: AsRef<Path>>(path: P, policy: FsyncPolicy) -> Result<Self, WalError> {
validate_policy(policy)?;
let path = path.as_ref();
fs::create_dir_all(parent_dir(path))?;
let existed = path.exists();
let file = open_append_file(path)?;
if !existed {
sync_parent_dir(path)?;
}
Ok(Self {
path: path.to_path_buf(),
file,
policy,
writes_since_sync: 0,
promise: None,
})
}
pub fn seed_promise(&mut self, record: super::promise::PromiseRecord) {
self.promise = Some(record);
}
pub fn append(&mut self, entry: &WalEntry) -> Result<(), WalError> {
let bytes = entry.serialise();
self.write_entry_frame(&bytes)?;
self.writes_since_sync = self.writes_since_sync.saturating_add(1);
if self.should_sync_after_append() {
self.file.sync_all()?;
self.writes_since_sync = 0;
}
Ok(())
}
pub fn append_mutation(&mut self, mutation: &Mutation) -> Result<(), WalError> {
self.append(&WalEntry::from(mutation))
}
pub fn append_promise(&mut self, record: &super::promise::PromiseRecord) -> Result<(), WalError> {
let bytes = record.serialise();
self.write_entry_frame(&bytes)?;
self.file.sync_all()?;
self.writes_since_sync = 0;
self.promise = Some(record.clone());
Ok(())
}
pub fn commit(&mut self, root_hash: Hash) -> Result<(), WalError> {
self.file.sync_all()?;
let mut replacement = truncation_marker_frame(root_hash);
if let Some(record) = &self.promise {
let bytes = record.serialise();
replacement.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
replacement.extend_from_slice(&bytes);
}
write_atomic(&self.path, &replacement)?;
self.file = open_append_file(&self.path)?;
self.writes_since_sync = 0;
Ok(())
}
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<WalFileContents, WalError> {
parse_wal_file(&fs::read(path)?)
}
fn write_entry_frame(&mut self, bytes: &[u8]) -> Result<(), WalError> {
let mut frame = Vec::with_capacity(FRAME_LEN_SIZE + bytes.len());
frame.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
frame.extend_from_slice(bytes);
self.file.write_all(&frame)?;
Ok(())
}
const fn should_sync_after_append(&self) -> bool {
match self.policy {
FsyncPolicy::PerWrite => true,
FsyncPolicy::Batched(interval) => self.writes_since_sync >= interval,
FsyncPolicy::CommitOnly => false,
}
}
}
const fn validate_policy(policy: FsyncPolicy) -> Result<(), WalError> {
match policy {
FsyncPolicy::Batched(0) => Err(WalError::InvalidFsyncPolicy { interval: 0 }),
FsyncPolicy::PerWrite | FsyncPolicy::Batched(_) | FsyncPolicy::CommitOnly => Ok(()),
}
}
fn open_append_file(path: &Path) -> Result<File, WalError> {
Ok(OpenOptions::new().create(true).append(true).open(path)?)
}
fn truncation_marker_frame(root_hash: Hash) -> Vec<u8> {
let mut marker = Vec::with_capacity(MARKER_TAG_SIZE + HASH_SIZE + MARKER_CHECKSUM_SIZE);
marker.push(TAG_TRUNCATION_MARKER);
marker.extend_from_slice(root_hash.as_bytes());
marker.extend_from_slice(&crc32fast::hash(&marker).to_le_bytes());
let mut frame = Vec::with_capacity(FRAME_LEN_SIZE + marker.len());
frame.extend_from_slice(&(marker.len() as u32).to_le_bytes());
frame.extend_from_slice(&marker);
frame
}
fn parse_wal_file(bytes: &[u8]) -> Result<WalFileContents, WalError> {
let mut cursor = ByteCursor::new(bytes);
let mut entries = Vec::new();
let mut committed_root = None;
while !cursor.is_finished() {
let frame = cursor.read_frame()?;
if is_truncation_marker(frame) {
committed_root = Some(parse_truncation_marker(frame)?);
} else if super::promise::is_promise_payload(frame) {
} else {
entries.push(WalEntry::deserialise(frame)?);
}
}
Ok(WalFileContents {
entries,
committed_root,
})
}
fn is_truncation_marker(frame: &[u8]) -> bool {
frame.len() == MARKER_TAG_SIZE + HASH_SIZE + MARKER_CHECKSUM_SIZE
&& frame.first() == Some(&TAG_TRUNCATION_MARKER)
}
fn parse_truncation_marker(frame: &[u8]) -> Result<Hash, WalError> {
if !is_truncation_marker(frame) {
return Err(WalError::InvalidTag {
found: frame.first().copied().unwrap_or_default(),
});
}
let payload_len = MARKER_TAG_SIZE + HASH_SIZE;
let (payload, checksum_bytes) = frame.split_at(payload_len);
let expected = u32::from_le_bytes([
checksum_bytes[0],
checksum_bytes[1],
checksum_bytes[2],
checksum_bytes[3],
]);
let actual = crc32fast::hash(payload);
if expected != actual {
return Err(WalError::ChecksumMismatch { expected, actual });
}
let mut bytes = [0; HASH_SIZE];
bytes.copy_from_slice(&payload[MARKER_TAG_SIZE..]);
Ok(Hash::from_bytes(bytes))
}
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), WalError> {
let parent = parent_dir(path);
fs::create_dir_all(parent)?;
let mut temp_file = tempfile::Builder::new()
.prefix(".wal-")
.suffix(".tmp")
.tempfile_in(parent)?;
temp_file.write_all(bytes)?;
temp_file.as_file_mut().sync_all()?;
temp_file
.persist(path)
.map(drop)
.map_err(|error| WalError::Io(error.error))?;
sync_dir(parent)
}
fn sync_parent_dir(path: &Path) -> Result<(), WalError> {
sync_dir(parent_dir(path))
}
fn parent_dir(path: &Path) -> &Path {
path.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
}
#[cfg(unix)]
fn sync_dir(parent: &Path) -> Result<(), WalError> {
File::open(parent)?.sync_all()?;
Ok(())
}
#[cfg(not(unix))]
fn sync_dir(parent: &Path) -> Result<(), WalError> {
if parent.as_os_str().is_empty() {
return Ok(());
}
Ok(())
}
#[derive(Debug)]
struct ByteCursor<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> ByteCursor<'a> {
const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
const fn is_finished(&self) -> bool {
self.offset == self.bytes.len()
}
fn read_frame(&mut self) -> Result<&'a [u8], WalError> {
let len = usize::try_from(self.read_u32()?).map_err(|_| WalError::LengthOverflow)?;
self.read_bytes(len)
}
fn read_u32(&mut self) -> Result<u32, WalError> {
let bytes = self.read_bytes(FRAME_LEN_SIZE)?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], WalError> {
let end = self
.offset
.checked_add(len)
.ok_or(WalError::LengthOverflow)?;
let slice = self
.bytes
.get(self.offset..end)
.ok_or(WalError::Truncated)?;
self.offset = end;
Ok(slice)
}
}
#[cfg(test)]
#[path = "durable_tests.rs"]
mod tests;