use crate::compact;
use crate::error::{Error, Result};
use crate::format::{base, wal};
use crate::mmap::MappedFile;
use crate::overlay::{publish_records, LogArena};
use crate::snapshot::Snapshot;
use crate::ttl::TtlConfig;
use crate::writer::{Txn, WriteState, Writer};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Mutex, MutexGuard, RwLock};
use std::time::{Duration, Instant};
const GEN_SLOTS: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Durability {
Always,
Batch(u32),
Never,
}
#[derive(Clone, Copy, Debug)]
pub struct Config {
pub durability: Durability,
pub compact_after_ops: usize,
pub compact_after_wal_bytes: u64,
pub ttl: TtlConfig,
pub lock_wait: Duration,
}
impl Default for Config {
fn default() -> Self {
Self {
durability: Durability::Always,
compact_after_ops: 4096,
compact_after_wal_bytes: 4 << 20,
ttl: TtlConfig::default(),
lock_wait: Duration::from_secs(10),
}
}
}
#[derive(Debug)]
pub(crate) struct GenInner {
map: MappedFile,
pub(crate) dir: base::BaseDir,
pub(crate) log: LogArena,
}
impl GenInner {
pub(crate) fn bytes(&self) -> &[u8] {
self.map.bytes()
}
pub(crate) fn from_parts(map: MappedFile, dir: base::BaseDir) -> Self {
Self { map, dir, log: LogArena::new() }
}
}
#[derive(Debug)]
pub(crate) struct GenSlot {
pub(crate) seq: AtomicU64,
pub(crate) inner: RwLock<Option<GenInner>>,
}
#[derive(Debug)]
pub struct Store {
pub(crate) dir: PathBuf,
pub(crate) base_file: PathBuf,
pub(crate) wal_file: PathBuf,
pub(crate) cfg: Config,
read_only: bool,
pub(crate) current: AtomicUsize,
pub(crate) gens: [GenSlot; GEN_SLOTS],
pub(crate) write: Mutex<WriteState>,
pub(crate) compact_inline: AtomicBool,
pub(crate) due: AtomicBool,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Seen {
pub(crate) stamp: Option<Stamp>,
pub(crate) wal_pos: u64,
pub(crate) log_current: bool,
pub(crate) base_gen: u64,
}
impl Seen {
pub(crate) fn appended(&mut self, n: u64) {
self.wal_pos += n;
if let Some(w) = self.stamp.as_mut().and_then(|s| s.wal.as_mut()) {
w.1 += n;
}
}
}
#[derive(Debug)]
pub struct CommitLock {
_held: Held,
dir: PathBuf,
}
#[derive(Debug)]
pub(crate) struct Held(pub(crate) fs::File);
fn open_guarded(dir: &Path, name: &str) -> Result<fs::File> {
let path = dir.join(name);
match fs::symlink_metadata(&path) {
Ok(md) if !md.file_type().is_file() => {
return Err(Error::corrupt(format!("{name} is not a regular file")));
}
_ => {}
}
Ok(fs::OpenOptions::new().create(true).truncate(false).write(true).open(path)?)
}
const NAP_MIN: Duration = Duration::from_micros(250);
const NAP_MAX: Duration = Duration::from_millis(2);
fn try_commit_lock(lock: &fs::File) -> Result<bool> {
#[cfg(test)]
probe::bump(&probe::LOCKS, 1);
match lock.try_lock() {
Ok(()) => Ok(true),
Err(fs::TryLockError::WouldBlock) => Ok(false),
Err(fs::TryLockError::Error(e)) => Err(e.into()),
}
}
fn lock_within(lock: &fs::File, turnstile: &fs::File, wait: Duration) -> Result<()> {
#[cfg(test)]
probe::bump(&probe::TURNSTILE, 1);
let queued = match turnstile.try_lock() {
Ok(()) => {
turnstile.unlock()?;
if try_commit_lock(lock)? {
return Ok(());
}
false
}
Err(fs::TryLockError::WouldBlock) => true,
Err(fs::TryLockError::Error(e)) => return Err(e.into()),
};
if wait.is_zero() {
return Err(Error::Locked);
}
let start = Instant::now();
let left = || wait.saturating_sub(start.elapsed());
loop {
match turnstile.try_lock_shared() {
Ok(()) => break,
Err(fs::TryLockError::WouldBlock) => {}
Err(fs::TryLockError::Error(e)) => return Err(e.into()),
}
if left().is_zero() {
return Err(Error::Locked);
}
std::thread::sleep(NAP_MIN.min(left()));
}
let mut nap = if queued { NAP_MAX } else { NAP_MIN };
let won = loop {
let l = left();
if l.is_zero() {
break Err(Error::Locked);
}
std::thread::sleep(nap.min(l));
nap = (nap * 2).min(NAP_MAX);
match try_commit_lock(lock) {
Ok(false) => {}
done => break done.map(|_| ()),
}
};
let _ = turnstile.unlock();
won
}
fn lock_dir(dir: &Path, wait: Duration) -> Result<Held> {
let file = open_guarded(dir, LOCK_FILE)?;
lock_within(&file, &open_guarded(dir, WAIT_FILE)?, wait)?;
Ok(Held(file))
}
#[cfg(test)]
pub(crate) mod probe {
use std::cell::Cell;
use std::thread::LocalKey;
thread_local! {
pub(crate) static LOCKS: Cell<u64> = const { Cell::new(0) };
pub(crate) static TURNSTILE: Cell<u64> = const { Cell::new(0) };
pub(crate) static FSYNCS: Cell<u64> = const { Cell::new(0) };
pub(crate) static WRITES: Cell<u64> = const { Cell::new(0) };
pub(crate) static TAIL_BYTES: Cell<u64> = const { Cell::new(0) };
pub(crate) static FULL: Cell<u64> = const { Cell::new(0) };
pub(crate) static FAIL_APPEND: Cell<u64> = const { Cell::new(0) };
pub(crate) static STUCK: Cell<u64> = const { Cell::new(0) };
}
pub(crate) fn full() -> bool {
FULL.with(Cell::get) != 0
}
pub(crate) fn stuck() -> bool {
STUCK.with(Cell::get) != 0
}
pub(crate) fn fail_append() -> bool {
FAIL_APPEND.with(|x| x.get().checked_sub(1).map(|n| x.set(n)).is_some())
}
pub(crate) fn bump(c: &'static LocalKey<Cell<u64>>, n: u64) {
c.with(|x| x.set(x.get() + n));
}
pub(crate) fn take(c: &'static LocalKey<Cell<u64>>) -> u64 {
c.with(|x| x.replace(0))
}
}
const RACE_RETRIES: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Stamp {
base: FileId,
wal: Option<(Ident, u64)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct FileId {
len: u64,
modified: Option<std::time::SystemTime>,
ident: Ident,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Ident {
#[cfg(unix)]
ino: (u64, u64),
#[cfg(not(unix))]
created: Option<std::time::SystemTime>,
}
impl Ident {
fn of(md: &fs::Metadata) -> Self {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt as _;
Self {
#[cfg(unix)]
ino: (md.dev(), md.ino()),
#[cfg(not(unix))]
created: md.created().ok(),
}
}
}
impl FileId {
fn of(md: &fs::Metadata) -> Self {
Self { len: md.len(), modified: md.modified().ok(), ident: Ident::of(md) }
}
}
impl Stamp {
pub(crate) fn read(dir: &Path) -> Option<Self> {
Self::of(&base_path(dir), &wal_path(dir))
}
fn of(base: &Path, wal: &Path) -> Option<Self> {
let base = FileId::of(&fs::metadata(base).ok()?);
let wal = match fs::metadata(wal) {
Ok(md) => Some((Ident::of(&md), md.len())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(_) => return None,
};
Some(Self { base, wal })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Caught {
Nothing,
Tail,
Full,
}
pub const BASE_FILE: &str = "graph.base";
pub const WAL_FILE: &str = "graph.wal";
pub const LOCK_FILE: &str = "graph.lock";
pub const WAIT_FILE: &str = "graph.wait";
pub(crate) fn base_path(dir: &Path) -> PathBuf {
dir.join(BASE_FILE)
}
pub(crate) fn wal_path(dir: &Path) -> PathBuf {
dir.join(WAL_FILE)
}
use crate::fsio::now_ms;
pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
crate::fsio::write_atomic(path, &[bytes]).map_err(Into::into)
}
fn lay_down(dir: &Path) -> Result<()> {
if !wal_path(dir).exists() {
atomic_write(&wal_path(dir), &wal::encode_header(1, now_ms()))?;
}
atomic_write(&base_path(dir), &compact::render_empty_base(1))
}
impl Store {
pub fn create(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
let dir = dir.as_ref();
fs::create_dir_all(dir)?;
{
let _held = lock_dir(dir, cfg.lock_wait)?;
if base_path(dir).exists() {
return Err(Error::format("store already exists; use open"));
}
lay_down(dir)?;
}
Self::open_with(dir, cfg, false)
}
pub fn open_or_create(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
let dir = dir.as_ref();
if !base_path(dir).exists() {
fs::create_dir_all(dir)?;
let _held = lock_dir(dir, cfg.lock_wait)?;
if !base_path(dir).exists() {
lay_down(dir)?;
}
}
Self::open_with(dir, cfg, false)
}
pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
Self::open_with(dir.as_ref(), Config::default(), false)
}
pub fn open_cfg(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
Self::open_with(dir.as_ref(), cfg, false)
}
pub fn open_read_only(dir: impl AsRef<Path>) -> Result<Self> {
Self::open_with(dir.as_ref(), Config::default(), true)
}
fn open_with(dir: &Path, cfg: Config, read_only: bool) -> Result<Self> {
let stamp = Stamp::read(dir);
let built = match load_generation(dir, false) {
Err(Error::Busy { .. }) if !read_only => match lock_now(dir)? {
Some(_held) => load_generation(dir, true)?,
None => return Err(Error::busy("compaction in progress; retry")),
},
other => other?,
};
let repair = !read_only && built.log != LogState::Current { torn: false };
let store = Self::assemble(dir.to_path_buf(), cfg, read_only, stamp, built);
if repair {
store.repair_now()?;
}
Ok(store)
}
fn repair_now(&self) -> Result<()> {
let Some(held) = lock_now(&self.dir)? else { return Ok(()) };
let mut state = self.write_state()?;
let r = self.sync(&mut state, true).map(|_| ());
drop(held);
r
}
pub fn open_image(base: bytes::Bytes, wal: &[u8]) -> Result<Self> {
let built = build_generation(MappedFile::from_bytes(base), wal.to_vec())?;
if built.log == LogState::Newer {
return Err(newer_image());
}
Ok(Self::assemble(PathBuf::new(), Config::default(), true, None, built))
}
pub fn reload_image(&self, base: Option<bytes::Bytes>, wal: &[u8]) -> Result<bool> {
if !self.read_only {
return Ok(false);
}
let _one_at_a_time = self.write.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
match base {
Some(b) => self
.install_or_apply(MappedFile::from_bytes(b), wal.to_vec())?
.map(|a| a.changed)
.ok_or_else(newer_image),
None => match self.apply_log(wal)? {
Some((changed, _)) => Ok(changed),
None => {
Err(Error::format("log image belongs to another generation; pass its base"))
}
},
}
}
#[must_use]
pub fn is_image(&self) -> bool {
self.dir.as_os_str().is_empty()
}
fn assemble(
dir: PathBuf,
cfg: Config,
read_only: bool,
stamp: Option<Stamp>,
built: Built,
) -> Self {
let generation = built.inner.dir.header.generation;
let seen = Seen {
stamp,
wal_pos: built.good_len as u64,
log_current: matches!(built.log, LogState::Current { .. }),
base_gen: generation,
};
let store = Self {
base_file: base_path(&dir),
wal_file: wal_path(&dir),
dir,
cfg,
read_only,
current: AtomicUsize::new(0),
gens: std::array::from_fn(|_| GenSlot {
seq: AtomicU64::new(0),
inner: RwLock::new(None),
}),
write: Mutex::new(WriteState::new(
built.next_seq,
built.replayed,
built.good_len as u64,
generation,
seen,
)),
compact_inline: AtomicBool::new(true),
due: AtomicBool::new(false),
};
*store.gens[0].inner.write().expect("fresh lock") = Some(built.inner);
store.gens[0].seq.store(generation, Ordering::Release);
store
}
#[must_use]
pub fn len(&self) -> usize {
self.snapshot().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn dir(&self) -> &Path {
&self.dir
}
#[must_use]
pub fn generation(&self) -> u64 {
self.gens[self.current.load(Ordering::Acquire)].seq.load(Ordering::Acquire)
}
pub fn set_compact_inline(&self, inline: bool) {
self.compact_inline.store(inline, Ordering::Relaxed);
}
#[must_use]
pub fn compaction_due(&self) -> bool {
self.due.load(Ordering::Acquire)
}
#[must_use]
pub fn snapshot(&self) -> Snapshot<'_> {
loop {
let cur = self.current.load(Ordering::Acquire);
if let Ok(guard) = self.gens[cur].inner.try_read() {
if guard.is_some() && self.current.load(Ordering::Acquire) == cur {
return Snapshot::new(self, guard);
}
}
std::hint::spin_loop();
}
}
fn write_state(&self) -> Result<MutexGuard<'_, WriteState>> {
self.write.lock().map_err(|_| Error::corrupt("writer mutex poisoned"))
}
pub fn writer(&self) -> Result<Writer<'_>> {
if self.read_only {
return Err(Error::ReadOnly);
}
Ok(Writer::new(self, self.write_state()?))
}
pub fn transact<F, R>(&self, f: F) -> Result<R>
where
F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
{
crate::writer::transact(self, None, self.cfg.lock_wait, f)
}
pub fn try_transact<F, R>(&self, f: F) -> Result<R>
where
F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
{
crate::writer::transact(self, None, Duration::ZERO, f)
}
pub fn commit_lock(&self) -> Result<CommitLock> {
self.commit_lock_within(self.cfg.lock_wait)
}
pub fn try_commit_lock(&self) -> Result<CommitLock> {
self.commit_lock_within(Duration::ZERO)
}
pub fn commit_lock_within(&self, wait: Duration) -> Result<CommitLock> {
if self.read_only {
return Err(Error::ReadOnly);
}
let mut state = self.write_state()?;
let held = lock_dir(&self.dir, wait)?;
self.sync(&mut state, true)?;
Ok(CommitLock { _held: held, dir: self.dir.clone() })
}
pub fn transact_with<F, R>(&self, lock: &CommitLock, f: F) -> Result<R>
where
F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
{
if lock.dir != self.dir {
return Err(Error::format("the commit lock belongs to another store"));
}
crate::writer::transact(self, Some(lock), Duration::ZERO, f)
}
pub fn compact(&self) -> Result<compact::CompactStats> {
if self.read_only {
return Err(Error::ReadOnly);
}
let mut state = self.write_state()?;
let held = self.acquire(&mut state, self.cfg.lock_wait)?;
let r = self.sync(&mut state, true).and_then(|_| compact::compact_locked(self, &mut state));
state.release(held);
r
}
pub fn reload_if_stale(&self) -> Result<bool> {
if self.is_image() {
return Ok(false); }
let mut state = if self.read_only {
self.write.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
} else {
match self.write.try_lock() {
Ok(g) => g,
Err(std::sync::TryLockError::WouldBlock) => return Ok(false),
Err(std::sync::TryLockError::Poisoned(p)) => p.into_inner(),
}
};
self.sync(&mut state, false)
}
pub(crate) fn acquire(&self, state: &mut WriteState, wait: Duration) -> Result<Held> {
let file = match state.lock_file.take() {
Some(f) => f,
None => open_guarded(&self.dir, LOCK_FILE)?,
};
if state.wait_file.is_none() {
match open_guarded(&self.dir, WAIT_FILE) {
Ok(t) => state.wait_file = Some(t),
Err(e) => {
state.lock_file = Some(file);
return Err(e);
}
}
}
let turnstile = state.wait_file.as_ref().expect("opened above");
match lock_within(&file, turnstile, wait) {
Ok(()) => Ok(Held(file)),
Err(e) => {
state.lock_file = Some(file);
Err(e)
}
}
}
pub(crate) fn sync(&self, state: &mut WriteState, repair: bool) -> Result<bool> {
let (changed, caught) = self.catch_up(&mut state.seen, repair)?;
if caught == Caught::Nothing && !repair {
return Ok(false);
}
if caught == Caught::Full {
state.drop_wal_handle(); }
if repair {
self.repair(state)?;
}
let cur = self.current.load(Ordering::Acquire);
let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
let g = guard.as_ref().ok_or_else(|| Error::corrupt("current slot empty"))?;
let next_seq = g.dir.header.wal_applied_seq.max(g.log.last_seq()) + 1;
state.adopt(next_seq, g.log.len(), state.seen.base_gen);
if caught == Caught::Full {
let over = state.ops_since_compact >= self.cfg.compact_after_ops
|| state.wal_bytes >= self.cfg.compact_after_wal_bytes;
self.due.store(over && !self.compact_inline.load(Ordering::Relaxed), Ordering::Release);
}
Ok(changed)
}
fn repair(&self, state: &mut WriteState) -> Result<()> {
let seen = &mut state.seen;
let file = &self.wal_file;
if !seen.log_current {
atomic_write(file, &wal::encode_header(seen.base_gen, now_ms()))?;
*seen = Seen {
stamp: Stamp::of(&self.base_file, file),
wal_pos: wal::HEADER_LEN as u64,
log_current: true,
base_gen: seen.base_gen,
};
state.drop_wal_handle();
return Ok(());
}
if let Some((ident, len)) = seen.stamp.and_then(|s| s.wal) {
if len > seen.wal_pos {
let f = fs::OpenOptions::new().write(true).open(file)?;
f.set_len(seen.wal_pos)?;
f.sync_all()?;
if let Some(s) = seen.stamp.as_mut() {
s.wal = Some((ident, seen.wal_pos));
}
}
}
Ok(())
}
fn catch_up(&self, seen: &mut Seen, locked: bool) -> Result<(bool, Caught)> {
let now = Stamp::of(&self.base_file, &self.wal_file);
if let (Some(n), Some(l)) = (now, seen.stamp) {
if n == l {
return Ok((false, Caught::Nothing));
}
if let (Some((ni, nlen)), Some((li, _))) = (n.wal, l.wal) {
if n.base == l.base
&& ni == li
&& nlen >= seen.wal_pos
&& seen.wal_pos >= wal::HEADER_LEN as u64
{
let (changed, pos) = self.reload_wal_tail(seen.wal_pos)?;
*seen = Seen { stamp: now, wal_pos: pos, ..*seen };
return Ok((changed, Caught::Tail));
}
}
}
let (changed, pos, log_current, base_gen) = self.reload_full(locked)?;
*seen = Seen { stamp: now, wal_pos: pos, log_current, base_gen };
Ok((changed, Caught::Full))
}
fn reload_wal_tail(&self, pos: u64) -> Result<(bool, u64)> {
use std::io::{Read as _, Seek as _, SeekFrom};
let mut tail = Vec::new();
match fs::File::open(&self.wal_file) {
Ok(mut f) => {
f.seek(SeekFrom::Start(pos))?;
f.read_to_end(&mut tail)?;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok((false, pos)),
Err(e) => return Err(e.into()),
}
#[cfg(test)]
probe::bump(&probe::TAIL_BYTES, tail.len() as u64);
let scanned = wal::scan(&tail, 0, None);
if let Some(e) = scanned.corrupt {
return Err(e);
}
tail.truncate(scanned.end);
Ok((self.publish_new(tail, 0)?, pos + scanned.end as u64))
}
fn publish_new(&self, seg: Vec<u8>, from: usize) -> Result<bool> {
let cur = self.current.load(Ordering::Acquire);
let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
let Some(g) = guard.as_ref() else { return Ok(false) };
publish_above(&g.log, g.dir.header.wal_applied_seq.max(g.log.last_seq()), seg, from)
}
fn apply_log(&self, raw: &[u8]) -> Result<Option<(bool, usize)>> {
if raw.is_empty() {
return Ok(Some((false, wal::HEADER_LEN)));
}
if wal::parse_header(raw)? != self.generation() {
return Ok(None);
}
let replayed = wal::replay(raw)?;
let served = self.served_seq()?;
let start = replayed.records().find(|r| r.last_seq() > served).map(|r| r.offset());
let changed = match start {
Some(at) => self.publish_new(raw[at..replayed.good_len].to_vec(), 0)?,
None => false,
};
Ok(Some((changed, replayed.good_len)))
}
fn reload_full(&self, locked: bool) -> Result<(bool, u64, bool, u64)> {
for _ in 0..RACE_RETRIES {
let map = MappedFile::open(&self.base_file)?;
let base_gen = base::Header::parse(map.bytes())?.generation;
let raw = match fs::read(&self.wal_file) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(e) => return Err(e.into()),
};
if let Some(a) = self.install_or_apply(map, raw)? {
return Ok((a.changed, a.pos as u64, a.current, base_gen));
}
if locked {
return Err(Error::corrupt("wal is newer than base"));
}
}
Err(Error::busy("compaction in progress; retry"))
}
fn install_or_apply(&self, map: MappedFile, raw: Vec<u8>) -> Result<Option<Applied>> {
let cur_gen = self.generation();
let new_gen = base::Header::parse(map.bytes())?.generation;
if new_gen == cur_gen {
return Ok(match self.apply_log(&raw)? {
Some((changed, pos)) => Some(Applied { changed, pos, current: !raw.is_empty() }),
None if wal::parse_header(&raw)? > new_gen => None,
None => Some(Applied { changed: false, pos: wal::HEADER_LEN, current: false }),
});
}
let built = build_generation(map, raw)?;
let pos = built.good_len;
let current = matches!(built.log, LogState::Current { .. });
if built.log == LogState::Newer {
return Ok(None);
}
let folded = built.inner.dir.header.wal_applied_seq;
match self.install(built.inner, new_gen) {
None => Ok(Some(Applied { changed: true, pos, current })),
Some(mut inner) => {
if folded > self.served_seq()? {
return Err(Error::busy("all generation slots pinned by snapshots"));
}
let changed = match inner.log.take_first_segment() {
Some(seg) => self.publish_new(seg, wal::HEADER_LEN)?,
None => false,
};
Ok(Some(Applied { changed, pos, current }))
}
}
}
fn served_seq(&self) -> Result<u64> {
let cur = self.current.load(Ordering::Acquire);
let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
Ok(guard.as_ref().map_or(0, |g| g.dir.header.wal_applied_seq.max(g.log.last_seq())))
}
#[must_use]
pub(crate) fn install(&self, inner: GenInner, generation: u64) -> Option<GenInner> {
let cur = self.current.load(Ordering::Acquire);
for (i, slot) in self.gens.iter().enumerate() {
if i == cur {
continue;
}
if let Ok(mut w) = slot.inner.try_write() {
*w = Some(inner);
drop(w);
slot.seq.store(generation, Ordering::Release);
self.current.store(i, Ordering::Release);
return None;
}
}
Some(inner)
}
}
fn publish_above(log: &LogArena, served: u64, seg: Vec<u8>, from: usize) -> Result<bool> {
let new: usize = wal::Records::trusted(&seg, from, seg.len())
.map(|r| match r.last_seq().checked_sub(served) {
Some(n) if n > 0 => n.min(u64::from(r.op_count())) as usize,
_ => 0,
})
.sum();
if new == 0 {
return Ok(false);
}
if !log.reserve(new, seg.len()) {
return Err(Error::busy("overlay arena full"));
}
publish_records(log, seg, from, served);
Ok(true)
}
struct Applied {
changed: bool,
pos: usize,
current: bool,
}
fn lock_now(dir: &Path) -> Result<Option<Held>> {
match lock_dir(dir, Duration::ZERO) {
Ok(h) => Ok(Some(h)),
Err(Error::Locked) => Ok(None),
Err(e) => Err(e),
}
}
fn newer_image() -> Error {
Error::busy("log image is newer than its base; fetch the base again")
}
struct Built {
inner: GenInner,
next_seq: u64,
replayed: usize,
good_len: usize,
log: LogState,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LogState {
Empty,
Stale,
Current { torn: bool },
Newer,
}
fn build_generation(map: MappedFile, mut raw: Vec<u8>) -> Result<Built> {
let bdir = base::BaseDir::parse(map.bytes())?;
let header = bdir.header;
let inner = GenInner { map, dir: bdir, log: LogArena::new() };
let folded = Built {
inner,
next_seq: header.wal_applied_seq + 1,
replayed: 0,
good_len: 0,
log: LogState::Empty,
};
if raw.is_empty() {
return Ok(folded);
}
let generation = wal::parse_header(&raw)?;
if generation > header.generation {
return Ok(Built { log: LogState::Newer, ..folded });
}
if generation < header.generation {
return Ok(Built { good_len: wal::HEADER_LEN, log: LogState::Stale, ..folded });
}
let scanned = wal::scan(&raw, wal::HEADER_LEN, None);
if let Some(e) = scanned.corrupt {
return Err(e);
}
let good_len = scanned.end;
let torn = good_len < raw.len();
let applied = header.wal_applied_seq;
let next_seq = scanned.last_seq.map_or(applied, |l| l.max(applied)) + 1;
raw.truncate(good_len);
publish_above(&folded.inner.log, applied, raw, wal::HEADER_LEN)
.map_err(|_| Error::busy("overlay arena full during replay"))?;
let replayed = folded.inner.log.len();
Ok(Built { next_seq, replayed, good_len, log: LogState::Current { torn }, ..folded })
}
fn load_generation(dir: &Path, locked: bool) -> Result<Built> {
let wal_file = wal_path(dir);
for _ in 0..RACE_RETRIES {
let map = MappedFile::open(&base_path(dir))?;
let raw = match fs::read(&wal_file) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(e) => return Err(e.into()),
};
let built = build_generation(map, raw)?;
if built.log != LogState::Newer {
return Ok(built);
}
if locked {
return Err(Error::corrupt("wal is newer than base"));
}
}
Err(Error::busy("compaction in progress; retry"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::writer::DocRecord;
use crate::UrlKey;
use std::time::{Duration, Instant};
fn doc(url: &str, hash: u64) -> DocRecord<'_> {
DocRecord {
url,
url_key: UrlKey::of(url),
content_hash: hash,
fetched_at_ms: 1,
title: None,
snippet: None,
etag: None,
pinned: false,
}
}
fn seqs(raw: &[u8]) -> Vec<u64> {
wal::replay(raw).unwrap().records().flat_map(|r| r.first_seq()..=r.last_seq()).collect()
}
fn write(store: &Store, url: &str, hash: u64) {
let mut w = store.writer().unwrap();
w.upsert_node(&doc(url, hash)).unwrap();
w.commit().unwrap();
}
#[test]
fn a_snapshot_does_not_observe_later_commits() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
let before = store.snapshot();
assert_eq!(before.len(), 1);
write(&store, "https://x.dev/b", 2);
assert_eq!(before.len(), 1, "an existing snapshot must not grow");
assert_eq!(store.snapshot().len(), 2, "a fresh snapshot sees the commit");
}
#[test]
fn compaction_leaves_an_existing_snapshot_on_its_own_generation() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
store.compact().unwrap();
let pinned = store.snapshot();
let pinned_gen = pinned.generation();
write(&store, "https://x.dev/b", 2);
store.compact().unwrap();
assert_eq!(pinned.generation(), pinned_gen, "a pinned generation must not move");
assert_eq!(pinned.len(), 1, "the old generation still reads its own state");
let fresh = store.snapshot();
assert!(fresh.generation() > pinned_gen, "the store advanced");
assert_eq!(fresh.len(), 2);
}
#[test]
fn each_compaction_advances_the_generation_exactly_once() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
let mut prev = store.snapshot().generation();
for _ in 0..6 {
store.compact().unwrap();
let now = store.snapshot().generation();
assert_eq!(now, prev + 1, "a compaction must advance the generation by one");
prev = now;
}
}
#[test]
fn exhausting_the_generation_ring_is_a_retryable_refusal() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
let mut pins = Vec::new();
for round in 0..GEN_SLOTS - 1 {
pins.push(store.snapshot());
store.compact().unwrap_or_else(|e| panic!("round {round} should still fit: {e}"));
}
pins.push(store.snapshot());
assert!(
matches!(store.compact(), Err(Error::Busy { .. })),
"a full ring must refuse with Busy, not stall or corrupt the swap"
);
drop(pins);
store.compact().expect("compaction must succeed once a slot frees up");
assert_eq!(store.snapshot().len(), 1);
}
#[test]
fn a_read_only_handle_reloads_a_newer_generation() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
store.compact().unwrap();
let reader = Store::open_read_only(dir.path()).unwrap();
assert_eq!(reader.snapshot().len(), 1);
assert!(!reader.reload_if_stale().unwrap(), "nothing has changed yet");
write(&store, "https://x.dev/b", 2);
store.compact().unwrap();
assert!(reader.reload_if_stale().unwrap(), "a new generation is visible");
assert_eq!(reader.snapshot().len(), 2);
}
fn quick() -> Config {
Config { lock_wait: Duration::from_millis(30), ..Config::default() }
}
fn urls(store: &Store) -> Vec<String> {
let mut v = Vec::new();
store.snapshot().for_each_live(|n| v.push(n.url.to_owned()));
v.sort();
v
}
#[test]
fn a_commit_catches_up_with_a_grown_log_first() {
let dir = tempfile::tempdir().unwrap();
let a = Store::create(dir.path(), Config::default()).unwrap();
let b = Store::open(dir.path()).unwrap();
write(&a, "https://x.dev/a", 1);
write(&b, "https://x.dev/b", 2);
write(&a, "https://x.dev/c", 3);
assert_eq!(urls(&a), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
let raw = fs::read(wal_path(dir.path())).unwrap();
let seqs = seqs(&raw);
assert_eq!(seqs, [1, 2, 3], "one sequence across both writers");
assert!(b.reload_if_stale().unwrap(), "a writable handle reloads too");
assert_eq!(urls(&b), urls(&a));
}
#[test]
fn a_commit_catches_up_after_another_handle_compacted() {
let dir = tempfile::tempdir().unwrap();
let a = Store::create(dir.path(), Config::default()).unwrap();
let b = Store::open(dir.path()).unwrap();
write(&a, "https://x.dev/a", 1); write(&b, "https://x.dev/b", 2);
b.compact().unwrap();
let generation = b.generation();
write(&a, "https://x.dev/c", 3);
assert_eq!(a.generation(), generation, "the compacted base was installed");
drop((a, b));
let fresh = Store::open_read_only(dir.path()).unwrap();
assert_eq!(urls(&fresh), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
}
#[test]
fn a_commit_past_lock_wait_is_locked_and_keeps_its_ops() {
let dir = tempfile::tempdir().unwrap();
let a = Store::create(dir.path(), Config::default()).unwrap();
let b = Store::open_cfg(dir.path(), quick()).unwrap();
let held = a.commit_lock().unwrap();
let mut w = b.writer().unwrap();
w.upsert_node(&doc("https://x.dev/a", 1)).unwrap();
let started = Instant::now();
assert!(matches!(w.commit(), Err(Error::Locked)));
assert!(started.elapsed() >= Duration::from_millis(30), "it waited lock_wait");
let third = Store::open_cfg(dir.path(), quick()).unwrap();
assert!(matches!(third.compact(), Err(Error::Locked)), "compaction takes the same lock");
let patient = Store::open(dir.path()).unwrap(); let started = Instant::now();
let ran = std::cell::Cell::new(false);
let refused = patient.try_transact(|_| {
ran.set(true);
Ok(())
});
assert!(matches!(refused, Err(Error::Locked)) && !ran.get());
assert!(started.elapsed() < Duration::from_secs(5), "try_transact waited");
drop(held);
w.commit().unwrap();
drop(w);
assert_eq!(b.len(), 1);
}
#[test]
fn an_aborted_transaction_writes_nothing() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
let before = fs::read(wal_path(dir.path())).unwrap();
let r: Result<()> = store.transact(|tx| {
tx.upsert_node(&doc("https://x.dev/a", 1))?;
Err(Error::format("caller changed its mind"))
});
assert!(r.is_err());
assert_eq!(fs::read(wal_path(dir.path())).unwrap(), before, "nothing appended");
assert!(store.is_empty());
store
.transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2)))
.expect("the next transaction commits");
assert_eq!(urls(&store), ["https://x.dev/b"], "the aborted op never resurfaces");
}
#[test]
fn transactions_allocate_ids_without_collisions() {
let dir = tempfile::tempdir().unwrap();
let a = Store::create(dir.path(), Config::default()).unwrap();
let b = Store::open(dir.path()).unwrap();
let next = |store: &Store| {
store
.transact(|tx| {
let id = tx.snapshot().len();
let url = format!("https://x.dev/id/{id}");
tx.upsert_node(&doc(&url, id as u64))?;
Ok(id)
})
.unwrap()
};
let ids: Vec<usize> = (0..10).map(|i| next(if i % 2 == 0 { &a } else { &b })).collect();
assert_eq!(ids, (0..10).collect::<Vec<_>>());
}
#[test]
fn open_takes_no_lock_and_a_transaction_is_one_lock_one_fsync() {
let dir = tempfile::tempdir().unwrap();
drop(Store::create(dir.path(), Config::default()).unwrap());
probe::take(&probe::LOCKS);
let store = Store::open(dir.path()).unwrap();
let other = Store::open(dir.path()).unwrap();
assert_eq!(probe::take(&probe::LOCKS), 0, "open made a lock call");
drop(lock_dir(dir.path(), Duration::ZERO).expect("the lock is free after open"));
probe::take(&probe::LOCKS);
probe::take(&probe::FSYNCS);
probe::take(&probe::TURNSTILE);
probe::take(&probe::WRITES);
store
.transact(|tx| {
for i in 0..100 {
tx.upsert_node(&doc(&format!("https://x.dev/{i}"), i))?;
}
Ok(())
})
.unwrap();
assert_eq!(probe::take(&probe::LOCKS), 1, "one lock per transaction");
assert_eq!(probe::take(&probe::TURNSTILE), 1, "one wait-queue probe per transaction");
assert_eq!(probe::take(&probe::WRITES), 1, "one write per transaction");
assert_eq!(probe::take(&probe::FSYNCS), 1, "one fsync per transaction");
let raw = fs::read(wal_path(dir.path())).unwrap();
assert_eq!(wal::replay(&raw).unwrap().records().count(), 1, "one record per transaction");
assert_eq!(other.len(), 0);
assert!(other.reload_if_stale().unwrap());
assert_eq!(other.len(), 100);
}
#[test]
fn a_writer_queues_behind_an_announced_waiter() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
let waiter = open_guarded(dir.path(), WAIT_FILE).unwrap();
waiter.try_lock_shared().unwrap();
let r = store.try_transact(|tx| tx.upsert_node(&doc("https://x.dev/a", 1)));
assert!(matches!(r, Err(Error::Locked)), "jumped the queue: {r:?}");
probe::take(&probe::LOCKS);
store.transact(|tx| tx.upsert_node(&doc("https://x.dev/a", 1))).unwrap();
assert_eq!(probe::take(&probe::LOCKS), 1, "tried the lock before standing back");
waiter.unlock().unwrap();
store.try_transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2))).unwrap();
assert_eq!(store.len(), 2);
}
#[test]
fn a_catch_up_reads_only_the_appended_bytes() {
let dir = tempfile::tempdir().unwrap();
let a = Store::create(dir.path(), Config::default()).unwrap();
let b = Store::open(dir.path()).unwrap();
for i in 0..50 {
write(&a, &format!("https://x.dev/{i}"), i);
}
assert!(b.reload_if_stale().unwrap());
let len = || fs::metadata(wal_path(dir.path())).unwrap().len();
let before = len();
write(&a, "https://x.dev/new", 99);
let appended = len() - before;
probe::take(&probe::TAIL_BYTES);
write(&b, "https://x.dev/mine", 100);
assert_eq!(probe::take(&probe::TAIL_BYTES), appended, "read only a's append");
assert_eq!(b.len(), 52);
}
#[test]
fn the_next_committer_repairs_a_torn_tail() {
use std::io::Write as _;
let dir = tempfile::tempdir().unwrap();
let a = Store::create(dir.path(), Config::default()).unwrap();
let b = Store::open(dir.path()).unwrap();
write(&a, "https://x.dev/a", 1);
let intact = fs::metadata(wal_path(dir.path())).unwrap().len();
let mut f = fs::OpenOptions::new().append(true).open(wal_path(dir.path())).unwrap();
f.write_all(&[0x40, 0, 0, 0, 1, 2, 3]).unwrap();
drop(f);
write(&b, "https://x.dev/b", 2);
let raw = fs::read(wal_path(dir.path())).unwrap();
assert_eq!(wal::committed_len(&raw), raw.len(), "no garbage left in the log");
assert!(raw.len() as u64 > intact);
drop((a, b));
let fresh = Store::open_read_only(dir.path()).unwrap();
assert_eq!(urls(&fresh), ["https://x.dev/a", "https://x.dev/b"]);
}
fn wal_len(dir: &Path) -> u64 {
fs::metadata(wal_path(dir)).unwrap().len()
}
#[test]
fn a_full_overlay_refuses_before_writing_and_keeps_the_ops() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
let before = wal_len(dir.path());
let mut w = store.writer().unwrap();
w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
probe::FULL.with(|f| f.set(1));
let r = w.commit();
probe::FULL.with(|f| f.set(0));
assert!(matches!(r, Err(Error::Busy { .. })), "{r:?}");
assert_eq!(wal_len(dir.path()), before, "a refused commit wrote to the log");
w.commit().expect("the staged op commits once there is room");
drop(w);
assert_eq!(urls(&store), ["https://x.dev/a", "https://x.dev/b"]);
}
#[test]
fn a_failed_append_keeps_the_ops() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
let before = wal_len(dir.path());
let mut w = store.writer().unwrap();
w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
probe::FAIL_APPEND.with(|f| f.set(1));
assert!(matches!(w.commit(), Err(Error::Io { .. })));
assert_eq!(wal_len(dir.path()), before, "the failed append was cut back");
w.commit().expect("the kept op commits on retry");
drop(w);
assert_eq!(urls(&store), ["https://x.dev/a", "https://x.dev/b"]);
}
#[test]
fn a_failed_append_that_stays_in_the_log_never_reuses_a_sequence() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
write(&store, "https://x.dev/a", 1);
let mut w = store.writer().unwrap();
w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
probe::FAIL_APPEND.with(|f| f.set(1));
probe::STUCK.with(|f| f.set(1));
let failed = w.commit();
probe::STUCK.with(|f| f.set(0));
assert!(matches!(failed, Err(Error::Io { .. })));
w.commit().expect("the kept op commits on retry");
w.upsert_node(&doc("https://x.dev/c", 3)).unwrap();
w.commit().unwrap();
drop(w);
drop(store);
let raw = fs::read(wal_path(dir.path())).unwrap();
wal::replay(&raw).expect("no sequence regression");
let seqs = seqs(&raw);
assert!(seqs.windows(2).all(|p| p[0] < p[1]), "sequences {seqs:?}");
let reopened = Store::open(dir.path()).expect("the store reopens");
assert_eq!(urls(&reopened), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
}
#[test]
fn a_failed_transaction_leaves_nothing_of_its_own_staged() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
probe::FAIL_APPEND.with(|f| f.set(1));
let r = store.transact(|tx| tx.upsert_node(&doc("https://x.dev/a", 1)));
assert!(matches!(r, Err(Error::Io { .. })), "{r:?}");
store.transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2))).unwrap();
assert_eq!(urls(&store), ["https://x.dev/b"]);
}
#[test]
fn an_inline_compaction_that_cannot_install_leaves_the_commit_ok_and_due() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config { compact_after_ops: 1, ..Config::default() };
let store = Store::create(dir.path(), cfg).unwrap();
let mut pins = vec![store.snapshot()];
for (i, url) in ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"].iter().enumerate()
{
write(&store, url, i as u64);
pins.push(store.snapshot());
}
let mut w = store.writer().unwrap();
w.upsert_node(&doc("https://x.dev/d", 4)).unwrap();
w.commit().expect("the commit itself succeeded");
drop(w);
assert!(store.compaction_due(), "the compaction is left due");
assert_eq!(store.len(), 4, "and the commit is visible");
drop(pins);
store.compact().unwrap();
assert!(!store.compaction_due());
assert_eq!(store.len(), 4);
}
#[test]
fn an_abort_truncates_to_its_mark_and_keeps_earlier_ops() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
let mut w = store.writer().unwrap();
w.upsert_node(&doc("https://x.dev/kept", 1)).unwrap();
drop(w);
let r: Result<()> = store.transact(|tx| {
tx.upsert_node(&doc("https://x.dev/aborted", 2))?;
tx.remove(UrlKey::of("https://x.dev/kept"))?;
Err(Error::format("abort"))
});
assert!(r.is_err());
store.writer().unwrap().commit().unwrap();
assert_eq!(urls(&store), ["https://x.dev/kept"]);
}
#[test]
fn a_transaction_over_the_record_cap_is_refused_before_writing() {
let dir = tempfile::tempdir().unwrap();
let store = Store::create(dir.path(), Config::default()).unwrap();
let before = wal_len(dir.path());
let big = "v".repeat(wal::MAX_ATTR_LEN);
let mut w = store.writer().unwrap();
w.upsert_node(&doc("https://x.dev/a", 1)).unwrap();
let mut k = 0u64;
while (k as usize) * wal::MAX_ATTR_LEN <= wal::MAX_RECORD {
w.set_attrs(UrlKey(k), &[(0, &big)]).unwrap();
k += 1;
}
let first = w.commit().unwrap_err();
assert!(matches!(first, Error::Format { .. }), "{first}");
assert_eq!(wal_len(dir.path()), before, "nothing was written");
let again = w.commit().unwrap_err();
assert_eq!(again.to_string(), first.to_string(), "the same ops are still staged");
w.discard();
w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
w.commit().unwrap();
drop(w);
assert_eq!(urls(&store), ["https://x.dev/b"]);
}
}