use super::log_payload_io::{
CRC_DELETE_MARKER, CRC_STORE_MARKER, LEGACY_DELETE_MARKER, LEGACY_STORE_MARKER,
};
use super::wal_cursor::{WalConsumerId, WalCursor, WalPosition, WalRecord, WalWatermarkRegistry};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
const DELETE_LEGACY_BODY: u64 = 8;
const DELETE_CRC_BODY: u64 = 8 + 4;
#[derive(Debug)]
pub struct LogWalCursor {
log_path: PathBuf,
registry: WalWatermarkRegistry,
}
impl LogWalCursor {
#[must_use]
pub fn new(dir: impl AsRef<Path>) -> Self {
Self {
log_path: dir.as_ref().join("payloads.log"),
registry: WalWatermarkRegistry::new(),
}
}
fn durable_len(&self) -> u64 {
std::fs::metadata(&self.log_path).map_or(0, |m| m.len())
}
#[must_use]
pub fn min_watermark(&self) -> Option<WalPosition> {
self.registry.min_watermark()
}
}
impl WalCursor for LogWalCursor {
fn read_from(&self, from: WalPosition, max: usize) -> crate::Result<Vec<WalRecord>> {
if max == 0 {
return Ok(Vec::new());
}
let durable_len = self.durable_len();
let mut pos = from.offset();
if pos >= durable_len {
return Ok(Vec::new());
}
let mut file = File::open(&self.log_path)?;
let mut out = Vec::new();
while out.len() < max && pos < durable_len {
let Some(record) = read_one_frame(&mut file, pos, durable_len)? else {
break; };
pos = record.next.offset();
out.push(record);
}
Ok(out)
}
fn tail_position(&self) -> WalPosition {
WalPosition::new(self.durable_len())
}
fn register_consumer(&self) -> WalConsumerId {
self.registry.register()
}
fn deregister_consumer(&self, consumer: WalConsumerId) {
self.registry.deregister(consumer);
}
fn advance_low_watermark(&self, consumer: WalConsumerId, up_to: WalPosition) {
self.registry.advance(consumer, up_to);
}
}
fn read_one_frame(file: &mut File, pos: u64, durable_len: u64) -> io::Result<Option<WalRecord>> {
file.seek(SeekFrom::Start(pos))?;
let mut marker = [0u8; 1];
if file.read_exact(&mut marker).is_err() {
return Ok(None);
}
let Some(body_len) = frame_body_len(file, marker[0]) else {
return Ok(None);
};
let total = 1 + body_len;
if pos.saturating_add(total) > durable_len {
return Ok(None); }
let bytes = read_frame_bytes(file, pos, total)?;
Ok(Some(WalRecord {
position: WalPosition::new(pos),
next: WalPosition::new(pos + total),
bytes,
}))
}
fn frame_body_len(file: &mut File, marker: u8) -> Option<u64> {
match marker {
LEGACY_STORE_MARKER => store_body_len(file, false),
CRC_STORE_MARKER => store_body_len(file, true),
LEGACY_DELETE_MARKER => Some(DELETE_LEGACY_BODY),
CRC_DELETE_MARKER => Some(DELETE_CRC_BODY),
_ => None,
}
}
fn store_body_len(file: &mut File, has_crc: bool) -> Option<u64> {
let mut id_bytes = [0u8; 8];
if file.read_exact(&mut id_bytes).is_err() {
return None;
}
let mut len_bytes = [0u8; 4];
if file.read_exact(&mut len_bytes).is_err() {
return None;
}
let payload_len = u64::from(u32::from_le_bytes(len_bytes));
let crc_len = if has_crc { 4 } else { 0 };
Some(8 + 4 + payload_len + crc_len)
}
fn read_frame_bytes(file: &mut File, pos: u64, total: u64) -> io::Result<Vec<u8>> {
file.seek(SeekFrom::Start(pos))?;
let cap = usize::try_from(total)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "WAL frame too large"))?;
let mut bytes = vec![0u8; cap];
file.read_exact(&mut bytes)?;
Ok(bytes)
}