1use crate::compact;
61use crate::error::{Error, Result};
62use crate::format::{base, wal};
63use crate::mmap::MappedFile;
64use crate::overlay::{publish_records, LogArena};
65use crate::snapshot::Snapshot;
66use crate::ttl::TtlConfig;
67use crate::writer::{Txn, WriteState, Writer};
68use std::fs;
69use std::path::{Path, PathBuf};
70use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
71use std::sync::{Mutex, MutexGuard, RwLock};
72use std::time::{Duration, Instant};
73
74const GEN_SLOTS: usize = 4;
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum Durability {
81 Always,
83 Batch(u32),
85 Never,
87}
88
89#[derive(Clone, Copy, Debug)]
91pub struct Config {
92 pub durability: Durability,
94 pub compact_after_ops: usize,
96 pub compact_after_wal_bytes: u64,
98 pub ttl: TtlConfig,
100 pub lock_wait: Duration,
103}
104
105impl Default for Config {
106 fn default() -> Self {
107 Self {
108 durability: Durability::Always,
109 compact_after_ops: 4096,
110 compact_after_wal_bytes: 4 << 20,
111 ttl: TtlConfig::default(),
112 lock_wait: Duration::from_secs(10),
113 }
114 }
115}
116
117#[derive(Debug)]
119pub(crate) struct GenInner {
120 map: MappedFile,
121 pub(crate) dir: base::BaseDir,
122 pub(crate) log: LogArena,
123}
124
125impl GenInner {
126 pub(crate) fn bytes(&self) -> &[u8] {
127 self.map.bytes()
128 }
129
130 pub(crate) fn from_parts(map: MappedFile, dir: base::BaseDir) -> Self {
131 Self { map, dir, log: LogArena::new() }
132 }
133}
134
135#[derive(Debug)]
136pub(crate) struct GenSlot {
137 pub(crate) seq: AtomicU64,
139 pub(crate) inner: RwLock<Option<GenInner>>,
140}
141
142#[derive(Debug)]
144pub struct Store {
145 pub(crate) dir: PathBuf,
146 pub(crate) base_file: PathBuf,
149 pub(crate) wal_file: PathBuf,
150 pub(crate) cfg: Config,
151 read_only: bool,
152 pub(crate) current: AtomicUsize,
153 pub(crate) gens: [GenSlot; GEN_SLOTS],
154 pub(crate) write: Mutex<WriteState>,
158 pub(crate) compact_inline: AtomicBool,
160 pub(crate) due: AtomicBool,
162}
163
164#[derive(Clone, Copy, Debug, Default)]
166pub(crate) struct Seen {
167 pub(crate) stamp: Option<Stamp>,
170 pub(crate) wal_pos: u64,
172 pub(crate) log_current: bool,
175 pub(crate) base_gen: u64,
178}
179
180impl Seen {
181 pub(crate) fn appended(&mut self, n: u64) {
183 self.wal_pos += n;
184 if let Some(w) = self.stamp.as_mut().and_then(|s| s.wal.as_mut()) {
185 w.1 += n;
186 }
187 }
188}
189
190#[derive(Debug)]
194pub struct CommitLock {
195 _held: Held,
196 dir: PathBuf,
197}
198
199#[derive(Debug)]
201pub(crate) struct Held(pub(crate) fs::File);
202
203fn open_guarded(dir: &Path, name: &str) -> Result<fs::File> {
208 let path = dir.join(name);
209 match fs::symlink_metadata(&path) {
210 Ok(md) if !md.file_type().is_file() => {
211 return Err(Error::corrupt(format!("{name} is not a regular file")));
212 }
213 _ => {}
214 }
215 Ok(fs::OpenOptions::new().create(true).truncate(false).write(true).open(path)?)
216}
217
218const NAP_MIN: Duration = Duration::from_micros(250);
222const NAP_MAX: Duration = Duration::from_millis(2);
223
224fn try_commit_lock(lock: &fs::File) -> Result<bool> {
226 #[cfg(test)]
227 probe::bump(&probe::LOCKS, 1);
228 match lock.try_lock() {
229 Ok(()) => Ok(true),
230 Err(fs::TryLockError::WouldBlock) => Ok(false),
231 Err(fs::TryLockError::Error(e)) => Err(e.into()),
232 }
233}
234
235fn lock_within(lock: &fs::File, turnstile: &fs::File, wait: Duration) -> Result<()> {
249 #[cfg(test)]
250 probe::bump(&probe::TURNSTILE, 1);
251 let queued = match turnstile.try_lock() {
252 Ok(()) => {
253 turnstile.unlock()?;
254 if try_commit_lock(lock)? {
255 return Ok(());
256 }
257 false
258 }
259 Err(fs::TryLockError::WouldBlock) => true,
260 Err(fs::TryLockError::Error(e)) => return Err(e.into()),
261 };
262 if wait.is_zero() {
263 return Err(Error::Locked);
264 }
265 let start = Instant::now();
266 let left = || wait.saturating_sub(start.elapsed());
267 loop {
270 match turnstile.try_lock_shared() {
271 Ok(()) => break,
272 Err(fs::TryLockError::WouldBlock) => {}
273 Err(fs::TryLockError::Error(e)) => return Err(e.into()),
274 }
275 if left().is_zero() {
276 return Err(Error::Locked);
277 }
278 std::thread::sleep(NAP_MIN.min(left()));
279 }
280 let mut nap = if queued { NAP_MAX } else { NAP_MIN };
281 let won = loop {
282 let l = left();
283 if l.is_zero() {
284 break Err(Error::Locked);
285 }
286 std::thread::sleep(nap.min(l));
287 nap = (nap * 2).min(NAP_MAX);
288 match try_commit_lock(lock) {
289 Ok(false) => {}
290 done => break done.map(|_| ()),
291 }
292 };
293 let _ = turnstile.unlock();
294 won
295}
296
297fn lock_dir(dir: &Path, wait: Duration) -> Result<Held> {
299 let file = open_guarded(dir, LOCK_FILE)?;
300 lock_within(&file, &open_guarded(dir, WAIT_FILE)?, wait)?;
301 Ok(Held(file))
302}
303
304#[cfg(test)]
307pub(crate) mod probe {
308 use std::cell::Cell;
309 use std::thread::LocalKey;
310 thread_local! {
311 pub(crate) static LOCKS: Cell<u64> = const { Cell::new(0) };
313 pub(crate) static TURNSTILE: Cell<u64> = const { Cell::new(0) };
315 pub(crate) static FSYNCS: Cell<u64> = const { Cell::new(0) };
317 pub(crate) static WRITES: Cell<u64> = const { Cell::new(0) };
319 pub(crate) static TAIL_BYTES: Cell<u64> = const { Cell::new(0) };
321 pub(crate) static FULL: Cell<u64> = const { Cell::new(0) };
323 pub(crate) static FAIL_APPEND: Cell<u64> = const { Cell::new(0) };
325 pub(crate) static STUCK: Cell<u64> = const { Cell::new(0) };
328 }
329 pub(crate) fn full() -> bool {
330 FULL.with(Cell::get) != 0
331 }
332 pub(crate) fn stuck() -> bool {
333 STUCK.with(Cell::get) != 0
334 }
335 pub(crate) fn fail_append() -> bool {
337 FAIL_APPEND.with(|x| x.get().checked_sub(1).map(|n| x.set(n)).is_some())
338 }
339 pub(crate) fn bump(c: &'static LocalKey<Cell<u64>>, n: u64) {
340 c.with(|x| x.set(x.get() + n));
341 }
342 pub(crate) fn take(c: &'static LocalKey<Cell<u64>>) -> u64 {
343 c.with(|x| x.replace(0))
344 }
345}
346
347const RACE_RETRIES: usize = 3;
350
351#[derive(Clone, Copy, Debug, PartialEq, Eq)]
358pub(crate) struct Stamp {
359 base: FileId,
360 wal: Option<(Ident, u64)>,
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365struct FileId {
366 len: u64,
367 modified: Option<std::time::SystemTime>,
368 ident: Ident,
369}
370
371#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373struct Ident {
374 #[cfg(unix)]
375 ino: (u64, u64),
376 #[cfg(not(unix))]
377 created: Option<std::time::SystemTime>,
378}
379
380impl Ident {
381 fn of(md: &fs::Metadata) -> Self {
382 #[cfg(unix)]
383 use std::os::unix::fs::MetadataExt as _;
384 Self {
385 #[cfg(unix)]
386 ino: (md.dev(), md.ino()),
387 #[cfg(not(unix))]
388 created: md.created().ok(),
389 }
390 }
391}
392
393impl FileId {
394 fn of(md: &fs::Metadata) -> Self {
395 Self { len: md.len(), modified: md.modified().ok(), ident: Ident::of(md) }
396 }
397}
398
399impl Stamp {
400 pub(crate) fn read(dir: &Path) -> Option<Self> {
403 Self::of(&base_path(dir), &wal_path(dir))
404 }
405
406 fn of(base: &Path, wal: &Path) -> Option<Self> {
408 let base = FileId::of(&fs::metadata(base).ok()?);
409 let wal = match fs::metadata(wal) {
410 Ok(md) => Some((Ident::of(&md), md.len())),
411 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
412 Err(_) => return None,
413 };
414 Some(Self { base, wal })
415 }
416}
417
418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420enum Caught {
421 Nothing,
423 Tail,
425 Full,
427}
428
429pub const BASE_FILE: &str = "graph.base";
431pub const WAL_FILE: &str = "graph.wal";
433pub const LOCK_FILE: &str = "graph.lock";
435pub const WAIT_FILE: &str = "graph.wait";
438
439pub(crate) fn base_path(dir: &Path) -> PathBuf {
441 dir.join(BASE_FILE)
442}
443pub(crate) fn wal_path(dir: &Path) -> PathBuf {
444 dir.join(WAL_FILE)
445}
446
447use crate::fsio::now_ms;
449
450pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
451 crate::fsio::write_atomic(path, &[bytes]).map_err(Into::into)
452}
453
454fn lay_down(dir: &Path) -> Result<()> {
459 if !wal_path(dir).exists() {
460 atomic_write(&wal_path(dir), &wal::encode_header(1, now_ms()))?;
461 }
462 atomic_write(&base_path(dir), &compact::render_empty_base(1))
463}
464
465impl Store {
466 pub fn create(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
472 let dir = dir.as_ref();
473 fs::create_dir_all(dir)?;
474 {
475 let _held = lock_dir(dir, cfg.lock_wait)?;
476 if base_path(dir).exists() {
477 return Err(Error::format("store already exists; use open"));
478 }
479 lay_down(dir)?;
480 }
481 Self::open_with(dir, cfg, false)
482 }
483
484 pub fn open_or_create(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
490 let dir = dir.as_ref();
491 if !base_path(dir).exists() {
492 fs::create_dir_all(dir)?;
493 let _held = lock_dir(dir, cfg.lock_wait)?;
494 if !base_path(dir).exists() {
495 lay_down(dir)?;
496 }
497 }
498 Self::open_with(dir, cfg, false)
499 }
500
501 pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
506 Self::open_with(dir.as_ref(), Config::default(), false)
507 }
508
509 pub fn open_cfg(dir: impl AsRef<Path>, cfg: Config) -> Result<Self> {
511 Self::open_with(dir.as_ref(), cfg, false)
512 }
513
514 pub fn open_read_only(dir: impl AsRef<Path>) -> Result<Self> {
518 Self::open_with(dir.as_ref(), Config::default(), true)
519 }
520
521 fn open_with(dir: &Path, cfg: Config, read_only: bool) -> Result<Self> {
522 let stamp = Stamp::read(dir);
523 let built = match load_generation(dir, false) {
524 Err(Error::Busy { .. }) if !read_only => match lock_now(dir)? {
528 Some(_held) => load_generation(dir, true)?,
529 None => return Err(Error::busy("compaction in progress; retry")),
530 },
531 other => other?,
532 };
533 let repair = !read_only && built.log != LogState::Current { torn: false };
534 let store = Self::assemble(dir.to_path_buf(), cfg, read_only, stamp, built);
535 if repair {
536 store.repair_now()?;
537 }
538 Ok(store)
539 }
540
541 fn repair_now(&self) -> Result<()> {
545 let Some(held) = lock_now(&self.dir)? else { return Ok(()) };
546 let mut state = self.write_state()?;
547 let r = self.sync(&mut state, true).map(|_| ());
548 drop(held);
549 r
550 }
551
552 pub fn open_image(base: bytes::Bytes, wal: &[u8]) -> Result<Self> {
559 let built = build_generation(MappedFile::from_bytes(base), wal.to_vec())?;
560 if built.log == LogState::Newer {
561 return Err(newer_image());
562 }
563 Ok(Self::assemble(PathBuf::new(), Config::default(), true, None, built))
564 }
565
566 pub fn reload_image(&self, base: Option<bytes::Bytes>, wal: &[u8]) -> Result<bool> {
571 if !self.read_only {
572 return Ok(false);
573 }
574 let _one_at_a_time = self.write.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
575 match base {
576 Some(b) => self
577 .install_or_apply(MappedFile::from_bytes(b), wal.to_vec())?
578 .map(|a| a.changed)
579 .ok_or_else(newer_image),
580 None => match self.apply_log(wal)? {
581 Some((changed, _)) => Ok(changed),
582 None => {
583 Err(Error::format("log image belongs to another generation; pass its base"))
584 }
585 },
586 }
587 }
588
589 #[must_use]
591 pub fn is_image(&self) -> bool {
592 self.dir.as_os_str().is_empty()
593 }
594
595 fn assemble(
596 dir: PathBuf,
597 cfg: Config,
598 read_only: bool,
599 stamp: Option<Stamp>,
600 built: Built,
601 ) -> Self {
602 let generation = built.inner.dir.header.generation;
603 let seen = Seen {
604 stamp,
605 wal_pos: built.good_len as u64,
606 log_current: matches!(built.log, LogState::Current { .. }),
607 base_gen: generation,
608 };
609 let store = Self {
610 base_file: base_path(&dir),
611 wal_file: wal_path(&dir),
612 dir,
613 cfg,
614 read_only,
615 current: AtomicUsize::new(0),
616 gens: std::array::from_fn(|_| GenSlot {
617 seq: AtomicU64::new(0),
618 inner: RwLock::new(None),
619 }),
620 write: Mutex::new(WriteState::new(
621 built.next_seq,
622 built.replayed,
623 built.good_len as u64,
624 generation,
625 seen,
626 )),
627 compact_inline: AtomicBool::new(true),
628 due: AtomicBool::new(false),
629 };
630 *store.gens[0].inner.write().expect("fresh lock") = Some(built.inner);
631 store.gens[0].seq.store(generation, Ordering::Release);
632 store
633 }
634
635 #[must_use]
638 pub fn len(&self) -> usize {
639 self.snapshot().len()
640 }
641
642 #[must_use]
644 pub fn is_empty(&self) -> bool {
645 self.len() == 0
646 }
647
648 #[must_use]
650 pub fn dir(&self) -> &Path {
651 &self.dir
652 }
653
654 #[must_use]
657 pub fn generation(&self) -> u64 {
658 self.gens[self.current.load(Ordering::Acquire)].seq.load(Ordering::Acquire)
659 }
660
661 pub fn set_compact_inline(&self, inline: bool) {
667 self.compact_inline.store(inline, Ordering::Relaxed);
668 }
669
670 #[must_use]
673 pub fn compaction_due(&self) -> bool {
674 self.due.load(Ordering::Acquire)
675 }
676
677 #[must_use]
680 pub fn snapshot(&self) -> Snapshot<'_> {
681 loop {
682 let cur = self.current.load(Ordering::Acquire);
683 if let Ok(guard) = self.gens[cur].inner.try_read() {
684 if guard.is_some() && self.current.load(Ordering::Acquire) == cur {
685 return Snapshot::new(self, guard);
686 }
687 }
688 std::hint::spin_loop();
689 }
690 }
691
692 fn write_state(&self) -> Result<MutexGuard<'_, WriteState>> {
693 self.write.lock().map_err(|_| Error::corrupt("writer mutex poisoned"))
694 }
695
696 pub fn writer(&self) -> Result<Writer<'_>> {
702 if self.read_only {
703 return Err(Error::ReadOnly);
704 }
705 Ok(Writer::new(self, self.write_state()?))
706 }
707
708 pub fn transact<F, R>(&self, f: F) -> Result<R>
718 where
719 F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
720 {
721 crate::writer::transact(self, None, self.cfg.lock_wait, f)
722 }
723
724 pub fn try_transact<F, R>(&self, f: F) -> Result<R>
728 where
729 F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
730 {
731 crate::writer::transact(self, None, Duration::ZERO, f)
732 }
733
734 pub fn commit_lock(&self) -> Result<CommitLock> {
740 self.commit_lock_within(self.cfg.lock_wait)
741 }
742
743 pub fn try_commit_lock(&self) -> Result<CommitLock> {
746 self.commit_lock_within(Duration::ZERO)
747 }
748
749 pub fn commit_lock_within(&self, wait: Duration) -> Result<CommitLock> {
753 if self.read_only {
754 return Err(Error::ReadOnly);
755 }
756 let mut state = self.write_state()?;
757 let held = lock_dir(&self.dir, wait)?;
758 self.sync(&mut state, true)?;
759 Ok(CommitLock { _held: held, dir: self.dir.clone() })
760 }
761
762 pub fn transact_with<F, R>(&self, lock: &CommitLock, f: F) -> Result<R>
765 where
766 F: for<'t> FnOnce(&mut Txn<'t>) -> Result<R>,
767 {
768 if lock.dir != self.dir {
769 return Err(Error::format("the commit lock belongs to another store"));
770 }
771 crate::writer::transact(self, Some(lock), Duration::ZERO, f)
772 }
773
774 pub fn compact(&self) -> Result<compact::CompactStats> {
778 if self.read_only {
779 return Err(Error::ReadOnly);
780 }
781 let mut state = self.write_state()?;
782 let held = self.acquire(&mut state, self.cfg.lock_wait)?;
783 let r = self.sync(&mut state, true).and_then(|_| compact::compact_locked(self, &mut state));
784 state.release(held);
785 r
786 }
787
788 pub fn reload_if_stale(&self) -> Result<bool> {
798 if self.is_image() {
799 return Ok(false); }
801 let mut state = if self.read_only {
802 self.write.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
803 } else {
804 match self.write.try_lock() {
805 Ok(g) => g,
806 Err(std::sync::TryLockError::WouldBlock) => return Ok(false),
807 Err(std::sync::TryLockError::Poisoned(p)) => p.into_inner(),
808 }
809 };
810 self.sync(&mut state, false)
811 }
812
813 pub(crate) fn acquire(&self, state: &mut WriteState, wait: Duration) -> Result<Held> {
816 let file = match state.lock_file.take() {
817 Some(f) => f,
818 None => open_guarded(&self.dir, LOCK_FILE)?,
819 };
820 if state.wait_file.is_none() {
821 match open_guarded(&self.dir, WAIT_FILE) {
822 Ok(t) => state.wait_file = Some(t),
823 Err(e) => {
824 state.lock_file = Some(file);
825 return Err(e);
826 }
827 }
828 }
829 let turnstile = state.wait_file.as_ref().expect("opened above");
830 match lock_within(&file, turnstile, wait) {
831 Ok(()) => Ok(Held(file)),
832 Err(e) => {
833 state.lock_file = Some(file);
834 Err(e)
835 }
836 }
837 }
838
839 pub(crate) fn sync(&self, state: &mut WriteState, repair: bool) -> Result<bool> {
845 let (changed, caught) = self.catch_up(&mut state.seen, repair)?;
846 if caught == Caught::Nothing && !repair {
847 return Ok(false);
848 }
849 if caught == Caught::Full {
850 state.drop_wal_handle(); }
852 if repair {
853 self.repair(state)?;
854 }
855 let cur = self.current.load(Ordering::Acquire);
856 let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
857 let g = guard.as_ref().ok_or_else(|| Error::corrupt("current slot empty"))?;
858 let next_seq = g.dir.header.wal_applied_seq.max(g.log.last_seq()) + 1;
859 state.adopt(next_seq, g.log.len(), state.seen.base_gen);
860 if caught == Caught::Full {
861 let over = state.ops_since_compact >= self.cfg.compact_after_ops
862 || state.wal_bytes >= self.cfg.compact_after_wal_bytes;
863 self.due.store(over && !self.compact_inline.load(Ordering::Relaxed), Ordering::Release);
864 }
865 Ok(changed)
866 }
867
868 fn repair(&self, state: &mut WriteState) -> Result<()> {
874 let seen = &mut state.seen;
875 let file = &self.wal_file;
876 if !seen.log_current {
877 atomic_write(file, &wal::encode_header(seen.base_gen, now_ms()))?;
878 *seen = Seen {
879 stamp: Stamp::of(&self.base_file, file),
880 wal_pos: wal::HEADER_LEN as u64,
881 log_current: true,
882 base_gen: seen.base_gen,
883 };
884 state.drop_wal_handle();
885 return Ok(());
886 }
887 if let Some((ident, len)) = seen.stamp.and_then(|s| s.wal) {
888 if len > seen.wal_pos {
889 let f = fs::OpenOptions::new().write(true).open(file)?;
890 f.set_len(seen.wal_pos)?;
891 f.sync_all()?;
892 if let Some(s) = seen.stamp.as_mut() {
893 s.wal = Some((ident, seen.wal_pos));
894 }
895 }
896 }
897 Ok(())
898 }
899
900 fn catch_up(&self, seen: &mut Seen, locked: bool) -> Result<(bool, Caught)> {
905 let now = Stamp::of(&self.base_file, &self.wal_file);
906 if let (Some(n), Some(l)) = (now, seen.stamp) {
907 if n == l {
908 return Ok((false, Caught::Nothing));
909 }
910 if let (Some((ni, nlen)), Some((li, _))) = (n.wal, l.wal) {
913 if n.base == l.base
914 && ni == li
915 && nlen >= seen.wal_pos
916 && seen.wal_pos >= wal::HEADER_LEN as u64
917 {
918 let (changed, pos) = self.reload_wal_tail(seen.wal_pos)?;
919 *seen = Seen { stamp: now, wal_pos: pos, ..*seen };
920 return Ok((changed, Caught::Tail));
921 }
922 }
923 }
924 let (changed, pos, log_current, base_gen) = self.reload_full(locked)?;
925 *seen = Seen { stamp: now, wal_pos: pos, log_current, base_gen };
926 Ok((changed, Caught::Full))
927 }
928
929 fn reload_wal_tail(&self, pos: u64) -> Result<(bool, u64)> {
935 use std::io::{Read as _, Seek as _, SeekFrom};
936 let mut tail = Vec::new();
937 match fs::File::open(&self.wal_file) {
938 Ok(mut f) => {
939 f.seek(SeekFrom::Start(pos))?;
940 f.read_to_end(&mut tail)?;
941 }
942 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok((false, pos)),
943 Err(e) => return Err(e.into()),
944 }
945 #[cfg(test)]
946 probe::bump(&probe::TAIL_BYTES, tail.len() as u64);
947 let scanned = wal::scan(&tail, 0, None);
948 if let Some(e) = scanned.corrupt {
949 return Err(e);
950 }
951 tail.truncate(scanned.end);
952 Ok((self.publish_new(tail, 0)?, pos + scanned.end as u64))
953 }
954
955 fn publish_new(&self, seg: Vec<u8>, from: usize) -> Result<bool> {
959 let cur = self.current.load(Ordering::Acquire);
960 let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
961 let Some(g) = guard.as_ref() else { return Ok(false) };
962 publish_above(&g.log, g.dir.header.wal_applied_seq.max(g.log.last_seq()), seg, from)
963 }
964
965 fn apply_log(&self, raw: &[u8]) -> Result<Option<(bool, usize)>> {
970 if raw.is_empty() {
971 return Ok(Some((false, wal::HEADER_LEN)));
972 }
973 if wal::parse_header(raw)? != self.generation() {
974 return Ok(None);
975 }
976 let replayed = wal::replay(raw)?;
977 let served = self.served_seq()?;
978 let start = replayed.records().find(|r| r.last_seq() > served).map(|r| r.offset());
979 let changed = match start {
980 Some(at) => self.publish_new(raw[at..replayed.good_len].to_vec(), 0)?,
981 None => false,
982 };
983 Ok(Some((changed, replayed.good_len)))
984 }
985
986 fn reload_full(&self, locked: bool) -> Result<(bool, u64, bool, u64)> {
991 for _ in 0..RACE_RETRIES {
992 let map = MappedFile::open(&self.base_file)?;
993 let base_gen = base::Header::parse(map.bytes())?.generation;
994 let raw = match fs::read(&self.wal_file) {
995 Ok(raw) => raw,
996 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
997 Err(e) => return Err(e.into()),
998 };
999 if let Some(a) = self.install_or_apply(map, raw)? {
1000 return Ok((a.changed, a.pos as u64, a.current, base_gen));
1001 }
1002 if locked {
1003 return Err(Error::corrupt("wal is newer than base"));
1004 }
1005 }
1006 Err(Error::busy("compaction in progress; retry"))
1007 }
1008
1009 fn install_or_apply(&self, map: MappedFile, raw: Vec<u8>) -> Result<Option<Applied>> {
1016 let cur_gen = self.generation();
1017 let new_gen = base::Header::parse(map.bytes())?.generation;
1018 if new_gen == cur_gen {
1019 return Ok(match self.apply_log(&raw)? {
1022 Some((changed, pos)) => Some(Applied { changed, pos, current: !raw.is_empty() }),
1023 None if wal::parse_header(&raw)? > new_gen => None,
1026 None => Some(Applied { changed: false, pos: wal::HEADER_LEN, current: false }),
1027 });
1028 }
1029 let built = build_generation(map, raw)?;
1030 let pos = built.good_len;
1031 let current = matches!(built.log, LogState::Current { .. });
1032 if built.log == LogState::Newer {
1033 return Ok(None);
1034 }
1035 let folded = built.inner.dir.header.wal_applied_seq;
1036 match self.install(built.inner, new_gen) {
1037 None => Ok(Some(Applied { changed: true, pos, current })),
1038 Some(mut inner) => {
1045 if folded > self.served_seq()? {
1046 return Err(Error::busy("all generation slots pinned by snapshots"));
1047 }
1048 let changed = match inner.log.take_first_segment() {
1049 Some(seg) => self.publish_new(seg, wal::HEADER_LEN)?,
1050 None => false,
1051 };
1052 Ok(Some(Applied { changed, pos, current }))
1053 }
1054 }
1055 }
1056
1057 fn served_seq(&self) -> Result<u64> {
1059 let cur = self.current.load(Ordering::Acquire);
1060 let guard = self.gens[cur].inner.try_read().map_err(|_| Error::busy("slot busy"))?;
1061 Ok(guard.as_ref().map_or(0, |g| g.dir.header.wal_applied_seq.max(g.log.last_seq())))
1062 }
1063
1064 #[must_use]
1069 pub(crate) fn install(&self, inner: GenInner, generation: u64) -> Option<GenInner> {
1070 let cur = self.current.load(Ordering::Acquire);
1071 for (i, slot) in self.gens.iter().enumerate() {
1072 if i == cur {
1073 continue;
1074 }
1075 if let Ok(mut w) = slot.inner.try_write() {
1076 *w = Some(inner);
1077 drop(w);
1078 slot.seq.store(generation, Ordering::Release);
1079 self.current.store(i, Ordering::Release);
1080 return None;
1081 }
1082 }
1083 Some(inner)
1084 }
1085}
1086
1087fn publish_above(log: &LogArena, served: u64, seg: Vec<u8>, from: usize) -> Result<bool> {
1091 let new: usize = wal::Records::trusted(&seg, from, seg.len())
1092 .map(|r| match r.last_seq().checked_sub(served) {
1093 Some(n) if n > 0 => n.min(u64::from(r.op_count())) as usize,
1094 _ => 0,
1095 })
1096 .sum();
1097 if new == 0 {
1098 return Ok(false);
1099 }
1100 if !log.reserve(new, seg.len()) {
1101 return Err(Error::busy("overlay arena full"));
1102 }
1103 publish_records(log, seg, from, served);
1104 Ok(true)
1105}
1106
1107struct Applied {
1109 changed: bool,
1111 pos: usize,
1113 current: bool,
1115}
1116
1117fn lock_now(dir: &Path) -> Result<Option<Held>> {
1119 match lock_dir(dir, Duration::ZERO) {
1120 Ok(h) => Ok(Some(h)),
1121 Err(Error::Locked) => Ok(None),
1122 Err(e) => Err(e),
1123 }
1124}
1125
1126fn newer_image() -> Error {
1127 Error::busy("log image is newer than its base; fetch the base again")
1128}
1129
1130struct Built {
1133 inner: GenInner,
1134 next_seq: u64,
1135 replayed: usize,
1136 good_len: usize,
1137 log: LogState,
1138}
1139
1140#[derive(Clone, Copy, PartialEq, Eq)]
1142enum LogState {
1143 Empty,
1145 Stale,
1147 Current { torn: bool },
1149 Newer,
1152}
1153
1154fn build_generation(map: MappedFile, mut raw: Vec<u8>) -> Result<Built> {
1160 let bdir = base::BaseDir::parse(map.bytes())?;
1161 let header = bdir.header;
1162 let inner = GenInner { map, dir: bdir, log: LogArena::new() };
1163 let folded = Built {
1164 inner,
1165 next_seq: header.wal_applied_seq + 1,
1166 replayed: 0,
1167 good_len: 0,
1168 log: LogState::Empty,
1169 };
1170 if raw.is_empty() {
1171 return Ok(folded);
1172 }
1173 let generation = wal::parse_header(&raw)?;
1174 if generation > header.generation {
1175 return Ok(Built { log: LogState::Newer, ..folded });
1176 }
1177 if generation < header.generation {
1178 return Ok(Built { good_len: wal::HEADER_LEN, log: LogState::Stale, ..folded });
1179 }
1180 let scanned = wal::scan(&raw, wal::HEADER_LEN, None);
1181 if let Some(e) = scanned.corrupt {
1182 return Err(e);
1183 }
1184 let good_len = scanned.end;
1185 let torn = good_len < raw.len();
1186 let applied = header.wal_applied_seq;
1187 let next_seq = scanned.last_seq.map_or(applied, |l| l.max(applied)) + 1;
1188 raw.truncate(good_len);
1189 publish_above(&folded.inner.log, applied, raw, wal::HEADER_LEN)
1190 .map_err(|_| Error::busy("overlay arena full during replay"))?;
1191 let replayed = folded.inner.log.len();
1192 Ok(Built { next_seq, replayed, good_len, log: LogState::Current { torn }, ..folded })
1193}
1194
1195fn load_generation(dir: &Path, locked: bool) -> Result<Built> {
1200 let wal_file = wal_path(dir);
1201 for _ in 0..RACE_RETRIES {
1202 let map = MappedFile::open(&base_path(dir))?;
1203 let raw = match fs::read(&wal_file) {
1204 Ok(raw) => raw,
1205 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
1206 Err(e) => return Err(e.into()),
1207 };
1208 let built = build_generation(map, raw)?;
1209 if built.log != LogState::Newer {
1210 return Ok(built);
1211 }
1212 if locked {
1213 return Err(Error::corrupt("wal is newer than base"));
1214 }
1215 }
1216 Err(Error::busy("compaction in progress; retry"))
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use super::*;
1222 use crate::writer::DocRecord;
1223 use crate::UrlKey;
1224 use std::time::{Duration, Instant};
1225
1226 fn doc(url: &str, hash: u64) -> DocRecord<'_> {
1227 DocRecord {
1228 url,
1229 url_key: UrlKey::of(url),
1230 content_hash: hash,
1231 fetched_at_ms: 1,
1232 title: None,
1233 snippet: None,
1234 etag: None,
1235 pinned: false,
1236 }
1237 }
1238
1239 fn seqs(raw: &[u8]) -> Vec<u64> {
1241 wal::replay(raw).unwrap().records().flat_map(|r| r.first_seq()..=r.last_seq()).collect()
1242 }
1243
1244 fn write(store: &Store, url: &str, hash: u64) {
1245 let mut w = store.writer().unwrap();
1246 w.upsert_node(&doc(url, hash)).unwrap();
1247 w.commit().unwrap();
1248 }
1249
1250 #[test]
1253 fn a_snapshot_does_not_observe_later_commits() {
1254 let dir = tempfile::tempdir().unwrap();
1255 let store = Store::create(dir.path(), Config::default()).unwrap();
1256 write(&store, "https://x.dev/a", 1);
1257
1258 let before = store.snapshot();
1259 assert_eq!(before.len(), 1);
1260
1261 write(&store, "https://x.dev/b", 2);
1262 assert_eq!(before.len(), 1, "an existing snapshot must not grow");
1263 assert_eq!(store.snapshot().len(), 2, "a fresh snapshot sees the commit");
1264 }
1265
1266 #[test]
1270 fn compaction_leaves_an_existing_snapshot_on_its_own_generation() {
1271 let dir = tempfile::tempdir().unwrap();
1272 let store = Store::create(dir.path(), Config::default()).unwrap();
1273 write(&store, "https://x.dev/a", 1);
1274 store.compact().unwrap();
1275
1276 let pinned = store.snapshot();
1277 let pinned_gen = pinned.generation();
1278
1279 write(&store, "https://x.dev/b", 2);
1280 store.compact().unwrap();
1281
1282 assert_eq!(pinned.generation(), pinned_gen, "a pinned generation must not move");
1283 assert_eq!(pinned.len(), 1, "the old generation still reads its own state");
1284 let fresh = store.snapshot();
1285 assert!(fresh.generation() > pinned_gen, "the store advanced");
1286 assert_eq!(fresh.len(), 2);
1287 }
1288
1289 #[test]
1292 fn each_compaction_advances_the_generation_exactly_once() {
1293 let dir = tempfile::tempdir().unwrap();
1294 let store = Store::create(dir.path(), Config::default()).unwrap();
1295 write(&store, "https://x.dev/a", 1);
1296
1297 let mut prev = store.snapshot().generation();
1298 for _ in 0..6 {
1299 store.compact().unwrap();
1300 let now = store.snapshot().generation();
1301 assert_eq!(now, prev + 1, "a compaction must advance the generation by one");
1302 prev = now;
1303 }
1304 }
1305
1306 #[test]
1311 fn exhausting_the_generation_ring_is_a_retryable_refusal() {
1312 let dir = tempfile::tempdir().unwrap();
1313 let store = Store::create(dir.path(), Config::default()).unwrap();
1314 write(&store, "https://x.dev/a", 1);
1315
1316 let mut pins = Vec::new();
1320 for round in 0..GEN_SLOTS - 1 {
1321 pins.push(store.snapshot());
1322 store.compact().unwrap_or_else(|e| panic!("round {round} should still fit: {e}"));
1323 }
1324
1325 pins.push(store.snapshot());
1327 assert!(
1328 matches!(store.compact(), Err(Error::Busy { .. })),
1329 "a full ring must refuse with Busy, not stall or corrupt the swap"
1330 );
1331
1332 drop(pins);
1334 store.compact().expect("compaction must succeed once a slot frees up");
1335 assert_eq!(store.snapshot().len(), 1);
1336 }
1337
1338 #[test]
1341 fn a_read_only_handle_reloads_a_newer_generation() {
1342 let dir = tempfile::tempdir().unwrap();
1343 let store = Store::create(dir.path(), Config::default()).unwrap();
1344 write(&store, "https://x.dev/a", 1);
1345 store.compact().unwrap();
1346
1347 let reader = Store::open_read_only(dir.path()).unwrap();
1348 assert_eq!(reader.snapshot().len(), 1);
1349 assert!(!reader.reload_if_stale().unwrap(), "nothing has changed yet");
1350
1351 write(&store, "https://x.dev/b", 2);
1352 store.compact().unwrap();
1353
1354 assert!(reader.reload_if_stale().unwrap(), "a new generation is visible");
1355 assert_eq!(reader.snapshot().len(), 2);
1356 }
1357
1358 fn quick() -> Config {
1361 Config { lock_wait: Duration::from_millis(30), ..Config::default() }
1362 }
1363
1364 fn urls(store: &Store) -> Vec<String> {
1365 let mut v = Vec::new();
1366 store.snapshot().for_each_live(|n| v.push(n.url.to_owned()));
1367 v.sort();
1368 v
1369 }
1370
1371 #[test]
1375 fn a_commit_catches_up_with_a_grown_log_first() {
1376 let dir = tempfile::tempdir().unwrap();
1377 let a = Store::create(dir.path(), Config::default()).unwrap();
1378 let b = Store::open(dir.path()).unwrap();
1379 write(&a, "https://x.dev/a", 1);
1380 write(&b, "https://x.dev/b", 2);
1381 write(&a, "https://x.dev/c", 3);
1382 assert_eq!(urls(&a), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
1383 let raw = fs::read(wal_path(dir.path())).unwrap();
1384 let seqs = seqs(&raw);
1385 assert_eq!(seqs, [1, 2, 3], "one sequence across both writers");
1386 assert!(b.reload_if_stale().unwrap(), "a writable handle reloads too");
1387 assert_eq!(urls(&b), urls(&a));
1388 }
1389
1390 #[test]
1393 fn a_commit_catches_up_after_another_handle_compacted() {
1394 let dir = tempfile::tempdir().unwrap();
1395 let a = Store::create(dir.path(), Config::default()).unwrap();
1396 let b = Store::open(dir.path()).unwrap();
1397 write(&a, "https://x.dev/a", 1); write(&b, "https://x.dev/b", 2);
1399 b.compact().unwrap();
1400 let generation = b.generation();
1401 write(&a, "https://x.dev/c", 3);
1402 assert_eq!(a.generation(), generation, "the compacted base was installed");
1403 drop((a, b));
1404 let fresh = Store::open_read_only(dir.path()).unwrap();
1405 assert_eq!(urls(&fresh), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
1406 }
1407
1408 #[test]
1410 fn a_commit_past_lock_wait_is_locked_and_keeps_its_ops() {
1411 let dir = tempfile::tempdir().unwrap();
1412 let a = Store::create(dir.path(), Config::default()).unwrap();
1413 let b = Store::open_cfg(dir.path(), quick()).unwrap();
1414 let held = a.commit_lock().unwrap();
1415 let mut w = b.writer().unwrap();
1416 w.upsert_node(&doc("https://x.dev/a", 1)).unwrap();
1417 let started = Instant::now();
1418 assert!(matches!(w.commit(), Err(Error::Locked)));
1419 assert!(started.elapsed() >= Duration::from_millis(30), "it waited lock_wait");
1420 let third = Store::open_cfg(dir.path(), quick()).unwrap();
1421 assert!(matches!(third.compact(), Err(Error::Locked)), "compaction takes the same lock");
1422 let patient = Store::open(dir.path()).unwrap(); let started = Instant::now();
1425 let ran = std::cell::Cell::new(false);
1426 let refused = patient.try_transact(|_| {
1427 ran.set(true);
1428 Ok(())
1429 });
1430 assert!(matches!(refused, Err(Error::Locked)) && !ran.get());
1431 assert!(started.elapsed() < Duration::from_secs(5), "try_transact waited");
1432 drop(held);
1433 w.commit().unwrap();
1434 drop(w);
1435 assert_eq!(b.len(), 1);
1436 }
1437
1438 #[test]
1441 fn an_aborted_transaction_writes_nothing() {
1442 let dir = tempfile::tempdir().unwrap();
1443 let store = Store::create(dir.path(), Config::default()).unwrap();
1444 let before = fs::read(wal_path(dir.path())).unwrap();
1445 let r: Result<()> = store.transact(|tx| {
1446 tx.upsert_node(&doc("https://x.dev/a", 1))?;
1447 Err(Error::format("caller changed its mind"))
1448 });
1449 assert!(r.is_err());
1450 assert_eq!(fs::read(wal_path(dir.path())).unwrap(), before, "nothing appended");
1451 assert!(store.is_empty());
1452 store
1453 .transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2)))
1454 .expect("the next transaction commits");
1455 assert_eq!(urls(&store), ["https://x.dev/b"], "the aborted op never resurfaces");
1456 }
1457
1458 #[test]
1461 fn transactions_allocate_ids_without_collisions() {
1462 let dir = tempfile::tempdir().unwrap();
1463 let a = Store::create(dir.path(), Config::default()).unwrap();
1464 let b = Store::open(dir.path()).unwrap();
1465 let next = |store: &Store| {
1466 store
1467 .transact(|tx| {
1468 let id = tx.snapshot().len();
1469 let url = format!("https://x.dev/id/{id}");
1470 tx.upsert_node(&doc(&url, id as u64))?;
1471 Ok(id)
1472 })
1473 .unwrap()
1474 };
1475 let ids: Vec<usize> = (0..10).map(|i| next(if i % 2 == 0 { &a } else { &b })).collect();
1476 assert_eq!(ids, (0..10).collect::<Vec<_>>());
1477 }
1478
1479 #[test]
1482 fn open_takes_no_lock_and_a_transaction_is_one_lock_one_fsync() {
1483 let dir = tempfile::tempdir().unwrap();
1484 drop(Store::create(dir.path(), Config::default()).unwrap());
1485 probe::take(&probe::LOCKS);
1486 let store = Store::open(dir.path()).unwrap();
1487 let other = Store::open(dir.path()).unwrap();
1488 assert_eq!(probe::take(&probe::LOCKS), 0, "open made a lock call");
1489 drop(lock_dir(dir.path(), Duration::ZERO).expect("the lock is free after open"));
1491 probe::take(&probe::LOCKS);
1492 probe::take(&probe::FSYNCS);
1493 probe::take(&probe::TURNSTILE);
1494 probe::take(&probe::WRITES);
1495 store
1496 .transact(|tx| {
1497 for i in 0..100 {
1498 tx.upsert_node(&doc(&format!("https://x.dev/{i}"), i))?;
1499 }
1500 Ok(())
1501 })
1502 .unwrap();
1503 assert_eq!(probe::take(&probe::LOCKS), 1, "one lock per transaction");
1504 assert_eq!(probe::take(&probe::TURNSTILE), 1, "one wait-queue probe per transaction");
1505 assert_eq!(probe::take(&probe::WRITES), 1, "one write per transaction");
1506 assert_eq!(probe::take(&probe::FSYNCS), 1, "one fsync per transaction");
1507 let raw = fs::read(wal_path(dir.path())).unwrap();
1508 assert_eq!(wal::replay(&raw).unwrap().records().count(), 1, "one record per transaction");
1509 assert_eq!(other.len(), 0);
1510 assert!(other.reload_if_stale().unwrap());
1511 assert_eq!(other.len(), 100);
1512 }
1513
1514 #[test]
1519 fn a_writer_queues_behind_an_announced_waiter() {
1520 let dir = tempfile::tempdir().unwrap();
1521 let store = Store::create(dir.path(), Config::default()).unwrap();
1522 let waiter = open_guarded(dir.path(), WAIT_FILE).unwrap();
1523 waiter.try_lock_shared().unwrap();
1524 let r = store.try_transact(|tx| tx.upsert_node(&doc("https://x.dev/a", 1)));
1525 assert!(matches!(r, Err(Error::Locked)), "jumped the queue: {r:?}");
1526 probe::take(&probe::LOCKS);
1527 store.transact(|tx| tx.upsert_node(&doc("https://x.dev/a", 1))).unwrap();
1528 assert_eq!(probe::take(&probe::LOCKS), 1, "tried the lock before standing back");
1529 waiter.unlock().unwrap();
1530 store.try_transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2))).unwrap();
1531 assert_eq!(store.len(), 2);
1532 }
1533
1534 #[test]
1537 fn a_catch_up_reads_only_the_appended_bytes() {
1538 let dir = tempfile::tempdir().unwrap();
1539 let a = Store::create(dir.path(), Config::default()).unwrap();
1540 let b = Store::open(dir.path()).unwrap();
1541 for i in 0..50 {
1542 write(&a, &format!("https://x.dev/{i}"), i);
1543 }
1544 assert!(b.reload_if_stale().unwrap());
1545 let len = || fs::metadata(wal_path(dir.path())).unwrap().len();
1546 let before = len();
1547 write(&a, "https://x.dev/new", 99);
1548 let appended = len() - before;
1549 probe::take(&probe::TAIL_BYTES);
1550 write(&b, "https://x.dev/mine", 100);
1551 assert_eq!(probe::take(&probe::TAIL_BYTES), appended, "read only a's append");
1552 assert_eq!(b.len(), 52);
1553 }
1554
1555 #[test]
1559 fn the_next_committer_repairs_a_torn_tail() {
1560 use std::io::Write as _;
1561 let dir = tempfile::tempdir().unwrap();
1562 let a = Store::create(dir.path(), Config::default()).unwrap();
1563 let b = Store::open(dir.path()).unwrap();
1564 write(&a, "https://x.dev/a", 1);
1565 let intact = fs::metadata(wal_path(dir.path())).unwrap().len();
1566 let mut f = fs::OpenOptions::new().append(true).open(wal_path(dir.path())).unwrap();
1568 f.write_all(&[0x40, 0, 0, 0, 1, 2, 3]).unwrap();
1569 drop(f);
1570 write(&b, "https://x.dev/b", 2);
1571 let raw = fs::read(wal_path(dir.path())).unwrap();
1572 assert_eq!(wal::committed_len(&raw), raw.len(), "no garbage left in the log");
1573 assert!(raw.len() as u64 > intact);
1574 drop((a, b));
1575 let fresh = Store::open_read_only(dir.path()).unwrap();
1576 assert_eq!(urls(&fresh), ["https://x.dev/a", "https://x.dev/b"]);
1577 }
1578
1579 fn wal_len(dir: &Path) -> u64 {
1582 fs::metadata(wal_path(dir)).unwrap().len()
1583 }
1584
1585 #[test]
1590 fn a_full_overlay_refuses_before_writing_and_keeps_the_ops() {
1591 let dir = tempfile::tempdir().unwrap();
1592 let store = Store::create(dir.path(), Config::default()).unwrap();
1593 write(&store, "https://x.dev/a", 1);
1594 let before = wal_len(dir.path());
1595 let mut w = store.writer().unwrap();
1596 w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
1597 probe::FULL.with(|f| f.set(1));
1598 let r = w.commit();
1599 probe::FULL.with(|f| f.set(0));
1600 assert!(matches!(r, Err(Error::Busy { .. })), "{r:?}");
1601 assert_eq!(wal_len(dir.path()), before, "a refused commit wrote to the log");
1602 w.commit().expect("the staged op commits once there is room");
1603 drop(w);
1604 assert_eq!(urls(&store), ["https://x.dev/a", "https://x.dev/b"]);
1605 }
1606
1607 #[test]
1610 fn a_failed_append_keeps_the_ops() {
1611 let dir = tempfile::tempdir().unwrap();
1612 let store = Store::create(dir.path(), Config::default()).unwrap();
1613 write(&store, "https://x.dev/a", 1);
1614 let before = wal_len(dir.path());
1615 let mut w = store.writer().unwrap();
1616 w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
1617 probe::FAIL_APPEND.with(|f| f.set(1));
1618 assert!(matches!(w.commit(), Err(Error::Io { .. })));
1619 assert_eq!(wal_len(dir.path()), before, "the failed append was cut back");
1620 w.commit().expect("the kept op commits on retry");
1621 drop(w);
1622 assert_eq!(urls(&store), ["https://x.dev/a", "https://x.dev/b"]);
1623 }
1624
1625 #[test]
1631 fn a_failed_append_that_stays_in_the_log_never_reuses_a_sequence() {
1632 let dir = tempfile::tempdir().unwrap();
1633 let store = Store::create(dir.path(), Config::default()).unwrap();
1634 write(&store, "https://x.dev/a", 1);
1635 let mut w = store.writer().unwrap();
1636 w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
1637 probe::FAIL_APPEND.with(|f| f.set(1));
1638 probe::STUCK.with(|f| f.set(1));
1639 let failed = w.commit();
1640 probe::STUCK.with(|f| f.set(0));
1641 assert!(matches!(failed, Err(Error::Io { .. })));
1642 w.commit().expect("the kept op commits on retry");
1643 w.upsert_node(&doc("https://x.dev/c", 3)).unwrap();
1644 w.commit().unwrap();
1645 drop(w);
1646 drop(store);
1647 let raw = fs::read(wal_path(dir.path())).unwrap();
1648 wal::replay(&raw).expect("no sequence regression");
1649 let seqs = seqs(&raw);
1650 assert!(seqs.windows(2).all(|p| p[0] < p[1]), "sequences {seqs:?}");
1651 let reopened = Store::open(dir.path()).expect("the store reopens");
1652 assert_eq!(urls(&reopened), ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"]);
1653 }
1654
1655 #[test]
1659 fn a_failed_transaction_leaves_nothing_of_its_own_staged() {
1660 let dir = tempfile::tempdir().unwrap();
1661 let store = Store::create(dir.path(), Config::default()).unwrap();
1662 probe::FAIL_APPEND.with(|f| f.set(1));
1663 let r = store.transact(|tx| tx.upsert_node(&doc("https://x.dev/a", 1)));
1664 assert!(matches!(r, Err(Error::Io { .. })), "{r:?}");
1665 store.transact(|tx| tx.upsert_node(&doc("https://x.dev/b", 2))).unwrap();
1666 assert_eq!(urls(&store), ["https://x.dev/b"]);
1667 }
1668
1669 #[test]
1673 fn an_inline_compaction_that_cannot_install_leaves_the_commit_ok_and_due() {
1674 let dir = tempfile::tempdir().unwrap();
1675 let cfg = Config { compact_after_ops: 1, ..Config::default() };
1676 let store = Store::create(dir.path(), cfg).unwrap();
1677 let mut pins = vec![store.snapshot()];
1678 for (i, url) in ["https://x.dev/a", "https://x.dev/b", "https://x.dev/c"].iter().enumerate()
1679 {
1680 write(&store, url, i as u64);
1681 pins.push(store.snapshot());
1682 }
1683 let mut w = store.writer().unwrap();
1684 w.upsert_node(&doc("https://x.dev/d", 4)).unwrap();
1685 w.commit().expect("the commit itself succeeded");
1686 drop(w);
1687 assert!(store.compaction_due(), "the compaction is left due");
1688 assert_eq!(store.len(), 4, "and the commit is visible");
1689 drop(pins);
1690 store.compact().unwrap();
1691 assert!(!store.compaction_due());
1692 assert_eq!(store.len(), 4);
1693 }
1694
1695 #[test]
1698 fn an_abort_truncates_to_its_mark_and_keeps_earlier_ops() {
1699 let dir = tempfile::tempdir().unwrap();
1700 let store = Store::create(dir.path(), Config::default()).unwrap();
1701 let mut w = store.writer().unwrap();
1702 w.upsert_node(&doc("https://x.dev/kept", 1)).unwrap();
1703 drop(w);
1704 let r: Result<()> = store.transact(|tx| {
1705 tx.upsert_node(&doc("https://x.dev/aborted", 2))?;
1706 tx.remove(UrlKey::of("https://x.dev/kept"))?;
1707 Err(Error::format("abort"))
1708 });
1709 assert!(r.is_err());
1710 store.writer().unwrap().commit().unwrap();
1711 assert_eq!(urls(&store), ["https://x.dev/kept"]);
1712 }
1713
1714 #[test]
1718 fn a_transaction_over_the_record_cap_is_refused_before_writing() {
1719 let dir = tempfile::tempdir().unwrap();
1720 let store = Store::create(dir.path(), Config::default()).unwrap();
1721 let before = wal_len(dir.path());
1722 let big = "v".repeat(wal::MAX_ATTR_LEN);
1723 let mut w = store.writer().unwrap();
1724 w.upsert_node(&doc("https://x.dev/a", 1)).unwrap();
1725 let mut k = 0u64;
1726 while (k as usize) * wal::MAX_ATTR_LEN <= wal::MAX_RECORD {
1727 w.set_attrs(UrlKey(k), &[(0, &big)]).unwrap();
1728 k += 1;
1729 }
1730 let first = w.commit().unwrap_err();
1731 assert!(matches!(first, Error::Format { .. }), "{first}");
1732 assert_eq!(wal_len(dir.path()), before, "nothing was written");
1733 let again = w.commit().unwrap_err();
1734 assert_eq!(again.to_string(), first.to_string(), "the same ops are still staged");
1735 w.discard();
1736 w.upsert_node(&doc("https://x.dev/b", 2)).unwrap();
1737 w.commit().unwrap();
1738 drop(w);
1739 assert_eq!(urls(&store), ["https://x.dev/b"]);
1740 }
1741}