1use crate::compact;
54use crate::error::{Error, Result};
55use crate::format::{base, wal};
56use crate::mmap::MappedFile;
57use crate::overlay::Arena;
58use crate::snapshot::Snapshot;
59use crate::ttl::TtlConfig;
60use crate::writer::{Txn, WriteState, Writer};
61use std::fs;
62use std::path::{Path, PathBuf};
63use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
64use std::sync::{Mutex, MutexGuard, RwLock};
65use std::time::{Duration, Instant};
66
67const GEN_SLOTS: usize = 4;
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum Durability {
74 Always,
76 Batch(u32),
78 Never,
80}
81
82#[derive(Clone, Copy, Debug)]
84pub struct Config {
85 pub durability: Durability,
87 pub compact_after_ops: usize,
89 pub compact_after_wal_bytes: u64,
91 pub ttl: TtlConfig,
93 pub lock_wait: Duration,
96}
97
98impl Default for Config {
99 fn default() -> Self {
100 Self {
101 durability: Durability::Always,
102 compact_after_ops: 4096,
103 compact_after_wal_bytes: 4 << 20,
104 ttl: TtlConfig::default(),
105 lock_wait: Duration::from_secs(10),
106 }
107 }
108}
109
110#[derive(Debug)]
112pub(crate) struct GenInner {
113 map: MappedFile,
114 pub(crate) dir: base::BaseDir,
115 pub(crate) arena: Arena,
116}
117
118impl GenInner {
119 pub(crate) fn bytes(&self) -> &[u8] {
120 self.map.bytes()
121 }
122
123 pub(crate) fn from_parts(map: MappedFile, dir: base::BaseDir) -> Self {
124 Self { map, dir, arena: Arena::new() }
125 }
126}
127
128#[derive(Debug)]
129pub(crate) struct GenSlot {
130 pub(crate) seq: AtomicU64,
132 pub(crate) inner: RwLock<Option<GenInner>>,
133}
134
135#[derive(Debug)]
137pub struct Store {
138 pub(crate) dir: PathBuf,
139 pub(crate) cfg: Config,
140 read_only: bool,
141 pub(crate) current: AtomicUsize,
142 pub(crate) gens: [GenSlot; GEN_SLOTS],
143 pub(crate) write: Mutex<WriteState>,
147 pub(crate) compact_inline: AtomicBool,
149 pub(crate) due: AtomicBool,
151}
152
153#[derive(Clone, Copy, Debug, Default)]
155pub(crate) struct Seen {
156 pub(crate) stamp: Option<Stamp>,
159 pub(crate) wal_pos: u64,
161 pub(crate) log_current: bool,
164 pub(crate) base_gen: u64,
167}
168
169impl Seen {
170 pub(crate) fn appended(&mut self, n: u64) {
172 self.wal_pos += n;
173 if let Some(w) = self.stamp.as_mut().and_then(|s| s.wal.as_mut()) {
174 w.1 += n;
175 }
176 }
177}
178
179#[derive(Debug)]
183pub struct CommitLock {
184 _held: Held,
185 dir: PathBuf,
186}
187
188#[derive(Debug)]
190pub(crate) struct Held(pub(crate) fs::File);
191
192fn open_lock_file(dir: &Path) -> Result<fs::File> {
196 let path = lock_path(dir);
197 match fs::symlink_metadata(&path) {
198 Ok(md) if !md.file_type().is_file() => {
199 return Err(Error::corrupt(format!("{LOCK_FILE} is not a regular file")));
200 }
201 _ => {}
202 }
203 Ok(fs::OpenOptions::new().create(true).truncate(false).write(true).open(path)?)
204}
205
206fn lock_within(file: &fs::File, wait: Duration) -> Result<()> {
209 let mut start = None;
210 let mut nap = Duration::from_millis(1);
211 loop {
212 #[cfg(test)]
213 probe::bump(&probe::LOCKS, 1);
214 match file.try_lock() {
215 Ok(()) => return Ok(()),
216 Err(fs::TryLockError::WouldBlock) => {}
217 Err(fs::TryLockError::Error(e)) => return Err(e.into()),
218 }
219 let left = wait.saturating_sub(start.get_or_insert_with(Instant::now).elapsed());
220 if left.is_zero() {
221 return Err(Error::Locked);
222 }
223 std::thread::sleep(nap.min(left));
224 nap = (nap * 2).min(Duration::from_millis(50));
225 }
226}
227
228fn lock_dir(dir: &Path, wait: Duration) -> Result<Held> {
230 let file = open_lock_file(dir)?;
231 lock_within(&file, wait)?;
232 Ok(Held(file))
233}
234
235#[cfg(test)]
238pub(crate) mod probe {
239 use std::cell::Cell;
240 use std::thread::LocalKey;
241 thread_local! {
242 pub(crate) static LOCKS: Cell<u64> = const { Cell::new(0) };
244 pub(crate) static FSYNCS: Cell<u64> = const { Cell::new(0) };
246 pub(crate) static TAIL_BYTES: Cell<u64> = const { Cell::new(0) };
248 }
249 pub(crate) fn bump(c: &'static LocalKey<Cell<u64>>, n: u64) {
250 c.with(|x| x.set(x.get() + n));
251 }
252 pub(crate) fn take(c: &'static LocalKey<Cell<u64>>) -> u64 {
253 c.with(|x| x.replace(0))
254 }
255}
256
257const RACE_RETRIES: usize = 3;
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub(crate) struct Stamp {
269 base: FileId,
270 wal: Option<(Ident, u64)>,
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275struct FileId {
276 len: u64,
277 modified: Option<std::time::SystemTime>,
278 ident: Ident,
279}
280
281#[derive(Clone, Copy, Debug, PartialEq, Eq)]
283struct Ident {
284 #[cfg(unix)]
285 ino: (u64, u64),
286 #[cfg(not(unix))]
287 created: Option<std::time::SystemTime>,
288}
289
290impl Ident {
291 fn of(md: &fs::Metadata) -> Self {
292 #[cfg(unix)]
293 use std::os::unix::fs::MetadataExt as _;
294 Self {
295 #[cfg(unix)]
296 ino: (md.dev(), md.ino()),
297 #[cfg(not(unix))]
298 created: md.created().ok(),
299 }
300 }
301}
302
303impl FileId {
304 fn of(md: &fs::Metadata) -> Self {
305 Self { len: md.len(), modified: md.modified().ok(), ident: Ident::of(md) }
306 }
307}
308
309impl Stamp {
310 pub(crate) fn read(dir: &Path) -> Option<Self> {
313 let base = FileId::of(&fs::metadata(base_path(dir)).ok()?);
314 let wal = match fs::metadata(wal_path(dir)) {
315 Ok(md) => Some((Ident::of(&md), md.len())),
316 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
317 Err(_) => return None,
318 };
319 Some(Self { base, wal })
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325enum Caught {
326 Nothing,
328 Tail,
330 Full,
332}
333
334pub const BASE_FILE: &str = "graph.base";
336pub const WAL_FILE: &str = "graph.wal";
338pub const LOCK_FILE: &str = "graph.lock";
340
341pub(crate) fn base_path(dir: &Path) -> PathBuf {
343 dir.join(BASE_FILE)
344}
345pub(crate) fn wal_path(dir: &Path) -> PathBuf {
346 dir.join(WAL_FILE)
347}
348fn lock_path(dir: &Path) -> PathBuf {
349 dir.join(LOCK_FILE)
350}
351
352use crate::fsio::now_ms;
354
355pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
356 crate::fsio::write_atomic(path, &[bytes]).map_err(Into::into)
357}
358
359fn lay_down(dir: &Path) -> Result<()> {
364 if !wal_path(dir).exists() {
365 atomic_write(&wal_path(dir), &wal::encode_header(1, now_ms()))?;
366 }
367 atomic_write(&base_path(dir), &compact::render_empty_base(1))
368}
369
370impl Store {
371 pub fn create(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
377 let dir = dir.as_ref();
378 fs::create_dir_all(dir)?;
379 {
380 let _held = lock_dir(dir, cfg.lock_wait)?;
381 if base_path(dir).exists() {
382 return Err(Error::format("store already exists; use open"));
383 }
384 lay_down(dir)?;
385 }
386 Self::open_with(dir, cfg, false)
387 }
388
389 pub fn open_or_create(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
395 let dir = dir.as_ref();
396 if !base_path(dir).exists() {
397 fs::create_dir_all(dir)?;
398 let _held = lock_dir(dir, cfg.lock_wait)?;
399 if !base_path(dir).exists() {
400 lay_down(dir)?;
401 }
402 }
403 Self::open_with(dir, cfg, false)
404 }
405
406 pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
411 Self::open_with(dir.as_ref(), Config::default(), false)
412 }
413
414 pub fn open_cfg(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
416 Self::open_with(dir.as_ref(), cfg, false)
417 }
418
419 pub fn open_read_only(dir: impl AsRef<Path>) -> Result<Self> {
423 Self::open_with(dir.as_ref(), Config::default(), true)
424 }
425
426 fn open_with(dir: &Path, cfg: Config, read_only: bool) -> Result<Self> {
427 let stamp = Stamp::read(dir);
428 let built = match load_generation(dir, false) {
429 Err(Error::Busy { .. }) if !read_only => match lock_now(dir)? {
433 Some(_held) => load_generation(dir, true)?,
434 None => return Err(Error::busy("compaction in progress; retry")),
435 },
436 other => other?,
437 };
438 let repair = !read_only && built.log != LogState::Current { torn: false };
439 let store = Self::assemble(dir.to_path_buf(), cfg, read_only, stamp, built);
440 if repair {
441 store.repair_now()?;
442 }
443 Ok(store)
444 }
445
446 fn repair_now(&self) -> Result<()> {
450 let Some(held) = lock_now(&self.dir)? else { return Ok(()) };
451 let mut state = self.write_state()?;
452 let r = self.sync(&mut state, true).map(|_| ());
453 drop(held);
454 r
455 }
456
457 pub fn open_image(base: bytes::Bytes, wal: &[u8]) -> Result<Self> {
464 let built = build_generation(MappedFile::from_bytes(base), wal)?;
465 if built.log == LogState::Newer {
466 return Err(newer_image());
467 }
468 Ok(Self::assemble(PathBuf::new(), Config::default(), true, None, built))
469 }
470
471 pub fn reload_image(&self, base: Option<bytes::Bytes>, wal: &[u8]) -> Result<bool> {
476 if !self.read_only {
477 return Ok(false);
478 }
479 let _one_at_a_time = self.write.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
480 match base {
481 Some(b) => {
482 self.install_or_apply(MappedFile::from_bytes(b), wal)?.ok_or_else(newer_image)
483 }
484 None => match self.apply_wal_bytes(wal)? {
485 Some(changed) => Ok(changed),
486 None => {
487 Err(Error::format("log image belongs to another generation; pass its base"))
488 }
489 },
490 }
491 }
492
493 #[must_use]
495 pub fn is_image(&self) -> bool {
496 self.dir.as_os_str().is_empty()
497 }
498
499 fn assemble(
500 dir: PathBuf,
501 cfg: Config,
502 read_only: bool,
503 stamp: Option<Stamp>,
504 built: Built,
505 ) -> Self {
506 let generation = built.inner.dir.header.generation;
507 let seen = Seen {
508 stamp,
509 wal_pos: built.good_len as u64,
510 log_current: matches!(built.log, LogState::Current { .. }),
511 base_gen: generation,
512 };
513 let store = Self {
514 dir,
515 cfg,
516 read_only,
517 current: AtomicUsize::new(0),
518 gens: std::array::from_fn(|_| GenSlot {
519 seq: AtomicU64::new(0),
520 inner: RwLock::new(None),
521 }),
522 write: Mutex::new(WriteState::new(
523 built.next_seq,
524 built.replayed,
525 built.good_len as u64,
526 generation,
527 seen,
528 )),
529 compact_inline: AtomicBool::new(true),
530 due: AtomicBool::new(false),
531 };
532 *store.gens[0].inner.write().expect("fresh lock") = Some(built.inner);
533 store.gens[0].seq.store(generation, Ordering::Release);
534 store
535 }
536
537 #[must_use]
540 pub fn len(&self) -> usize {
541 self.snapshot().len()
542 }
543
544 #[must_use]
546 pub fn is_empty(&self) -> bool {
547 self.len() == 0
548 }
549
550 #[must_use]
552 pub fn dir(&self) -> &Path {
553 &self.dir
554 }
555
556 #[must_use]
559 pub fn generation(&self) -> u64 {
560 self.gens[self.current.load(Ordering::Acquire)].seq.load(Ordering::Acquire)
561 }
562
563 pub fn set_compact_inline(&self, inline: bool) {
569 self.compact_inline.store(inline, Ordering::Relaxed);
570 }
571
572 #[must_use]
575 pub fn compaction_due(&self) -> bool {
576 self.due.load(Ordering::Acquire)
577 }
578
579 #[must_use]
582 pub fn snapshot(&self) -> Snapshot<'_> {
583 loop {
584 let cur = self.current.load(Ordering::Acquire);
585 if let Ok(guard) = self.gens[cur].inner.try_read() {
586 if guard.is_some() && self.current.load(Ordering::Acquire) == cur {
587 return Snapshot::new(self, guard);
588 }
589 }
590 std::hint::spin_loop();
591 }
592 }
593
594 fn write_state(&self) -> Result<MutexGuard<'_, WriteState>> {
595 self.write.lock().map_err(|_| Error::corrupt("writer mutex poisoned"))
596 }
597
598 pub fn writer(&self) -> Result<Writer<'_>> {
604 if self.read_only {
605 return Err(Error::ReadOnly);
606 }
607 Ok(Writer::new(self, self.write_state()?))
608 }
609
610 pub fn transact<F, R>(&self, f: F) -> Result<R>
620 where
621 F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
622 {
623 crate::writer::transact(self, None, self.cfg.lock_wait, f)
624 }
625
626 pub fn try_transact<F, R>(&self, f: F) -> Result<R>
630 where
631 F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
632 {
633 crate::writer::transact(self, None, Duration::ZERO, f)
634 }
635
636 pub fn commit_lock(&self) -> Result<CommitLock> {
642 self.commit_lock_within(self.cfg.lock_wait)
643 }
644
645 pub fn try_commit_lock(&self) -> Result<CommitLock> {
648 self.commit_lock_within(Duration::ZERO)
649 }
650
651 fn commit_lock_within(&self, wait: Duration) -> Result<CommitLock> {
652 if self.read_only {
653 return Err(Error::ReadOnly);
654 }
655 let mut state = self.write_state()?;
656 let held = lock_dir(&self.dir, wait)?;
657 self.sync(&mut state, true)?;
658 Ok(CommitLock { _held: held, dir: self.dir.clone() })
659 }
660
661 pub fn transact_with<F, R>(&self, lock: &CommitLock, f: F) -> Result<R>
664 where
665 F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
666 {
667 if lock.dir != self.dir {
668 return Err(Error::format("the commit lock belongs to another store"));
669 }
670 crate::writer::transact(self, Some(lock), Duration::ZERO, f)
671 }
672
673 pub fn compact(&self) -> Result<compact::CompactStats> {
677 if self.read_only {
678 return Err(Error::ReadOnly);
679 }
680 let mut state = self.write_state()?;
681 let held = self.acquire(&mut state, self.cfg.lock_wait)?;
682 let r = self.sync(&mut state, true).and_then(|_| compact::compact_locked(self, &mut state));
683 state.release(held);
684 r
685 }
686
687 pub fn reload_if_stale(&self) -> Result<bool> {
697 if self.is_image() {
698 return Ok(false); }
700 let mut state = if self.read_only {
701 self.write.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
702 } else {
703 match self.write.try_lock() {
704 Ok(g) => g,
705 Err(std::sync::TryLockError::WouldBlock) => return Ok(false),
706 Err(std::sync::TryLockError::Poisoned(p)) => p.into_inner(),
707 }
708 };
709 self.sync(&mut state, false)
710 }
711
712 pub(crate) fn acquire(&self, state: &mut WriteState, wait: Duration) -> Result<Held> {
715 let file = match state.lock_file.take() {
716 Some(f) => f,
717 None => open_lock_file(&self.dir)?,
718 };
719 match lock_within(&file, wait) {
720 Ok(()) => Ok(Held(file)),
721 Err(e) => {
722 state.lock_file = Some(file);
723 Err(e)
724 }
725 }
726 }
727
728 pub(crate) fn sync(&self, state: &mut WriteState, repair: bool) -> Result<bool> {
734 let (changed, caught) = self.catch_up(&mut state.seen, repair)?;
735 if caught == Caught::Nothing && !repair {
736 return Ok(false);
737 }
738 if caught == Caught::Full {
739 state.drop_wal_handle(); }
741 if repair {
742 self.repair(state)?;
743 }
744 let cur = self.current.load(Ordering::Acquire);
745 let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
746 let g = guard.as_ref().ok_or_else(|| Error::corrupt("current slot empty"))?;
747 let next_seq = g.dir.header.wal_applied_seq.max(g.arena.last_seq()) + 1;
748 state.adopt(next_seq, g.arena.len(), state.seen.base_gen);
749 if caught == Caught::Full {
750 let over = state.ops_since_compact >= self.cfg.compact_after_ops
751 || state.wal_bytes >= self.cfg.compact_after_wal_bytes;
752 self.due.store(over && !self.compact_inline.load(Ordering::Relaxed), Ordering::Release);
753 }
754 Ok(changed)
755 }
756
757 fn repair(&self, state: &mut WriteState) -> Result<()> {
761 let seen = &mut state.seen;
762 let file = wal_path(&self.dir);
763 if !seen.log_current {
764 atomic_write(&file, &wal::encode_header(seen.base_gen, now_ms()))?;
765 *seen = Seen {
766 stamp: Stamp::read(&self.dir),
767 wal_pos: wal::HEADER_LEN as u64,
768 log_current: true,
769 base_gen: seen.base_gen,
770 };
771 state.drop_wal_handle();
772 return Ok(());
773 }
774 if let Some((ident, len)) = seen.stamp.and_then(|s| s.wal) {
775 if len > seen.wal_pos {
776 let f = fs::OpenOptions::new().write(true).open(&file)?;
777 f.set_len(seen.wal_pos)?;
778 f.sync_all()?;
779 if let Some(s) = seen.stamp.as_mut() {
780 s.wal = Some((ident, seen.wal_pos));
781 }
782 }
783 }
784 Ok(())
785 }
786
787 fn catch_up(&self, seen: &mut Seen, locked: bool) -> Result<(bool, Caught)> {
792 let now = Stamp::read(&self.dir);
793 if let (Some(n), Some(l)) = (now, seen.stamp) {
794 if n == l {
795 return Ok((false, Caught::Nothing));
796 }
797 if let (Some((ni, nlen)), Some((li, _))) = (n.wal, l.wal) {
800 if n.base == l.base
801 && ni == li
802 && nlen >= seen.wal_pos
803 && seen.wal_pos >= wal::HEADER_LEN as u64
804 {
805 let (changed, pos) = self.reload_wal_tail(seen.wal_pos)?;
806 *seen = Seen { stamp: now, wal_pos: pos, ..*seen };
807 return Ok((changed, Caught::Tail));
808 }
809 }
810 }
811 let (changed, pos, log_current, base_gen) = self.reload_full(locked)?;
812 *seen = Seen { stamp: now, wal_pos: pos, log_current, base_gen };
813 Ok((changed, Caught::Full))
814 }
815
816 fn reload_wal_tail(&self, pos: u64) -> Result<(bool, u64)> {
820 use std::io::{Read as _, Seek as _, SeekFrom};
821 let mut tail = Vec::new();
822 match fs::File::open(wal_path(&self.dir)) {
823 Ok(mut f) => {
824 f.seek(SeekFrom::Start(pos))?;
825 f.read_to_end(&mut tail)?;
826 }
827 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok((false, pos)),
828 Err(e) => return Err(e.into()),
829 }
830 #[cfg(test)]
831 probe::bump(&probe::TAIL_BYTES, tail.len() as u64);
832 let (ops, used) = wal::replay_frames(&tail)?;
833 Ok((self.publish_new(ops)?, pos + used as u64))
834 }
835
836 fn publish_new(&self, ops: Vec<(u64, wal::Op)>) -> Result<bool> {
839 let cur = self.current.load(Ordering::Acquire);
840 let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
841 let Some(g) = guard.as_ref() else { return Ok(false) };
842 let seen = g.dir.header.wal_applied_seq.max(g.arena.last_seq());
843 let mut changed = false;
844 for (seq, op) in ops.into_iter().filter(|&(s, _)| s > seen) {
845 if !g.arena.publish(seq, op) {
846 return Err(Error::busy("overlay arena full"));
847 }
848 changed = true;
849 }
850 Ok(changed)
851 }
852
853 fn apply_wal_bytes(&self, raw: &[u8]) -> Result<Option<bool>> {
856 if raw.is_empty() {
857 return Ok(Some(false));
858 }
859 let replayed = wal::replay(raw)?;
860 if replayed.base_generation != self.generation() {
861 return Ok(None);
862 }
863 self.publish_new(replayed.ops).map(Some)
864 }
865
866 fn reload_full(&self, locked: bool) -> Result<(bool, u64, bool, u64)> {
871 for _ in 0..RACE_RETRIES {
872 let map = MappedFile::open(&base_path(&self.dir))?;
873 let base_gen = base::Header::parse(map.bytes())?.generation;
874 let raw = match fs::read(wal_path(&self.dir)) {
875 Ok(raw) => raw,
876 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
877 Err(e) => return Err(e.into()),
878 };
879 if let Some(changed) = self.install_or_apply(map, &raw)? {
880 let current = wal::parse_header(&raw).ok() == Some(base_gen);
881 let pos = if current { wal::committed_len(&raw) } else { wal::HEADER_LEN };
882 return Ok((changed, pos as u64, current, base_gen));
883 }
884 if locked {
885 return Err(Error::corrupt("wal is newer than base"));
886 }
887 }
888 Err(Error::busy("compaction in progress; retry"))
889 }
890
891 fn install_or_apply(&self, map: MappedFile, raw: &[u8]) -> Result<Option<bool>> {
896 let cur_gen = self.generation();
897 let new_gen = base::Header::parse(map.bytes())?.generation;
898 if new_gen == cur_gen {
899 return match self.apply_wal_bytes(raw)? {
902 Some(changed) => Ok(Some(changed)),
903 None if wal::replay(raw)?.base_generation > new_gen => Ok(None),
906 None => Ok(Some(false)),
907 };
908 }
909 let built = build_generation(map, raw)?;
910 if built.log == LogState::Newer {
911 return Ok(None);
912 }
913 let folded = built.inner.dir.header.wal_applied_seq;
914 match self.install(built.inner, new_gen) {
915 Ok(()) => Ok(Some(true)),
916 Err(busy) => {
923 if folded > self.served_seq()? {
924 return Err(busy);
925 }
926 match built.log {
927 LogState::Current { .. } => self.publish_new(wal::replay(raw)?.ops).map(Some),
928 _ => Ok(Some(false)), }
930 }
931 }
932 }
933
934 fn served_seq(&self) -> Result<u64> {
936 let cur = self.current.load(Ordering::Acquire);
937 let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
938 Ok(guard.as_ref().map_or(0, |g| g.dir.header.wal_applied_seq.max(g.arena.last_seq())))
939 }
940
941 pub(crate) fn install(&self, inner: GenInner, generation: u64) -> Result<()> {
944 let cur = self.current.load(Ordering::Acquire);
945 for (i, slot) in self.gens.iter().enumerate() {
946 if i == cur {
947 continue;
948 }
949 if let Ok(mut w) = slot.inner.try_write() {
950 *w = Some(inner);
951 drop(w);
952 slot.seq.store(generation, Ordering::Release);
953 self.current.store(i, Ordering::Release);
954 return Ok(());
955 }
956 }
957 Err(Error::busy("all generation slots pinned by snapshots"))
958 }
959}
960
961fn lock_now(dir: &Path) -> Result<Option<Held>> {
963 match lock_dir(dir, Duration::ZERO) {
964 Ok(h) => Ok(Some(h)),
965 Err(Error::Locked) => Ok(None),
966 Err(e) => Err(e),
967 }
968}
969
970fn newer_image() -> Error {
971 Error::busy("log image is newer than its base; fetch the base again")
972}
973
974struct Built {
977 inner: GenInner,
978 next_seq: u64,
979 replayed: usize,
980 good_len: usize,
981 log: LogState,
982}
983
984#[derive(Clone, Copy, PartialEq, Eq)]
986enum LogState {
987 Empty,
989 Stale,
991 Current { torn: bool },
993 Newer,
996}
997
998fn build_generation(map: MappedFile, raw: &[u8]) -> Result<Built> {
1001 let bdir = base::BaseDir::parse(map.bytes())?;
1002 let header = bdir.header;
1003 let inner = GenInner { map, dir: bdir, arena: Arena::new() };
1004 if raw.is_empty() {
1005 return Ok(Built {
1006 inner,
1007 next_seq: header.wal_applied_seq + 1,
1008 replayed: 0,
1009 good_len: 0,
1010 log: LogState::Empty,
1011 });
1012 }
1013 let replayed = wal::replay(raw)?;
1014 if replayed.base_generation > header.generation {
1015 return Ok(Built {
1016 inner,
1017 next_seq: header.wal_applied_seq + 1,
1018 replayed: 0,
1019 good_len: 0,
1020 log: LogState::Newer,
1021 });
1022 }
1023 if replayed.base_generation < header.generation {
1024 return Ok(Built {
1025 inner,
1026 next_seq: header.wal_applied_seq + 1,
1027 replayed: 0,
1028 good_len: wal::HEADER_LEN,
1029 log: LogState::Stale,
1030 });
1031 }
1032 let torn = replayed.good_len < raw.len();
1033 let next_seq = replayed
1034 .ops
1035 .last()
1036 .map_or(header.wal_applied_seq + 1, |&(s, _)| s.max(header.wal_applied_seq) + 1);
1037 let mut count = 0;
1038 for (seq, op) in replayed.ops.into_iter().filter(|&(s, _)| s > header.wal_applied_seq) {
1039 if !inner.arena.publish(seq, op) {
1040 return Err(Error::busy("overlay arena full during replay"));
1041 }
1042 count += 1;
1043 }
1044 Ok(Built {
1045 inner,
1046 next_seq,
1047 replayed: count,
1048 good_len: replayed.good_len,
1049 log: LogState::Current { torn },
1050 })
1051}
1052
1053fn load_generation(dir: &Path, locked: bool) -> Result<Built> {
1058 let wal_file = wal_path(dir);
1059 for _ in 0..RACE_RETRIES {
1060 let map = MappedFile::open(&base_path(dir))?;
1061 let raw = match fs::read(&wal_file) {
1062 Ok(raw) => raw,
1063 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
1064 Err(e) => return Err(e.into()),
1065 };
1066 let built = build_generation(map, &raw)?;
1067 if built.log != LogState::Newer {
1068 return Ok(built);
1069 }
1070 if locked {
1071 return Err(Error::corrupt("wal is newer than base"));
1072 }
1073 }
1074 Err(Error::busy("compaction in progress; retry"))
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080 use crate::writer::DocRecord;
1081 use crate::UrlKey;
1082 use std::time::{Duration, Instant};
1083
1084 fn doc(url: &str, hash: u64) -> DocRecord<'_> {
1085 DocRecord {
1086 url,
1087 url_key: UrlKey::of(url),
1088 content_hash: hash,
1089 fetched_at_ms: 1,
1090 title: None,
1091 snippet: None,
1092 etag: None,
1093 pinned: false,
1094 }
1095 }
1096
1097 fn write(store: &Store, url: &str, hash: u64) {
1098 let mut w = store.writer().unwrap();
1099 w.upsert_node(&doc(url, hash)).unwrap();
1100 w.commit().unwrap();
1101 }
1102
1103 #[test]
1106 fn a_snapshot_does_not_observe_later_commits() {
1107 let dir = tempfile::tempdir().unwrap();
1108 let store = Store::create(dir.path(), Config::default()).unwrap();
1109 write(&store, "https://x.dev/a", 1);
1110
1111 let before = store.snapshot();
1112 assert_eq!(before.len(), 1);
1113
1114 write(&store, "https://x.dev/b", 2);
1115 assert_eq!(before.len(), 1, "an existing snapshot must not grow");
1116 assert_eq!(store.snapshot().len(), 2, "a fresh snapshot sees the commit");
1117 }
1118
1119 #[test]
1123 fn compaction_leaves_an_existing_snapshot_on_its_own_generation() {
1124 let dir = tempfile::tempdir().unwrap();
1125 let store = Store::create(dir.path(), Config::default()).unwrap();
1126 write(&store, "https://x.dev/a", 1);
1127 store.compact().unwrap();
1128
1129 let pinned = store.snapshot();
1130 let pinned_gen = pinned.generation();
1131
1132 write(&store, "https://x.dev/b", 2);
1133 store.compact().unwrap();
1134
1135 assert_eq!(pinned.generation(), pinned_gen, "a pinned generation must not move");
1136 assert_eq!(pinned.len(), 1, "the old generation still reads its own state");
1137 let fresh = store.snapshot();
1138 assert!(fresh.generation() > pinned_gen, "the store advanced");
1139 assert_eq!(fresh.len(), 2);
1140 }
1141
1142 #[test]
1145 fn each_compaction_advances_the_generation_exactly_once() {
1146 let dir = tempfile::tempdir().unwrap();
1147 let store = Store::create(dir.path(), Config::default()).unwrap();
1148 write(&store, "https://x.dev/a", 1);
1149
1150 let mut prev = store.snapshot().generation();
1151 for _ in 0..6 {
1152 store.compact().unwrap();
1153 let now = store.snapshot().generation();
1154 assert_eq!(now, prev + 1, "a compaction must advance the generation by one");
1155 prev = now;
1156 }
1157 }
1158
1159 #[test]
1164 fn exhausting_the_generation_ring_is_a_retryable_refusal() {
1165 let dir = tempfile::tempdir().unwrap();
1166 let store = Store::create(dir.path(), Config::default()).unwrap();
1167 write(&store, "https://x.dev/a", 1);
1168
1169 let mut pins = Vec::new();
1173 for round in 0..GEN_SLOTS - 1 {
1174 pins.push(store.snapshot());
1175 store.compact().unwrap_or_else(|e| panic!("round {round} should still fit: {e}"));
1176 }
1177
1178 pins.push(store.snapshot());
1180 assert!(
1181 matches!(store.compact(), Err(Error::Busy { .. })),
1182 "a full ring must refuse with Busy, not stall or corrupt the swap"
1183 );
1184
1185 drop(pins);
1187 store.compact().expect("compaction must succeed once a slot frees up");
1188 assert_eq!(store.snapshot().len(), 1);
1189 }
1190
1191 #[test]
1194 fn a_read_only_handle_reloads_a_newer_generation() {
1195 let dir = tempfile::tempdir().unwrap();
1196 let store = Store::create(dir.path(), Config::default()).unwrap();
1197 write(&store, "https://x.dev/a", 1);
1198 store.compact().unwrap();
1199
1200 let reader = Store::open_read_only(dir.path()).unwrap();
1201 assert_eq!(reader.snapshot().len(), 1);
1202 assert!(!reader.reload_if_stale().unwrap(), "nothing has changed yet");
1203
1204 write(&store, "https://x.dev/b", 2);
1205 store.compact().unwrap();
1206
1207 assert!(reader.reload_if_stale().unwrap(), "a new generation is visible");
1208 assert_eq!(reader.snapshot().len(), 2);
1209 }
1210
1211 fn quick() -> Config {
1214 Config { lock_wait: Duration::from_millis(30), ..Config::default() }
1215 }
1216
1217 fn urls(store: &Store) -> Vec<String> {
1218 let mut v = Vec::new();
1219 store.snapshot().for_each_live(|n| v.push(n.url.to_owned()));
1220 v.sort();
1221 v
1222 }
1223
1224 #[test]
1228 fn a_commit_catches_up_with_a_grown_log_first() {
1229 let dir = tempfile::tempdir().unwrap();
1230 let a = Store::create(dir.path(), Config::default()).unwrap();
1231 let b = Store::open(dir.path()).unwrap();
1232 write(&a, "https://x.dev/a", 1);
1233 write(&b, "https://x.dev/b", 2);
1234 write(&a, "https://x.dev/c", 3);
1235 assert_eq!(urls(&a), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
1236 let raw = fs::read(wal_path(dir.path())).unwrap();
1237 let seqs: Vec<u64> = wal::replay(&raw).unwrap().ops.iter().map(|o| o.0).collect();
1238 assert_eq!(seqs, [1, 2, 3], "one sequence across both writers");
1239 assert!(b.reload_if_stale().unwrap(), "a writable handle reloads too");
1240 assert_eq!(urls(&b), urls(&a));
1241 }
1242
1243 #[test]
1246 fn a_commit_catches_up_after_another_handle_compacted() {
1247 let dir = tempfile::tempdir().unwrap();
1248 let a = Store::create(dir.path(), Config::default()).unwrap();
1249 let b = Store::open(dir.path()).unwrap();
1250 write(&a, "https://x.dev/a", 1); write(&b, "https://x.dev/b", 2);
1252 b.compact().unwrap();
1253 let generation = b.generation();
1254 write(&a, "https://x.dev/c", 3);
1255 assert_eq!(a.generation(), generation, "the compacted base was installed");
1256 drop((a, b));
1257 let fresh = Store::open_read_only(dir.path()).unwrap();
1258 assert_eq!(urls(&fresh), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
1259 }
1260
1261 #[test]
1263 fn a_commit_past_lock_wait_is_locked_and_keeps_its_ops() {
1264 let dir = tempfile::tempdir().unwrap();
1265 let a = Store::create(dir.path(), Config::default()).unwrap();
1266 let b = Store::open_cfg(dir.path(), quick()).unwrap();
1267 let held = a.commit_lock().unwrap();
1268 let mut w = b.writer().unwrap();
1269 w.upsert_node(&doc("https://x.dev/a", 1)).unwrap();
1270 let started = Instant::now();
1271 assert!(matches!(w.commit(), Err(Error::Locked)));
1272 assert!(started.elapsed() >= Duration::from_millis(30), "it waited lock_wait");
1273 let third = Store::open_cfg(dir.path(), quick()).unwrap();
1274 assert!(matches!(third.compact(), Err(Error::Locked)), "compaction takes the same lock");
1275 let patient = Store::open(dir.path()).unwrap(); let started = Instant::now();
1278 let ran = std::cell::Cell::new(false);
1279 let refused = patient.try_transact(|_| {
1280 ran.set(true);
1281 Ok(())
1282 });
1283 assert!(matches!(refused, Err(Error::Locked)) && !ran.get());
1284 assert!(started.elapsed() < Duration::from_secs(5), "try_transact waited");
1285 drop(held);
1286 w.commit().unwrap();
1287 drop(w);
1288 assert_eq!(b.len(), 1);
1289 }
1290
1291 #[test]
1294 fn an_aborted_transaction_writes_nothing() {
1295 let dir = tempfile::tempdir().unwrap();
1296 let store = Store::create(dir.path(), Config::default()).unwrap();
1297 let before = fs::read(wal_path(dir.path())).unwrap();
1298 let r: Result<()> = store.transact(|tx| {
1299 tx.upsert_node(&doc("https://x.dev/a", 1))?;
1300 Err(Error::format("caller changed its mind"))
1301 });
1302 assert!(r.is_err());
1303 assert_eq!(fs::read(wal_path(dir.path())).unwrap(), before, "nothing appended");
1304 assert!(store.is_empty());
1305 store
1306 .transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2)))
1307 .expect("the next transaction commits");
1308 assert_eq!(urls(&store), ["https://x.dev/b"], "the aborted op never resurfaces");
1309 }
1310
1311 #[test]
1314 fn transactions_allocate_ids_without_collisions() {
1315 let dir = tempfile::tempdir().unwrap();
1316 let a = Store::create(dir.path(), Config::default()).unwrap();
1317 let b = Store::open(dir.path()).unwrap();
1318 let next = |store: &Store| {
1319 store
1320 .transact(|tx| {
1321 let id = tx.snapshot().len();
1322 let url = format!("https://x.dev/id/{id}");
1323 tx.upsert_node(&doc(&url, id as u64))?;
1324 Ok(id)
1325 })
1326 .unwrap()
1327 };
1328 let ids: Vec<usize> = (0..10).map(|i| next(if i % 2 == 0 { &a } else { &b })).collect();
1329 assert_eq!(ids, (0..10).collect::<Vec<_>>());
1330 }
1331
1332 #[test]
1335 fn open_takes_no_lock_and_a_transaction_is_one_lock_one_fsync() {
1336 let dir = tempfile::tempdir().unwrap();
1337 drop(Store::create(dir.path(), Config::default()).unwrap());
1338 probe::take(&probe::LOCKS);
1339 let store = Store::open(dir.path()).unwrap();
1340 let other = Store::open(dir.path()).unwrap();
1341 assert_eq!(probe::take(&probe::LOCKS), 0, "open made a lock call");
1342 drop(lock_dir(dir.path(), Duration::ZERO).expect("the lock is free after open"));
1344 probe::take(&probe::LOCKS);
1345 probe::take(&probe::FSYNCS);
1346 store
1347 .transact(|tx| {
1348 for i in 0..100 {
1349 tx.upsert_node(&doc(&format!("https://x.dev/{i}"), i))?;
1350 }
1351 Ok(())
1352 })
1353 .unwrap();
1354 assert_eq!(probe::take(&probe::LOCKS), 1, "one lock per transaction");
1355 assert_eq!(probe::take(&probe::FSYNCS), 1, "one fsync per transaction");
1356 assert_eq!(other.len(), 0);
1357 assert!(other.reload_if_stale().unwrap());
1358 assert_eq!(other.len(), 100);
1359 }
1360
1361 #[test]
1364 fn a_catch_up_reads_only_the_appended_bytes() {
1365 let dir = tempfile::tempdir().unwrap();
1366 let a = Store::create(dir.path(), Config::default()).unwrap();
1367 let b = Store::open(dir.path()).unwrap();
1368 for i in 0..50 {
1369 write(&a, &format!("https://x.dev/{i}"), i);
1370 }
1371 assert!(b.reload_if_stale().unwrap());
1372 let len = || fs::metadata(wal_path(dir.path())).unwrap().len();
1373 let before = len();
1374 write(&a, "https://x.dev/new", 99);
1375 let appended = len() - before;
1376 probe::take(&probe::TAIL_BYTES);
1377 write(&b, "https://x.dev/mine", 100);
1378 assert_eq!(probe::take(&probe::TAIL_BYTES), appended, "read only a's append");
1379 assert_eq!(b.len(), 52);
1380 }
1381
1382 #[test]
1386 fn the_next_committer_repairs_a_torn_tail() {
1387 use std::io::Write as _;
1388 let dir = tempfile::tempdir().unwrap();
1389 let a = Store::create(dir.path(), Config::default()).unwrap();
1390 let b = Store::open(dir.path()).unwrap();
1391 write(&a, "https://x.dev/a", 1);
1392 let intact = fs::metadata(wal_path(dir.path())).unwrap().len();
1393 let mut f = fs::OpenOptions::new().append(true).open(wal_path(dir.path())).unwrap();
1395 f.write_all(&[0x40, 0, 0, 0, 1, 2, 3]).unwrap();
1396 drop(f);
1397 write(&b, "https://x.dev/b", 2);
1398 let raw = fs::read(wal_path(dir.path())).unwrap();
1399 assert_eq!(wal::committed_len(&raw), raw.len(), "no garbage left in the log");
1400 assert!(raw.len() as u64 > intact);
1401 drop((a, b));
1402 let fresh = Store::open_read_only(dir.path()).unwrap();
1403 assert_eq!(urls(&fresh), ["https://x.dev/a", "https://x.dev/b"]);
1404 }
1405}