use std::io::{BufWriter, Write};
use std::path::Path;
use crate::error::{Error, Result};
pub(crate) fn open_wal_writer(wal_path: &Path, context: &str) -> Result<BufWriter<std::fs::File>> {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(wal_path)
.map_err(|e| Error::Index(format!("{context} open: {e}")))?;
#[cfg(test)]
io_counters::record_open(wal_path, context);
Ok(BufWriter::new(file))
}
pub(crate) fn wal_write(
w: &mut BufWriter<std::fs::File>,
bytes: &[u8],
context: &str,
) -> Result<()> {
w.write_all(bytes)
.map_err(|e| Error::Index(format!("{context} write: {e}")))
}
pub(crate) fn flush_wal(
w: &mut BufWriter<std::fs::File>,
wal_path: &Path,
context: &str,
) -> Result<()> {
w.flush()
.map_err(|e| Error::Index(format!("{context} flush: {e}")))?;
#[cfg(test)]
io_counters::record_flush(wal_path, context);
w.get_ref()
.sync_all()
.map_err(|e| Error::Index(format!("{context} fsync: {e}")))?;
#[cfg(test)]
io_counters::record_sync(wal_path, context);
let _ = wal_path;
Ok(())
}
pub(crate) fn wal_truncate(wal_path: &Path, context: &str) -> Result<()> {
if !wal_path.exists() {
return Ok(());
}
let file = std::fs::OpenOptions::new()
.write(true)
.open(wal_path)
.map_err(|e| Error::Index(format!("{context} truncate open: {e}")))?;
file.set_len(0)
.map_err(|e| Error::Index(format!("{context} truncate: {e}")))
}
pub(crate) fn read_entry_header(data: &[u8], pos: usize, context: &str) -> Option<(usize, usize)> {
if pos + 4 > data.len() {
tracing::warn!("{context} truncated at offset {pos}: not enough bytes for length prefix");
return None;
}
let bytes: [u8; 4] = data[pos..pos + 4].try_into().ok()?;
let body_len = u32::from_le_bytes(bytes) as usize;
Some((pos + 4, body_len))
}
#[cfg(test)]
pub(crate) mod io_counters {
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Mutex, MutexGuard};
static OPENS: AtomicU32 = AtomicU32::new(0);
static FLUSHES: AtomicU32 = AtomicU32::new(0);
static SYNCS: AtomicU32 = AtomicU32::new(0);
static WATCH: Mutex<Option<PathBuf>> = Mutex::new(None);
static SERIALISE: Mutex<()> = Mutex::new(());
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WalIoCounts {
pub opens: u32,
pub flushes: u32,
pub syncs: u32,
}
fn watched(wal_path: &Path) -> bool {
WATCH
.lock()
.map(|w| w.as_deref() == Some(wal_path))
.unwrap_or(false)
}
pub(super) fn record_open(wal_path: &Path, _context: &str) {
if watched(wal_path) {
OPENS.fetch_add(1, Ordering::Relaxed);
}
}
pub(super) fn record_flush(wal_path: &Path, _context: &str) {
if watched(wal_path) {
FLUSHES.fetch_add(1, Ordering::Relaxed);
}
}
pub(super) fn record_sync(wal_path: &Path, _context: &str) {
if watched(wal_path) {
SYNCS.fetch_add(1, Ordering::Relaxed);
}
}
struct Watch<'a>(#[allow(dead_code)] MutexGuard<'a, ()>);
impl Drop for Watch<'_> {
fn drop(&mut self) {
if let Ok(mut w) = WATCH.lock() {
*w = None;
}
}
}
pub(crate) fn count_wal_io<T>(wal_path: &Path, f: impl FnOnce() -> T) -> (T, WalIoCounts) {
let guard = SERIALISE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Ok(mut w) = WATCH.lock() {
*w = Some(wal_path.to_path_buf());
}
OPENS.store(0, Ordering::Relaxed);
FLUSHES.store(0, Ordering::Relaxed);
SYNCS.store(0, Ordering::Relaxed);
let _watch = Watch(guard);
let out = f();
let counts = WalIoCounts {
opens: OPENS.load(Ordering::Relaxed),
flushes: FLUSHES.load(Ordering::Relaxed),
syncs: SYNCS.load(Ordering::Relaxed),
};
(out, counts)
}
}