use std::fs::OpenOptions;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use memmap2::{Mmap, MmapMut, MmapOptions};
pub const ARENA_HEADER_BYTES: usize = 64;
pub const DEFAULT_ARENA_BYTES: u64 = 1024 * 1024 * 1024;
const NEXT_OFFSET_AT: usize = 0;
const WRAP_EPOCH_AT: usize = 8;
#[derive(Debug)]
pub enum ArenaError {
Io(std::io::Error),
PayloadTooLarge { payload: u64, max_slab: u64 },
OutOfBounds { offset: u64, len: u32, arena: u64 },
}
impl std::fmt::Display for ArenaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "arena io: {e}"),
Self::PayloadTooLarge { payload, max_slab } => {
write!(f, "arena payload {payload} > max_slab {max_slab}")
}
Self::OutOfBounds { offset, len, arena } => {
write!(f, "arena read out of bounds: offset={offset} len={len} arena_size={arena}")
}
}
}
}
impl std::error::Error for ArenaError {}
impl From<std::io::Error> for ArenaError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
pub struct ArenaWriter {
mmap: MmapMut,
size: u64,
}
impl ArenaWriter {
pub fn create(path: &Path, size_bytes: u64) -> Result<Self, ArenaError> {
assert!(
size_bytes > ARENA_HEADER_BYTES as u64,
"arena size {size_bytes} must exceed header {ARENA_HEADER_BYTES}",
);
let file =
OpenOptions::new().read(true).write(true).create(true).truncate(true).open(path)?;
file.set_len(size_bytes)?;
#[allow(unsafe_code)]
let mut mmap = unsafe { MmapOptions::new().len(size_bytes as usize).map_mut(&file)? };
let header = &mut mmap[..ARENA_HEADER_BYTES];
header.fill(0);
header[NEXT_OFFSET_AT..NEXT_OFFSET_AT + 8]
.copy_from_slice(&(ARENA_HEADER_BYTES as u64).to_le_bytes());
Ok(Self { mmap, size: size_bytes })
}
pub fn write_payload(&mut self, bytes: &[u8]) -> Result<(u64, u32), ArenaError> {
let len = bytes.len() as u64;
let max_slab = self.size - ARENA_HEADER_BYTES as u64;
if len > max_slab {
return Err(ArenaError::PayloadTooLarge { payload: len, max_slab });
}
let next_off = u64::from_le_bytes(
self.mmap[NEXT_OFFSET_AT..NEXT_OFFSET_AT + 8].try_into().expect("8 byte header field"),
);
let (slab_off, new_next, did_wrap) = if next_off + len > self.size {
(ARENA_HEADER_BYTES as u64, ARENA_HEADER_BYTES as u64 + len, true)
} else {
(next_off, next_off + len, false)
};
let dst = &mut self.mmap[slab_off as usize..slab_off as usize + bytes.len()];
dst.copy_from_slice(bytes);
self.mmap[NEXT_OFFSET_AT..NEXT_OFFSET_AT + 8].copy_from_slice(&new_next.to_le_bytes());
if did_wrap {
let prev_epoch = u64::from_le_bytes(
self.mmap[WRAP_EPOCH_AT..WRAP_EPOCH_AT + 8].try_into().expect("8 byte"),
);
self.mmap[WRAP_EPOCH_AT..WRAP_EPOCH_AT + 8]
.copy_from_slice(&(prev_epoch + 1).to_le_bytes());
}
Ok((slab_off, len as u32))
}
#[must_use]
pub fn size(&self) -> u64 {
self.size
}
#[must_use]
pub fn next_offset(&self) -> u64 {
u64::from_le_bytes(
self.mmap[NEXT_OFFSET_AT..NEXT_OFFSET_AT + 8].try_into().expect("8 byte header field"),
)
}
#[must_use]
pub fn wrap_epoch(&self) -> u64 {
u64::from_le_bytes(
self.mmap[WRAP_EPOCH_AT..WRAP_EPOCH_AT + 8].try_into().expect("8 byte header field"),
)
}
}
pub struct ArenaReader {
mmap: Mmap,
size: u64,
}
impl ArenaReader {
pub fn open(path: &Path) -> Result<Self, ArenaError> {
let file = OpenOptions::new().read(true).open(path)?;
let metadata = file.metadata()?;
let size = metadata.len();
#[allow(unsafe_code)]
let mmap = unsafe { MmapOptions::new().len(size as usize).map(&file)? };
Ok(Self { mmap, size })
}
pub fn read_payload(&self, offset: u64, len: u32) -> Result<&[u8], ArenaError> {
let end = offset + u64::from(len);
if end > self.size || offset < ARENA_HEADER_BYTES as u64 {
return Err(ArenaError::OutOfBounds { offset, len, arena: self.size });
}
Ok(&self.mmap[offset as usize..end as usize])
}
#[must_use]
pub fn size(&self) -> u64 {
self.size
}
#[must_use]
pub fn wrap_epoch(&self) -> u64 {
u64::from_le_bytes(
self.mmap[WRAP_EPOCH_AT..WRAP_EPOCH_AT + 8].try_into().expect("8 byte header field"),
)
}
}
#[must_use]
pub fn arena_path(prefix: &str) -> std::path::PathBuf {
std::path::PathBuf::from(format!("/tmp/wombatkv-arena-{prefix}.bin"))
}
#[allow(dead_code)]
fn _atomic_marker(x: &AtomicU64) -> u64 {
x.load(Ordering::Acquire)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_then_read_round_trip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("arena.bin");
let size = 1024 * 1024;
{
let mut w = ArenaWriter::create(&path, size).unwrap();
let payload = vec![0xABu8; 4096];
let (off, len) = w.write_payload(&payload).unwrap();
assert_eq!(len as usize, payload.len());
assert!(off >= ARENA_HEADER_BYTES as u64);
}
let r = ArenaReader::open(&path).unwrap();
let view = r.read_payload(ARENA_HEADER_BYTES as u64, 4096).unwrap();
assert_eq!(view.len(), 4096);
assert!(view.iter().all(|&b| b == 0xAB));
}
#[test]
fn bump_allocator_advances_offset() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("arena.bin");
let mut w = ArenaWriter::create(&path, 64 * 1024).unwrap();
let p1 = vec![0x11; 1000];
let p2 = vec![0x22; 2000];
let p3 = vec![0x33; 3000];
let (o1, l1) = w.write_payload(&p1).unwrap();
let (o2, l2) = w.write_payload(&p2).unwrap();
let (o3, _l3) = w.write_payload(&p3).unwrap();
assert_eq!(o1, ARENA_HEADER_BYTES as u64);
assert_eq!(o2, o1 + u64::from(l1));
assert_eq!(o3, o2 + u64::from(l2));
}
#[test]
fn bump_wraps_when_full() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("arena.bin");
let size = ARENA_HEADER_BYTES as u64 + 1024;
let mut w = ArenaWriter::create(&path, size).unwrap();
let p = vec![0x55; 600];
let (o1, _l1) = w.write_payload(&p).unwrap();
let (o2, _l2) = w.write_payload(&p).unwrap();
assert_eq!(o1, ARENA_HEADER_BYTES as u64);
assert_eq!(o2, ARENA_HEADER_BYTES as u64); assert_eq!(w.wrap_epoch(), 1);
}
#[test]
fn payload_too_large_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("arena.bin");
let size = ARENA_HEADER_BYTES as u64 + 1024;
let mut w = ArenaWriter::create(&path, size).unwrap();
let p = vec![0; 2048];
match w.write_payload(&p) {
Err(ArenaError::PayloadTooLarge { .. }) => {}
other => panic!("expected PayloadTooLarge, got {other:?}"),
}
}
#[test]
fn out_of_bounds_read_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("arena.bin");
let mut w = ArenaWriter::create(&path, 64 * 1024).unwrap();
let _ = w.write_payload(&[0; 100]).unwrap();
drop(w);
let r = ArenaReader::open(&path).unwrap();
assert!(matches!(r.read_payload(0, 64), Err(ArenaError::OutOfBounds { .. })));
assert!(matches!(r.read_payload(64 * 1024 - 32, 64), Err(ArenaError::OutOfBounds { .. })));
}
}