1#![allow(clippy::not_unsafe_ptr_arg_deref)]
2
3use crate::io::FileSyncType;
4use crate::sync::Mutex;
5use crate::sync::OnceLock;
6use crate::{turso_assert, turso_assert_greater_than, turso_debug_assert};
7use branches::mark_unlikely;
8use rustc_hash::{FxHashMap, FxHashSet};
9use std::array;
10use std::borrow::Cow;
11use std::collections::BTreeMap;
12use std::num::NonZeroUsize;
13use strum::EnumString;
14use tracing::{instrument, Level};
15
16use crate::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
17use crate::sync::RwLock;
18use bitflags::bitflags;
19use std::fmt::{Debug, Formatter};
20use std::{fmt, sync::Arc};
21
22use super::buffer_pool::BufferPool;
23use super::pager::{PageRef, Pager};
24use super::sqlite3_ondisk::{
25 self, checksum_wal, DatabaseHeader, WalHeader, WAL_MAGIC_BE, WAL_MAGIC_LE,
26};
27use crate::fast_lock::SpinLock;
28use crate::io::clock::MonotonicInstant;
29use crate::io::CompletionGroup;
30use crate::io::{File, IO};
31use crate::storage::database::{DatabaseStorage, EncryptionOrChecksum};
32#[cfg(host_shared_wal)]
33use crate::storage::shared_wal_coordination::SharedWalCoordinationOpenMode;
34#[cfg(host_shared_wal)]
35use crate::storage::shared_wal_coordination::{
36 MappedSharedWalCoordination, SharedOwnerRecord, SharedReaderSlot, SharedWalCoordinationHeader,
37};
38use crate::storage::sqlite3_ondisk::{
39 begin_read_wal_frame, begin_read_wal_frame_raw, finish_read_page, prepare_wal_frame,
40 write_pages_vectored, PageSize, WAL_FRAME_HEADER_SIZE, WAL_HEADER_SIZE,
41};
42use crate::types::{IOCompletions, IOResult};
43use crate::util::IOExt as _;
44use crate::{
45 bail_corrupt_error, io_yield_one, Buffer, Completion, CompletionError, IOContext, LimboError,
46 Result,
47};
48
49#[derive(Debug, Clone)]
51pub struct RollbackTo {
52 pub frame: u64,
53 pub checksum: (u32, u32),
54 pub checkpoint_seq: u32,
57}
58
59#[derive(Debug, Clone, Default)]
60pub struct CheckpointResult {
61 pub wal_max_frame: u64,
64 pub wal_total_backfilled: u64,
66 pub wal_checkpoint_backfilled: u64,
68 maybe_guard: Option<CheckpointLocks>,
71 pub db_truncate_sent: bool,
72 pub db_sync_sent: bool,
73 pub wal_truncate_sent: bool,
75 pub wal_sync_sent: bool,
77}
78
79impl Drop for CheckpointResult {
80 fn drop(&mut self) {
81 let _ = self.maybe_guard.take();
82 }
83}
84
85impl CheckpointResult {
86 pub fn new(
87 wal_max_frame: u64,
88 wal_total_backfilled: u64,
89 wal_checkpoint_backfilled: u64,
90 ) -> Self {
91 Self {
92 wal_max_frame,
93 wal_total_backfilled,
94 wal_checkpoint_backfilled,
95 maybe_guard: None,
96 db_sync_sent: false,
97 db_truncate_sent: false,
98 wal_truncate_sent: false,
99 wal_sync_sent: false,
100 }
101 }
102
103 pub const fn everything_backfilled(&self) -> bool {
104 self.wal_max_frame == self.wal_total_backfilled
105 }
106 pub fn should_truncate(&self) -> bool {
107 self.everything_backfilled()
110 }
111 pub fn release_guard(&mut self) {
112 let _ = self.maybe_guard.take();
113 }
114}
115
116#[cfg(host_shared_wal)]
117pub(crate) fn coordination_path_for_wal_path(wal_path: &str) -> String {
118 if let Some(db_path) = wal_path.strip_suffix("-wal") {
119 format!("{db_path}-tshm")
120 } else {
121 format!("{wal_path}-tshm")
122 }
123}
124
125bitflags! {
126 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
137 pub struct WalAutoActions: u8 {
138 const Checkpoint = 0b01;
141 const Restart = 0b10;
144 }
145}
146
147impl WalAutoActions {
148 pub const fn all_enabled() -> Self {
150 Self::from_bits_truncate(Self::Checkpoint.bits() | Self::Restart.bits())
151 }
152}
153
154#[derive(Debug, Copy, Clone, PartialEq, EnumString)]
155#[strum(ascii_case_insensitive)]
156pub enum CheckpointMode {
157 Passive { upper_bound_inclusive: Option<u64> },
162 Full,
164 Restart,
166 Truncate { upper_bound_inclusive: Option<u64> },
171}
172
173impl CheckpointMode {
174 pub(crate) fn should_restart_log(&self) -> bool {
175 matches!(
176 self,
177 CheckpointMode::Truncate { .. } | CheckpointMode::Restart
178 )
179 }
180 fn require_all_backfilled(&self) -> bool {
183 !matches!(self, CheckpointMode::Passive { .. })
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189struct WalSnapshot {
190 max_frame: u64,
191 nbackfills: u64,
192 last_checksum: (u32, u32),
193 checkpoint_seq: u32,
194 transaction_count: u64,
195}
196
197impl WalSnapshot {
198 const fn min_frame(self) -> u64 {
200 self.nbackfills + 1
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum ReadGuardKind {
207 None,
208 DbFile,
209 ReadMark(NonZeroUsize),
210}
211
212impl ReadGuardKind {
213 const fn from_lock_index(lock_index: usize) -> Self {
215 match lock_index {
216 NO_LOCK_HELD => Self::None,
217 0 => Self::DbFile,
218 idx => Self::ReadMark(NonZeroUsize::new(idx).expect("idx checked to be non-zero")),
219 }
220 }
221
222 fn lock_index(self) -> usize {
224 match self {
225 Self::None => NO_LOCK_HELD,
226 Self::DbFile => 0,
227 Self::ReadMark(idx) => idx.into(),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234struct WalConnectionState {
235 snapshot: WalSnapshot,
236 read_guard: ReadGuardKind,
237}
238
239impl WalConnectionState {
240 const fn new(snapshot: WalSnapshot, read_guard: ReadGuardKind) -> Self {
242 Self {
243 snapshot,
244 read_guard,
245 }
246 }
247
248 const fn with_snapshot(self, snapshot: WalSnapshot) -> Self {
250 Self {
251 snapshot,
252 read_guard: self.read_guard,
253 }
254 }
255}
256
257#[repr(transparent)]
258#[derive(Debug, Default)]
259pub struct TursoRwLock(AtomicU64);
277
278pub const READMARK_NOT_USED: u32 = 0xffffffff;
279const NO_LOCK_HELD: usize = usize::MAX;
280
281impl TursoRwLock {
282 const WRITER: u64 = 0b1;
284
285 const READER_INC: u64 = 0b10;
287
288 const READER_SHIFT: u32 = 1;
290
291 const READER_COUNT_MASK: u64 = 0x7fff_ffffu64 << Self::READER_SHIFT;
293
294 const VALUE_SHIFT: u32 = 32;
296
297 const VALUE_MASK: u64 = 0xffff_ffffu64 << Self::VALUE_SHIFT;
299
300 #[inline]
301 pub const fn new() -> Self {
302 Self(AtomicU64::new(0))
303 }
304
305 const fn has_writer(val: u64) -> bool {
306 val & Self::WRITER != 0
307 }
308
309 const fn has_readers(val: u64) -> bool {
310 val & Self::READER_COUNT_MASK != 0
311 }
312
313 #[inline]
314 pub fn read(&self) -> bool {
316 let mut count = 0;
317 while count < 1_000_000 {
320 let cur = self.0.load(Ordering::Acquire);
321 if Self::has_writer(cur) {
323 return false;
324 }
325 let desired = cur.wrapping_add(Self::READER_INC);
328 let res = self
331 .0
332 .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed);
333 if res.is_err() {
334 count += 1;
335 crate::thread::spin_loop();
336 continue;
337 }
338 return true;
339 }
340 false
342 }
343
344 #[inline]
346 pub fn write(&self) -> bool {
347 let cur = self.0.load(Ordering::Acquire);
348 if Self::has_writer(cur) || Self::has_readers(cur) {
350 return false;
351 }
352 let desired = cur | Self::WRITER;
353 self.0 .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed)
355 .is_ok()
356 }
357
358 #[inline]
362 pub fn upgrade(&self) -> bool {
363 let cur = self.0.load(Ordering::Acquire);
364 if (cur & !Self::VALUE_MASK) != Self::READER_INC {
366 return false;
367 }
368 let desired = (cur & Self::VALUE_MASK) | Self::WRITER;
370 self.0
371 .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed)
372 .is_ok()
373 }
374
375 #[inline]
378 pub fn downgrade(&self) {
379 let cur = self.0.load(Ordering::Acquire);
380 turso_debug_assert!(Self::has_writer(cur));
381 let desired = (cur & Self::VALUE_MASK) | Self::READER_INC;
383 #[cfg(debug_assertions)]
384 {
385 let prev = self
386 .0
387 .compare_exchange(cur, desired, Ordering::AcqRel, Ordering::Relaxed);
388 turso_debug_assert!(
389 prev.is_ok(),
390 "downgrade CAS failed — lock was mutated concurrently"
391 );
392 }
393 #[cfg(not(debug_assertions))]
394 {
395 self.0.store(desired, Ordering::Release);
396 }
397 }
398
399 #[inline]
400 pub fn unlock(&self) {
404 let cur = self.0.load(Ordering::Acquire);
405 if (cur & Self::WRITER) != 0 {
406 let cur = self.0.fetch_and(!Self::WRITER, Ordering::Release);
409 turso_assert!(!Self::has_readers(cur), "write lock was held with readers");
410 } else {
411 turso_assert!(
412 Self::has_readers(cur),
413 "unlock called with no readers or writers"
414 );
415 self.0.fetch_sub(Self::READER_INC, Ordering::Release);
416 }
417 }
418
419 #[inline]
420 pub fn get_value(&self) -> u32 {
422 (self.0.load(Ordering::Acquire) >> Self::VALUE_SHIFT) as u32
423 }
424
425 #[inline]
426 pub fn held_value(&self) -> Option<u32> {
430 let cur = self.0.load(Ordering::Acquire);
431 if Self::has_readers(cur) {
432 Some((cur >> Self::VALUE_SHIFT) as u32)
433 } else {
434 None
435 }
436 }
437
438 #[inline]
439 pub fn set_value_exclusive(&self, v: u32) {
441 let cur = self.0.load(Ordering::Acquire);
443 turso_assert!(Self::has_writer(cur), "must hold exclusive lock");
444 let desired = (cur & !Self::VALUE_MASK) | ((v as u64) << Self::VALUE_SHIFT);
445 self.0.store(desired, Ordering::Release);
446 }
447}
448
449pub struct PreparedFrames {
452 pub offset: u64,
454 pub bufs: Vec<Arc<Buffer>>,
456 pub metadata: Vec<(PageRef, u64, (u32, u32))>,
458 pub final_checksum: (u32, u32),
460 pub final_max_frame: u64,
462 pub epoch: u32,
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468struct WalCommitState {
469 max_frame: u64,
470 last_checksum: (u32, u32),
471 transaction_count: u64,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475enum CoordinationCheckpointGuardKind {
476 Read0,
477 Writer,
478}
479
480trait WalCoordination: Debug + Send + Sync {
482 fn load_snapshot(&self) -> WalSnapshot;
484
485 fn ensure_local_frame_cache_covers(
487 &self,
488 _io: &Arc<dyn IO>,
489 _snapshot: WalSnapshot,
490 ) -> Result<()> {
491 Ok(())
492 }
493
494 fn publish_commit(&self, commit: WalCommitState);
496
497 fn publish_backfill(&self, max_frame: u64);
499
500 fn install_durable_backfill_proof(
503 &self,
504 max_frame: u64,
505 db_size_pages: u32,
506 db_header_crc32c: u32,
507 sync_type: FileSyncType,
508 ) -> Result<Option<Completion>>;
509
510 fn find_frame(
512 &self,
513 page_id: u64,
514 min_frame: u64,
515 max_frame: u64,
516 frame_watermark: Option<u64>,
517 ) -> Option<u64>;
518
519 fn iter_latest_frames(&self, min_frame: u64, max_frame: u64) -> Vec<(u64, u64)>;
521
522 fn checkpoint_epoch(&self) -> u32;
524
525 fn bump_checkpoint_epoch(&self) -> u32;
527
528 fn try_begin_read_tx(&self, snapshot: WalSnapshot) -> Option<ReadGuardKind>;
530
531 fn end_read_tx(&self, guard: ReadGuardKind);
533
534 fn try_begin_write_tx(&self) -> bool;
536
537 fn end_write_tx(&self);
539
540 fn acquire_checkpoint_guard(
542 &self,
543 mode: CheckpointMode,
544 ) -> Result<CoordinationCheckpointGuardKind>;
545
546 fn acquire_vacuum_checkpoint_guard_from_held_lock(
553 &self,
554 ) -> Result<CoordinationCheckpointGuardKind>;
555
556 fn release_checkpoint_guard(&self, guard: CoordinationCheckpointGuardKind);
558
559 fn determine_max_safe_checkpoint_frame(&self, max_frame: u64) -> u64;
561
562 fn min_pinned_read_frame(&self) -> Option<u64>;
564
565 fn begin_restart(&self, io: &dyn IO) -> Result<WalSnapshot>;
567
568 fn end_restart(&self);
570
571 fn try_restart_log_for_write(&self, io: &dyn IO) -> Result<Option<WalSnapshot>>;
573
574 fn prepare_truncate(&self) -> Result<Arc<dyn File>>;
576
577 fn wal_header(&self) -> WalHeader;
579
580 fn wal_file(&self) -> Result<Arc<dyn File>>;
582
583 fn shared_wal_state(&self) -> Arc<RwLock<WalFileShared>>;
585
586 fn wal_is_initialized(&self) -> bool;
588
589 fn prepare_wal_header(&self, io: &dyn IO, page_size: PageSize) -> Option<WalHeader>;
591
592 fn mark_initialized(&self);
594
595 fn cache_frame(&self, page_id: u64, frame_id: u64);
597
598 fn rollback_cache(&self, max_frame: u64);
600
601 fn should_checkpoint_on_close(&self) -> bool;
603
604 #[cfg(clt_turso_tests)]
605 fn backend_name(&self) -> &'static str;
606
607 #[cfg(clt_turso_tests)]
608 fn shared_ptr(&self) -> usize;
609
610 #[cfg(clt_turso_tests)]
611 fn open_mode_name(&self) -> Option<&'static str> {
612 None
613 }
614}
615
616#[aristo::intent("The WAL subsystem maintains LSN monotonicity, frame commitment ordering, recovery idempotency, checkpoint safety, and group commit atomicity.", id = "wal_protocol_correctness", verify = "neural")]
618pub trait Wal: Debug + Send + Sync {
619 fn begin_read_tx(&self) -> Result<bool>;
622 fn mvcc_refresh_if_db_changed(&self) -> bool;
624
625 fn begin_write_tx(&self, allowed_auto_actions: WalAutoActions) -> Result<()>;
633
634 fn end_read_tx(&self);
636
637 fn end_write_tx(&self);
639
640 fn holds_read_lock(&self) -> bool;
642
643 fn holds_write_lock(&self) -> bool;
645
646 fn should_checkpoint_on_close(&self) -> bool;
648
649 fn find_frame(&self, page_id: u64, frame_watermark: Option<u64>) -> Result<Option<u64>>;
654
655 fn read_frame(
657 &self,
658 frame_id: u64,
659 page: PageRef,
660 buffer_pool: Arc<BufferPool>,
661 ) -> Result<Completion>;
662
663 fn read_frames_batch(
673 &self,
674 start_frame: u64,
675 pages: &[PageRef],
676 buffer_pool: Arc<BufferPool>,
677 scratch_buf: Option<Arc<Buffer>>,
678 ) -> Result<Completion>;
679
680 fn read_frame_raw(&self, frame_id: u64, frame: &mut [u8]) -> Result<Completion>;
682
683 fn write_frame_raw(
686 &self,
687 buffer_pool: Arc<BufferPool>,
688 frame_id: u64,
689 page_id: u64,
690 db_size: u64,
691 page: &[u8],
692 sync_type: FileSyncType,
693 ) -> Result<()>;
694
695 fn prepare_wal_start(&self, page_sz: PageSize) -> Result<Option<Completion>>;
698
699 fn prepare_wal_finish(&self, sync_type: FileSyncType) -> Result<Completion>;
700
701 fn prepare_frames(
703 &self,
704 pages: &[PageRef],
705 page_sz: PageSize,
706 db_size_on_commit: Option<u32>,
707 prev: Option<&PreparedFrames>,
708 ) -> Result<PreparedFrames>;
709
710 fn commit_prepared_frames(&self, prepared: &[PreparedFrames]);
713
714 fn finalize_committed_pages(&self, prepared: &[PreparedFrames]);
716
717 fn wal_file(&self) -> Result<Arc<dyn File>>;
719
720 fn append_frames_vectored(&self, pages: Vec<PageRef>, page_sz: PageSize) -> Result<Completion>;
725
726 fn finish_append_frames_commit(&self) -> Result<()>;
729
730 fn should_checkpoint(&self) -> bool;
731 fn checkpoint(&self, pager: &Pager, mode: CheckpointMode)
732 -> Result<IOResult<CheckpointResult>>;
733 fn install_durable_backfill_proof(
734 &self,
735 max_frame: u64,
736 db_size_pages: u32,
737 db_header_crc32c: u32,
738 sync_type: FileSyncType,
739 ) -> Result<Option<Completion>>;
740 fn publish_backfill(&self, max_frame: u64);
741 fn sync(&self, sync_type: FileSyncType) -> Result<Completion>;
742 fn is_syncing(&self) -> bool;
743 fn is_dirty(&self) -> bool;
748 fn get_max_frame_in_wal(&self) -> u64;
749 fn get_checkpoint_seq(&self) -> u32;
750 fn get_max_frame(&self) -> u64;
751 fn connection_wal_pos(&self) -> (u32, u64);
756 fn min_pinned_read_frame(&self) -> Option<u64>;
763 fn get_min_frame(&self) -> u64;
764 fn backfill_frame(&self) -> u64;
768 fn rollback(&self, rollback_to: Option<RollbackTo>);
769 fn abort_checkpoint(&self);
770 fn get_last_checksum(&self) -> (u32, u32);
771
772 fn changed_pages_after(&self, frame_watermark: u64) -> Result<Vec<u32>>;
774
775 fn set_io_context(&self, ctx: IOContext);
776
777 fn update_max_frame(&self);
781
782 fn truncate_wal(
786 &self,
787 result: &mut CheckpointResult,
788 sync_type: FileSyncType,
789 ) -> Result<IOResult<()>>;
790
791 fn try_begin_vacuum_checkpoint_lock(&self) -> Result<()>;
795
796 fn release_vacuum_checkpoint_lock(&self);
799
800 fn begin_vacuum_blocking_tx(&self) -> Result<()>;
813
814 fn vacuum_checkpoint_with_held_lock(&self, pager: &Pager)
819 -> Result<IOResult<CheckpointResult>>;
820
821 fn release_vacuum_lock(&self);
825
826 #[cfg(any(clt_turso_tests, debug_assertions))]
827 fn as_any(&self) -> &dyn std::any::Any;
828}
829
830#[derive(Debug)]
831struct InProcessWalCoordination {
832 shared: Arc<RwLock<WalFileShared>>,
833}
834
835impl InProcessWalCoordination {
836 fn new(shared: Arc<RwLock<WalFileShared>>) -> Self {
838 Self { shared }
839 }
840
841 fn try_read_mark_shared(&self, slot: usize) -> bool {
842 self.shared.read().runtime.read_locks[slot].read()
843 }
844
845 fn try_read_mark_exclusive(&self, slot: usize) -> bool {
846 self.shared.read().runtime.read_locks[slot].write()
847 }
848
849 fn unlock_read_mark(&self, slot: usize) {
850 self.shared.read().runtime.read_locks[slot].unlock();
851 }
852
853 fn read_mark_value(&self, slot: usize) -> u32 {
854 self.shared.read().runtime.read_locks[slot].get_value()
855 }
856
857 fn min_pinned_read_frame_inner(&self) -> Option<u64> {
860 let shared = self.shared.read();
861 let mut min: Option<u64> = None;
862 for slot in 1..5 {
863 if let Some(v) = shared.runtime.read_locks[slot].held_value() {
864 if v != READMARK_NOT_USED {
865 let f = v as u64;
866 min = Some(min.map_or(f, |m: u64| m.min(f)));
867 }
868 }
869 }
870 min
871 }
872
873 fn set_read_mark_value_exclusive(&self, slot: usize, value: u32) {
874 self.shared.read().runtime.read_locks[slot].set_value_exclusive(value);
875 }
876
877 fn try_upgrade_read_mark(&self, slot: usize) -> bool {
878 self.shared.read().runtime.read_locks[slot].upgrade()
879 }
880
881 fn downgrade_read_mark(&self, slot: usize) {
882 self.shared.read().runtime.read_locks[slot].downgrade();
883 }
884
885 fn try_write_lock(&self) -> bool {
886 self.shared.read().runtime.write_lock.write()
887 }
888
889 fn unlock_write_lock(&self) {
890 self.shared.read().runtime.write_lock.unlock();
891 }
892
893 fn try_checkpoint_lock(&self) -> bool {
894 self.shared.read().runtime.checkpoint_lock.write()
895 }
896
897 fn unlock_checkpoint_lock(&self) {
898 self.shared.read().runtime.checkpoint_lock.unlock();
899 }
900}
901
902impl WalCoordination for InProcessWalCoordination {
903 fn load_snapshot(&self) -> WalSnapshot {
904 let shared = self.shared.read();
905 let checkpoint_seq = shared.metadata.wal_header.lock().checkpoint_seq;
906 WalSnapshot {
907 max_frame: shared.metadata.max_frame.load(Ordering::Acquire),
908 nbackfills: shared.metadata.nbackfills.load(Ordering::Acquire),
909 last_checksum: shared.metadata.last_checksum,
910 checkpoint_seq,
911 transaction_count: shared.metadata.transaction_count.load(Ordering::Acquire),
912 }
913 }
914
915 fn publish_commit(&self, commit: WalCommitState) {
916 let mut shared = self.shared.write();
917 shared
918 .metadata
919 .max_frame
920 .store(commit.max_frame, Ordering::Release);
921 shared.metadata.last_checksum = commit.last_checksum;
922 shared
923 .metadata
924 .transaction_count
925 .store(commit.transaction_count, Ordering::Release);
926 }
927
928 fn publish_backfill(&self, max_frame: u64) {
929 self.shared
930 .write()
931 .metadata
932 .nbackfills
933 .store(max_frame, Ordering::Release);
934 }
935
936 fn install_durable_backfill_proof(
937 &self,
938 _max_frame: u64,
939 _db_size_pages: u32,
940 _db_header_crc32c: u32,
941 _sync_type: FileSyncType,
942 ) -> Result<Option<Completion>> {
943 Ok(None)
944 }
945
946 fn find_frame(
947 &self,
948 page_id: u64,
949 min_frame: u64,
950 max_frame: u64,
951 frame_watermark: Option<u64>,
952 ) -> Option<u64> {
953 let shared = self.shared.read();
954 let frame_cache = shared.runtime.frame_cache.lock();
955 let range = frame_watermark
956 .map(|x| 0..=x)
957 .unwrap_or(min_frame..=max_frame);
958 let result = frame_cache.get(&page_id).and_then(|frames| {
959 frames
960 .iter()
961 .rfind(|&&frame| range.contains(&frame))
962 .copied()
963 });
964 result
965 }
966
967 fn iter_latest_frames(&self, min_frame: u64, max_frame: u64) -> Vec<(u64, u64)> {
968 let shared = self.shared.read();
969 let frame_cache = shared.runtime.frame_cache.lock();
970 let mut list = Vec::with_capacity(frame_cache.len());
971 for (&page_id, frames) in frame_cache.iter() {
972 if let Some(&frame_id) = frames
973 .iter()
974 .rfind(|&&frame| (min_frame..=max_frame).contains(&frame))
975 {
976 list.push((page_id, frame_id));
977 }
978 }
979 list.sort_unstable_by_key(|&(page_id, _)| page_id);
980 list
981 }
982
983 fn checkpoint_epoch(&self) -> u32 {
984 self.shared.read().runtime.epoch.load(Ordering::Acquire)
985 }
986
987 fn bump_checkpoint_epoch(&self) -> u32 {
988 self.shared
989 .read()
990 .runtime
991 .epoch
992 .fetch_add(1, Ordering::Release)
993 }
994
995 fn try_begin_read_tx(&self, snapshot: WalSnapshot) -> Option<ReadGuardKind> {
996 turso_assert!(
997 snapshot.max_frame <= u32::MAX as u64,
998 "max_frame exceeds u32 read mark range"
999 );
1000 if snapshot.max_frame == snapshot.nbackfills {
1001 if !self.try_read_mark_shared(0) {
1002 return None;
1003 }
1004 if self.load_snapshot() != snapshot {
1005 self.unlock_read_mark(0);
1006 return None;
1007 }
1008 return Some(ReadGuardKind::DbFile);
1009 }
1010
1011 let mut best_idx: i64 = -1;
1012 let mut best_mark: u32 = 0;
1013 for idx in 1..5 {
1014 let mark = self.read_mark_value(idx);
1015 if mark != READMARK_NOT_USED && mark <= snapshot.max_frame as u32 && mark > best_mark {
1016 best_mark = mark;
1017 best_idx = idx as i64;
1018 }
1019 }
1020
1021 if best_idx == -1 || (best_mark as u64) < snapshot.max_frame {
1022 for idx in 1..5 {
1023 if !self.try_read_mark_exclusive(idx) {
1024 continue;
1025 }
1026 self.set_read_mark_value_exclusive(idx, snapshot.max_frame as u32);
1027 best_idx = idx as i64;
1028 best_mark = snapshot.max_frame as u32;
1029 self.unlock_read_mark(idx);
1030 break;
1031 }
1032 }
1033
1034 if best_idx == -1 || !self.try_read_mark_shared(best_idx as usize) {
1035 return None;
1036 }
1037
1038 let snapshot_after_lock = self.load_snapshot();
1039 let current_slot_mark = self.read_mark_value(best_idx as usize);
1040 if current_slot_mark != best_mark || snapshot_after_lock != snapshot {
1041 self.unlock_read_mark(best_idx as usize);
1042 return None;
1043 }
1044
1045 Some(ReadGuardKind::ReadMark(
1046 NonZeroUsize::new(best_idx as usize)
1047 .expect("best_idx checked to be non-negative and non-zero"),
1048 ))
1049 }
1050
1051 fn end_read_tx(&self, guard: ReadGuardKind) {
1052 match guard {
1053 ReadGuardKind::None => {}
1054 ReadGuardKind::DbFile => self.unlock_read_mark(0),
1055 ReadGuardKind::ReadMark(slot) => self.unlock_read_mark(slot.into()),
1056 }
1057 }
1058
1059 fn try_begin_write_tx(&self) -> bool {
1060 self.try_write_lock()
1061 }
1062
1063 fn end_write_tx(&self) {
1064 self.unlock_write_lock();
1065 }
1066
1067 fn acquire_checkpoint_guard(
1068 &self,
1069 mode: CheckpointMode,
1070 ) -> Result<CoordinationCheckpointGuardKind> {
1071 if !self.try_checkpoint_lock() {
1072 tracing::trace!("CheckpointGuard::new: checkpoint lock failed, returning Busy");
1073 return Err(LimboError::Busy);
1074 }
1075 match mode {
1076 CheckpointMode::Passive { .. } => {
1077 if !self.try_read_mark_exclusive(0) {
1078 self.unlock_checkpoint_lock();
1079 tracing::trace!("CheckpointGuard: read0 lock failed, returning Busy");
1080 return Err(LimboError::Busy);
1081 }
1082 Ok(CoordinationCheckpointGuardKind::Read0)
1083 }
1084 CheckpointMode::Full => {
1085 if !self.try_read_mark_exclusive(0) {
1086 self.unlock_checkpoint_lock();
1087 tracing::trace!("CheckpointGuard: read0 lock failed (Full), Busy");
1088 return Err(LimboError::Busy);
1089 }
1090 if !self.try_write_lock() {
1091 self.unlock_read_mark(0);
1092 self.unlock_checkpoint_lock();
1093 tracing::trace!("CheckpointGuard: write lock failed (Full), Busy");
1094 return Err(LimboError::Busy);
1095 }
1096 Ok(CoordinationCheckpointGuardKind::Writer)
1097 }
1098 CheckpointMode::Restart | CheckpointMode::Truncate { .. } => {
1099 if !self.try_read_mark_exclusive(0) {
1100 self.unlock_checkpoint_lock();
1101 tracing::trace!("CheckpointGuard: read0 lock failed, returning Busy");
1102 return Err(LimboError::Busy);
1103 }
1104 if !self.try_write_lock() {
1105 self.unlock_checkpoint_lock();
1106 self.unlock_read_mark(0);
1107 tracing::trace!("CheckpointGuard: write lock failed, returning Busy");
1108 return Err(LimboError::Busy);
1109 }
1110 Ok(CoordinationCheckpointGuardKind::Writer)
1111 }
1112 }
1113 }
1114
1115 fn acquire_vacuum_checkpoint_guard_from_held_lock(
1116 &self,
1117 ) -> Result<CoordinationCheckpointGuardKind> {
1118 if !self.try_read_mark_exclusive(0) {
1119 self.unlock_checkpoint_lock();
1120 tracing::trace!("CheckpointGuard: held VACUUM read0 lock failed, returning Busy");
1121 return Err(LimboError::Busy);
1122 }
1123 if !self.try_write_lock() {
1124 self.unlock_read_mark(0);
1125 self.unlock_checkpoint_lock();
1126 tracing::trace!("CheckpointGuard: held VACUUM write lock failed, returning Busy");
1127 return Err(LimboError::Busy);
1128 }
1129 Ok(CoordinationCheckpointGuardKind::Writer)
1130 }
1131
1132 fn release_checkpoint_guard(&self, guard: CoordinationCheckpointGuardKind) {
1133 match guard {
1134 CoordinationCheckpointGuardKind::Writer => {
1135 self.unlock_write_lock();
1136 self.unlock_read_mark(0);
1137 self.unlock_checkpoint_lock();
1138 }
1139 CoordinationCheckpointGuardKind::Read0 => {
1140 self.unlock_read_mark(0);
1141 self.unlock_checkpoint_lock();
1142 }
1143 }
1144 }
1145
1146 fn determine_max_safe_checkpoint_frame(&self, max_frame: u64) -> u64 {
1147 turso_assert!(
1148 max_frame <= u32::MAX as u64,
1149 "max_frame exceeds u32 read mark range"
1150 );
1151 let mut max_safe_frame = max_frame;
1152 for read_lock_idx in 1..5 {
1153 let this_mark = self.read_mark_value(read_lock_idx);
1154 if this_mark < max_safe_frame as u32 {
1155 let busy = !self.try_read_mark_exclusive(read_lock_idx);
1156 if !busy {
1157 let val = if read_lock_idx == 1 {
1158 max_safe_frame as u32
1159 } else {
1160 READMARK_NOT_USED
1161 };
1162 self.set_read_mark_value_exclusive(read_lock_idx, val);
1163 self.unlock_read_mark(read_lock_idx);
1164 } else {
1165 max_safe_frame = this_mark as u64;
1166 }
1167 }
1168 }
1169 max_safe_frame
1170 }
1171
1172 fn min_pinned_read_frame(&self) -> Option<u64> {
1173 self.min_pinned_read_frame_inner()
1174 }
1175
1176 fn begin_restart(&self, io: &dyn IO) -> Result<WalSnapshot> {
1177 for idx in 1..5 {
1178 if !self.try_read_mark_exclusive(idx) {
1179 for j in 1..idx {
1180 self.unlock_read_mark(j);
1181 }
1182 return Err(LimboError::Busy);
1183 }
1184 self.set_read_mark_value_exclusive(idx, READMARK_NOT_USED);
1185 }
1186 let mut shared = self.shared.write();
1187 shared.restart_wal_header(io);
1188 let checkpoint_seq = shared.metadata.wal_header.lock().checkpoint_seq;
1189 Ok(WalSnapshot {
1190 max_frame: shared.metadata.max_frame.load(Ordering::Acquire),
1191 nbackfills: shared.metadata.nbackfills.load(Ordering::Acquire),
1192 last_checksum: shared.metadata.last_checksum,
1193 checkpoint_seq,
1194 transaction_count: shared.metadata.transaction_count.load(Ordering::Acquire),
1195 })
1196 }
1197
1198 fn end_restart(&self) {
1199 for idx in 1..5 {
1200 self.unlock_read_mark(idx);
1201 }
1202 }
1203
1204 fn try_restart_log_for_write(&self, io: &dyn IO) -> Result<Option<WalSnapshot>> {
1205 if !self.try_upgrade_read_mark(0) {
1206 return Ok(None);
1207 }
1208 let result = self.begin_restart(io);
1209 self.downgrade_read_mark(0);
1210 match result {
1211 Ok(snapshot) => {
1212 self.end_restart();
1213 Ok(Some(snapshot))
1214 }
1215 Err(err) => Err(err),
1216 }
1217 }
1218
1219 fn prepare_truncate(&self) -> Result<Arc<dyn File>> {
1220 let shared = self.shared.read();
1221 turso_assert!(
1222 shared.metadata.enabled.load(Ordering::Relaxed),
1223 "WAL must be enabled"
1224 );
1225 shared.metadata.initialized.store(false, Ordering::Release);
1226 shared.runtime.file.as_ref().cloned().ok_or_else(|| {
1227 mark_unlikely();
1228 LimboError::InternalError("WAL file not open".into())
1229 })
1230 }
1231
1232 fn wal_header(&self) -> WalHeader {
1233 *self.shared.read().metadata.wal_header.lock()
1234 }
1235
1236 fn wal_file(&self) -> Result<Arc<dyn File>> {
1237 let shared = self.shared.read();
1238 turso_assert!(
1239 shared.metadata.enabled.load(Ordering::Relaxed),
1240 "WAL must be enabled"
1241 );
1242 shared.runtime.file.as_ref().cloned().ok_or_else(|| {
1243 mark_unlikely();
1244 LimboError::InternalError("WAL file not open".into())
1245 })
1246 }
1247
1248 fn wal_is_initialized(&self) -> bool {
1249 self.shared
1250 .read()
1251 .metadata
1252 .initialized
1253 .load(Ordering::Acquire)
1254 }
1255
1256 fn prepare_wal_header(&self, io: &dyn IO, page_size: PageSize) -> Option<WalHeader> {
1257 let mut shared: crate::sync::RwLockWriteGuard<'_, WalFileShared> = self.shared.write();
1258 if shared.metadata.initialized.load(Ordering::Acquire) {
1259 return None;
1260 }
1261
1262 let (header, checksum) = {
1263 let mut hdr = shared.metadata.wal_header.lock();
1264 hdr.magic = if cfg!(target_endian = "big") {
1265 WAL_MAGIC_BE
1266 } else {
1267 WAL_MAGIC_LE
1268 };
1269 if hdr.page_size == 0 {
1270 hdr.page_size = page_size.get();
1271 }
1272 if hdr.salt_1 == 0 && hdr.salt_2 == 0 {
1273 hdr.salt_1 = io.generate_random_number() as u32;
1274 hdr.salt_2 = io.generate_random_number() as u32;
1275 }
1276
1277 let prefix = &hdr.as_bytes()[..WAL_HEADER_SIZE - 8];
1278 let use_native = (hdr.magic & 1) != 0;
1279 let (c1, c2) = checksum_wal(prefix, &hdr, (0, 0), use_native);
1280 hdr.checksum_1 = c1;
1281 hdr.checksum_2 = c2;
1282 (*hdr, (c1, c2))
1283 };
1284 shared.metadata.last_checksum = checksum;
1285 Some(header)
1286 }
1287
1288 fn mark_initialized(&self) {
1289 self.shared
1290 .read()
1291 .metadata
1292 .initialized
1293 .store(true, Ordering::Release);
1294 }
1295
1296 fn cache_frame(&self, page_id: u64, frame_id: u64) {
1297 let shared = self.shared.read();
1298 let mut frame_cache = shared.runtime.frame_cache.lock();
1299 let high_water = shared
1311 .runtime
1312 .frame_cache_high_water
1313 .load(Ordering::Acquire);
1314 if frame_id <= high_water {
1315 frame_cache.retain(|_page_id, frames| {
1316 while frames.last().is_some_and(|&frame| frame >= frame_id) {
1317 frames.pop();
1318 }
1319 !frames.is_empty()
1320 });
1321 }
1322 match frame_cache.get_mut(&page_id) {
1323 Some(frames) => {
1324 frames.push(frame_id);
1325 }
1326 None => {
1327 frame_cache.insert(page_id, vec![frame_id]);
1328 }
1329 }
1330 shared
1331 .runtime
1332 .frame_cache_high_water
1333 .store(frame_id, Ordering::Release);
1334 }
1335
1336 fn rollback_cache(&self, max_frame: u64) {
1337 let shared = self.shared.read();
1338 let mut frame_cache = shared.runtime.frame_cache.lock();
1339 frame_cache.retain(|_page_id, frames| {
1340 while frames.last().is_some_and(|&frame| frame > max_frame) {
1341 frames.pop();
1342 }
1343 !frames.is_empty()
1344 });
1345 if shared
1348 .runtime
1349 .frame_cache_high_water
1350 .load(Ordering::Acquire)
1351 > max_frame
1352 {
1353 shared
1354 .runtime
1355 .frame_cache_high_water
1356 .store(max_frame, Ordering::Release);
1357 }
1358 }
1359
1360 fn should_checkpoint_on_close(&self) -> bool {
1361 true
1362 }
1363
1364 #[cfg(clt_turso_tests)]
1365 fn backend_name(&self) -> &'static str {
1366 "in_process"
1367 }
1368
1369 #[cfg(clt_turso_tests)]
1370 fn shared_ptr(&self) -> usize {
1371 Arc::as_ptr(&self.shared) as usize
1372 }
1373
1374 fn shared_wal_state(&self) -> Arc<RwLock<WalFileShared>> {
1375 self.shared.clone()
1376 }
1377}
1378
1379#[cfg(host_shared_wal)]
1391#[derive(Debug)]
1392struct ShmWalCoordination {
1393 shared: Arc<RwLock<WalFileShared>>,
1394 fallback: InProcessWalCoordination,
1395 authority: Arc<MappedSharedWalCoordination>,
1396 active_reader: Mutex<Option<SharedReaderSlot>>,
1398 owner: SharedOwnerRecord,
1401}
1402
1403#[cfg(host_shared_wal)]
1404impl ShmWalCoordination {
1405 fn overflow_fallback_covers(
1406 &self,
1407 snapshot: SharedWalCoordinationHeader,
1408 max_frame: u64,
1409 ) -> bool {
1410 self.shared
1411 .read()
1412 .runtime
1413 .overflow_fallback_coverage
1414 .lock()
1415 .covers(snapshot, max_frame)
1416 }
1417
1418 fn clear_overflow_fallback_coverage(&self) {
1419 self.shared
1420 .read()
1421 .runtime
1422 .overflow_fallback_coverage
1423 .lock()
1424 .clear();
1425 }
1426
1427 fn local_authority_snapshot_from_shared(
1428 shared: &WalFileShared,
1429 authority_snapshot: SharedWalCoordinationHeader,
1430 ) -> SharedWalCoordinationHeader {
1431 let header = shared.metadata.wal_header.lock();
1432 SharedWalCoordinationHeader {
1433 max_frame: shared.metadata.max_frame.load(Ordering::Acquire),
1434 nbackfills: shared.metadata.nbackfills.load(Ordering::Acquire),
1435 transaction_count: shared.metadata.transaction_count.load(Ordering::Acquire),
1436 visibility_generation: authority_snapshot.visibility_generation,
1437 checkpoint_seq: header.checkpoint_seq,
1438 checkpoint_epoch: shared.runtime.epoch.load(Ordering::Acquire),
1439 page_size: header.page_size,
1440 salt_1: header.salt_1,
1441 salt_2: header.salt_2,
1442 checksum_1: shared.metadata.last_checksum.0,
1443 checksum_2: shared.metadata.last_checksum.1,
1444 reader_slot_count: authority_snapshot.reader_slot_count,
1445 }
1446 }
1447
1448 fn new(
1449 shared: Arc<RwLock<WalFileShared>>,
1450 authority: Arc<MappedSharedWalCoordination>,
1451 ) -> Self {
1452 let fallback = InProcessWalCoordination::new(shared.clone());
1453 let coordination = Self {
1454 shared,
1455 fallback,
1456 owner: authority.owner_record(),
1457 authority,
1458 active_reader: Mutex::new(None),
1459 };
1460 coordination.seed_or_sync_authority();
1461 coordination
1462 }
1463
1464 fn authority_is_uninitialized(snapshot: SharedWalCoordinationHeader) -> bool {
1465 snapshot.max_frame == 0
1466 && snapshot.nbackfills == 0
1467 && snapshot.transaction_count == 0
1468 && snapshot.visibility_generation == 0
1469 && snapshot.checkpoint_seq == 0
1470 && snapshot.checkpoint_epoch == 0
1471 && snapshot.page_size == 0
1472 && snapshot.salt_1 == 0
1473 && snapshot.salt_2 == 0
1474 && snapshot.checksum_1 == 0
1475 && snapshot.checksum_2 == 0
1476 }
1477
1478 fn local_authority_snapshot(&self) -> SharedWalCoordinationHeader {
1479 let authority_snapshot = self.authority.snapshot();
1480 let shared = self.shared.read();
1481 Self::local_authority_snapshot_from_shared(&shared, authority_snapshot)
1482 }
1483
1484 fn install_local_snapshot(
1485 shared: &mut WalFileShared,
1486 snapshot: SharedWalCoordinationHeader,
1487 install_header: bool,
1488 ) {
1489 shared
1490 .metadata
1491 .max_frame
1492 .store(snapshot.max_frame, Ordering::Release);
1493 shared
1494 .metadata
1495 .nbackfills
1496 .store(snapshot.nbackfills, Ordering::Release);
1497 shared.metadata.last_checksum = (snapshot.checksum_1, snapshot.checksum_2);
1498 shared
1499 .metadata
1500 .transaction_count
1501 .store(snapshot.transaction_count, Ordering::Release);
1502 shared
1503 .runtime
1504 .epoch
1505 .store(snapshot.checkpoint_epoch, Ordering::Release);
1506 if install_header {
1507 let mut header = shared.metadata.wal_header.lock();
1508 header.checkpoint_seq = snapshot.checkpoint_seq;
1509 header.page_size = snapshot.page_size;
1510 header.salt_1 = snapshot.salt_1;
1511 header.salt_2 = snapshot.salt_2;
1512 header.checksum_1 = snapshot.checksum_1;
1513 header.checksum_2 = snapshot.checksum_2;
1514 }
1515 }
1516
1517 fn sync_local_from_authority(&self, snapshot: SharedWalCoordinationHeader) {
1518 let mut shared = self.shared.write();
1519 Self::install_local_snapshot(&mut shared, snapshot, snapshot.page_size != 0);
1520 }
1521
1522 fn sync_authority_from_local(&self) {
1523 self.authority
1524 .install_snapshot(self.local_authority_snapshot());
1525 }
1526
1527 fn sync_local_to_zero_frame_authority(&self, snapshot: SharedWalCoordinationHeader) {
1528 let mut shared = self.shared.write();
1529 Self::install_local_snapshot(&mut shared, snapshot, true);
1530 shared.metadata.initialized.store(false, Ordering::Release);
1531 shared.runtime.frame_cache.lock().clear();
1532 shared
1533 .runtime
1534 .frame_cache_high_water
1535 .store(0, Ordering::Release);
1536 shared.runtime.overflow_fallback_coverage.lock().clear();
1537 }
1538
1539 fn sync_authority_frames_from_local(&self) {
1540 let entries = {
1541 let shared = self.shared.read();
1542 let frame_cache = shared.runtime.frame_cache.lock();
1543 let mut entries = Vec::new();
1544 for (&page_id, frames) in frame_cache.iter() {
1545 for &frame_id in frames {
1546 entries.push((frame_id, page_id));
1547 }
1548 }
1549 entries
1550 };
1551 let mut entries = entries;
1552 entries.sort_unstable();
1553 for (frame_id, page_id) in entries {
1554 self.authority.record_frame(page_id, frame_id);
1555 }
1556 }
1557
1558 fn repair_or_reseed_authority_from_local_disk_scan(
1559 &self,
1560 mut authority_snapshot: SharedWalCoordinationHeader,
1561 ) {
1562 self.authority.repair_transient_state_for_exclusive_open();
1563 if authority_snapshot.nbackfills != 0 {
1564 authority_snapshot.nbackfills = 0;
1569 self.authority.install_snapshot(authority_snapshot);
1570 }
1571 let local_snapshot = self.local_authority_snapshot();
1572 if Self::local_scan_predates_zero_frame_authority(authority_snapshot, local_snapshot) {
1573 self.sync_local_to_zero_frame_authority(authority_snapshot);
1574 return;
1575 }
1576 if Self::local_scan_cannot_disprove_zero_frame_authority(authority_snapshot, local_snapshot)
1577 {
1578 self.sync_local_from_authority(authority_snapshot);
1579 return;
1580 }
1581 if Self::local_scan_cannot_disprove_positive_authority(authority_snapshot, local_snapshot) {
1582 self.sync_local_from_authority(authority_snapshot);
1583 return;
1584 }
1585 if Self::authority_matches_local_wal_scan(authority_snapshot, local_snapshot) {
1586 self.sync_local_from_authority(authority_snapshot);
1587 if self.authority.frame_index_overflowed()
1594 || (self.authority.open_mode() == SharedWalCoordinationOpenMode::Exclusive
1595 && !self.authority_frame_index_matches_local_wal_scan(local_snapshot.max_frame))
1596 {
1597 self.authority
1598 .discard_durable_frame_index_for_exclusive_rebuild();
1599 self.sync_authority_frames_from_local();
1600 }
1601 return;
1602 }
1603 if Self::authority_is_same_or_newer_generation(authority_snapshot, local_snapshot) {
1615 self.sync_local_from_authority(authority_snapshot);
1616 if authority_snapshot.checkpoint_seq == local_snapshot.checkpoint_seq
1617 && self.authority.frame_index_overflowed()
1618 {
1619 self.authority
1620 .discard_durable_frame_index_for_exclusive_rebuild();
1621 self.sync_authority_frames_from_local();
1622 }
1623 return;
1624 }
1625
1626 self.authority
1627 .discard_durable_frame_index_for_exclusive_rebuild();
1628 self.sync_authority_from_local();
1629 self.sync_authority_frames_from_local();
1630 }
1631
1632 fn local_scan_cannot_disprove_zero_frame_authority(
1633 authority_snapshot: SharedWalCoordinationHeader,
1634 local_snapshot: SharedWalCoordinationHeader,
1635 ) -> bool {
1636 !Self::authority_is_uninitialized(authority_snapshot)
1637 && authority_snapshot.max_frame == 0
1638 && local_snapshot.max_frame == 0
1639 }
1640
1641 fn local_scan_predates_zero_frame_authority(
1642 authority_snapshot: SharedWalCoordinationHeader,
1643 local_snapshot: SharedWalCoordinationHeader,
1644 ) -> bool {
1645 !Self::authority_is_uninitialized(authority_snapshot)
1646 && authority_snapshot.max_frame == 0
1647 && local_snapshot.max_frame > 0
1648 && local_snapshot.checkpoint_seq < authority_snapshot.checkpoint_seq
1649 }
1650
1651 fn local_scan_cannot_disprove_positive_authority(
1652 authority_snapshot: SharedWalCoordinationHeader,
1653 local_snapshot: SharedWalCoordinationHeader,
1654 ) -> bool {
1655 authority_snapshot.max_frame > 0
1656 && local_snapshot.max_frame == 0
1657 && local_snapshot.checkpoint_seq == authority_snapshot.checkpoint_seq
1658 && local_snapshot.page_size == authority_snapshot.page_size
1659 && local_snapshot.salt_1 == authority_snapshot.salt_1
1660 && local_snapshot.salt_2 == authority_snapshot.salt_2
1661 }
1662
1663 fn authority_is_same_or_newer_generation(
1673 authority_snapshot: SharedWalCoordinationHeader,
1674 local_snapshot: SharedWalCoordinationHeader,
1675 ) -> bool {
1676 if Self::authority_is_uninitialized(authority_snapshot) {
1677 return false;
1678 }
1679 if authority_snapshot.checkpoint_seq > local_snapshot.checkpoint_seq {
1681 return true;
1682 }
1683 authority_snapshot.checkpoint_seq == local_snapshot.checkpoint_seq
1688 && authority_snapshot.salt_1 == local_snapshot.salt_1
1689 && authority_snapshot.salt_2 == local_snapshot.salt_2
1690 && authority_snapshot.max_frame >= local_snapshot.max_frame
1691 }
1692
1693 fn authority_matches_local_wal_scan(
1694 authority_snapshot: SharedWalCoordinationHeader,
1695 local_snapshot: SharedWalCoordinationHeader,
1696 ) -> bool {
1697 authority_snapshot.max_frame == local_snapshot.max_frame
1698 && authority_snapshot.checkpoint_seq == local_snapshot.checkpoint_seq
1699 && authority_snapshot.page_size == local_snapshot.page_size
1700 && authority_snapshot.salt_1 == local_snapshot.salt_1
1701 && authority_snapshot.salt_2 == local_snapshot.salt_2
1702 && authority_snapshot.checksum_1 == local_snapshot.checksum_1
1703 && authority_snapshot.checksum_2 == local_snapshot.checksum_2
1704 }
1705
1706 fn authority_frame_index_matches_local_wal_scan(&self, max_frame: u64) -> bool {
1707 self.authority.iter_latest_frames(0, max_frame)
1708 == self.fallback.iter_latest_frames(0, max_frame)
1709 }
1710
1711 fn local_zero_frame_generation_is_initialized(
1712 &self,
1713 authority_snapshot: SharedWalCoordinationHeader,
1714 ) -> bool {
1715 let shared = self.shared.read();
1716 if !shared.metadata.initialized.load(Ordering::Acquire) {
1717 return false;
1718 }
1719 Self::local_zero_frame_generation_matches_authority_snapshot(authority_snapshot, &shared)
1720 }
1721
1722 fn local_zero_frame_generation_matches_authority_snapshot(
1723 authority_snapshot: SharedWalCoordinationHeader,
1724 shared: &WalFileShared,
1725 ) -> bool {
1726 let header = shared.metadata.wal_header.lock();
1727 header.checkpoint_seq == authority_snapshot.checkpoint_seq
1728 && header.page_size == authority_snapshot.page_size
1729 && header.salt_1 == authority_snapshot.salt_1
1730 && header.salt_2 == authority_snapshot.salt_2
1731 }
1732
1733 fn authority_needs_local_header_seed(snapshot: SharedWalCoordinationHeader) -> bool {
1734 Self::authority_is_uninitialized(snapshot) || snapshot.page_size == 0
1735 }
1736
1737 fn seed_or_sync_authority(&self) {
1758 let reconciliation = self.shared.read().runtime.authority_reconciliation.clone();
1763 let _reconciliation = reconciliation.lock();
1764 let snapshot = self.authority.snapshot();
1765 let local_wal_view_loaded_from_disk = self
1766 .shared
1767 .read()
1768 .metadata
1769 .loaded_from_disk_scan
1770 .swap(false, Ordering::AcqRel);
1771 let recovery_guard =
1772 if Self::authority_is_uninitialized(snapshot) || local_wal_view_loaded_from_disk {
1773 self.authority.try_recovery_guard()
1774 } else {
1775 None
1776 };
1777 if let Some(_guard) = recovery_guard {
1778 let snapshot = self.authority.snapshot();
1781 if Self::authority_is_uninitialized(snapshot) {
1782 self.sync_authority_from_local();
1783 self.sync_authority_frames_from_local();
1784 } else {
1785 self.repair_or_reseed_authority_from_local_disk_scan(snapshot);
1786 }
1787 } else {
1788 let snapshot = self.authority.snapshot();
1789 let needs_zero_frame_header_rewrite = snapshot.max_frame == 0 && {
1790 let shared = self.shared.read();
1791 !shared.metadata.initialized.load(Ordering::Acquire)
1792 || !Self::local_zero_frame_generation_matches_authority_snapshot(
1793 snapshot, &shared,
1794 )
1795 };
1796 self.sync_local_from_authority(snapshot);
1797 if needs_zero_frame_header_rewrite {
1798 self.shared
1799 .read()
1800 .metadata
1801 .initialized
1802 .store(false, Ordering::Release);
1803 }
1804 }
1805 }
1806
1807 fn restart_snapshot_from_authority(
1808 &self,
1809 snapshot: SharedWalCoordinationHeader,
1810 io: &dyn IO,
1811 ) -> WalSnapshot {
1812 let checkpoint_seq = snapshot.checkpoint_seq.wrapping_add(1);
1813 let salt_1 = snapshot.salt_1.wrapping_add(1);
1814 let salt_2 = io.generate_random_number() as u32;
1815 let restarted = SharedWalCoordinationHeader {
1816 max_frame: 0,
1817 nbackfills: 0,
1818 transaction_count: snapshot.transaction_count,
1819 visibility_generation: snapshot.visibility_generation,
1820 checkpoint_seq,
1821 checkpoint_epoch: snapshot.checkpoint_epoch,
1822 page_size: snapshot.page_size,
1823 salt_1,
1824 salt_2,
1825 checksum_1: snapshot.checksum_1,
1826 checksum_2: snapshot.checksum_2,
1827 reader_slot_count: snapshot.reader_slot_count,
1828 };
1829
1830 {
1831 let mut shared = self.shared.write();
1832 Self::install_local_snapshot(&mut shared, restarted, true);
1833 shared.metadata.initialized.store(false, Ordering::Release);
1834 shared.runtime.frame_cache.lock().clear();
1835 shared
1836 .runtime
1837 .frame_cache_high_water
1838 .store(0, Ordering::Release);
1839 shared.runtime.overflow_fallback_coverage.lock().clear();
1840 shared.runtime.read_locks[0].set_value_exclusive(0);
1841 shared.runtime.read_locks[1].set_value_exclusive(0);
1842 for lock in &shared.runtime.read_locks[2..] {
1843 lock.set_value_exclusive(READMARK_NOT_USED);
1844 }
1845 }
1846
1847 self.authority.rollback_frames(0);
1848 self.authority.install_snapshot(restarted);
1849
1850 WalSnapshot {
1851 max_frame: restarted.max_frame,
1852 nbackfills: restarted.nbackfills,
1853 last_checksum: (restarted.checksum_1, restarted.checksum_2),
1854 checkpoint_seq: restarted.checkpoint_seq,
1855 transaction_count: restarted.transaction_count,
1856 }
1857 }
1858
1859 fn ensure_local_frame_cache_covers_snapshot(
1860 &self,
1861 io: &Arc<dyn IO>,
1862 required_snapshot: WalSnapshot,
1863 ) -> Result<()> {
1864 if required_snapshot.max_frame == 0 || !self.authority.frame_index_overflowed() {
1865 return Ok(());
1866 }
1867
1868 let authority_snapshot = self.authority.snapshot();
1869 if authority_snapshot.checkpoint_seq != required_snapshot.checkpoint_seq {
1870 return Err(LimboError::Busy);
1871 }
1872 if self.overflow_fallback_covers(authority_snapshot, required_snapshot.max_frame) {
1873 return Ok(());
1874 }
1875
1876 let _ = io;
1877 tracing::debug!(
1878 required_max_frame = required_snapshot.max_frame,
1879 authority_max_frame = authority_snapshot.max_frame,
1880 authority_checkpoint_seq = authority_snapshot.checkpoint_seq,
1881 "refusing live overflow fallback refresh on a read path because it would require blocking WAL scan I/O"
1882 );
1883 Err(LimboError::Busy)
1884 }
1885}
1886
1887#[cfg(host_shared_wal)]
1888impl WalCoordination for ShmWalCoordination {
1889 fn load_snapshot(&self) -> WalSnapshot {
1890 let snapshot = self.authority.snapshot();
1891 WalSnapshot {
1892 max_frame: snapshot.max_frame,
1893 nbackfills: snapshot.nbackfills,
1894 last_checksum: (snapshot.checksum_1, snapshot.checksum_2),
1895 checkpoint_seq: snapshot.checkpoint_seq,
1896 transaction_count: snapshot.transaction_count,
1897 }
1898 }
1899
1900 fn ensure_local_frame_cache_covers(
1901 &self,
1902 io: &Arc<dyn IO>,
1903 snapshot: WalSnapshot,
1904 ) -> Result<()> {
1905 self.ensure_local_frame_cache_covers_snapshot(io, snapshot)
1906 }
1907
1908 fn publish_commit(&self, commit: WalCommitState) {
1909 {
1910 let mut shared = self.shared.write();
1911 shared
1912 .metadata
1913 .max_frame
1914 .store(commit.max_frame, Ordering::Release);
1915 shared.metadata.last_checksum = commit.last_checksum;
1916 shared
1917 .metadata
1918 .transaction_count
1919 .store(commit.transaction_count, Ordering::Release);
1920 let mut header = shared.metadata.wal_header.lock();
1921 header.checksum_1 = commit.last_checksum.0;
1922 header.checksum_2 = commit.last_checksum.1;
1923 }
1924 self.authority.publish_commit(
1925 commit.max_frame,
1926 commit.last_checksum.0,
1927 commit.last_checksum.1,
1928 commit.transaction_count,
1929 );
1930 if self.authority.frame_index_overflowed() {
1931 let snapshot = self.authority.snapshot();
1932 let shared = self.shared.read();
1933 let mut coverage = shared.runtime.overflow_fallback_coverage.lock();
1934 if coverage.covers(snapshot, commit.max_frame.saturating_sub(1)) {
1935 coverage.record_snapshot(snapshot, commit.max_frame);
1936 }
1937 }
1938 }
1939
1940 fn publish_backfill(&self, max_frame: u64) {
1941 self.shared
1942 .write()
1943 .metadata
1944 .nbackfills
1945 .store(max_frame, Ordering::Release);
1946 self.authority.publish_backfill(max_frame);
1947 }
1948
1949 fn install_durable_backfill_proof(
1950 &self,
1951 nbackfills: u64,
1952 db_size_pages: u32,
1953 db_header_crc32c: u32,
1954 sync_type: FileSyncType,
1955 ) -> Result<Option<Completion>> {
1956 let snapshot = self.authority.snapshot();
1957 turso_assert!(
1958 (snapshot.nbackfills..=snapshot.max_frame).contains(&nbackfills),
1959 "durable backfill proof requires nbackfills within the authoritative WAL range",
1960 {
1961 "nbackfills": nbackfills,
1962 "authority_nbackfills": snapshot.nbackfills,
1963 "authority_max_frame": snapshot.max_frame
1964 }
1965 );
1966 let proof_snapshot = SharedWalCoordinationHeader {
1967 nbackfills,
1968 ..snapshot
1969 };
1970 self.authority
1971 .install_backfill_proof(proof_snapshot, db_size_pages, db_header_crc32c);
1972 Ok(Some(self.authority.begin_sync(sync_type)?))
1973 }
1974
1975 fn find_frame(
1976 &self,
1977 page_id: u64,
1978 min_frame: u64,
1979 max_frame: u64,
1980 frame_watermark: Option<u64>,
1981 ) -> Option<u64> {
1982 if self.authority.frame_index_overflowed() {
1986 return self
1987 .fallback
1988 .find_frame(page_id, min_frame, max_frame, frame_watermark);
1989 }
1990 self.authority
1991 .find_frame(page_id, min_frame, max_frame, frame_watermark)
1992 }
1993
1994 fn iter_latest_frames(&self, min_frame: u64, max_frame: u64) -> Vec<(u64, u64)> {
1995 if self.authority.frame_index_overflowed() {
1998 return self.fallback.iter_latest_frames(min_frame, max_frame);
1999 }
2000 self.authority.iter_latest_frames(min_frame, max_frame)
2001 }
2002
2003 fn checkpoint_epoch(&self) -> u32 {
2004 self.authority.checkpoint_epoch()
2005 }
2006
2007 fn bump_checkpoint_epoch(&self) -> u32 {
2008 let prev = self.authority.bump_checkpoint_epoch();
2009 self.shared
2010 .write()
2011 .runtime
2012 .epoch
2013 .store(prev + 1, Ordering::Release);
2014 prev
2015 }
2016
2017 fn try_begin_read_tx(&self, snapshot: WalSnapshot) -> Option<ReadGuardKind> {
2018 turso_assert!(
2019 snapshot.max_frame <= u32::MAX as u64,
2020 "max_frame exceeds u32 read mark range"
2021 );
2022 let shared = self.shared.read();
2023 let read_locks = &shared.runtime.read_locks;
2024
2025 if snapshot.max_frame == snapshot.nbackfills {
2026 if !read_locks[0].read() {
2027 return None;
2028 }
2029 if self.load_snapshot() != snapshot {
2030 read_locks[0].unlock();
2031 return None;
2032 }
2033 return Some(ReadGuardKind::DbFile);
2034 }
2035
2036 let mut best_idx: i64 = -1;
2037 let mut best_mark: u32 = 0;
2038 for (idx, lock) in read_locks.iter().enumerate().take(5).skip(1) {
2039 let mark = lock.get_value();
2040 if mark != READMARK_NOT_USED && mark <= snapshot.max_frame as u32 && mark > best_mark {
2041 best_mark = mark;
2042 best_idx = idx as i64;
2043 }
2044 }
2045
2046 if best_idx == -1 || (best_mark as u64) < snapshot.max_frame {
2047 for (idx, lock) in read_locks.iter().enumerate().take(5).skip(1) {
2048 if !lock.write() {
2049 continue;
2050 }
2051 lock.set_value_exclusive(snapshot.max_frame as u32);
2052 best_idx = idx as i64;
2053 best_mark = snapshot.max_frame as u32;
2054 read_locks[idx].unlock();
2055 break;
2056 }
2057 }
2058
2059 if best_idx == -1 || !read_locks[best_idx as usize].read() {
2060 return None;
2061 }
2062
2063 let current_slot_mark = read_locks[best_idx as usize].get_value();
2064 if current_slot_mark != best_mark || self.load_snapshot() != snapshot {
2065 read_locks[best_idx as usize].unlock();
2066 return None;
2067 }
2068
2069 let read_mark_index =
2070 NonZeroUsize::new(best_idx as usize).expect("best_idx checked to be positive");
2071 let reader = self
2072 .authority
2073 .register_reader_for_snapshot(self.owner, snapshot.max_frame)?;
2074 if self.load_snapshot() != snapshot {
2075 self.authority.unregister_reader_for_snapshot(reader);
2076 read_locks[best_idx as usize].unlock();
2077 return None;
2078 }
2079
2080 let mut active_reader = self.active_reader.lock();
2081 turso_assert!(active_reader.is_none(), "shared reader registration leaked");
2082 *active_reader = Some(reader);
2083 Some(ReadGuardKind::ReadMark(read_mark_index))
2084 }
2085
2086 fn end_read_tx(&self, guard: ReadGuardKind) {
2087 if let Some(reader) = self.active_reader.lock().take() {
2088 self.authority.unregister_reader_for_snapshot(reader);
2089 }
2090 self.fallback.end_read_tx(guard);
2091 }
2092
2093 fn try_begin_write_tx(&self) -> bool {
2094 if !self.authority.try_acquire_writer(self.owner) {
2095 return false;
2096 }
2097 if !self.fallback.try_write_lock() {
2098 self.authority.release_writer(self.owner);
2099 return false;
2100 }
2101 true
2102 }
2103
2104 fn end_write_tx(&self) {
2105 self.fallback.unlock_write_lock();
2106 self.authority.release_writer(self.owner);
2107 }
2108
2109 fn acquire_checkpoint_guard(
2110 &self,
2111 mode: CheckpointMode,
2112 ) -> Result<CoordinationCheckpointGuardKind> {
2113 if !self.authority.try_acquire_checkpoint(self.owner) {
2114 return Err(LimboError::Busy);
2115 }
2116 let needs_writer = !matches!(mode, CheckpointMode::Passive { .. });
2117 if needs_writer && !self.authority.try_acquire_writer(self.owner) {
2118 self.authority.release_checkpoint(self.owner);
2119 return Err(LimboError::Busy);
2120 }
2121 if !self.fallback.try_checkpoint_lock() {
2122 if needs_writer {
2123 self.authority.release_writer(self.owner);
2124 }
2125 self.authority.release_checkpoint(self.owner);
2126 return Err(LimboError::Busy);
2127 }
2128 match mode {
2129 CheckpointMode::Passive { .. } => {
2130 if !self.fallback.try_read_mark_exclusive(0) {
2131 self.fallback.unlock_checkpoint_lock();
2132 if needs_writer {
2133 self.authority.release_writer(self.owner);
2134 }
2135 self.authority.release_checkpoint(self.owner);
2136 return Err(LimboError::Busy);
2137 }
2138 Ok(CoordinationCheckpointGuardKind::Read0)
2139 }
2140 CheckpointMode::Full | CheckpointMode::Restart | CheckpointMode::Truncate { .. } => {
2141 if !self.fallback.try_read_mark_exclusive(0) {
2142 self.fallback.unlock_checkpoint_lock();
2143 self.authority.release_writer(self.owner);
2144 self.authority.release_checkpoint(self.owner);
2145 return Err(LimboError::Busy);
2146 }
2147 if !self.fallback.try_write_lock() {
2148 self.fallback.unlock_read_mark(0);
2149 self.fallback.unlock_checkpoint_lock();
2150 self.authority.release_writer(self.owner);
2151 self.authority.release_checkpoint(self.owner);
2152 return Err(LimboError::Busy);
2153 }
2154 Ok(CoordinationCheckpointGuardKind::Writer)
2155 }
2156 }
2157 }
2158
2159 fn acquire_vacuum_checkpoint_guard_from_held_lock(
2160 &self,
2161 ) -> Result<CoordinationCheckpointGuardKind> {
2162 if !self.authority.try_acquire_checkpoint(self.owner) {
2163 self.fallback.unlock_checkpoint_lock();
2164 return Err(LimboError::Busy);
2165 }
2166 if !self.authority.try_acquire_writer(self.owner) {
2167 self.authority.release_checkpoint(self.owner);
2168 self.fallback.unlock_checkpoint_lock();
2169 return Err(LimboError::Busy);
2170 }
2171 if !self.fallback.try_read_mark_exclusive(0) {
2172 self.fallback.unlock_checkpoint_lock();
2173 self.authority.release_writer(self.owner);
2174 self.authority.release_checkpoint(self.owner);
2175 return Err(LimboError::Busy);
2176 }
2177 if !self.fallback.try_write_lock() {
2178 self.fallback.unlock_read_mark(0);
2179 self.fallback.unlock_checkpoint_lock();
2180 self.authority.release_writer(self.owner);
2181 self.authority.release_checkpoint(self.owner);
2182 return Err(LimboError::Busy);
2183 }
2184 Ok(CoordinationCheckpointGuardKind::Writer)
2185 }
2186
2187 fn release_checkpoint_guard(&self, guard: CoordinationCheckpointGuardKind) {
2188 match guard {
2189 CoordinationCheckpointGuardKind::Writer => {
2190 self.fallback.unlock_write_lock();
2191 self.fallback.unlock_read_mark(0);
2192 self.fallback.unlock_checkpoint_lock();
2193 self.authority.release_writer(self.owner);
2194 self.authority.release_checkpoint(self.owner);
2195 }
2196 CoordinationCheckpointGuardKind::Read0 => {
2197 self.fallback.unlock_read_mark(0);
2198 self.fallback.unlock_checkpoint_lock();
2199 self.authority.release_checkpoint(self.owner);
2200 }
2201 }
2202 }
2203
2204 fn determine_max_safe_checkpoint_frame(&self, max_frame: u64) -> u64 {
2205 turso_assert!(
2206 max_frame <= u32::MAX as u64,
2207 "max_frame exceeds u32 read mark range"
2208 );
2209 let mut max_safe_frame = max_frame;
2210 for read_lock_idx in 1..5 {
2211 let this_mark = self.fallback.read_mark_value(read_lock_idx);
2212 if this_mark < max_safe_frame as u32 {
2213 let busy = !self.fallback.try_read_mark_exclusive(read_lock_idx);
2214 if !busy {
2215 let val = if read_lock_idx == 1 {
2216 max_safe_frame as u32
2217 } else {
2218 READMARK_NOT_USED
2219 };
2220 self.fallback
2221 .set_read_mark_value_exclusive(read_lock_idx, val);
2222 self.fallback.unlock_read_mark(read_lock_idx);
2223 } else {
2224 max_safe_frame = this_mark as u64;
2225 }
2226 }
2227 }
2228 match self.authority.min_active_reader_frame() {
2229 Some(shared_min) => max_safe_frame.min(shared_min),
2230 None => max_safe_frame,
2231 }
2232 }
2233
2234 fn min_pinned_read_frame(&self) -> Option<u64> {
2235 let local = self.fallback.min_pinned_read_frame_inner();
2238 match (local, self.authority.min_active_reader_frame()) {
2239 (Some(a), Some(b)) => Some(a.min(b)),
2240 (Some(a), None) => Some(a),
2241 (None, b) => b,
2242 }
2243 }
2244
2245 fn begin_restart(&self, io: &dyn IO) -> Result<WalSnapshot> {
2246 for idx in 1..5 {
2247 if !self.fallback.try_read_mark_exclusive(idx) {
2248 for held_idx in 1..idx {
2249 self.fallback.unlock_read_mark(held_idx);
2250 }
2251 return Err(LimboError::Busy);
2252 }
2253 }
2254 if self.authority.min_active_reader_frame().is_some() {
2260 for idx in 1..5 {
2261 self.fallback.unlock_read_mark(idx);
2262 }
2263 return Err(LimboError::Busy);
2264 }
2265 Ok(self.restart_snapshot_from_authority(self.authority.snapshot(), io))
2266 }
2267
2268 fn end_restart(&self) {
2269 self.fallback.end_restart();
2270 }
2271
2272 fn try_restart_log_for_write(&self, io: &dyn IO) -> Result<Option<WalSnapshot>> {
2273 if !self.fallback.try_upgrade_read_mark(0) {
2274 return Ok(None);
2275 }
2276 let result = self.begin_restart(io);
2277 self.fallback.downgrade_read_mark(0);
2278 match result {
2279 Ok(snapshot) => {
2280 self.end_restart();
2281 Ok(Some(snapshot))
2282 }
2283 Err(err) => Err(err),
2284 }
2285 }
2286
2287 fn prepare_truncate(&self) -> Result<Arc<dyn File>> {
2288 self.fallback.prepare_truncate()
2289 }
2290
2291 fn wal_header(&self) -> WalHeader {
2292 let snapshot = self.authority.snapshot();
2293 let mut header = self.fallback.wal_header();
2294 if snapshot.page_size == 0 {
2295 return header;
2296 }
2297 header.page_size = snapshot.page_size;
2298 header.checkpoint_seq = snapshot.checkpoint_seq;
2299 header.salt_1 = snapshot.salt_1;
2300 header.salt_2 = snapshot.salt_2;
2301 header.checksum_1 = snapshot.checksum_1;
2302 header.checksum_2 = snapshot.checksum_2;
2303 header
2304 }
2305
2306 fn wal_file(&self) -> Result<Arc<dyn File>> {
2307 self.fallback.wal_file()
2308 }
2309
2310 fn shared_wal_state(&self) -> Arc<RwLock<WalFileShared>> {
2311 self.shared.clone()
2312 }
2313
2314 fn wal_is_initialized(&self) -> bool {
2315 let authority_snapshot = self.authority.snapshot();
2316 if Self::authority_needs_local_header_seed(authority_snapshot) {
2317 return self.fallback.wal_is_initialized();
2318 }
2319 if authority_snapshot.max_frame > 0 {
2320 self.sync_local_from_authority(authority_snapshot);
2321 self.fallback.mark_initialized();
2322 return true;
2323 }
2324 if self.local_zero_frame_generation_is_initialized(authority_snapshot) {
2325 return true;
2326 }
2327
2328 self.sync_local_from_authority(authority_snapshot);
2329 self.shared
2330 .read()
2331 .metadata
2332 .initialized
2333 .store(false, Ordering::Release);
2334 false
2335 }
2336
2337 fn prepare_wal_header(&self, io: &dyn IO, page_size: PageSize) -> Option<WalHeader> {
2338 let authority_snapshot = self.authority.snapshot();
2339 if Self::authority_needs_local_header_seed(authority_snapshot) {
2345 let header = self.fallback.prepare_wal_header(io, page_size);
2346 if header.is_some() {
2347 self.sync_authority_from_local();
2348 }
2349 return header;
2350 }
2351 self.sync_local_from_authority(authority_snapshot);
2352 let header = self.fallback.prepare_wal_header(io, page_size);
2353 if header.is_some() {
2354 self.sync_authority_from_local();
2355 }
2356 header
2357 }
2358
2359 fn mark_initialized(&self) {
2360 self.fallback.mark_initialized();
2361 }
2362
2363 fn cache_frame(&self, page_id: u64, frame_id: u64) {
2364 self.fallback.cache_frame(page_id, frame_id);
2365 self.authority.record_frame(page_id, frame_id);
2366 }
2367
2368 fn rollback_cache(&self, max_frame: u64) {
2369 self.fallback.rollback_cache(max_frame);
2370 self.authority.rollback_frames(max_frame);
2371 self.clear_overflow_fallback_coverage();
2372 }
2373
2374 fn should_checkpoint_on_close(&self) -> bool {
2375 self.authority.is_last_process_mapping()
2376 }
2377
2378 #[cfg(clt_turso_tests)]
2379 fn backend_name(&self) -> &'static str {
2380 "tshm"
2381 }
2382
2383 #[cfg(clt_turso_tests)]
2384 fn shared_ptr(&self) -> usize {
2385 Arc::as_ptr(&self.shared) as usize
2386 }
2387
2388 #[cfg(clt_turso_tests)]
2389 fn open_mode_name(&self) -> Option<&'static str> {
2390 Some(match self.authority.open_mode() {
2391 SharedWalCoordinationOpenMode::Exclusive => "exclusive",
2392 SharedWalCoordinationOpenMode::MultiProcess => "multiprocess",
2393 })
2394 }
2395}
2396
2397#[derive(Debug, Clone)]
2398pub enum CheckpointState {
2399 Start,
2400 Processing,
2401 DetermineResult,
2403 Finalize {
2406 checkpoint_result: Option<CheckpointResult>,
2407 },
2408}
2409
2410pub const CKPT_BATCH_PAGES: usize = 512;
2412
2413const MIN_AVG_RUN_FOR_FLUSH: f32 = 32.0;
2416const MIN_BATCH_LEN_FOR_FLUSH: usize = 512;
2417const MAX_INFLIGHT_WRITES: usize = 64;
2418pub const MAX_INFLIGHT_READS: usize = 512;
2419pub const IOV_MAX: usize = 1024;
2420
2421type PageId = usize;
2422struct InflightRead {
2423 completion: Completion,
2424 page_id: PageId,
2425 buf: Arc<SpinLock<Option<Arc<Buffer>>>>,
2427}
2428
2429#[derive(Default)]
2432struct WriteBatch {
2433 items: BTreeMap<PageId, Arc<Buffer>>,
2435 run_count: usize,
2437}
2438
2439impl WriteBatch {
2440 fn new() -> Self {
2441 Self {
2442 items: BTreeMap::new(),
2443 run_count: 0,
2444 }
2445 }
2446
2447 #[inline]
2448 fn insert(&mut self, page_id: PageId, buf: Arc<Buffer>) {
2450 if let std::collections::btree_map::Entry::Occupied(mut e) = self.items.entry(page_id) {
2451 e.insert(buf);
2452 return;
2453 }
2454 let start = page_id.saturating_sub(1);
2456 let end = page_id.saturating_add(1);
2457 let mut has_left = false;
2458 let mut has_right = false;
2459
2460 for (k, _) in self.items.range(start..=end) {
2461 if *k == page_id.wrapping_sub(1) {
2462 has_left = true;
2463 }
2464 if *k == page_id.wrapping_add(1) {
2465 has_right = true;
2466 }
2467 }
2468 match (has_left, has_right) {
2469 (false, false) => self.run_count += 1,
2470 (true, true) => self.run_count = self.run_count.saturating_sub(1),
2471 _ => {}
2472 }
2473 self.items.insert(page_id, buf);
2474 }
2475
2476 #[inline]
2477 fn len(&self) -> usize {
2478 self.items.len()
2479 }
2480 #[inline]
2481 fn is_empty(&self) -> bool {
2482 self.items.is_empty()
2483 }
2484 #[inline]
2485 fn is_full(&self) -> bool {
2486 self.items.len() >= CKPT_BATCH_PAGES
2487 }
2488
2489 #[inline]
2490 fn avg_run_len(&self) -> f32 {
2491 if self.run_count == 0 {
2492 0.0
2493 } else {
2494 self.items.len() as f32 / self.run_count as f32
2495 }
2496 }
2497
2498 #[inline]
2499 fn take(&mut self) -> BTreeMap<PageId, Arc<Buffer>> {
2500 self.run_count = 0;
2501 std::mem::take(&mut self.items)
2502 }
2503
2504 #[inline]
2505 fn clear(&mut self) {
2506 self.items.clear();
2507 self.run_count = 0;
2508 }
2509}
2510
2511impl std::ops::Deref for WriteBatch {
2512 type Target = BTreeMap<PageId, Arc<Buffer>>;
2513 fn deref(&self) -> &Self::Target {
2514 &self.items
2515 }
2516}
2517impl std::ops::DerefMut for WriteBatch {
2518 fn deref_mut(&mut self) -> &mut Self::Target {
2519 &mut self.items
2520 }
2521}
2522
2523struct OngoingCheckpoint {
2525 time: MonotonicInstant,
2527 min_frame: u64,
2529 max_frame: u64,
2531 current_page: u64,
2533 state: CheckpointState,
2535 pending_writes: WriteBatch,
2537 inflight_reads: Vec<InflightRead>,
2539 inflight_writes: Vec<InflightWriteBatch>,
2541 pages_to_checkpoint: Vec<(u64, u64)>,
2543}
2544
2545struct InflightWriteBatch {
2546 done: Arc<AtomicBool>,
2547 err: Arc<crate::sync::OnceLock<CompletionError>>,
2548}
2549
2550impl OngoingCheckpoint {
2551 fn reset(&mut self) {
2552 self.min_frame = 0;
2553 self.max_frame = 0;
2554 self.current_page = 0;
2555 self.pages_to_checkpoint.clear();
2556 self.pending_writes.clear();
2557 self.inflight_reads.clear();
2558 self.inflight_writes.clear();
2559 self.state = CheckpointState::Start;
2560 }
2561
2562 #[inline]
2563 fn should_issue_reads(&self) -> bool {
2565 (self.current_page as usize) < self.pages_to_checkpoint.len()
2566 && !self.pending_writes.is_full()
2567 && self.inflight_reads.len() < MAX_INFLIGHT_READS
2568 }
2569
2570 #[inline]
2571 fn complete(&self) -> bool {
2573 (self.current_page as usize) >= self.pages_to_checkpoint.len()
2574 && self.inflight_reads.is_empty()
2575 && self.pending_writes.is_empty()
2576 && self.inflight_writes.is_empty()
2577 }
2578
2579 #[inline]
2580 fn should_flush_batch(&self) -> bool {
2582 self.pending_writes.is_full()
2583 || (self.pending_writes.len() >= MIN_BATCH_LEN_FOR_FLUSH
2584 && self.pending_writes.avg_run_len() >= MIN_AVG_RUN_FOR_FLUSH)
2585 || ((self.current_page as usize) >= self.pages_to_checkpoint.len()
2586 && self.inflight_reads.is_empty()
2587 && !self.pending_writes.is_empty())
2588 }
2589
2590 #[inline]
2591 fn process_inflight_writes(&mut self) -> bool {
2594 let before_len = self.inflight_writes.len();
2595 self.inflight_writes
2596 .retain(|w| !w.done.load(Ordering::Acquire));
2597 before_len > self.inflight_writes.len()
2598 }
2599
2600 #[inline]
2601 fn process_pending_reads(&mut self) -> Result<bool> {
2604 let mut moved = false;
2605 let mut err: Option<CompletionError> = None;
2606
2607 self.inflight_reads.retain(|slot| {
2608 if !slot.completion.finished() {
2609 return true;
2610 }
2611 if slot.completion.succeeded() {
2612 if let Some(buf) = slot.buf.lock().take() {
2613 self.pending_writes.insert(slot.page_id, buf);
2614 moved = true;
2615 } else {
2616 err = Some(CompletionError::IOError(std::io::ErrorKind::Other, "read"));
2617 }
2618 } else {
2619 err = Some(
2620 slot.completion
2621 .get_error()
2622 .unwrap_or(CompletionError::IOError(std::io::ErrorKind::Other, "read")),
2623 );
2624 }
2625 false
2626 });
2627 if let Some(e) = err {
2628 return Err(LimboError::CompletionError(e));
2629 }
2630 Ok(moved)
2631 }
2632
2633 fn first_write_error(&self) -> Option<CompletionError>
2634 where
2635 CompletionError: Clone,
2636 {
2637 self.inflight_writes
2638 .iter()
2639 .find_map(|w| w.err.get().cloned())
2640 }
2641}
2642
2643impl InflightWriteBatch {
2644 #[inline]
2645 fn new() -> InflightWriteBatch {
2646 InflightWriteBatch {
2647 done: Arc::new(AtomicBool::new(false)),
2648 err: Arc::new(OnceLock::new()),
2649 }
2650 }
2651}
2652
2653impl fmt::Debug for OngoingCheckpoint {
2654 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2655 f.debug_struct("OngoingCheckpoint")
2656 .field("state", &self.state)
2657 .field("min_frame", &self.min_frame)
2658 .field("max_frame", &self.max_frame)
2659 .field("current_page", &self.current_page)
2660 .finish()
2661 }
2662}
2663
2664pub struct WalFile {
2665 io: Arc<dyn IO>,
2666 buffer_pool: Arc<BufferPool>,
2667 coordination: Arc<dyn WalCoordination>,
2668
2669 syncing: Arc<AtomicBool>,
2670 write_lock_held: AtomicBool,
2671
2672 ongoing_checkpoint: RwLock<OngoingCheckpoint>,
2673 checkpoint_threshold: usize,
2674 max_frame_read_lock_index: AtomicUsize,
2677 max_frame: AtomicU64,
2679 min_frame: AtomicU64,
2681 last_checksum: RwLock<(u32, u32)>,
2683 checkpoint_seq: AtomicU32,
2684 transaction_count: AtomicU64,
2685
2686 checkpoint_guard: RwLock<Option<CheckpointLocks>>,
2688 vacuum_lock_guard: RwLock<Option<VacuumLockGuard>>,
2692
2693 io_ctx: RwLock<IOContext>,
2694
2695 dirty: Arc<AtomicBool>,
2702}
2703
2704impl fmt::Debug for WalFile {
2705 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2706 f.debug_struct("WalFile")
2707 .field("syncing", &self.syncing.load(Ordering::Relaxed))
2708 .field("page_size", &self.page_size())
2709 .field("ongoing_checkpoint", &*self.ongoing_checkpoint.read())
2710 .field("checkpoint_threshold", &self.checkpoint_threshold)
2711 .field("max_frame_read_lock_index", &self.max_frame_read_lock_index)
2712 .field("max_frame", &self.max_frame)
2713 .field("min_frame", &self.min_frame)
2714 .finish()
2716 }
2717}
2718
2719pub struct WalSharedMetadata {
2776 pub enabled: AtomicBool,
2777 pub wal_header: Arc<SpinLock<WalHeader>>,
2778 pub min_frame: AtomicU64,
2779 pub max_frame: AtomicU64,
2780 pub nbackfills: AtomicU64,
2781 pub transaction_count: AtomicU64,
2782 pub last_checksum: (u32, u32), pub loaded: AtomicBool,
2784 pub loaded_from_disk_scan: AtomicBool,
2785 pub initialized: AtomicBool,
2786}
2787
2788pub struct WalSharedRuntime {
2790 pub authority_reconciliation: Arc<Mutex<()>>,
2792 pub frame_cache: Arc<SpinLock<FxHashMap<u64, Vec<u64>>>>,
2799 pub frame_cache_high_water: AtomicU64,
2808 pub file: Option<Arc<dyn File>>,
2809 pub read_locks: [TursoRwLock; 5],
2816 pub vacuum_lock: TursoRwLock,
2822 pub write_lock: TursoRwLock,
2825
2826 pub checkpoint_lock: TursoRwLock,
2828 pub epoch: AtomicU32,
2831 pub overflow_fallback_coverage: Arc<SpinLock<OverflowFallbackCoverage>>,
2834}
2835
2836pub enum OpenSharedWal {
2840 Noop(Arc<RwLock<WalFileShared>>),
2841 Build(sqlite3_ondisk::BuildSharedWal),
2842}
2843
2844impl OpenSharedWal {
2845 pub fn poll(&mut self) -> Result<IOResult<Arc<RwLock<WalFileShared>>>> {
2846 match self {
2847 OpenSharedWal::Noop(wal) => Ok(IOResult::Done(wal.clone())),
2848 OpenSharedWal::Build(driver) => driver.poll(),
2849 }
2850 }
2851}
2852
2853pub struct WalFileShared {
2855 pub metadata: WalSharedMetadata,
2856 pub runtime: WalSharedRuntime,
2857}
2858
2859#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2860pub struct OverflowFallbackCoverage {
2861 checkpoint_seq: u32,
2862 salt_1: u32,
2863 salt_2: u32,
2864 max_frame: u64,
2865 valid: bool,
2866}
2867
2868impl OverflowFallbackCoverage {
2869 pub(crate) fn clear(&mut self) {
2870 *self = Self::default();
2871 }
2872
2873 pub(crate) fn record(&mut self, checkpoint_seq: u32, salt_1: u32, salt_2: u32, max_frame: u64) {
2874 if max_frame == 0 {
2875 self.clear();
2876 return;
2877 }
2878 self.checkpoint_seq = checkpoint_seq;
2879 self.salt_1 = salt_1;
2880 self.salt_2 = salt_2;
2881 self.max_frame = max_frame;
2882 self.valid = true;
2883 }
2884
2885 #[cfg(host_shared_wal)]
2886 pub(crate) fn record_snapshot(
2887 &mut self,
2888 snapshot: SharedWalCoordinationHeader,
2889 max_frame: u64,
2890 ) {
2891 self.record(
2892 snapshot.checkpoint_seq,
2893 snapshot.salt_1,
2894 snapshot.salt_2,
2895 max_frame,
2896 );
2897 }
2898
2899 #[cfg(host_shared_wal)]
2900 pub(crate) fn same_generation(&self, snapshot: SharedWalCoordinationHeader) -> bool {
2901 self.valid
2902 && self.checkpoint_seq == snapshot.checkpoint_seq
2903 && self.salt_1 == snapshot.salt_1
2904 && self.salt_2 == snapshot.salt_2
2905 }
2906
2907 #[cfg(host_shared_wal)]
2908 pub(crate) fn covers(&self, snapshot: SharedWalCoordinationHeader, max_frame: u64) -> bool {
2909 self.same_generation(snapshot) && self.max_frame >= max_frame
2910 }
2911}
2912
2913impl fmt::Debug for WalFileShared {
2914 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2915 f.debug_struct("WalFileShared")
2916 .field("enabled", &self.metadata.enabled.load(Ordering::Relaxed))
2917 .field("wal_header", &self.metadata.wal_header)
2918 .field("min_frame", &self.metadata.min_frame)
2919 .field("max_frame", &self.metadata.max_frame)
2920 .field("nbackfills", &self.metadata.nbackfills)
2921 .field("frame_cache", &self.runtime.frame_cache)
2922 .field("last_checksum", &self.metadata.last_checksum)
2923 .finish()
2925 }
2926}
2927
2928#[derive(Debug)]
2929enum VacuumLockGuard {
2930 Read { ptr: Arc<RwLock<WalFileShared>> },
2931 Write { ptr: Arc<RwLock<WalFileShared>> },
2932}
2933
2934impl VacuumLockGuard {
2935 fn try_read(ptr: Arc<RwLock<WalFileShared>>) -> Option<Self> {
2936 let acquired = {
2937 let shared = ptr.read();
2938 shared.runtime.vacuum_lock.read()
2939 };
2940 if acquired {
2941 Some(Self::Read { ptr })
2942 } else {
2943 None
2944 }
2945 }
2946
2947 fn try_write(ptr: Arc<RwLock<WalFileShared>>) -> Option<Self> {
2948 let acquired = {
2949 let shared = ptr.read();
2950 shared.runtime.vacuum_lock.write()
2951 };
2952 if acquired {
2953 Some(Self::Write { ptr })
2954 } else {
2955 None
2956 }
2957 }
2958
2959 const fn is_read(&self) -> bool {
2960 matches!(self, Self::Read { .. })
2961 }
2962
2963 const fn is_write(&self) -> bool {
2964 matches!(self, Self::Write { .. })
2965 }
2966}
2967
2968impl Drop for VacuumLockGuard {
2969 fn drop(&mut self) {
2970 match self {
2971 Self::Read { ptr } | Self::Write { ptr } => {
2972 ptr.read().runtime.vacuum_lock.unlock();
2973 }
2974 }
2975 }
2976}
2977
2978#[derive(Clone, Debug)]
2979enum CheckpointLocks {
2983 Writer {
2984 coordination: Arc<dyn WalCoordination>,
2985 },
2986 Read0 {
2987 coordination: Arc<dyn WalCoordination>,
2988 },
2989}
2990
2991#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2995pub(crate) enum CheckpointLockSource {
2996 #[default]
2997 Acquire,
2998 HeldByCaller,
2999}
3000
3001impl CheckpointLocks {
3009 fn new(coordination: Arc<dyn WalCoordination>, mode: CheckpointMode) -> Result<Self> {
3010 let guard = coordination.acquire_checkpoint_guard(mode)?;
3011 Ok(match guard {
3012 CoordinationCheckpointGuardKind::Read0 => Self::Read0 { coordination },
3013 CoordinationCheckpointGuardKind::Writer => Self::Writer { coordination },
3014 })
3015 }
3016
3017 fn from_held_vacuum_checkpoint_lock(coordination: Arc<dyn WalCoordination>) -> Result<Self> {
3023 let guard = coordination.acquire_vacuum_checkpoint_guard_from_held_lock()?;
3024 Ok(match guard {
3025 CoordinationCheckpointGuardKind::Read0 => Self::Read0 { coordination },
3026 CoordinationCheckpointGuardKind::Writer => Self::Writer { coordination },
3027 })
3028 }
3029}
3030
3031impl Drop for CheckpointLocks {
3032 fn drop(&mut self) {
3033 match self {
3034 CheckpointLocks::Writer { coordination } => {
3035 coordination.release_checkpoint_guard(CoordinationCheckpointGuardKind::Writer);
3036 }
3037 CheckpointLocks::Read0 { coordination } => {
3038 coordination.release_checkpoint_guard(CoordinationCheckpointGuardKind::Read0);
3039 }
3040 }
3041 }
3042}
3043
3044enum TryBeginReadResult {
3046 Ok(bool),
3048 Retry,
3050 Err(LimboError),
3052 Busy,
3058}
3059
3060impl WalFile {
3061 fn load_coordination_snapshot(&self) -> WalSnapshot {
3063 self.coordination.load_snapshot()
3064 }
3065
3066 fn connection_state(&self) -> WalConnectionState {
3068 WalConnectionState::new(
3069 WalSnapshot {
3070 max_frame: self.max_frame.load(Ordering::Acquire),
3071 nbackfills: self.min_frame.load(Ordering::Acquire).saturating_sub(1),
3072 last_checksum: *self.last_checksum.read(),
3073 checkpoint_seq: self.checkpoint_seq.load(Ordering::Acquire),
3074 transaction_count: self.transaction_count.load(Ordering::Acquire),
3075 },
3076 ReadGuardKind::from_lock_index(self.max_frame_read_lock_index.load(Ordering::Acquire)),
3077 )
3078 }
3079
3080 fn install_connection_state(&self, state: WalConnectionState) {
3082 self.max_frame
3083 .store(state.snapshot.max_frame, Ordering::Release);
3084 self.min_frame
3085 .store(state.snapshot.min_frame(), Ordering::Release);
3086 *self.last_checksum.write() = state.snapshot.last_checksum;
3087 self.checkpoint_seq
3088 .store(state.snapshot.checkpoint_seq, Ordering::Release);
3089 self.transaction_count
3090 .store(state.snapshot.transaction_count, Ordering::Release);
3091 self.max_frame_read_lock_index
3092 .store(state.read_guard.lock_index(), Ordering::Release);
3093 }
3094
3095 fn db_changed_against(&self, snapshot: WalSnapshot, local_state: WalConnectionState) -> bool {
3097 snapshot != local_state.snapshot
3098 }
3099
3100 fn has_vacuum_read_lock_guard(&self) -> bool {
3101 self.vacuum_lock_guard
3102 .read()
3103 .as_ref()
3104 .is_some_and(VacuumLockGuard::is_read)
3105 }
3106
3107 fn install_vacuum_lock_guard(&self, guard: VacuumLockGuard) {
3118 let mut slot = self.vacuum_lock_guard.write();
3119 turso_assert!(slot.is_none(), "VACUUM lock guard is already installed");
3120 *slot = Some(guard);
3121 }
3122
3123 fn release_vacuum_read_lock_guard(&self) {
3124 let guard = {
3125 let mut slot = self.vacuum_lock_guard.write();
3126 turso_assert!(
3127 slot.as_ref().is_some_and(VacuumLockGuard::is_read),
3128 "VACUUM read lock guard is not held"
3129 );
3130 slot.take()
3131 .expect("VACUUM read lock guard should be present after kind check")
3132 };
3133 drop(guard);
3134 }
3135
3136 fn release_vacuum_write_lock_guard(&self) {
3137 let guard = {
3138 let mut slot = self.vacuum_lock_guard.write();
3139 turso_assert!(
3140 slot.as_ref().is_some_and(VacuumLockGuard::is_write),
3141 "VACUUM write lock guard is not held"
3142 );
3143 slot.take()
3144 .expect("VACUUM write lock guard should be present after kind check")
3145 };
3146 drop(guard);
3147 }
3148
3149 fn try_begin_read_tx(&self) -> TryBeginReadResult {
3152 turso_assert!(
3153 self.max_frame_read_lock_index
3154 .load(Ordering::Acquire)
3155 .eq(&NO_LOCK_HELD),
3156 "cannot start a new read tx without ending an existing one",
3157 { "lock_value": self.max_frame_read_lock_index.load(Ordering::Acquire), "expected": NO_LOCK_HELD }
3158 );
3159 turso_assert!(
3160 self.vacuum_lock_guard.read().is_none(),
3161 "VACUUM lock guard already held"
3162 );
3163
3164 let Some(vacuum_lock_guard) =
3168 VacuumLockGuard::try_read(self.coordination.shared_wal_state())
3169 else {
3170 tracing::debug!("begin_read_tx: VACUUM holds the vacuum lock, returning Busy");
3171 return TryBeginReadResult::Busy;
3172 };
3173
3174 let shared_snapshot = self.load_coordination_snapshot();
3177 turso_assert!(
3178 shared_snapshot.nbackfills <= shared_snapshot.max_frame,
3179 "WAL snapshot cannot have backfills beyond max frame",
3180 {
3181 "nbackfills": shared_snapshot.nbackfills,
3182 "max_frame": shared_snapshot.max_frame,
3183 "checkpoint_seq": shared_snapshot.checkpoint_seq
3184 }
3185 );
3186 tracing::debug!(
3187 "try_begin_read_tx: shared_max={}, nbackfills={}, last_checksum={:?}, checkpoint_seq={:?}, transaction_count={}",
3188 shared_snapshot.max_frame,
3189 shared_snapshot.nbackfills,
3190 shared_snapshot.last_checksum,
3191 shared_snapshot.checkpoint_seq,
3192 shared_snapshot.transaction_count
3193 );
3194 if let Err(err) = self
3195 .coordination
3196 .ensure_local_frame_cache_covers(&self.io, shared_snapshot)
3197 {
3198 return match err {
3199 LimboError::Busy => TryBeginReadResult::Retry,
3200 other => TryBeginReadResult::Err(other),
3201 };
3202 }
3203
3204 let db_changed = self.db_changed_against(shared_snapshot, self.connection_state());
3207
3208 tracing::debug!("try_begin_read_tx: db_changed={}", db_changed);
3209
3210 if shared_snapshot.max_frame == shared_snapshot.nbackfills {
3213 tracing::debug!(
3214 "begin_read_tx: WAL fully checkpointed, shared_max={}, nbackfills={}",
3215 shared_snapshot.max_frame,
3216 shared_snapshot.nbackfills
3217 );
3218 }
3219
3220 let Some(read_guard) = self.coordination.try_begin_read_tx(shared_snapshot) else {
3221 return TryBeginReadResult::Retry;
3222 };
3223 self.install_vacuum_lock_guard(vacuum_lock_guard);
3224 self.install_connection_state(WalConnectionState::new(shared_snapshot, read_guard));
3225 tracing::debug!(
3226 "begin_read_tx(min={}, max={}, slot={}, max_frame_in_wal={})",
3227 self.min_frame.load(Ordering::Acquire),
3228 self.max_frame.load(Ordering::Acquire),
3229 read_guard.lock_index(),
3230 shared_snapshot.max_frame
3231 );
3232 TryBeginReadResult::Ok(db_changed)
3233 }
3234}
3235
3236impl Wal for WalFile {
3237 fn begin_read_tx(&self) -> Result<bool> {
3238 let mut cnt = 0u32;
3244 loop {
3245 tracing::trace!("begin_read_tx: cnt={cnt}");
3246 match self.try_begin_read_tx() {
3247 TryBeginReadResult::Ok(changed) => return Ok(changed),
3248 TryBeginReadResult::Err(err) => return Err(err),
3249 TryBeginReadResult::Busy => return Err(LimboError::Busy),
3250 TryBeginReadResult::Retry => {
3251 cnt += 1;
3252 if cnt > 100 {
3253 return Err(LimboError::Busy);
3254 }
3255 if cnt > 5 {
3258 if cnt < 10 {
3259 self.io.yield_now();
3261 } else {
3262 let delay_us = ((cnt - 9) * (cnt - 9) * 39) as u64;
3265 self.io.sleep(std::time::Duration::from_micros(delay_us));
3266 }
3267 }
3268 continue;
3269 }
3270 }
3271 }
3272 }
3273
3274 fn mvcc_refresh_if_db_changed(&self) -> bool {
3275 WalFile::mvcc_refresh_if_db_changed(self)
3276 }
3277
3278 #[inline(always)]
3280 #[instrument(skip_all, level = Level::DEBUG)]
3281 fn end_read_tx(&self) {
3282 let slot = self.max_frame_read_lock_index.load(Ordering::Acquire);
3283 if slot != NO_LOCK_HELD {
3284 self.coordination
3285 .end_read_tx(ReadGuardKind::from_lock_index(slot));
3286 self.max_frame_read_lock_index
3287 .store(NO_LOCK_HELD, Ordering::Release);
3288 self.release_vacuum_read_lock_guard();
3289 tracing::debug!("end_read_tx(slot={slot})");
3290 } else {
3291 turso_assert!(
3293 !self.has_vacuum_read_lock_guard(),
3294 "vacuum read lock guard held without setting lock slot NO_LOCK_HELD"
3295 );
3296 tracing::debug!("end_read_tx(slot=no_lock)");
3297 }
3298 }
3299
3300 #[instrument(skip_all, level = Level::DEBUG)]
3302 fn begin_write_tx(&self, allowed_auto_actions: WalAutoActions) -> Result<()> {
3303 tracing::debug!("begin_write_tx");
3304 let begin_write_result: Result<()> = {
3305 turso_assert!(
3311 self.max_frame_read_lock_index.load(Ordering::Acquire) != NO_LOCK_HELD,
3312 "must have a read transaction to begin a write transaction"
3313 );
3314 turso_assert!(
3315 !self.holds_write_lock(),
3316 "write lock already held by this connection"
3317 );
3318 if !self.coordination.try_begin_write_tx() {
3319 return Err(LimboError::Busy);
3320 }
3321 let db_changed =
3322 self.db_changed_against(self.load_coordination_snapshot(), self.connection_state());
3323 if db_changed {
3324 tracing::debug!(
3329 "unable to upgrade transaction from read to write: snapshot is stale, give up and let caller retry from scratch, self.max_frame={}, shared_max={}",
3330 self.max_frame.load(Ordering::Acquire),
3331 self.load_coordination_snapshot().max_frame
3332 );
3333 self.coordination.end_write_tx();
3334 return Err(LimboError::BusySnapshot);
3335 }
3336
3337 Ok(())
3338 };
3339 begin_write_result?;
3340 if self
3341 .write_lock_held
3342 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
3343 .is_err()
3344 {
3345 self.coordination.end_write_tx();
3346 turso_assert!(
3347 false,
3348 "begin_write_tx called while write lock already held according to connection state"
3349 );
3350 }
3351
3352 if !allowed_auto_actions.contains(WalAutoActions::Restart) {
3353 return Ok(());
3354 }
3355
3356 let result = self.try_restart_log_before_write();
3357 if let Err(LimboError::Busy) | Ok(()) = &result {
3358 return Ok(());
3360 }
3361
3362 self.coordination.end_write_tx();
3364 turso_assert!(
3365 self.write_lock_held
3366 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
3367 .is_ok(),
3368 "end_write_tx called while write lock not held according to connection state"
3369 );
3370
3371 Err(result.expect_err("Ok case handled above"))
3372 }
3373
3374 #[instrument(skip_all, level = Level::DEBUG)]
3376 fn end_write_tx(&self) {
3377 turso_assert!(
3378 self.write_lock_held
3379 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
3380 .is_ok(),
3381 "end_write_tx called while write lock not held according to connection state"
3382 );
3383 self.coordination.end_write_tx();
3384 }
3385
3386 fn holds_read_lock(&self) -> bool {
3388 self.max_frame_read_lock_index.load(Ordering::Acquire) != NO_LOCK_HELD
3389 }
3390
3391 fn holds_write_lock(&self) -> bool {
3393 self.write_lock_held.load(Ordering::Acquire)
3394 }
3395
3396 fn should_checkpoint_on_close(&self) -> bool {
3397 self.coordination.should_checkpoint_on_close()
3398 }
3399
3400 #[instrument(skip_all, level = Level::DEBUG)]
3402 #[aristo::intent(
3403 "find_frame never reads outside the live frame range [nbackfills, max_frame]\n",
3404 id = "aristos:wal_find_frame_range_invariant",
3405 verify = "full",
3406 parent = "wal_protocol_correctness"
3407 )]
3408 fn find_frame(&self, page_id: u64, frame_watermark: Option<u64>) -> Result<Option<u64>> {
3409 #[cfg(not(clt_turso_feature = "conn_raw_api"))]
3410 turso_assert!(
3411 frame_watermark.is_none(),
3412 "unexpected use of frame_watermark optional argument"
3413 );
3414
3415 turso_assert!(
3416 frame_watermark.unwrap_or(0) <= self.max_frame.load(Ordering::Acquire),
3417 "frame_watermark must be <= than current WAL max_frame value"
3418 );
3419
3420 let nbackfills = self.load_coordination_snapshot().nbackfills;
3425 turso_assert!(
3426 frame_watermark.is_none() || frame_watermark.unwrap() >= nbackfills,
3427 "frame_watermark must be >= than current WAL backfill amount",
3428 { "frame_watermark": frame_watermark, "nbackfills": nbackfills }
3429 );
3430
3431 if self.max_frame_read_lock_index.load(Ordering::Acquire) == 0
3438 && self.max_frame.load(Ordering::Acquire) < self.min_frame.load(Ordering::Acquire)
3439 {
3440 tracing::debug!(
3441 "find_frame(page_id={}, frame_watermark={:?}): max_frame is 0 - read from DB file",
3442 page_id,
3443 frame_watermark,
3444 );
3445 return Ok(None);
3446 }
3447 let min_frame = self.min_frame.load(Ordering::Acquire);
3448 let max_frame = self.max_frame.load(Ordering::Acquire);
3449 self.coordination.ensure_local_frame_cache_covers(
3450 &self.io,
3451 WalSnapshot {
3452 max_frame,
3453 nbackfills: self.min_frame.load(Ordering::Acquire).saturating_sub(1),
3454 last_checksum: *self.last_checksum.read(),
3455 checkpoint_seq: self.coordination.wal_header().checkpoint_seq,
3456 transaction_count: self.transaction_count.load(Ordering::Acquire),
3457 },
3458 )?;
3459 tracing::debug!(
3460 "find_frame(page_id={}, frame_watermark={:?}): min_frame={}, max_frame={}",
3461 page_id,
3462 frame_watermark,
3463 min_frame,
3464 max_frame
3465 );
3466 let frame = self
3467 .coordination
3468 .find_frame(page_id, min_frame, max_frame, frame_watermark);
3469 if let Some(frame) = frame {
3470 tracing::debug!(
3471 "find_frame(page_id={}, frame_watermark={:?}): found frame={}",
3472 page_id,
3473 frame_watermark,
3474 frame
3475 );
3476 }
3477 Ok(frame)
3478 }
3479
3480 #[instrument(skip_all, level = Level::DEBUG)]
3482 fn read_frame(
3483 &self,
3484 frame_id: u64,
3485 page: PageRef,
3486 buffer_pool: Arc<BufferPool>,
3487 ) -> Result<Completion> {
3488 tracing::debug!(
3489 "read_frame(page_idx = {}, frame_id = {})",
3490 page.get().id,
3491 frame_id
3492 );
3493 let offset = self.frame_offset(frame_id);
3494 page.set_locked();
3495 let frame = page.clone();
3496 let page_idx = page.get().id;
3497 let epoch_at_issue = self.coordination.checkpoint_epoch();
3498 let complete = Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3499 let Ok((buf, bytes_read)) = res else {
3500 tracing::debug!(err = ?res.unwrap_err());
3501 page.clear_locked();
3502 page.clear_wal_tag();
3503 return None; };
3505 let buf_len = buf.len();
3506 if bytes_read != buf_len as i32 {
3507 tracing::debug!(
3508 "WAL short read at offset {offset}, page {page_idx}, frame_id={frame_id}: expected {buf_len} bytes, got {bytes_read}"
3509 );
3510 page.clear_locked();
3511 page.clear_wal_tag();
3512 return Some(CompletionError::ShortReadWalFrame {
3513 offset,
3514 expected: buf_len,
3515 actual: bytes_read as usize,
3516 });
3517 }
3518 let cloned = frame.clone();
3519 finish_read_page(page.get().id, buf, cloned);
3520 frame.set_wal_tag(frame_id, epoch_at_issue);
3521 None
3522 });
3523 let file = self.coordination.wal_file()?;
3526 begin_read_wal_frame(
3527 file.as_ref(),
3528 offset + WAL_FRAME_HEADER_SIZE as u64,
3529 buffer_pool,
3530 complete,
3531 page_idx,
3532 &self.io_ctx.read(),
3533 )
3534 }
3535
3536 #[instrument(skip_all, level = Level::DEBUG)]
3537 fn read_frames_batch(
3538 &self,
3539 start_frame: u64,
3540 pages: &[PageRef],
3541 buffer_pool: Arc<BufferPool>,
3542 scratch_buf: Option<Arc<Buffer>>,
3543 ) -> Result<Completion> {
3544 turso_assert!(
3545 !pages.is_empty(),
3546 "read_frames_batch requires at least one page"
3547 );
3548 let page_size = self.page_size() as usize;
3549 turso_assert!(page_size > 0, "WAL page size must be initialized");
3550 let frame_size = WAL_FRAME_HEADER_SIZE + page_size;
3551 let count = pages.len();
3552 let total = frame_size * count;
3553 let offset = self.frame_offset(start_frame);
3554 if let Some(buf) = &scratch_buf {
3555 turso_assert!(
3556 buf.len() == total,
3557 "read_frames_batch scratch_buf size must match expected pread length",
3558 { "buf_len": buf.len(), "expected": total }
3559 );
3560 }
3561
3562 let mut slots: Vec<(PageRef, Arc<Buffer>)> = Vec::with_capacity(count);
3565 for page in pages.iter() {
3566 #[cfg(debug_assertions)]
3567 {
3568 turso_assert!(
3569 !page.is_locked(), "read_frames_batch target page must not already be locked",
3570 { "page_id": page.get().id }
3571 );
3572 turso_assert!(
3573 !page.is_loaded(), "read_frames_batch target page must be an unloaded scratch page",
3574 { "page_id": page.get().id }
3575 );
3576 turso_assert!(
3577 page.get().buffer.is_none(),
3578 "read_frames_batch target page must not already retain a buffer",
3579 { "page_id": page.get().id }
3580 );
3581 }
3582 page.set_locked();
3583 slots.push((page.clone(), Arc::new(buffer_pool.get_page())));
3584 }
3585
3586 let epoch = self.coordination.checkpoint_epoch();
3587 let enc_or_csum = self.io_ctx.read().encryption_or_checksum().clone();
3588 let raw_buf = scratch_buf.unwrap_or_else(|| Arc::new(Buffer::new_temporary(total)));
3589
3590 let complete = Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3591 let clear_slots_on_err = |slots: &[(PageRef, Arc<Buffer>)]| {
3592 for (page, _) in slots {
3593 page.clear_locked();
3594 page.clear_wal_tag();
3595 }
3596 };
3597
3598 let Ok((buf, bytes_read)) = res else {
3599 tracing::debug!(err = ?res.unwrap_err());
3600 clear_slots_on_err(&slots);
3601 return None;
3602 };
3603 if bytes_read != total as i32 {
3604 tracing::debug!(
3605 "short read on WAL batch at offset {offset}: expected {total} bytes, got {bytes_read}"
3606 );
3607 clear_slots_on_err(&slots);
3608 return Some(CompletionError::ShortReadWalFrame {
3609 offset,
3610 expected: total,
3611 actual: bytes_read as usize,
3612 });
3613 }
3614 let raw = buf.as_slice();
3615 for (i, (page, page_buf)) in slots.iter().enumerate() {
3616 let frame_start = i * frame_size;
3617 let frame = &raw[frame_start..frame_start + frame_size];
3618 let (header, page_body) = sqlite3_ondisk::parse_wal_frame_header(frame);
3619 let expected_page_id = page.get().id;
3620 if header.page_number as usize != expected_page_id {
3621 mark_unlikely();
3622 tracing::error!(
3623 frame_id = start_frame + i as u64,
3624 expected = expected_page_id,
3625 got = header.page_number,
3626 "WAL batch frame page_no mismatch"
3627 );
3628 clear_slots_on_err(&slots);
3629 return Some(CompletionError::WalFramePageMismatch {
3630 frame_id: start_frame + i as u64,
3631 expected: expected_page_id,
3632 actual: header.page_number,
3633 });
3634 }
3635
3636 let body_slice = page_buf.as_mut_slice();
3637 turso_assert!(
3638 body_slice.len() == page_size,
3639 "read_frames_batch buffer size must match WAL page size",
3640 { "buffer_len": body_slice.len(), "page_size": page_size }
3641 );
3642 body_slice.copy_from_slice(page_body);
3643
3644 match &enc_or_csum {
3645 EncryptionOrChecksum::Encryption(ctx) => {
3646 match ctx.decrypt_page(body_slice, expected_page_id) {
3647 Ok(decrypted) => body_slice.copy_from_slice(&decrypted),
3648 Err(e) => {
3649 mark_unlikely();
3650 tracing::error!(
3651 "Failed to decrypt WAL batch frame for page_idx={expected_page_id}: {e}"
3652 );
3653 clear_slots_on_err(&slots);
3654 return Some(CompletionError::DecryptionError {
3655 page_idx: expected_page_id,
3656 });
3657 }
3658 }
3659 }
3660 EncryptionOrChecksum::Checksum(ctx) => {
3661 if let Err(e) = ctx.verify_checksum(body_slice, expected_page_id) {
3662 mark_unlikely();
3663 tracing::error!(
3664 "Failed to verify checksum for page_id={expected_page_id}: {e}"
3665 );
3666 clear_slots_on_err(&slots);
3667 return Some(e);
3668 }
3669 }
3670 EncryptionOrChecksum::None => {}
3671 }
3672 }
3673
3674 for (i, (page, page_buf)) in slots.iter().enumerate() {
3675 let page_id = page.get().id;
3676 finish_read_page(page_id, page_buf.clone(), page.clone());
3677 page.set_wal_tag(start_frame + i as u64, epoch);
3678 }
3679 None
3680 });
3681
3682 let c = Completion::new_read(raw_buf, complete);
3683 let file = self.coordination.wal_file()?;
3684 file.pread(offset, c)
3685 }
3686
3687 #[instrument(skip_all, level = Level::DEBUG)]
3688 fn read_frame_raw(&self, frame_id: u64, frame: &mut [u8]) -> Result<Completion> {
3691 tracing::debug!("read_frame_raw({})", frame_id);
3692 let offset = self.frame_offset(frame_id);
3693
3694 let (frame_ptr, frame_len) = (frame.as_mut_ptr() as usize, frame.len());
3697
3698 let encryption_ctx = {
3699 let io_ctx = self.io_ctx.read();
3700 io_ctx.encryption_context().cloned()
3701 };
3702 let complete = Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3703 let Ok((buf, bytes_read)) = res else {
3704 return None; };
3706 let buf_len = buf.len();
3707 if bytes_read != buf_len as i32 {
3708 tracing::debug!(
3709 "short read on WAL frame {frame_id} at offset {offset}: expected {buf_len} bytes, got {bytes_read}"
3710 );
3711 return Some(CompletionError::ShortReadWalFrame {
3712 offset,
3713 expected: buf_len,
3714 actual: bytes_read as usize,
3715 });
3716 }
3717 let buf_ptr = buf.as_ptr();
3718 let frame_ptr = frame_ptr as *mut u8;
3719 let frame_ref: &mut [u8] =
3720 unsafe { std::slice::from_raw_parts_mut(frame_ptr, frame_len) };
3721
3722 unsafe {
3724 std::ptr::copy_nonoverlapping(buf_ptr, frame_ptr, frame_len);
3725 }
3726
3727 let (header, raw_page) = sqlite3_ondisk::parse_wal_frame_header(frame_ref);
3729
3730 if let Some(ctx) = encryption_ctx.clone() {
3731 match ctx.decrypt_page(raw_page, header.page_number as usize) {
3732 Ok(decrypted_data) => {
3733 turso_assert!(
3734 (frame_len - WAL_FRAME_HEADER_SIZE) == decrypted_data.len(),
3735 "frame_len minus header_size does not equal expected decrypted data length",
3736 { "frame_len_minus_header": frame_len - WAL_FRAME_HEADER_SIZE, "decrypted_data_len": decrypted_data.len() }
3737 );
3738 frame_ref[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&decrypted_data);
3739 }
3740 Err(_) => {
3741 tracing::debug!("Failed to decrypt page data for frame_id={frame_id}");
3742 }
3743 }
3744 }
3745 None
3746 });
3747 let file = self.coordination.wal_file()?;
3748 let c = begin_read_wal_frame_raw(&self.buffer_pool, file.as_ref(), offset, complete)?;
3749 Ok(c)
3750 }
3751
3752 #[instrument(skip_all, level = Level::DEBUG)]
3753 fn write_frame_raw(
3756 &self,
3757 buffer_pool: Arc<BufferPool>,
3758 frame_id: u64,
3759 page_id: u64,
3760 db_size: u64,
3761 page: &[u8],
3762 sync_type: FileSyncType,
3763 ) -> Result<()> {
3764 let Some(page_size) = PageSize::new(page.len() as u32) else {
3765 bail_corrupt_error!("invalid page size: {}", page.len());
3766 };
3767 self.ensure_header_if_needed(page_size, sync_type)?;
3768 tracing::debug!("write_raw_frame({})", frame_id);
3769 if self.page_size() != 0 && page.len() != self.page_size() as usize {
3771 return Err(LimboError::InvalidArgument(format!(
3772 "unexpected page size in frame: got={}, expected={}",
3773 page.len(),
3774 self.page_size(),
3775 )));
3776 }
3777 if frame_id > self.max_frame.load(Ordering::Acquire) + 1 {
3778 return Err(LimboError::InvalidArgument(format!(
3780 "frame_id is beyond next frame in the WAL: frame_id={}, max_frame={}",
3781 frame_id,
3782 self.max_frame.load(Ordering::Acquire)
3783 )));
3784 }
3785 if frame_id <= self.max_frame.load(Ordering::Acquire) {
3786 let offset = self.frame_offset(frame_id);
3788 let conflict = Arc::new(Mutex::new(false));
3789
3790 let (page_ptr, page_len) = (page.as_ptr() as usize, page.len());
3793
3794 let complete = Box::new({
3795 let conflict = conflict.clone();
3796 move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3797 let Ok((buf, bytes_read)) = res else {
3798 return None; };
3800 let buf_len = buf.len();
3801 if bytes_read != buf_len as i32 {
3802 tracing::debug!(
3803 "short read on WAL frame validation at offset {offset}, page_id={page_id}: expected {buf_len} bytes, got {bytes_read}"
3804 );
3805 return Some(CompletionError::ShortReadWalFrame {
3806 offset,
3807 expected: buf_len,
3808 actual: bytes_read as usize,
3809 });
3810 }
3811 let page = unsafe { std::slice::from_raw_parts(page_ptr as *mut u8, page_len) };
3812 if buf.as_slice() != page {
3813 *conflict.lock() = true;
3814 }
3815 None
3816 }
3817 });
3818 let file = self.coordination.wal_file()?;
3819 let c = begin_read_wal_frame(
3820 file.as_ref(),
3821 offset + WAL_FRAME_HEADER_SIZE as u64,
3822 buffer_pool,
3823 complete,
3824 page_id as usize,
3825 &self.io_ctx.read(),
3826 )?;
3827 self.io.wait_for_completion(c)?;
3828 return if *conflict.lock() {
3829 Err(LimboError::Conflict(format!(
3830 "frame content differs from the WAL: frame_id={frame_id}"
3831 )))
3832 } else {
3833 Ok(())
3834 };
3835 }
3836
3837 let offset = self.frame_offset(frame_id);
3839 let header = self.coordination.wal_header();
3840 let file = self.coordination.wal_file()?;
3841 let checksums = *self.last_checksum.read();
3842 let (checksums, frame_bytes) = prepare_wal_frame(
3843 &self.buffer_pool,
3844 &header,
3845 checksums,
3846 header.page_size,
3847 page_id as u32,
3848 db_size as u32,
3849 page,
3850 );
3851 let c = Completion::new_write(|_| {});
3852 let c = file.pwrite(offset, frame_bytes, c)?;
3853 self.io.wait_for_completion(c)?;
3854 self.complete_append_frame(page_id, frame_id, checksums);
3855 if db_size > 0 {
3856 self.finish_append_frames_commit()?;
3857 }
3858 Ok(())
3859 }
3860
3861 #[instrument(skip_all, level = Level::DEBUG)]
3862 fn should_checkpoint(&self) -> bool {
3863 let snapshot = self.load_coordination_snapshot();
3864 snapshot.max_frame as usize > self.checkpoint_threshold + snapshot.nbackfills as usize
3865 }
3866
3867 #[instrument(skip_all, level = Level::DEBUG)]
3868 fn checkpoint(
3869 &self,
3870 pager: &Pager,
3871 mode: CheckpointMode,
3872 ) -> Result<IOResult<CheckpointResult>> {
3873 self.checkpoint_inner(pager, mode, CheckpointLockSource::Acquire)
3874 .inspect_err(|e| {
3875 tracing::debug!("WAL checkpoint failed: {e}");
3876 let _ = self.checkpoint_guard.write().take();
3877 self.ongoing_checkpoint.write().state = CheckpointState::Start;
3878 })
3879 }
3880
3881 fn vacuum_checkpoint_with_held_lock(
3882 &self,
3883 pager: &Pager,
3884 ) -> Result<IOResult<CheckpointResult>> {
3885 self.checkpoint_inner(
3886 pager,
3887 CheckpointMode::Truncate {
3888 upper_bound_inclusive: None,
3889 },
3890 CheckpointLockSource::HeldByCaller,
3891 )
3892 .inspect_err(|e| {
3893 tracing::debug!("WAL checkpoint failed: {e}");
3894 let _ = self.checkpoint_guard.write().take();
3895 self.ongoing_checkpoint.write().state = CheckpointState::Start;
3896 })
3897 }
3898
3899 fn install_durable_backfill_proof(
3900 &self,
3901 max_frame: u64,
3902 db_size_pages: u32,
3903 db_header_crc32c: u32,
3904 sync_type: FileSyncType,
3905 ) -> Result<Option<Completion>> {
3906 self.coordination.install_durable_backfill_proof(
3907 max_frame,
3908 db_size_pages,
3909 db_header_crc32c,
3910 sync_type,
3911 )
3912 }
3913
3914 fn publish_backfill(&self, max_frame: u64) {
3915 let snapshot = self.load_coordination_snapshot();
3916 turso_assert!(
3917 (snapshot.nbackfills..=snapshot.max_frame).contains(&max_frame),
3918 "published backfill must stay within the current WAL generation",
3919 {
3920 "publish_backfill": max_frame,
3921 "current_nbackfills": snapshot.nbackfills,
3922 "current_max_frame": snapshot.max_frame
3923 }
3924 );
3925 self.coordination.publish_backfill(max_frame);
3926 }
3927
3928 #[instrument(err, skip_all, level = Level::DEBUG)]
3929 fn sync(&self, sync_type: FileSyncType) -> Result<Completion> {
3930 tracing::debug!("wal_sync");
3931 let syncing = self.syncing.clone();
3932 let dirty = self.dirty.clone();
3933 let completion = Completion::new_sync(move |result| {
3934 tracing::debug!("wal_sync finish");
3935 if let Err(err) = result {
3936 tracing::debug!("wal_sync failed: {err}");
3937 } else {
3938 dirty.store(false, Ordering::Release);
3939 }
3940 syncing.store(false, Ordering::Release);
3941 });
3942 let file = self.coordination.wal_file()?;
3943 self.syncing.store(true, Ordering::Release);
3944 let c = file.sync(completion, sync_type)?;
3945 Ok(c)
3946 }
3947
3948 fn is_syncing(&self) -> bool {
3950 self.syncing.load(Ordering::Acquire)
3951 }
3952
3953 fn is_dirty(&self) -> bool {
3954 self.dirty.load(Ordering::Acquire)
3955 }
3956
3957 fn get_max_frame_in_wal(&self) -> u64 {
3958 self.load_coordination_snapshot().max_frame
3959 }
3960
3961 fn get_checkpoint_seq(&self) -> u32 {
3962 self.load_coordination_snapshot().checkpoint_seq
3963 }
3964
3965 fn get_max_frame(&self) -> u64 {
3966 self.max_frame.load(Ordering::Acquire)
3967 }
3968
3969 fn connection_wal_pos(&self) -> (u32, u64) {
3970 (
3971 self.checkpoint_seq.load(Ordering::Acquire),
3972 self.max_frame.load(Ordering::Acquire),
3973 )
3974 }
3975
3976 fn min_pinned_read_frame(&self) -> Option<u64> {
3977 self.coordination.min_pinned_read_frame()
3978 }
3979
3980 fn get_min_frame(&self) -> u64 {
3981 self.min_frame.load(Ordering::Acquire)
3982 }
3983
3984 fn backfill_frame(&self) -> u64 {
3985 self.load_coordination_snapshot().nbackfills
3986 }
3987
3988 fn get_last_checksum(&self) -> (u32, u32) {
3989 *self.last_checksum.read()
3990 }
3991 #[instrument(skip_all, level = Level::DEBUG)]
3992
3993 fn rollback(&self, rollback_to: Option<RollbackTo>) {
3994 let is_savepoint = rollback_to.is_some();
3995 let snapshot = self.load_coordination_snapshot();
3996 if let Some(r) = &rollback_to {
3997 turso_assert!(
4006 r.checkpoint_seq == snapshot.checkpoint_seq,
4007 "savepoint WAL position must be from the current WAL generation",
4008 {
4009 "savepoint_checkpoint_seq": r.checkpoint_seq,
4010 "authority_checkpoint_seq": snapshot.checkpoint_seq,
4011 "savepoint_frame": r.frame,
4012 "authority_max_frame": snapshot.max_frame
4013 }
4014 );
4015 turso_assert!(
4018 r.frame >= snapshot.max_frame,
4019 "savepoint WAL position must not be behind the committed high-water mark",
4020 { "savepoint_frame": r.frame, "authority_max_frame": snapshot.max_frame }
4021 );
4022 }
4023 let max_frame = rollback_to
4028 .as_ref()
4029 .map(|r| r.frame)
4030 .unwrap_or(snapshot.max_frame);
4031 let last_checksum = rollback_to
4032 .as_ref()
4033 .map(|r| r.checksum)
4034 .unwrap_or(snapshot.last_checksum);
4035 self.coordination.rollback_cache(max_frame);
4036 *self.last_checksum.write() = last_checksum;
4037 self.max_frame.store(max_frame, Ordering::Release);
4038 if !is_savepoint {
4039 self.reset_internal_states();
4040 }
4041 }
4042
4043 fn abort_checkpoint(&self) {
4044 let _ = self.checkpoint_guard.write().take();
4045 self.reset_internal_states();
4046 }
4047
4048 fn try_begin_vacuum_checkpoint_lock(&self) -> Result<()> {
4049 self.with_shared(|shared| {
4050 if !shared.runtime.checkpoint_lock.write() {
4051 return Err(LimboError::Busy);
4052 }
4053 Ok(())
4054 })
4055 }
4056
4057 fn release_vacuum_checkpoint_lock(&self) {
4058 self.with_shared(|shared| {
4059 shared.runtime.checkpoint_lock.unlock();
4060 });
4061 }
4062
4063 fn begin_vacuum_blocking_tx(&self) -> Result<()> {
4064 turso_assert!(
4065 self.max_frame_read_lock_index.load(Ordering::Acquire) == NO_LOCK_HELD,
4066 "begin_vacuum_blocking_tx: must not already hold a read lock"
4067 );
4068 turso_assert!(
4069 !self.holds_write_lock(),
4070 "begin_vacuum_blocking_tx: must not already hold the write lock"
4071 );
4072 turso_assert!(
4073 self.vacuum_lock_guard.read().is_none(),
4074 "VACUUM lock guard already held"
4075 );
4076
4077 let Some(vacuum_lock_guard) =
4078 VacuumLockGuard::try_write(self.coordination.shared_wal_state())
4079 else {
4080 return Err(LimboError::Busy);
4081 };
4082
4083 self.with_shared(|shared| {
4086 for idx in 0..shared.runtime.read_locks.len() {
4087 turso_assert!(
4090 shared.runtime.read_locks[idx].write(),
4091 "begin_vacuum_blocking_tx: read lock held after VACUUM lock acquired",
4092 { "read_lock_idx": idx }
4093 );
4094 shared.runtime.read_locks[idx].unlock();
4095 }
4096 });
4097
4098 let snapshot = self.load_coordination_snapshot();
4100 self.install_connection_state(WalConnectionState::new(snapshot, ReadGuardKind::None));
4101 turso_assert!(
4102 self.with_shared(|shared| shared.runtime.write_lock.write()),
4103 "begin_vacuum_blocking_tx: write lock held after VACUUM lock acquired"
4104 );
4105 if self
4106 .write_lock_held
4107 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
4108 .is_err()
4109 {
4110 turso_assert!(
4111 false,
4112 "begin_vacuum_blocking_tx: write_lock_held already set"
4113 );
4114 }
4115 self.install_vacuum_lock_guard(vacuum_lock_guard);
4116 Ok(())
4117 }
4118
4119 fn release_vacuum_lock(&self) {
4120 turso_assert!(
4123 !self.holds_write_lock(),
4124 "release_vacuum_lock called while source write lock is still held"
4125 );
4126 self.release_vacuum_write_lock_guard();
4127 }
4128
4129 #[instrument(skip_all, level = Level::DEBUG)]
4130 fn finish_append_frames_commit(&self) -> Result<()> {
4131 let max_frame = self.max_frame.load(Ordering::Acquire);
4132 let last_checksum = *self.last_checksum.read();
4133 tracing::trace!(max_frame, ?last_checksum);
4134 let transaction_count = self.transaction_count.fetch_add(1, Ordering::AcqRel) + 1;
4135 self.coordination.publish_commit(WalCommitState {
4136 max_frame,
4137 last_checksum,
4138 transaction_count,
4139 });
4140 Ok(())
4141 }
4142
4143 fn changed_pages_after(&self, frame_watermark: u64) -> Result<Vec<u32>> {
4144 let frame_count = self.get_max_frame();
4145 let page_size = self.page_size();
4146 let mut frame = vec![0u8; page_size as usize + WAL_FRAME_HEADER_SIZE];
4147 let mut seen = FxHashSet::default();
4148 turso_assert!(
4149 frame_count >= frame_watermark,
4150 "frame_count must be not less than frame_watermark",
4151 { "frame_count": frame_count, "frame_watermark": frame_watermark }
4152 );
4153 let mut pages = Vec::with_capacity((frame_count - frame_watermark) as usize);
4154 for frame_no in frame_watermark + 1..=frame_count {
4155 let c = self.read_frame_raw(frame_no, &mut frame)?;
4156 self.io.wait_for_completion(c)?;
4157 let (header, _) = sqlite3_ondisk::parse_wal_frame_header(&frame);
4158 if seen.insert(header.page_number) {
4159 pages.push(header.page_number);
4160 }
4161 }
4162 Ok(pages)
4163 }
4164
4165 fn prepare_wal_start(&self, page_size: PageSize) -> Result<Option<Completion>> {
4166 if self.coordination.wal_is_initialized() {
4167 return Ok(None);
4168 }
4169 tracing::debug!("ensure_header_if_needed");
4170 let Some(header) = self
4171 .coordination
4172 .prepare_wal_header(self.io.as_ref(), page_size)
4173 else {
4174 return Ok(None);
4175 };
4176 *self.last_checksum.write() = (header.checksum_1, header.checksum_2);
4177
4178 self.max_frame.store(0, Ordering::Release);
4179 let file = self.coordination.wal_file()?;
4180 let header_c = sqlite3_ondisk::begin_write_wal_header(file.as_ref(), &header)?;
4181
4182 let should_skip_truncate = match file.size() {
4188 Ok(size) => size <= WAL_HEADER_SIZE as u64,
4189 Err(_) => {
4190 tracing::warn!("Failed to get WAL file size");
4191 true
4192 }
4193 };
4194 if !should_skip_truncate {
4195 let trunc_c = file.truncate(
4196 WAL_HEADER_SIZE as u64,
4197 Completion::new_trunc(|res| {
4198 if let Err(err) = res {
4199 tracing::warn!("WAL truncate of orphaned frames failed: {err}");
4200 }
4201 }),
4202 )?;
4203 let mut group = CompletionGroup::new(|_| {});
4204 group.add(&header_c);
4205 group.add(&trunc_c);
4206 Ok(Some(group.build()))
4207 } else {
4208 Ok(Some(header_c))
4209 }
4210 }
4211
4212 #[aristo::intent(
4213 "The WAL initialized flag is set true only after a successful sync of the wal-header\n",
4214 id = "aristos:wal_initialized_reflects_sync_outcome",
4215 verify = "full",
4216 parent = "wal_protocol_correctness"
4217 )]
4218 fn prepare_wal_finish(&self, sync_type: FileSyncType) -> Result<Completion> {
4219 let file = self.coordination.wal_file()?;
4220 let coordination = self.coordination.clone();
4221 let c = file.sync(
4222 Completion::new_sync(move |res| {
4223 if res.is_ok() {
4228 coordination.mark_initialized();
4229 }
4230 }),
4231 sync_type,
4232 )?;
4233 Ok(c)
4234 }
4235
4236 fn prepare_frames(
4248 &self,
4249 pages: &[PageRef],
4250 page_sz: PageSize,
4251 db_size_on_commit: Option<u32>,
4252 prev: Option<&PreparedFrames>,
4253 ) -> Result<PreparedFrames> {
4254 turso_assert!(
4255 !pages.is_empty(),
4256 "prepare_frames requires at least one page"
4257 );
4258 turso_assert!(
4259 pages.len() <= IOV_MAX,
4260 "supported up to IOV_MAX pages at once"
4261 );
4262 turso_assert!(
4263 self.coordination.wal_is_initialized(),
4264 "WAL must be initialized"
4265 );
4266
4267 let header = self.coordination.wal_header();
4268 let epoch = self.coordination.checkpoint_epoch();
4269
4270 turso_assert!(
4271 header.page_size == page_sz.get(),
4272 "page size mismatch between header and requested",
4273 { "header_page_size": header.page_size, "requested_page_size": page_sz.get() }
4274 );
4275
4276 let (mut rolling_checksum, mut next_frame_id) = match prev {
4280 Some(p) => (p.final_checksum, p.final_max_frame + 1),
4281 None => {
4282 let snapshot = self.load_coordination_snapshot();
4283 let local_state = self.connection_state();
4284 if local_state.snapshot.max_frame > snapshot.max_frame {
4285 (
4290 local_state.snapshot.last_checksum,
4291 local_state.snapshot.max_frame + 1,
4292 )
4293 } else {
4294 turso_assert!(
4299 local_state.snapshot.max_frame == snapshot.max_frame,
4300 "connection WAL position must not be behind the committed high-water mark",
4301 {
4302 "local_max_frame": local_state.snapshot.max_frame,
4303 "authority_max_frame": snapshot.max_frame
4304 }
4305 );
4306 if snapshot != local_state.snapshot {
4311 self.install_connection_state(local_state.with_snapshot(snapshot));
4312 }
4313 (snapshot.last_checksum, snapshot.max_frame + 1)
4314 }
4315 }
4316 };
4317
4318 if next_frame_id == 1 {
4326 rolling_checksum = (header.checksum_1, header.checksum_2);
4327 }
4328
4329 let first_frame_id = next_frame_id;
4330
4331 let mut bufs: Vec<Arc<Buffer>> = Vec::with_capacity(pages.len());
4332 let mut metadata = Vec::with_capacity(pages.len());
4333
4334 for (idx, page) in pages.iter().enumerate() {
4335 let page_id = page.get().id;
4336 let plain = page.get_contents().as_ptr();
4337
4338 let data: Cow<[u8]> = {
4339 let io_ctx = self.io_ctx.read();
4340 match io_ctx.encryption_or_checksum() {
4341 EncryptionOrChecksum::Encryption(ctx) => {
4342 Cow::Owned(ctx.encrypt_page(plain, page_id)?)
4343 }
4344 EncryptionOrChecksum::Checksum(ctx) => {
4345 ctx.add_checksum_to_page(plain, page_id)?;
4346 Cow::Borrowed(plain)
4347 }
4348 EncryptionOrChecksum::None => Cow::Borrowed(plain),
4349 }
4350 };
4351
4352 let frame_db_size = if idx + 1 == pages.len() {
4355 db_size_on_commit.unwrap_or(0)
4356 } else {
4357 0
4358 };
4359 let (checksum, frame_buf) = prepare_wal_frame(
4360 &self.buffer_pool,
4361 &header,
4362 rolling_checksum,
4363 header.page_size,
4364 page_id as u32,
4365 frame_db_size,
4366 &data,
4367 );
4368 bufs.push(frame_buf);
4369 metadata.push((page.clone(), next_frame_id, checksum));
4370 rolling_checksum = checksum;
4371 next_frame_id += 1;
4372 }
4373 let offset = self.frame_offset(first_frame_id);
4374 Ok(PreparedFrames {
4375 offset,
4376 bufs,
4377 metadata,
4378 final_checksum: rolling_checksum,
4379 final_max_frame: next_frame_id - 1,
4380 epoch,
4381 })
4382 }
4383
4384 fn commit_prepared_frames(&self, batches: &[PreparedFrames]) {
4387 for batch in batches {
4388 for (page, frame_id, checksum) in &batch.metadata {
4389 self.complete_append_frame(page.get().id as u64, *frame_id, *checksum);
4391 }
4392 *self.last_checksum.write() = batch.final_checksum;
4394 self.max_frame
4396 .store(batch.final_max_frame, Ordering::Release);
4397 }
4398 }
4399
4400 fn finalize_committed_pages(&self, prepared: &[PreparedFrames]) {
4402 for batch in prepared {
4403 for (page, frame_id, _) in &batch.metadata {
4404 page.clear_dirty();
4405 page.set_wal_tag(*frame_id, batch.epoch);
4406 }
4407 }
4408 }
4409
4410 fn wal_file(&self) -> Result<Arc<dyn File>> {
4412 self.coordination.wal_file()
4413 }
4414
4415 fn append_frames_vectored(&self, pages: Vec<PageRef>, page_sz: PageSize) -> Result<Completion> {
4422 turso_assert!(
4423 pages.len() <= IOV_MAX,
4424 "we limit number of iovecs to IOV_MAX"
4425 );
4426 turso_assert!(
4427 self.coordination.wal_is_initialized(),
4428 "WAL must be prepared with prepare_wal_start/prepare_wal_finish method"
4429 );
4430
4431 let header = self.coordination.wal_header();
4432 let shared_page_size = header.page_size;
4433 let epoch = self.coordination.checkpoint_epoch();
4434 turso_assert!(
4435 shared_page_size == page_sz.get(),
4436 "page size mismatch, tried to change page size after WAL header was already initialized",
4437 { "shared_page_size": shared_page_size, "page_size": page_sz.get() }
4438 );
4439
4440 let mut iovecs: Vec<Arc<Buffer>> = Vec::with_capacity(pages.len());
4442 let mut page_frame_and_checksum: Vec<(PageRef, u64, (u32, u32))> =
4443 Vec::with_capacity(pages.len());
4444
4445 let mut rolling_checksum: (u32, u32) = *self.last_checksum.read();
4447
4448 let mut next_frame_id = self.max_frame.load(Ordering::Acquire) + 1;
4449 for page in pages.iter() {
4451 tracing::debug!("append_frames_vectored: page_id={}", page.get().id);
4452 let page_id = page.get().id;
4453 let plain = page.get_contents().as_ptr();
4454
4455 let data_to_write: std::borrow::Cow<[u8]> = {
4456 let io_ctx = self.io_ctx.read();
4457 match &io_ctx.encryption_or_checksum() {
4458 EncryptionOrChecksum::Encryption(ctx) => {
4459 Cow::Owned(ctx.encrypt_page(plain, page_id)?)
4460 }
4461 EncryptionOrChecksum::Checksum(ctx) => {
4462 ctx.add_checksum_to_page(plain, page_id)?;
4463 Cow::Borrowed(plain)
4464 }
4465 EncryptionOrChecksum::None => Cow::Borrowed(plain),
4466 }
4467 };
4468
4469 let frame_db_size = 0; let (new_checksum, frame_bytes) = prepare_wal_frame(
4471 &self.buffer_pool,
4472 &header,
4473 rolling_checksum,
4474 shared_page_size,
4475 page_id as u32,
4476 frame_db_size,
4477 &data_to_write,
4478 );
4479 iovecs.push(frame_bytes);
4480
4481 page_frame_and_checksum.push((page.clone(), next_frame_id, new_checksum));
4483
4484 rolling_checksum = new_checksum;
4486 next_frame_id += 1;
4487 }
4488
4489 let first_frame_id = self.max_frame.load(Ordering::Acquire) + 1;
4490 let start_off = self.frame_offset(first_frame_id);
4491
4492 let total_len: i32 = iovecs.iter().map(|b| b.len() as i32).sum();
4494 let page_frame_for_cb = page_frame_and_checksum.clone();
4495 let coordination = self.coordination.clone();
4503 let on_complete = move |res: Result<i32, CompletionError>| {
4504 let Ok(bytes_written) = res else {
4505 return;
4506 };
4507 turso_assert!(
4508 bytes_written == total_len,
4509 "pwritev wrote unexpected number of bytes",
4510 { "bytes_written": bytes_written, "expected": total_len }
4511 );
4512
4513 for (page, fid, _csum) in &page_frame_for_cb {
4514 page.set_wal_tag(*fid, epoch);
4515 coordination.cache_frame(page.get().id as u64, *fid);
4516 }
4517 };
4518
4519 let c = Completion::new_write(on_complete);
4520
4521 let file = self.coordination.wal_file()?;
4522 let c = file.pwritev(start_off, iovecs, c)?;
4523
4524 if let Some((_, last_frame_id, last_checksum)) = page_frame_and_checksum.last() {
4544 self.dirty.store(true, Ordering::Release);
4545 *self.last_checksum.write() = *last_checksum;
4546 self.max_frame.store(*last_frame_id, Ordering::Release);
4547 }
4548
4549 Ok(c)
4550 }
4551
4552 #[cfg(any(clt_turso_tests, debug_assertions))]
4553 fn as_any(&self) -> &dyn std::any::Any {
4554 self
4555 }
4556
4557 fn set_io_context(&self, ctx: IOContext) {
4558 *self.io_ctx.write() = ctx;
4559 }
4560
4561 fn update_max_frame(&self) {
4562 let new_max_frame = self.load_coordination_snapshot().max_frame;
4563 self.max_frame.store(new_max_frame, Ordering::Release);
4564 }
4565
4566 fn truncate_wal(
4567 &self,
4568 result: &mut CheckpointResult,
4569 sync_type: FileSyncType,
4570 ) -> Result<IOResult<()>> {
4571 self.truncate_log(result, sync_type)
4572 }
4573}
4574
4575impl WalFile {
4576 #[cfg(host_shared_wal)]
4577 pub(crate) fn new_with_shared_coordination(
4578 io: Arc<dyn IO>,
4579 shared: Arc<RwLock<WalFileShared>>,
4580 authority: Arc<MappedSharedWalCoordination>,
4581 _last_checksum_and_max_frame: ((u32, u32), u64),
4582 buffer_pool: Arc<BufferPool>,
4583 ) -> Self {
4584 let coordination: Arc<dyn WalCoordination> =
4585 Arc::new(ShmWalCoordination::new(shared, authority));
4586 let snapshot = coordination.load_snapshot();
4587 Self::new_with_coordination(
4588 io,
4589 coordination,
4590 (snapshot.last_checksum, snapshot.max_frame),
4591 buffer_pool,
4592 )
4593 }
4594
4595 pub fn new(
4596 io: Arc<dyn IO>,
4597 shared: Arc<RwLock<WalFileShared>>,
4598 (last_checksum, max_frame): ((u32, u32), u64),
4599 buffer_pool: Arc<BufferPool>,
4600 ) -> Self {
4601 let coordination: Arc<dyn WalCoordination> =
4602 Arc::new(InProcessWalCoordination::new(shared));
4603 Self::new_with_coordination(io, coordination, (last_checksum, max_frame), buffer_pool)
4604 }
4605
4606 fn new_with_coordination(
4608 io: Arc<dyn IO>,
4609 coordination: Arc<dyn WalCoordination>,
4610 (last_checksum, max_frame): ((u32, u32), u64),
4611 buffer_pool: Arc<BufferPool>,
4612 ) -> Self {
4613 let now = io.current_time_monotonic();
4614 Self {
4615 io,
4616 coordination,
4617 max_frame: AtomicU64::new(max_frame),
4619 ongoing_checkpoint: RwLock::new(OngoingCheckpoint {
4620 time: now,
4621 pending_writes: WriteBatch::new(),
4622 inflight_writes: Vec::new(),
4623 state: CheckpointState::Start,
4624 min_frame: 0,
4625 max_frame: 0,
4626 current_page: 0,
4627 pages_to_checkpoint: Vec::new(),
4628 inflight_reads: Vec::with_capacity(MAX_INFLIGHT_READS),
4629 }),
4630 checkpoint_threshold: 1000,
4631 buffer_pool,
4632 checkpoint_seq: AtomicU32::new(0),
4633 syncing: Arc::new(AtomicBool::new(false)),
4634 write_lock_held: AtomicBool::new(false),
4635 vacuum_lock_guard: RwLock::new(None),
4636 min_frame: AtomicU64::new(0),
4637 transaction_count: AtomicU64::new(0),
4638 max_frame_read_lock_index: AtomicUsize::new(NO_LOCK_HELD),
4639 last_checksum: RwLock::new(last_checksum),
4640 checkpoint_guard: RwLock::new(None),
4641 io_ctx: RwLock::new(IOContext::default()),
4642 dirty: Arc::new(AtomicBool::new(false)),
4643 }
4644 }
4645
4646 #[cfg(clt_turso_tests)]
4647 pub(crate) fn shared_ptr(&self) -> usize {
4648 self.coordination.shared_ptr()
4649 }
4650
4651 #[cfg(clt_turso_tests)]
4652 pub(crate) fn coordination_backend_name(&self) -> &'static str {
4653 self.coordination.backend_name()
4654 }
4655
4656 #[cfg(clt_turso_tests)]
4657 pub(crate) fn coordination_open_mode_name(&self) -> Option<&'static str> {
4658 self.coordination.open_mode_name()
4659 }
4660
4661 fn with_shared<F, R>(&self, func: F) -> R
4662 where
4663 F: FnOnce(&WalFileShared) -> R,
4664 {
4665 let shared = self.coordination.shared_wal_state();
4666 let guard = shared.read();
4667 func(&guard)
4668 }
4669
4670 fn page_size(&self) -> u32 {
4671 self.coordination.wal_header().page_size
4672 }
4673
4674 fn frame_offset(&self, frame_id: u64) -> u64 {
4675 turso_assert_greater_than!(frame_id, 0, "Frame ID must be 1-based");
4676 let page_offset = (frame_id - 1) * (self.page_size() + WAL_FRAME_HEADER_SIZE as u32) as u64;
4677 WAL_HEADER_SIZE as u64 + page_offset
4678 }
4679
4680 fn increment_checkpoint_epoch(&self) {
4681 let prev = self.coordination.bump_checkpoint_epoch();
4682 tracing::debug!("increment checkpoint epoch: prev={}", prev);
4683 }
4684
4685 fn complete_append_frame(&self, page_id: u64, frame_id: u64, checksums: (u32, u32)) {
4686 self.dirty.store(true, Ordering::Release);
4687 *self.last_checksum.write() = checksums;
4688 self.max_frame.store(frame_id, Ordering::Release);
4689 self.coordination.cache_frame(page_id, frame_id);
4690 }
4691
4692 fn reset_internal_states(&self) {
4694 self.ongoing_checkpoint.write().reset();
4695 self.syncing.store(false, Ordering::Release);
4696 }
4697
4698 fn ensure_header_if_needed(&self, page_size: PageSize, sync_type: FileSyncType) -> Result<()> {
4701 let Some(c) = self.prepare_wal_start(page_size)? else {
4702 return Ok(());
4703 };
4704 self.io.wait_for_completion(c)?;
4705 let c = self.prepare_wal_finish(sync_type)?;
4706 self.io.wait_for_completion(c)?;
4707 Ok(())
4708 }
4709
4710 fn checkpoint_inner(
4711 &self,
4712 pager: &Pager,
4713 mode: CheckpointMode,
4714 lock_source: CheckpointLockSource,
4715 ) -> Result<IOResult<CheckpointResult>> {
4716 loop {
4717 let state = self.ongoing_checkpoint.read().state.clone();
4718 tracing::debug!(?state);
4719 match state {
4720 CheckpointState::Start => {
4724 let snapshot = self.load_coordination_snapshot();
4725 let max_frame = snapshot.max_frame;
4726 let nbackfills = snapshot.nbackfills;
4727 tracing::debug!("shared_wal: max_frame={max_frame}, nbackfills={nbackfills}");
4728 let needs_backfill = max_frame > nbackfills;
4729 if matches!(lock_source, CheckpointLockSource::HeldByCaller) {
4730 turso_assert!(
4731 needs_backfill,
4732 "held checkpoint-lock path requires WAL frames to backfill",
4733 { "max_frame": max_frame, "nbackfills": nbackfills }
4734 );
4735 }
4736 if !needs_backfill && !mode.should_restart_log() {
4737 return Ok(IOResult::Done(CheckpointResult::new(
4740 max_frame, nbackfills, 0,
4741 )));
4742 }
4743 self.acquire_proper_checkpoint_guard(mode, lock_source)?;
4745 let mut max_frame = self.determine_max_safe_checkpoint_frame();
4746
4747 if let CheckpointMode::Truncate {
4748 upper_bound_inclusive: Some(upper_bound),
4749 } = mode
4750 {
4751 if max_frame > upper_bound {
4752 tracing::debug!(
4753 "abort checkpoint because latest frame in WAL is greater than upper_bound in TRUNCATE mode: {max_frame} != {upper_bound}"
4754 );
4755 return Err(LimboError::Busy);
4756 }
4757 }
4758 if let CheckpointMode::Passive {
4759 upper_bound_inclusive: Some(upper_bound),
4760 } = mode
4761 {
4762 max_frame = max_frame.min(upper_bound);
4763 }
4764
4765 {
4766 let mut oc = self.ongoing_checkpoint.write();
4767 oc.max_frame = max_frame;
4768 oc.min_frame = nbackfills + 1;
4769 }
4770 let (oc_min_frame, oc_max_frame) = {
4771 let oc = self.ongoing_checkpoint.read();
4772 (oc.min_frame, oc.max_frame)
4773 };
4774 self.coordination.ensure_local_frame_cache_covers(
4775 &self.io,
4776 WalSnapshot {
4777 max_frame: oc_max_frame,
4778 ..self.load_coordination_snapshot()
4779 },
4780 )?;
4781 tracing::debug!(
4782 "checkpoint_inner::Start: min_frame={oc_min_frame}, max_frame={oc_max_frame}"
4783 );
4784 let mut to_checkpoint = self
4785 .coordination
4786 .iter_latest_frames(oc_min_frame, oc_max_frame);
4787 to_checkpoint.sort_unstable_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0)));
4789 {
4790 let mut oc = self.ongoing_checkpoint.write();
4791 oc.pages_to_checkpoint = to_checkpoint;
4792 oc.current_page = 0;
4793 oc.inflight_writes.clear();
4794 oc.inflight_reads.clear();
4795 oc.state = CheckpointState::Processing;
4796 oc.time = self.io.current_time_monotonic();
4797 }
4798 tracing::trace!(
4799 "checkpoint_start(min_frame={}, max_frame={})",
4800 oc_min_frame,
4801 oc_max_frame,
4802 );
4803 }
4804 CheckpointState::Processing => {
4810 let mut nr_completions = 0;
4812 let mut group = CompletionGroup::new(|_| {});
4813 let mut ongoing_chkpt = self.ongoing_checkpoint.write();
4814
4815 if ongoing_chkpt.process_inflight_writes() {
4817 tracing::trace!("Completed a write batch");
4818 }
4819 if ongoing_chkpt.process_pending_reads()? {
4821 tracing::trace!("Drained reads into batch");
4822 }
4823 if let Some(e) = ongoing_chkpt.first_write_error() {
4824 mark_unlikely();
4825 let to_cancel: Vec<Completion> = ongoing_chkpt
4827 .inflight_reads
4828 .iter()
4829 .map(|r| r.completion.clone())
4830 .collect();
4831 pager.io.cancel(&to_cancel)?;
4832 pager.io.drain_completions(&to_cancel)?;
4833 return Err(LimboError::CompletionError(e));
4834 }
4835 let epoch = self.coordination.checkpoint_epoch();
4836 'inner: while ongoing_chkpt.should_issue_reads() {
4838 let (page_id, target_frame) = {
4839 ongoing_chkpt.pages_to_checkpoint[ongoing_chkpt.current_page as usize]
4840 };
4841 if let Some(cached_page) =
4842 pager.cache_get_for_checkpoint(page_id as usize, target_frame, epoch)?
4843 {
4844 let buffer = cached_page
4845 .get_contents()
4846 .buffer
4847 .as_ref()
4848 .expect("buffer missing")
4849 .clone();
4850 {
4851 ongoing_chkpt
4852 .pending_writes
4853 .insert(page_id as usize, buffer);
4854 let current = ongoing_chkpt.current_page as usize;
4856 ongoing_chkpt.pages_to_checkpoint[current] =
4857 (page_id, target_frame);
4858 ongoing_chkpt.current_page += 1;
4859 }
4860 continue 'inner;
4861 }
4862 let inflight =
4865 self.issue_wal_read_into_buffer(page_id as usize, target_frame)?;
4866 group.add(&inflight.completion);
4867 nr_completions += 1;
4868 ongoing_chkpt.inflight_reads.push(inflight);
4869 ongoing_chkpt.current_page += 1;
4870 }
4871
4872 let should_flush = ongoing_chkpt.inflight_writes.len() < MAX_INFLIGHT_WRITES
4874 && ongoing_chkpt.should_flush_batch();
4875 if should_flush {
4876 let batch_map = ongoing_chkpt.pending_writes.take();
4877 if !batch_map.is_empty() {
4878 let new_write = InflightWriteBatch::new();
4879 for c in write_pages_vectored(
4880 pager,
4881 batch_map,
4882 new_write.done.clone(),
4883 new_write.err.clone(),
4884 )? {
4885 group.add(&c);
4886 nr_completions += 1;
4887 }
4888 ongoing_chkpt.inflight_writes.push(new_write);
4889 }
4890 }
4891 if nr_completions > 0 {
4892 io_yield_one!(group.build());
4893 } else if ongoing_chkpt.complete() {
4894 ongoing_chkpt.state = CheckpointState::DetermineResult;
4895 } else {
4896 mark_unlikely();
4898 return Err(LimboError::InternalError(
4899 "checkpoint stuck: no inflight completions but not complete".into(),
4900 ));
4901 }
4902 }
4903 CheckpointState::DetermineResult => {
4906 let mut ongoing_chkpt = self.ongoing_checkpoint.write();
4907 turso_assert!(
4908 ongoing_chkpt.complete(),
4909 "checkpoint pending flush must have finished"
4910 );
4911 let wal_max_frame = self.load_coordination_snapshot().max_frame;
4912 let wal_total_backfilled = ongoing_chkpt.max_frame;
4913 let wal_checkpoint_backfilled =
4918 wal_total_backfilled.saturating_sub(ongoing_chkpt.min_frame - 1);
4919
4920 let checkpoint_result = CheckpointResult::new(
4921 wal_max_frame,
4922 wal_total_backfilled,
4923 wal_checkpoint_backfilled,
4924 );
4925 tracing::debug!("checkpoint_result={:?}, mode={:?}", checkpoint_result, mode);
4926 if mode.require_all_backfilled() && !checkpoint_result.everything_backfilled() {
4927 return Err(LimboError::Busy);
4928 }
4929 if mode.should_restart_log() {
4930 turso_assert!(
4931 matches!(
4932 *self.checkpoint_guard.read(),
4933 Some(CheckpointLocks::Writer { .. })
4934 ),
4935 "We must hold writer and checkpoint locks to restart the log",
4936 { "checkpoint_guard": *self.checkpoint_guard.read() }
4937 );
4938 self.restart_log()?;
4939 }
4940 ongoing_chkpt.state = CheckpointState::Finalize {
4941 checkpoint_result: Some(checkpoint_result),
4942 };
4943 }
4944 CheckpointState::Finalize { .. } => {
4945 if mode.should_restart_log() {
4952 Self::unlock_after_restart(&self.coordination, None);
4953 }
4954 let mut checkpoint_result = {
4955 let mut oc = self.ongoing_checkpoint.write();
4956 let CheckpointState::Finalize {
4957 checkpoint_result, ..
4958 } = &mut oc.state
4959 else {
4960 panic!("unexpected state");
4961 };
4962 checkpoint_result.take().unwrap()
4963 };
4964 self.increment_checkpoint_epoch();
4966
4967 tracing::debug!("checkpoint_result={:?}", checkpoint_result);
4968 if checkpoint_result.should_truncate()
4974 || checkpoint_result.wal_checkpoint_backfilled > 0
4975 {
4976 checkpoint_result.maybe_guard = self.checkpoint_guard.write().take();
4982 } else {
4983 let _ = self.checkpoint_guard.write().take();
4984 }
4985 {
4986 let mut oc = self.ongoing_checkpoint.write();
4987 oc.inflight_writes.clear();
4988 oc.pending_writes.clear();
4989 oc.pages_to_checkpoint.clear();
4990 oc.current_page = 0;
4991 }
4992 let oc_time = self.ongoing_checkpoint.read().time;
4993 tracing::debug!(
4994 "total time spent checkpointing: {:?}",
4995 self.io
4996 .current_time_monotonic()
4997 .duration_since(oc_time)
4998 .as_millis()
4999 );
5000 self.ongoing_checkpoint.write().state = CheckpointState::Start;
5001 return Ok(IOResult::Done(checkpoint_result));
5002 }
5003 }
5004 }
5005 }
5006
5007 fn determine_max_safe_checkpoint_frame(&self) -> u64 {
5037 self.coordination
5038 .determine_max_safe_checkpoint_frame(self.load_coordination_snapshot().max_frame)
5039 }
5040
5041 pub fn try_restart_log_before_write(&self) -> Result<()> {
5048 let max_frame_read_lock_index = self.max_frame_read_lock_index.load(Ordering::Acquire);
5049 if max_frame_read_lock_index != 0 {
5050 tracing::debug!(
5051 "try_restart_log_before_write: max_frame_read_lock_index={max_frame_read_lock_index}, writer use WAL - can't restart the log"
5052 );
5053 return Ok(());
5054 }
5055 let snapshot = self.load_coordination_snapshot();
5056 let max_frame = snapshot.max_frame;
5057 let nbackfills = snapshot.nbackfills;
5058 if nbackfills == 0 {
5059 tracing::debug!(
5060 "try_restart_log_before_write: nbackfills={nbackfills}, nothing were backfilled - can't restart the log"
5061 );
5062 return Ok(());
5063 }
5064 turso_assert!(
5065 max_frame >= nbackfills,
5066 "backfills can't be more than max_frame"
5067 );
5068 if max_frame != nbackfills {
5069 tracing::debug!(
5070 "try_restart_log_before_write: max_frame={max_frame}, nbackfills={nbackfills}, not everything is backfilled to the DB file - can't restart the log"
5071 );
5072 return Ok(());
5073 }
5074 let Some(snapshot) = self
5075 .coordination
5076 .try_restart_log_for_write(self.io.as_ref())?
5077 else {
5078 return Ok(());
5079 };
5080 self.apply_restart_snapshot(snapshot);
5081 self.increment_checkpoint_epoch();
5082 let result = Ok(());
5083 tracing::debug!("try_restart_log_before_write: result={:?}", result);
5084 result
5085 }
5086
5087 fn restart_log(&self) -> Result<()> {
5088 tracing::debug!("restart_log");
5089 let snapshot = self.coordination.begin_restart(self.io.as_ref())?;
5090 self.apply_restart_snapshot(snapshot);
5091 Ok(())
5092 }
5093
5094 #[aristo::intent("WAL truncate is atomic: no committed frame can be observed lost across the truncate operation\n", id = "aristos:wal_truncate_atomic_under_concurrent_writers", verify = "full", parent = "wal_protocol_correctness")]
5096 fn truncate_log(
5097 &self,
5098 result: &mut CheckpointResult,
5099 sync_type: FileSyncType,
5100 ) -> Result<IOResult<()>> {
5101 let file = self.coordination.prepare_truncate()?;
5102
5103 if !result.wal_truncate_sent {
5104 let c = Completion::new_trunc({
5105 move |res| {
5106 if let Err(err) = res {
5107 tracing::debug!("WAL truncate failed: {err}")
5108 } else {
5109 tracing::trace!("WAL file truncated to 0 B");
5110 }
5111 }
5112 });
5113 let c = file.truncate(0, c)?;
5114 result.wal_truncate_sent = true;
5115 result.wal_max_frame = 0;
5117 result.wal_total_backfilled = 0;
5118 io_yield_one!(c);
5119 } else if !result.wal_sync_sent {
5120 let c = file.sync(
5121 Completion::new_sync(move |res| {
5122 if let Err(err) = res {
5123 tracing::debug!("WAL sync failed: {err}")
5124 } else {
5125 tracing::trace!("WAL file synced after truncation");
5126 }
5127 }),
5128 sync_type,
5129 )?;
5130 result.wal_sync_sent = true;
5131 io_yield_one!(c);
5132 }
5133 Ok(IOResult::Done(()))
5134 }
5135
5136 fn apply_restart_snapshot(&self, snapshot: WalSnapshot) {
5137 *self.last_checksum.write() = snapshot.last_checksum;
5138 self.max_frame.store(snapshot.max_frame, Ordering::Release);
5139 self.min_frame.store(0, Ordering::Release);
5140 self.checkpoint_seq
5141 .store(snapshot.checkpoint_seq, Ordering::Release);
5142 }
5143
5144 fn unlock_after_restart(coordination: &Arc<dyn WalCoordination>, e: Option<&LimboError>) {
5146 coordination.end_restart();
5147 if let Some(e) = e {
5148 mark_unlikely();
5149 tracing::debug!(
5150 "Failed to restart WAL header: {:?}, releasing read locks",
5151 e
5152 );
5153 }
5154 }
5155
5156 fn acquire_proper_checkpoint_guard(
5157 &self,
5158 mode: CheckpointMode,
5159 lock_source: CheckpointLockSource,
5160 ) -> Result<()> {
5161 let needs_new_guard = {
5162 let guard = self.checkpoint_guard.read();
5163 !matches!(
5164 (&*guard, mode),
5165 (
5166 Some(CheckpointLocks::Read0 { .. }),
5167 CheckpointMode::Passive { .. },
5168 ) | (
5169 Some(CheckpointLocks::Writer { .. }),
5170 CheckpointMode::Restart | CheckpointMode::Truncate { .. },
5171 ),
5172 )
5173 };
5174 if needs_new_guard {
5175 if self.checkpoint_guard.read().is_some() {
5177 let _ = self.checkpoint_guard.write().take();
5178 }
5179 let guard = match lock_source {
5180 CheckpointLockSource::Acquire => {
5181 CheckpointLocks::new(self.coordination.clone(), mode)?
5182 }
5183 CheckpointLockSource::HeldByCaller => {
5184 CheckpointLocks::from_held_vacuum_checkpoint_lock(self.coordination.clone())?
5185 }
5186 };
5187 *self.checkpoint_guard.write() = Some(guard);
5188 }
5189 Ok(())
5190 }
5191
5192 fn issue_wal_read_into_buffer(&self, page_id: usize, frame_id: u64) -> Result<InflightRead> {
5193 let offset = self.frame_offset(frame_id);
5194 let buf_slot = Arc::new(SpinLock::new(None));
5195 tracing::debug!(
5196 "Issuing WAL read: page_id={}, frame_id={}, offset={}",
5197 page_id,
5198 frame_id,
5199 offset
5200 );
5201
5202 let complete = {
5203 let buf_slot = buf_slot.clone();
5204 Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
5205 let Ok((buf, read)) = res else {
5206 return None;
5207 };
5208 let buf_len = buf.len();
5209 turso_assert!(
5210 read == buf_len as i32,
5211 "read bytes does not match expected buffer length",
5212 { "read": read, "expected": buf_len, "frame_id": frame_id }
5213 );
5214 *buf_slot.lock() = Some(buf);
5215 None
5216 })
5217 };
5218 let file = self.coordination.wal_file()?;
5220 let c = begin_read_wal_frame(
5221 file.as_ref(),
5222 offset + WAL_FRAME_HEADER_SIZE as u64,
5223 self.buffer_pool.clone(),
5224 complete,
5225 page_id,
5226 &self.io_ctx.read(),
5227 )?;
5228
5229 Ok(InflightRead {
5230 completion: c,
5231 page_id,
5232 buf: buf_slot,
5233 })
5234 }
5235
5236 pub fn mvcc_refresh_if_db_changed(&self) -> bool {
5244 let snapshot = self.load_coordination_snapshot();
5245 let local_state = self.connection_state();
5246 let changed = self.db_changed_against(snapshot, local_state);
5247 if changed {
5248 self.install_connection_state(local_state.with_snapshot(snapshot));
5249 }
5250 changed
5251 }
5252}
5253
5254#[cfg(host_shared_wal)]
5255fn read_exact_bytes_from_file(
5256 io: &Arc<dyn IO>,
5257 file: &Arc<dyn File>,
5258 offset: u64,
5259 len: usize,
5260) -> Result<Option<Vec<u8>>> {
5261 let read_buf = Arc::new(Buffer::new_temporary(len));
5262 let bytes_read = Arc::new(AtomicUsize::new(usize::MAX));
5263 let c = file.pread(
5264 offset,
5265 Completion::new_read(read_buf.clone(), {
5266 let bytes_read = bytes_read.clone();
5267 Box::new(move |res| {
5268 if let Ok((_buf, count)) = res {
5269 bytes_read.store(count as usize, Ordering::Release);
5270 }
5271 None
5272 })
5273 }),
5274 )?;
5275 io.wait_for_completion(c)?;
5276 if bytes_read.load(Ordering::Acquire) != len {
5277 return Ok(None);
5278 }
5279 Ok(Some(read_buf.as_slice()[..len].to_vec()))
5280}
5281
5282#[cfg(host_shared_wal)]
5283fn read_validated_wal_header_from_file(
5284 io: &Arc<dyn IO>,
5285 file: &Arc<dyn File>,
5286) -> Result<Option<WalHeader>> {
5287 let Some(bytes) = read_exact_bytes_from_file(io, file, 0, WAL_HEADER_SIZE)? else {
5288 return Ok(None);
5289 };
5290 let header = WalHeader {
5291 magic: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
5292 file_format: u32::from_be_bytes(bytes[4..8].try_into().unwrap()),
5293 page_size: u32::from_be_bytes(bytes[8..12].try_into().unwrap()),
5294 checkpoint_seq: u32::from_be_bytes(bytes[12..16].try_into().unwrap()),
5295 salt_1: u32::from_be_bytes(bytes[16..20].try_into().unwrap()),
5296 salt_2: u32::from_be_bytes(bytes[20..24].try_into().unwrap()),
5297 checksum_1: u32::from_be_bytes(bytes[24..28].try_into().unwrap()),
5298 checksum_2: u32::from_be_bytes(bytes[28..32].try_into().unwrap()),
5299 };
5300 if !matches!(header.magic, WAL_MAGIC_LE | WAL_MAGIC_BE) {
5301 return Ok(None);
5302 }
5303 if PageSize::new(header.page_size).is_none() {
5304 return Ok(None);
5305 }
5306 let use_native_endian = cfg!(target_endian = "big") == ((header.magic & 1) != 0);
5307 let calc = checksum_wal(
5308 &bytes[..WAL_HEADER_SIZE - 8],
5309 &header,
5310 (0, 0),
5311 use_native_endian,
5312 );
5313 if calc != (header.checksum_1, header.checksum_2) {
5314 return Ok(None);
5315 }
5316 Ok(Some(header))
5317}
5318
5319#[cfg(host_shared_wal)]
5320fn wal_header_matches_authority_snapshot(
5321 wal_header: WalHeader,
5322 snapshot: SharedWalCoordinationHeader,
5323) -> bool {
5324 wal_header.page_size == snapshot.page_size
5325 && wal_header.checkpoint_seq == snapshot.checkpoint_seq
5326 && wal_header.salt_1 == snapshot.salt_1
5327 && wal_header.salt_2 == snapshot.salt_2
5328}
5329
5330pub(crate) fn database_identity_from_header_bytes(header_bytes: &[u8]) -> Result<(u32, u32)> {
5331 if header_bytes.len() < DatabaseHeader::SIZE {
5332 return Err(LimboError::Corrupt(format!(
5333 "database header must be at least {} bytes, got {}",
5334 DatabaseHeader::SIZE,
5335 header_bytes.len()
5336 )));
5337 }
5338 if header_bytes[0..16] != *b"SQLite format 3\0" {
5339 return Err(LimboError::Corrupt("database header magic mismatch".into()));
5340 }
5341 let db_size_pages = u32::from_be_bytes(header_bytes[28..32].try_into().unwrap());
5342 let header_crc32c = crc32c::crc32c(&header_bytes[..DatabaseHeader::SIZE]);
5343 Ok((db_size_pages, header_crc32c))
5344}
5345
5346fn read_database_identity_from_storage(
5347 io: &Arc<dyn IO>,
5348 db_file: &Arc<dyn DatabaseStorage>,
5349) -> Result<Option<(u32, u32)>> {
5350 let read_buf = Arc::new(Buffer::new_temporary(PageSize::MIN as usize));
5351 let bytes_read = Arc::new(AtomicUsize::new(usize::MAX));
5352 let c = db_file.read_header(Completion::new_read(read_buf.clone(), {
5353 let bytes_read = bytes_read.clone();
5354 Box::new(move |res| {
5355 if let Ok((_buf, count)) = res {
5356 bytes_read.store(count as usize, Ordering::Release);
5357 }
5358 None
5359 })
5360 }))?;
5361 io.wait_for_completion(c)?;
5362 if bytes_read.load(Ordering::Acquire) < DatabaseHeader::SIZE {
5363 return Ok(None);
5364 }
5365 Ok(Some(database_identity_from_header_bytes(
5366 &read_buf.as_slice()[..DatabaseHeader::SIZE],
5367 )?))
5368}
5369
5370#[cfg(all(clt_turso_tests, host_shared_wal))]
5371fn read_database_identity_from_file_path(
5372 io: &Arc<dyn IO>,
5373 wal_path: &str,
5374) -> Result<Option<(u32, u32)>> {
5375 let db_path = wal_path
5376 .strip_suffix("-wal")
5377 .unwrap_or(wal_path)
5378 .to_string();
5379 let file = match io.open_file(&db_path, crate::OpenFlags::None, false) {
5380 Ok(file) => file,
5381 Err(LimboError::CompletionError(CompletionError::IOError(
5382 std::io::ErrorKind::NotFound,
5383 _,
5384 ))) => return Ok(None),
5385 Err(err) => return Err(err),
5386 };
5387 let Some(bytes) = read_exact_bytes_from_file(io, &file, 0, DatabaseHeader::SIZE)? else {
5388 return Ok(None);
5389 };
5390 Ok(Some(database_identity_from_header_bytes(&bytes)?))
5391}
5392
5393#[cfg(host_shared_wal)]
5394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5395enum AuthoritySnapshotValidation {
5396 Trusted,
5397 RebuildFromDisk(AuthoritySnapshotRebuildReason),
5398}
5399
5400#[cfg(host_shared_wal)]
5401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5402enum AuthoritySnapshotRebuildReason {
5403 WalHeaderUnreadable,
5404 WalHeaderMismatch,
5405 WalTooShortForSnapshot,
5406 WalLengthMismatch,
5407 LastFrameMissing,
5408 LastFrameNotCommit,
5409 LastFrameSaltMismatch,
5410 LastFrameChecksumMismatch,
5411}
5412
5413#[cfg(host_shared_wal)]
5414fn classify_authority_snapshot_against_wal(
5415 io: &Arc<dyn IO>,
5416 file: &Arc<dyn File>,
5417 snapshot: SharedWalCoordinationHeader,
5418) -> Result<AuthoritySnapshotValidation> {
5419 let wal_size = file.size()?;
5420 if snapshot.max_frame == 0 {
5421 if wal_size == 0 {
5422 return Ok(AuthoritySnapshotValidation::Trusted);
5423 }
5424 if wal_size == WAL_HEADER_SIZE as u64 {
5425 let Some(wal_header) = read_validated_wal_header_from_file(io, file)? else {
5426 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5427 AuthoritySnapshotRebuildReason::WalHeaderUnreadable,
5428 ));
5429 };
5430 return Ok(
5431 if wal_header_matches_authority_snapshot(wal_header, snapshot) {
5432 AuthoritySnapshotValidation::Trusted
5433 } else {
5434 AuthoritySnapshotValidation::RebuildFromDisk(
5435 AuthoritySnapshotRebuildReason::WalHeaderMismatch,
5436 )
5437 },
5438 );
5439 }
5440 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5441 AuthoritySnapshotRebuildReason::WalLengthMismatch,
5442 ));
5443 }
5444
5445 if wal_size < WAL_HEADER_SIZE as u64 {
5446 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5447 AuthoritySnapshotRebuildReason::WalTooShortForSnapshot,
5448 ));
5449 }
5450
5451 let Some(wal_header) = read_validated_wal_header_from_file(io, file)? else {
5452 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5453 AuthoritySnapshotRebuildReason::WalHeaderUnreadable,
5454 ));
5455 };
5456 if !wal_header_matches_authority_snapshot(wal_header, snapshot) {
5457 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5458 AuthoritySnapshotRebuildReason::WalHeaderMismatch,
5459 ));
5460 }
5461
5462 let frame_size = WAL_FRAME_HEADER_SIZE as u64 + wal_header.page_size as u64;
5463 let expected_wal_len = WAL_HEADER_SIZE as u64 + snapshot.max_frame * frame_size;
5464 if wal_size != expected_wal_len {
5465 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5466 AuthoritySnapshotRebuildReason::WalLengthMismatch,
5467 ));
5468 }
5469
5470 let last_frame_offset = WAL_HEADER_SIZE as u64 + (snapshot.max_frame - 1) * frame_size;
5471 let Some(frame_bytes) =
5472 read_exact_bytes_from_file(io, file, last_frame_offset, frame_size as usize)?
5473 else {
5474 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5475 AuthoritySnapshotRebuildReason::LastFrameMissing,
5476 ));
5477 };
5478 let (frame_header, _) = sqlite3_ondisk::parse_wal_frame_header(&frame_bytes);
5479 if !frame_header.is_commit_frame() {
5480 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5481 AuthoritySnapshotRebuildReason::LastFrameNotCommit,
5482 ));
5483 }
5484 if frame_header.salt_1 != snapshot.salt_1 || frame_header.salt_2 != snapshot.salt_2 {
5485 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5486 AuthoritySnapshotRebuildReason::LastFrameSaltMismatch,
5487 ));
5488 }
5489 if frame_header.checksum_1 != snapshot.checksum_1
5490 || frame_header.checksum_2 != snapshot.checksum_2
5491 {
5492 return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5493 AuthoritySnapshotRebuildReason::LastFrameChecksumMismatch,
5494 ));
5495 }
5496 Ok(AuthoritySnapshotValidation::Trusted)
5497}
5498
5499impl WalFileShared {
5500 pub fn last_checksum_and_max_frame(&self) -> ((u32, u32), u64) {
5501 (
5502 self.metadata.last_checksum,
5503 self.metadata.max_frame.load(Ordering::Acquire),
5504 )
5505 }
5506
5507 #[cfg(host_shared_wal)]
5508 pub(crate) fn open_shared_from_authority_if_exists(
5509 io: &Arc<dyn IO>,
5510 path: &str,
5511 flags: crate::OpenFlags,
5512 authority: &Arc<MappedSharedWalCoordination>,
5513 db_file: &Arc<dyn DatabaseStorage>,
5514 ) -> Result<Arc<RwLock<WalFileShared>>> {
5515 let snapshot = authority.snapshot();
5516 let file = match io.open_file(path, flags, false) {
5517 Ok(file) => file,
5518 Err(LimboError::CompletionError(CompletionError::IOError(
5519 std::io::ErrorKind::NotFound,
5520 _,
5521 ))) if flags.contains(crate::OpenFlags::ReadOnly) => {
5522 return Ok(WalFileShared::new_noop());
5523 }
5524 Err(e) => return Err(e),
5525 };
5526 let wal_size = file.size()?;
5527
5528 match classify_authority_snapshot_against_wal(io, &file, snapshot)? {
5529 AuthoritySnapshotValidation::Trusted => {}
5530 AuthoritySnapshotValidation::RebuildFromDisk(reason) => {
5531 tracing::debug!(
5532 ?reason,
5533 "rebuilding WAL state from disk because persisted authority is not provably reusable"
5534 );
5535 return sqlite3_ondisk::build_shared_wal(&file, io);
5536 }
5537 }
5538 if authority.frame_index_overflowed() {
5539 tracing::debug!(
5540 "rebuilding WAL state from disk because the persisted tshm frame index is marked overflowed"
5541 );
5542 return sqlite3_ondisk::build_shared_wal(&file, io);
5543 }
5544 if snapshot.nbackfills != 0
5545 && authority.open_mode() == SharedWalCoordinationOpenMode::Exclusive
5546 {
5547 tracing::debug!(
5548 nbackfills = snapshot.nbackfills,
5549 max_frame = snapshot.max_frame,
5550 "rebuilding WAL state from disk because an exclusive reopen must conservatively clear published backfill progress"
5551 );
5552 return sqlite3_ondisk::build_shared_wal(&file, io);
5553 }
5554 if snapshot.max_frame > snapshot.nbackfills
5555 && authority
5556 .iter_latest_frames(0, snapshot.max_frame)
5557 .is_empty()
5558 {
5559 tracing::debug!(
5560 max_frame = snapshot.max_frame,
5561 nbackfills = snapshot.nbackfills,
5562 "rebuilding WAL state from disk because the persisted tshm frame index has no entries for a visible WAL tail"
5563 );
5564 return sqlite3_ondisk::build_shared_wal(&file, io);
5565 }
5566 if snapshot.nbackfills != 0 {
5567 let Some((db_size_pages, db_header_crc32c)) =
5568 read_database_identity_from_storage(io, db_file)?
5569 else {
5570 tracing::debug!(
5571 nbackfills = snapshot.nbackfills,
5572 "rebuilding WAL state from disk because the main database header is unavailable for backfill-proof validation"
5573 );
5574 return sqlite3_ondisk::build_shared_wal(&file, io);
5575 };
5576 if !authority.validate_backfill_proof(snapshot, db_size_pages, db_header_crc32c) {
5577 tracing::debug!(
5578 nbackfills = snapshot.nbackfills,
5579 "rebuilding WAL state from disk because persisted tshm backfill proof is not valid for the current database header"
5580 );
5581 return sqlite3_ondisk::build_shared_wal(&file, io);
5582 }
5583 }
5584 let wal_is_initialized = wal_size >= WAL_HEADER_SIZE as u64;
5585
5586 let wal_header = WalHeader {
5587 page_size: snapshot.page_size,
5588 checkpoint_seq: snapshot.checkpoint_seq,
5589 salt_1: snapshot.salt_1,
5590 salt_2: snapshot.salt_2,
5591 checksum_1: snapshot.checksum_1,
5592 checksum_2: snapshot.checksum_2,
5593 ..WalHeader::new()
5594 };
5595 let read_locks = array::from_fn(|_| TursoRwLock::new());
5596 for (i, lock) in read_locks.iter().enumerate() {
5597 lock.write();
5598 lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED });
5599 lock.unlock();
5600 }
5601
5602 let shared = WalFileShared {
5603 metadata: WalSharedMetadata {
5604 enabled: AtomicBool::new(true),
5605 wal_header: Arc::new(SpinLock::new(wal_header)),
5606 min_frame: AtomicU64::new(0),
5607 max_frame: AtomicU64::new(snapshot.max_frame),
5608 nbackfills: AtomicU64::new(snapshot.nbackfills),
5609 transaction_count: AtomicU64::new(snapshot.transaction_count),
5610 last_checksum: (snapshot.checksum_1, snapshot.checksum_2),
5611 loaded: AtomicBool::new(true),
5612 loaded_from_disk_scan: AtomicBool::new(false),
5613 initialized: AtomicBool::new(wal_is_initialized),
5614 },
5615 runtime: WalSharedRuntime {
5616 authority_reconciliation: Default::default(),
5617 frame_cache: Arc::new(SpinLock::new(FxHashMap::default())),
5618 frame_cache_high_water: AtomicU64::new(0),
5619 file: Some(file),
5620 read_locks,
5621 vacuum_lock: TursoRwLock::new(),
5622 write_lock: TursoRwLock::new(),
5623 checkpoint_lock: TursoRwLock::new(),
5624 epoch: AtomicU32::new(snapshot.checkpoint_epoch),
5625 overflow_fallback_coverage: Arc::new(SpinLock::new(
5626 OverflowFallbackCoverage::default(),
5627 )),
5628 },
5629 };
5630 Ok(Arc::new(RwLock::new(shared)))
5631 }
5632
5633 pub fn open_shared_if_exists(
5634 io: &Arc<dyn IO>,
5635 path: &str,
5636 flags: crate::OpenFlags,
5637 ) -> Result<Arc<RwLock<WalFileShared>>> {
5638 let mut driver = Self::open_shared_if_exists_begin(io, path, flags)?;
5639 io.block(|| driver.poll())
5640 }
5641
5642 pub fn open_shared_if_exists_begin(
5646 io: &Arc<dyn IO>,
5647 path: &str,
5648 flags: crate::OpenFlags,
5649 ) -> Result<OpenSharedWal> {
5650 let file = match io.open_file(path, flags, false) {
5651 Ok(file) => file,
5652 Err(LimboError::CompletionError(CompletionError::IOError(
5653 std::io::ErrorKind::NotFound,
5654 _,
5655 ))) if flags.contains(crate::OpenFlags::ReadOnly) => {
5656 return Ok(OpenSharedWal::Noop(WalFileShared::new_noop()));
5659 }
5660 Err(e) => return Err(e),
5661 };
5662 Ok(OpenSharedWal::Build(sqlite3_ondisk::BuildSharedWal::begin(
5663 &file,
5664 )?))
5665 }
5666
5667 pub fn is_initialized(&self) -> Result<bool> {
5668 Ok(self.metadata.initialized.load(Ordering::Acquire))
5669 }
5670
5671 pub fn new_noop() -> Arc<RwLock<WalFileShared>> {
5672 let wal_header = WalHeader::new();
5673 let read_locks = array::from_fn(|_| TursoRwLock::new());
5674 for (i, lock) in read_locks.iter().enumerate() {
5675 lock.write();
5676 lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED });
5677 lock.unlock();
5678 }
5679 let shared = WalFileShared {
5680 metadata: WalSharedMetadata {
5681 enabled: AtomicBool::new(false),
5682 wal_header: Arc::new(SpinLock::new(wal_header)),
5683 min_frame: AtomicU64::new(0),
5684 max_frame: AtomicU64::new(0),
5685 nbackfills: AtomicU64::new(0),
5686 transaction_count: AtomicU64::new(0),
5687 last_checksum: (0, 0),
5688 loaded: AtomicBool::new(true),
5689 loaded_from_disk_scan: AtomicBool::new(false),
5690 initialized: AtomicBool::new(false),
5691 },
5692 runtime: WalSharedRuntime {
5693 authority_reconciliation: Default::default(),
5694 frame_cache: Arc::new(SpinLock::new(FxHashMap::default())),
5695 frame_cache_high_water: AtomicU64::new(0),
5696 file: None,
5697 read_locks,
5698 vacuum_lock: TursoRwLock::new(),
5699 write_lock: TursoRwLock::new(),
5700 checkpoint_lock: TursoRwLock::new(),
5701 epoch: AtomicU32::new(0),
5702 overflow_fallback_coverage: Arc::new(SpinLock::new(
5703 OverflowFallbackCoverage::default(),
5704 )),
5705 },
5706 };
5707 Arc::new(RwLock::new(shared))
5708 }
5709
5710 #[cfg(clt_turso_tests)]
5711 pub(super) fn new_shared(file: Arc<dyn File>) -> Result<Arc<RwLock<WalFileShared>>> {
5712 let wal_header = WalHeader::new();
5713 let read_locks = array::from_fn(|_| TursoRwLock::new());
5714 for (i, lock) in read_locks.iter().enumerate() {
5718 lock.write();
5719 lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED });
5720 lock.unlock();
5721 }
5722 let shared = WalFileShared {
5723 metadata: WalSharedMetadata {
5724 enabled: AtomicBool::new(true),
5725 wal_header: Arc::new(SpinLock::new(wal_header)),
5726 min_frame: AtomicU64::new(0),
5727 max_frame: AtomicU64::new(0),
5728 nbackfills: AtomicU64::new(0),
5729 transaction_count: AtomicU64::new(0),
5730 last_checksum: (0, 0),
5731 loaded: AtomicBool::new(true),
5732 loaded_from_disk_scan: AtomicBool::new(false),
5733 initialized: AtomicBool::new(false),
5734 },
5735 runtime: WalSharedRuntime {
5736 authority_reconciliation: Default::default(),
5737 frame_cache: Arc::new(SpinLock::new(FxHashMap::default())),
5738 frame_cache_high_water: AtomicU64::new(0),
5739 file: Some(file),
5740 read_locks,
5741 vacuum_lock: TursoRwLock::new(),
5742 write_lock: TursoRwLock::new(),
5743 checkpoint_lock: TursoRwLock::new(),
5744 epoch: AtomicU32::new(0),
5745 overflow_fallback_coverage: Arc::new(SpinLock::new(
5746 OverflowFallbackCoverage::default(),
5747 )),
5748 },
5749 };
5750 Ok(Arc::new(RwLock::new(shared)))
5751 }
5752
5753 pub fn page_size(&self) -> u32 {
5754 self.metadata.wal_header.lock().page_size
5755 }
5756
5757 fn restart_wal_header(&mut self, io: &dyn IO) {
5773 {
5774 let mut hdr = self.metadata.wal_header.lock();
5775 hdr.checkpoint_seq = hdr.checkpoint_seq.wrapping_add(1);
5776 hdr.salt_1 = hdr.salt_1.wrapping_add(1);
5778 hdr.salt_2 = io.generate_random_number() as u32;
5779
5780 self.metadata.max_frame.store(0, Ordering::Release);
5781 self.metadata.nbackfills.store(0, Ordering::Release);
5782 self.metadata.last_checksum = (hdr.checksum_1, hdr.checksum_2);
5783 self.metadata.initialized.store(false, Ordering::Release);
5786 }
5787
5788 self.runtime.frame_cache.lock().clear();
5789 self.runtime
5790 .frame_cache_high_water
5791 .store(0, Ordering::Release);
5792 self.runtime.read_locks[0].set_value_exclusive(0);
5794 self.runtime.read_locks[1].set_value_exclusive(0);
5795 for lock in &self.runtime.read_locks[2..] {
5796 lock.set_value_exclusive(READMARK_NOT_USED);
5797 }
5798 }
5799
5800 #[cfg(clt_turso_feature = "conn_raw_api")]
5810 pub fn replace_after_external_restore(&mut self, restored: WalFileShared) {
5811 self.metadata = restored.metadata;
5812 self.runtime.frame_cache = restored.runtime.frame_cache;
5813 self.runtime.file = restored.runtime.file;
5814 self.runtime.epoch.store(
5815 restored.runtime.epoch.load(Ordering::Acquire),
5816 Ordering::Release,
5817 );
5818 self.runtime.overflow_fallback_coverage = restored.runtime.overflow_fallback_coverage;
5819 }
5820}
5821
5822#[cfg(clt_turso_tests)]
5823pub mod test {
5824 #[cfg(host_shared_wal)]
5825 use super::{
5826 classify_authority_snapshot_against_wal, AuthoritySnapshotRebuildReason,
5827 AuthoritySnapshotValidation, ShmWalCoordination,
5828 };
5829 use super::{
5830 CheckpointLocks, InProcessWalCoordination, ReadGuardKind, RollbackTo, TryBeginReadResult,
5831 Wal, WalAutoActions, WalCommitState, WalConnectionState, WalCoordination, WalFile,
5832 WalSnapshot, NO_LOCK_HELD,
5833 };
5834 #[cfg(host_shared_wal)]
5835 use crate::storage::shared_wal_coordination::{
5836 MappedSharedWalCoordination, SharedWalCoordinationHeader, SharedWalCoordinationOpenMode,
5837 };
5838 use crate::sync::{atomic::Ordering, Arc};
5839 use crate::sync::{Mutex, RwLock};
5840 use crate::{
5841 io::FileSyncType,
5842 storage::{
5843 buffer_pool::BufferPool,
5844 database::{DatabaseFile, DatabaseStorage},
5845 pager::{allocate_new_page, PageRef},
5846 sqlite3_ondisk::{self, PageSize, WAL_HEADER_SIZE},
5847 wal::READMARK_NOT_USED,
5848 },
5849 types::IOResult,
5850 util::IOExt,
5851 Buffer, CheckpointMode, CheckpointResult, Completion, CompletionError, Connection,
5852 Database, File, LimboError, MemoryIO, OpenFlags, PlatformIO, SyncMode, WalFileShared, IO,
5853 };
5854 use std::num::NonZeroUsize;
5855 #[cfg(unix)]
5856 use std::os::unix::fs::MetadataExt;
5857 fn shared_wal_test_io() -> Arc<dyn IO> {
5862 #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp"))]
5863 {
5864 Arc::new(crate::WindowsIOCP::new().unwrap())
5865 }
5866 #[cfg(not(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp")))]
5867 {
5868 Arc::new(PlatformIO::new().unwrap())
5869 }
5870 }
5871
5872 #[allow(clippy::arc_with_non_send_sync)]
5873 pub(crate) fn get_database() -> (Arc<Database>, std::path::PathBuf) {
5874 let mut path = tempfile::tempdir().unwrap().keep();
5875 let dbpath = path.clone();
5876 path.push("test.db");
5877 {
5878 let connection = rusqlite::Connection::open(&path).unwrap();
5879 connection
5880 .pragma_update(None, "journal_mode", "wal")
5881 .unwrap();
5882 }
5883 let io = shared_wal_test_io();
5884 let db = Database::open_file_with_flags(
5885 io.clone(),
5886 path.to_str().unwrap(),
5887 crate::OpenFlags::default(),
5888 crate::DatabaseOpts::new().with_multiprocess_wal(true),
5889 None,
5890 )
5891 .unwrap();
5892 (db, dbpath)
5894 }
5895
5896 struct DeferredReadFile {
5897 inner: Arc<dyn File>,
5898 pending_reads: Mutex<Vec<(u64, Completion)>>,
5899 }
5900
5901 impl DeferredReadFile {
5902 fn new(inner: Arc<dyn File>) -> Self {
5903 Self {
5904 inner,
5905 pending_reads: Mutex::new(Vec::new()),
5906 }
5907 }
5908
5909 fn complete_pending_reads(&self) {
5910 let pending_reads = std::mem::take(&mut *self.pending_reads.lock());
5911 for (pos, completion) in pending_reads {
5912 std::mem::drop(self.inner.pread(pos, completion).unwrap());
5913 }
5914 }
5915 }
5916
5917 impl File for DeferredReadFile {
5918 fn lock_file(&self, exclusive: bool) -> crate::Result<()> {
5919 self.inner.lock_file(exclusive)
5920 }
5921
5922 fn unlock_file(&self) -> crate::Result<()> {
5923 self.inner.unlock_file()
5924 }
5925
5926 fn pread(&self, pos: u64, c: Completion) -> crate::Result<Completion> {
5927 self.pending_reads.lock().push((pos, c.clone()));
5928 Ok(c)
5929 }
5930
5931 fn pwrite(
5932 &self,
5933 pos: u64,
5934 buffer: Arc<Buffer>,
5935 c: Completion,
5936 ) -> crate::Result<Completion> {
5937 self.inner.pwrite(pos, buffer, c)
5938 }
5939
5940 fn sync(
5941 &self,
5942 c: Completion,
5943 sync_type: crate::io::FileSyncType,
5944 ) -> crate::Result<Completion> {
5945 self.inner.sync(c, sync_type)
5946 }
5947
5948 fn size(&self) -> crate::Result<u64> {
5949 self.inner.size()
5950 }
5951
5952 fn truncate(&self, len: u64, c: Completion) -> crate::Result<Completion> {
5953 self.inner.truncate(len, c)
5954 }
5955 }
5956
5957 #[cfg(clt_turso_feature = "conn_raw_api")]
5958 #[test]
5959 fn replace_after_external_restore_preserves_lock_identity() {
5960 let shared = WalFileShared::new_noop();
5961 let restored = WalFileShared::new_noop();
5962
5963 let read_lock_ptrs = {
5964 let shared = shared.read();
5965 shared
5966 .runtime
5967 .read_locks
5968 .iter()
5969 .map(std::ptr::from_ref)
5970 .collect::<Vec<_>>()
5971 };
5972 {
5973 let shared = shared.read();
5974 assert!(shared.runtime.read_locks[1].write());
5975 shared.runtime.read_locks[1].set_value_exclusive(7);
5976 shared.runtime.read_locks[1].unlock();
5977 }
5978 {
5979 let restored = restored.read();
5980 restored.metadata.max_frame.store(42, Ordering::Release);
5981 assert!(restored.runtime.read_locks[1].write());
5982 restored.runtime.read_locks[1].set_value_exclusive(99);
5983 restored.runtime.read_locks[1].unlock();
5984 }
5985
5986 let restored = match Arc::try_unwrap(restored) {
5987 Ok(restored) => restored.into_inner(),
5988 Err(_) => panic!("restored WAL test state should not be shared"),
5989 };
5990 shared.write().replace_after_external_restore(restored);
5991
5992 let shared = shared.read();
5993 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 42);
5994 assert_eq!(shared.runtime.read_locks[1].get_value(), 7);
5995 for (idx, lock) in shared.runtime.read_locks.iter().enumerate() {
5996 assert_eq!(std::ptr::from_ref(lock), read_lock_ptrs[idx]);
5997 }
5998 }
5999
6000 #[test]
6001 fn test_truncate_file() {
6002 let (db, _path) = get_database();
6003 let conn = db.connect().unwrap();
6004 conn.execute("create table test (id integer primary key, value text)")
6005 .unwrap();
6006 let _ = conn.execute("insert into test (value) values ('test1'), ('test2'), ('test3')");
6007 let wal = db.shared_wal.write();
6008 let wal_file = wal.runtime.file.as_ref().unwrap().clone();
6009 let done = Arc::new(Mutex::new(false));
6010 let _done = done.clone();
6011 let _ = wal_file.truncate(
6012 WAL_HEADER_SIZE as u64,
6013 Completion::new_trunc(move |_| {
6014 *_done.lock() = true;
6015 }),
6016 );
6017 assert!(wal_file.size().unwrap() == WAL_HEADER_SIZE as u64);
6018 assert!(*done.lock());
6019 }
6020
6021 #[test]
6022 fn test_wal_truncate_checkpoint() {
6023 let (db, path) = get_database();
6024 let mut walpath = path.clone().into_os_string().into_string().unwrap();
6025 walpath.push_str("/test.db-wal");
6026 let walpath = std::path::PathBuf::from(walpath);
6027
6028 let conn = db.connect().unwrap();
6029 conn.execute("create table test (id integer primary key, value text)")
6030 .unwrap();
6031 for _i in 0..25 {
6032 let _ = conn.execute("insert into test (value) values (randomblob(1024)), (randomblob(1024)), (randomblob(1024))");
6033 }
6034 let pager = conn.pager.load();
6035 let _ = pager.cacheflush();
6036
6037 let stat = std::fs::metadata(&walpath).unwrap();
6038 let meta_before = std::fs::metadata(&walpath).unwrap();
6039 let bytes_before = meta_before.len();
6040 run_checkpoint_until_done(
6041 &pager,
6042 CheckpointMode::Truncate {
6043 upper_bound_inclusive: None,
6044 },
6045 );
6046
6047 assert_eq!(pager.wal_state().unwrap().max_frame, 0);
6048
6049 tracing::debug!("wal filepath: {walpath:?}, size: {}", stat.len());
6050 let meta_after = std::fs::metadata(&walpath).unwrap();
6051 let bytes_after = meta_after.len();
6052 assert_ne!(
6053 bytes_before, bytes_after,
6054 "WAL file should not have been empty before checkpoint"
6055 );
6056 assert_eq!(
6057 bytes_after, 0,
6058 "WAL file should be truncated to 0 bytes, but is {bytes_after} bytes",
6059 );
6060 std::fs::remove_dir_all(path).unwrap();
6061 }
6062
6063 #[test]
6064 #[cfg_attr(
6065 windows,
6066 ignore = "shutdown checkpoint does not truncate the WAL file to zero on Windows"
6067 )]
6068 fn test_shutdown_checkpoint_truncates_after_restart() {
6069 let (db, path) = get_database();
6070 let mut walpath = path.clone().into_os_string().into_string().unwrap();
6071 walpath.push_str("/test.db-wal");
6072 let walpath = std::path::PathBuf::from(walpath);
6073
6074 let conn = db.connect().unwrap();
6075 conn.execute("create table test (id integer primary key, value text)")
6076 .unwrap();
6077 conn.execute("insert into test (value) values ('v1'), ('v2')")
6078 .unwrap();
6079
6080 let pager = conn.pager.load();
6081 run_checkpoint_until_done(&pager, CheckpointMode::Restart);
6082
6083 let bytes_before = std::fs::metadata(&walpath).unwrap().len();
6084 assert!(
6085 bytes_before > 0,
6086 "WAL should still have data after RESTART checkpoint"
6087 );
6088
6089 conn.close().unwrap();
6090
6091 let bytes_after = std::fs::metadata(&walpath).unwrap().len();
6092 assert_eq!(
6093 bytes_after, 0,
6094 "Shutdown checkpoint should truncate WAL after RESTART, but WAL is {bytes_after} bytes",
6095 );
6096 std::fs::remove_dir_all(path).unwrap();
6097 }
6098
6099 fn bulk_inserts(conn: &Arc<Connection>, n_txns: usize, rows_per_txn: usize) {
6100 for _ in 0..n_txns {
6101 conn.execute("begin transaction").unwrap();
6102 for i in 0..rows_per_txn {
6103 conn.execute(format!("insert into test(value) values ('v{i}')"))
6104 .unwrap();
6105 }
6106 conn.execute("commit").unwrap();
6107 }
6108 }
6109
6110 fn count_test_table(conn: &Arc<Connection>) -> i64 {
6111 let mut stmt = conn.prepare("select count(*) from test").unwrap();
6112 let mut count: i64 = 0;
6113 stmt.run_with_row_callback(|row| {
6114 count = row.get(0).unwrap();
6115 Ok(())
6116 })
6117 .unwrap();
6118 count
6119 }
6120
6121 fn run_checkpoint_until_done(pager: &crate::Pager, mode: CheckpointMode) -> CheckpointResult {
6122 pager
6125 .io
6126 .block(|| pager.checkpoint(mode, crate::SyncMode::Full, true))
6127 .unwrap()
6128 }
6129
6130 fn run_wal_checkpoint_until_done(
6131 db: &Database,
6132 pager: &crate::Pager,
6133 mode: CheckpointMode,
6134 ) -> CheckpointResult {
6135 let wal = pager.wal.as_ref().expect("wal should be present");
6136 loop {
6137 match wal.checkpoint(pager, mode) {
6138 Ok(IOResult::IO(io)) => io.wait(db.io.as_ref()).unwrap(),
6139 Ok(IOResult::Done(result)) => return result,
6140 Err(err) => panic!("checkpoint should succeed: {err:?}"),
6141 }
6142 }
6143 }
6144
6145 #[test]
6146 fn test_wal_checkpoint_defers_backfill_publication_until_db_sync() {
6147 let (db, _path) = get_database();
6148 let wal_shared = db.shared_wal.clone();
6149 let conn = db.connect().unwrap();
6150 conn.execute("create table test(id integer primary key, value text)")
6151 .unwrap();
6152 bulk_inserts(&conn, 8, 2);
6153
6154 let pager = conn.pager.load();
6155 let result = run_wal_checkpoint_until_done(&db, &pager, CheckpointMode::Full);
6156 assert!(
6157 result.wal_total_backfilled > 0,
6158 "checkpoint setup should backfill frames before DB sync"
6159 );
6160 assert_eq!(
6161 wal_shared.read().metadata.nbackfills.load(Ordering::SeqCst),
6162 0,
6163 "wal.checkpoint() must not publish positive nbackfills before DB sync completes"
6164 );
6165 }
6166
6167 #[test]
6168 fn test_checkpoint_sync_mode_off_leaves_backfill_unpublished() {
6169 let (db, _path) = get_database();
6170 let wal_shared = db.shared_wal.clone();
6171 let conn = db.connect().unwrap();
6172 conn.execute("create table test(id integer primary key, value text)")
6173 .unwrap();
6174 bulk_inserts(&conn, 8, 2);
6175
6176 let pager = conn.pager.load();
6177 let result = pager
6178 .io
6179 .block(|| pager.checkpoint(CheckpointMode::Full, SyncMode::Off, true))
6180 .unwrap();
6181 assert!(
6182 result.wal_total_backfilled > 0,
6183 "sync-mode-off checkpoint setup should still backfill frames into the DB file"
6184 );
6185 assert_eq!(
6186 wal_shared.read().metadata.nbackfills.load(Ordering::SeqCst),
6187 0,
6188 "SyncMode::Off must not publish positive nbackfills as durable shared state"
6189 );
6190 }
6191
6192 fn make_test_wal() -> (Arc<RwLock<WalFileShared>>, WalFile) {
6193 let io = shared_wal_test_io();
6194 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6195 let shared = WalFileShared::new_noop();
6196 let coordination: Arc<dyn WalCoordination> =
6197 Arc::new(InProcessWalCoordination::new(shared.clone()));
6198 let wal = WalFile::new_with_coordination(io, coordination, ((0, 0), 0), buffer_pool);
6199 (shared, wal)
6200 }
6201
6202 fn make_test_wal_from_shared(shared: Arc<RwLock<WalFileShared>>) -> WalFile {
6203 let io = shared_wal_test_io();
6204 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6205 let snapshot = shared.read().last_checksum_and_max_frame();
6206 WalFile::new(io, shared, snapshot, buffer_pool)
6207 }
6208
6209 fn make_initialized_memory_wal(page_size: u32) -> (Arc<dyn IO>, Arc<BufferPool>, WalFile) {
6210 let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
6211 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6212 buffer_pool
6213 .finalize_with_page_size(page_size as usize)
6214 .unwrap();
6215 let file = io
6216 .open_file("direct-batch-read.db-wal", OpenFlags::Create, false)
6217 .unwrap();
6218 let shared = WalFileShared::new_shared(file).unwrap();
6219 let wal = WalFile::new(io.clone(), shared, ((0, 0), 0), buffer_pool.clone());
6220 let page_size = PageSize::new(page_size).unwrap();
6221
6222 if let Some(c) = wal.prepare_wal_start(page_size).unwrap() {
6223 io.wait_for_completion(c).unwrap();
6224 }
6225 let c = wal.prepare_wal_finish(FileSyncType::Fsync).unwrap();
6226 io.wait_for_completion(c).unwrap();
6227
6228 (io, buffer_pool, wal)
6229 }
6230
6231 #[cfg(clt_turso_feature = "io_memory_yield")]
6236 fn make_initialized_memory_yield_wal(
6237 page_size: u32,
6238 ) -> (Arc<dyn IO>, Arc<BufferPool>, WalFile) {
6239 let io: Arc<dyn IO> = Arc::new(crate::io::MemoryYieldIO::new());
6240 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6241 buffer_pool
6242 .finalize_with_page_size(page_size as usize)
6243 .unwrap();
6244 let file = io
6245 .open_file("spill-visibility.db-wal", OpenFlags::Create, false)
6246 .unwrap();
6247 let shared = WalFileShared::new_shared(file).unwrap();
6248 let wal = WalFile::new(io.clone(), shared, ((0, 0), 0), buffer_pool.clone());
6249 let page_size = PageSize::new(page_size).unwrap();
6250
6251 if let Some(c) = wal.prepare_wal_start(page_size).unwrap() {
6252 io.wait_for_completion(c).unwrap();
6253 }
6254 let c = wal.prepare_wal_finish(FileSyncType::Fsync).unwrap();
6255 io.wait_for_completion(c).unwrap();
6256
6257 (io, buffer_pool, wal)
6258 }
6259
6260 #[cfg(clt_turso_feature = "io_memory_yield")]
6271 #[test]
6272 fn append_frames_vectored_frame_hidden_until_write_is_durable() {
6273 let page_size = 512;
6274 let (io, buffer_pool, wal) = make_initialized_memory_yield_wal(page_size);
6275 let page = page_with_pattern(7, 0x70, &buffer_pool);
6276
6277 let completion = wal
6278 .append_frames_vectored(vec![page], PageSize::new(page_size).unwrap())
6279 .unwrap();
6280
6281 assert!(
6284 !completion.succeeded(),
6285 "MemoryYieldIO must defer the write completion until io.step()"
6286 );
6287 assert_eq!(
6288 wal.get_max_frame(),
6289 1,
6290 "write cursor advances synchronously"
6291 );
6292
6293 assert_eq!(
6297 wal.find_frame(7, None).unwrap(),
6298 None,
6299 "frame must not be visible to readers before its write is durable"
6300 );
6301
6302 io.step().unwrap();
6305 assert!(completion.succeeded());
6306
6307 assert_eq!(
6308 wal.find_frame(7, None).unwrap(),
6309 Some(1),
6310 "frame must be visible once its write is durable"
6311 );
6312 }
6313
6314 fn page_with_pattern(page_id: i64, seed: u8, buffer_pool: &Arc<BufferPool>) -> PageRef {
6315 let page = allocate_new_page(page_id, buffer_pool);
6316 for (idx, byte) in page.get_contents().as_ptr().iter_mut().enumerate() {
6317 *byte = seed.wrapping_add(idx as u8).wrapping_add(page_id as u8);
6318 }
6319 page
6320 }
6321
6322 fn append_test_pages(
6323 io: &Arc<dyn IO>,
6324 wal: &WalFile,
6325 page_size: u32,
6326 pages: &[PageRef],
6327 ) -> Vec<Vec<u8>> {
6328 let prepared = wal
6329 .prepare_frames(pages, PageSize::new(page_size).unwrap(), Some(99), None)
6330 .unwrap();
6331 let expected = pages
6332 .iter()
6333 .map(|page| page.get_contents().as_ptr().to_vec())
6334 .collect::<Vec<_>>();
6335
6336 let file = wal.wal_file().unwrap();
6337 let c = file
6338 .pwritev(
6339 prepared.offset,
6340 prepared.bufs.clone(),
6341 Completion::new_write(|_| {}),
6342 )
6343 .unwrap();
6344 io.wait_for_completion(c).unwrap();
6345 wal.commit_prepared_frames(&[prepared]);
6346 wal.finish_append_frames_commit().unwrap();
6347 expected
6348 }
6349
6350 #[test]
6351 fn append_frames_vectored_spill_frames_are_not_reused_by_next_prepare() {
6352 let page_size = 512;
6353 let (_io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6354 let spill_page = page_with_pattern(7, 0x70, &buffer_pool);
6355
6356 let completion = wal
6357 .append_frames_vectored(vec![spill_page], PageSize::new(page_size).unwrap())
6358 .unwrap();
6359 assert!(completion.succeeded());
6360 assert_eq!(wal.get_max_frame(), 1);
6361 assert_eq!(wal.get_max_frame_in_wal(), 0);
6362
6363 let commit_page = page_with_pattern(9, 0x90, &buffer_pool);
6364 let prepared = wal
6365 .prepare_frames(
6366 &[commit_page],
6367 PageSize::new(page_size).unwrap(),
6368 Some(99),
6369 None,
6370 )
6371 .unwrap();
6372
6373 assert_eq!(
6374 prepared.metadata[0].1, 2,
6375 "prepare_frames must chain after unpublished spill frames"
6376 );
6377 assert_eq!(prepared.final_max_frame, 2);
6378 }
6379
6380 fn wait_for_completion_error(io: &Arc<dyn IO>, completion: Completion) -> CompletionError {
6381 match io.wait_for_completion(completion) {
6382 Err(LimboError::CompletionError(err)) => err,
6383 other => panic!("expected completion error, got {other:?}"),
6384 }
6385 }
6386
6387 #[test]
6388 fn read_frames_batch_reads_contiguous_wal_frames_directly() {
6389 let page_size = 512;
6390 let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6391 let source_pages = vec![
6392 page_with_pattern(2, 0x10, &buffer_pool),
6393 page_with_pattern(3, 0x20, &buffer_pool),
6394 page_with_pattern(4, 0x30, &buffer_pool),
6395 page_with_pattern(5, 0x40, &buffer_pool),
6396 ];
6397 let expected = append_test_pages(&io, &wal, page_size, &source_pages);
6398
6399 let target_pages = vec![
6400 Arc::new(crate::Page::new(2)),
6401 Arc::new(crate::Page::new(3)),
6402 Arc::new(crate::Page::new(4)),
6403 Arc::new(crate::Page::new(5)),
6404 ];
6405 let c = wal
6406 .read_frames_batch(1, &target_pages, buffer_pool, None)
6407 .unwrap();
6408 io.wait_for_completion(c).unwrap();
6409
6410 for (idx, page) in target_pages.iter().enumerate() {
6411 assert!(page.is_loaded(), "page {} should be loaded", page.get().id);
6412 assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6413 assert_eq!(page.wal_tag_pair(), ((idx + 1) as u64, 0));
6414 assert_eq!(page.get_contents().as_ptr(), expected[idx].as_slice());
6415 }
6416 }
6417
6418 #[test]
6419 fn read_frames_batch_can_start_from_middle_frame() {
6420 let page_size = 512;
6421 let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6422 let source_pages = vec![
6423 page_with_pattern(10, 0x01, &buffer_pool),
6424 page_with_pattern(11, 0x02, &buffer_pool),
6425 page_with_pattern(12, 0x03, &buffer_pool),
6426 page_with_pattern(13, 0x04, &buffer_pool),
6427 ];
6428 let expected = append_test_pages(&io, &wal, page_size, &source_pages);
6429
6430 let target_pages = vec![
6431 Arc::new(crate::Page::new(11)),
6432 Arc::new(crate::Page::new(12)),
6433 Arc::new(crate::Page::new(13)),
6434 ];
6435 let c = wal
6436 .read_frames_batch(2, &target_pages, buffer_pool, None)
6437 .unwrap();
6438 io.wait_for_completion(c).unwrap();
6439
6440 for (idx, page) in target_pages.iter().enumerate() {
6441 assert!(page.is_loaded(), "page {} should be loaded", page.get().id);
6442 assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6443 assert_eq!(page.wal_tag_pair(), ((idx + 2) as u64, 0));
6444 assert_eq!(page.get_contents().as_ptr(), expected[idx + 1].as_slice());
6445 }
6446 }
6447
6448 #[test]
6449 fn read_frames_batch_follows_physical_frame_order_not_page_id_order() {
6450 let page_size = 512;
6451 let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6452 let source_pages = vec![
6453 page_with_pattern(7, 0x71, &buffer_pool),
6454 page_with_pattern(2, 0x22, &buffer_pool),
6455 page_with_pattern(5, 0x55, &buffer_pool),
6456 page_with_pattern(9, 0x99, &buffer_pool),
6457 ];
6458 let expected = append_test_pages(&io, &wal, page_size, &source_pages);
6459
6460 let target_pages = vec![
6461 Arc::new(crate::Page::new(7)),
6462 Arc::new(crate::Page::new(2)),
6463 Arc::new(crate::Page::new(5)),
6464 Arc::new(crate::Page::new(9)),
6465 ];
6466 let c = wal
6467 .read_frames_batch(1, &target_pages, buffer_pool, None)
6468 .unwrap();
6469 io.wait_for_completion(c).unwrap();
6470
6471 for (idx, page) in target_pages.iter().enumerate() {
6472 assert_eq!(
6473 page.get_contents().as_ptr(),
6474 expected[idx].as_slice(),
6475 "frame-order read should preserve page {} contents",
6476 page.get().id
6477 );
6478 assert_eq!(page.wal_tag_pair(), ((idx + 1) as u64, 0));
6479 }
6480 }
6481
6482 #[test]
6483 fn read_frames_batch_short_read_errors_and_clears_page_locks() {
6484 let page_size = 512;
6485 let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6486 let source_pages = vec![
6487 page_with_pattern(20, 0x20, &buffer_pool),
6488 page_with_pattern(21, 0x21, &buffer_pool),
6489 ];
6490 append_test_pages(&io, &wal, page_size, &source_pages);
6491
6492 let target_pages = vec![
6493 Arc::new(crate::Page::new(20)),
6494 Arc::new(crate::Page::new(21)),
6495 Arc::new(crate::Page::new(22)),
6496 ];
6497 let c = wal
6498 .read_frames_batch(1, &target_pages, buffer_pool, None)
6499 .unwrap();
6500 let err = wait_for_completion_error(&io, c);
6501
6502 assert!(
6503 matches!(err, CompletionError::ShortReadWalFrame { .. }),
6504 "unexpected error: {err:?}"
6505 );
6506 for page in &target_pages {
6507 assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6508 assert!(
6509 !page.is_loaded(),
6510 "page {} should not be loaded",
6511 page.get().id
6512 );
6513 assert!(
6514 !page.has_wal_tag(),
6515 "page {} should not be tagged",
6516 page.get().id
6517 );
6518 }
6519 }
6520
6521 #[test]
6522 fn read_frames_batch_page_number_mismatch_returns_error_not_panic() {
6523 let page_size = 512;
6524 let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6525 let source_pages = vec![
6526 page_with_pattern(30, 0x30, &buffer_pool),
6527 page_with_pattern(31, 0x31, &buffer_pool),
6528 ];
6529 append_test_pages(&io, &wal, page_size, &source_pages);
6530
6531 let target_pages = vec![
6532 Arc::new(crate::Page::new(30)),
6533 Arc::new(crate::Page::new(99)),
6534 ];
6535 let c = wal
6536 .read_frames_batch(1, &target_pages, buffer_pool, None)
6537 .unwrap();
6538 let err = wait_for_completion_error(&io, c);
6539
6540 assert!(
6541 matches!(
6542 err,
6543 CompletionError::WalFramePageMismatch {
6544 frame_id: 2,
6545 expected: 99,
6546 actual: 31
6547 }
6548 ),
6549 "unexpected error: {err:?}"
6550 );
6551 for page in &target_pages {
6552 assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6553 assert!(
6554 !page.is_loaded(),
6555 "page {} should not be loaded",
6556 page.get().id
6557 );
6558 assert!(
6559 !page.has_wal_tag(),
6560 "page {} should not be tagged",
6561 page.get().id
6562 );
6563 assert!(
6564 page.get().buffer.is_none(),
6565 "page {} should not retain a buffer",
6566 page.get().id
6567 );
6568 }
6569 }
6570
6571 fn set_shared_snapshot(shared: &Arc<RwLock<WalFileShared>>, snapshot: WalSnapshot) {
6572 let mut guard = shared.write();
6573 guard
6574 .metadata
6575 .max_frame
6576 .store(snapshot.max_frame, Ordering::Release);
6577 guard
6578 .metadata
6579 .nbackfills
6580 .store(snapshot.nbackfills, Ordering::Release);
6581 guard.metadata.last_checksum = snapshot.last_checksum;
6582 guard.metadata.wal_header.lock().checkpoint_seq = snapshot.checkpoint_seq;
6583 guard
6584 .metadata
6585 .transaction_count
6586 .store(snapshot.transaction_count, Ordering::Release);
6587 }
6588
6589 fn make_test_coordination(shared: &Arc<RwLock<WalFileShared>>) -> InProcessWalCoordination {
6590 InProcessWalCoordination::new(shared.clone())
6591 }
6592
6593 #[cfg(host_shared_wal)]
6594 fn make_test_shm_coordination(
6595 shared: &Arc<RwLock<WalFileShared>>,
6596 path: &std::path::Path,
6597 ) -> (Arc<MappedSharedWalCoordination>, ShmWalCoordination) {
6598 let io = shared_wal_test_io();
6599 let authority =
6600 Arc::new(MappedSharedWalCoordination::create_or_open(&io, path, 64).unwrap());
6601 let coordination = ShmWalCoordination::new(shared.clone(), authority.clone());
6602 (authority, coordination)
6603 }
6604
6605 #[cfg(host_shared_wal)]
6606 fn active_shared_reader_slot_count(authority: &MappedSharedWalCoordination) -> usize {
6607 let reader_slot_count = authority.snapshot().reader_slot_count;
6608 (0..reader_slot_count)
6609 .filter(|&slot_index| authority.reader_owner(slot_index).is_some())
6610 .count()
6611 }
6612
6613 #[cfg(host_shared_wal)]
6614 fn write_test_wal_with_single_commit_frame(
6615 io: &Arc<dyn IO>,
6616 wal_path: &std::path::Path,
6617 ) -> SharedWalCoordinationHeader {
6618 let file = io
6619 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
6620 .unwrap();
6621 let mut wal_header = sqlite3_ondisk::WalHeader {
6622 page_size: 4096,
6623 checkpoint_seq: 5,
6624 salt_1: 17,
6625 salt_2: 23,
6626 ..sqlite3_ondisk::WalHeader::new()
6627 };
6628 let use_native_endian = cfg!(target_endian = "big") == ((wal_header.magic & 1) != 0);
6629 let mut header_prefix = [0u8; WAL_HEADER_SIZE - 8];
6630 header_prefix[0..4].copy_from_slice(&wal_header.magic.to_be_bytes());
6631 header_prefix[4..8].copy_from_slice(&wal_header.file_format.to_be_bytes());
6632 header_prefix[8..12].copy_from_slice(&wal_header.page_size.to_be_bytes());
6633 header_prefix[12..16].copy_from_slice(&wal_header.checkpoint_seq.to_be_bytes());
6634 header_prefix[16..20].copy_from_slice(&wal_header.salt_1.to_be_bytes());
6635 header_prefix[20..24].copy_from_slice(&wal_header.salt_2.to_be_bytes());
6636 let header_checksum =
6637 sqlite3_ondisk::checksum_wal(&header_prefix, &wal_header, (0, 0), use_native_endian);
6638 wal_header.checksum_1 = header_checksum.0;
6639 wal_header.checksum_2 = header_checksum.1;
6640
6641 io.wait_for_completion(
6642 sqlite3_ondisk::begin_write_wal_header(file.as_ref(), &wal_header).unwrap(),
6643 )
6644 .unwrap();
6645
6646 let buffer_pool = BufferPool::begin_init(io, BufferPool::TEST_ARENA_SIZE);
6647 buffer_pool
6648 .finalize_with_page_size(wal_header.page_size as usize)
6649 .unwrap();
6650 #[allow(unused_mut)]
6651 let mut page = vec![0x5a; wal_header.page_size as usize];
6652 #[cfg(clt_turso_feature = "checksum")]
6653 crate::storage::checksum::ChecksumContext::new()
6654 .add_checksum_to_page(&mut page, 7)
6655 .unwrap();
6656 let (frame_checksum, frame_buf) = sqlite3_ondisk::prepare_wal_frame(
6657 &buffer_pool,
6658 &wal_header,
6659 header_checksum,
6660 wal_header.page_size,
6661 7,
6662 1,
6663 &page,
6664 );
6665 let c = file
6666 .pwrite(
6667 WAL_HEADER_SIZE as u64,
6668 frame_buf,
6669 Completion::new_write(|_| {}),
6670 )
6671 .unwrap();
6672 io.wait_for_completion(c).unwrap();
6673 let c = file
6674 .sync(Completion::new_sync(|_| {}), crate::io::FileSyncType::Fsync)
6675 .unwrap();
6676 io.wait_for_completion(c).unwrap();
6677
6678 SharedWalCoordinationHeader {
6679 max_frame: 1,
6680 nbackfills: 0,
6681 transaction_count: 9,
6682 visibility_generation: 3,
6683 checkpoint_seq: wal_header.checkpoint_seq,
6684 checkpoint_epoch: 7,
6685 page_size: wal_header.page_size,
6686 salt_1: wal_header.salt_1,
6687 salt_2: wal_header.salt_2,
6688 checksum_1: frame_checksum.0,
6689 checksum_2: frame_checksum.1,
6690 reader_slot_count: 64,
6691 }
6692 }
6693
6694 #[cfg(host_shared_wal)]
6695 fn open_test_db_file_for_wal(
6696 io: &Arc<dyn IO>,
6697 wal_path: &std::path::Path,
6698 ) -> Arc<dyn DatabaseStorage> {
6699 let db_path = wal_path.with_extension("db");
6700 Arc::new(DatabaseFile::new(
6701 io.open_file(db_path.to_str().unwrap(), crate::OpenFlags::Create, false)
6702 .unwrap(),
6703 ))
6704 }
6705
6706 #[test]
6707 #[cfg(host_shared_wal)]
6708 fn test_read_frame_keeps_epoch_from_issue_time() {
6709 let dir = tempfile::tempdir().unwrap();
6710 let wal_path = dir.path().join("epoch-race.db-wal");
6711 let io = shared_wal_test_io();
6712 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
6713
6714 let file = io
6715 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
6716 .unwrap();
6717 let shared = WalFileShared::new_shared(file.clone()).unwrap();
6718 let deferred_file = Arc::new(DeferredReadFile::new(file));
6719 {
6720 let mut shared = shared.write();
6721 shared.runtime.file = Some(deferred_file.clone());
6722 shared
6723 .runtime
6724 .epoch
6725 .store(snapshot.checkpoint_epoch, Ordering::Release);
6726 }
6727
6728 let coordination: Arc<dyn WalCoordination> = Arc::new(make_test_coordination(&shared));
6729 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6730 buffer_pool
6731 .finalize_with_page_size(snapshot.page_size as usize)
6732 .unwrap();
6733 let wal = WalFile::new_with_coordination(
6734 io.clone(),
6735 coordination,
6736 (
6737 (snapshot.checksum_1, snapshot.checksum_2),
6738 snapshot.max_frame,
6739 ),
6740 buffer_pool.clone(),
6741 );
6742
6743 let page = Arc::new(crate::storage::pager::Page::new(7));
6744 let issued_epoch = wal.coordination.checkpoint_epoch();
6745 let completion = wal.read_frame(1, page.clone(), buffer_pool).unwrap();
6746
6747 wal.increment_checkpoint_epoch();
6748 deferred_file.complete_pending_reads();
6749 io.wait_for_completion(completion).unwrap();
6750
6751 assert_eq!(
6752 page.wal_tag_pair(),
6753 (1, issued_epoch),
6754 "WAL reads must retain the epoch from when the read was issued"
6755 );
6756 }
6757
6758 #[cfg(clt_turso_tests)]
6759 fn read_slots_with_readers(shared: &WalFileShared) -> Vec<usize> {
6760 shared
6761 .runtime
6762 .read_locks
6763 .iter()
6764 .enumerate()
6765 .filter_map(|(slot, lock)| {
6766 let state = lock.0.load(Ordering::Acquire);
6767 let has_readers = (state & super::TursoRwLock::READER_COUNT_MASK) != 0;
6768 has_readers.then_some(slot)
6769 })
6770 .collect()
6771 }
6772
6773 fn wal_header_snapshot(shared: &Arc<RwLock<WalFileShared>>) -> (u32, u32, u32, u32) {
6774 let shared_guard = shared.read();
6776 let hdr = shared_guard.metadata.wal_header.lock();
6777 (hdr.checkpoint_seq, hdr.salt_1, hdr.salt_2, hdr.page_size)
6778 }
6779
6780 #[test]
6781 fn test_wal_connection_state_round_trip() {
6782 let (_shared, wal) = make_test_wal();
6783 let state = WalConnectionState::new(
6784 WalSnapshot {
6785 max_frame: 11,
6786 nbackfills: 7,
6787 last_checksum: (31, 47),
6788 checkpoint_seq: 5,
6789 transaction_count: 13,
6790 },
6791 ReadGuardKind::ReadMark(NonZeroUsize::new(3).unwrap()),
6792 );
6793
6794 wal.install_connection_state(state);
6795
6796 assert_eq!(wal.connection_state(), state);
6797 assert_eq!(wal.connection_state().snapshot.min_frame(), 8);
6798 }
6799
6800 #[test]
6801 fn test_wal_explicit_backend_constructor_does_not_keep_shared_handle() {
6802 let io = shared_wal_test_io();
6803 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6804 let shared = WalFileShared::new_noop();
6805 let coordination: Arc<dyn WalCoordination> =
6806 Arc::new(InProcessWalCoordination::new(shared.clone()));
6807
6808 assert_eq!(Arc::strong_count(&shared), 2);
6809
6810 let _wal = WalFile::new_with_coordination(io, coordination, ((0, 0), 0), buffer_pool);
6811
6812 assert_eq!(Arc::strong_count(&shared), 2);
6813 }
6814
6815 #[test]
6816 fn test_mvcc_refresh_updates_snapshot_without_changing_read_guard() {
6817 let (shared, wal) = make_test_wal();
6818 let initial = WalSnapshot {
6819 max_frame: 4,
6820 nbackfills: 2,
6821 last_checksum: (9, 10),
6822 checkpoint_seq: 1,
6823 transaction_count: 3,
6824 };
6825 set_shared_snapshot(&shared, initial);
6826 wal.install_connection_state(WalConnectionState::new(
6827 initial,
6828 ReadGuardKind::ReadMark(NonZeroUsize::new(2).unwrap()),
6829 ));
6830
6831 assert!(!wal.mvcc_refresh_if_db_changed());
6832
6833 let updated = WalSnapshot {
6834 max_frame: 8,
6835 nbackfills: 5,
6836 last_checksum: (21, 34),
6837 checkpoint_seq: 7,
6838 transaction_count: 4,
6839 };
6840 set_shared_snapshot(&shared, updated);
6841
6842 assert!(wal.mvcc_refresh_if_db_changed());
6843 assert_eq!(
6844 wal.connection_state(),
6845 WalConnectionState::new(
6846 updated,
6847 ReadGuardKind::ReadMark(NonZeroUsize::new(2).unwrap())
6848 )
6849 );
6850 }
6851
6852 #[test]
6853 fn test_in_process_coordination_uses_shared_authority() {
6854 let (shared, _wal) = make_test_wal();
6855 let coordination = make_test_coordination(&shared);
6856 let snapshot = WalSnapshot {
6857 max_frame: 9,
6858 nbackfills: 3,
6859 last_checksum: (55, 89),
6860 checkpoint_seq: 7,
6861 transaction_count: 11,
6862 };
6863 set_shared_snapshot(&shared, snapshot);
6864 {
6865 let guard = shared.write();
6866 guard.runtime.epoch.store(5, Ordering::Release);
6867 guard.runtime.frame_cache.lock().extend([
6868 (1, vec![1, 4, 8]),
6869 (2, vec![2, 6]),
6870 (3, vec![3]),
6871 ]);
6872 }
6873
6874 assert_eq!(coordination.load_snapshot(), snapshot);
6875 assert_eq!(coordination.checkpoint_epoch(), 5);
6876 assert_eq!(coordination.find_frame(1, 4, 9, None), Some(8));
6877 assert_eq!(coordination.find_frame(2, 4, 9, Some(5)), Some(2));
6878 assert_eq!(coordination.iter_latest_frames(4, 9), vec![(1, 8), (2, 6)]);
6879
6880 coordination.publish_commit(WalCommitState {
6881 max_frame: 12,
6882 last_checksum: (144, 233),
6883 transaction_count: 12,
6884 });
6885 let published = coordination.load_snapshot();
6886 assert_eq!(published.max_frame, 12);
6887 assert_eq!(published.last_checksum, (144, 233));
6888 assert_eq!(published.transaction_count, 12);
6889 assert_eq!(published.nbackfills, snapshot.nbackfills);
6890 assert_eq!(published.checkpoint_seq, snapshot.checkpoint_seq);
6891 }
6892
6893 #[test]
6894 fn test_in_process_coordination_publishes_checkpoint_and_restart_state() {
6895 let (shared, _wal) = make_test_wal();
6896 let coordination = make_test_coordination(&shared);
6897 let io = PlatformIO::new().unwrap();
6898 let snapshot = WalSnapshot {
6899 max_frame: 9,
6900 nbackfills: 3,
6901 last_checksum: (55, 89),
6902 checkpoint_seq: 7,
6903 transaction_count: 11,
6904 };
6905 set_shared_snapshot(&shared, snapshot);
6906 {
6907 let guard = shared.write();
6908 let mut header = guard.metadata.wal_header.lock();
6909 header.page_size = 4096;
6910 header.checksum_1 = 144;
6911 header.checksum_2 = 233;
6912 guard.metadata.initialized.store(true, Ordering::Release);
6913 guard.runtime.epoch.store(5, Ordering::Release);
6914 guard
6915 .runtime
6916 .frame_cache
6917 .lock()
6918 .extend([(1, vec![1, 4, 8]), (2, vec![2, 6])]);
6919 }
6920
6921 coordination.publish_backfill(8);
6922 assert_eq!(coordination.load_snapshot().nbackfills, 8);
6923 assert_eq!(coordination.bump_checkpoint_epoch(), 5);
6924 assert_eq!(coordination.checkpoint_epoch(), 6);
6925
6926 assert!(coordination.try_read_mark_exclusive(0));
6927 let restarted = coordination.begin_restart(&io).unwrap();
6928 coordination.end_restart();
6929 coordination.unlock_read_mark(0);
6930
6931 assert_eq!(restarted.max_frame, 0);
6932 assert_eq!(restarted.nbackfills, 0);
6933 assert_eq!(restarted.last_checksum, (144, 233));
6934 assert_eq!(restarted.checkpoint_seq, 8);
6935 assert_eq!(restarted.transaction_count, 11);
6936
6937 let guard = shared.read();
6938 assert_eq!(guard.runtime.read_locks[0].get_value(), 0);
6939 assert_eq!(guard.runtime.read_locks[1].get_value(), 0);
6940 for lock in &guard.runtime.read_locks[2..] {
6941 assert_eq!(lock.get_value(), READMARK_NOT_USED);
6942 }
6943 assert!(guard.runtime.frame_cache.lock().is_empty());
6944 assert!(!guard.metadata.initialized.load(Ordering::Acquire));
6945 }
6946
6947 #[test]
6948 fn test_in_process_coordination_manages_frame_cache() {
6949 let (shared, _wal) = make_test_wal();
6950 let coordination = make_test_coordination(&shared);
6951
6952 coordination.cache_frame(7, 2);
6955 coordination.cache_frame(9, 4);
6956 coordination.cache_frame(7, 5);
6957
6958 assert_eq!(coordination.find_frame(7, 0, 5, None), Some(5));
6959 assert_eq!(coordination.iter_latest_frames(0, 5), vec![(7, 5), (9, 4)]);
6960
6961 coordination.rollback_cache(4);
6962
6963 assert_eq!(coordination.find_frame(7, 0, 5, None), Some(2));
6964 assert_eq!(coordination.iter_latest_frames(0, 5), vec![(7, 2), (9, 4)]);
6965 assert_eq!(
6966 shared.read().runtime.frame_cache.lock().get(&7),
6967 Some(&vec![2])
6968 );
6969 }
6970
6971 #[test]
6980 fn cache_frame_purges_stale_mapping_on_frame_slot_reuse() {
6981 let (shared, _wal) = make_test_wal();
6982 let coordination = make_test_coordination(&shared);
6983
6984 coordination.cache_frame(7, 3);
6986 coordination.cache_frame(9, 4);
6987 coordination.cache_frame(7, 5);
6988 assert_eq!(coordination.find_frame(9, 0, 10, None), Some(4));
6989
6990 coordination.cache_frame(11, 4);
6994 coordination.cache_frame(13, 5);
6995
6996 assert_eq!(
6997 coordination.find_frame(9, 0, 10, None),
6998 None,
6999 "stale page 9 -> frame 4 mapping must be purged once slot 4 is reused"
7000 );
7001 assert_eq!(
7002 coordination.find_frame(11, 0, 10, None),
7003 Some(4),
7004 "page 11 now owns frame slot 4"
7005 );
7006 assert_eq!(
7007 coordination.find_frame(13, 0, 10, None),
7008 Some(5),
7009 "page 13 now owns frame slot 5"
7010 );
7011 assert_eq!(coordination.find_frame(7, 0, 10, None), Some(3));
7013 }
7014
7015 #[test]
7016 fn test_savepoint_rollback_discards_frame_cache_past_rollback_point() {
7017 let (shared, wal) = make_test_wal();
7018 let coordination = make_test_coordination(&shared);
7019 set_shared_snapshot(
7020 &shared,
7021 WalSnapshot {
7022 max_frame: 25,
7023 nbackfills: 0,
7024 last_checksum: (55, 89),
7025 checkpoint_seq: 1,
7026 transaction_count: 3,
7027 },
7028 );
7029
7030 coordination.cache_frame(7, 10);
7034 coordination.cache_frame(9, 26);
7035 coordination.cache_frame(11, 28);
7036 wal.max_frame.store(30, Ordering::Release);
7037
7038 wal.rollback(Some(RollbackTo {
7039 frame: 27,
7040 checksum: (13, 21),
7041 checkpoint_seq: 1,
7042 }));
7043
7044 assert_eq!(coordination.find_frame(7, 0, 30, None), Some(10));
7047 assert_eq!(coordination.find_frame(9, 0, 30, None), Some(26));
7048 assert_eq!(coordination.find_frame(11, 0, 30, None), None);
7049 assert_eq!(wal.get_max_frame(), 27);
7050 assert_eq!(*wal.last_checksum.read(), (13, 21));
7051
7052 wal.rollback(Some(RollbackTo {
7055 frame: 25,
7056 checksum: (55, 89),
7057 checkpoint_seq: 1,
7058 }));
7059 assert_eq!(coordination.find_frame(9, 0, 30, None), None);
7060 assert_eq!(wal.get_max_frame(), 25);
7061 }
7062
7063 #[test]
7064 fn test_in_process_coordination_transaction_guards() {
7065 let (shared, _wal) = make_test_wal();
7066 let coordination = make_test_coordination(&shared);
7067
7068 let db_file_snapshot = WalSnapshot {
7069 max_frame: 0,
7070 nbackfills: 0,
7071 last_checksum: (0, 0),
7072 checkpoint_seq: 0,
7073 transaction_count: 0,
7074 };
7075 set_shared_snapshot(&shared, db_file_snapshot);
7076 let read_guard = coordination.try_begin_read_tx(db_file_snapshot).unwrap();
7077 assert_eq!(read_guard, ReadGuardKind::DbFile);
7078 coordination.end_read_tx(read_guard);
7079
7080 let wal_snapshot = WalSnapshot {
7081 max_frame: 5,
7082 nbackfills: 2,
7083 last_checksum: (11, 13),
7084 checkpoint_seq: 1,
7085 transaction_count: 2,
7086 };
7087 set_shared_snapshot(&shared, wal_snapshot);
7088 let read_guard = coordination.try_begin_read_tx(wal_snapshot).unwrap();
7089 assert!(matches!(read_guard, ReadGuardKind::ReadMark(_)));
7090 coordination.end_read_tx(read_guard);
7091
7092 assert!(coordination.try_begin_write_tx());
7093 assert!(!coordination.try_begin_write_tx());
7094 coordination.end_write_tx();
7095 }
7096
7097 #[cfg(host_shared_wal)]
7098 #[test]
7099 #[cfg_attr(
7100 windows,
7101 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
7102 )]
7103 fn test_shm_coordination_uses_shared_authority() {
7104 let dir = tempfile::tempdir().unwrap();
7105 let wal_path = dir.path().join("test.db-wal");
7106 let shm_path = dir.path().join("test.db-tshm");
7107 let io = shared_wal_test_io();
7108 let file_a = io
7109 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7110 .unwrap();
7111 let file_b = io
7112 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7113 .unwrap();
7114 let shared_a = WalFileShared::new_shared(file_a).unwrap();
7115 let shared_b = WalFileShared::new_shared(file_b).unwrap();
7116 let snapshot = WalSnapshot {
7117 max_frame: 14,
7118 nbackfills: 8,
7119 last_checksum: (31, 37),
7120 checkpoint_seq: 5,
7121 transaction_count: 9,
7122 };
7123 set_shared_snapshot(&shared_a, snapshot);
7124 {
7125 let shared = shared_a.write();
7126 let mut header = shared.metadata.wal_header.lock();
7127 header.page_size = 4096;
7128 header.salt_1 = 17;
7129 header.salt_2 = 23;
7130 header.checksum_1 = snapshot.last_checksum.0;
7131 header.checksum_2 = snapshot.last_checksum.1;
7132 }
7133
7134 let (_authority_a, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
7135 let (authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
7136 coordination_a.cache_frame(7, 2);
7137 coordination_a.cache_frame(9, 4);
7138 coordination_a.cache_frame(7, 5);
7139
7140 assert_eq!(coordination_b.load_snapshot(), snapshot);
7141 assert_eq!(coordination_b.wal_header().page_size, 4096);
7142 assert_eq!(coordination_b.wal_header().salt_1, 17);
7143 assert_eq!(coordination_b.wal_header().salt_2, 23);
7144 assert_eq!(coordination_b.find_frame(7, 0, 5, None), Some(5));
7145 assert_eq!(
7146 coordination_b.iter_latest_frames(0, 5),
7147 vec![(7, 5), (9, 4)]
7148 );
7149 assert_eq!(coordination_a.checkpoint_epoch(), 0);
7150 assert_eq!(coordination_b.bump_checkpoint_epoch(), 0);
7151 assert_eq!(coordination_a.checkpoint_epoch(), 1);
7152
7153 assert!(coordination_a.try_begin_write_tx());
7154 assert!(!coordination_b.try_begin_write_tx());
7155 coordination_a.end_write_tx();
7156
7157 let read_guard = coordination_a.try_begin_read_tx(snapshot).unwrap();
7158 assert_eq!(
7159 authority_b.min_active_reader_frame(),
7160 Some(snapshot.max_frame)
7161 );
7162 coordination_a.end_read_tx(read_guard);
7163 assert_eq!(authority_b.min_active_reader_frame(), None);
7164
7165 coordination_b.publish_commit(WalCommitState {
7166 max_frame: 21,
7167 last_checksum: (55, 89),
7168 transaction_count: 10,
7169 });
7170 assert_eq!(
7171 coordination_a.load_snapshot(),
7172 WalSnapshot {
7173 max_frame: 21,
7174 nbackfills: 8,
7175 last_checksum: (55, 89),
7176 checkpoint_seq: 5,
7177 transaction_count: 10,
7178 }
7179 );
7180
7181 coordination_b.rollback_cache(4);
7182 assert_eq!(coordination_a.find_frame(7, 0, 5, None), Some(2));
7183 assert_eq!(
7184 coordination_a.iter_latest_frames(0, 5),
7185 vec![(7, 2), (9, 4)]
7186 );
7187 assert!(shm_path.exists());
7188 }
7189
7190 #[cfg(host_shared_wal)]
7191 #[test]
7192 fn test_shm_coordination_many_same_snapshot_readers_share_one_published_slot() {
7193 let dir = tempfile::tempdir().unwrap();
7194 let wal_path = dir.path().join("test-many-same-snapshot-readers.db-wal");
7195 let shm_path = dir.path().join("test-many-same-snapshot-readers.db-tshm");
7196 let io = shared_wal_test_io();
7197 let file = io
7198 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7199 .unwrap();
7200 let shared = WalFileShared::new_shared(file).unwrap();
7201 let snapshot = WalSnapshot {
7202 max_frame: 9,
7203 nbackfills: 2,
7204 last_checksum: (31, 37),
7205 checkpoint_seq: 5,
7206 transaction_count: 9,
7207 };
7208 set_shared_snapshot(&shared, snapshot);
7209 {
7210 let shared = shared.write();
7211 let mut header = shared.metadata.wal_header.lock();
7212 header.page_size = 4096;
7213 header.salt_1 = 17;
7214 header.salt_2 = 23;
7215 header.checksum_1 = snapshot.last_checksum.0;
7216 header.checksum_2 = snapshot.last_checksum.1;
7217 }
7218
7219 let authority =
7220 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7221 let mut readers = Vec::new();
7222 for _ in 0..128 {
7223 let coordination = ShmWalCoordination::new(shared.clone(), authority.clone());
7224 let read_guard = coordination
7225 .try_begin_read_tx(snapshot)
7226 .expect("same-snapshot readers should share a published reader barrier");
7227 readers.push((coordination, read_guard));
7228 }
7229
7230 assert_eq!(
7231 authority.min_active_reader_frame(),
7232 Some(snapshot.max_frame)
7233 );
7234 assert_eq!(
7235 active_shared_reader_slot_count(&authority),
7236 1,
7237 "same-snapshot readers should collapse onto one shared reader slot"
7238 );
7239
7240 for (coordination, read_guard) in readers {
7241 coordination.end_read_tx(read_guard);
7242 }
7243 assert_eq!(authority.min_active_reader_frame(), None);
7244 assert_eq!(active_shared_reader_slot_count(&authority), 0);
7245 }
7246
7247 #[cfg(host_shared_wal)]
7248 #[test]
7249 fn test_shm_coordination_uses_one_published_slot_per_active_snapshot_generation() {
7250 let dir = tempfile::tempdir().unwrap();
7251 let wal_path = dir.path().join("test-mixed-snapshot-readers.db-wal");
7252 let shm_path = dir.path().join("test-mixed-snapshot-readers.db-tshm");
7253 let io = shared_wal_test_io();
7254 let file = io
7255 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7256 .unwrap();
7257 let shared = WalFileShared::new_shared(file).unwrap();
7258 let snapshot_a = WalSnapshot {
7259 max_frame: 5,
7260 nbackfills: 2,
7261 last_checksum: (31, 37),
7262 checkpoint_seq: 5,
7263 transaction_count: 9,
7264 };
7265 set_shared_snapshot(&shared, snapshot_a);
7266 {
7267 let shared = shared.write();
7268 let mut header = shared.metadata.wal_header.lock();
7269 header.page_size = 4096;
7270 header.salt_1 = 17;
7271 header.salt_2 = 23;
7272 header.checksum_1 = snapshot_a.last_checksum.0;
7273 header.checksum_2 = snapshot_a.last_checksum.1;
7274 }
7275
7276 let authority =
7277 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7278 let reader_a1 = ShmWalCoordination::new(shared.clone(), authority.clone());
7279 let guard_a1 = reader_a1.try_begin_read_tx(snapshot_a).unwrap();
7280 let reader_a2 = ShmWalCoordination::new(shared.clone(), authority.clone());
7281 let guard_a2 = reader_a2.try_begin_read_tx(snapshot_a).unwrap();
7282 assert_eq!(
7283 authority.min_active_reader_frame(),
7284 Some(snapshot_a.max_frame)
7285 );
7286 assert_eq!(active_shared_reader_slot_count(&authority), 1);
7287
7288 let snapshot_b = WalSnapshot {
7289 max_frame: 9,
7290 nbackfills: 2,
7291 last_checksum: (41, 43),
7292 checkpoint_seq: 5,
7293 transaction_count: 10,
7294 };
7295 reader_a1.publish_commit(WalCommitState {
7296 max_frame: snapshot_b.max_frame,
7297 last_checksum: snapshot_b.last_checksum,
7298 transaction_count: snapshot_b.transaction_count,
7299 });
7300
7301 let reader_b1 = ShmWalCoordination::new(shared.clone(), authority.clone());
7302 let guard_b1 = reader_b1.try_begin_read_tx(snapshot_b).unwrap();
7303 let reader_b2 = ShmWalCoordination::new(shared, authority.clone());
7304 let guard_b2 = reader_b2.try_begin_read_tx(snapshot_b).unwrap();
7305
7306 assert_eq!(
7307 active_shared_reader_slot_count(&authority),
7308 2,
7309 "distinct live snapshots should each publish one shared reader slot"
7310 );
7311 assert_eq!(
7312 authority.min_active_reader_frame(),
7313 Some(snapshot_a.max_frame),
7314 "checkpoint barrier should stay pinned to the oldest active snapshot"
7315 );
7316
7317 reader_a1.end_read_tx(guard_a1);
7318 reader_a2.end_read_tx(guard_a2);
7319 assert_eq!(
7320 authority.min_active_reader_frame(),
7321 Some(snapshot_b.max_frame)
7322 );
7323 assert_eq!(active_shared_reader_slot_count(&authority), 1);
7324
7325 reader_b1.end_read_tx(guard_b1);
7326 reader_b2.end_read_tx(guard_b2);
7327 assert_eq!(authority.min_active_reader_frame(), None);
7328 assert_eq!(active_shared_reader_slot_count(&authority), 0);
7329 }
7330
7331 #[cfg(host_shared_wal)]
7332 #[test]
7333 #[cfg_attr(
7334 windows,
7335 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
7336 )]
7337 fn test_shm_coordination_shared_index_grows_past_old_fixed_limit() {
7338 const OLD_FIXED_LIMIT: u64 = 65_536;
7339
7340 let dir = tempfile::tempdir().unwrap();
7341 let wal_path = dir.path().join("test.db-wal");
7342 let shm_path = dir.path().join("test.db-tshm");
7343 let io = shared_wal_test_io();
7344 let file_a = io
7345 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7346 .unwrap();
7347 let file_b = io
7348 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7349 .unwrap();
7350 let shared_a = WalFileShared::new_shared(file_a).unwrap();
7351 let shared_b = WalFileShared::new_shared(file_b).unwrap();
7352 let snapshot = WalSnapshot {
7353 max_frame: OLD_FIXED_LIMIT + 2,
7354 nbackfills: 0,
7355 last_checksum: (31, 37),
7356 checkpoint_seq: 5,
7357 transaction_count: OLD_FIXED_LIMIT + 2,
7358 };
7359 set_shared_snapshot(&shared_a, snapshot);
7360 {
7361 let shared = shared_a.write();
7362 let mut header = shared.metadata.wal_header.lock();
7363 header.page_size = 4096;
7364 header.salt_1 = 17;
7365 header.salt_2 = 23;
7366 header.checksum_1 = snapshot.last_checksum.0;
7367 header.checksum_2 = snapshot.last_checksum.1;
7368 }
7369
7370 let (_authority_a, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
7371 let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
7372
7373 coordination_a.cache_frame(7, 2);
7374 for frame_id in 3..=OLD_FIXED_LIMIT + 1 {
7375 coordination_a.cache_frame(100 + (frame_id % 31), frame_id);
7376 }
7377 coordination_a.cache_frame(7, OLD_FIXED_LIMIT + 2);
7378
7379 assert_eq!(
7380 coordination_b.find_frame(7, 0, OLD_FIXED_LIMIT + 2, None),
7381 Some(OLD_FIXED_LIMIT + 2)
7382 );
7383 assert_eq!(
7384 coordination_b.find_frame(7, 0, OLD_FIXED_LIMIT + 2, Some(OLD_FIXED_LIMIT + 1)),
7385 Some(2)
7386 );
7387 assert!(coordination_b
7388 .iter_latest_frames(0, OLD_FIXED_LIMIT + 2)
7389 .contains(&(7, OLD_FIXED_LIMIT + 2)));
7390 }
7391
7392 #[cfg(host_shared_wal)]
7393 #[test]
7394 fn test_shm_coordination_restart_uses_authority_snapshot() {
7395 let dir = tempfile::tempdir().unwrap();
7396 let wal_path = dir.path().join("test.db-wal");
7397 let shm_path = dir.path().join("test.db-tshm");
7398 let io = PlatformIO::new().unwrap();
7399 let file_a = io
7400 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7401 .unwrap();
7402 let file_b = io
7403 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7404 .unwrap();
7405 let shared_a = WalFileShared::new_shared(file_a).unwrap();
7406 let shared_b = WalFileShared::new_shared(file_b).unwrap();
7407 let snapshot = WalSnapshot {
7408 max_frame: 12,
7409 nbackfills: 12,
7410 last_checksum: (31, 37),
7411 checkpoint_seq: 5,
7412 transaction_count: 9,
7413 };
7414 set_shared_snapshot(&shared_a, snapshot);
7415 {
7416 let shared = shared_a.write();
7417 let mut header = shared.metadata.wal_header.lock();
7418 header.page_size = 4096;
7419 header.salt_1 = 17;
7420 header.salt_2 = 23;
7421 header.checksum_1 = snapshot.last_checksum.0;
7422 header.checksum_2 = snapshot.last_checksum.1;
7423 shared.metadata.initialized.store(true, Ordering::Release);
7424 shared.runtime.epoch.store(5, Ordering::Release);
7425 }
7426
7427 let (_authority_a, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
7428 let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
7429 coordination_a.cache_frame(7, 2);
7430 coordination_a.cache_frame(9, 4);
7431
7432 {
7433 let mut shared = shared_b.write();
7434 shared.metadata.max_frame.store(99, Ordering::Release);
7435 shared.metadata.nbackfills.store(77, Ordering::Release);
7436 shared.metadata.last_checksum = (1, 2);
7437 shared
7438 .metadata
7439 .transaction_count
7440 .store(42, Ordering::Release);
7441 shared.runtime.epoch.store(99, Ordering::Release);
7442 shared.metadata.initialized.store(true, Ordering::Release);
7443 let mut header = shared.metadata.wal_header.lock();
7444 header.checkpoint_seq = 88;
7445 header.page_size = 2048;
7446 header.salt_1 = 91;
7447 header.salt_2 = 92;
7448 header.checksum_1 = 93;
7449 header.checksum_2 = 94;
7450 }
7451
7452 assert!(coordination_b.fallback.try_read_mark_exclusive(0));
7453 let restarted = coordination_b.begin_restart(&io).unwrap();
7454 coordination_b.end_restart();
7455 coordination_b.fallback.unlock_read_mark(0);
7456
7457 assert_eq!(
7458 restarted,
7459 WalSnapshot {
7460 max_frame: 0,
7461 nbackfills: 0,
7462 last_checksum: snapshot.last_checksum,
7463 checkpoint_seq: snapshot.checkpoint_seq.wrapping_add(1),
7464 transaction_count: snapshot.transaction_count,
7465 }
7466 );
7467 assert_eq!(
7468 coordination_a.load_snapshot(),
7469 WalSnapshot {
7470 max_frame: 0,
7471 nbackfills: 0,
7472 last_checksum: snapshot.last_checksum,
7473 checkpoint_seq: snapshot.checkpoint_seq.wrapping_add(1),
7474 transaction_count: snapshot.transaction_count,
7475 }
7476 );
7477 let header = coordination_a.wal_header();
7478 assert_eq!(header.page_size, 4096);
7479 assert_eq!(
7480 header.checkpoint_seq,
7481 snapshot.checkpoint_seq.wrapping_add(1)
7482 );
7483 assert_eq!(header.salt_1, 18);
7484 assert_ne!(header.salt_2, 23);
7485 assert_eq!(header.checksum_1, snapshot.last_checksum.0);
7486 assert_eq!(header.checksum_2, snapshot.last_checksum.1);
7487 assert_eq!(coordination_a.iter_latest_frames(0, u64::MAX), Vec::new());
7488 assert_eq!(coordination_a.checkpoint_epoch(), 5);
7489
7490 let shared = shared_b.read();
7491 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 0);
7492 assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7493 assert_eq!(shared.metadata.last_checksum, snapshot.last_checksum);
7494 assert_eq!(
7495 shared.metadata.transaction_count.load(Ordering::Acquire),
7496 snapshot.transaction_count
7497 );
7498 assert_eq!(shared.runtime.epoch.load(Ordering::Acquire), 5);
7499 assert!(!shared.metadata.initialized.load(Ordering::Acquire));
7500 assert!(shared.runtime.frame_cache.lock().is_empty());
7501 }
7502
7503 #[cfg(host_shared_wal)]
7504 #[test]
7505 fn test_shm_coordination_exclusive_reopen_reuses_persisted_authority() {
7506 let dir = tempfile::tempdir().unwrap();
7507 let wal_path = dir.path().join("test.db-wal");
7508 let shm_path = dir.path().join("test.db-tshm");
7509 let io = shared_wal_test_io();
7510
7511 {
7512 let file = io
7513 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7514 .unwrap();
7515 let shared = WalFileShared::new_shared(file).unwrap();
7516 let snapshot = WalSnapshot {
7517 max_frame: 12,
7518 nbackfills: 8,
7519 last_checksum: (31, 37),
7520 checkpoint_seq: 5,
7521 transaction_count: 9,
7522 };
7523 set_shared_snapshot(&shared, snapshot);
7524 {
7525 let shared = shared.write();
7526 let mut header = shared.metadata.wal_header.lock();
7527 header.page_size = 4096;
7528 header.salt_1 = 17;
7529 header.salt_2 = 23;
7530 header.checksum_1 = snapshot.last_checksum.0;
7531 header.checksum_2 = snapshot.last_checksum.1;
7532 }
7533
7534 let (authority, coordination) = make_test_shm_coordination(&shared, &shm_path);
7535 coordination.cache_frame(7, 2);
7536 coordination.cache_frame(7, 5);
7537 assert_eq!(
7538 authority.open_mode(),
7539 SharedWalCoordinationOpenMode::Exclusive
7540 );
7541 assert_eq!(coordination.load_snapshot(), snapshot);
7542 assert_eq!(coordination.find_frame(7, 0, 5, None), Some(5));
7543 }
7544
7545 let reopened_file = io
7546 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7547 .unwrap();
7548 let reopened_shared = WalFileShared::new_shared(reopened_file).unwrap();
7549 let (reopened_authority, reopened_coordination) =
7550 make_test_shm_coordination(&reopened_shared, &shm_path);
7551
7552 assert_eq!(
7553 reopened_authority.open_mode(),
7554 SharedWalCoordinationOpenMode::Exclusive
7555 );
7556 assert_eq!(
7557 reopened_coordination.load_snapshot(),
7558 WalSnapshot {
7559 max_frame: 12,
7560 nbackfills: 8,
7561 last_checksum: (31, 37),
7562 checkpoint_seq: 5,
7563 transaction_count: 9,
7564 }
7565 );
7566 assert_eq!(
7567 reopened_coordination.iter_latest_frames(0, u64::MAX),
7568 vec![(7, 5)]
7569 );
7570 assert_eq!(reopened_authority.min_active_reader_frame(), None);
7571 }
7572
7573 #[cfg(host_shared_wal)]
7574 #[test]
7575 fn test_open_shared_from_authority_reuses_trusted_snapshot_after_exclusive_reopen() {
7576 let dir = tempfile::tempdir().unwrap();
7577 let wal_path = dir.path().join("test.db-wal");
7578 let shm_path = dir.path().join("test.db-tshm");
7579 let io = shared_wal_test_io();
7580 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7581 {
7582 let authority =
7583 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7584 authority.install_snapshot(snapshot);
7585 authority.record_frame(7, 1);
7586 }
7587 let reopened_authority =
7588 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7589 assert_eq!(
7590 reopened_authority.open_mode(),
7591 SharedWalCoordinationOpenMode::Exclusive
7592 );
7593
7594 let shared = WalFileShared::open_shared_from_authority_if_exists(
7595 &io,
7596 wal_path.to_str().unwrap(),
7597 crate::OpenFlags::Create,
7598 &reopened_authority,
7599 &open_test_db_file_for_wal(&io, &wal_path),
7600 )
7601 .unwrap();
7602
7603 let shared = shared.read();
7604 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7605 assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7606 assert_eq!(
7607 shared.metadata.transaction_count.load(Ordering::Acquire),
7608 snapshot.transaction_count
7609 );
7610 assert_eq!(
7611 shared.metadata.last_checksum,
7612 (snapshot.checksum_1, snapshot.checksum_2)
7613 );
7614 assert_eq!(
7615 shared.runtime.epoch.load(Ordering::Acquire),
7616 snapshot.checkpoint_epoch
7617 );
7618 assert!(shared.metadata.initialized.load(Ordering::Acquire));
7619 assert!(!shared
7620 .metadata
7621 .loaded_from_disk_scan
7622 .load(Ordering::Acquire));
7623 assert!(shared.runtime.frame_cache.lock().is_empty());
7624 }
7625
7626 #[cfg(host_shared_wal)]
7627 #[test]
7628 fn test_shm_coordination_live_overflow_returns_busy_without_runtime_disk_scan() {
7629 let dir = tempfile::tempdir().unwrap();
7630 let wal_path = dir.path().join("test-live-overflow.db-wal");
7631 let shm_path = dir.path().join("test-live-overflow.db-tshm");
7632 let io = shared_wal_test_io();
7633 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7634 let authority =
7635 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7636 authority.install_snapshot(snapshot);
7637 authority.record_frame(7, 1);
7638
7639 let reopened_authority =
7640 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7641 let shared = WalFileShared::open_shared_from_authority_if_exists(
7642 &io,
7643 wal_path.to_str().unwrap(),
7644 crate::OpenFlags::Create,
7645 &reopened_authority,
7646 &open_test_db_file_for_wal(&io, &wal_path),
7647 )
7648 .unwrap();
7649 assert!(shared.read().runtime.frame_cache.lock().is_empty());
7650
7651 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
7652 buffer_pool.finalize_with_page_size(4096).unwrap();
7653 let wal = WalFile::new_with_shared_coordination(
7654 io.clone(),
7655 shared.clone(),
7656 reopened_authority.clone(),
7657 ((0, 0), 0),
7658 buffer_pool,
7659 );
7660
7661 wal.begin_read_tx().unwrap();
7662 reopened_authority.mark_frame_index_overflowed_for_tests();
7663
7664 assert!(
7665 matches!(wal.find_frame(7, None), Err(LimboError::Busy)),
7666 "page lookup must refuse the overflowed path instead of rescanning the WAL synchronously"
7667 );
7668 assert!(
7669 shared.read().runtime.frame_cache.lock().is_empty(),
7670 "refusing the overflow refresh must leave the local fallback cache untouched"
7671 );
7672
7673 wal.end_read_tx();
7674 assert!(
7675 matches!(wal.begin_read_tx(), Err(LimboError::Busy)),
7676 "new readers must also refuse an uncovered overflowed frame index without blocking"
7677 );
7678 }
7679
7680 #[cfg(host_shared_wal)]
7681 #[test]
7682 fn test_open_shared_from_authority_exclusive_rebuilds_positive_snapshot_from_disk() {
7683 let dir = tempfile::tempdir().unwrap();
7684 let wal_path = dir.path().join("test-exclusive-positive.db-wal");
7685 let shm_path = dir.path().join("test-exclusive-positive.db-tshm");
7686 let io = shared_wal_test_io();
7687 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7688 {
7689 let authority =
7690 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7691 authority.install_snapshot(SharedWalCoordinationHeader {
7692 nbackfills: snapshot.max_frame,
7693 ..snapshot
7694 });
7695 authority.record_frame(7, 1);
7696 assert_eq!(
7697 authority.open_mode(),
7698 SharedWalCoordinationOpenMode::Exclusive
7699 );
7700 }
7701
7702 let reopened_authority =
7703 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7704 assert_eq!(
7705 reopened_authority.open_mode(),
7706 SharedWalCoordinationOpenMode::Exclusive
7707 );
7708
7709 let shared = WalFileShared::open_shared_from_authority_if_exists(
7710 &io,
7711 wal_path.to_str().unwrap(),
7712 crate::OpenFlags::Create,
7713 &reopened_authority,
7714 &open_test_db_file_for_wal(&io, &wal_path),
7715 )
7716 .unwrap();
7717
7718 let shared = shared.read();
7719 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7720 assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7721 assert!(shared
7722 .metadata
7723 .loaded_from_disk_scan
7724 .load(Ordering::Acquire));
7725 assert_eq!(
7726 shared.runtime.frame_cache.lock().get(&7).cloned(),
7727 Some(vec![1])
7728 );
7729 }
7730
7731 #[cfg(host_shared_wal)]
7732 #[test]
7733 fn test_shared_coordination_open_uses_reconciled_snapshot_for_local_wal_state() {
7734 let dir = tempfile::tempdir().unwrap();
7735 let wal_path = dir.path().join("test.db-wal");
7736 let shm_path = dir.path().join("test.db-tshm");
7737 let io = shared_wal_test_io();
7738
7739 let file = io
7740 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7741 .unwrap();
7742 let shared = WalFileShared::new_shared(file).unwrap();
7743
7744 let authority =
7745 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7746 let snapshot = SharedWalCoordinationHeader {
7747 max_frame: 1,
7748 nbackfills: 0,
7749 transaction_count: 9,
7750 visibility_generation: 3,
7751 checkpoint_seq: 5,
7752 checkpoint_epoch: 7,
7753 page_size: 4096,
7754 salt_1: 17,
7755 salt_2: 23,
7756 checksum_1: 31,
7757 checksum_2: 37,
7758 reader_slot_count: 64,
7759 };
7760 authority.install_snapshot(snapshot);
7761 authority.record_frame(7, 1);
7762
7763 let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
7764 buffer_pool.finalize_with_page_size(4096).unwrap();
7765 let wal =
7766 WalFile::new_with_shared_coordination(io, shared, authority, ((0, 0), 0), buffer_pool);
7767
7768 assert_eq!(wal.get_max_frame(), 1);
7769 assert_eq!(wal.get_last_checksum(), (31, 37));
7770 }
7771
7772 #[cfg(host_shared_wal)]
7773 #[test]
7774 fn test_open_shared_from_authority_rebuilds_from_disk_when_snapshot_is_stale() {
7775 let dir = tempfile::tempdir().unwrap();
7776 let wal_path = dir.path().join("test-stale.db-wal");
7777 let shm_path = dir.path().join("test-stale.db-tshm");
7778 let io = shared_wal_test_io();
7779 let valid_snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7780 let authority =
7781 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7782 authority.install_snapshot(SharedWalCoordinationHeader {
7783 max_frame: 0,
7784 nbackfills: 0,
7785 transaction_count: 0,
7786 visibility_generation: 0,
7787 checkpoint_seq: valid_snapshot.checkpoint_seq,
7788 checkpoint_epoch: 0,
7789 page_size: valid_snapshot.page_size,
7790 salt_1: valid_snapshot.salt_1,
7791 salt_2: valid_snapshot.salt_2,
7792 checksum_1: 0,
7793 checksum_2: 0,
7794 reader_slot_count: 64,
7795 });
7796
7797 let shared = WalFileShared::open_shared_from_authority_if_exists(
7798 &io,
7799 wal_path.to_str().unwrap(),
7800 crate::OpenFlags::Create,
7801 &authority,
7802 &open_test_db_file_for_wal(&io, &wal_path),
7803 )
7804 .unwrap();
7805
7806 let shared = shared.read();
7807 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7808 assert_eq!(
7809 shared.metadata.last_checksum,
7810 (valid_snapshot.checksum_1, valid_snapshot.checksum_2)
7811 );
7812 assert!(shared
7813 .metadata
7814 .loaded_from_disk_scan
7815 .load(Ordering::Acquire));
7816 assert_eq!(
7817 shared.runtime.frame_cache.lock().get(&7).cloned(),
7818 Some(vec![1])
7819 );
7820 }
7821
7822 #[cfg(host_shared_wal)]
7823 #[test]
7824 fn test_open_shared_from_authority_rebuilt_authority_persists_across_exclusive_reopen() {
7825 let dir = tempfile::tempdir().unwrap();
7826 let wal_path = dir.path().join("test-republish.db-wal");
7827 let shm_path = dir.path().join("test-republish.db-tshm");
7828 let io = shared_wal_test_io();
7829 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7830 let authority =
7831 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7832 authority.install_snapshot(snapshot);
7833
7834 let exclusive = WalFileShared::open_shared_from_authority_if_exists(
7835 &io,
7836 wal_path.to_str().unwrap(),
7837 crate::OpenFlags::Create,
7838 &authority,
7839 &open_test_db_file_for_wal(&io, &wal_path),
7840 )
7841 .unwrap();
7842 assert!(exclusive
7843 .read()
7844 .metadata
7845 .loaded_from_disk_scan
7846 .load(Ordering::Acquire));
7847 assert!(
7848 authority.iter_latest_frames(0, u64::MAX).is_empty(),
7849 "open_shared_from_authority_if_exists should not republish authority before coordination reconciliation"
7850 );
7851
7852 let exclusive_coordination = ShmWalCoordination::new(exclusive, authority.clone());
7853 assert_eq!(authority.iter_latest_frames(0, u64::MAX), vec![(7, 1)]);
7854 assert_eq!(exclusive_coordination.find_frame(7, 0, 1, None), Some(1));
7855
7856 drop(exclusive_coordination);
7857 drop(authority);
7858
7859 let reopened_authority =
7860 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7861 assert_eq!(
7862 reopened_authority.open_mode(),
7863 SharedWalCoordinationOpenMode::Exclusive
7864 );
7865
7866 let reopened_shared = WalFileShared::open_shared_from_authority_if_exists(
7867 &io,
7868 wal_path.to_str().unwrap(),
7869 crate::OpenFlags::Create,
7870 &reopened_authority,
7871 &open_test_db_file_for_wal(&io, &wal_path),
7872 )
7873 .unwrap();
7874 assert!(!reopened_shared
7875 .read()
7876 .metadata
7877 .loaded_from_disk_scan
7878 .load(Ordering::Acquire));
7879 let reopened_coordination = ShmWalCoordination::new(reopened_shared, reopened_authority);
7880 assert_eq!(reopened_coordination.find_frame(7, 0, 1, None), Some(1));
7881 }
7882
7883 #[cfg(host_shared_wal)]
7884 #[test]
7885 fn test_open_shared_from_authority_exclusive_disk_scan_does_not_downgrade_newer_zero_frame_generation(
7886 ) {
7887 let dir = tempfile::tempdir().unwrap();
7888 let wal_path = dir.path().join("test-zero-frame-reopen.db-wal");
7889 let shm_path = dir.path().join("test-zero-frame-reopen.db-tshm");
7890 let io = shared_wal_test_io();
7891 let prior_generation = write_test_wal_with_single_commit_frame(&io, &wal_path);
7892 let authority =
7893 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7894 let restarted_generation = SharedWalCoordinationHeader {
7895 max_frame: 0,
7896 nbackfills: 0,
7897 transaction_count: prior_generation.transaction_count,
7898 visibility_generation: prior_generation.visibility_generation,
7899 checkpoint_seq: prior_generation.checkpoint_seq.wrapping_add(1),
7900 checkpoint_epoch: prior_generation.checkpoint_epoch,
7901 page_size: prior_generation.page_size,
7902 salt_1: prior_generation.salt_1.wrapping_add(1),
7903 salt_2: prior_generation.salt_2.wrapping_add(1),
7904 checksum_1: prior_generation.checksum_1,
7905 checksum_2: prior_generation.checksum_2,
7906 reader_slot_count: prior_generation.reader_slot_count,
7907 };
7908 authority.install_snapshot(restarted_generation);
7909
7910 let shared = WalFileShared::open_shared_from_authority_if_exists(
7911 &io,
7912 wal_path.to_str().unwrap(),
7913 crate::OpenFlags::Create,
7914 &authority,
7915 &open_test_db_file_for_wal(&io, &wal_path),
7916 )
7917 .unwrap();
7918 assert!(shared
7919 .read()
7920 .metadata
7921 .loaded_from_disk_scan
7922 .load(Ordering::Acquire));
7923
7924 let coordination = ShmWalCoordination::new(shared.clone(), authority.clone());
7925 let reopened = coordination.load_snapshot();
7926 assert_eq!(reopened.max_frame, 0);
7927 assert_eq!(reopened.nbackfills, 0);
7928 assert_eq!(reopened.checkpoint_seq, restarted_generation.checkpoint_seq);
7929 assert_eq!(
7930 authority.snapshot().checkpoint_seq,
7931 restarted_generation.checkpoint_seq
7932 );
7933 assert!(
7934 !coordination.wal_is_initialized(),
7935 "preserving a newer zero-frame generation must require the first append to rewrite the WAL header"
7936 );
7937 assert!(
7938 shared.read().runtime.frame_cache.lock().is_empty(),
7939 "older WAL frames from a prior generation must not survive zero-frame authority recovery"
7940 );
7941 }
7942
7943 #[cfg(host_shared_wal)]
7944 #[test]
7945 fn test_open_shared_from_authority_ignores_unpublished_backfill_proof_after_exclusive_reopen() {
7946 let dir = tempfile::tempdir().unwrap();
7947 let wal_path = dir.path().join("test-unpublished-proof.db-wal");
7948 let shm_path = dir.path().join("test-unpublished-proof.db-tshm");
7949 let io = shared_wal_test_io();
7950 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7951 {
7952 let authority =
7953 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7954 authority.install_snapshot(snapshot);
7955 authority.install_backfill_proof(
7956 SharedWalCoordinationHeader {
7957 nbackfills: snapshot.max_frame,
7958 ..snapshot
7959 },
7960 11,
7961 0xAABB_CCDD,
7962 );
7963 }
7964 let reopened_authority =
7965 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7966 assert_eq!(
7967 reopened_authority.open_mode(),
7968 SharedWalCoordinationOpenMode::Exclusive
7969 );
7970
7971 let shared = WalFileShared::open_shared_from_authority_if_exists(
7972 &io,
7973 wal_path.to_str().unwrap(),
7974 crate::OpenFlags::Create,
7975 &reopened_authority,
7976 &open_test_db_file_for_wal(&io, &wal_path),
7977 )
7978 .unwrap();
7979
7980 let shared = shared.read();
7981 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7982 assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7983 assert!(shared
7984 .metadata
7985 .loaded_from_disk_scan
7986 .load(Ordering::Acquire));
7987 }
7988
7989 #[cfg(host_shared_wal)]
7990 #[test]
7991 fn test_restart_checkpoint_clears_backfill_proof_and_later_replaces_it() {
7992 let (db, path) = get_database();
7993 let wal_path = path.join("test.db-wal");
7994 let wal_path_str = wal_path.to_str().unwrap();
7995 let conn = db.connect().unwrap();
7996 conn.wal_auto_actions_disable();
7997 conn.execute("create table test(id integer primary key, value text)")
7998 .unwrap();
7999 bulk_inserts(&conn, 8, 2);
8000
8001 let pager = conn.pager.load();
8002 let partial = run_checkpoint_until_done(
8003 &pager,
8004 CheckpointMode::Passive {
8005 upper_bound_inclusive: Some(1),
8006 },
8007 );
8008 assert!(
8009 partial.wal_total_backfilled > 0 && !partial.everything_backfilled(),
8010 "setup must create a partial checkpoint with a positive durable backfill proof"
8011 );
8012
8013 let authority = db.shared_wal_coordination().unwrap().unwrap();
8014 let snapshot_before_restart = authority.snapshot();
8015 let (db_size_before, db_crc_before) =
8016 super::read_database_identity_from_file_path(&db.io, wal_path_str)
8017 .unwrap()
8018 .unwrap();
8019 assert!(
8020 authority.validate_backfill_proof(
8021 snapshot_before_restart,
8022 db_size_before,
8023 db_crc_before
8024 ),
8025 "setup must install a valid proof before RESTART"
8026 );
8027
8028 let restart = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
8029 assert!(
8030 restart.everything_backfilled(),
8031 "RESTART should fully backfill before resetting the WAL generation"
8032 );
8033
8034 let snapshot_after_restart = authority.snapshot();
8035 assert_eq!(snapshot_after_restart.max_frame, 0);
8036 assert_eq!(snapshot_after_restart.nbackfills, 0);
8037 assert!(
8038 !authority.validate_backfill_proof(
8039 snapshot_before_restart,
8040 db_size_before,
8041 db_crc_before
8042 ),
8043 "RESTART must clear the proof for the old WAL generation"
8044 );
8045
8046 bulk_inserts(&conn, 6, 2);
8047 let replacement = run_checkpoint_until_done(
8048 &pager,
8049 CheckpointMode::Passive {
8050 upper_bound_inclusive: Some(1),
8051 },
8052 );
8053 assert!(
8054 replacement.wal_total_backfilled > 0 && !replacement.everything_backfilled(),
8055 "replacement setup must create a new partial checkpoint after RESTART"
8056 );
8057
8058 let snapshot_after_replacement = authority.snapshot();
8059 let (db_size_after, db_crc_after) =
8060 super::read_database_identity_from_file_path(&db.io, wal_path_str)
8061 .unwrap()
8062 .unwrap();
8063 assert!(
8064 authority.validate_backfill_proof(
8065 snapshot_after_replacement,
8066 db_size_after,
8067 db_crc_after
8068 ),
8069 "partial checkpoint after RESTART must install a replacement proof for the new generation"
8070 );
8071 assert_ne!(
8072 snapshot_after_replacement.checkpoint_seq, snapshot_before_restart.checkpoint_seq,
8073 "replacement proof must belong to the restarted WAL generation"
8074 );
8075 }
8076
8077 #[cfg(host_shared_wal)]
8078 #[test]
8079 fn test_truncate_checkpoint_clears_backfill_proof_and_later_replaces_it() {
8080 let (db, path) = get_database();
8081 let wal_path = path.join("test.db-wal");
8082 let wal_path_str = wal_path.to_str().unwrap();
8083 let conn = db.connect().unwrap();
8084 conn.wal_auto_actions_disable();
8085 conn.execute("create table test(id integer primary key, value text)")
8086 .unwrap();
8087 bulk_inserts(&conn, 8, 2);
8088
8089 let pager = conn.pager.load();
8090 let partial = run_checkpoint_until_done(
8091 &pager,
8092 CheckpointMode::Passive {
8093 upper_bound_inclusive: Some(1),
8094 },
8095 );
8096 assert!(
8097 partial.wal_total_backfilled > 0 && !partial.everything_backfilled(),
8098 "setup must create a partial checkpoint with a positive durable backfill proof"
8099 );
8100
8101 let authority = db.shared_wal_coordination().unwrap().unwrap();
8102 let snapshot_before_truncate = authority.snapshot();
8103 let (db_size_before, db_crc_before) =
8104 super::read_database_identity_from_file_path(&db.io, wal_path_str)
8105 .unwrap()
8106 .unwrap();
8107 assert!(
8108 authority.validate_backfill_proof(
8109 snapshot_before_truncate,
8110 db_size_before,
8111 db_crc_before
8112 ),
8113 "setup must install a valid proof before TRUNCATE"
8114 );
8115
8116 let truncate = run_checkpoint_until_done(
8117 &pager,
8118 CheckpointMode::Truncate {
8119 upper_bound_inclusive: None,
8120 },
8121 );
8122 assert!(
8123 truncate.everything_backfilled(),
8124 "TRUNCATE should fully backfill before truncating the WAL"
8125 );
8126
8127 let snapshot_after_truncate = authority.snapshot();
8128 assert_eq!(snapshot_after_truncate.max_frame, 0);
8129 assert_eq!(snapshot_after_truncate.nbackfills, 0);
8130 assert!(
8131 !authority.validate_backfill_proof(
8132 snapshot_before_truncate,
8133 db_size_before,
8134 db_crc_before
8135 ),
8136 "TRUNCATE must clear the proof for the truncated WAL generation"
8137 );
8138 assert_eq!(
8139 std::fs::metadata(&wal_path).unwrap().len(),
8140 0,
8141 "TRUNCATE must leave the WAL file empty before the new generation begins"
8142 );
8143
8144 bulk_inserts(&conn, 6, 2);
8145 let replacement = run_checkpoint_until_done(
8146 &pager,
8147 CheckpointMode::Passive {
8148 upper_bound_inclusive: Some(1),
8149 },
8150 );
8151 assert!(
8152 replacement.wal_total_backfilled > 0 && !replacement.everything_backfilled(),
8153 "replacement setup must create a new partial checkpoint after TRUNCATE"
8154 );
8155
8156 let snapshot_after_replacement = authority.snapshot();
8157 let (db_size_after, db_crc_after) =
8158 super::read_database_identity_from_file_path(&db.io, wal_path_str)
8159 .unwrap()
8160 .unwrap();
8161 assert!(
8162 authority.validate_backfill_proof(
8163 snapshot_after_replacement,
8164 db_size_after,
8165 db_crc_after
8166 ),
8167 "partial checkpoint after TRUNCATE must install a replacement proof for the new generation"
8168 );
8169 assert_ne!(
8170 snapshot_after_replacement.checkpoint_seq, snapshot_before_truncate.checkpoint_seq,
8171 "replacement proof must belong to the truncated WAL generation"
8172 );
8173 }
8174
8175 #[cfg(host_shared_wal)]
8176 #[test]
8177 fn test_classify_authority_snapshot_marks_truncated_wal_for_rebuild() {
8178 let dir = tempfile::tempdir().unwrap();
8179 let wal_path = dir.path().join("test-truncated.db-wal");
8180 let io = shared_wal_test_io();
8181 let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
8182
8183 let wal_len = std::fs::metadata(&wal_path).unwrap().len();
8184 std::fs::OpenOptions::new()
8185 .write(true)
8186 .open(&wal_path)
8187 .unwrap()
8188 .set_len(wal_len - 1)
8189 .unwrap();
8190
8191 assert_eq!(
8192 classify_authority_snapshot_against_wal(
8193 &io,
8194 &io.open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8195 .unwrap(),
8196 snapshot,
8197 )
8198 .unwrap(),
8199 AuthoritySnapshotValidation::RebuildFromDisk(
8200 AuthoritySnapshotRebuildReason::WalLengthMismatch
8201 )
8202 );
8203 }
8204
8205 #[cfg(host_shared_wal)]
8206 #[test]
8207 fn test_classify_authority_snapshot_marks_corrupt_header_for_rebuild() {
8208 let dir = tempfile::tempdir().unwrap();
8209 let wal_path = dir.path().join("test-corrupt-header.db-wal");
8210 let io = shared_wal_test_io();
8211 std::fs::write(&wal_path, [0u8; WAL_HEADER_SIZE]).unwrap();
8212
8213 let snapshot = SharedWalCoordinationHeader {
8214 max_frame: 0,
8215 nbackfills: 0,
8216 transaction_count: 9,
8217 visibility_generation: 1,
8218 checkpoint_seq: 5,
8219 checkpoint_epoch: 7,
8220 page_size: 4096,
8221 salt_1: 17,
8222 salt_2: 23,
8223 checksum_1: 31,
8224 checksum_2: 37,
8225 reader_slot_count: 64,
8226 };
8227
8228 assert_eq!(
8229 classify_authority_snapshot_against_wal(
8230 &io,
8231 &io.open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8232 .unwrap(),
8233 snapshot,
8234 )
8235 .unwrap(),
8236 AuthoritySnapshotValidation::RebuildFromDisk(
8237 AuthoritySnapshotRebuildReason::WalHeaderUnreadable
8238 )
8239 );
8240 }
8241
8242 #[cfg(host_shared_wal)]
8243 #[test]
8244 fn test_open_shared_from_authority_keeps_zero_length_wal_uninitialized_after_exclusive_reopen()
8245 {
8246 let dir = tempfile::tempdir().unwrap();
8247 let wal_path = dir.path().join("test-empty.db-wal");
8248 let shm_path = dir.path().join("test-empty.db-tshm");
8249 let io = shared_wal_test_io();
8250
8251 io.open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8252 .unwrap();
8253 {
8254 let authority =
8255 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8256 authority.install_snapshot(SharedWalCoordinationHeader {
8257 max_frame: 0,
8258 nbackfills: 0,
8259 transaction_count: 9,
8260 visibility_generation: 1,
8261 checkpoint_seq: 5,
8262 checkpoint_epoch: 7,
8263 page_size: 4096,
8264 salt_1: 17,
8265 salt_2: 23,
8266 checksum_1: 31,
8267 checksum_2: 37,
8268 reader_slot_count: 64,
8269 });
8270 }
8271 let reopened_authority =
8272 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8273 assert_eq!(
8274 reopened_authority.open_mode(),
8275 SharedWalCoordinationOpenMode::Exclusive
8276 );
8277
8278 let shared = WalFileShared::open_shared_from_authority_if_exists(
8279 &io,
8280 wal_path.to_str().unwrap(),
8281 crate::OpenFlags::Create,
8282 &reopened_authority,
8283 &open_test_db_file_for_wal(&io, &wal_path),
8284 )
8285 .unwrap();
8286
8287 let shared = shared.read();
8288 assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 0);
8289 assert_eq!(shared.metadata.last_checksum, (31, 37));
8290 assert!(!shared.metadata.initialized.load(Ordering::Acquire));
8291 assert!(!shared
8292 .metadata
8293 .loaded_from_disk_scan
8294 .load(Ordering::Acquire));
8295 }
8296
8297 #[cfg(host_shared_wal)]
8298 #[test]
8299 #[cfg_attr(
8300 windows,
8301 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8302 )]
8303 fn test_shm_coordination_secondary_disk_scan_does_not_reseed_authority_while_writer_active() {
8304 let dir = tempfile::tempdir().unwrap();
8305 let wal_path = dir.path().join("test.db-wal");
8306 let shm_path = dir.path().join("test.db-tshm");
8307 let io = shared_wal_test_io();
8308
8309 let file_a = io
8310 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8311 .unwrap();
8312 let shared_a = WalFileShared::new_shared(file_a).unwrap();
8313 let authoritative = WalSnapshot {
8314 max_frame: 5,
8315 nbackfills: 0,
8316 last_checksum: (31, 37),
8317 checkpoint_seq: 5,
8318 transaction_count: 9,
8319 };
8320 set_shared_snapshot(&shared_a, authoritative);
8321 {
8322 let shared = shared_a.write();
8323 let mut header = shared.metadata.wal_header.lock();
8324 header.page_size = 4096;
8325 header.salt_1 = 17;
8326 header.salt_2 = 23;
8327 header.checksum_1 = authoritative.last_checksum.0;
8328 header.checksum_2 = authoritative.last_checksum.1;
8329 }
8330 let (authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8331 coordination_a.cache_frame(7, 2);
8332 coordination_a.cache_frame(7, 5);
8333 assert!(authority.try_acquire_writer(authority.owner_record()));
8334
8335 let file_b = io
8336 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8337 .unwrap();
8338 let shared_b = WalFileShared::new_shared(file_b).unwrap();
8339 let stale = WalSnapshot {
8340 max_frame: 2,
8341 nbackfills: 0,
8342 last_checksum: (11, 13),
8343 checkpoint_seq: 4,
8344 transaction_count: 3,
8345 };
8346 set_shared_snapshot(&shared_b, stale);
8347 {
8348 let shared = shared_b.write();
8349 shared
8350 .metadata
8351 .loaded_from_disk_scan
8352 .store(true, Ordering::Release);
8353 let mut header = shared.metadata.wal_header.lock();
8354 header.page_size = 4096;
8355 header.salt_1 = 17;
8356 header.salt_2 = 23;
8357 header.checksum_1 = stale.last_checksum.0;
8358 header.checksum_2 = stale.last_checksum.1;
8359 shared.runtime.frame_cache.lock().insert(7, vec![2]);
8360 }
8361
8362 let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8363
8364 assert_eq!(coordination_b.load_snapshot(), authoritative);
8365 assert_eq!(authority.snapshot().max_frame, authoritative.max_frame);
8366 assert_eq!(
8367 authority.snapshot().transaction_count,
8368 authoritative.transaction_count
8369 );
8370 assert_eq!(coordination_b.find_frame(7, 0, 5, None), Some(5));
8371 authority.release_writer(authority.owner_record());
8372 }
8373
8374 #[cfg(host_shared_wal)]
8375 #[test]
8376 #[cfg_attr(
8377 windows,
8378 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8379 )]
8380 fn test_shm_coordination_disk_scan_matching_authority_keeps_frame_index() {
8381 let dir = tempfile::tempdir().unwrap();
8382 let wal_path = dir.path().join("test.db-wal");
8383 let shm_path = dir.path().join("test.db-tshm");
8384 let io = shared_wal_test_io();
8385
8386 let file_a = io
8387 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8388 .unwrap();
8389 let shared_a = WalFileShared::new_shared(file_a).unwrap();
8390 let authoritative = WalSnapshot {
8391 max_frame: 5,
8392 nbackfills: 2,
8393 last_checksum: (31, 37),
8394 checkpoint_seq: 5,
8395 transaction_count: 9,
8396 };
8397 set_shared_snapshot(&shared_a, authoritative);
8398 {
8399 let shared = shared_a.write();
8400 let mut header = shared.metadata.wal_header.lock();
8401 header.page_size = 4096;
8402 header.salt_1 = 17;
8403 header.salt_2 = 23;
8404 header.checksum_1 = authoritative.last_checksum.0;
8405 header.checksum_2 = authoritative.last_checksum.1;
8406 }
8407 let (authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8408 coordination_a.cache_frame(7, 2);
8409 coordination_a.cache_frame(9, 5);
8410
8411 let file_b = io
8412 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8413 .unwrap();
8414 let shared_b = WalFileShared::new_shared(file_b).unwrap();
8415 set_shared_snapshot(&shared_b, authoritative);
8416 {
8417 let shared = shared_b.write();
8418 shared
8419 .metadata
8420 .loaded_from_disk_scan
8421 .store(true, Ordering::Release);
8422 let mut header = shared.metadata.wal_header.lock();
8423 header.page_size = 4096;
8424 header.salt_1 = 17;
8425 header.salt_2 = 23;
8426 header.checksum_1 = authoritative.last_checksum.0;
8427 header.checksum_2 = authoritative.last_checksum.1;
8428 let mut frame_cache = shared.runtime.frame_cache.lock();
8429 frame_cache.insert(7, vec![2]);
8430 frame_cache.insert(9, vec![5]);
8431 }
8432
8433 let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8434
8435 let reopened = coordination_b.load_snapshot();
8436 assert_eq!(reopened.max_frame, authoritative.max_frame);
8437 assert_eq!(reopened.last_checksum, authoritative.last_checksum);
8438 assert_eq!(reopened.checkpoint_seq, authoritative.checkpoint_seq);
8439 assert_eq!(reopened.transaction_count, authoritative.transaction_count);
8440 assert_eq!(
8441 reopened.nbackfills, 0,
8442 "disk-scan reconciliation must preserve the frame index without reviving positive nbackfills"
8443 );
8444 assert_eq!(authority.find_frame(7, 0, 5, None), Some(2));
8445 assert_eq!(authority.find_frame(9, 0, 5, None), Some(5));
8446 assert_eq!(
8447 authority.iter_latest_frames(0, authoritative.max_frame),
8448 vec![(7, 2), (9, 5)]
8449 );
8450 }
8451
8452 #[cfg(host_shared_wal)]
8453 #[test]
8454 #[cfg_attr(
8455 windows,
8456 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8457 )]
8458 fn test_shm_coordination_disk_scan_matching_snapshot_rebuilds_stale_frame_index() {
8459 let dir = tempfile::tempdir().unwrap();
8460 let wal_path = dir.path().join("test.db-wal");
8461 let shm_path = dir.path().join("test.db-tshm");
8462 let io = shared_wal_test_io();
8463
8464 let file_a = io
8465 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8466 .unwrap();
8467 let shared_a = WalFileShared::new_shared(file_a).unwrap();
8468 let authoritative = WalSnapshot {
8469 max_frame: 5,
8470 nbackfills: 0,
8471 last_checksum: (31, 37),
8472 checkpoint_seq: 5,
8473 transaction_count: 9,
8474 };
8475 set_shared_snapshot(&shared_a, authoritative);
8476 {
8477 let shared = shared_a.write();
8478 let mut header = shared.metadata.wal_header.lock();
8479 header.page_size = 4096;
8480 header.salt_1 = 17;
8481 header.salt_2 = 23;
8482 header.checksum_1 = authoritative.last_checksum.0;
8483 header.checksum_2 = authoritative.last_checksum.1;
8484 }
8485 {
8486 let (_authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8487 coordination_a.cache_frame(7, 2);
8488 coordination_a.cache_frame(9, 4);
8489 }
8490
8491 let file_b = io
8492 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8493 .unwrap();
8494 let shared_b = WalFileShared::new_shared(file_b).unwrap();
8495 set_shared_snapshot(&shared_b, authoritative);
8496 {
8497 let shared = shared_b.write();
8498 shared
8499 .metadata
8500 .loaded_from_disk_scan
8501 .store(true, Ordering::Release);
8502 let mut header = shared.metadata.wal_header.lock();
8503 header.page_size = 4096;
8504 header.salt_1 = 17;
8505 header.salt_2 = 23;
8506 header.checksum_1 = authoritative.last_checksum.0;
8507 header.checksum_2 = authoritative.last_checksum.1;
8508 shared.runtime.frame_cache.lock().insert(7, vec![2]);
8509 shared.runtime.frame_cache.lock().insert(9, vec![5]);
8510 }
8511
8512 let (authority, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8513 assert_eq!(
8514 authority.open_mode(),
8515 SharedWalCoordinationOpenMode::Exclusive
8516 );
8517
8518 let reopened = coordination_b.load_snapshot();
8519 assert_eq!(reopened.max_frame, authoritative.max_frame);
8520 assert_eq!(reopened.last_checksum, authoritative.last_checksum);
8521 assert_eq!(reopened.checkpoint_seq, authoritative.checkpoint_seq);
8522 assert_eq!(reopened.transaction_count, authoritative.transaction_count);
8523 assert_eq!(authority.find_frame(7, 0, 5, None), Some(2));
8524 assert_eq!(
8525 authority.find_frame(9, 0, 5, None),
8526 Some(5),
8527 "matching snapshot metadata must not preserve a stale shared frame index across restart recovery"
8528 );
8529 assert_eq!(
8530 authority.iter_latest_frames(0, authoritative.max_frame),
8531 vec![(7, 2), (9, 5)]
8532 );
8533 }
8534
8535 #[cfg(host_shared_wal)]
8536 #[test]
8537 fn test_shm_coordination_empty_disk_scan_keeps_zero_frame_authority_metadata() {
8538 let dir = tempfile::tempdir().unwrap();
8539 let wal_path = dir.path().join("test.db-wal");
8540 let shm_path = dir.path().join("test.db-tshm");
8541 let io = shared_wal_test_io();
8542
8543 let authority =
8544 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8545 let authoritative = SharedWalCoordinationHeader {
8546 max_frame: 0,
8547 nbackfills: 0,
8548 transaction_count: 9,
8549 visibility_generation: 3,
8550 checkpoint_seq: 5,
8551 checkpoint_epoch: 7,
8552 page_size: 4096,
8553 salt_1: 17,
8554 salt_2: 23,
8555 checksum_1: 31,
8556 checksum_2: 37,
8557 reader_slot_count: 64,
8558 };
8559 authority.install_snapshot(authoritative);
8560
8561 let file = io
8562 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8563 .unwrap();
8564 let shared = WalFileShared::new_shared(file).unwrap();
8565 {
8566 let shared = shared.write();
8567 shared
8568 .metadata
8569 .loaded_from_disk_scan
8570 .store(true, Ordering::Release);
8571 }
8572
8573 let coordination = ShmWalCoordination::new(shared, authority.clone());
8574 let snapshot = authority.snapshot();
8575 assert_eq!(snapshot, authoritative);
8576 let header = coordination.wal_header();
8577 assert_eq!(header.page_size, 4096);
8578 assert_eq!(header.checkpoint_seq, authoritative.checkpoint_seq);
8579 assert_eq!(header.salt_1, authoritative.salt_1);
8580 assert_eq!(header.salt_2, authoritative.salt_2);
8581 }
8582
8583 #[cfg(host_shared_wal)]
8584 #[test]
8585 #[cfg_attr(
8586 windows,
8587 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8588 )]
8589 fn test_shm_coordination_empty_disk_scan_does_not_clobber_positive_authority() {
8590 let dir = tempfile::tempdir().unwrap();
8591 let wal_path = dir.path().join("test.db-wal");
8592 let shm_path = dir.path().join("test.db-tshm");
8593 let io = shared_wal_test_io();
8594
8595 let file_a = io
8596 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8597 .unwrap();
8598 let shared_a = WalFileShared::new_shared(file_a).unwrap();
8599 let authoritative = WalSnapshot {
8600 max_frame: 5,
8601 nbackfills: 0,
8602 last_checksum: (31, 37),
8603 checkpoint_seq: 5,
8604 transaction_count: 9,
8605 };
8606 set_shared_snapshot(&shared_a, authoritative);
8607 {
8608 let shared = shared_a.write();
8609 let mut header = shared.metadata.wal_header.lock();
8610 header.page_size = 4096;
8611 header.salt_1 = 17;
8612 header.salt_2 = 23;
8613 header.checksum_1 = authoritative.last_checksum.0;
8614 header.checksum_2 = authoritative.last_checksum.1;
8615 }
8616 let (authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8617 coordination_a.cache_frame(7, 2);
8618 coordination_a.cache_frame(9, 5);
8619
8620 let file_b = io
8621 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8622 .unwrap();
8623 let shared_b = WalFileShared::new_shared(file_b).unwrap();
8624 {
8625 let shared = shared_b.write();
8626 shared
8627 .metadata
8628 .loaded_from_disk_scan
8629 .store(true, Ordering::Release);
8630 let mut header = shared.metadata.wal_header.lock();
8631 header.page_size = 4096;
8632 header.checkpoint_seq = authoritative.checkpoint_seq;
8633 header.salt_1 = 17;
8634 header.salt_2 = 23;
8635 header.checksum_1 = 11;
8636 header.checksum_2 = 13;
8637 }
8638
8639 let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8640 let reopened = coordination_b.load_snapshot();
8641 assert_eq!(reopened.max_frame, authoritative.max_frame);
8642 assert_eq!(reopened.checkpoint_seq, authoritative.checkpoint_seq);
8643 assert_eq!(reopened.transaction_count, authoritative.transaction_count);
8644 assert_eq!(
8645 authority.find_frame(7, 0, authoritative.max_frame, None),
8646 Some(2)
8647 );
8648 assert_eq!(
8649 authority.find_frame(9, 0, authoritative.max_frame, None),
8650 Some(5)
8651 );
8652 }
8653
8654 #[cfg(host_shared_wal)]
8655 #[test]
8656 fn test_shm_zero_frame_authority_invalidates_stale_local_initialized_state() {
8657 let dir = tempfile::tempdir().unwrap();
8658 let wal_path = dir.path().join("test.db-wal");
8659 let shm_path = dir.path().join("test.db-tshm");
8660 let io = shared_wal_test_io();
8661
8662 let authority =
8663 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8664 let authoritative = SharedWalCoordinationHeader {
8665 max_frame: 0,
8666 nbackfills: 0,
8667 transaction_count: 9,
8668 visibility_generation: 3,
8669 checkpoint_seq: 5,
8670 checkpoint_epoch: 7,
8671 page_size: 4096,
8672 salt_1: 17,
8673 salt_2: 23,
8674 checksum_1: 31,
8675 checksum_2: 37,
8676 reader_slot_count: 64,
8677 };
8678 authority.install_snapshot(authoritative);
8679
8680 let file = io
8681 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8682 .unwrap();
8683 let shared = WalFileShared::new_shared(file).unwrap();
8684 {
8685 let mut shared = shared.write();
8686 shared.metadata.max_frame.store(11, Ordering::Release);
8687 shared.metadata.nbackfills.store(11, Ordering::Release);
8688 shared.metadata.last_checksum = (11, 13);
8689 shared
8690 .metadata
8691 .transaction_count
8692 .store(3, Ordering::Release);
8693 shared.metadata.initialized.store(true, Ordering::Release);
8694 let mut header = shared.metadata.wal_header.lock();
8695 header.checkpoint_seq = 2;
8696 header.page_size = 4096;
8697 header.salt_1 = 7;
8698 header.salt_2 = 13;
8699 header.checksum_1 = 11;
8700 header.checksum_2 = 13;
8701 shared.runtime.epoch.store(1, Ordering::Release);
8702 }
8703
8704 let coordination = ShmWalCoordination::new(shared.clone(), authority);
8705 assert!(
8706 !coordination.wal_is_initialized(),
8707 "a stale local initialized bit must not suppress the first header rewrite after RESTART/TRUNCATE"
8708 );
8709 {
8710 let shared = shared.read();
8711 assert!(
8712 !shared.metadata.initialized.load(Ordering::Acquire),
8713 "stale local initialized state must be cleared"
8714 );
8715 let header = shared.metadata.wal_header.lock();
8716 assert_eq!(header.checkpoint_seq, authoritative.checkpoint_seq);
8717 assert_eq!(header.page_size, authoritative.page_size);
8718 assert_eq!(header.salt_1, authoritative.salt_1);
8719 assert_eq!(header.salt_2, authoritative.salt_2);
8720 assert_eq!(header.checksum_1, authoritative.checksum_1);
8721 assert_eq!(header.checksum_2, authoritative.checksum_2);
8722 }
8723
8724 let prepared = coordination
8725 .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8726 .expect("zero-frame authority should force a header rewrite");
8727 assert_eq!(prepared.checkpoint_seq, authoritative.checkpoint_seq);
8728 coordination.mark_initialized();
8729 assert!(
8730 coordination.wal_is_initialized(),
8731 "once the current-generation header is durably rewritten, wal_is_initialized should succeed"
8732 );
8733 }
8734
8735 #[cfg(host_shared_wal)]
8736 #[test]
8737 fn test_shm_prepare_wal_header_seeds_uninitialized_authority_from_prepared_header() {
8738 let dir = tempfile::tempdir().unwrap();
8739 let wal_path = dir.path().join("test.db-wal");
8740 let shm_path = dir.path().join("test.db-tshm");
8741 let io = shared_wal_test_io();
8742
8743 let authority =
8744 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8745 let file = io
8746 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8747 .unwrap();
8748 let shared = WalFileShared::new_shared(file).unwrap();
8749 let coordination = ShmWalCoordination::new(shared, authority.clone());
8750
8751 let prepared = coordination
8752 .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8753 .expect("fresh authority should accept the first prepared header");
8754
8755 let snapshot = authority.snapshot();
8756 assert_eq!(
8757 snapshot.page_size, prepared.page_size,
8758 "authority must publish the prepared page size for later writers and checkpointers"
8759 );
8760 assert_eq!(
8761 snapshot.checkpoint_seq, prepared.checkpoint_seq,
8762 "authority must publish the prepared checkpoint generation"
8763 );
8764 assert_eq!(snapshot.salt_1, prepared.salt_1);
8765 assert_eq!(snapshot.salt_2, prepared.salt_2);
8766 }
8767
8768 #[cfg(host_shared_wal)]
8769 #[test]
8770 #[cfg_attr(
8771 windows,
8772 ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8773 )]
8774 fn test_shm_prepare_wal_header_does_not_clobber_zero_frame_authority_snapshot() {
8775 let dir = tempfile::tempdir().unwrap();
8776 let wal_path = dir.path().join("test.db-wal");
8777 let shm_path = dir.path().join("test.db-tshm");
8778 let io = shared_wal_test_io();
8779
8780 let file_a = io
8781 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8782 .unwrap();
8783 let shared_a = WalFileShared::new_shared(file_a).unwrap();
8784 let authoritative = SharedWalCoordinationHeader {
8785 max_frame: 0,
8786 nbackfills: 0,
8787 transaction_count: 9,
8788 visibility_generation: 3,
8789 checkpoint_seq: 5,
8790 checkpoint_epoch: 7,
8791 page_size: 4096,
8792 salt_1: 17,
8793 salt_2: 23,
8794 checksum_1: 31,
8795 checksum_2: 37,
8796 reader_slot_count: 64,
8797 };
8798 {
8799 let mut shared = shared_a.write();
8800 shared.metadata.max_frame.store(0, Ordering::Release);
8801 shared.metadata.nbackfills.store(0, Ordering::Release);
8802 shared
8803 .metadata
8804 .transaction_count
8805 .store(authoritative.transaction_count, Ordering::Release);
8806 shared.metadata.last_checksum = (31, 37);
8807 let mut header = shared.metadata.wal_header.lock();
8808 header.checkpoint_seq = authoritative.checkpoint_seq;
8809 header.page_size = authoritative.page_size;
8810 header.salt_1 = authoritative.salt_1;
8811 header.salt_2 = authoritative.salt_2;
8812 header.checksum_1 = authoritative.checksum_1;
8813 header.checksum_2 = authoritative.checksum_2;
8814 shared
8815 .runtime
8816 .epoch
8817 .store(authoritative.checkpoint_epoch, Ordering::Release);
8818 shared.metadata.initialized.store(false, Ordering::Release);
8819 }
8820 let authority =
8821 Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8822 authority.install_snapshot(authoritative);
8823
8824 let file_b = io
8825 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8826 .unwrap();
8827 let shared_b = WalFileShared::new_shared(file_b).unwrap();
8828 let coordination_b = ShmWalCoordination::new(shared_b.clone(), authority.clone());
8829 {
8833 let mut shared = shared_b.write();
8834 shared.metadata.max_frame.store(0, Ordering::Release);
8835 shared.metadata.nbackfills.store(0, Ordering::Release);
8836 shared.metadata.last_checksum = (11, 13);
8837 shared
8838 .metadata
8839 .transaction_count
8840 .store(3, Ordering::Release);
8841 let mut header = shared.metadata.wal_header.lock();
8842 header.checkpoint_seq = 2;
8843 header.page_size = 4096;
8844 header.salt_1 = 17;
8845 header.salt_2 = 23;
8846 header.checksum_1 = 11;
8847 header.checksum_2 = 13;
8848 shared.runtime.epoch.store(1, Ordering::Release);
8849 shared.metadata.initialized.store(false, Ordering::Release);
8850 }
8851
8852 let page_size = PageSize::new(4096).unwrap();
8853 let prepared = coordination_b
8854 .prepare_wal_header(io.as_ref(), page_size)
8855 .expect("prepare_wal_header should produce a header");
8856
8857 let snapshot = authority.snapshot();
8858 assert_eq!(
8859 snapshot.transaction_count, authoritative.transaction_count,
8860 "first writer after restart must not downgrade authority transaction_count"
8861 );
8862 assert_eq!(
8863 snapshot.checkpoint_seq, authoritative.checkpoint_seq,
8864 "first writer after restart must not downgrade checkpoint metadata"
8865 );
8866 assert_eq!(
8867 prepared.checkpoint_seq, authoritative.checkpoint_seq,
8868 "header written after restart must use authority checkpoint metadata"
8869 );
8870 assert_eq!(prepared.page_size, authoritative.page_size);
8871 assert_eq!(prepared.salt_1, authoritative.salt_1);
8872 assert_eq!(prepared.salt_2, authoritative.salt_2);
8873 let refreshed = authority.snapshot();
8874 assert_eq!(
8875 refreshed.checksum_1, prepared.checksum_1,
8876 "preparing the first zero-frame header must refresh the authoritative checksum seed"
8877 );
8878 assert_eq!(
8879 refreshed.checksum_2, prepared.checksum_2,
8880 "preparing the first zero-frame header must refresh the authoritative checksum seed"
8881 );
8882 }
8883
8884 #[test]
8885 fn test_in_process_coordination_lock_primitives() {
8886 let (shared, _wal) = make_test_wal();
8887 let coordination = make_test_coordination(&shared);
8888
8889 assert!(coordination.try_checkpoint_lock());
8890 coordination.unlock_checkpoint_lock();
8891
8892 assert!(coordination.try_write_lock());
8893 assert!(!coordination.try_write_lock());
8894 coordination.unlock_write_lock();
8895
8896 assert!(coordination.try_read_mark_exclusive(1));
8897 coordination.set_read_mark_value_exclusive(1, 42);
8898 assert_eq!(coordination.read_mark_value(1), 42);
8899 coordination.unlock_read_mark(1);
8900
8901 assert!(coordination.try_read_mark_shared(1));
8902 assert!(coordination.try_upgrade_read_mark(1));
8903 coordination.downgrade_read_mark(1);
8904 coordination.unlock_read_mark(1);
8905
8906 assert_eq!(shared.read().runtime.read_locks[1].get_value(), 42);
8908 }
8909
8910 #[test]
8911 fn test_in_process_coordination_prepare_truncate_marks_wal_uninitialized() {
8912 let io = shared_wal_test_io();
8913 let dir = tempfile::tempdir().unwrap();
8914 let wal_path = dir.path().join("test.wal");
8915 let file = io
8916 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8917 .unwrap();
8918 let shared = WalFileShared::new_shared(file).unwrap();
8919 let coordination = make_test_coordination(&shared);
8920
8921 shared
8922 .read()
8923 .metadata
8924 .initialized
8925 .store(true, Ordering::Release);
8926 let file = coordination.prepare_truncate().unwrap();
8927
8928 assert!(file.size().is_ok());
8929 assert!(!shared.read().metadata.initialized.load(Ordering::Acquire));
8930 }
8931
8932 #[test]
8933 fn test_in_process_coordination_exposes_wal_io_state() {
8934 let io = shared_wal_test_io();
8935 let dir = tempfile::tempdir().unwrap();
8936 let wal_path = dir.path().join("test.wal");
8937 let file = io
8938 .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8939 .unwrap();
8940 let shared = WalFileShared::new_shared(file).unwrap();
8941 let coordination = make_test_coordination(&shared);
8942
8943 assert!(!coordination.wal_is_initialized());
8944 assert_eq!(coordination.wal_header().page_size, 0);
8945 assert!(coordination.wal_file().unwrap().size().is_ok());
8946
8947 let header = coordination
8948 .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8949 .unwrap();
8950 assert_eq!(header.page_size, 4096);
8951 assert_eq!(
8952 coordination.load_snapshot().last_checksum,
8953 (header.checksum_1, header.checksum_2)
8954 );
8955 assert!(!coordination.wal_is_initialized());
8956
8957 coordination.mark_initialized();
8958 assert!(coordination.wal_is_initialized());
8959 assert!(coordination
8960 .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8961 .is_none());
8962 }
8963
8964 #[test]
8965 fn test_vacuum_lock_blocks_new_read_transactions_until_release() {
8966 let (shared, vacuum_wal) = make_test_wal();
8967 let reader_wal = make_test_wal_from_shared(shared);
8968
8969 vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
8970 vacuum_wal.begin_vacuum_blocking_tx().unwrap();
8971
8972 assert!(
8973 matches!(reader_wal.try_begin_read_tx(), TryBeginReadResult::Busy),
8974 "VACUUM lock should block new WAL readers before they take a read-mark slot"
8975 );
8976 assert!(
8977 !vacuum_wal.holds_read_lock(),
8978 "exclusive VACUUM snapshot must not masquerade as a read-mark lock"
8979 );
8980 assert!(
8981 vacuum_wal.holds_write_lock(),
8982 "begin_vacuum_blocking_tx should acquire the source write lock"
8983 );
8984
8985 vacuum_wal.end_write_tx();
8986 vacuum_wal.release_vacuum_lock();
8987 vacuum_wal.release_vacuum_checkpoint_lock();
8988
8989 assert!(
8990 matches!(reader_wal.try_begin_read_tx(), TryBeginReadResult::Ok(_)),
8991 "reader should start after VACUUM releases the lock"
8992 );
8993 reader_wal.end_read_tx();
8994 }
8995
8996 #[test]
8997 fn test_active_reader_blocks_vacuum_exclusive_tx() {
8998 let (shared, reader_wal) = make_test_wal();
8999 let vacuum_wal = make_test_wal_from_shared(shared);
9000
9001 assert!(matches!(
9002 reader_wal.try_begin_read_tx(),
9003 TryBeginReadResult::Ok(_)
9004 ));
9005 vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
9006
9007 assert!(
9008 matches!(vacuum_wal.begin_vacuum_blocking_tx(), Err(LimboError::Busy)),
9009 "active reader should prevent VACUUM from acquiring its exclusive lock"
9010 );
9011
9012 reader_wal.end_read_tx();
9013 vacuum_wal.begin_vacuum_blocking_tx().unwrap();
9014 vacuum_wal.end_write_tx();
9015 vacuum_wal.release_vacuum_lock();
9016 vacuum_wal.release_vacuum_checkpoint_lock();
9017 }
9018
9019 #[test]
9020 fn test_read_retry_does_not_leak_vacuum_guard_or_block_vacuum() {
9021 let (shared, _) = make_test_wal();
9022 let retry_reader = make_test_wal_from_shared(shared.clone());
9023 let vacuum_wal = make_test_wal_from_shared(shared.clone());
9024
9025 set_shared_snapshot(
9026 &shared,
9027 WalSnapshot {
9028 max_frame: 5,
9029 nbackfills: 0,
9030 last_checksum: (0, 0),
9031 checkpoint_seq: 0,
9032 transaction_count: 1,
9033 },
9034 );
9035
9036 for idx in 1..5 {
9037 assert!(
9038 shared.read().runtime.read_locks[idx].write(),
9039 "expected setup to occupy read-mark slot {idx}"
9040 );
9041 }
9042
9043 assert!(
9044 matches!(retry_reader.try_begin_read_tx(), TryBeginReadResult::Retry),
9045 "reader should retry when all read-mark slots are transiently unavailable"
9046 );
9047 assert!(
9048 !retry_reader.has_vacuum_read_lock_guard(),
9049 "retry path must not retain a shared VACUUM lock guard"
9050 );
9051 assert_eq!(
9052 retry_reader
9053 .max_frame_read_lock_index
9054 .load(Ordering::Acquire),
9055 NO_LOCK_HELD,
9056 "retry path must not retain a read-mark slot"
9057 );
9058
9059 for idx in 1..5 {
9060 shared.read().runtime.read_locks[idx].unlock();
9061 }
9062
9063 vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
9064 vacuum_wal.begin_vacuum_blocking_tx().unwrap();
9065 vacuum_wal.end_write_tx();
9066 vacuum_wal.release_vacuum_lock();
9067 vacuum_wal.release_vacuum_checkpoint_lock();
9068 }
9069
9070 #[test]
9071 fn test_held_vacuum_checkpoint_locks_do_not_release_vacuum_lock() {
9072 let (shared, vacuum_wal) = make_test_wal();
9073 let contender_wal = make_test_wal_from_shared(shared);
9074
9075 vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
9076 vacuum_wal.begin_vacuum_blocking_tx().unwrap();
9077
9078 assert!(vacuum_wal.holds_write_lock());
9079 assert!(!vacuum_wal.holds_read_lock());
9080 assert!(
9081 matches!(
9082 contender_wal.try_begin_vacuum_checkpoint_lock(),
9083 Err(LimboError::Busy)
9084 ),
9085 "held checkpoint lock should block other checkpointers"
9086 );
9087
9088 vacuum_wal.end_write_tx();
9089 assert!(!vacuum_wal.holds_write_lock());
9090
9091 let guard =
9092 CheckpointLocks::from_held_vacuum_checkpoint_lock(vacuum_wal.coordination.clone())
9093 .unwrap();
9094 assert!(
9095 matches!(contender_wal.try_begin_read_tx(), TryBeginReadResult::Busy),
9096 "VACUUM lock should continue blocking readers during final checkpoint"
9097 );
9098
9099 drop(guard);
9100 assert!(
9101 contender_wal.try_begin_vacuum_checkpoint_lock().is_ok(),
9102 "checkpoint cleanup should release the checkpoint lock"
9103 );
9104 contender_wal.release_vacuum_checkpoint_lock();
9105 assert!(
9106 matches!(contender_wal.try_begin_read_tx(), TryBeginReadResult::Busy),
9107 "checkpoint cleanup must not release the VACUUM lock"
9108 );
9109
9110 vacuum_wal.release_vacuum_lock();
9111 assert!(matches!(
9112 contender_wal.try_begin_read_tx(),
9113 TryBeginReadResult::Ok(_)
9114 ));
9115 contender_wal.end_read_tx();
9116 }
9117
9118 #[test]
9119 fn restart_checkpoint_reset_wal_state_handling() {
9120 let (db, path) = get_database();
9121
9122 let walpath = {
9123 let mut p = path.clone().into_os_string().into_string().unwrap();
9124 p.push_str("/test.db-wal");
9125 std::path::PathBuf::from(p)
9126 };
9127
9128 let conn = db.connect().unwrap();
9129 conn.execute("create table test(id integer primary key, value text)")
9130 .unwrap();
9131 bulk_inserts(&conn, 20, 3);
9132 let IOResult::Done(completions) = conn.pager.load().cacheflush().unwrap() else {
9133 panic!()
9134 };
9135 for c in completions {
9136 db.io.wait_for_completion(c).unwrap();
9137 }
9138
9139 let wal_shared = db.shared_wal.clone();
9141 let (seq_before, salt1_before, salt2_before, _ps_before) = wal_header_snapshot(&wal_shared);
9142 let (mx_before, backfill_before) = {
9143 let s = wal_shared.read();
9144 (
9145 s.metadata.max_frame.load(Ordering::SeqCst),
9146 s.metadata.nbackfills.load(Ordering::SeqCst),
9147 )
9148 };
9149 assert!(mx_before > 0);
9150 assert_eq!(backfill_before, 0);
9151
9152 let meta_before = std::fs::metadata(&walpath).unwrap();
9153 #[cfg(unix)]
9154 let size_before = meta_before.blocks();
9155 #[cfg(not(unix))]
9156 let size_before = meta_before.len();
9157 {
9160 let pager = conn.pager.load();
9161 let res = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
9162 assert_eq!(res.wal_max_frame, mx_before);
9163 assert_eq!(res.wal_total_backfilled, mx_before);
9164 assert_eq!(res.wal_checkpoint_backfilled, mx_before);
9165 }
9166
9167 let (seq_after, salt1_after, salt2_after, _ps_after) = wal_header_snapshot(&wal_shared);
9169 assert_eq!(
9170 seq_after,
9171 seq_before.wrapping_add(1),
9172 "checkpoint_seq must increment on RESTART"
9173 );
9174 assert_eq!(
9175 salt1_after,
9176 salt1_before.wrapping_add(1),
9177 "salt_1 is incremented"
9178 );
9179 assert_ne!(salt2_after, salt2_before, "salt_2 is randomized");
9180
9181 let (mx_after, backfill_after) = {
9182 let s = wal_shared.read();
9183 (
9184 s.metadata.max_frame.load(Ordering::SeqCst),
9185 s.metadata.nbackfills.load(Ordering::SeqCst),
9186 )
9187 };
9188 assert_eq!(mx_after, 0, "mxFrame reset to 0 after RESTART");
9189 assert_eq!(backfill_after, 0, "nBackfill reset to 0 after RESTART");
9190
9191 let meta_after = std::fs::metadata(&walpath).unwrap();
9193 #[cfg(unix)]
9194 let size_after = meta_after.blocks();
9195 #[cfg(not(unix))]
9196 let size_after = meta_after.len();
9197 assert_eq!(
9198 size_before, size_after,
9199 "RESTART must not change WAL file size"
9200 );
9201
9202 conn.execute("insert into test(value) values ('post_restart')")
9204 .unwrap();
9205 conn.pager
9206 .load()
9207 .wal
9208 .as_ref()
9209 .unwrap()
9210 .finish_append_frames_commit()
9211 .unwrap();
9212 let new_max = wal_shared.read().metadata.max_frame.load(Ordering::SeqCst);
9213 assert_eq!(new_max, 1, "first append after RESTART starts at frame 1");
9214
9215 std::fs::remove_dir_all(path).unwrap();
9216 }
9217
9218 #[test]
9219 fn test_wal_passive_partial_then_complete() {
9220 let (db, _tmp) = get_database();
9221 let conn1 = db.connect().unwrap();
9222 let conn2 = db.connect().unwrap();
9223
9224 conn1
9225 .execute("create table test(id integer primary key, value text)")
9226 .unwrap();
9227 bulk_inserts(&conn1, 15, 2);
9228 let IOResult::Done(completions) = conn1.pager.load().cacheflush().unwrap() else {
9229 panic!()
9230 };
9231 for c in completions {
9232 db.io.wait_for_completion(c).unwrap();
9233 }
9234
9235 let readmark = {
9237 let pager = conn2.pager.load();
9238 let wal2 = pager.wal.as_ref().unwrap();
9239 wal2.begin_read_tx().unwrap();
9240 wal2.get_max_frame()
9241 };
9242
9243 bulk_inserts(&conn1, 15, 2);
9245 let IOResult::Done(completions) = conn1.pager.load().cacheflush().unwrap() else {
9246 panic!()
9247 };
9248 for c in completions {
9249 db.io.wait_for_completion(c).unwrap();
9250 }
9251
9252 let (res1, max_before) = {
9254 let pager = conn1.pager.load();
9255 let res = run_checkpoint_until_done(
9256 &pager,
9257 CheckpointMode::Passive {
9258 upper_bound_inclusive: None,
9259 },
9260 );
9261 let maxf = db
9262 .shared_wal
9263 .read()
9264 .metadata
9265 .max_frame
9266 .load(Ordering::SeqCst);
9267 (res, maxf)
9268 };
9269 assert_eq!(res1.wal_max_frame, max_before);
9270 assert!(
9271 res1.wal_total_backfilled < res1.wal_max_frame,
9272 "Partial backfill expected, {} : {}",
9273 res1.wal_total_backfilled,
9274 res1.wal_max_frame
9275 );
9276 assert_eq!(
9277 res1.wal_total_backfilled, readmark,
9278 "Checkpointed frames should match read mark"
9279 );
9280 {
9282 let pager = conn2.pager.load();
9283 let wal2 = pager.wal.as_ref().unwrap();
9284 wal2.end_read_tx();
9285 }
9286
9287 let pager = conn1.pager.load();
9289 let res2 = run_checkpoint_until_done(
9290 &pager,
9291 CheckpointMode::Passive {
9292 upper_bound_inclusive: None,
9293 },
9294 );
9295 assert_eq!(
9296 res2.wal_total_backfilled, res2.wal_max_frame,
9297 "Second checkpoint completes remaining frames"
9298 );
9299 }
9300
9301 #[test]
9302 fn test_wal_restart_blocks_readers() {
9303 let (db, _) = get_database();
9304 let conn1 = db.connect().unwrap();
9305 let conn2 = db.connect().unwrap();
9306
9307 conn2
9309 .pager
9310 .load()
9311 .wal
9312 .as_ref()
9313 .unwrap()
9314 .begin_read_tx()
9315 .unwrap();
9316
9317 let p = conn1.pager.load();
9320 let w = p.wal.as_ref().unwrap();
9321 loop {
9322 match w.checkpoint(&p, CheckpointMode::Restart) {
9323 Ok(IOResult::IO(io)) => {
9324 io.wait(db.io.as_ref()).unwrap();
9325 }
9326 e => {
9327 assert!(
9328 matches!(e, Err(LimboError::Busy)),
9329 "reader is holding readmark0 we should return Busy"
9330 );
9331 break;
9332 }
9333 }
9334 }
9335 conn2.pager.load().end_read_tx();
9336
9337 conn1
9338 .execute("create table test(id integer primary key, value text)")
9339 .unwrap();
9340 for i in 0..10 {
9341 conn1
9342 .execute(format!("insert into test(value) values ('value{i}')"))
9343 .unwrap();
9344 }
9345 conn2.pager.load().begin_read_tx().unwrap();
9347 let p = conn1.pager.load();
9348 let w = p.wal.as_ref().unwrap();
9349 loop {
9350 match w.checkpoint(&p, CheckpointMode::Restart) {
9351 Ok(IOResult::IO(io)) => {
9352 io.wait(db.io.as_ref()).unwrap();
9353 }
9354 Ok(IOResult::Done(_)) => {
9355 panic!("Checkpoint should not have succeeded");
9356 }
9357 Err(e) => {
9358 assert!(
9359 matches!(e, LimboError::Busy),
9360 "should return busy if we have readers"
9361 );
9362 break;
9363 }
9364 }
9365 }
9366 }
9367
9368 #[test]
9369 fn test_wal_read_marks_after_restart() {
9370 let (db, _path) = get_database();
9371 let wal_shared = db.shared_wal.clone();
9372
9373 let conn = db.connect().unwrap();
9374 conn.execute("create table test(id integer primary key, value text)")
9375 .unwrap();
9376 bulk_inserts(&conn, 10, 5);
9377 {
9379 let pager = conn.pager.load();
9380 let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
9381 assert!(result.everything_backfilled());
9382 }
9383
9384 let read_marks_after: Vec<_> = {
9386 let s = wal_shared.read();
9387 (0..5)
9388 .map(|i| s.runtime.read_locks[i].get_value())
9389 .collect()
9390 };
9391
9392 assert_eq!(read_marks_after[0], 0, "Slot 0 should remain 0");
9393 assert_eq!(
9394 read_marks_after[1], 0,
9395 "Slot 1 (default reader) should be reset to 0"
9396 );
9397 for (i, item) in read_marks_after.iter().take(5).skip(2).enumerate() {
9398 assert_eq!(
9399 *item, READMARK_NOT_USED,
9400 "Slot {i} should be READMARK_NOT_USED after restart",
9401 );
9402 }
9403 }
9404
9405 #[test]
9406 fn test_wal_concurrent_readers_during_checkpoint() {
9407 let (db, _path) = get_database();
9408 let conn_writer = db.connect().unwrap();
9409
9410 conn_writer
9411 .execute("create table test(id integer primary key, value text)")
9412 .unwrap();
9413 bulk_inserts(&conn_writer, 5, 10);
9414
9415 let conn_r1 = db.connect().unwrap();
9417 let conn_r2 = db.connect().unwrap();
9418
9419 let r1_max_frame = {
9421 let pager = conn_r1.pager.load();
9422 let wal = pager.wal.as_ref().unwrap();
9423 wal.begin_read_tx().unwrap();
9424 wal.get_max_frame()
9425 };
9426 bulk_inserts(&conn_writer, 5, 10);
9427
9428 let r2_max_frame = {
9430 let pager = conn_r2.pager.load();
9431 let wal = pager.wal.as_ref().unwrap();
9432 wal.begin_read_tx().unwrap();
9433 wal.get_max_frame()
9434 };
9435
9436 let checkpoint_result = {
9438 let pager = conn_writer.pager.load();
9439 run_checkpoint_until_done(
9440 &pager,
9441 CheckpointMode::Passive {
9442 upper_bound_inclusive: None,
9443 },
9444 )
9445 };
9446
9447 assert!(
9448 checkpoint_result.wal_total_backfilled < checkpoint_result.wal_max_frame,
9449 "Should not checkpoint all frames when readers are active"
9450 );
9451 assert_eq!(
9452 checkpoint_result.wal_total_backfilled, r1_max_frame,
9453 "Should have checkpointed up to R1's max frame"
9454 );
9455
9456 assert_eq!(
9458 conn_r2.pager.load().wal.as_ref().unwrap().get_max_frame(),
9459 r2_max_frame,
9460 "Reader should maintain its snapshot"
9461 );
9462 }
9463
9464 #[test]
9465 fn test_wal_checkpoint_updates_read_marks() {
9466 let (db, _path) = get_database();
9467 let wal_shared = db.shared_wal.clone();
9468
9469 let conn = db.connect().unwrap();
9470 conn.execute("create table test(id integer primary key, value text)")
9471 .unwrap();
9472 bulk_inserts(&conn, 10, 5);
9473
9474 let max_frame_before = wal_shared.read().metadata.max_frame.load(Ordering::SeqCst);
9476
9477 {
9478 let pager = conn.pager.load();
9479 let _result = run_checkpoint_until_done(
9480 &pager,
9481 CheckpointMode::Passive {
9482 upper_bound_inclusive: None,
9483 },
9484 );
9485 }
9486
9487 let read_mark_1 = wal_shared.read().runtime.read_locks[1].get_value();
9489
9490 assert_eq!(
9491 read_mark_1 as u64, max_frame_before,
9492 "Read mark 1 should be updated to max frame during checkpoint"
9493 );
9494 }
9495
9496 #[test]
9497 fn test_wal_writer_blocks_restart_checkpoint() {
9498 let (db, _path) = get_database();
9499 let conn1 = db.connect().unwrap();
9500 let conn2 = db.connect().unwrap();
9501
9502 conn1
9503 .execute("create table test(id integer primary key, value text)")
9504 .unwrap();
9505 bulk_inserts(&conn1, 5, 5);
9506
9507 {
9509 let pager = conn2.pager.load();
9510 let wal = pager.wal.as_ref().unwrap();
9511 let _ = wal.begin_read_tx().unwrap();
9512 wal.begin_write_tx(WalAutoActions::all_enabled()).unwrap();
9513 }
9514
9515 let result = {
9517 let pager = conn1.pager.load();
9518 let wal = pager.wal.as_ref().unwrap();
9519 wal.checkpoint(&pager, CheckpointMode::Restart)
9520 };
9521
9522 assert!(
9523 matches!(result, Err(LimboError::Busy)),
9524 "Restart checkpoint should fail when write lock is held"
9525 );
9526
9527 conn2.pager.load().wal.as_ref().unwrap().end_read_tx();
9528 conn2.pager.load().wal.as_ref().unwrap().end_write_tx();
9530
9531 let result = {
9533 let pager = conn1.pager.load();
9534 run_checkpoint_until_done(&pager, CheckpointMode::Restart)
9535 };
9536
9537 assert!(result.everything_backfilled());
9538 }
9539
9540 #[test]
9541 #[should_panic(expected = "must have a read transaction to begin a write transaction")]
9542 fn test_wal_read_transaction_required_before_write() {
9543 let (db, _path) = get_database();
9544 let conn = db.connect().unwrap();
9545
9546 conn.execute("create table test(id integer primary key, value text)")
9547 .unwrap();
9548
9549 let pager = conn.pager.load();
9551 let wal = pager.wal.as_ref().unwrap();
9552 let _ = wal.begin_write_tx(WalAutoActions::all_enabled());
9553 }
9554
9555 fn check_read_lock_slot(conn: &Arc<Connection>, _expected_slot: usize) -> bool {
9556 let pager = conn.pager.load();
9557 let _wal = pager.wal.as_ref().unwrap();
9558 #[cfg(debug_assertions)]
9559 {
9560 let wal_any = _wal.as_any();
9561 if let Some(wal_file) = wal_any.downcast_ref::<crate::WalFile>() {
9562 return wal_file.max_frame_read_lock_index.load(Ordering::Acquire)
9563 == _expected_slot;
9564 }
9565 }
9566
9567 false
9568 }
9569
9570 #[test]
9571 fn test_wal_multiple_readers_at_different_frames() {
9572 let (db, _path) = get_database();
9573 let conn_writer = db.connect().unwrap();
9574
9575 conn_writer
9576 .execute("CREATE TABLE test(id INTEGER PRIMARY KEY, value TEXT)")
9577 .unwrap();
9578
9579 fn start_reader(conn: &Arc<Connection>) -> (u64, crate::Statement) {
9580 conn.execute("BEGIN").unwrap();
9581 let mut stmt = conn.prepare("SELECT * FROM test").unwrap();
9582 stmt.step().unwrap();
9583 let frame = conn.pager.load().wal.as_ref().unwrap().get_max_frame();
9584 (frame, stmt)
9585 }
9586
9587 bulk_inserts(&conn_writer, 3, 5);
9588
9589 let conn1 = &db.connect().unwrap();
9590 let (r1_frame, _stmt) = start_reader(conn1); bulk_inserts(&conn_writer, 3, 5);
9593
9594 let conn_r2 = db.connect().unwrap();
9595 let (r2_frame, _stmt2) = start_reader(&conn_r2); bulk_inserts(&conn_writer, 3, 5);
9598
9599 let conn_r3 = db.connect().unwrap();
9600 let (r3_frame, _stmt3) = start_reader(&conn_r3); assert!(r1_frame < r2_frame && r2_frame < r3_frame);
9603
9604 let result1 = {
9606 let pager = conn_writer.pager.load();
9607 run_checkpoint_until_done(
9608 &pager,
9609 CheckpointMode::Passive {
9610 upper_bound_inclusive: None,
9611 },
9612 )
9613 };
9614 assert_eq!(result1.wal_total_backfilled, r1_frame);
9615
9616 conn1.execute("COMMIT").unwrap();
9618
9619 let result2 = {
9621 let pager = conn_writer.pager.load();
9622 run_checkpoint_until_done(
9623 &pager,
9624 CheckpointMode::Passive {
9625 upper_bound_inclusive: None,
9626 },
9627 )
9628 };
9629 assert_eq!(
9630 result1.wal_checkpoint_backfilled + result2.wal_checkpoint_backfilled,
9631 r2_frame
9632 );
9633
9634 let r2_cnt = count_test_table(&conn_r2);
9636 let r3_cnt = count_test_table(&conn_r3);
9637
9638 assert_eq!(r2_cnt, 30);
9639 assert_eq!(r3_cnt, 45);
9640 }
9641
9642 #[test]
9643 fn test_checkpoint_truncate_reset_handling() {
9644 let (db, path) = get_database();
9645 let conn = db.connect().unwrap();
9646
9647 let walpath = {
9648 let mut p = path.clone().into_os_string().into_string().unwrap();
9649 p.push_str("/test.db-wal");
9650 std::path::PathBuf::from(p)
9651 };
9652
9653 conn.execute("create table test(id integer primary key, value text)")
9654 .unwrap();
9655 bulk_inserts(&conn, 10, 10);
9656
9657 let size_before = std::fs::metadata(&walpath).unwrap().len();
9659 assert!(size_before > 0, "WAL file should have content");
9660
9661 {
9663 let pager = conn.pager.load();
9664 run_checkpoint_until_done(
9665 &pager,
9666 CheckpointMode::Truncate {
9667 upper_bound_inclusive: None,
9668 },
9669 );
9670 }
9671
9672 let size_after = std::fs::metadata(&walpath).unwrap().len();
9674 assert_eq!(size_after, 0, "WAL file should be truncated to 0 bytes");
9675
9676 conn.execute("INSERT INTO test VALUES (1001, 'after-truncate')")
9678 .unwrap();
9679
9680 let new_size = std::fs::metadata(&walpath).unwrap().len();
9682 assert!(new_size >= 32, "WAL file too small");
9683 let hdr = read_wal_header(&walpath);
9684 let expected_magic = if cfg!(target_endian = "big") {
9685 sqlite3_ondisk::WAL_MAGIC_BE
9686 } else {
9687 sqlite3_ondisk::WAL_MAGIC_LE
9688 };
9689 assert!(
9690 hdr.magic == expected_magic,
9691 "bad WAL magic: {:#X}, expected: {:#X}",
9692 hdr.magic,
9693 sqlite3_ondisk::WAL_MAGIC_BE
9694 );
9695 assert_eq!(hdr.file_format, 3007000);
9696 assert_eq!(hdr.page_size, 4096, "invalid page size");
9697 assert_eq!(hdr.checkpoint_seq, 1, "invalid checkpoint_seq");
9698 std::fs::remove_dir_all(path).unwrap();
9699 }
9700
9701 #[test]
9702 fn test_wal_checkpoint_truncate_db_file_contains_data() {
9703 let (db, path) = get_database();
9704 let conn = db.connect().unwrap();
9705
9706 let walpath = {
9707 let mut p = path.clone().into_os_string().into_string().unwrap();
9708 p.push_str("/test.db-wal");
9709 std::path::PathBuf::from(p)
9710 };
9711
9712 conn.execute("create table test(id integer primary key, value text)")
9713 .unwrap();
9714 bulk_inserts(&conn, 10, 100);
9715
9716 let size_before = std::fs::metadata(&walpath).unwrap().len();
9718 assert!(size_before > 0, "WAL file should have content");
9719
9720 {
9722 let pager = conn.pager.load();
9723 run_checkpoint_until_done(
9724 &pager,
9725 CheckpointMode::Truncate {
9726 upper_bound_inclusive: None,
9727 },
9728 );
9729 }
9730
9731 let size_after = std::fs::metadata(&walpath).unwrap().len();
9733 assert_eq!(size_after, 0, "WAL file should be truncated to 0 bytes");
9734
9735 conn.execute("INSERT INTO test VALUES (1001, 'after-truncate')")
9737 .unwrap();
9738
9739 let new_size = std::fs::metadata(&walpath).unwrap().len();
9741 assert!(new_size >= 32, "WAL file too small");
9742 let hdr = read_wal_header(&walpath);
9743 let expected_magic = if cfg!(target_endian = "big") {
9744 sqlite3_ondisk::WAL_MAGIC_BE
9745 } else {
9746 sqlite3_ondisk::WAL_MAGIC_LE
9747 };
9748 assert!(
9749 hdr.magic == expected_magic,
9750 "bad WAL magic: {:#X}, expected: {:#X}",
9751 hdr.magic,
9752 sqlite3_ondisk::WAL_MAGIC_BE
9753 );
9754 assert_eq!(hdr.file_format, 3007000);
9755 assert_eq!(hdr.page_size, 4096, "invalid page size");
9756 assert_eq!(hdr.checkpoint_seq, 1, "invalid checkpoint_seq");
9757 {
9758 let pager = conn.pager.load();
9759 run_checkpoint_until_done(
9760 &pager,
9761 CheckpointMode::Passive {
9762 upper_bound_inclusive: None,
9763 },
9764 );
9765 }
9766 std::fs::remove_file(&walpath).unwrap();
9769
9770 let count = count_test_table(&conn);
9771 assert_eq!(
9772 count, 1001,
9773 "we should have 1001 rows in the table all together"
9774 );
9775 std::fs::remove_dir_all(path).unwrap();
9776 }
9777
9778 fn read_wal_header(path: &std::path::Path) -> sqlite3_ondisk::WalHeader {
9779 use std::{fs::File, io::Read};
9780 let mut hdr = [0u8; 32];
9781 File::open(path).unwrap().read_exact(&mut hdr).unwrap();
9782 let be = |i| u32::from_be_bytes(hdr[i..i + 4].try_into().unwrap());
9783 sqlite3_ondisk::WalHeader {
9784 magic: be(0x00),
9785 file_format: be(0x04),
9786 page_size: be(0x08),
9787 checkpoint_seq: be(0x0C),
9788 salt_1: be(0x10),
9789 salt_2: be(0x14),
9790 checksum_1: be(0x18),
9791 checksum_2: be(0x1C),
9792 }
9793 }
9794
9795 #[test]
9796 fn test_wal_stale_snapshot_in_write_transaction() {
9797 let (db, _path) = get_database();
9798 let conn1 = db.connect().unwrap();
9799 let conn2 = db.connect().unwrap();
9800
9801 conn1
9802 .execute("create table test(id integer primary key, value text)")
9803 .unwrap();
9804 {
9806 let pager = conn2.pager.load();
9807 let wal = pager.wal.as_ref().unwrap();
9808 wal.begin_read_tx().unwrap();
9809 }
9810 bulk_inserts(&conn1, 5, 5);
9812 let result = {
9814 let pager = conn2.pager.load();
9815 let wal = pager.wal.as_ref().unwrap();
9816 wal.begin_write_tx(WalAutoActions::all_enabled())
9817 };
9818 assert!(matches!(result, Err(LimboError::BusySnapshot)));
9820
9821 {
9823 let pager = conn2.pager.load();
9824 let wal = pager.wal.as_ref().unwrap();
9825 wal.end_read_tx();
9826 wal.begin_read_tx().unwrap();
9827 }
9828 let result = {
9830 let pager = conn2.pager.load();
9831 let wal = pager.wal.as_ref().unwrap();
9832 wal.begin_write_tx(WalAutoActions::all_enabled())
9833 };
9834 assert!(matches!(result, Ok(())));
9835 }
9836
9837 #[test]
9838 fn test_wal_readlock0_optimization_behavior() {
9839 let (db, _path) = get_database();
9840 let conn1 = db.connect().unwrap();
9841 let conn2 = db.connect().unwrap();
9842
9843 conn1
9844 .execute("create table test(id integer primary key, value text)")
9845 .unwrap();
9846 bulk_inserts(&conn1, 5, 5);
9847 {
9849 let pager = conn1.pager.load();
9850 run_checkpoint_until_done(
9851 &pager,
9852 CheckpointMode::Passive {
9853 upper_bound_inclusive: None,
9854 },
9855 );
9856 }
9857
9858 {
9860 let pager = conn2.pager.load();
9861 let wal = pager.wal.as_ref().unwrap();
9862 wal.begin_read_tx().unwrap();
9863 }
9864 assert!(check_read_lock_slot(&conn2, 0));
9866 {
9867 let pager = conn1.pager.load();
9868 let wal = pager.wal.as_ref().unwrap();
9869 let frame = wal.find_frame(5, None);
9870 assert!(frame.is_ok_and(|f| f.is_none()));
9872 }
9873 {
9875 let pager = conn1.pager.load();
9876 let wal = pager.wal.as_ref().unwrap();
9877 let result = wal.checkpoint(&pager, CheckpointMode::Restart);
9878
9879 assert!(
9880 matches!(result, Err(LimboError::Busy)),
9881 "RESTART checkpoint should fail when a reader is using slot 0"
9882 );
9883 }
9884 {
9886 let pager = conn2.pager.load();
9887 let wal = pager.wal.as_ref().unwrap();
9888 wal.end_read_tx();
9889 }
9890 {
9891 let pager = conn1.pager.load();
9892 let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
9893 assert!(
9894 result.everything_backfilled(),
9895 "RESTART checkpoint should succeed after reader releases slot 0"
9896 );
9897 }
9898 }
9899
9900 #[test]
9901 fn test_wal_full_backfills_all() {
9902 let (db, _tmp) = get_database();
9903 let conn = db.connect().unwrap();
9904
9905 conn.execute("create table test(id integer primary key, value text)")
9907 .unwrap();
9908 bulk_inserts(&conn, 8, 4);
9909
9910 let IOResult::Done(completions) = conn.pager.load().cacheflush().unwrap() else {
9912 panic!()
9913 };
9914 for c in completions {
9915 db.io.wait_for_completion(c).unwrap();
9916 }
9917
9918 let wal_shared = db.shared_wal.clone();
9920 let mx_before = wal_shared.read().metadata.max_frame.load(Ordering::SeqCst);
9921 assert!(mx_before > 0, "expected frames in WAL before FULL");
9922
9923 let result = {
9925 let pager = conn.pager.load();
9926 run_checkpoint_until_done(&pager, CheckpointMode::Full)
9927 };
9928
9929 assert_eq!(result.wal_checkpoint_backfilled, mx_before);
9930 assert_eq!(result.wal_total_backfilled, mx_before);
9931 }
9932
9933 #[test]
9934 fn test_wal_full_waits_for_old_reader_then_succeeds() {
9935 let (db, _tmp) = get_database();
9936 let writer = db.connect().unwrap();
9937 let reader = db.connect().unwrap();
9938
9939 writer
9940 .execute("create table test(id integer primary key, value text)")
9941 .unwrap();
9942
9943 bulk_inserts(&writer, 2, 3);
9945 let IOResult::Done(completions) = writer.pager.load().cacheflush().unwrap() else {
9946 panic!()
9947 };
9948 for c in completions {
9949 db.io.wait_for_completion(c).unwrap();
9950 }
9951
9952 {
9954 let pager = reader.pager.load();
9955 let wal = pager.wal.as_ref().unwrap();
9956 wal.begin_read_tx().unwrap();
9957 }
9958 let r_snapshot = {
9959 let pager = reader.pager.load();
9960 let wal = pager.wal.as_ref().unwrap();
9961 wal.get_max_frame()
9962 };
9963
9964 bulk_inserts(&writer, 3, 4);
9966 let IOResult::Done(completions) = writer.pager.load().cacheflush().unwrap() else {
9967 panic!()
9968 };
9969 for c in completions {
9970 db.io.wait_for_completion(c).unwrap();
9971 }
9972 let mx_now = db
9973 .shared_wal
9974 .read()
9975 .metadata
9976 .max_frame
9977 .load(Ordering::SeqCst);
9978 assert!(mx_now > r_snapshot);
9979
9980 {
9982 let pager = writer.pager.load();
9983 let wal = pager.wal.as_ref().unwrap();
9984 loop {
9985 match wal.checkpoint(&pager, CheckpointMode::Full) {
9986 Ok(IOResult::IO(io)) => {
9987 io.wait(db.io.as_ref()).unwrap();
9989 }
9990 Err(LimboError::Busy) => {
9991 break;
9992 }
9993 other => panic!("expected Busy from FULL with old reader, got {other:?}"),
9994 }
9995 }
9996 }
9997 assert_eq!(
9998 db.shared_wal
9999 .read()
10000 .metadata
10001 .nbackfills
10002 .load(Ordering::SeqCst),
10003 0,
10004 "a FULL checkpoint that returns Busy must not publish positive nbackfills before DB sync"
10005 );
10006
10007 {
10009 let pager = reader.pager.load();
10010 let wal = pager.wal.as_ref().unwrap();
10011 wal.end_read_tx();
10012 }
10013
10014 let result = {
10015 let pager = writer.pager.load();
10016 run_checkpoint_until_done(&pager, CheckpointMode::Full)
10017 };
10018
10019 assert_eq!(
10020 result.wal_checkpoint_backfilled, mx_now,
10021 "the successful FULL reruns from the last durable backfill point because the Busy attempt did not publish progress"
10022 );
10023 assert!(result.everything_backfilled());
10024 }
10025
10026 #[test]
10027 fn test_rollback_releases_read_lock() {
10028 let (db, _path) = get_database();
10029 let conn = db.connect().unwrap();
10030
10031 conn.execute("CREATE TABLE t(x)").unwrap();
10032 conn.execute("BEGIN").unwrap();
10033 conn.execute("INSERT INTO t VALUES(1)").unwrap();
10034
10035 {
10036 let pager = conn.pager.load();
10037 let wal = pager.wal.as_ref().unwrap();
10038 assert!(
10039 wal.holds_read_lock(),
10040 "read lock must be held during write tx"
10041 );
10042 }
10043
10044 conn.execute("ROLLBACK").unwrap();
10045
10046 {
10047 let pager = conn.pager.load();
10048 let wal = pager.wal.as_ref().unwrap();
10049 assert!(
10050 !wal.holds_read_lock(),
10051 "read lock must be released after ROLLBACK"
10052 );
10053 }
10054 }
10055
10056 #[test]
10057 fn test_rollback_releases_shared_read_lock_slot() {
10058 let (db, _path) = get_database();
10059 let conn = db.connect().unwrap();
10060
10061 conn.execute("CREATE TABLE t(x)").unwrap();
10062 conn.execute("BEGIN").unwrap();
10063 conn.execute("INSERT INTO t VALUES(1)").unwrap();
10064
10065 let locked_slots_before = {
10066 let shared = db.shared_wal.read();
10067 read_slots_with_readers(&shared)
10068 };
10069 assert_eq!(
10070 locked_slots_before.len(),
10071 1,
10072 "expected exactly one shared read-lock slot while transaction is active"
10073 );
10074
10075 conn.execute("ROLLBACK").unwrap();
10076
10077 let locked_slots_after = {
10078 let shared = db.shared_wal.read();
10079 read_slots_with_readers(&shared)
10080 };
10081 assert!(
10082 locked_slots_after.is_empty(),
10083 "ROLLBACK must release the shared read-lock slot"
10084 );
10085 }
10086
10087 #[test]
10088 fn test_rollback_releases_slot_zero_read_lock() {
10089 let (db, _path) = get_database();
10090 let conn = db.connect().unwrap();
10091
10092 conn.execute("CREATE TABLE test(id integer primary key, value text)")
10093 .unwrap();
10094 bulk_inserts(&conn, 3, 3);
10095 {
10096 let pager = conn.pager.load();
10097 let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
10098 assert!(
10099 result.everything_backfilled(),
10100 "restart checkpoint setup must fully backfill WAL"
10101 );
10102 }
10103
10104 conn.execute("BEGIN").unwrap();
10105 conn.execute("INSERT INTO test(value) VALUES('slot0')")
10106 .unwrap();
10107
10108 let locked_slots_before = {
10109 let shared = db.shared_wal.read();
10110 read_slots_with_readers(&shared)
10111 };
10112 assert_eq!(
10113 locked_slots_before,
10114 vec![0],
10115 "writer should use slot 0 when WAL is fully checkpointed"
10116 );
10117
10118 conn.execute("ROLLBACK").unwrap();
10119
10120 let locked_slots_after = {
10121 let shared = db.shared_wal.read();
10122 read_slots_with_readers(&shared)
10123 };
10124 assert!(
10125 locked_slots_after.is_empty(),
10126 "ROLLBACK must release slot 0 shared read-lock as well"
10127 );
10128 }
10129
10130 #[test]
10131 fn test_savepoint_rollback_preserves_read_lock() {
10132 let (db, _path) = get_database();
10133 let conn = db.connect().unwrap();
10134
10135 conn.execute("CREATE TABLE t(x INTEGER PRIMARY KEY)")
10136 .unwrap();
10137 conn.execute("BEGIN").unwrap();
10138 conn.execute("INSERT INTO t VALUES(1)").unwrap();
10139
10140 let res = conn.execute("INSERT INTO t VALUES(1)");
10145 assert!(res.is_err(), "duplicate PK insert must fail");
10146
10147 {
10148 let pager = conn.pager.load();
10149 let wal = pager.wal.as_ref().unwrap();
10150 assert!(
10151 wal.holds_read_lock(),
10152 "read lock must still be held after savepoint rollback"
10153 );
10154 assert!(
10155 wal.holds_write_lock(),
10156 "write lock must still be held after savepoint rollback"
10157 );
10158 }
10159
10160 conn.execute("COMMIT").unwrap();
10163
10164 let mut stmt = conn.prepare("SELECT count(*) FROM t").unwrap();
10165 let mut count: i64 = 0;
10166 stmt.run_with_row_callback(|row| {
10167 count = row.get(0).unwrap();
10168 Ok(())
10169 })
10170 .unwrap();
10171 assert_eq!(count, 1, "first insert should survive savepoint rollback");
10172 }
10173
10174 #[test]
10175 fn test_savepoint_then_tx_rollback_allows_restart_checkpoint_from_other_connection() {
10176 let (db, _path) = get_database();
10177 let conn1 = db.connect().unwrap();
10178 let conn2 = db.connect().unwrap();
10179
10180 conn1
10181 .execute("CREATE TABLE test(id integer primary key, value text)")
10182 .unwrap();
10183 bulk_inserts(&conn1, 2, 2);
10184 let count_before = count_test_table(&conn1);
10185
10186 conn1.execute("BEGIN").unwrap();
10187 conn1
10188 .execute("INSERT INTO test(id, value) VALUES(1000, 'first')")
10189 .unwrap();
10190 let duplicate = conn1.execute("INSERT INTO test(id, value) VALUES(1000, 'dup')");
10191 assert!(duplicate.is_err(), "duplicate PK insert must fail");
10192
10193 {
10194 let pager = conn1.pager.load();
10195 let wal = pager.wal.as_ref().unwrap();
10196 assert!(
10197 wal.holds_read_lock(),
10198 "read lock must still be held after savepoint rollback"
10199 );
10200 assert!(
10201 wal.holds_write_lock(),
10202 "write lock must still be held after savepoint rollback"
10203 );
10204 }
10205
10206 conn1.execute("ROLLBACK").unwrap();
10207
10208 {
10209 let pager = conn1.pager.load();
10210 let wal = pager.wal.as_ref().unwrap();
10211 assert!(
10212 !wal.holds_read_lock(),
10213 "read lock must be released after transaction rollback"
10214 );
10215 assert!(
10216 !wal.holds_write_lock(),
10217 "write lock must be released after transaction rollback"
10218 );
10219 }
10220
10221 let locked_slots_after_rollback = {
10222 let shared = db.shared_wal.read();
10223 read_slots_with_readers(&shared)
10224 };
10225 assert!(
10226 locked_slots_after_rollback.is_empty(),
10227 "transaction rollback after savepoint failure must not leak shared read locks"
10228 );
10229 assert_eq!(
10230 count_test_table(&conn1),
10231 count_before,
10232 "transaction rollback should remove writes made before savepoint failure"
10233 );
10234
10235 let result = {
10236 let pager = conn2.pager.load();
10237 run_checkpoint_until_done(&pager, CheckpointMode::Restart)
10238 };
10239 assert!(
10240 result.everything_backfilled(),
10241 "restart checkpoint from another connection must succeed after full rollback"
10242 );
10243 }
10244
10245 #[test]
10246 fn test_checkpoint_succeeds_after_rollback() {
10247 let (db, _path) = get_database();
10248 let conn = db.connect().unwrap();
10249
10250 conn.execute("CREATE TABLE test(id integer primary key, value text)")
10251 .unwrap();
10252 bulk_inserts(&conn, 5, 3);
10253
10254 conn.execute("BEGIN").unwrap();
10255 conn.execute("INSERT INTO test(value) VALUES('rollback_me')")
10256 .unwrap();
10257 conn.execute("ROLLBACK").unwrap();
10258
10259 let pager = conn.pager.load();
10260 let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
10261 assert!(
10262 result.everything_backfilled(),
10263 "checkpoint must succeed after rollback, not return Busy"
10264 );
10265 }
10266}