use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use crate::Result;
const SLOT_MAGIC: [u8; 8] = *b"SEKREAD\0";
const SLOT_VERSION: u16 = 1;
const FRAME_LEN: usize = 24;
const SLOT_LEN: usize = FRAME_LEN * 2;
fn encode_frame(generation: u64) -> [u8; FRAME_LEN] {
let mut frame = [0u8; FRAME_LEN];
frame[0..8].copy_from_slice(&SLOT_MAGIC);
frame[8..10].copy_from_slice(&SLOT_VERSION.to_le_bytes());
frame[12..20].copy_from_slice(&generation.to_le_bytes());
let crc = crc32c::crc32c(&frame[..20]);
frame[20..24].copy_from_slice(&crc.to_le_bytes());
frame
}
fn decode_slot(bytes: &[u8]) -> Option<u64> {
if bytes.len() != SLOT_LEN || bytes[..FRAME_LEN] != bytes[FRAME_LEN..] {
return None;
}
let frame = &bytes[..FRAME_LEN];
if frame[..8] != SLOT_MAGIC
|| u16::from_le_bytes(frame[8..10].try_into().ok()?) != SLOT_VERSION
|| frame[10..12] != [0, 0]
|| u32::from_le_bytes(frame[20..24].try_into().ok()?) != crc32c::crc32c(&frame[..20])
{
return None;
}
Some(u64::from_le_bytes(frame[12..20].try_into().ok()?))
}
fn readers_dir(db: &Path) -> PathBuf { db.join("readers") }
pub struct ReaderSlot {
path: PathBuf,
persistent: bool,
_file: std::fs::File, }
impl ReaderSlot {
pub fn register(db: &Path, gen: u64) -> Result<ReaderSlot> {
Self::create(db, gen, true)
}
pub(crate) fn reserve(db: &Path) -> Result<ReaderSlot> {
Self::create(db, 0, false)
}
fn create(db: &Path, gen: u64, sync: bool) -> Result<ReaderSlot> {
if let Some(limits) = crate::limits::read(db)? {
return Self::create_fixed(db, gen, limits.readers);
}
Self::create_with_after_file(db, gen, sync, || {})
}
fn create_with_after_file(db: &Path, gen: u64, sync: bool, after_file: impl FnOnce()) -> Result<ReaderSlot> {
let dir = readers_dir(db);
std::fs::create_dir_all(&dir)?;
static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let mut candidate = tempfile::Builder::new().prefix(".reader-pending-").tempfile_in(db)?;
after_file();
if !crate::io::try_lock_exclusive(candidate.as_file())? {
return Err(std::io::Error::new(std::io::ErrorKind::WouldBlock,
"reader slot file unexpectedly locked").into());
}
let frame = encode_frame(gen);
candidate.write_all(&frame)?;
candidate.write_all(&frame)?;
if sync { candidate.as_file().sync_all()?; }
loop {
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = dir.join(format!("r-{}-{}", std::process::id(), n));
match candidate.persist_noclobber(&path) {
Ok(file) => return Ok(ReaderSlot { path, _file: file, persistent: false }),
Err(e) if e.error.kind() == std::io::ErrorKind::AlreadyExists => candidate = e.file,
Err(e) => return Err(e.error.into()),
}
}
}
fn create_fixed(db: &Path, generation: u64, count: u32) -> Result<Self> {
let dir = readers_dir(db);
std::fs::create_dir_all(&dir)?;
for n in 0..count {
let path = dir.join(format!("fixed-{n}"));
let file = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&path)?;
if !crate::io::try_lock_exclusive(&file)? { continue; }
let mut slot = Self { path, _file: file, persistent: true };
slot.set_generation(generation)?;
return Ok(slot);
}
Err(crate::Error::ResourceLimit("snapshot reader slots full"))
}
pub fn set_generation(&mut self, generation: u64) -> Result<()> {
self._file.rewind()?;
let frame = encode_frame(generation);
self._file.write_all(&frame)?;
self._file.write_all(&frame)?;
self._file.set_len(SLOT_LEN as u64)?;
self._file.sync_all()?;
Ok(())
}
}
impl Drop for ReaderSlot {
fn drop(&mut self) {
if !self.persistent { let _ = std::fs::remove_file(&self.path); }
let _ = crate::io::unlock(&self._file);
}
}
pub fn oldest_live_reader(db: &Path) -> u64 {
live_generations(db).map_or(0, |g| g.first().copied().unwrap_or(u64::MAX))
}
pub(crate) fn live_generations(db: &Path) -> Option<Vec<u64>> {
let dir = readers_dir(db);
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Some(Vec::new()),
Err(_) => return None, };
let mut generations = Vec::new();
for entry in entries {
let Ok(entry) = entry else { return None };
let path = entry.path();
let f = match std::fs::File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return None,
};
match crate::io::try_lock_exclusive(&f) {
Ok(true) => {
if !entry.file_name().to_string_lossy().starts_with("fixed-") {
let _ = std::fs::remove_file(&path);
}
let _ = crate::io::unlock(&f);
continue;
}
Ok(false) => {
let mut f2 = f;
let mut buf = [0u8; SLOT_LEN];
if f2.read_exact(&mut buf).is_err() { return None; }
let mut extra = [0u8; 1];
match f2.read(&mut extra) {
Ok(0) => {}
Ok(_) | Err(_) => return None,
}
let Some(generation) = decode_slot(&buf) else { return None };
if generation == 0 { return None; }
if generations.len() >= 4096 { return None; }
generations.push(generation);
}
Err(_) => return None, }
}
generations.sort_unstable();
generations.dedup();
Some(generations)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_writer_sweep_during_creation_cannot_hide_a_live_reader() {
let d = tempfile::TempDir::new().unwrap();
let _reader = ReaderSlot::create_with_after_file(d.path(), 41, true, || {
assert_eq!(oldest_live_reader(d.path()), u64::MAX);
}).unwrap();
assert_eq!(oldest_live_reader(d.path()), 41,
"registration returned success but writer cannot see the reader");
}
#[test]
fn a_dropped_slot_disappears_and_a_live_one_pins() {
let d = tempfile::TempDir::new().unwrap();
assert_eq!(oldest_live_reader(d.path()), u64::MAX, "empty table: no pin");
let s1 = ReaderSlot::register(d.path(), 41).unwrap();
let _s2 = ReaderSlot::register(d.path(), 44).unwrap();
assert_eq!(oldest_live_reader(d.path()), 41);
drop(s1);
assert_eq!(oldest_live_reader(d.path()), 44);
}
#[test]
fn a_crashed_readers_file_is_swept_not_trusted() {
let d = tempfile::TempDir::new().unwrap();
let dir = readers_dir(d.path());
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("r-99999-0"), 7u64.to_le_bytes()).unwrap();
assert_eq!(oldest_live_reader(d.path()), u64::MAX,
"an unlocked file is a dead reader, not a pin");
assert!(!dir.join("r-99999-0").exists(), "and it is swept");
}
#[test]
fn a_flipped_byte_in_a_live_slot_pins_generation_zero() {
let d = tempfile::TempDir::new().unwrap();
let _slot = ReaderSlot::register(d.path(), 41).unwrap();
let dir = readers_dir(d.path());
let path = std::fs::read_dir(&dir).unwrap().next().unwrap().unwrap().path();
let mut bytes = std::fs::read(&path).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0x80;
std::fs::write(&path, bytes).unwrap();
assert_eq!(
oldest_live_reader(d.path()),
0,
"an unverifiable live reader must stop recycling, never advance its generation",
);
}
}