1use std::collections::{HashMap, HashSet};
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use fsqlite_error::{FrankenError, Result};
16use fsqlite_pager::traits::{
17 PreparedWalChecksumSeed, PreparedWalFinalizationState, PreparedWalFrameBatch,
18 PreparedWalFrameMeta, WalFrameRef, WalFuture, WalLogicalReadSnapshot,
19};
20use fsqlite_pager::{
21 CheckpointMode, CheckpointPageWriter, CheckpointResult, ParallelWalCommitReconciliation,
22 WalBackend, WalPublicationSnapshot,
23};
24use fsqlite_types::cx::Cx;
25use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
26use fsqlite_types::{CommitSeq, PageNumber, PageSize};
27#[cfg(all(feature = "native", any(unix, windows)))]
28use fsqlite_vfs::DatabaseNamespaceBinding;
29use fsqlite_vfs::{SyncKind, Vfs, VfsFile, VfsWriteCompletion};
30use fsqlite_wal::checkpoint_executor::CheckpointTargetFuture;
31use fsqlite_wal::checksum::{SqliteWalChecksum, WAL_FRAME_HEADER_SIZE, WalChecksumTransform};
32use fsqlite_wal::wal::WalAppendFrameRef;
33use fsqlite_wal::{
34 CheckpointMode as WalCheckpointMode, CheckpointState, CheckpointTarget,
35 PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC, PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE,
36 ParallelWalCommitCertificate, ParallelWalDurableCertificateRecord,
37 ParallelWalFramePayloadDigestBuilder, TransactionConflictPageBaseline,
38 TransactionConflictSnapshot, WAL_HEADER_SIZE, WalFile, WalGenerationIdentity, WalHeader,
39 WalSalts, execute_checkpoint, validate_wal_header_checksum,
40};
41use tracing::debug;
42#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
43use tracing::warn;
44
45#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
46use crate::wal_fec_adapter::{FecCommitHook, FecCommitResult};
47
48#[cfg(test)]
49mod test_support {
50 use std::fmt::Debug;
51 use std::future::Future;
52
53 std::thread_local! {
54 static TEST_RUNTIME: asupersync::runtime::Runtime =
55 asupersync::runtime::RuntimeBuilder::current_thread()
56 .blocking_threads(1, 2)
57 .build()
58 .expect("WAL adapter test runtime should build");
59 }
60
61 fn block_on<F: Future>(future: F) -> F::Output {
62 TEST_RUNTIME.with(|runtime| runtime.block_on(future))
63 }
64
65 pub(super) trait FutureResultTestExt<T, E>:
66 Future<Output = std::result::Result<T, E>> + Sized
67 {
68 fn wait(self) -> std::result::Result<T, E> {
69 block_on(self)
70 }
71
72 fn expect(self, message: &str) -> T
73 where
74 E: Debug,
75 {
76 block_on(self).expect(message)
77 }
78
79 fn expect_err(self, message: &str) -> E
80 where
81 T: Debug,
82 {
83 block_on(self).expect_err(message)
84 }
85 }
86
87 impl<F, T, E> FutureResultTestExt<T, E> for F where
88 F: Future<Output = std::result::Result<T, E>> + Sized
89 {
90 }
91}
92
93#[cfg(test)]
94use self::test_support::FutureResultTestExt;
95
96struct WalWriteCompletionPreflight<'a> {
103 completion: Option<&'a VfsWriteCompletion>,
104}
105
106impl<'a> WalWriteCompletionPreflight<'a> {
107 const fn new(completion: Option<&'a VfsWriteCompletion>) -> Self {
108 Self { completion }
109 }
110
111 fn hand_off(&mut self) {
112 self.completion = None;
113 }
114}
115
116impl Drop for WalWriteCompletionPreflight<'_> {
117 fn drop(&mut self) {
118 if let Some(completion) = self.completion {
119 completion.complete_error();
120 }
121 }
122}
123
124const PAGE_INDEX_MAX_ENTRIES: usize = usize::MAX;
135
136fn sqlite_database_header_page_size(page_one: &[u8]) -> Option<u32> {
137 if page_one.len() < 18 || !page_one.starts_with(b"SQLite format 3\0") {
138 return None;
139 }
140 let encoded = u16::from_be_bytes([page_one[16], page_one[17]]);
141 let decoded = if encoded == 1 {
142 65_536
143 } else {
144 u32::from(encoded)
145 };
146 PageSize::new(decoded).map(PageSize::get)
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156enum WalPageLookupResolution {
157 AuthoritativeHit { frame_index: usize },
158 AuthoritativeMiss,
159 PartialIndexFallbackHit { frame_index: usize },
160 PartialIndexFallbackMiss,
161}
162
163impl WalPageLookupResolution {
164 #[must_use]
165 const fn frame_index(self) -> Option<usize> {
166 match self {
167 Self::AuthoritativeHit { frame_index }
168 | Self::PartialIndexFallbackHit { frame_index } => Some(frame_index),
169 Self::AuthoritativeMiss | Self::PartialIndexFallbackMiss => None,
170 }
171 }
172
173 #[must_use]
174 const fn lookup_mode(self) -> &'static str {
175 match self {
176 Self::AuthoritativeHit { .. } | Self::AuthoritativeMiss => "authoritative_index",
177 Self::PartialIndexFallbackHit { .. } | Self::PartialIndexFallbackMiss => {
178 "partial_index_fallback"
179 }
180 }
181 }
182
183 #[must_use]
184 const fn fallback_reason(self) -> &'static str {
185 match self {
186 Self::AuthoritativeHit { .. } | Self::AuthoritativeMiss => "none",
187 Self::PartialIndexFallbackHit { .. } | Self::PartialIndexFallbackMiss => {
188 "partial_index_cap"
189 }
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
200struct WalPublishedSnapshot {
201 publication_seq: u64,
202 generation: WalGenerationIdentity,
203 last_commit_frame: Option<usize>,
204 commit_count: u64,
205 page_index: Arc<HashMap<u32, usize>>,
206 index_is_partial: bool,
207}
208
209impl WalPublishedSnapshot {
210 #[must_use]
211 fn empty(publication_seq: u64, generation: WalGenerationIdentity) -> Self {
212 Self {
213 publication_seq,
214 generation,
215 last_commit_frame: None,
216 commit_count: 0,
217 page_index: Arc::new(HashMap::new()),
218 index_is_partial: false,
219 }
220 }
221}
222
223#[must_use]
224fn wal_publication_snapshot_from_published(
225 snapshot: &WalPublishedSnapshot,
226) -> WalPublicationSnapshot {
227 WalPublicationSnapshot {
228 publication_seq: snapshot.publication_seq,
229 generation: snapshot.generation,
230 last_commit_frame: snapshot.last_commit_frame,
231 commit_count: snapshot.commit_count,
232 latest_frame_entries: snapshot.page_index.len(),
233 index_is_partial: snapshot.index_is_partial,
234 }
235}
236
237#[derive(Debug, Clone, Copy)]
238struct PendingPublicationFrame {
239 page_number: u32,
240 frame_index: usize,
241 is_commit: bool,
242}
243
244pub struct WalBackendAdapter<F: VfsFile> {
245 wal: WalFile<F>,
246 refresh_before_append: bool,
248 published_snapshot: WalPublishedSnapshot,
250 next_publication_seq: u64,
252 read_snapshot: Option<WalPublishedSnapshot>,
254 pending_publication_frames: Vec<PendingPublicationFrame>,
256 pending_publication_commit: Option<usize>,
262 pending_publication_generation: Option<WalGenerationIdentity>,
267 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
269 fec_hook: Option<FecCommitHook>,
270 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
272 fec_pending: Vec<FecCommitResult>,
273 page_index_cap: usize,
277}
278
279impl<F: VfsFile> WalBackendAdapter<F> {
280 #[must_use]
282 pub fn new(wal: WalFile<F>) -> Self {
283 let generation = wal.generation_identity();
284 Self {
285 wal,
286 refresh_before_append: true,
287 published_snapshot: WalPublishedSnapshot::empty(0, generation),
288 next_publication_seq: 1,
289 read_snapshot: None,
290 pending_publication_frames: Vec::new(),
291 pending_publication_commit: None,
292 pending_publication_generation: None,
293 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
294 fec_hook: None,
295 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
296 fec_pending: Vec::new(),
297 page_index_cap: PAGE_INDEX_MAX_ENTRIES,
298 }
299 }
300
301 #[must_use]
303 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
304 pub fn with_fec_hook(wal: WalFile<F>, hook: FecCommitHook) -> Self {
305 let generation = wal.generation_identity();
306 Self {
307 wal,
308 refresh_before_append: true,
309 published_snapshot: WalPublishedSnapshot::empty(0, generation),
310 next_publication_seq: 1,
311 read_snapshot: None,
312 pending_publication_frames: Vec::new(),
313 pending_publication_commit: None,
314 pending_publication_generation: None,
315 fec_hook: Some(hook),
316 fec_pending: Vec::new(),
317 page_index_cap: PAGE_INDEX_MAX_ENTRIES,
318 }
319 }
320
321 #[must_use]
331 pub fn has_pending_publication(&self) -> bool {
332 self.pending_publication_commit.is_some() || !self.pending_publication_frames.is_empty()
333 }
334
335 pub fn into_inner(self) -> Result<WalFile<F>> {
345 if self.has_pending_publication() {
346 return Err(FrankenError::Busy);
347 }
348 Ok(self.wal)
349 }
350
351 #[must_use]
353 pub fn inner(&self) -> &WalFile<F> {
354 &self.wal
355 }
356
357 pub fn inner_mut(&mut self) -> Result<&mut WalFile<F>> {
366 if self.has_pending_publication() {
367 return Err(FrankenError::Busy);
368 }
369 self.invalidate_publication();
370 Ok(&mut self.wal)
371 }
372
373 #[must_use]
379 pub fn published_snapshot(&self) -> WalPublicationSnapshot {
380 wal_publication_snapshot_from_published(&self.published_snapshot)
381 }
382
383 #[must_use]
385 pub fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
386 self.read_snapshot
387 .as_ref()
388 .map(wal_publication_snapshot_from_published)
389 }
390
391 pub async fn refresh_published_snapshot(&mut self, cx: &Cx) -> Result<WalPublicationSnapshot> {
394 self.wal.refresh(cx).await?;
395 self.publish_latest_committed_snapshot(cx, "refresh_published_snapshot")
396 .await?;
397 Ok(self.published_snapshot())
398 }
399
400 fn invalidate_publication(&mut self) {
402 self.read_snapshot = None;
403 self.discard_pending_publication();
404 self.published_snapshot = WalPublishedSnapshot::empty(
405 self.published_snapshot.publication_seq,
406 self.published_snapshot.generation,
407 );
408 }
409
410 async fn publish_visible_snapshot(
416 &mut self,
417 cx: &Cx,
418 last_commit_frame: Option<usize>,
419 scenario_id: &'static str,
420 ) -> Result<()> {
421 let generation = self.wal.generation_identity();
422 if self.published_snapshot.generation == generation
423 && self.published_snapshot.last_commit_frame == last_commit_frame
424 {
425 return Ok(());
426 }
427
428 let previous_generation = self.published_snapshot.generation;
429 let previous_last_commit = self.published_snapshot.last_commit_frame;
430 let previous_commit_count = if previous_generation == generation {
431 self.published_snapshot.commit_count
432 } else {
433 0
434 };
435 let mut page_index = if previous_generation == generation {
436 std::mem::replace(
437 &mut self.published_snapshot.page_index,
438 Arc::new(HashMap::new()),
439 )
440 } else {
441 Arc::new(HashMap::new())
442 };
443 let mut index_is_partial = if previous_generation == generation {
444 self.published_snapshot.index_is_partial
445 } else {
446 false
447 };
448
449 let frame_delta_count = match (previous_last_commit, last_commit_frame) {
450 (Some(prev), Some(curr)) if curr >= prev => curr.saturating_sub(prev),
451 (Some(_) | None, Some(curr)) => curr.saturating_add(1),
452 (Some(prev), None) => prev.saturating_add(1),
453 (None, None) => 0,
454 };
455
456 let scan_result = match last_commit_frame {
457 None => {
458 Arc::make_mut(&mut page_index).clear();
459 index_is_partial = false;
460 Ok(0)
461 }
462 Some(current_last_commit) => {
463 let (start, base_commit_count) =
464 match (previous_generation == generation, previous_last_commit) {
465 (true, Some(previous_last_commit))
466 if previous_last_commit < current_last_commit =>
467 {
468 (
469 previous_last_commit.saturating_add(1),
470 previous_commit_count,
471 )
472 }
473 (true, Some(previous_last_commit))
474 if previous_last_commit == current_last_commit =>
475 {
476 (current_last_commit.saturating_add(1), previous_commit_count)
477 }
478 _ => {
479 Arc::make_mut(&mut page_index).clear();
480 index_is_partial = false;
481 (0, 0)
482 }
483 };
484 if start <= current_last_commit {
485 self.index_range_and_count_commits(
486 cx,
487 Arc::make_mut(&mut page_index),
488 &mut index_is_partial,
489 start,
490 current_last_commit,
491 )
492 .await
493 .map(|delta| base_commit_count.saturating_add(delta))
494 } else {
495 Ok(base_commit_count)
496 }
497 }
498 };
499 let commit_count = match scan_result {
500 Ok(commit_count) => commit_count,
501 Err(error) => {
502 if previous_generation == generation {
503 self.published_snapshot.page_index = page_index;
504 }
505 return Err(error);
506 }
507 };
508
509 let publication_seq = self.next_publication_seq;
510 self.next_publication_seq = self.next_publication_seq.saturating_add(1);
511 let latest_frame_entries = page_index.len();
512 self.published_snapshot = WalPublishedSnapshot {
513 publication_seq,
514 generation,
515 last_commit_frame,
516 commit_count,
517 page_index,
518 index_is_partial,
519 };
520
521 tracing::trace!(
522 target: "fsqlite.wal_publication",
523 trace_id = cx.trace_id(),
524 run_id = "wal-publication",
525 scenario_id,
526 wal_generation = generation.checkpoint_seq,
527 wal_salt1 = generation.salts.salt1,
528 wal_salt2 = generation.salts.salt2,
529 publication_seq,
530 frame_delta_count,
531 latest_frame_entries,
532 snapshot_age = 0_u64,
533 lookup_mode = "published_visibility_map",
534 fallback_reason = if index_is_partial {
535 "partial_index_cap"
536 } else {
537 "none"
538 },
539 "published WAL visibility snapshot"
540 );
541
542 Ok(())
543 }
544
545 async fn resolve_visible_frame(
552 &self,
553 cx: &Cx,
554 snapshot: &WalPublishedSnapshot,
555 page_number: u32,
556 ) -> Result<WalPageLookupResolution> {
557 match snapshot.page_index.get(&page_number) {
558 Some(&frame_index) => Ok(WalPageLookupResolution::AuthoritativeHit { frame_index }),
559 None if !snapshot.index_is_partial => Ok(WalPageLookupResolution::AuthoritativeMiss),
560 None => match snapshot.last_commit_frame {
561 Some(last_commit_frame) => {
562 match self
563 .scan_backwards_for_page(cx, page_number, last_commit_frame)
564 .await?
565 {
566 Some(frame_index) => {
567 Ok(WalPageLookupResolution::PartialIndexFallbackHit { frame_index })
568 }
569 None => Ok(WalPageLookupResolution::PartialIndexFallbackMiss),
570 }
571 }
572 None => Ok(WalPageLookupResolution::AuthoritativeMiss),
573 },
574 }
575 }
576
577 async fn index_range_and_count_commits(
583 &self,
584 cx: &Cx,
585 page_index: &mut HashMap<u32, usize>,
586 index_is_partial: &mut bool,
587 start: usize,
588 end: usize,
589 ) -> Result<u64> {
590 if start > end {
591 return Ok(0);
592 }
593
594 let mut commit_count = 0_u64;
595 for frame_index in start..=end {
596 let header = self.wal.read_frame_header(cx, frame_index).await?;
597 if page_index.len() < self.page_index_cap
600 || page_index.contains_key(&header.page_number)
601 {
602 page_index.insert(header.page_number, frame_index);
603 } else {
604 *index_is_partial = true;
608 }
609 if header.is_commit() {
610 commit_count = commit_count.saturating_add(1);
611 }
612 }
613 Ok(commit_count)
614 }
615
616 async fn scan_backwards_for_page(
623 &self,
624 cx: &Cx,
625 page_number: u32,
626 last_commit_frame: usize,
627 ) -> Result<Option<usize>> {
628 for frame_index in (0..=last_commit_frame).rev() {
629 let header = self.wal.read_frame_header(cx, frame_index).await?;
630 if header.page_number == page_number {
631 return Ok(Some(frame_index));
632 }
633 }
634 Ok(None)
635 }
636
637 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
639 pub fn take_fec_pending(&mut self) -> Vec<FecCommitResult> {
640 std::mem::take(&mut self.fec_pending)
641 }
642
643 #[must_use]
645 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
646 pub fn fec_enabled(&self) -> bool {
647 self.fec_hook
648 .as_ref()
649 .is_some_and(FecCommitHook::is_enabled)
650 }
651
652 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
654 pub fn fec_discard(&mut self) {
655 if let Some(hook) = &mut self.fec_hook {
656 hook.discard_buffered();
657 }
658 }
659
660 #[cfg(test)]
662 fn set_page_index_cap(&mut self, cap: usize) {
663 self.page_index_cap = cap;
664 self.invalidate_publication();
666 }
667
668 #[must_use]
669 fn current_prepared_finalization_state(&self) -> PreparedWalFinalizationState {
670 let generation = self.wal.generation_identity();
671 let seed = self.wal.running_checksum();
672 PreparedWalFinalizationState {
673 checkpoint_seq: generation.checkpoint_seq,
674 salt1: generation.salts.salt1,
675 salt2: generation.salts.salt2,
676 start_frame_index: self.wal.frame_count(),
677 seed: PreparedWalChecksumSeed {
678 s1: seed.s1,
679 s2: seed.s2,
680 },
681 }
682 }
683
684 #[must_use]
685 fn prepared_batch_matches_current_state(&self, prepared: &PreparedWalFrameBatch) -> bool {
686 prepared
687 .finalized_for
688 .is_some_and(|state| state == self.current_prepared_finalization_state())
689 }
690
691 async fn prepared_batch_matches_disk_state(
692 &self,
693 cx: &Cx,
694 prepared: &PreparedWalFrameBatch,
695 ) -> Result<bool> {
696 let Some(state) = prepared.finalized_for else {
697 return Ok(false);
698 };
699 let generation = WalGenerationIdentity {
700 checkpoint_seq: state.checkpoint_seq,
701 salts: fsqlite_wal::checksum::WalSalts {
702 salt1: state.salt1,
703 salt2: state.salt2,
704 },
705 };
706 self.wal
707 .prepared_append_window_still_current(cx, generation, state.start_frame_index)
708 .await
709 }
710
711 fn checksum_transforms_for_prepared(
712 prepared: &PreparedWalFrameBatch,
713 ) -> Vec<WalChecksumTransform> {
714 prepared
715 .checksum_transforms
716 .iter()
717 .map(|transform| WalChecksumTransform {
718 a11: transform.a11,
719 a12: transform.a12,
720 a21: transform.a21,
721 a22: transform.a22,
722 c1: transform.c1,
723 c2: transform.c2,
724 })
725 .collect()
726 }
727
728 fn finalize_prepared_batch_against_current_state(
729 &self,
730 prepared: &mut PreparedWalFrameBatch,
731 ) -> Result<()> {
732 let checksum_transforms = Self::checksum_transforms_for_prepared(prepared);
733 let final_running_checksum = self
734 .wal
735 .finalize_prepared_frame_bytes(&mut prepared.frame_bytes, &checksum_transforms)?;
736 prepared.finalized_for = Some(self.current_prepared_finalization_state());
737 prepared.finalized_running_checksum = Some(PreparedWalChecksumSeed {
738 s1: final_running_checksum.s1,
739 s2: final_running_checksum.s2,
740 });
741 Ok(())
742 }
743
744 fn finalized_running_checksum(prepared: &PreparedWalFrameBatch) -> Result<SqliteWalChecksum> {
745 let Some(checksum) = prepared.finalized_running_checksum else {
746 return Err(FrankenError::internal(
747 "prepared WAL batch missing finalized running checksum",
748 ));
749 };
750 Ok(SqliteWalChecksum {
751 s1: checksum.s1,
752 s2: checksum.s2,
753 })
754 }
755
756 async fn publish_latest_committed_snapshot(
757 &mut self,
758 cx: &Cx,
759 scenario_id: &'static str,
760 ) -> Result<()> {
761 let last_commit_frame = self.wal.last_commit_frame(cx)?;
762 let last_commit_frame = if self.pending_publication_commit.is_some() {
768 let durable_frames = self.wal.last_fsynced_frame_count();
769 last_commit_frame.filter(|frame| {
770 frame
771 .checked_add(1)
772 .is_some_and(|frame_count| frame_count <= durable_frames)
773 })
774 } else {
775 last_commit_frame
776 };
777 self.publish_visible_snapshot(cx, last_commit_frame, scenario_id)
778 .await
779 }
780
781 async fn synchronize_publication_before_append(
782 &mut self,
783 cx: &Cx,
784 scenario_id: &'static str,
785 ) -> Result<()> {
786 if self.has_pending_publication() {
794 return Err(FrankenError::Busy);
795 }
796 self.wal.refresh(cx).await?;
797 self.discard_pending_publication();
798 self.publish_latest_committed_snapshot(cx, scenario_id)
799 .await
800 }
801
802 fn discard_pending_publication(&mut self) {
804 self.pending_publication_frames.clear();
805 self.pending_publication_commit = None;
806 self.pending_publication_generation = None;
807 }
808
809 fn stage_pending_commit_publication(&mut self, last_commit_frame: usize) -> Result<()> {
816 let generation = self.wal.generation_identity();
817 if self
821 .pending_publication_generation
822 .is_some_and(|staged| staged != generation)
823 {
824 return Err(FrankenError::WalCorrupt {
825 detail: "cannot stage a commit horizon across differing WAL generations".to_owned(),
826 });
827 }
828 let staged = self
829 .pending_publication_commit
830 .map_or(last_commit_frame, |staged| staged.max(last_commit_frame));
831 self.pending_publication_commit = Some(staged);
832 self.pending_publication_generation = Some(generation);
833 Ok(())
834 }
835
836 fn assert_pending_horizon_matches_wal(
844 &mut self,
845 cx: &Cx,
846 last_commit_frame: usize,
847 ) -> Result<()> {
848 let generation = self.wal.generation_identity();
849 if self
850 .pending_publication_generation
851 .is_some_and(|staged| staged != generation)
852 {
853 return Err(FrankenError::WalCorrupt {
854 detail: "WAL generation changed before the staged commit horizon was published"
855 .to_owned(),
856 });
857 }
858
859 let frame_count = self.wal.frame_count();
860 if last_commit_frame >= frame_count {
861 return Err(FrankenError::WalCorrupt {
862 detail: format!(
863 "staged commit horizon {last_commit_frame} exceeds WAL frame count {frame_count}"
864 ),
865 });
866 }
867
868 let live_last_commit = self.wal.last_commit_frame(cx)?;
869 if live_last_commit.is_none_or(|live| live < last_commit_frame) {
870 return Err(FrankenError::WalCorrupt {
871 detail: format!(
872 "WAL does not report staged commit horizon {last_commit_frame} as committed"
873 ),
874 });
875 }
876
877 if self
878 .pending_publication_frames
879 .iter()
880 .any(|frame| frame.frame_index >= frame_count)
881 {
882 return Err(FrankenError::WalCorrupt {
883 detail: format!(
884 "a staged publication frame lies beyond WAL frame count {frame_count}"
885 ),
886 });
887 }
888
889 Ok(())
890 }
891
892 fn assert_publish_safe(&mut self, cx: &Cx, last_commit_frame: usize) -> Result<()> {
893 self.assert_pending_horizon_matches_wal(cx, last_commit_frame)?;
894
895 let publish_frame_count =
898 last_commit_frame
899 .checked_add(1)
900 .ok_or_else(|| FrankenError::WalCorrupt {
901 detail: "staged commit horizon overflows the publishable frame count"
902 .to_owned(),
903 })?;
904 self.wal.assert_publish_safe(publish_frame_count)?;
905
906 Ok(())
907 }
908
909 fn publish_authorized_deferred_commit(&mut self, cx: &Cx) -> Result<()> {
916 let Some(last_commit_frame) = self.pending_publication_commit else {
917 return Ok(());
918 };
919 self.assert_pending_horizon_matches_wal(cx, last_commit_frame)?;
920 self.publish_pending_commit_snapshot(cx, last_commit_frame, "authorized_deferred_commit");
921 self.pending_publication_commit = None;
922 self.pending_publication_generation = None;
923 Ok(())
924 }
925
926 fn publish_pending_after_sync(&mut self, cx: &Cx) -> Result<()> {
933 let Some(last_commit_frame) = self.pending_publication_commit else {
934 return Ok(());
935 };
936 self.assert_publish_safe(cx, last_commit_frame)?;
937 self.publish_pending_commit_snapshot(cx, last_commit_frame, "sync_publish_commit");
938 self.pending_publication_commit = None;
939 self.pending_publication_generation = None;
940 Ok(())
941 }
942
943 fn record_appended_frames<I>(&mut self, start_frame_index: usize, frames: I) -> Option<usize>
944 where
945 I: IntoIterator<Item = (u32, u32)>,
946 {
947 let mut last_commit_frame = None;
948 for (offset, (page_number, db_size_if_commit)) in frames.into_iter().enumerate() {
949 let frame_index = start_frame_index.saturating_add(offset);
950 self.pending_publication_frames
951 .push(PendingPublicationFrame {
952 page_number,
953 frame_index,
954 is_commit: db_size_if_commit != 0,
955 });
956 if db_size_if_commit != 0 {
957 last_commit_frame = Some(frame_index);
958 }
959 }
960 last_commit_frame
961 }
962
963 fn publish_pending_commit_snapshot(
970 &mut self,
971 cx: &Cx,
972 last_commit_frame: usize,
973 scenario_id: &'static str,
974 ) {
975 let generation = self.wal.generation_identity();
976 let previous_last_commit = self.published_snapshot.last_commit_frame;
977 let can_extend_previous = self.published_snapshot.generation == generation
978 && self
979 .published_snapshot
980 .last_commit_frame
981 .is_none_or(|previous_last_commit| previous_last_commit < last_commit_frame);
982 let mut page_index = if can_extend_previous {
983 std::mem::replace(
984 &mut self.published_snapshot.page_index,
985 Arc::new(HashMap::new()),
986 )
987 } else {
988 Arc::new(HashMap::new())
989 };
990 let mut index_is_partial = if can_extend_previous {
991 self.published_snapshot.index_is_partial
992 } else {
993 false
994 };
995 let previous_last_commit = if can_extend_previous {
996 previous_last_commit
997 } else {
998 None
999 };
1000 let previous_commit_count = if can_extend_previous {
1001 self.published_snapshot.commit_count
1002 } else {
1003 0
1004 };
1005
1006 let mut frame_delta_count = 0_usize;
1007 let mut commit_delta_count = 0_u64;
1008 for frame in &self.pending_publication_frames {
1009 if previous_last_commit
1010 .is_some_and(|previous_last_commit| frame.frame_index <= previous_last_commit)
1011 || frame.frame_index > last_commit_frame
1012 {
1013 continue;
1014 }
1015
1016 frame_delta_count = frame_delta_count.saturating_add(1);
1017 let page_index_map = Arc::make_mut(&mut page_index);
1018 if page_index_map.len() < self.page_index_cap
1019 || page_index_map.contains_key(&frame.page_number)
1020 {
1021 page_index_map.insert(frame.page_number, frame.frame_index);
1022 } else {
1023 index_is_partial = true;
1024 }
1025 if frame.is_commit {
1026 commit_delta_count = commit_delta_count.saturating_add(1);
1027 }
1028 }
1029
1030 if frame_delta_count == 0 {
1031 if can_extend_previous {
1038 self.published_snapshot.page_index = page_index;
1039 }
1040 self.pending_publication_frames.clear();
1041 return;
1042 }
1043
1044 let publication_seq = self.next_publication_seq;
1045 self.next_publication_seq = self.next_publication_seq.saturating_add(1);
1046 let latest_frame_entries = page_index.len();
1047 self.published_snapshot = WalPublishedSnapshot {
1048 publication_seq,
1049 generation,
1050 last_commit_frame: Some(last_commit_frame),
1051 commit_count: previous_commit_count.saturating_add(commit_delta_count),
1052 page_index,
1053 index_is_partial,
1054 };
1055 self.pending_publication_frames.clear();
1056
1057 tracing::trace!(
1058 target: "fsqlite.wal_publication",
1059 trace_id = cx.trace_id(),
1060 run_id = "wal-publication",
1061 scenario_id,
1062 wal_generation = generation.checkpoint_seq,
1063 wal_salt1 = generation.salts.salt1,
1064 wal_salt2 = generation.salts.salt2,
1065 publication_seq,
1066 frame_delta_count,
1067 latest_frame_entries,
1068 snapshot_age = 0_u64,
1069 lookup_mode = "published_visibility_map",
1070 fallback_reason = if index_is_partial {
1071 "partial_index_cap"
1072 } else {
1073 "none"
1074 },
1075 "published WAL visibility snapshot from commit path"
1076 );
1077 }
1078}
1079
1080fn to_wal_mode(mode: CheckpointMode) -> WalCheckpointMode {
1082 match mode {
1083 CheckpointMode::Passive => WalCheckpointMode::Passive,
1084 CheckpointMode::Full => WalCheckpointMode::Full,
1085 CheckpointMode::Restart => WalCheckpointMode::Restart,
1086 CheckpointMode::Truncate => WalCheckpointMode::Truncate,
1087 }
1088}
1089
1090impl<F: VfsFile> WalBackend for WalBackendAdapter<F> {
1091 fn begin_transaction<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
1092 Box::pin(async move {
1093 if self.has_pending_publication() {
1099 return Err(FrankenError::Busy);
1100 }
1101 self.wal.refresh(cx).await?;
1104 self.publish_latest_committed_snapshot(cx, "begin_transaction")
1105 .await?;
1106 self.read_snapshot = Some(self.published_snapshot.clone());
1107 self.refresh_before_append = true;
1108 Ok(())
1109 })
1110 }
1111
1112 fn published_snapshot(&self) -> Option<WalPublicationSnapshot> {
1113 Some(Self::published_snapshot(self))
1114 }
1115
1116 fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
1117 Self::pinned_read_snapshot(self)
1118 }
1119
1120 fn refresh_published_snapshot<'a>(
1121 &'a mut self,
1122 cx: &'a Cx,
1123 ) -> WalFuture<'a, Option<WalPublicationSnapshot>> {
1124 Box::pin(async move { Self::refresh_published_snapshot(self, cx).await.map(Some) })
1125 }
1126
1127 fn publish_authorized_deferred_commit<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
1128 Box::pin(async move { Self::publish_authorized_deferred_commit(self, cx) })
1129 }
1130
1131 fn append_frame<'a>(
1132 &'a mut self,
1133 cx: &'a Cx,
1134 page_number: u32,
1135 page_data: &'a [u8],
1136 db_size_if_commit: u32,
1137 ) -> WalFuture<'a, ()> {
1138 Box::pin(async move {
1139 if self.refresh_before_append {
1140 self.synchronize_publication_before_append(cx, "append_frame_pre_refresh")
1144 .await?;
1145 }
1146 let start_frame_index = self.wal.frame_count();
1147 self.wal
1148 .append_frame(cx, page_number, page_data, db_size_if_commit)
1149 .await?;
1150 self.refresh_before_append = false;
1151 let last_commit_frame =
1152 self.record_appended_frames(start_frame_index, [(page_number, db_size_if_commit)]);
1153
1154 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1157 if let Some(hook) = &mut self.fec_hook {
1158 match hook.on_frame(cx, page_number, page_data, db_size_if_commit) {
1159 Ok(Some(result)) => {
1160 debug!(
1161 pages = result.page_numbers.len(),
1162 k_source = result.k_source,
1163 symbols = result.symbols.len(),
1164 "FEC commit group encoded"
1165 );
1166 self.fec_pending.push(result);
1167 }
1168 Ok(None) => {}
1169 Err(e) => {
1170 warn!(error = %e, "FEC encoding failed; commit proceeds without repair symbols");
1172 }
1173 }
1174 }
1175
1176 if let Some(last_commit_frame) = last_commit_frame {
1177 self.stage_pending_commit_publication(last_commit_frame)?;
1180 }
1181
1182 Ok(())
1183 })
1184 }
1185
1186 fn append_frames<'a>(
1187 &'a mut self,
1188 cx: &'a Cx,
1189 frames: &'a [WalFrameRef<'a>],
1190 ) -> WalFuture<'a, ()> {
1191 Box::pin(async move {
1192 if frames.is_empty() {
1193 return Ok(());
1194 }
1195
1196 if self.refresh_before_append {
1197 self.synchronize_publication_before_append(cx, "append_frames_pre_refresh")
1198 .await?;
1199 }
1200
1201 let start_frame_index = self.wal.frame_count();
1202 let mut wal_frames = Vec::with_capacity(frames.len());
1203 for frame in frames {
1204 wal_frames.push(WalAppendFrameRef {
1205 page_number: frame.page_number,
1206 page_data: frame.page_data,
1207 db_size_if_commit: frame.db_size_if_commit,
1208 });
1209 }
1210 self.wal.append_frames(cx, &wal_frames).await?;
1211 self.refresh_before_append = false;
1212 let last_commit_frame = self.record_appended_frames(
1213 start_frame_index,
1214 frames
1215 .iter()
1216 .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1217 );
1218
1219 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1220 if let Some(hook) = &mut self.fec_hook {
1221 for frame in frames {
1222 match hook.on_frame(
1223 cx,
1224 frame.page_number,
1225 frame.page_data,
1226 frame.db_size_if_commit,
1227 ) {
1228 Ok(Some(result)) => {
1229 debug!(
1230 pages = result.page_numbers.len(),
1231 k_source = result.k_source,
1232 symbols = result.symbols.len(),
1233 "FEC commit group encoded"
1234 );
1235 self.fec_pending.push(result);
1236 }
1237 Ok(None) => {}
1238 Err(e) => {
1239 warn!(
1240 error = %e,
1241 "FEC encoding failed; commit proceeds without repair symbols"
1242 );
1243 }
1244 }
1245 }
1246 }
1247
1248 if let Some(last_commit_frame) = last_commit_frame {
1249 self.stage_pending_commit_publication(last_commit_frame)?;
1251 }
1252
1253 Ok(())
1254 })
1255 }
1256
1257 fn append_frames_tracked<'a>(
1258 &'a mut self,
1259 cx: &'a Cx,
1260 frames: &'a [WalFrameRef<'a>],
1261 completion: VfsWriteCompletion,
1262 ) -> WalFuture<'a, ()> {
1263 Box::pin(async move {
1264 let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
1265 if frames.is_empty() {
1266 completion.complete_success();
1267 preflight.hand_off();
1268 return Ok(());
1269 }
1270
1271 if self.refresh_before_append {
1272 self.synchronize_publication_before_append(cx, "append_frames_pre_refresh")
1273 .await?;
1274 }
1275
1276 let start_frame_index = self.wal.frame_count();
1277 let mut wal_frames = Vec::with_capacity(frames.len());
1278 for frame in frames {
1279 wal_frames.push(WalAppendFrameRef {
1280 page_number: frame.page_number,
1281 page_data: frame.page_data,
1282 db_size_if_commit: frame.db_size_if_commit,
1283 });
1284 }
1285 preflight.hand_off();
1286 drop(preflight);
1287 self.wal
1288 .append_frames_tracked(cx, &wal_frames, completion)
1289 .await?;
1290 self.refresh_before_append = false;
1291 let last_commit_frame = self.record_appended_frames(
1292 start_frame_index,
1293 frames
1294 .iter()
1295 .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1296 );
1297
1298 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1299 if let Some(hook) = &mut self.fec_hook {
1300 for frame in frames {
1301 match hook.on_frame(
1302 cx,
1303 frame.page_number,
1304 frame.page_data,
1305 frame.db_size_if_commit,
1306 ) {
1307 Ok(Some(result)) => {
1308 debug!(
1309 pages = result.page_numbers.len(),
1310 k_source = result.k_source,
1311 symbols = result.symbols.len(),
1312 "FEC commit group encoded"
1313 );
1314 self.fec_pending.push(result);
1315 }
1316 Ok(None) => {}
1317 Err(e) => {
1318 warn!(
1319 error = %e,
1320 "FEC encoding failed; commit proceeds without repair symbols"
1321 );
1322 }
1323 }
1324 }
1325 }
1326
1327 if let Some(last_commit_frame) = last_commit_frame {
1328 self.stage_pending_commit_publication(last_commit_frame)?;
1330 }
1331
1332 Ok(())
1333 })
1334 }
1335
1336 fn prepare_append_frames(
1337 &self,
1338 frames: &[WalFrameRef<'_>],
1339 ) -> Result<Option<PreparedWalFrameBatch>> {
1340 if frames.is_empty() {
1341 return Ok(None);
1342 }
1343
1344 let mut frame_bytes = Vec::new();
1345 let mut checksum_transforms = Vec::new();
1346 let last_commit_frame_offset = self.wal.prepare_frame_bytes_with_transforms_into(
1347 frames.len(),
1348 frames.iter().map(|frame| WalAppendFrameRef {
1349 page_number: frame.page_number,
1350 page_data: frame.page_data,
1351 db_size_if_commit: frame.db_size_if_commit,
1352 }),
1353 &mut frame_bytes,
1354 &mut checksum_transforms,
1355 )?;
1356 let frame_metas = frames
1357 .iter()
1358 .map(|frame| PreparedWalFrameMeta {
1359 page_number: frame.page_number,
1360 db_size_if_commit: frame.db_size_if_commit,
1361 })
1362 .collect();
1363
1364 Ok(Some(PreparedWalFrameBatch {
1365 frame_size: self.wal.frame_size(),
1366 page_data_offset: WAL_FRAME_HEADER_SIZE,
1367 big_endian_checksum: self.wal.big_endian_checksum(),
1368 frame_metas,
1369 checksum_transforms,
1370 frame_bytes,
1371 last_commit_frame_offset,
1372 finalized_for: None,
1373 finalized_running_checksum: None,
1374 }))
1375 }
1376
1377 fn finalize_prepared_frames(
1378 &self,
1379 _cx: &Cx,
1380 prepared: &mut PreparedWalFrameBatch,
1381 ) -> Result<()> {
1382 if prepared.frame_count() == 0 {
1383 return Ok(());
1384 }
1385 self.finalize_prepared_batch_against_current_state(prepared)
1389 }
1390
1391 fn append_prepared_frames<'a>(
1392 &'a mut self,
1393 cx: &'a Cx,
1394 prepared: &'a mut PreparedWalFrameBatch,
1395 ) -> WalFuture<'a, ()> {
1396 Box::pin(async move {
1397 if prepared.frame_count() == 0 {
1398 return Ok(());
1399 }
1400
1401 let can_reuse_prelock_finalize = self.refresh_before_append
1402 && self.prepared_batch_matches_current_state(prepared)
1403 && self.prepared_batch_matches_disk_state(cx, prepared).await?;
1404 if self.refresh_before_append && !can_reuse_prelock_finalize {
1405 self.synchronize_publication_before_append(cx, "append_prepared_pre_refresh")
1406 .await?;
1407 }
1408
1409 if !self.prepared_batch_matches_current_state(prepared) {
1410 self.finalize_prepared_batch_against_current_state(prepared)?;
1411 }
1412
1413 let start_frame_index = self.wal.frame_count();
1414 self.wal
1415 .append_finalized_prepared_frame_bytes(
1416 cx,
1417 &prepared.frame_bytes,
1418 prepared.frame_count(),
1419 Self::finalized_running_checksum(prepared)?,
1420 prepared.last_commit_frame_offset,
1421 )
1422 .await?;
1423 self.refresh_before_append = false;
1424 let last_commit_frame = self.record_appended_frames(
1425 start_frame_index,
1426 prepared
1427 .frame_metas
1428 .iter()
1429 .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1430 );
1431
1432 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1433 if let Some(hook) = &mut self.fec_hook {
1434 for (index, frame) in prepared.frame_metas.iter().enumerate() {
1435 match hook.on_frame(
1436 cx,
1437 frame.page_number,
1438 prepared.page_data(index),
1439 frame.db_size_if_commit,
1440 ) {
1441 Ok(Some(result)) => {
1442 debug!(
1443 pages = result.page_numbers.len(),
1444 k_source = result.k_source,
1445 symbols = result.symbols.len(),
1446 "FEC commit group encoded"
1447 );
1448 self.fec_pending.push(result);
1449 }
1450 Ok(None) => {}
1451 Err(e) => {
1452 warn!(
1453 error = %e,
1454 "FEC encoding failed; commit proceeds without repair symbols"
1455 );
1456 }
1457 }
1458 }
1459 }
1460
1461 if let Some(last_commit_frame) = last_commit_frame {
1462 self.stage_pending_commit_publication(last_commit_frame)?;
1464 }
1465
1466 Ok(())
1467 })
1468 }
1469
1470 fn append_prepared_frames_tracked<'a>(
1471 &'a mut self,
1472 cx: &'a Cx,
1473 prepared: &'a mut PreparedWalFrameBatch,
1474 completion: VfsWriteCompletion,
1475 ) -> WalFuture<'a, ()> {
1476 Box::pin(async move {
1477 let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
1478 if prepared.frame_count() == 0 {
1479 completion.complete_success();
1480 preflight.hand_off();
1481 return Ok(());
1482 }
1483
1484 let can_reuse_prelock_finalize = self.refresh_before_append
1485 && self.prepared_batch_matches_current_state(prepared)
1486 && self.prepared_batch_matches_disk_state(cx, prepared).await?;
1487 if self.refresh_before_append && !can_reuse_prelock_finalize {
1488 self.synchronize_publication_before_append(cx, "append_prepared_pre_refresh")
1489 .await?;
1490 }
1491
1492 if !self.prepared_batch_matches_current_state(prepared) {
1493 self.finalize_prepared_batch_against_current_state(prepared)?;
1494 }
1495
1496 let start_frame_index = self.wal.frame_count();
1497 let final_running_checksum = Self::finalized_running_checksum(prepared)?;
1498 preflight.hand_off();
1499 drop(preflight);
1500 self.wal
1501 .append_finalized_prepared_frame_bytes_tracked(
1502 cx,
1503 &prepared.frame_bytes,
1504 prepared.frame_count(),
1505 final_running_checksum,
1506 prepared.last_commit_frame_offset,
1507 completion,
1508 )
1509 .await?;
1510 self.refresh_before_append = false;
1511 let last_commit_frame = self.record_appended_frames(
1512 start_frame_index,
1513 prepared
1514 .frame_metas
1515 .iter()
1516 .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1517 );
1518
1519 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1520 if let Some(hook) = &mut self.fec_hook {
1521 for (index, frame) in prepared.frame_metas.iter().enumerate() {
1522 match hook.on_frame(
1523 cx,
1524 frame.page_number,
1525 prepared.page_data(index),
1526 frame.db_size_if_commit,
1527 ) {
1528 Ok(Some(result)) => {
1529 debug!(
1530 pages = result.page_numbers.len(),
1531 k_source = result.k_source,
1532 symbols = result.symbols.len(),
1533 "FEC commit group encoded"
1534 );
1535 self.fec_pending.push(result);
1536 }
1537 Ok(None) => {}
1538 Err(e) => {
1539 warn!(
1540 error = %e,
1541 "FEC encoding failed; commit proceeds without repair symbols"
1542 );
1543 }
1544 }
1545 }
1546 }
1547
1548 if let Some(last_commit_frame) = last_commit_frame {
1549 self.stage_pending_commit_publication(last_commit_frame)?;
1551 }
1552
1553 Ok(())
1554 })
1555 }
1556
1557 fn read_page<'a>(&'a mut self, cx: &'a Cx, page_number: u32) -> WalFuture<'a, Option<Vec<u8>>> {
1558 Box::pin(async move {
1559 let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1560 snapshot
1561 } else {
1562 self.publish_latest_committed_snapshot(cx, "read_page_unpinned")
1563 .await?;
1564 self.published_snapshot.clone()
1565 };
1566 if snapshot.last_commit_frame.is_none() {
1567 return Ok(None);
1568 }
1569 let snapshot_age = self
1570 .published_snapshot
1571 .publication_seq
1572 .saturating_sub(snapshot.publication_seq);
1573
1574 let resolution = self
1575 .resolve_visible_frame(cx, &snapshot, page_number)
1576 .await?;
1577 let Some(frame_index) = resolution.frame_index() else {
1578 debug!(
1579 page_number,
1580 wal_checkpoint_seq = snapshot.generation.checkpoint_seq,
1581 wal_salt1 = snapshot.generation.salts.salt1,
1582 wal_salt2 = snapshot.generation.salts.salt2,
1583 publication_seq = snapshot.publication_seq,
1584 snapshot_age,
1585 lookup_mode = resolution.lookup_mode(),
1586 fallback_reason = resolution.fallback_reason(),
1587 "WAL adapter: page absent from current generation"
1588 );
1589 return Ok(None);
1590 };
1591
1592 let mut frame_buf = vec![0u8; self.wal.frame_size()];
1594 let header = self
1595 .wal
1596 .read_frame_into(cx, frame_index, &mut frame_buf)
1597 .await?;
1598
1599 if header.page_number != page_number {
1602 return Err(FrankenError::WalCorrupt {
1603 detail: format!(
1604 "WAL page index integrity failure: expected page {page_number} \
1605 at frame {frame_index}, found page {}",
1606 header.page_number
1607 ),
1608 });
1609 }
1610
1611 let header_size = fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE;
1623 let page_size = self.wal.page_size();
1624 frame_buf.copy_within(header_size.., 0);
1625 frame_buf.truncate(page_size);
1626 debug!(
1627 page_number,
1628 frame_index,
1629 wal_checkpoint_seq = snapshot.generation.checkpoint_seq,
1630 wal_salt1 = snapshot.generation.salts.salt1,
1631 wal_salt2 = snapshot.generation.salts.salt2,
1632 publication_seq = snapshot.publication_seq,
1633 snapshot_age,
1634 lookup_mode = resolution.lookup_mode(),
1635 fallback_reason = resolution.fallback_reason(),
1636 "WAL adapter: resolved page from current WAL generation"
1637 );
1638 Ok(Some(frame_buf))
1639 })
1640 }
1641
1642 fn read_page_pinned<'a>(
1644 &'a self,
1645 cx: &'a Cx,
1646 page_number: u32,
1647 ) -> WalFuture<'a, Option<Vec<u8>>> {
1648 Box::pin(async move {
1649 let snapshot = self.read_snapshot.as_ref().ok_or_else(|| {
1650 FrankenError::internal(
1651 "read_page_pinned called without a pinned read snapshot; \
1652 use read_page(&mut self) or call begin_transaction first",
1653 )
1654 })?;
1655 if snapshot.last_commit_frame.is_none() {
1656 return Ok(None);
1657 }
1658
1659 let resolution = self
1660 .resolve_visible_frame(cx, snapshot, page_number)
1661 .await?;
1662 let Some(frame_index) = resolution.frame_index() else {
1663 return Ok(None);
1664 };
1665
1666 let mut frame_buf = vec![0u8; self.wal.frame_size()];
1667 let header = self
1668 .wal
1669 .read_frame_into(cx, frame_index, &mut frame_buf)
1670 .await?;
1671
1672 if header.page_number != page_number {
1673 return Err(FrankenError::WalCorrupt {
1674 detail: format!(
1675 "WAL page index integrity failure: expected page {page_number} \
1676 at frame {frame_index}, found page {}",
1677 header.page_number
1678 ),
1679 });
1680 }
1681
1682 let header_size = fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE;
1694 let page_size = self.wal.page_size();
1695 frame_buf.copy_within(header_size.., 0);
1696 frame_buf.truncate(page_size);
1697 Ok(Some(frame_buf))
1698 })
1699 }
1700
1701 fn supports_pinned_reads(&self) -> bool {
1702 self.read_snapshot.is_some()
1703 }
1704
1705 fn committed_txns_since_page<'a>(
1706 &'a mut self,
1707 cx: &'a Cx,
1708 page_number: u32,
1709 ) -> WalFuture<'a, u64> {
1710 Box::pin(async move {
1711 let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1712 snapshot
1713 } else {
1714 self.publish_latest_committed_snapshot(cx, "committed_txns_since_page")
1715 .await?;
1716 self.published_snapshot.clone()
1717 };
1718 let Some(last_commit_frame) = snapshot.last_commit_frame else {
1719 return Ok(0);
1720 };
1721
1722 let resolution = self
1723 .resolve_visible_frame(cx, &snapshot, page_number)
1724 .await?;
1725 let Some(last_page_frame) = resolution.frame_index() else {
1726 let mut total_commits = 0_u64;
1727 for frame_index in 0..=last_commit_frame {
1728 if self
1729 .wal
1730 .read_frame_header(cx, frame_index)
1731 .await?
1732 .is_commit()
1733 {
1734 total_commits = total_commits.saturating_add(1);
1735 }
1736 }
1737 return Ok(total_commits);
1738 };
1739
1740 let mut page_commit_frame = None;
1741 for frame_index in last_page_frame..=last_commit_frame {
1742 if self
1743 .wal
1744 .read_frame_header(cx, frame_index)
1745 .await?
1746 .is_commit()
1747 {
1748 page_commit_frame = Some(frame_index);
1749 break;
1750 }
1751 }
1752
1753 let Some(page_commit_frame) = page_commit_frame else {
1754 return Ok(0);
1755 };
1756
1757 let mut committed_txns_after_page = 0_u64;
1758 for frame_index in page_commit_frame.saturating_add(1)..=last_commit_frame {
1759 if self
1760 .wal
1761 .read_frame_header(cx, frame_index)
1762 .await?
1763 .is_commit()
1764 {
1765 committed_txns_after_page = committed_txns_after_page.saturating_add(1);
1766 }
1767 }
1768
1769 Ok(committed_txns_after_page)
1770 })
1771 }
1772
1773 fn conflicting_pages_since_snapshot<'a>(
1774 &'a mut self,
1775 cx: &'a Cx,
1776 snapshot: TransactionConflictSnapshot,
1777 page_numbers: &'a [u32],
1778 _page_baselines: &'a [TransactionConflictPageBaseline],
1779 ) -> WalFuture<'a, Vec<u32>> {
1780 Box::pin(async move {
1781 if page_numbers.is_empty() {
1782 return Ok(Vec::new());
1783 }
1784
1785 let mut candidates = page_numbers
1786 .iter()
1787 .copied()
1788 .filter(|page| *page != 0)
1789 .collect::<Vec<_>>();
1790 candidates.sort_unstable();
1791 candidates.dedup();
1792 if candidates.is_empty() {
1793 return Ok(Vec::new());
1794 }
1795
1796 self.wal.refresh(cx).await?;
1797 self.publish_latest_committed_snapshot(cx, "conflicting_pages_since_snapshot")
1798 .await?;
1799 let latest = self.published_snapshot();
1800 if latest.commit_count <= snapshot.commit_count
1801 && latest.generation == snapshot.generation
1802 && latest.last_commit_frame <= snapshot.last_commit_frame
1803 {
1804 return Ok(Vec::new());
1805 }
1806
1807 if latest.generation != snapshot.generation {
1808 return Ok(candidates);
1809 }
1810
1811 let Some(latest_last_commit_frame) = latest.last_commit_frame else {
1812 return Ok(Vec::new());
1813 };
1814 let start_frame = snapshot
1815 .last_commit_frame
1816 .map_or(0, |frame| frame.saturating_add(1));
1817 if start_frame > latest_last_commit_frame {
1818 return Ok(Vec::new());
1819 }
1820
1821 let candidate_set = candidates.iter().copied().collect::<HashSet<_>>();
1822 let mut conflicts = HashSet::<u32>::new();
1823 for frame_index in start_frame..=latest_last_commit_frame {
1824 let header = self.wal.read_frame_header(cx, frame_index).await?;
1825 if candidate_set.contains(&header.page_number) {
1826 conflicts.insert(header.page_number);
1827 }
1828 }
1829
1830 let mut conflicts = conflicts.into_iter().collect::<Vec<_>>();
1831 conflicts.sort_unstable();
1832 Ok(conflicts)
1833 })
1834 }
1835
1836 fn committed_txn_count<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, u64> {
1837 Box::pin(async move {
1838 let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1839 snapshot
1840 } else {
1841 self.publish_latest_committed_snapshot(cx, "committed_txn_count")
1842 .await?;
1843 self.published_snapshot.clone()
1844 };
1845 Ok(snapshot.commit_count)
1846 })
1847 }
1848
1849 fn sync(&mut self, cx: &Cx) -> Result<()> {
1850 self.wal.sync(cx, SyncFlags::NORMAL)?;
1860 self.publish_pending_after_sync(cx)?;
1861 if !self.has_pending_publication() {
1871 self.refresh_before_append = true;
1872 }
1873 Ok(())
1874 }
1875
1876 fn frame_count(&self) -> usize {
1877 self.wal.frame_count()
1878 }
1879
1880 fn checkpoint<'a>(
1881 &'a mut self,
1882 cx: &'a Cx,
1883 mode: CheckpointMode,
1884 writer: &'a mut dyn CheckpointPageWriter,
1885 backfilled_frames: u32,
1886 oldest_reader_frame: Option<u32>,
1887 ) -> WalFuture<'a, CheckpointResult> {
1888 Box::pin(async move {
1889 if self.has_pending_publication() {
1896 return Err(FrankenError::CheckpointFailed {
1897 detail: "staged, unpublished frames remain; a successful commit sync must \
1898 drain them before checkpointing"
1899 .to_owned(),
1900 });
1901 }
1902 self.wal.refresh(cx).await?;
1904 self.refresh_before_append = true;
1905 let total_frames = u32::try_from(self.wal.frame_count()).unwrap_or(u32::MAX);
1906
1907 let state = CheckpointState {
1909 total_frames,
1910 backfilled_frames,
1911 oldest_reader_frame,
1912 };
1913
1914 let mut target = CheckpointTargetAdapterRef { writer };
1916
1917 let result =
1919 execute_checkpoint(cx, &mut self.wal, to_wal_mode(mode), state, &mut target)
1920 .await?;
1921
1922 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1926 if result.frames_backfilled > 0 {
1927 let drained = self.fec_pending.len();
1928 self.fec_pending.clear();
1929 if drained > 0 {
1930 debug!(
1931 drained_groups = drained,
1932 frames_backfilled = result.frames_backfilled,
1933 "FEC symbols reclaimed after checkpoint"
1934 );
1935 }
1936 }
1937
1938 #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1941 if result.wal_was_reset {
1942 self.fec_discard();
1943 }
1944 if result.wal_was_reset {
1945 self.invalidate_publication();
1946 }
1947
1948 self.publish_latest_committed_snapshot(cx, "checkpoint")
1949 .await?;
1950
1951 Ok(CheckpointResult {
1952 total_frames,
1953 frames_backfilled: result.frames_backfilled,
1954 completed: result.plan.completes_checkpoint(),
1955 wal_was_reset: result.wal_was_reset,
1956 requested_mode: mode,
1957 effective_mode: mode,
1958 })
1959 })
1960 }
1961}
1962
1963const MIN_DURABLE_CERTIFICATE_RECORD_SIZE: usize =
1964 ParallelWalDurableCertificateRecord::MIN_ENCODED_SIZE;
1965const DURABLE_CERTIFICATE_RECORD_HEADER_SIZE: usize = 14;
1966const MAX_ORPHAN_CERTIFICATE_LOOKBACK: usize = 64;
1967
1968fn durable_certificate_declared_len(bytes: &[u8]) -> Option<usize> {
1969 let length_bytes = bytes.get(10..DURABLE_CERTIFICATE_RECORD_HEADER_SIZE)?;
1970 usize::try_from(u32::from_le_bytes([
1971 length_bytes[0],
1972 length_bytes[1],
1973 length_bytes[2],
1974 length_bytes[3],
1975 ]))
1976 .ok()
1977}
1978
1979fn durable_certificate_declares_len(bytes: &[u8], expected: usize) -> bool {
1980 durable_certificate_declared_len(bytes).is_some_and(|actual| actual.cmp(&expected).is_eq())
1981}
1982
1983fn decode_durable_certificate_record(
1984 bytes: &[u8],
1985 location: &str,
1986) -> Result<ParallelWalDurableCertificateRecord> {
1987 ParallelWalDurableCertificateRecord::from_bytes(bytes).map_err(|error| {
1988 FrankenError::WalCorrupt {
1989 detail: format!("parallel WAL certificate {location} is invalid: {error}"),
1990 }
1991 })
1992}
1993
1994fn validate_incomplete_certificate_suffix(bytes: &[u8], anchored: bool) -> Result<()> {
1995 if bytes.is_empty() {
1996 return Ok(());
1997 }
1998 if bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
1999 return Err(FrankenError::WalCorrupt {
2000 detail: format!(
2001 "parallel WAL certificate torn suffix exceeds {} bytes",
2002 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2003 ),
2004 });
2005 }
2006
2007 if bytes.len() < PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.len() {
2008 if anchored || PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.starts_with(bytes) {
2013 return Ok(());
2014 }
2015 return Err(FrankenError::WalCorrupt {
2016 detail: "parallel WAL certificate sidecar starts with non-record garbage".to_owned(),
2017 });
2018 }
2019 if !bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC) {
2020 return Err(FrankenError::WalCorrupt {
2021 detail: "parallel WAL certificate suffix does not start at a record boundary"
2022 .to_owned(),
2023 });
2024 }
2025 if bytes.len() < 10 {
2026 return Ok(());
2027 }
2028 let version = u16::from_le_bytes([bytes[8], bytes[9]]);
2029 if version != fsqlite_wal::PARALLEL_WAL_DURABLE_CERTIFICATE_RECORD_VERSION {
2030 return Err(FrankenError::WalCorrupt {
2031 detail: format!(
2032 "parallel WAL certificate suffix has unsupported record version {version}"
2033 ),
2034 });
2035 }
2036 if bytes.len() < DURABLE_CERTIFICATE_RECORD_HEADER_SIZE {
2037 return Ok(());
2038 }
2039 let declared_len =
2040 durable_certificate_declared_len(bytes).ok_or_else(|| FrankenError::WalCorrupt {
2041 detail: "parallel WAL certificate suffix length exceeds usize".to_owned(),
2042 })?;
2043 if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2044 .contains(&declared_len)
2045 {
2046 return Err(FrankenError::WalCorrupt {
2047 detail: format!(
2048 "parallel WAL certificate suffix declares invalid record length {declared_len}"
2049 ),
2050 });
2051 }
2052 if bytes.len() < declared_len {
2053 return Ok(());
2054 }
2055
2056 decode_durable_certificate_record(&bytes[..declared_len], "suffix")?;
2061 Err(FrankenError::WalCorrupt {
2062 detail:
2063 "parallel WAL certificate sidecar contains a complete record outside the footer chain"
2064 .to_owned(),
2065 })
2066}
2067
2068fn combine_sidecar_io_results<const N: usize>(
2069 context: &str,
2070 results: [(&str, Result<()>); N],
2071) -> Result<()> {
2072 let failures = results
2073 .into_iter()
2074 .filter_map(|(stage, result)| result.err().map(|error| (stage, error)))
2075 .collect::<Vec<_>>();
2076 if failures.is_empty() {
2077 return Ok(());
2078 }
2079 if failures.len() == 1 {
2080 return failures
2081 .into_iter()
2082 .next()
2083 .map_or(Ok(()), |(_, error)| Err(error));
2084 }
2085 let details = failures
2086 .iter()
2087 .map(|(stage, error)| format!("{stage}={error}"))
2088 .collect::<Vec<_>>()
2089 .join("; ");
2090 Err(FrankenError::internal(format!("{context}: {details}")))
2091}
2092
2093pub struct PathRefreshingWalBackend<V: Vfs>
2103where
2104 V::File: Send + Sync + 'static,
2105{
2106 vfs: V,
2107 db_path: PathBuf,
2108 wal_path: PathBuf,
2109 page_size: u32,
2110 create_missing: bool,
2111 #[cfg(all(feature = "native", any(unix, windows)))]
2112 namespace_binding: Option<Arc<DatabaseNamespaceBinding>>,
2113 inner: WalBackendAdapter<V::File>,
2114}
2115
2116impl<V> PathRefreshingWalBackend<V>
2117where
2118 V: Vfs + 'static,
2119 V::File: Send + Sync + 'static,
2120{
2121 #[must_use]
2122 pub fn new(
2123 vfs: V,
2124 db_path: impl AsRef<Path>,
2125 wal_path: impl AsRef<Path>,
2126 page_size: u32,
2127 wal: WalFile<V::File>,
2128 create_missing: bool,
2129 #[cfg(all(feature = "native", any(unix, windows)))] namespace_binding: Option<
2130 Arc<DatabaseNamespaceBinding>,
2131 >,
2132 ) -> Self {
2133 Self {
2134 vfs,
2135 db_path: db_path.as_ref().to_path_buf(),
2136 wal_path: wal_path.as_ref().to_path_buf(),
2137 page_size,
2138 create_missing,
2139 #[cfg(all(feature = "native", any(unix, windows)))]
2140 namespace_binding,
2141 inner: WalBackendAdapter::new(wal),
2142 }
2143 }
2144
2145 #[must_use]
2146 pub fn into_inner(self) -> WalBackendAdapter<V::File> {
2147 self.inner
2148 }
2149
2150 fn replace_inner(&mut self, cx: &Cx, wal: WalFile<V::File>) -> Result<()> {
2158 if self.inner.has_pending_publication() {
2159 let cleanup_cx = cx.create_child();
2160 let _cleanup_mask = cleanup_cx.masked();
2161 let _ = wal.close(&cleanup_cx);
2162 return Err(FrankenError::Busy);
2163 }
2164 let old = std::mem::replace(&mut self.inner, WalBackendAdapter::new(wal));
2165 let old_wal = old.into_inner()?;
2166 let _ = old_wal.close(cx);
2167 Ok(())
2168 }
2169
2170 async fn create_replacement_wal(&self, cx: &Cx) -> Result<WalFile<V::File>> {
2171 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2172 let (file, _) = self.vfs.open(cx, Some(&self.wal_path), flags)?;
2173 let wal = WalFile::create(cx, file, self.page_size, 0, WalSalts::generate()).await?;
2176 if let Err(error) = self.vfs.sync_parent_directory(cx, &self.wal_path) {
2177 let cleanup_cx = cx.create_child();
2178 let _cleanup_mask = cleanup_cx.masked();
2179 let _ = wal.close(&cleanup_cx);
2180 return Err(error);
2181 }
2182 Ok(wal)
2183 }
2184
2185 async fn replace_with_created_wal(&mut self, cx: &Cx) -> Result<()> {
2186 let wal = self.create_replacement_wal(cx).await?;
2187 self.replace_inner(cx, wal)
2188 }
2189
2190 async fn open_replacement_wal(&self, cx: &Cx, path_file: V::File) -> Result<WalFile<V::File>> {
2191 let wal = WalFile::open(cx, path_file).await?;
2192 if u32::try_from(wal.page_size()).ok() != Some(self.page_size) {
2193 let actual_page_size = wal.page_size();
2194 let expected_page_size = self.page_size;
2195 let _ = wal.close(cx);
2196 return Err(FrankenError::WalCorrupt {
2197 detail: format!(
2198 "WAL page size {actual_page_size} does not match database page size {expected_page_size} during path refresh"
2199 ),
2200 });
2201 }
2202 Ok(wal)
2203 }
2204
2205 async fn path_header_matches_current_handle(
2206 &self,
2207 cx: &Cx,
2208 path_file: &V::File,
2209 ) -> Result<bool> {
2210 let mut header_buf = [0_u8; WAL_HEADER_SIZE];
2211 let bytes_read = path_file.read(cx, &mut header_buf, 0).await?;
2212 if bytes_read < WAL_HEADER_SIZE {
2213 return Ok(false);
2214 }
2215
2216 let path_header = WalHeader::from_bytes(&header_buf)?;
2217 if !validate_wal_header_checksum(&header_buf, path_header.big_endian_checksum())? {
2218 return Err(FrankenError::WalCorrupt {
2219 detail: "WAL header checksum mismatch during path refresh".to_owned(),
2220 });
2221 }
2222
2223 let current_header = self.inner.inner().header();
2224 Ok(path_header.magic == current_header.magic
2225 && path_header.format_version == current_header.format_version
2226 && path_header.page_size == current_header.page_size
2227 && path_header.checkpoint_seq == current_header.checkpoint_seq
2228 && path_header.salts == current_header.salts)
2229 }
2230
2231 async fn conflicts_after_generation_change(
2247 &mut self,
2248 cx: &Cx,
2249 page_numbers: &[u32],
2250 page_baselines: &[TransactionConflictPageBaseline],
2251 ) -> Vec<u32> {
2252 let mut candidates = page_numbers
2253 .iter()
2254 .copied()
2255 .filter(|page| *page != 0)
2256 .collect::<Vec<_>>();
2257 candidates.sort_unstable();
2258 candidates.dedup();
2259 if candidates.is_empty() {
2260 return Vec::new();
2261 }
2262
2263 let mut baselines = HashMap::<u32, [u8; 32]>::new();
2264 let mut ambiguous_baselines = HashSet::<u32>::new();
2265 for baseline in page_baselines {
2266 if baseline.page_number == 0 {
2267 continue;
2268 }
2269 if let Some(previous) = baselines.insert(baseline.page_number, baseline.page_hash)
2270 && previous != baseline.page_hash
2271 {
2272 ambiguous_baselines.insert(baseline.page_number);
2273 }
2274 }
2275
2276 let main_db_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
2277 let (mut db_file, _) = match self.vfs.open(cx, Some(&self.db_path), main_db_flags) {
2278 Ok(opened) => opened,
2279 Err(_) => return candidates,
2280 };
2281 let page_size = match usize::try_from(self.page_size) {
2282 Ok(page_size) if page_size > 0 => page_size,
2283 _ => {
2284 let _ = db_file.close(cx);
2285 return candidates;
2286 }
2287 };
2288
2289 let mut page_one = vec![0_u8; page_size];
2292 let page_one_read = match db_file.read(cx, &mut page_one, 0).await {
2293 Ok(bytes_read) => bytes_read,
2294 Err(_) => {
2295 let _ = db_file.close(cx);
2296 return candidates;
2297 }
2298 };
2299 let header_page_size =
2300 (page_one_read == page_size).then(|| sqlite_database_header_page_size(&page_one));
2301 if header_page_size.flatten() != Some(self.page_size) {
2302 let _ = db_file.close(cx);
2303 return candidates;
2304 }
2305
2306 let mut conflicts = Vec::new();
2307 for &page_number in &candidates {
2308 let Some(expected_hash) = baselines.get(&page_number).copied() else {
2309 conflicts.push(page_number);
2310 continue;
2311 };
2312 if ambiguous_baselines.contains(&page_number) {
2313 conflicts.push(page_number);
2314 continue;
2315 }
2316
2317 let current_page = match self.inner.read_page(cx, page_number).await {
2318 Ok(Some(page)) if page.len() == page_size => page,
2319 Ok(Some(_)) | Err(_) => {
2320 conflicts.push(page_number);
2321 continue;
2322 }
2323 Ok(None) => {
2324 let mut page = vec![0_u8; page_size];
2325 let page_offset = u64::from(page_number.saturating_sub(1))
2326 .saturating_mul(u64::from(self.page_size));
2327 match db_file.read(cx, &mut page, page_offset).await {
2328 Ok(bytes_read) if bytes_read == page_size => page,
2329 Ok(_) | Err(_) => {
2330 conflicts.push(page_number);
2331 continue;
2332 }
2333 }
2334 }
2335 };
2336 let current_hash = *blake3::hash(¤t_page).as_bytes();
2337 if current_hash != expected_hash {
2338 conflicts.push(page_number);
2339 }
2340 }
2341
2342 if db_file.close(cx).is_err() {
2343 return candidates;
2344 }
2345 conflicts.sort_unstable();
2346 conflicts.dedup();
2347 conflicts
2348 }
2349
2350 async fn ensure_current_wal_path(&mut self, cx: &Cx) -> Result<()> {
2351 #[cfg(all(feature = "native", any(unix, windows)))]
2352 if let Some(binding) = &self.namespace_binding {
2353 binding.validate_path_identity()?;
2354 }
2355 if !self.vfs.access(cx, &self.wal_path, AccessFlags::EXISTS)? {
2356 if self.create_missing {
2357 return self.replace_with_created_wal(cx).await;
2358 }
2359 return Ok(());
2360 }
2361
2362 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::WAL;
2363 let (mut path_file, _) = self.vfs.open(cx, Some(&self.wal_path), flags)?;
2364 let path_size = path_file.file_size(cx)?;
2365 if path_size < u64::try_from(WAL_HEADER_SIZE).unwrap_or(32) {
2366 let _ = path_file.close(cx);
2367 if self.create_missing {
2368 return self.replace_with_created_wal(cx).await;
2369 }
2370 return Ok(());
2371 }
2372
2373 let current_size = self.inner.inner().file().file_size(cx).unwrap_or(u64::MAX);
2374 let path_matches_current = if path_size == current_size {
2375 match self
2376 .path_header_matches_current_handle(cx, &path_file)
2377 .await
2378 {
2379 Ok(matches) => matches,
2380 Err(err) => {
2381 let _ = path_file.close(cx);
2382 return Err(err);
2383 }
2384 }
2385 } else {
2386 false
2387 };
2388 if !path_matches_current {
2389 let wal = self.open_replacement_wal(cx, path_file).await?;
2390 self.replace_inner(cx, wal)?;
2391 } else {
2392 let _ = path_file.close(cx);
2393 }
2394 Ok(())
2395 }
2396
2397 fn certificate_sidecar_path(&self) -> PathBuf {
2398 let mut path = self.wal_path.as_os_str().to_owned();
2399 path.push("-cert");
2400 PathBuf::from(path)
2401 }
2402
2403 fn certificate_checkpoint_handoff_path(&self) -> PathBuf {
2404 let mut path = self.wal_path.as_os_str().to_owned();
2405 path.push("-cert-head");
2406 PathBuf::from(path)
2407 }
2408
2409 async fn read_certificate_sidecar_exact(
2410 file: &V::File,
2411 cx: &Cx,
2412 offset: u64,
2413 len: usize,
2414 location: &str,
2415 ) -> Result<Vec<u8>> {
2416 let mut bytes = vec![0_u8; len];
2417 let bytes_read = file.read(cx, &mut bytes, offset).await?;
2418 if bytes_read != len {
2419 return Err(FrankenError::WalCorrupt {
2420 detail: format!(
2421 "parallel WAL certificate {location} at offset {offset} was short-read: got {bytes_read} of {len}"
2422 ),
2423 });
2424 }
2425 Ok(bytes)
2426 }
2427
2428 async fn read_certificate_record_ending_at(
2429 file: &V::File,
2430 cx: &Cx,
2431 record_end: u64,
2432 ) -> Result<(u64, ParallelWalDurableCertificateRecord)> {
2433 let footer_size =
2434 u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE).unwrap_or(4);
2435 let footer_offset = record_end.checked_sub(footer_size).ok_or_else(|| {
2436 FrankenError::WalCorrupt {
2437 detail: format!(
2438 "parallel WAL certificate record ending at {record_end} has no length footer"
2439 ),
2440 }
2441 })?;
2442 let footer = Self::read_certificate_sidecar_exact(
2443 file,
2444 cx,
2445 footer_offset,
2446 ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
2447 "length footer",
2448 )
2449 .await?;
2450 let record_len = usize::try_from(u32::from_le_bytes([
2451 footer[0], footer[1], footer[2], footer[3],
2452 ]))
2453 .map_err(|_| FrankenError::WalCorrupt {
2454 detail: "parallel WAL certificate footer length exceeds usize".to_owned(),
2455 })?;
2456 if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2457 .contains(&record_len)
2458 {
2459 return Err(FrankenError::WalCorrupt {
2460 detail: format!(
2461 "parallel WAL certificate footer declares invalid record length {record_len}"
2462 ),
2463 });
2464 }
2465 let record_len_u64 = u64::try_from(record_len).map_err(|_| FrankenError::WalCorrupt {
2466 detail: "parallel WAL certificate record length exceeds u64".to_owned(),
2467 })?;
2468 let record_start =
2469 record_end
2470 .checked_sub(record_len_u64)
2471 .ok_or_else(|| FrankenError::WalCorrupt {
2472 detail: format!(
2473 "parallel WAL certificate record length {record_len} exceeds end offset {record_end}"
2474 ),
2475 })?;
2476 let bytes =
2477 Self::read_certificate_sidecar_exact(file, cx, record_start, record_len, "record")
2478 .await?;
2479 let record = decode_durable_certificate_record(&bytes, "record")?;
2480 Ok((record_start, record))
2481 }
2482
2483 async fn prepare_certificate_sidecar_for_append(file: &mut V::File, cx: &Cx) -> Result<u64> {
2490 let file_size = file.file_size(cx)?;
2491 if file_size == 0 {
2492 return Ok(0);
2493 }
2494
2495 let footer_size =
2496 u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE).unwrap_or(4);
2497 if file_size >= footer_size {
2498 let footer = Self::read_certificate_sidecar_exact(
2499 file,
2500 cx,
2501 file_size - footer_size,
2502 ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
2503 "append-boundary length footer",
2504 )
2505 .await?;
2506 let record_len = usize::try_from(u32::from_le_bytes([
2507 footer[0], footer[1], footer[2], footer[3],
2508 ]))
2509 .unwrap_or(usize::MAX);
2510 let record_len_u64 = u64::try_from(record_len).unwrap_or(u64::MAX);
2511 if (MIN_DURABLE_CERTIFICATE_RECORD_SIZE
2512 ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2513 .contains(&record_len)
2514 && record_len_u64 <= file_size
2515 {
2516 let record_start = file_size - record_len_u64;
2517 let bytes = Self::read_certificate_sidecar_exact(
2518 file,
2519 cx,
2520 record_start,
2521 record_len,
2522 "append-boundary record",
2523 )
2524 .await?;
2525 if bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
2526 || durable_certificate_declares_len(&bytes, record_len)
2527 {
2528 decode_durable_certificate_record(&bytes, "append-boundary record")?;
2529 return Ok(file_size);
2530 }
2531 }
2532 }
2533
2534 let recovery_window_size =
2538 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE.saturating_mul(2);
2539 let recovery_window_size_u64 = u64::try_from(recovery_window_size).unwrap_or(u64::MAX);
2540 let tail_offset = file_size.saturating_sub(recovery_window_size_u64);
2541 let tail_len =
2542 usize::try_from(file_size - tail_offset).map_err(|_| FrankenError::WalCorrupt {
2543 detail: "parallel WAL certificate append-repair window exceeds usize".to_owned(),
2544 })?;
2545 let tail = Self::read_certificate_sidecar_exact(
2546 file,
2547 cx,
2548 tail_offset,
2549 tail_len,
2550 "append-repair window",
2551 )
2552 .await?;
2553 let minimum_candidate_end = tail
2554 .len()
2555 .saturating_sub(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2556 .max(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE);
2557 let mut anchor_end = None;
2558 for candidate_end in (minimum_candidate_end..tail.len()).rev() {
2559 let footer_start =
2560 candidate_end - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
2561 let footer = &tail[footer_start..candidate_end];
2562 let record_len = usize::try_from(u32::from_le_bytes([
2563 footer[0], footer[1], footer[2], footer[3],
2564 ]))
2565 .unwrap_or(usize::MAX);
2566 if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE
2567 ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2568 .contains(&record_len)
2569 || record_len > candidate_end
2570 {
2571 continue;
2572 }
2573 let record_start = candidate_end - record_len;
2574 let record_bytes = &tail[record_start..candidate_end];
2575 if !record_bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
2576 || !durable_certificate_declares_len(record_bytes, record_len)
2577 {
2578 continue;
2579 }
2580 if ParallelWalDurableCertificateRecord::from_bytes(record_bytes).is_ok() {
2581 anchor_end = Some(candidate_end);
2582 break;
2583 }
2584 }
2585
2586 let safe_end = if let Some(anchor_end) = anchor_end {
2587 validate_incomplete_certificate_suffix(&tail[anchor_end..], true)?;
2588 tail_offset
2589 .checked_add(u64::try_from(anchor_end).unwrap_or(u64::MAX))
2590 .ok_or_else(|| FrankenError::WalCorrupt {
2591 detail: "parallel WAL certificate append-repair boundary overflow".to_owned(),
2592 })?
2593 } else {
2594 if file_size
2595 > u64::try_from(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2596 .unwrap_or(u64::MAX)
2597 {
2598 return Err(FrankenError::WalCorrupt {
2599 detail: format!(
2600 "parallel WAL certificate sidecar has no valid append boundary within its bounded {}-byte recovery suffix",
2601 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2602 ),
2603 });
2604 }
2605 validate_incomplete_certificate_suffix(&tail, false)?;
2606 0
2607 };
2608
2609 if safe_end < file_size {
2610 file.truncate(cx, safe_end)?;
2611 }
2612 Ok(safe_end)
2613 }
2614
2615 async fn append_durable_certificate_record(
2616 &self,
2617 cx: &Cx,
2618 certificate: &ParallelWalCommitCertificate,
2619 wal_frame_start: u64,
2620 wal_frame_end: u64,
2621 sync: bool,
2622 ) -> Result<()> {
2623 self.append_durable_certificate_record_with_completion(
2624 cx,
2625 certificate,
2626 wal_frame_start,
2627 wal_frame_end,
2628 sync,
2629 None,
2630 )
2631 .await
2632 }
2633
2634 async fn append_durable_certificate_record_with_completion(
2635 &self,
2636 cx: &Cx,
2637 certificate: &ParallelWalCommitCertificate,
2638 wal_frame_start: u64,
2639 wal_frame_end: u64,
2640 sync: bool,
2641 completion: Option<&VfsWriteCompletion>,
2642 ) -> Result<()> {
2643 let mut preflight = WalWriteCompletionPreflight::new(completion);
2644 let expected_frame_start = u64::try_from(self.inner.frame_count())
2645 .unwrap_or(u64::MAX)
2646 .saturating_add(1);
2647 if wal_frame_start != expected_frame_start {
2648 return Err(FrankenError::internal(format!(
2649 "parallel WAL certificate starts at frame {wal_frame_start}, expected {expected_frame_start}"
2650 )));
2651 }
2652 let record = ParallelWalDurableCertificateRecord::new(
2653 self.inner.inner().generation_identity(),
2654 wal_frame_start,
2655 wal_frame_end,
2656 certificate.clone(),
2657 )
2658 .map_err(|error| {
2659 FrankenError::internal(format!(
2660 "could not encode parallel WAL durability certificate: {error}"
2661 ))
2662 })?;
2663 let record_bytes = record.to_bytes();
2664 if record_bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2665 return Err(FrankenError::WalCorrupt {
2666 detail: format!(
2667 "parallel WAL certificate record is {} bytes; maximum is {}",
2668 record_bytes.len(),
2669 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2670 ),
2671 });
2672 }
2673 let certificate_path = self.certificate_sidecar_path();
2674 let existed = self
2675 .vfs
2676 .access(cx, &certificate_path, AccessFlags::EXISTS)?;
2677 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2678 let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2679 let append_offset = Self::prepare_certificate_sidecar_for_append(&mut file, cx).await?;
2680 preflight.hand_off();
2681 drop(preflight);
2682 let write_result = if let Some(completion) = completion {
2683 file.write_tracked(cx, &record_bytes, append_offset, completion.clone())
2684 .await
2685 } else {
2686 file.write(cx, &record_bytes, append_offset).await
2687 };
2688 if let Err(write_error) = write_result {
2689 let cleanup_cx = cx.create_child();
2694 let _cleanup_mask = cleanup_cx.masked();
2695 let cleanup_result = file.truncate(&cleanup_cx, append_offset);
2696 let close_result = file.close(&cleanup_cx);
2697 return combine_sidecar_io_results(
2698 "parallel WAL certificate append cleanup failed",
2699 [
2700 ("write", Err(write_error)),
2701 ("truncate", cleanup_result),
2702 ("close", close_result),
2703 ],
2704 );
2705 }
2706
2707 let finalization_cx = cx.create_child();
2711 let _finalization_mask = finalization_cx.masked();
2712 let sync_result = if sync {
2713 file.durable_sync(&finalization_cx, SyncKind::FullDurable)
2714 } else {
2715 Ok(())
2716 };
2717 let directory_sync_result = if sync && !existed && sync_result.is_ok() {
2718 self.vfs
2719 .sync_parent_directory(&finalization_cx, &certificate_path)
2720 } else {
2721 Ok(())
2722 };
2723 let close_result = file.close(&finalization_cx);
2724 combine_sidecar_io_results(
2725 "parallel WAL certificate append finalization failed",
2726 [
2727 ("file_sync", sync_result),
2728 ("directory_sync", directory_sync_result),
2729 ("close", close_result),
2730 ],
2731 )
2732 }
2733
2734 async fn reconcile_certificate_sidecar_record(
2735 &self,
2736 cx: &Cx,
2737 expected: &ParallelWalDurableCertificateRecord,
2738 remove_expected_orphan: bool,
2739 sync: bool,
2740 ) -> Result<bool> {
2741 let certificate_path = self.certificate_sidecar_path();
2742 if !self
2743 .vfs
2744 .access(cx, &certificate_path, AccessFlags::EXISTS)?
2745 {
2746 return Ok(false);
2747 }
2748
2749 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::WAL;
2750 let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2751 let reconciliation_result = async {
2752 let original_size = file.file_size(cx)?;
2753 let safe_end = Self::prepare_certificate_sidecar_for_append(&mut file, cx).await?;
2754 let latest = if safe_end == 0 {
2755 None
2756 } else {
2757 Some(Self::read_certificate_record_ending_at(&file, cx, safe_end).await?)
2758 };
2759 let latest_is_expected = latest
2760 .as_ref()
2761 .is_some_and(|(_, record)| record == expected);
2762 let sidecar_changed = if remove_expected_orphan
2763 && let Some((record_start, _)) = latest.as_ref()
2764 && latest_is_expected
2765 {
2766 file.truncate(cx, *record_start)?;
2767 true
2768 } else {
2769 safe_end != original_size
2770 };
2771 if sync && (latest_is_expected || sidecar_changed) {
2772 file.durable_sync(cx, SyncKind::FullDurable)?;
2773 }
2774 if sync && latest_is_expected && !remove_expected_orphan {
2775 self.vfs.sync_parent_directory(cx, &certificate_path)?;
2778 }
2779 Ok(latest_is_expected)
2780 }
2781 .await;
2782
2783 let cleanup_cx = cx.create_child();
2784 let _cleanup_mask = cleanup_cx.masked();
2785 let close_result = file.close(&cleanup_cx);
2786 match (reconciliation_result, close_result) {
2787 (Ok(latest_is_expected), Ok(())) => Ok(latest_is_expected),
2788 (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
2789 (Err(reconciliation_error), Err(close_error)) => Err(FrankenError::internal(format!(
2790 "parallel WAL certificate reconciliation failed and close also failed: reconciliation={reconciliation_error}; close={close_error}"
2791 ))),
2792 }
2793 }
2794
2795 async fn persist_checkpoint_certificate_handoff(
2796 &self,
2797 cx: &Cx,
2798 record: &ParallelWalDurableCertificateRecord,
2799 ) -> Result<()> {
2800 let handoff_path = self.certificate_checkpoint_handoff_path();
2801 let existed = self.vfs.access(cx, &handoff_path, AccessFlags::EXISTS)?;
2802 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2803 let (mut file, _) = self.vfs.open(cx, Some(&handoff_path), flags)?;
2804 let record_bytes = record.to_bytes();
2805 if record_bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2806 let cleanup_cx = cx.create_child();
2807 let _cleanup_mask = cleanup_cx.masked();
2808 let close_result = file.close(&cleanup_cx);
2809 return combine_sidecar_io_results(
2810 "parallel WAL checkpoint certificate handoff is oversized",
2811 [
2812 (
2813 "record_size",
2814 Err(FrankenError::WalCorrupt {
2815 detail: format!(
2816 "parallel WAL checkpoint certificate handoff is {} bytes; maximum is {}",
2817 record_bytes.len(),
2818 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2819 ),
2820 }),
2821 ),
2822 ("close", close_result),
2823 ],
2824 );
2825 }
2826 if existed {
2831 let file_size = file.file_size(cx)?;
2832 if file_size == u64::try_from(record_bytes.len()).unwrap_or(u64::MAX) {
2833 let mut current = vec![0_u8; record_bytes.len()];
2834 let unchanged = file
2835 .read(cx, &mut current, 0)
2836 .await
2837 .is_ok_and(|bytes_read| {
2838 bytes_read == record_bytes.len() && current == record_bytes
2839 });
2840 if unchanged {
2841 let cleanup_cx = cx.create_child();
2842 let _cleanup_mask = cleanup_cx.masked();
2843 return file.close(&cleanup_cx);
2844 }
2845 }
2846 }
2847 let mutation_cx = cx.create_child();
2852 let _mutation_mask = mutation_cx.masked();
2853 let truncate_result = file.truncate(&mutation_cx, 0);
2854 let write_result = if truncate_result.is_ok() {
2855 file.write(&mutation_cx, &record_bytes, 0).await
2856 } else {
2857 Ok(())
2858 };
2859 if let Err(write_error) = write_result {
2860 let cleanup_result = file.truncate(&mutation_cx, 0);
2861 let close_result = file.close(&mutation_cx);
2862 return combine_sidecar_io_results(
2863 "parallel WAL checkpoint certificate handoff cleanup failed",
2864 [
2865 ("truncate_before_write", truncate_result),
2866 ("write", Err(write_error)),
2867 ("truncate_after_write", cleanup_result),
2868 ("close", close_result),
2869 ],
2870 );
2871 }
2872 let sync_result = if truncate_result.is_ok() {
2873 file.durable_sync(&mutation_cx, SyncKind::FullDurable)
2874 } else {
2875 Ok(())
2876 };
2877 let directory_sync_result = if !existed && truncate_result.is_ok() && sync_result.is_ok() {
2878 self.vfs.sync_parent_directory(&mutation_cx, &handoff_path)
2879 } else {
2880 Ok(())
2881 };
2882 let close_result = file.close(&mutation_cx);
2883 combine_sidecar_io_results(
2884 "parallel WAL checkpoint certificate handoff finalization failed",
2885 [
2886 ("truncate", truncate_result),
2887 ("file_sync", sync_result),
2888 ("directory_sync", directory_sync_result),
2889 ("close", close_result),
2890 ],
2891 )
2892 }
2893
2894 async fn checkpoint_certificate_handoff(
2895 &self,
2896 cx: &Cx,
2897 ) -> Result<Option<ParallelWalCommitCertificate>> {
2898 let handoff_path = self.certificate_checkpoint_handoff_path();
2899 if !self.vfs.access(cx, &handoff_path, AccessFlags::EXISTS)? {
2900 return Ok(None);
2901 }
2902 let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
2903 let (mut file, _) = self.vfs.open(cx, Some(&handoff_path), flags)?;
2904 let read_result = async {
2905 let file_size =
2906 usize::try_from(file.file_size(cx)?).map_err(|_| FrankenError::WalCorrupt {
2907 detail: "parallel WAL checkpoint certificate handoff exceeds usize".to_owned(),
2908 })?;
2909 if file_size == 0 || file_size > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2910 return Err(FrankenError::WalCorrupt {
2911 detail: format!(
2912 "parallel WAL checkpoint certificate handoff has invalid size {file_size}"
2913 ),
2914 });
2915 }
2916 let mut bytes = vec![0_u8; file_size];
2917 let bytes_read = file.read(cx, &mut bytes, 0).await?;
2918 if bytes_read != bytes.len() {
2919 return Err(FrankenError::WalCorrupt {
2920 detail: "parallel WAL checkpoint certificate handoff was short-read".to_owned(),
2921 });
2922 }
2923 let record =
2924 ParallelWalDurableCertificateRecord::from_bytes(&bytes).map_err(|error| {
2925 FrankenError::WalCorrupt {
2926 detail: format!(
2927 "parallel WAL checkpoint certificate handoff is invalid: {error}"
2928 ),
2929 }
2930 })?;
2931 Ok(Some(record.certificate))
2932 }
2933 .await;
2934 let cleanup_cx = cx.create_child();
2935 let _cleanup_mask = cleanup_cx.masked();
2936 let close_result = file.close(&cleanup_cx);
2937 match (read_result, close_result) {
2938 (Ok(certificate), Ok(())) => Ok(certificate),
2939 (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
2940 (Err(read_error), Err(close_error)) => Err(FrankenError::internal(format!(
2941 "parallel WAL checkpoint handoff read failed and close also failed: read={read_error}; close={close_error}"
2942 ))),
2943 }
2944 }
2945
2946 async fn wal_frame_payload_digest(
2947 &self,
2948 cx: &Cx,
2949 wal_frame_start: u64,
2950 wal_frame_end: u64,
2951 ) -> Result<[u8; 32]> {
2952 if wal_frame_start == 0 || wal_frame_end < wal_frame_start {
2953 return Err(FrankenError::WalCorrupt {
2954 detail: format!(
2955 "invalid parallel WAL digest interval {wal_frame_start}..={wal_frame_end}"
2956 ),
2957 });
2958 }
2959
2960 let mut digest = ParallelWalFramePayloadDigestBuilder::new();
2961 for frame_number in wal_frame_start..=wal_frame_end {
2962 let frame_index = usize::try_from(frame_number.saturating_sub(1)).map_err(|_| {
2963 FrankenError::WalCorrupt {
2964 detail: format!(
2965 "parallel WAL digest frame number {frame_number} exceeds usize"
2966 ),
2967 }
2968 })?;
2969 let (header, page_data) = self.inner.inner().read_frame(cx, frame_index).await?;
2970 let page_number =
2971 PageNumber::new(header.page_number).ok_or_else(|| FrankenError::WalCorrupt {
2972 detail: format!(
2973 "parallel WAL digest frame {frame_number} has invalid page number {}",
2974 header.page_number
2975 ),
2976 })?;
2977 digest.update(page_number, header.db_size, &page_data);
2978 }
2979 Ok(digest.finalize())
2980 }
2981
2982 async fn latest_authorized_durable_certificate_record(
2983 &self,
2984 cx: &Cx,
2985 ) -> Result<Option<ParallelWalDurableCertificateRecord>> {
2986 let certificate_path = self.certificate_sidecar_path();
2987 if !self
2988 .vfs
2989 .access(cx, &certificate_path, AccessFlags::EXISTS)?
2990 {
2991 return Ok(None);
2992 }
2993 let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
2994 let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2995 let read_result = async {
2996 let file_size = file.file_size(cx)?;
2997 if file_size == 0 {
2998 return Ok(None);
2999 }
3000
3001 let footer_size =
3004 u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE)
3005 .unwrap_or(4);
3006 let mut newest = None;
3007 if file_size >= footer_size {
3008 let footer_offset = file_size - footer_size;
3009 let footer = Self::read_certificate_sidecar_exact(
3010 &file,
3011 cx,
3012 footer_offset,
3013 ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
3014 "newest length footer",
3015 )
3016 .await?;
3017 let record_len = usize::try_from(u32::from_le_bytes([
3018 footer[0], footer[1], footer[2], footer[3],
3019 ]))
3020 .map_err(|_| FrankenError::WalCorrupt {
3021 detail: "parallel WAL certificate newest footer length exceeds usize"
3022 .to_owned(),
3023 })?;
3024 let record_len_u64 = u64::try_from(record_len).unwrap_or(u64::MAX);
3025 if (MIN_DURABLE_CERTIFICATE_RECORD_SIZE
3026 ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3027 .contains(&record_len)
3028 && record_len_u64 <= file_size
3029 {
3030 let record_start = file_size - record_len_u64;
3031 let bytes = Self::read_certificate_sidecar_exact(
3032 &file,
3033 cx,
3034 record_start,
3035 record_len,
3036 "newest record",
3037 )
3038 .await?;
3039 if bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
3044 || durable_certificate_declares_len(&bytes, record_len)
3045 {
3046 let record =
3047 decode_durable_certificate_record(&bytes, "newest record")?;
3048 newest = Some((record_start, record));
3049 }
3050 }
3051 }
3052
3053 if newest.is_none() {
3054 let recovery_window_size =
3061 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE.saturating_mul(2);
3062 let recovery_window_size_u64 =
3063 u64::try_from(recovery_window_size).unwrap_or(u64::MAX);
3064 let tail_offset = file_size.saturating_sub(recovery_window_size_u64);
3065 let tail_len =
3066 usize::try_from(file_size - tail_offset).map_err(|_| {
3067 FrankenError::WalCorrupt {
3068 detail: "parallel WAL certificate recovery window exceeds usize"
3069 .to_owned(),
3070 }
3071 })?;
3072 let tail = Self::read_certificate_sidecar_exact(
3073 &file,
3074 cx,
3075 tail_offset,
3076 tail_len,
3077 "recovery window",
3078 )
3079 .await?;
3080 let minimum_candidate_end = tail
3081 .len()
3082 .saturating_sub(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3083 .max(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE);
3084 let mut anchor = None;
3085 for candidate_end in (minimum_candidate_end..tail.len()).rev() {
3086 let footer_start = candidate_end
3087 - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
3088 let footer = &tail[footer_start..candidate_end];
3089 let record_len = usize::try_from(u32::from_le_bytes([
3090 footer[0], footer[1], footer[2], footer[3],
3091 ]))
3092 .unwrap_or(usize::MAX);
3093 if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE
3094 ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3095 .contains(&record_len)
3096 || record_len > candidate_end
3097 {
3098 continue;
3099 }
3100 let record_start = candidate_end - record_len;
3101 let record_bytes = &tail[record_start..candidate_end];
3102 if !record_bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
3103 || !durable_certificate_declares_len(record_bytes, record_len)
3104 {
3105 continue;
3106 }
3107 if let Ok(record) =
3108 ParallelWalDurableCertificateRecord::from_bytes(record_bytes)
3109 {
3110 anchor = Some((record_start, candidate_end, record));
3111 break;
3112 }
3113 }
3114
3115 if let Some((record_start, record_end, record)) = anchor {
3116 validate_incomplete_certificate_suffix(&tail[record_end..], true)?;
3117 let absolute_start = tail_offset
3118 .checked_add(u64::try_from(record_start).unwrap_or(u64::MAX))
3119 .ok_or_else(|| FrankenError::WalCorrupt {
3120 detail:
3121 "parallel WAL certificate recovery anchor offset overflow"
3122 .to_owned(),
3123 })?;
3124 newest = Some((absolute_start, record));
3125 } else {
3126 if file_size
3127 > u64::try_from(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3128 .unwrap_or(u64::MAX)
3129 {
3130 return Err(FrankenError::WalCorrupt {
3131 detail: format!(
3132 "parallel WAL certificate sidecar has no valid record within its bounded {}-byte recovery suffix",
3133 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
3134 ),
3135 });
3136 }
3137 validate_incomplete_certificate_suffix(&tail, false)?;
3138 return Ok(None);
3139 }
3140 }
3141
3142 let valid_frame_count = u64::try_from(self.inner.frame_count()).unwrap_or(u64::MAX);
3143 let wal_generation = self.inner.inner().generation_identity();
3144 let (mut record_start, mut record) = newest.ok_or_else(|| {
3145 FrankenError::WalCorrupt {
3146 detail: "parallel WAL certificate recovery produced no record".to_owned(),
3147 }
3148 })?;
3149 let mut unauthorized_records = 0_usize;
3150 loop {
3151 if record.wal_generation != wal_generation {
3156 return Ok(None);
3157 }
3158 let frame_index =
3159 usize::try_from(record.wal_frame_end.saturating_sub(1)).map_err(|_| {
3160 FrankenError::WalCorrupt {
3161 detail: "parallel WAL certificate commit-marker index exceeds usize"
3162 .to_owned(),
3163 }
3164 })?;
3165 let commit_marker_frame = if frame_index < self.inner.frame_count()
3166 && self
3167 .inner
3168 .inner()
3169 .read_frame_header(cx, frame_index)
3170 .await?
3171 .is_commit()
3172 {
3173 record.wal_frame_end
3174 } else {
3175 0
3176 };
3177 let actual_wal_frame_payload_digest =
3178 if commit_marker_frame == record.wal_frame_end {
3179 Some(
3180 self.wal_frame_payload_digest(
3181 cx,
3182 record.wal_frame_start,
3183 record.wal_frame_end,
3184 )
3185 .await?,
3186 )
3187 } else {
3188 None
3189 };
3190 if actual_wal_frame_payload_digest.is_some_and(|actual_digest| {
3191 record.authorizes_wal_boundary(
3192 wal_generation,
3193 valid_frame_count,
3194 commit_marker_frame,
3195 actual_digest,
3196 )
3197 }) {
3198 return Ok(Some(record));
3199 }
3200
3201 unauthorized_records = unauthorized_records.saturating_add(1);
3202 if unauthorized_records > MAX_ORPHAN_CERTIFICATE_LOOKBACK {
3203 return Err(FrankenError::WalCorrupt {
3204 detail: format!(
3205 "parallel WAL certificate sidecar exceeded bounded orphan lookback {MAX_ORPHAN_CERTIFICATE_LOOKBACK}"
3206 ),
3207 });
3208 }
3209 tracing::debug!(
3210 target: "fsqlite::wal::durability_combiner",
3211 orphan_certificate_epoch = record.certificate.certificate_epoch,
3212 orphan_commit_seq_hi = record.certificate.commit_seq_hi.get(),
3213 orphan_wal_frame_end = record.wal_frame_end,
3214 lookback = unauthorized_records,
3215 "ignored unauthorized parallel WAL certificate tail"
3216 );
3217 if record_start == 0 {
3218 return Ok(None);
3219 }
3220 (record_start, record) =
3221 Self::read_certificate_record_ending_at(&file, cx, record_start).await?;
3222 }
3223 }
3224 .await;
3225 let cleanup_cx = cx.create_child();
3226 let _cleanup_mask = cleanup_cx.masked();
3227 let close_result = file.close(&cleanup_cx);
3228 match (read_result, close_result) {
3229 (Ok(certificate), Ok(())) => Ok(certificate),
3230 (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
3231 (Err(read_error), Err(close_error)) => Err(FrankenError::internal(format!(
3232 "parallel WAL certificate tail read failed and close also failed: read={read_error}; close={close_error}"
3233 ))),
3234 }
3235 }
3236}
3237
3238impl<V> WalBackend for PathRefreshingWalBackend<V>
3239where
3240 V: Vfs + 'static,
3241 V::File: Send + Sync + 'static,
3242{
3243 fn begin_transaction<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
3244 Box::pin(async move {
3245 self.ensure_current_wal_path(cx).await?;
3246 self.inner.begin_transaction(cx).await
3247 })
3248 }
3249
3250 fn published_snapshot(&self) -> Option<WalPublicationSnapshot> {
3251 Some(self.inner.published_snapshot())
3252 }
3253
3254 fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
3255 self.inner.pinned_read_snapshot()
3256 }
3257
3258 fn pinned_logical_read_snapshot<'a>(
3259 &'a self,
3260 cx: &'a Cx,
3261 ) -> WalFuture<'a, Option<WalLogicalReadSnapshot>> {
3262 Box::pin(async move {
3263 let Some(pinned) = self.inner.pinned_read_snapshot() else {
3264 return Ok(None);
3265 };
3266 let Some(last_commit_frame) = pinned.last_commit_frame else {
3267 return Ok(None);
3268 };
3269 let Some(record) = self
3270 .latest_authorized_durable_certificate_record(cx)
3271 .await?
3272 else {
3273 return Ok(None);
3274 };
3275 if record.wal_generation != pinned.generation {
3276 return Err(FrankenError::WalCorrupt {
3277 detail: "current logical WAL certificate generation differs from pinned reader"
3278 .to_owned(),
3279 });
3280 }
3281 let certificate_commit_frame =
3282 usize::try_from(record.wal_frame_end.checked_sub(1).ok_or_else(|| {
3283 FrankenError::WalCorrupt {
3284 detail: "current logical WAL certificate ends at frame zero".to_owned(),
3285 }
3286 })?)
3287 .map_err(|_| FrankenError::WalCorrupt {
3288 detail: "current logical WAL certificate frame exceeds usize".to_owned(),
3289 })?;
3290 if certificate_commit_frame > last_commit_frame {
3291 return Err(FrankenError::WalCorrupt {
3292 detail: "current logical WAL certificate extends past pinned reader horizon"
3293 .to_owned(),
3294 });
3295 }
3296
3297 let first_tail_frame =
3298 usize::try_from(record.wal_frame_end).map_err(|_| FrankenError::WalCorrupt {
3299 detail: "logical WAL tail frame exceeds usize".to_owned(),
3300 })?;
3301 let mut tail_commit_count = 0_u64;
3302 if first_tail_frame <= last_commit_frame {
3303 for frame_index in first_tail_frame..=last_commit_frame {
3304 if self
3305 .inner
3306 .inner()
3307 .read_frame_header(cx, frame_index)
3308 .await?
3309 .is_commit()
3310 {
3311 tail_commit_count = tail_commit_count.checked_add(1).ok_or_else(|| {
3312 FrankenError::WalCorrupt {
3313 detail: "logical WAL tail commit count overflow".to_owned(),
3314 }
3315 })?;
3316 }
3317 }
3318 }
3319 let visible_commit_seq = CommitSeq::new(
3320 record
3321 .certificate
3322 .commit_seq_hi
3323 .get()
3324 .checked_add(tail_commit_count)
3325 .ok_or_else(|| FrankenError::WalCorrupt {
3326 detail: "logical WAL visible commit sequence overflow".to_owned(),
3327 })?,
3328 );
3329 Ok(Some(WalLogicalReadSnapshot {
3330 generation: pinned.generation,
3331 last_commit_frame: pinned.last_commit_frame,
3332 visible_commit_seq,
3333 }))
3334 })
3335 }
3336
3337 fn refresh_published_snapshot<'a>(
3338 &'a mut self,
3339 cx: &'a Cx,
3340 ) -> WalFuture<'a, Option<WalPublicationSnapshot>> {
3341 Box::pin(async move {
3342 self.ensure_current_wal_path(cx).await?;
3343 self.inner.refresh_published_snapshot(cx).await.map(Some)
3344 })
3345 }
3346
3347 fn publish_authorized_deferred_commit<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
3348 Box::pin(async move { self.inner.publish_authorized_deferred_commit(cx) })
3349 }
3350
3351 fn append_frame<'a>(
3352 &'a mut self,
3353 cx: &'a Cx,
3354 page_number: u32,
3355 page_data: &'a [u8],
3356 db_size_if_commit: u32,
3357 ) -> WalFuture<'a, ()> {
3358 Box::pin(async move {
3359 self.ensure_current_wal_path(cx).await?;
3360 self.inner
3361 .append_frame(cx, page_number, page_data, db_size_if_commit)
3362 .await
3363 })
3364 }
3365
3366 fn append_frames<'a>(
3367 &'a mut self,
3368 cx: &'a Cx,
3369 frames: &'a [WalFrameRef<'a>],
3370 ) -> WalFuture<'a, ()> {
3371 Box::pin(async move {
3372 self.ensure_current_wal_path(cx).await?;
3373 self.inner.append_frames(cx, frames).await
3374 })
3375 }
3376
3377 fn append_frames_tracked<'a>(
3378 &'a mut self,
3379 cx: &'a Cx,
3380 frames: &'a [WalFrameRef<'a>],
3381 completion: VfsWriteCompletion,
3382 ) -> WalFuture<'a, ()> {
3383 Box::pin(async move {
3384 let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3385 self.ensure_current_wal_path(cx).await?;
3386 preflight.hand_off();
3387 drop(preflight);
3388 self.inner
3389 .append_frames_tracked(cx, frames, completion)
3390 .await
3391 })
3392 }
3393
3394 fn prepare_append_frames(
3395 &self,
3396 frames: &[WalFrameRef<'_>],
3397 ) -> Result<Option<PreparedWalFrameBatch>> {
3398 self.inner.prepare_append_frames(frames)
3399 }
3400
3401 fn finalize_prepared_frames(
3402 &self,
3403 cx: &Cx,
3404 prepared: &mut PreparedWalFrameBatch,
3405 ) -> Result<()> {
3406 self.inner.finalize_prepared_frames(cx, prepared)
3407 }
3408
3409 fn append_prepared_frames<'a>(
3410 &'a mut self,
3411 cx: &'a Cx,
3412 prepared: &'a mut PreparedWalFrameBatch,
3413 ) -> WalFuture<'a, ()> {
3414 Box::pin(async move {
3415 self.ensure_current_wal_path(cx).await?;
3416 self.inner.append_prepared_frames(cx, prepared).await
3417 })
3418 }
3419
3420 fn append_prepared_frames_tracked<'a>(
3421 &'a mut self,
3422 cx: &'a Cx,
3423 prepared: &'a mut PreparedWalFrameBatch,
3424 completion: VfsWriteCompletion,
3425 ) -> WalFuture<'a, ()> {
3426 Box::pin(async move {
3427 let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3428 self.ensure_current_wal_path(cx).await?;
3429 preflight.hand_off();
3430 drop(preflight);
3431 self.inner
3432 .append_prepared_frames_tracked(cx, prepared, completion)
3433 .await
3434 })
3435 }
3436
3437 fn persist_parallel_wal_commit_certificate<'a>(
3438 &'a mut self,
3439 cx: &'a Cx,
3440 certificate: &'a ParallelWalCommitCertificate,
3441 wal_frame_start: u64,
3442 wal_frame_end: u64,
3443 sync: bool,
3444 ) -> WalFuture<'a, ()> {
3445 Box::pin(async move {
3446 self.ensure_current_wal_path(cx).await?;
3447 self.append_durable_certificate_record(
3448 cx,
3449 certificate,
3450 wal_frame_start,
3451 wal_frame_end,
3452 sync,
3453 )
3454 .await
3455 })
3456 }
3457
3458 fn persist_parallel_wal_commit_certificate_tracked<'a>(
3459 &'a mut self,
3460 cx: &'a Cx,
3461 certificate: &'a ParallelWalCommitCertificate,
3462 wal_frame_start: u64,
3463 wal_frame_end: u64,
3464 sync: bool,
3465 completion: VfsWriteCompletion,
3466 ) -> WalFuture<'a, ()> {
3467 Box::pin(async move {
3468 let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3469 self.ensure_current_wal_path(cx).await?;
3470 preflight.hand_off();
3471 drop(preflight);
3472 self.append_durable_certificate_record_with_completion(
3473 cx,
3474 certificate,
3475 wal_frame_start,
3476 wal_frame_end,
3477 sync,
3478 Some(&completion),
3479 )
3480 .await
3481 })
3482 }
3483
3484 fn reconcile_parallel_wal_commit<'a>(
3485 &'a mut self,
3486 cx: &'a Cx,
3487 certificate: &'a ParallelWalCommitCertificate,
3488 wal_frame_start: u64,
3489 wal_frame_end: u64,
3490 sync: bool,
3491 ) -> WalFuture<'a, ParallelWalCommitReconciliation> {
3492 Box::pin(async move {
3493 self.ensure_current_wal_path(cx).await?;
3494 self.inner.wal.refresh(cx).await?;
3495 let wal_generation = self.inner.wal.generation_identity();
3496 let expected_record = ParallelWalDurableCertificateRecord::new(
3497 wal_generation,
3498 wal_frame_start,
3499 wal_frame_end,
3500 certificate.clone(),
3501 )
3502 .map_err(|error| {
3503 FrankenError::internal(format!(
3504 "could not reconstruct in-doubt parallel WAL certificate: {error}"
3505 ))
3506 })?;
3507
3508 let valid_frame_count = u64::try_from(self.inner.wal.frame_count()).unwrap_or(u64::MAX);
3509 let target_commit_present = if valid_frame_count < wal_frame_end {
3510 false
3511 } else {
3512 let target_index =
3513 usize::try_from(wal_frame_end.saturating_sub(1)).map_err(|_| {
3514 FrankenError::WalCorrupt {
3515 detail: "in-doubt WAL commit-marker index exceeds usize".to_owned(),
3516 }
3517 })?;
3518 self.inner
3519 .wal
3520 .read_frame_header(cx, target_index)
3521 .await?
3522 .is_commit()
3523 };
3524
3525 if target_commit_present {
3526 if valid_frame_count != wal_frame_end {
3527 return Err(FrankenError::WalCorrupt {
3528 detail: format!(
3529 "in-doubt parallel WAL interval ends at frame {wal_frame_end}, but the retained writer gate observed committed frame count {valid_frame_count}"
3530 ),
3531 });
3532 }
3533 let actual_wal_frame_payload_digest = self
3534 .wal_frame_payload_digest(cx, wal_frame_start, wal_frame_end)
3535 .await?;
3536 if !expected_record.authorizes_wal_boundary(
3537 wal_generation,
3538 valid_frame_count,
3539 wal_frame_end,
3540 actual_wal_frame_payload_digest,
3541 ) {
3542 return Err(FrankenError::WalCorrupt {
3543 detail: format!(
3544 "in-doubt parallel WAL interval {wal_frame_start}..={wal_frame_end} does not match its content-bound certificate"
3545 ),
3546 });
3547 }
3548 let sidecar_is_exact = self
3549 .reconcile_certificate_sidecar_record(cx, &expected_record, false, sync)
3550 .await?;
3551 if !sidecar_is_exact {
3552 return Err(FrankenError::WalCorrupt {
3553 detail: format!(
3554 "parallel WAL commit marker at frame {wal_frame_end} has no exact durable certificate"
3555 ),
3556 });
3557 }
3558 if sync {
3559 self.inner.wal.sync(cx, SyncFlags::NORMAL)?;
3560 self.vfs.sync_parent_directory(cx, &self.wal_path)?;
3561 }
3562 return Ok(ParallelWalCommitReconciliation::Authorized);
3563 }
3564
3565 let committed_prefix_before =
3566 wal_frame_start
3567 .checked_sub(1)
3568 .ok_or_else(|| FrankenError::WalCorrupt {
3569 detail: "parallel WAL recovery interval starts at frame zero".to_owned(),
3570 })?;
3571 if valid_frame_count != committed_prefix_before {
3572 return Err(FrankenError::WalCorrupt {
3573 detail: format!(
3574 "in-doubt WAL interval {wal_frame_start}..={wal_frame_end} has unexpected committed prefix {valid_frame_count}"
3575 ),
3576 });
3577 }
3578 self.reconcile_certificate_sidecar_record(cx, &expected_record, true, sync)
3583 .await?;
3584 self.inner.wal.repair_uncommitted_tail(cx)?;
3585 if sync {
3586 self.inner.wal.sync(cx, SyncFlags::NORMAL)?;
3587 self.vfs.sync_parent_directory(cx, &self.wal_path)?;
3588 }
3589 Ok(ParallelWalCommitReconciliation::NotCommitted)
3590 })
3591 }
3592
3593 fn latest_authorized_parallel_wal_commit_certificate<'a>(
3594 &'a mut self,
3595 cx: &'a Cx,
3596 ) -> WalFuture<'a, Option<ParallelWalCommitCertificate>> {
3597 Box::pin(async move {
3598 self.ensure_current_wal_path(cx).await?;
3599 if let Some(record) = self
3600 .latest_authorized_durable_certificate_record(cx)
3601 .await?
3602 {
3603 return Ok(Some(record.certificate));
3604 }
3605 self.checkpoint_certificate_handoff(cx).await
3606 })
3607 }
3608
3609 fn read_page<'a>(&'a mut self, cx: &'a Cx, page_number: u32) -> WalFuture<'a, Option<Vec<u8>>> {
3610 Box::pin(async move {
3611 self.ensure_current_wal_path(cx).await?;
3612 self.inner.read_page(cx, page_number).await
3613 })
3614 }
3615
3616 fn read_page_pinned<'a>(
3617 &'a self,
3618 cx: &'a Cx,
3619 page_number: u32,
3620 ) -> WalFuture<'a, Option<Vec<u8>>> {
3621 Box::pin(async move { self.inner.read_page_pinned(cx, page_number).await })
3622 }
3623
3624 fn supports_pinned_reads(&self) -> bool {
3625 self.inner.supports_pinned_reads()
3626 }
3627
3628 fn committed_txns_since_page<'a>(
3629 &'a mut self,
3630 cx: &'a Cx,
3631 page_number: u32,
3632 ) -> WalFuture<'a, u64> {
3633 Box::pin(async move {
3634 self.ensure_current_wal_path(cx).await?;
3635 self.inner.committed_txns_since_page(cx, page_number).await
3636 })
3637 }
3638
3639 fn conflicting_pages_since_snapshot<'a>(
3640 &'a mut self,
3641 cx: &'a Cx,
3642 snapshot: TransactionConflictSnapshot,
3643 page_numbers: &'a [u32],
3644 page_baselines: &'a [TransactionConflictPageBaseline],
3645 ) -> WalFuture<'a, Vec<u32>> {
3646 Box::pin(async move {
3647 self.ensure_current_wal_path(cx).await?;
3648 let latest = self.inner.refresh_published_snapshot(cx).await?;
3649 if latest.generation != snapshot.generation {
3650 return Ok(self
3651 .conflicts_after_generation_change(cx, page_numbers, page_baselines)
3652 .await);
3653 }
3654 self.inner
3655 .conflicting_pages_since_snapshot(cx, snapshot, page_numbers, page_baselines)
3656 .await
3657 })
3658 }
3659
3660 fn committed_txn_count<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, u64> {
3661 Box::pin(async move {
3662 self.ensure_current_wal_path(cx).await?;
3663 self.inner.committed_txn_count(cx).await
3664 })
3665 }
3666
3667 fn sync(&mut self, cx: &Cx) -> Result<()> {
3668 #[cfg(all(feature = "native", any(unix, windows)))]
3669 if let Some(binding) = &self.namespace_binding {
3670 binding.validate_path_identity()?;
3671 }
3672 self.inner.sync(cx)
3673 }
3674
3675 fn frame_count(&self) -> usize {
3676 self.inner.frame_count()
3677 }
3678
3679 fn checkpoint<'a>(
3680 &'a mut self,
3681 cx: &'a Cx,
3682 mode: CheckpointMode,
3683 writer: &'a mut dyn CheckpointPageWriter,
3684 backfilled_frames: u32,
3685 oldest_reader_frame: Option<u32>,
3686 ) -> WalFuture<'a, CheckpointResult> {
3687 Box::pin(async move {
3688 self.ensure_current_wal_path(cx).await?;
3689 let checkpoint_handoff = self
3690 .latest_authorized_durable_certificate_record(cx)
3691 .await?;
3692 if let Some(record) = checkpoint_handoff.as_ref() {
3693 self.persist_checkpoint_certificate_handoff(cx, record)
3700 .await?;
3701 }
3702 let result = self
3703 .inner
3704 .checkpoint(cx, mode, writer, backfilled_frames, oldest_reader_frame)
3705 .await?;
3706 Ok(result)
3707 })
3708 }
3709}
3710
3711struct CheckpointTargetAdapterRef<'a> {
3716 writer: &'a mut dyn CheckpointPageWriter,
3717}
3718
3719impl CheckpointTarget for CheckpointTargetAdapterRef<'_> {
3720 fn write_page<'a>(
3721 &'a mut self,
3722 cx: &'a Cx,
3723 page_no: PageNumber,
3724 data: &'a [u8],
3725 ) -> CheckpointTargetFuture<'a, ()> {
3726 Box::pin(async move { self.writer.write_page(cx, page_no, data).await })
3727 }
3728
3729 fn truncate_db<'a>(&'a mut self, cx: &'a Cx, n_pages: u32) -> CheckpointTargetFuture<'a, ()> {
3730 Box::pin(async move { self.writer.truncate(cx, n_pages).await })
3731 }
3732
3733 fn sync_db<'a>(&'a mut self, cx: &'a Cx) -> CheckpointTargetFuture<'a, ()> {
3734 Box::pin(async move { self.writer.sync(cx).await })
3735 }
3736}
3737
3738#[cfg(test)]
3743mod tests {
3744 use std::sync::Mutex;
3745
3746 use fsqlite_pager::MockCheckpointPageWriter;
3747 use fsqlite_pager::traits::WalFrameRef;
3748 use fsqlite_types::flags::VfsOpenFlags;
3749 use fsqlite_vfs::MemoryVfs;
3750 use fsqlite_vfs::traits::{Vfs, VfsFile};
3751 use fsqlite_wal::checksum::WalSalts;
3752
3753 use super::*;
3754
3755 const PAGE_SIZE: u32 = 4096;
3756 const CERTIFICATE_PATH: &str = "test.db-wal-cert";
3757 const CHECKPOINT_HANDOFF_PATH: &str = "test.db-wal-cert-head";
3758
3759 #[derive(Clone, Copy, Debug)]
3760 enum CheckpointHandoffWriteFault {
3761 Error,
3762 Pending,
3763 }
3764
3765 #[derive(Clone, Debug, Eq, PartialEq)]
3766 enum CertificateSyncObservation {
3767 Ordinary(PathBuf),
3768 Durable(PathBuf, SyncKind),
3769 }
3770
3771 #[derive(Debug, Default)]
3772 struct CheckpointHandoffFaultState {
3773 next_write: Option<CheckpointHandoffWriteFault>,
3774 fail_next_sync: bool,
3775 fail_next_wal_sync: bool,
3777 sync_observations: Vec<CertificateSyncObservation>,
3778 }
3779
3780 #[derive(Clone, Debug)]
3781 struct CheckpointHandoffFaultVfs {
3782 inner: MemoryVfs,
3783 faults: Arc<Mutex<CheckpointHandoffFaultState>>,
3784 }
3785
3786 impl CheckpointHandoffFaultVfs {
3787 fn new() -> Self {
3788 Self {
3789 inner: MemoryVfs::new(),
3790 faults: Arc::new(Mutex::new(CheckpointHandoffFaultState::default())),
3791 }
3792 }
3793
3794 fn fail_next_handoff_write(&self) {
3795 self.faults
3796 .lock()
3797 .unwrap_or_else(std::sync::PoisonError::into_inner)
3798 .next_write = Some(CheckpointHandoffWriteFault::Error);
3799 }
3800
3801 fn pend_next_handoff_write(&self) {
3802 self.faults
3803 .lock()
3804 .unwrap_or_else(std::sync::PoisonError::into_inner)
3805 .next_write = Some(CheckpointHandoffWriteFault::Pending);
3806 }
3807
3808 fn fail_next_handoff_sync(&self) {
3809 self.faults
3810 .lock()
3811 .unwrap_or_else(std::sync::PoisonError::into_inner)
3812 .fail_next_sync = true;
3813 }
3814
3815 fn fail_next_wal_sync(&self) {
3817 self.faults
3818 .lock()
3819 .unwrap_or_else(std::sync::PoisonError::into_inner)
3820 .fail_next_wal_sync = true;
3821 }
3822
3823 fn take_sync_observations(&self) -> Vec<CertificateSyncObservation> {
3824 std::mem::take(
3825 &mut self
3826 .faults
3827 .lock()
3828 .unwrap_or_else(std::sync::PoisonError::into_inner)
3829 .sync_observations,
3830 )
3831 }
3832 }
3833
3834 #[derive(Debug)]
3835 struct CheckpointHandoffFaultFile {
3836 inner: <MemoryVfs as Vfs>::File,
3837 faults: Arc<Mutex<CheckpointHandoffFaultState>>,
3838 path: Option<PathBuf>,
3839 is_checkpoint_handoff: bool,
3840 }
3841
3842 impl Vfs for CheckpointHandoffFaultVfs {
3843 type File = CheckpointHandoffFaultFile;
3844
3845 fn name(&self) -> &'static str {
3846 "checkpoint-handoff-fault"
3847 }
3848
3849 fn open(
3850 &self,
3851 cx: &Cx,
3852 path: Option<&Path>,
3853 flags: VfsOpenFlags,
3854 ) -> Result<(Self::File, VfsOpenFlags)> {
3855 let is_checkpoint_handoff =
3856 path.is_some_and(|candidate| candidate == Path::new(CHECKPOINT_HANDOFF_PATH));
3857 let (inner, actual_flags) = self.inner.open(cx, path, flags)?;
3858 Ok((
3859 CheckpointHandoffFaultFile {
3860 inner,
3861 faults: Arc::clone(&self.faults),
3862 path: path.map(Path::to_path_buf),
3863 is_checkpoint_handoff,
3864 },
3865 actual_flags,
3866 ))
3867 }
3868
3869 fn delete(&self, cx: &Cx, path: &Path, sync_dir: bool) -> Result<()> {
3870 self.inner.delete(cx, path, sync_dir)
3871 }
3872
3873 fn sync_parent_directory(&self, cx: &Cx, path: &Path) -> Result<()> {
3874 self.inner.sync_parent_directory(cx, path)
3875 }
3876
3877 fn access(&self, cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool> {
3878 self.inner.access(cx, path, flags)
3879 }
3880
3881 fn path_entry_exists(&self, cx: &Cx, path: &Path) -> Result<bool> {
3882 self.inner.path_entry_exists(cx, path)
3883 }
3884
3885 fn full_pathname(&self, cx: &Cx, path: &Path) -> Result<PathBuf> {
3886 self.inner.full_pathname(cx, path)
3887 }
3888
3889 fn randomness(&self, cx: &Cx, buf: &mut [u8]) {
3890 self.inner.randomness(cx, buf);
3891 }
3892
3893 fn current_time(&self, cx: &Cx) -> f64 {
3894 self.inner.current_time(cx)
3895 }
3896
3897 fn is_memory(&self) -> bool {
3898 true
3899 }
3900 }
3901
3902 impl VfsFile for CheckpointHandoffFaultFile {
3903 fn close(&mut self, cx: &Cx) -> Result<()> {
3904 self.inner.close(cx)
3905 }
3906
3907 fn file_identity(&self) -> Result<Option<fsqlite_vfs::FileIdentity>> {
3908 self.inner.file_identity()
3909 }
3910
3911 fn read<'a>(
3912 &'a self,
3913 cx: &'a Cx,
3914 buf: &'a mut [u8],
3915 offset: u64,
3916 ) -> impl std::future::Future<Output = Result<usize>> + Send + 'a {
3917 self.inner.read(cx, buf, offset)
3918 }
3919
3920 async fn write<'a>(&'a self, cx: &'a Cx, buf: &'a [u8], offset: u64) -> Result<()> {
3921 let fault = if self.is_checkpoint_handoff {
3922 self.faults
3923 .lock()
3924 .unwrap_or_else(std::sync::PoisonError::into_inner)
3925 .next_write
3926 .take()
3927 } else {
3928 None
3929 };
3930 match fault {
3931 Some(CheckpointHandoffWriteFault::Error) => Err(FrankenError::Io(
3932 std::io::Error::other("injected checkpoint handoff write failure"),
3933 )),
3934 Some(CheckpointHandoffWriteFault::Pending) => {
3935 std::future::pending::<Result<()>>().await
3936 }
3937 None => self.inner.write(cx, buf, offset).await,
3938 }
3939 }
3940
3941 fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()> {
3942 self.inner.truncate(cx, size)
3943 }
3944
3945 fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()> {
3946 let mut faults = self
3947 .faults
3948 .lock()
3949 .unwrap_or_else(std::sync::PoisonError::into_inner);
3950 if let Some(path) = self.path.as_ref().filter(|path| {
3951 path.as_path() == Path::new(CERTIFICATE_PATH)
3952 || path.as_path() == Path::new(CHECKPOINT_HANDOFF_PATH)
3953 }) {
3954 faults
3955 .sync_observations
3956 .push(CertificateSyncObservation::Ordinary(path.clone()));
3957 }
3958 let fail = self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_sync);
3959 let fail_wal =
3960 !self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_wal_sync);
3961 drop(faults);
3962 if fail {
3963 Err(FrankenError::Io(std::io::Error::other(
3964 "injected checkpoint handoff sync failure",
3965 )))
3966 } else if fail_wal {
3967 Err(FrankenError::Io(std::io::Error::other(
3968 "injected WAL sync failure",
3969 )))
3970 } else {
3971 self.inner.sync(cx, flags)
3972 }
3973 }
3974
3975 fn durable_sync(&mut self, cx: &Cx, kind: SyncKind) -> Result<()> {
3976 let mut faults = self
3977 .faults
3978 .lock()
3979 .unwrap_or_else(std::sync::PoisonError::into_inner);
3980 if let Some(path) = self.path.as_ref().filter(|path| {
3981 path.as_path() == Path::new(CERTIFICATE_PATH)
3982 || path.as_path() == Path::new(CHECKPOINT_HANDOFF_PATH)
3983 }) {
3984 faults
3985 .sync_observations
3986 .push(CertificateSyncObservation::Durable(path.clone(), kind));
3987 }
3988 let fail = self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_sync);
3989 drop(faults);
3990 if fail {
3991 Err(FrankenError::Io(std::io::Error::other(
3992 "injected checkpoint handoff durable-sync failure",
3993 )))
3994 } else {
3995 self.inner.durable_sync(cx, kind)
3996 }
3997 }
3998
3999 fn file_size(&self, cx: &Cx) -> Result<u64> {
4000 self.inner.file_size(cx)
4001 }
4002
4003 fn lock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
4004 self.inner.lock(cx, level)
4005 }
4006
4007 fn unlock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
4008 self.inner.unlock(cx, level)
4009 }
4010
4011 fn lock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
4012 self.inner.lock_external_shared_snapshot(cx)
4013 }
4014
4015 fn restore_external_shared_snapshot_attempt(&mut self, cx: &Cx) -> Result<()> {
4016 self.inner.restore_external_shared_snapshot_attempt(cx)
4017 }
4018
4019 fn lock_external_maintenance(&mut self, cx: &Cx, wal_mode: bool) -> Result<()> {
4020 self.inner.lock_external_maintenance(cx, wal_mode)
4021 }
4022
4023 fn restore_external_maintenance_attempt(&mut self, cx: &Cx) -> Result<()> {
4024 self.inner.restore_external_maintenance_attempt(cx)
4025 }
4026
4027 fn check_reserved_lock(&self, cx: &Cx) -> Result<bool> {
4028 self.inner.check_reserved_lock(cx)
4029 }
4030
4031 fn sector_size(&self) -> u32 {
4032 self.inner.sector_size()
4033 }
4034
4035 fn device_characteristics(&self) -> u32 {
4036 self.inner.device_characteristics()
4037 }
4038
4039 fn shm_map(
4040 &mut self,
4041 cx: &Cx,
4042 region: u32,
4043 size: u32,
4044 extend: bool,
4045 ) -> Result<fsqlite_vfs::ShmRegion> {
4046 self.inner.shm_map(cx, region, size, extend)
4047 }
4048
4049 fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
4050 self.inner.shm_lock(cx, offset, n, flags)
4051 }
4052
4053 fn shm_barrier(&self) {
4054 self.inner.shm_barrier();
4055 }
4056
4057 fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
4058 self.inner.shm_unmap(cx, delete)
4059 }
4060
4061 fn set_busy_timeout_ms(&mut self, ms: u64) {
4062 self.inner.set_busy_timeout_ms(ms);
4063 }
4064 }
4065
4066 fn init_wal_publication_test_tracing() {}
4082
4083 #[test]
4093 fn wal_publication_tracing_helper_installs_no_global_subscriber() {
4094 let before = tracing::dispatcher::has_been_set();
4095 init_wal_publication_test_tracing();
4096
4097 assert_eq!(
4098 before,
4099 tracing::dispatcher::has_been_set(),
4100 "init_wal_publication_test_tracing must not install or alter a global subscriber"
4101 );
4102 }
4103
4104 fn test_cx() -> Cx {
4105 Cx::default()
4106 }
4107
4108 fn test_salts() -> WalSalts {
4109 WalSalts {
4110 salt1: 0xDEAD_BEEF,
4111 salt2: 0xCAFE_BABE,
4112 }
4113 }
4114
4115 fn sample_page(seed: u8) -> Vec<u8> {
4116 let page_size = usize::try_from(PAGE_SIZE).expect("page size fits usize");
4117 let mut page = vec![0u8; page_size];
4118 for (i, byte) in page.iter_mut().enumerate() {
4119 let reduced = u8::try_from(i % 251).expect("modulo fits u8");
4120 *byte = reduced ^ seed;
4121 }
4122 page
4123 }
4124
4125 fn test_frame_payload_digest(
4126 page_number: u32,
4127 page_data: &[u8],
4128 db_size_if_commit: u32,
4129 ) -> [u8; 32] {
4130 let mut digest = ParallelWalFramePayloadDigestBuilder::new();
4131 digest.update(
4132 PageNumber::new(page_number).expect("test page number must be valid"),
4133 db_size_if_commit,
4134 page_data,
4135 );
4136 digest.finalize()
4137 }
4138
4139 fn sample_certificate(
4140 certificate_epoch: u64,
4141 commit_seq: u64,
4142 lane_record_counts: Vec<u32>,
4143 ) -> ParallelWalCommitCertificate {
4144 let lane_count = u16::try_from(lane_record_counts.len()).expect("test lane count fits u16");
4145 let mut certificate = ParallelWalCommitCertificate {
4146 format_version: fsqlite_wal::PARALLEL_WAL_COMMIT_CERTIFICATE_VERSION,
4147 residue: fsqlite_wal::ParallelWalOrderedResidue::CommitCertificateThenPublish,
4148 certificate_epoch,
4149 commit_seq_lo: fsqlite_types::CommitSeq::new(commit_seq),
4150 commit_seq_hi: fsqlite_types::CommitSeq::new(commit_seq),
4151 durable_segment_epoch: certificate_epoch,
4152 lane_count,
4153 lane_record_counts,
4154 db_size_pages: 1,
4155 page_set_size: 1,
4156 wal_frame_payload_digest: [0xA5; 32],
4157 certificate_crc32c: 0,
4158 fallback_active: false,
4159 };
4160 certificate.certificate_crc32c = certificate.computed_crc32c();
4161 certificate
4162 }
4163
4164 fn make_path_refreshing_backend(
4165 vfs: &MemoryVfs,
4166 cx: &Cx,
4167 ) -> PathRefreshingWalBackend<MemoryVfs> {
4168 let wal = WalFile::create(cx, open_wal_file(vfs, cx), PAGE_SIZE, 0, test_salts())
4169 .expect("create WAL");
4170 PathRefreshingWalBackend::new(
4171 vfs.clone(),
4172 std::path::Path::new("test.db"),
4173 std::path::Path::new("test.db-wal"),
4174 PAGE_SIZE,
4175 wal,
4176 true,
4177 #[cfg(all(feature = "native", any(unix, windows)))]
4178 None,
4179 )
4180 }
4181
4182 fn make_authorized_certificate_backend(
4183 vfs: &MemoryVfs,
4184 cx: &Cx,
4185 ) -> (
4186 PathRefreshingWalBackend<MemoryVfs>,
4187 ParallelWalCommitCertificate,
4188 ) {
4189 let mut backend = make_path_refreshing_backend(vfs, cx);
4190 let committed_page = sample_page(0x44);
4191 let mut certificate = sample_certificate(1, 1, vec![1]);
4192 certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &committed_page, 1);
4193 certificate.certificate_crc32c = certificate.computed_crc32c();
4194 backend
4195 .persist_parallel_wal_commit_certificate(cx, &certificate, 1, 1, true)
4196 .expect("persist authorized certificate");
4197 backend
4198 .append_frame(cx, 1, &committed_page, 1)
4199 .expect("append matching commit marker");
4200 backend.sync(cx).expect("sync matching commit marker");
4201 (backend, certificate)
4202 }
4203
4204 struct AuthoritativeWalSnapshot {
4205 generation: WalGenerationIdentity,
4206 frame_count: usize,
4207 wal_bytes: Vec<u8>,
4208 certificate: ParallelWalCommitCertificate,
4209 committed_page: Vec<u8>,
4210 }
4211
4212 fn make_checkpoint_handoff_fault_backend(
4213 vfs: &CheckpointHandoffFaultVfs,
4214 cx: &Cx,
4215 ) -> (
4216 PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4217 ParallelWalCommitCertificate,
4218 Vec<u8>,
4219 ) {
4220 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
4221 let (file, _) = vfs
4222 .open(cx, Some(Path::new("test.db-wal")), flags)
4223 .expect("open fault-injected WAL file");
4224 let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts())
4225 .expect("create fault-injected WAL");
4226 let mut backend = PathRefreshingWalBackend::new(
4227 vfs.clone(),
4228 Path::new("test.db"),
4229 Path::new("test.db-wal"),
4230 PAGE_SIZE,
4231 wal,
4232 true,
4233 #[cfg(all(feature = "native", any(unix, windows)))]
4234 None,
4235 );
4236 let committed_page = sample_page(0x47);
4237 let mut certificate = sample_certificate(1, 1, vec![1]);
4238 certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &committed_page, 1);
4239 certificate.certificate_crc32c = certificate.computed_crc32c();
4240 backend
4241 .persist_parallel_wal_commit_certificate(cx, &certificate, 1, 1, true)
4242 .expect("persist authorized certificate");
4243 backend
4244 .append_frame(cx, 1, &committed_page, 1)
4245 .expect("append matching commit marker");
4246 backend.sync(cx).expect("sync matching commit marker");
4247 (backend, certificate, committed_page)
4248 }
4249
4250 fn read_fault_injected_wal(vfs: &CheckpointHandoffFaultVfs, cx: &Cx) -> Vec<u8> {
4251 let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
4252 let (mut file, _) = vfs
4253 .open(cx, Some(Path::new("test.db-wal")), flags)
4254 .expect("open WAL snapshot");
4255 let len = usize::try_from(file.file_size(cx).expect("read WAL size"))
4256 .expect("WAL size fits usize");
4257 let mut bytes = vec![0_u8; len];
4258 assert_eq!(
4259 file.read(cx, &mut bytes, 0).expect("read WAL snapshot"),
4260 len
4261 );
4262 file.close(cx).expect("close WAL snapshot");
4263 bytes
4264 }
4265
4266 fn capture_authoritative_wal(
4267 backend: &PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4268 vfs: &CheckpointHandoffFaultVfs,
4269 cx: &Cx,
4270 certificate: ParallelWalCommitCertificate,
4271 committed_page: Vec<u8>,
4272 ) -> AuthoritativeWalSnapshot {
4273 AuthoritativeWalSnapshot {
4274 generation: backend.inner.inner().generation_identity(),
4275 frame_count: backend.inner.frame_count(),
4276 wal_bytes: read_fault_injected_wal(vfs, cx),
4277 certificate,
4278 committed_page,
4279 }
4280 }
4281
4282 fn assert_authoritative_wal_unchanged(
4283 backend: &mut PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4284 vfs: &CheckpointHandoffFaultVfs,
4285 cx: &Cx,
4286 before: &AuthoritativeWalSnapshot,
4287 ) {
4288 assert_eq!(
4289 backend.inner.inner().generation_identity(),
4290 before.generation,
4291 "checkpoint handoff failure must not reset the WAL generation"
4292 );
4293 assert_eq!(
4294 backend.inner.frame_count(),
4295 before.frame_count,
4296 "checkpoint handoff failure must not change the visible frame count"
4297 );
4298 assert_eq!(
4299 read_fault_injected_wal(vfs, cx),
4300 before.wal_bytes,
4301 "checkpoint handoff failure must leave the authoritative WAL byte-for-byte unchanged"
4302 );
4303 assert!(
4304 backend
4305 .inner
4306 .inner()
4307 .read_frame_header(cx, 0)
4308 .expect("read original commit frame")
4309 .is_commit(),
4310 "the original generation's commit marker must remain authoritative"
4311 );
4312 assert_eq!(
4313 backend
4314 .latest_authorized_parallel_wal_commit_certificate(cx)
4315 .expect("recover certificate from unchanged WAL generation"),
4316 Some(before.certificate.clone())
4317 );
4318 assert_eq!(
4319 backend
4320 .read_page(cx, 1)
4321 .expect("read committed page from unchanged WAL generation"),
4322 Some(before.committed_page.clone())
4323 );
4324 }
4325
4326 fn read_certificate_sidecar(vfs: &MemoryVfs, cx: &Cx) -> Vec<u8> {
4327 let path = std::path::Path::new("test.db-wal-cert");
4328 let (mut file, _) = vfs
4329 .open(cx, Some(path), VfsOpenFlags::READONLY | VfsOpenFlags::WAL)
4330 .expect("open certificate sidecar");
4331 let len = usize::try_from(file.file_size(cx).expect("read certificate sidecar size"))
4332 .expect("certificate sidecar size fits usize");
4333 let mut bytes = vec![0_u8; len];
4334 assert_eq!(
4335 file.read(cx, &mut bytes, 0)
4336 .expect("read certificate sidecar"),
4337 len
4338 );
4339 file.close(cx).expect("close certificate sidecar");
4340 bytes
4341 }
4342
4343 fn replace_certificate_sidecar(vfs: &MemoryVfs, cx: &Cx, bytes: &[u8]) {
4344 let path = std::path::Path::new("test.db-wal-cert");
4345 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
4346 let (mut file, _) = vfs
4347 .open(cx, Some(path), flags)
4348 .expect("open mutable certificate sidecar");
4349 file.truncate(cx, 0)
4350 .expect("truncate mutable certificate sidecar");
4351 file.write(cx, bytes, 0)
4352 .expect("replace certificate sidecar bytes");
4353 file.close(cx).expect("close mutable certificate sidecar");
4354 }
4355
4356 fn assert_wal_corrupt<T: std::fmt::Debug>(result: Result<T>, scenario: &str) {
4357 assert!(
4358 matches!(&result, Err(FrankenError::WalCorrupt { .. })),
4359 "{scenario} must fail closed with WalCorrupt, got {result:?}"
4360 );
4361 }
4362
4363 fn sqlite_page_one(encoded_page_size: u16) -> Vec<u8> {
4364 let mut page = sample_page(0x11);
4365 page[..16].copy_from_slice(b"SQLite format 3\0");
4366 page[16..18].copy_from_slice(&encoded_page_size.to_be_bytes());
4367 page
4368 }
4369
4370 fn write_main_db_pages(vfs: &MemoryVfs, cx: &Cx, pages: &[Vec<u8>]) {
4371 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::MAIN_DB;
4372 let (mut file, _) = vfs
4373 .open(cx, Some(std::path::Path::new("test.db")), flags)
4374 .expect("open main database");
4375 file.truncate(cx, 0).expect("truncate main database");
4376 for (index, page) in pages.iter().enumerate() {
4377 let offset = u64::try_from(index)
4378 .expect("page index fits u64")
4379 .saturating_mul(u64::from(PAGE_SIZE));
4380 file.write(cx, page, offset).expect("write database page");
4381 }
4382 file.close(cx).expect("close main database");
4383 }
4384
4385 fn replacement_salts() -> WalSalts {
4386 WalSalts {
4387 salt1: 0x1234_5678,
4388 salt2: 0x9ABC_DEF0,
4389 }
4390 }
4391
4392 fn replace_path_visible_wal(vfs: &MemoryVfs, cx: &Cx) {
4393 let wal_path = std::path::Path::new("test.db-wal");
4394 vfs.delete(cx, wal_path, false)
4395 .expect("remove old path-visible WAL");
4396 let file = open_wal_file(vfs, cx);
4397 WalFile::create(cx, file, PAGE_SIZE, 1, replacement_salts())
4398 .expect("create replacement WAL")
4399 .close(cx)
4400 .expect("close replacement WAL");
4401 }
4402
4403 fn append_replacement_wal_page(
4404 vfs: &MemoryVfs,
4405 cx: &Cx,
4406 page_number: u32,
4407 page: &[u8],
4408 db_size_if_commit: u32,
4409 ) {
4410 let file = open_wal_file(vfs, cx);
4411 let wal = WalFile::open(cx, file).expect("open replacement WAL");
4412 let mut adapter = WalBackendAdapter::new(wal);
4413 adapter
4414 .append_frame(cx, page_number, page, db_size_if_commit)
4415 .expect("append replacement WAL page");
4416 adapter.sync(cx).expect("sync replacement WAL page");
4417 adapter
4418 .into_inner()
4419 .expect("sync drained the staged frames")
4420 .close(cx)
4421 .expect("close replacement WAL");
4422 }
4423
4424 fn make_generation_transition_backend(
4425 vfs: &MemoryVfs,
4426 cx: &Cx,
4427 ) -> (
4428 PathRefreshingWalBackend<MemoryVfs>,
4429 TransactionConflictSnapshot,
4430 Vec<u8>,
4431 ) {
4432 let page_one = sqlite_page_one(u16::try_from(PAGE_SIZE).expect("page size fits u16"));
4433 let page_two = sample_page(0x22);
4434 write_main_db_pages(vfs, cx, &[page_one.clone(), page_two.clone()]);
4435
4436 let file = open_wal_file(vfs, cx);
4437 let wal =
4438 WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create original WAL");
4439 let mut backend = PathRefreshingWalBackend::new(
4440 vfs.clone(),
4441 std::path::Path::new("test.db"),
4442 std::path::Path::new("test.db-wal"),
4443 PAGE_SIZE,
4444 wal,
4445 true,
4446 #[cfg(all(feature = "native", any(unix, windows)))]
4447 None,
4448 );
4449 backend
4450 .append_frame(cx, 1, &page_one, 0)
4451 .expect("append original page 1");
4452 backend
4453 .append_frame(cx, 2, &page_two, 2)
4454 .expect("append original commit");
4455 backend
4456 .begin_transaction(cx)
4457 .expect("pin original WAL generation");
4458 let pinned = backend.pinned_read_snapshot().expect("pinned WAL snapshot");
4459 let snapshot = TransactionConflictSnapshot {
4460 generation: pinned.generation,
4461 last_commit_frame: pinned.last_commit_frame,
4462 commit_count: pinned.commit_count,
4463 };
4464 replace_path_visible_wal(vfs, cx);
4465 (backend, snapshot, page_two)
4466 }
4467
4468 #[test]
4469 fn durable_certificate_sidecar_precedes_and_reconstructs_wal_commit() {
4470 let cx = test_cx();
4471 let vfs = MemoryVfs::new();
4472 let committed_page = sample_page(0x44);
4473 let file = open_wal_file(&vfs, &cx);
4474 let wal = WalFile::create(&cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
4475 let mut backend = PathRefreshingWalBackend::new(
4476 vfs.clone(),
4477 std::path::Path::new("test.db"),
4478 std::path::Path::new("test.db-wal"),
4479 PAGE_SIZE,
4480 wal,
4481 true,
4482 #[cfg(all(feature = "native", any(unix, windows)))]
4483 None,
4484 );
4485 let mut certificate = ParallelWalCommitCertificate {
4486 format_version: fsqlite_wal::PARALLEL_WAL_COMMIT_CERTIFICATE_VERSION,
4487 residue: fsqlite_wal::ParallelWalOrderedResidue::CommitCertificateThenPublish,
4488 certificate_epoch: 1,
4489 commit_seq_lo: fsqlite_types::CommitSeq::new(1),
4490 commit_seq_hi: fsqlite_types::CommitSeq::new(1),
4491 durable_segment_epoch: 1,
4492 lane_count: 1,
4493 lane_record_counts: vec![1],
4494 db_size_pages: 1,
4495 page_set_size: 1,
4496 wal_frame_payload_digest: test_frame_payload_digest(1, &committed_page, 1),
4497 certificate_crc32c: 0,
4498 fallback_active: false,
4499 };
4500 certificate.certificate_crc32c = certificate.computed_crc32c();
4501
4502 backend
4503 .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4504 .expect("persist certificate before WAL commit marker");
4505 assert_eq!(
4506 backend.inner.frame_count(),
4507 0,
4508 "certificate persistence must not itself expose a WAL commit marker"
4509 );
4510
4511 let certificate_path = std::path::Path::new("test.db-wal-cert");
4512 let (mut certificate_file, _) = vfs
4513 .open(
4514 &cx,
4515 Some(certificate_path),
4516 VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
4517 )
4518 .expect("open certificate sidecar");
4519 let certificate_len = usize::try_from(
4520 certificate_file
4521 .file_size(&cx)
4522 .expect("certificate sidecar size"),
4523 )
4524 .expect("certificate sidecar size fits usize");
4525 let mut record_bytes = vec![0_u8; certificate_len];
4526 assert_eq!(
4527 certificate_file
4528 .read(&cx, &mut record_bytes, 0)
4529 .expect("read certificate sidecar"),
4530 certificate_len
4531 );
4532 certificate_file
4533 .close(&cx)
4534 .expect("close certificate sidecar");
4535 let reconstructed = ParallelWalDurableCertificateRecord::from_bytes(&record_bytes)
4536 .expect("reconstruct durable certificate record");
4537 assert_eq!(reconstructed.certificate, certificate);
4538 assert_eq!(reconstructed.wal_frame_start, 1);
4539 assert_eq!(reconstructed.wal_frame_end, 1);
4540 assert_eq!(
4541 reconstructed.wal_generation,
4542 backend.inner.inner().generation_identity()
4543 );
4544 assert!(
4545 !reconstructed.authorizes_wal_boundary(
4546 backend.inner.inner().generation_identity(),
4547 0,
4548 0,
4549 test_frame_payload_digest(1, &committed_page, 1),
4550 ),
4551 "orphan certificate must not authorize visibility before the matching commit marker"
4552 );
4553
4554 backend
4555 .append_frame(&cx, 1, &committed_page, 1)
4556 .expect("append matching WAL commit marker");
4557 backend.sync(&cx).expect("sync WAL commit marker");
4558 assert!(
4559 backend
4560 .inner
4561 .inner()
4562 .read_frame_header(&cx, 0)
4563 .expect("read matching WAL commit frame")
4564 .is_commit()
4565 );
4566 assert!(reconstructed.authorizes_wal_boundary(
4567 backend.inner.inner().generation_identity(),
4568 1,
4569 1,
4570 test_frame_payload_digest(1, &committed_page, 1),
4571 ));
4572
4573 let (mut certificate_file, _) = vfs
4574 .open(
4575 &cx,
4576 Some(certificate_path),
4577 VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
4578 )
4579 .expect("reopen certificate sidecar");
4580 let torn_offset = certificate_file
4581 .file_size(&cx)
4582 .expect("certificate sidecar size before torn tail");
4583 certificate_file
4584 .write(&cx, &[0xA5], torn_offset)
4585 .expect("append torn footer byte");
4586 certificate_file
4587 .close(&cx)
4588 .expect("close sidecar with torn tail");
4589 let recovered = backend
4590 .latest_authorized_parallel_wal_commit_certificate(&cx)
4591 .wait()
4592 .expect("torn certificate tail should recover the prior valid record")
4593 .expect("prior authorized certificate should remain discoverable");
4594 assert_eq!(recovered, certificate);
4595 }
4596
4597 #[test]
4598 fn content_mismatched_wal_interval_cannot_be_authorized_or_repaired() {
4599 let cx = test_cx();
4600 let vfs = MemoryVfs::new();
4601 let certified_page = sample_page(0x61);
4602 let actual_page = sample_page(0x62);
4603 let mut backend = make_path_refreshing_backend(&vfs, &cx);
4604 let mut certificate = sample_certificate(1, 1, vec![1]);
4605 certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &certified_page, 1);
4606 certificate.certificate_crc32c = certificate.computed_crc32c();
4607
4608 backend
4609 .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4610 .expect("persist content-bound certificate");
4611 backend
4612 .append_frame(&cx, 1, &actual_page, 1)
4613 .expect("append differently valued commit frame");
4614 backend.sync(&cx).expect("sync mismatched commit frame");
4615
4616 let sidecar_before = read_certificate_sidecar(&vfs, &cx);
4617 assert!(
4618 backend
4619 .latest_authorized_parallel_wal_commit_certificate(&cx)
4620 .wait()
4621 .expect("content mismatch is a non-authorizing record")
4622 .is_none(),
4623 "matching generation and commit marker must not authorize different frame bytes"
4624 );
4625
4626 assert_wal_corrupt(
4627 backend
4628 .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4629 .wait(),
4630 "in-doubt content-bound reconciliation mismatch",
4631 );
4632 assert_eq!(
4633 read_certificate_sidecar(&vfs, &cx),
4634 sidecar_before,
4635 "digest mismatch must be diagnosed before sidecar repair"
4636 );
4637 assert_eq!(
4638 backend.inner.frame_count(),
4639 1,
4640 "digest mismatch must preserve the live WAL for diagnosis and retry"
4641 );
4642 }
4643
4644 #[test]
4645 fn absent_commit_marker_repairs_certificate_and_partial_wal_tail() {
4646 let cx = test_cx();
4647 let vfs = MemoryVfs::new();
4648 let mut backend = make_path_refreshing_backend(&vfs, &cx);
4649 let certificate = sample_certificate(1, 1, vec![1]);
4650 backend
4651 .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4652 .expect("persist orphan certificate");
4653
4654 let (mut tail_writer, _) = vfs
4655 .open(
4656 &cx,
4657 Some(std::path::Path::new("test.db-wal")),
4658 VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
4659 )
4660 .expect("open WAL for partial-tail injection");
4661 let committed_size = tail_writer.file_size(&cx).expect("read committed WAL size");
4662 tail_writer
4663 .write(&cx, &[0xA5; 7], committed_size)
4664 .expect("inject a partial physical frame");
4665 assert!(
4666 tail_writer.file_size(&cx).expect("read extended WAL size") > committed_size,
4667 "fault fixture must extend the physical WAL"
4668 );
4669 tail_writer.close(&cx).expect("close partial-tail injector");
4670
4671 assert_eq!(
4672 backend
4673 .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4674 .wait()
4675 .expect("missing commit marker must be exactly repairable"),
4676 ParallelWalCommitReconciliation::NotCommitted
4677 );
4678 assert!(
4679 read_certificate_sidecar(&vfs, &cx).is_empty(),
4680 "matching orphan certificate must be removed after NotCommitted proof"
4681 );
4682 let (mut repaired_wal, _) = vfs
4683 .open(
4684 &cx,
4685 Some(std::path::Path::new("test.db-wal")),
4686 VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
4687 )
4688 .expect("open repaired WAL");
4689 assert_eq!(
4690 repaired_wal.file_size(&cx).expect("read repaired WAL size"),
4691 committed_size,
4692 "NotCommitted reconciliation must truncate the physical partial tail"
4693 );
4694 repaired_wal.close(&cx).expect("close repaired WAL");
4695 }
4696
4697 #[test]
4698 fn durable_certificate_recovery_accepts_every_truncated_record_prefix() {
4699 let cx = test_cx();
4700 let vfs = MemoryVfs::new();
4701 let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4702 let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4703 let orphan = sample_certificate(2, 2, vec![1]);
4704 let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4705 backend.inner.inner().generation_identity(),
4706 2,
4707 2,
4708 orphan,
4709 )
4710 .expect("construct orphan record")
4711 .to_bytes();
4712
4713 for prefix_len in 1..orphan_bytes.len() {
4714 let mut sidecar = authorized_bytes.clone();
4715 sidecar.extend_from_slice(&orphan_bytes[..prefix_len]);
4716 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4717 let recovered_result = backend
4718 .latest_authorized_parallel_wal_commit_certificate(&cx)
4719 .wait();
4720 assert!(
4721 recovered_result.is_ok(),
4722 "truncated certificate prefix of {prefix_len} bytes must recover: {recovered_result:?}"
4723 );
4724 let recovered = recovered_result
4725 .expect("truncated certificate recovery was asserted successful")
4726 .expect("authorized record must remain discoverable");
4727 assert_eq!(recovered, authorized, "failed at prefix {prefix_len}");
4728 }
4729 }
4730
4731 #[test]
4732 fn durable_certificate_append_repairs_the_accepted_torn_suffix() {
4733 let cx = test_cx();
4734 let vfs = MemoryVfs::new();
4735 let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4736 let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4737 let orphan = sample_certificate(2, 2, vec![1]);
4738 let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4739 backend.inner.inner().generation_identity(),
4740 2,
4741 2,
4742 orphan.clone(),
4743 )
4744 .expect("construct orphan record")
4745 .to_bytes();
4746 for prefix_len in 1..orphan_bytes.len() {
4747 let mut torn_sidecar = authorized_bytes.clone();
4748 torn_sidecar.extend_from_slice(&orphan_bytes[..prefix_len]);
4749 replace_certificate_sidecar(&vfs, &cx, &torn_sidecar);
4750
4751 assert_eq!(
4752 backend
4753 .latest_authorized_parallel_wal_commit_certificate(&cx)
4754 .wait()
4755 .expect("one torn suffix should recover")
4756 .expect("authorized predecessor remains visible"),
4757 authorized,
4758 "read recovery failed for prefix {prefix_len}"
4759 );
4760
4761 backend
4762 .persist_parallel_wal_commit_certificate(&cx, &orphan, 2, 2, true)
4763 .expect("next append repairs the torn suffix first");
4764 let repaired_sidecar = read_certificate_sidecar(&vfs, &cx);
4765 assert_eq!(
4766 repaired_sidecar.len(),
4767 authorized_bytes.len() + orphan_bytes.len(),
4768 "replacement record did not start at the prior complete boundary for prefix {prefix_len}"
4769 );
4770 assert_eq!(
4771 backend
4772 .latest_authorized_parallel_wal_commit_certificate(&cx)
4773 .wait()
4774 .expect("orphan lookback crosses the repaired boundary")
4775 .expect("authorized predecessor remains discoverable"),
4776 authorized,
4777 "orphan lookback failed after repairing prefix {prefix_len}"
4778 );
4779 }
4780
4781 let mut corrupt_record = orphan_bytes;
4782 let envelope_crc_offset =
4783 corrupt_record.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE - 4;
4784 corrupt_record[envelope_crc_offset] ^= 0x80;
4785 let mut corrupt_sidecar = authorized_bytes;
4786 corrupt_sidecar.extend_from_slice(&corrupt_record);
4787 replace_certificate_sidecar(&vfs, &cx, &corrupt_sidecar);
4788 assert_wal_corrupt(
4789 backend
4790 .persist_parallel_wal_commit_certificate(&cx, &orphan, 2, 2, true)
4791 .wait(),
4792 "append-time complete record corruption",
4793 );
4794 }
4795
4796 #[test]
4797 fn durable_certificate_recovery_rejects_complete_corruption_and_garbage() {
4798 let cx = test_cx();
4799 let vfs = MemoryVfs::new();
4800 let (mut backend, _) = make_authorized_certificate_backend(&vfs, &cx);
4801 let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4802 let orphan = sample_certificate(2, 2, vec![1]);
4803 let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4804 backend.inner.inner().generation_identity(),
4805 2,
4806 2,
4807 orphan,
4808 )
4809 .expect("construct orphan record")
4810 .to_bytes();
4811
4812 let mut bad_crc = orphan_bytes.clone();
4813 let envelope_crc_offset =
4814 bad_crc.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE - 4;
4815 bad_crc[envelope_crc_offset] ^= 0x80;
4816 let mut sidecar = authorized_bytes.clone();
4817 sidecar.extend_from_slice(&bad_crc);
4818 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4819 assert_wal_corrupt(
4820 backend
4821 .latest_authorized_parallel_wal_commit_certificate(&cx)
4822 .wait(),
4823 "complete record with bad CRC",
4824 );
4825
4826 let mut bad_version = orphan_bytes.clone();
4827 bad_version[8] ^= 0x01;
4828 let mut sidecar = authorized_bytes.clone();
4829 sidecar.extend_from_slice(&bad_version);
4830 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4831 assert_wal_corrupt(
4832 backend
4833 .latest_authorized_parallel_wal_commit_certificate(&cx)
4834 .wait(),
4835 "complete record with bad version",
4836 );
4837
4838 let mut bad_magic = orphan_bytes.clone();
4839 bad_magic[0] ^= 0x01;
4840 let mut sidecar = authorized_bytes.clone();
4841 sidecar.extend_from_slice(&bad_magic);
4842 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4843 assert_wal_corrupt(
4844 backend
4845 .latest_authorized_parallel_wal_commit_certificate(&cx)
4846 .wait(),
4847 "complete record with bad magic",
4848 );
4849
4850 let mut bad_footer = orphan_bytes;
4851 let footer_offset =
4852 bad_footer.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
4853 bad_footer[footer_offset] ^= 0x80;
4854 let mut sidecar = authorized_bytes;
4855 sidecar.extend_from_slice(&bad_footer);
4856 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4857 assert_wal_corrupt(
4858 backend
4859 .latest_authorized_parallel_wal_commit_certificate(&cx)
4860 .wait(),
4861 "complete record with bad footer",
4862 );
4863
4864 let garbage_vfs = MemoryVfs::new();
4865 let mut garbage_backend = make_path_refreshing_backend(&garbage_vfs, &cx);
4866 replace_certificate_sidecar(&garbage_vfs, &cx, &[0xA5; 128]);
4867 assert_wal_corrupt(
4868 garbage_backend
4869 .latest_authorized_parallel_wal_commit_certificate(&cx)
4870 .wait(),
4871 "nonempty garbage sidecar",
4872 );
4873
4874 let mut fake_magic = vec![0_u8; MIN_DURABLE_CERTIFICATE_RECORD_SIZE];
4875 fake_magic[..PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.len()]
4876 .copy_from_slice(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC);
4877 fake_magic[8..10].copy_from_slice(
4878 &fsqlite_wal::PARALLEL_WAL_DURABLE_CERTIFICATE_RECORD_VERSION.to_le_bytes(),
4879 );
4880 let fake_record_len = u32::try_from(fake_magic.len()).expect("fake record length fits u32");
4881 fake_magic[10..14].copy_from_slice(&fake_record_len.to_le_bytes());
4882 let fake_footer_offset =
4883 fake_magic.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
4884 fake_magic[fake_footer_offset..].copy_from_slice(&fake_record_len.to_le_bytes());
4885 replace_certificate_sidecar(&garbage_vfs, &cx, &fake_magic);
4886 assert_wal_corrupt(
4887 garbage_backend
4888 .latest_authorized_parallel_wal_commit_certificate(&cx)
4889 .wait(),
4890 "fake magic and length without a valid envelope",
4891 );
4892 }
4893
4894 #[test]
4895 fn durable_certificate_maximum_size_is_shared_by_writer_and_reader() {
4896 let cx = test_cx();
4897 let vfs = MemoryVfs::new();
4898 let mut backend = make_path_refreshing_backend(&vfs, &cx);
4899 let certificate = sample_certificate(1, 1, vec![1; usize::from(u16::MAX)]);
4900 let record = ParallelWalDurableCertificateRecord::new(
4901 backend.inner.inner().generation_identity(),
4902 1,
4903 1,
4904 certificate.clone(),
4905 )
4906 .expect("construct maximum-size record");
4907 assert_eq!(
4908 record.to_bytes().len(),
4909 PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
4910 );
4911 backend
4912 .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4913 .expect("writer accepts maximum-size record");
4914 assert!(
4915 backend
4916 .latest_authorized_parallel_wal_commit_certificate(&cx)
4917 .wait()
4918 .expect("reader accepts maximum-size record")
4919 .is_none(),
4920 "record remains unauthorized until its WAL commit marker exists"
4921 );
4922 }
4923
4924 #[test]
4925 fn durable_certificate_orphan_lookback_allows_exact_boundary_plus_torn_tail() {
4926 let cx = test_cx();
4927 let vfs = MemoryVfs::new();
4928 let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4929 let mut sidecar = read_certificate_sidecar(&vfs, &cx);
4930 for orphan_index in 0..MAX_ORPHAN_CERTIFICATE_LOOKBACK {
4931 let epoch = u64::try_from(orphan_index).expect("orphan index fits u64") + 2;
4932 let orphan = sample_certificate(epoch, epoch, vec![1]);
4933 sidecar.extend_from_slice(
4934 &ParallelWalDurableCertificateRecord::new(
4935 backend.inner.inner().generation_identity(),
4936 2,
4937 2,
4938 orphan,
4939 )
4940 .expect("construct bounded orphan")
4941 .to_bytes(),
4942 );
4943 }
4944 sidecar.push(0xA5);
4945 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4946 assert_eq!(
4947 backend
4948 .latest_authorized_parallel_wal_commit_certificate(&cx)
4949 .wait()
4950 .expect("64 orphans plus one torn suffix remain within bound")
4951 .expect("authorized predecessor is found"),
4952 authorized
4953 );
4954
4955 sidecar.pop();
4956 let overflow_epoch =
4957 u64::try_from(MAX_ORPHAN_CERTIFICATE_LOOKBACK).expect("lookback fits u64") + 2;
4958 let overflow = sample_certificate(overflow_epoch, overflow_epoch, vec![1]);
4959 sidecar.extend_from_slice(
4960 &ParallelWalDurableCertificateRecord::new(
4961 backend.inner.inner().generation_identity(),
4962 2,
4963 2,
4964 overflow,
4965 )
4966 .expect("construct overflow orphan")
4967 .to_bytes(),
4968 );
4969 replace_certificate_sidecar(&vfs, &cx, &sidecar);
4970 assert_wal_corrupt(
4971 backend
4972 .latest_authorized_parallel_wal_commit_certificate(&cx)
4973 .wait(),
4974 "65 unauthorized records",
4975 );
4976 }
4977
4978 #[test]
4979 fn certificate_and_handoff_fences_request_full_durability() {
4980 let cx = test_cx();
4981 let vfs = CheckpointHandoffFaultVfs::new();
4982 let (mut backend, certificate, _) = make_checkpoint_handoff_fault_backend(&vfs, &cx);
4983
4984 assert_eq!(
4985 vfs.take_sync_observations(),
4986 vec![CertificateSyncObservation::Durable(
4987 PathBuf::from(CERTIFICATE_PATH),
4988 SyncKind::FullDurable,
4989 )],
4990 "certificate append must use the strongest durability intent"
4991 );
4992
4993 assert_eq!(
4994 backend
4995 .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4996 .wait()
4997 .expect("reconcile committed certificate"),
4998 ParallelWalCommitReconciliation::Authorized
4999 );
5000 assert_eq!(
5001 vfs.take_sync_observations(),
5002 vec![CertificateSyncObservation::Durable(
5003 PathBuf::from(CERTIFICATE_PATH),
5004 SyncKind::FullDurable,
5005 )],
5006 "certificate reconciliation must preserve full durability intent"
5007 );
5008
5009 let record = backend
5010 .latest_authorized_durable_certificate_record(&cx)
5011 .wait()
5012 .expect("read authorized certificate record")
5013 .expect("authorized certificate record must exist");
5014 backend
5015 .persist_checkpoint_certificate_handoff(&cx, &record)
5016 .wait()
5017 .expect("persist checkpoint certificate handoff");
5018 assert_eq!(
5019 vfs.take_sync_observations(),
5020 vec![CertificateSyncObservation::Durable(
5021 PathBuf::from(CHECKPOINT_HANDOFF_PATH),
5022 SyncKind::FullDurable,
5023 )],
5024 "checkpoint handoff must use the strongest durability intent"
5025 );
5026 }
5027
5028 #[test]
5029 fn checkpoint_handoff_write_failure_preserves_authoritative_wal_generation() {
5030 let cx = test_cx();
5031 let vfs = CheckpointHandoffFaultVfs::new();
5032 let (mut backend, certificate, committed_page) =
5033 make_checkpoint_handoff_fault_backend(&vfs, &cx);
5034 let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5035 vfs.fail_next_handoff_write();
5036
5037 let mut checkpoint_writer = MockCheckpointPageWriter;
5038 let error = backend
5039 .checkpoint(
5040 &cx,
5041 CheckpointMode::Truncate,
5042 &mut checkpoint_writer,
5043 0,
5044 None,
5045 )
5046 .expect_err("checkpoint must fail before reset when the handoff write fails");
5047 assert!(
5048 error
5049 .to_string()
5050 .contains("injected checkpoint handoff write failure"),
5051 "unexpected handoff write error: {error}"
5052 );
5053 assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5054 }
5055
5056 #[test]
5057 fn checkpoint_handoff_durable_sync_failure_preserves_authoritative_wal_generation() {
5058 let cx = test_cx();
5059 let vfs = CheckpointHandoffFaultVfs::new();
5060 let (mut backend, certificate, committed_page) =
5061 make_checkpoint_handoff_fault_backend(&vfs, &cx);
5062 let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5063 vfs.fail_next_handoff_sync();
5064
5065 let mut checkpoint_writer = MockCheckpointPageWriter;
5066 let error = backend
5067 .checkpoint(
5068 &cx,
5069 CheckpointMode::Truncate,
5070 &mut checkpoint_writer,
5071 0,
5072 None,
5073 )
5074 .expect_err("checkpoint must fail before reset when the handoff sync fails");
5075 assert!(
5076 error
5077 .to_string()
5078 .contains("injected checkpoint handoff durable-sync failure"),
5079 "unexpected handoff durable-sync error: {error}"
5080 );
5081 assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5082 }
5083
5084 #[test]
5085 fn dropping_pending_checkpoint_handoff_write_preserves_authoritative_wal_generation() {
5086 let cx = test_cx();
5087 let vfs = CheckpointHandoffFaultVfs::new();
5088 let (mut backend, certificate, committed_page) =
5089 make_checkpoint_handoff_fault_backend(&vfs, &cx);
5090 let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5091 vfs.pend_next_handoff_write();
5092
5093 let mut checkpoint_writer = MockCheckpointPageWriter;
5094 let reached_pending_handoff = {
5095 let mut checkpoint = backend.checkpoint(
5096 &cx,
5097 CheckpointMode::Truncate,
5098 &mut checkpoint_writer,
5099 0,
5100 None,
5101 );
5102 let mut task_cx = std::task::Context::from_waker(std::task::Waker::noop());
5103 matches!(
5104 std::future::Future::poll(checkpoint.as_mut(), &mut task_cx),
5105 std::task::Poll::Pending
5106 )
5107 };
5108 assert!(
5109 reached_pending_handoff,
5110 "checkpoint should remain pending inside the injected handoff write"
5111 );
5112 assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5113 }
5114
5115 #[test]
5116 fn two_backend_instances_continue_authorized_certificate_clocks() {
5117 let cx = test_cx();
5118 let vfs = MemoryVfs::new();
5119 let wal = WalFile::create(&cx, open_wal_file(&vfs, &cx), PAGE_SIZE, 0, test_salts())
5120 .expect("create shared WAL");
5121 let mut first_backend = PathRefreshingWalBackend::new(
5122 vfs.clone(),
5123 std::path::Path::new("test.db"),
5124 std::path::Path::new("test.db-wal"),
5125 PAGE_SIZE,
5126 wal,
5127 true,
5128 #[cfg(all(feature = "native", any(unix, windows)))]
5129 None,
5130 );
5131 let request =
5132 |batch_id, wal_frame_payload_digest| fsqlite_wal::ParallelWalDurabilityRequest {
5133 trace_id: batch_id,
5134 scenario_id: "two-instance-continuity".to_owned(),
5135 certificate_epoch: 0,
5136 durable_segment_epoch: 0,
5137 batch_size: 1,
5138 batch_ids: vec![batch_id],
5139 lane_record_counts: vec![1],
5140 db_size_pages: 1,
5141 page_set_size: 1,
5142 control_mode: fsqlite_wal::ParallelWalOperatingMode::Auto,
5143 fallback_reason: None,
5144 checkpoint_active: false,
5145 wal_frame_payload_digest,
5146 };
5147
5148 let first_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5149 let first_page = sample_page(0x51);
5150 let first_receipt = first_combiner
5151 .certify_and_publish(
5152 request(1, test_frame_payload_digest(1, &first_page, 1)),
5153 |certificate| {
5154 first_backend
5155 .persist_parallel_wal_commit_certificate(&cx, certificate, 1, 1, true)
5156 .wait()
5157 .and_then(|()| first_backend.append_frame(&cx, 1, &first_page, 1).wait())
5158 .and_then(|()| first_backend.sync(&cx))
5159 .map_err(|error| error.to_string())
5160 },
5161 )
5162 .expect("first backend publishes certificate");
5163
5164 let second_wal =
5165 WalFile::open(&cx, open_wal_file(&vfs, &cx)).expect("second backend opens shared WAL");
5166 let mut second_backend = PathRefreshingWalBackend::new(
5167 vfs.clone(),
5168 std::path::Path::new("test.db"),
5169 std::path::Path::new("test.db-wal"),
5170 PAGE_SIZE,
5171 second_wal,
5172 true,
5173 #[cfg(all(feature = "native", any(unix, windows)))]
5174 None,
5175 );
5176
5177 let orphan_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5181 orphan_combiner
5182 .reconcile_authorized_seed(&first_receipt.certificate)
5183 .expect("seed orphan-producing process");
5184 let orphan_receipt = orphan_combiner
5185 .certify_and_publish(
5186 request(99, test_frame_payload_digest(1, &sample_page(0x52), 1)),
5187 |_| Ok(()),
5188 )
5189 .expect("construct deterministic orphan certificate");
5190 second_backend
5191 .persist_parallel_wal_commit_certificate(&cx, &orphan_receipt.certificate, 2, 2, true)
5192 .expect("persist well-formed orphan certificate tail");
5193 let authorized_seed = second_backend
5194 .latest_authorized_parallel_wal_commit_certificate(&cx)
5195 .expect("second backend performs bounded orphan lookback")
5196 .expect("preceding first certificate remains authorized");
5197 assert_eq!(authorized_seed, first_receipt.certificate);
5198
5199 let second_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5200 second_combiner
5201 .reconcile_authorized_seed(&authorized_seed)
5202 .expect("seed second process-local combiner");
5203 let second_page = sample_page(0x52);
5204 let second_receipt = second_combiner
5205 .certify_and_publish(
5206 request(2, test_frame_payload_digest(1, &second_page, 1)),
5207 |certificate| {
5208 second_backend
5209 .persist_parallel_wal_commit_certificate(&cx, certificate, 2, 2, true)
5210 .wait()
5211 .and_then(|()| second_backend.append_frame(&cx, 1, &second_page, 1).wait())
5212 .and_then(|()| second_backend.sync(&cx))
5213 .map_err(|error| error.to_string())
5214 },
5215 )
5216 .expect("second backend publishes certificate");
5217
5218 assert_eq!(
5219 second_receipt.certificate.commit_seq_lo.get(),
5220 first_receipt.certificate.commit_seq_hi.get() + 1
5221 );
5222 assert_eq!(
5223 second_receipt.certificate.certificate_epoch,
5224 first_receipt.certificate.certificate_epoch + 1
5225 );
5226 assert_eq!(
5227 second_receipt.certificate, orphan_receipt.certificate,
5228 "continuation may reuse an orphan identity but must not overlap any authorized certificate"
5229 );
5230 let latest = second_backend
5231 .latest_authorized_parallel_wal_commit_certificate(&cx)
5232 .expect("read second bounded authorized tail")
5233 .expect("second certificate is authorized");
5234 assert_eq!(latest, second_receipt.certificate);
5235
5236 let generation_before_checkpoint = second_backend.inner.inner().generation_identity();
5237 let mut checkpoint_writer = MockCheckpointPageWriter;
5238 let checkpoint = second_backend
5239 .checkpoint(
5240 &cx,
5241 CheckpointMode::Truncate,
5242 &mut checkpoint_writer,
5243 0,
5244 None,
5245 )
5246 .expect("truncate checkpoint records certificate clock handoff");
5247 assert!(checkpoint.wal_was_reset);
5248 assert_ne!(
5249 second_backend.inner.inner().generation_identity(),
5250 generation_before_checkpoint
5251 );
5252 let checkpoint_seed = second_backend
5253 .latest_authorized_parallel_wal_commit_certificate(&cx)
5254 .expect("read checkpoint certificate clock handoff")
5255 .expect("reset generation retains the last consumed certificate clock");
5256 assert_eq!(checkpoint_seed, second_receipt.certificate);
5257 second_backend
5258 .begin_transaction(&cx)
5259 .expect("pin reset-generation reader snapshot");
5260 let reset_pinned = second_backend
5261 .pinned_read_snapshot()
5262 .expect("reset-generation reader snapshot");
5263 assert_eq!(
5264 reset_pinned.generation,
5265 second_backend.inner.inner().generation_identity(),
5266 "reader snapshot must bind the reset WAL generation"
5267 );
5268 assert_eq!(
5269 reset_pinned.last_commit_frame, None,
5270 "truncate checkpoint leaves no current-generation commit marker"
5271 );
5272 assert_eq!(
5273 second_backend
5274 .pinned_logical_read_snapshot(&cx)
5275 .expect("inspect reset-generation reader horizon"),
5276 None,
5277 "an earlier-generation checkpoint handoff is a clock seed, never reader visibility"
5278 );
5279
5280 let post_checkpoint_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5281 post_checkpoint_combiner
5282 .reconcile_authorized_seed(&checkpoint_seed)
5283 .expect("seed fresh post-checkpoint combiner");
5284 let post_checkpoint_page = sample_page(0x53);
5285 let post_checkpoint_receipt = post_checkpoint_combiner
5286 .certify_and_publish(
5287 request(3, test_frame_payload_digest(1, &post_checkpoint_page, 1)),
5288 |certificate| {
5289 second_backend
5290 .persist_parallel_wal_commit_certificate(&cx, certificate, 1, 1, true)
5291 .wait()
5292 .and_then(|()| {
5293 second_backend
5294 .append_frame(&cx, 1, &post_checkpoint_page, 1)
5295 .wait()
5296 })
5297 .and_then(|()| second_backend.sync(&cx))
5298 .map_err(|error| error.to_string())
5299 },
5300 )
5301 .expect("publish first certificate in reset WAL generation");
5302 assert_eq!(
5303 post_checkpoint_receipt.certificate.commit_seq_lo.get(),
5304 second_receipt.certificate.commit_seq_hi.get() + 1
5305 );
5306 assert_eq!(
5307 post_checkpoint_receipt.certificate.certificate_epoch,
5308 second_receipt.certificate.certificate_epoch + 1
5309 );
5310 assert_eq!(
5311 second_backend
5312 .latest_authorized_parallel_wal_commit_certificate(&cx)
5313 .expect("read post-checkpoint current-generation certificate")
5314 .expect("post-checkpoint certificate is authorized"),
5315 post_checkpoint_receipt.certificate
5316 );
5317 second_backend
5318 .begin_transaction(&cx)
5319 .expect("pin post-checkpoint reader snapshot");
5320 let pinned = second_backend
5321 .pinned_read_snapshot()
5322 .expect("post-checkpoint reader snapshot");
5323 let logical = second_backend
5324 .pinned_logical_read_snapshot(&cx)
5325 .expect("inspect post-checkpoint reader horizon")
5326 .expect("current-generation certificate exposes a reader horizon");
5327 assert_eq!(logical.generation, pinned.generation);
5328 assert_eq!(logical.last_commit_frame, pinned.last_commit_frame);
5329 assert_eq!(
5330 logical.visible_commit_seq,
5331 post_checkpoint_receipt.certificate.commit_seq_hi
5332 );
5333 }
5334
5335 #[test]
5336 fn pinned_logical_reader_horizon_counts_physical_tail_after_current_certificate() {
5337 let cx = test_cx();
5338 let vfs = MemoryVfs::new();
5339 let (mut backend, certificate) = make_authorized_certificate_backend(&vfs, &cx);
5340
5341 backend
5342 .begin_transaction(&cx)
5343 .expect("pin certificate reader snapshot");
5344 let initial_pinned = backend
5345 .pinned_read_snapshot()
5346 .expect("initial reader snapshot");
5347 let initial_logical = backend
5348 .pinned_logical_read_snapshot(&cx)
5349 .expect("inspect certificate reader horizon")
5350 .expect("current certificate exposes reader horizon");
5351 assert_eq!(initial_logical.generation, initial_pinned.generation);
5352 assert_eq!(
5353 initial_logical.last_commit_frame,
5354 initial_pinned.last_commit_frame
5355 );
5356 assert_eq!(
5357 initial_logical.visible_commit_seq, certificate.commit_seq_hi,
5358 "certificate horizon is exact when no later physical commit exists"
5359 );
5360
5361 let tail_page = sample_page(0x45);
5362 backend
5363 .append_frame(&cx, 2, &tail_page, 2)
5364 .expect("append later ordinary commit marker");
5365 backend
5366 .sync(&cx)
5367 .expect("sync later ordinary commit marker");
5368 backend
5369 .begin_transaction(&cx)
5370 .expect("repin reader after ordinary tail commit");
5371 let pinned = backend
5372 .pinned_read_snapshot()
5373 .expect("reader snapshot includes ordinary tail commit");
5374 let logical = backend
5375 .pinned_logical_read_snapshot(&cx)
5376 .expect("inspect reader horizon with ordinary tail")
5377 .expect("current certificate remains reader-authoritative");
5378 assert_eq!(logical.generation, pinned.generation);
5379 assert_eq!(logical.last_commit_frame, pinned.last_commit_frame);
5380 assert_eq!(
5381 logical.visible_commit_seq.get(),
5382 certificate.commit_seq_hi.get() + 1
5383 );
5384 }
5385
5386 fn open_wal_file(vfs: &MemoryVfs, cx: &Cx) -> <MemoryVfs as Vfs>::File {
5387 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5388 let (file, _) = vfs
5389 .open(cx, Some(std::path::Path::new("test.db-wal")), flags)
5390 .expect("open WAL file");
5391 file
5392 }
5393
5394 fn make_adapter(vfs: &MemoryVfs, cx: &Cx) -> WalBackendAdapter<<MemoryVfs as Vfs>::File> {
5395 let file = open_wal_file(vfs, cx);
5396 let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5397 WalBackendAdapter::new(wal)
5398 }
5399
5400 fn make_fault_adapter(
5402 vfs: &CheckpointHandoffFaultVfs,
5403 cx: &Cx,
5404 ) -> WalBackendAdapter<<CheckpointHandoffFaultVfs as Vfs>::File> {
5405 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5406 let (file, _) = vfs
5407 .open(cx, Some(std::path::Path::new("test.db-wal")), flags)
5408 .expect("open fault WAL file");
5409 let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create fault WAL");
5410 WalBackendAdapter::new(wal)
5411 }
5412
5413 #[test]
5416 fn test_adapter_append_and_frame_count() {
5417 let cx = test_cx();
5418 let vfs = MemoryVfs::new();
5419 let mut adapter = make_adapter(&vfs, &cx);
5420
5421 assert_eq!(adapter.frame_count(), 0);
5422
5423 let page = sample_page(0x42);
5424 adapter
5425 .append_frame(&cx, 1, &page, 0)
5426 .expect("append frame");
5427 assert_eq!(adapter.frame_count(), 1);
5428
5429 adapter
5430 .append_frame(&cx, 2, &sample_page(0x43), 2)
5431 .expect("append commit frame");
5432 assert_eq!(adapter.frame_count(), 2);
5433 }
5434
5435 #[test]
5436 fn test_adapter_read_page_found() {
5437 let cx = test_cx();
5438 let vfs = MemoryVfs::new();
5439 let mut adapter = make_adapter(&vfs, &cx);
5440
5441 let page1 = sample_page(0x10);
5442 let page2 = sample_page(0x20);
5443 adapter.append_frame(&cx, 1, &page1, 0).expect("append");
5444 adapter
5445 .append_frame(&cx, 2, &page2, 2)
5446 .expect("append commit");
5447
5448 let result = adapter.read_page(&cx, 1).expect("read page 1");
5449 assert_eq!(result, Some(page1));
5450
5451 let result = adapter.read_page(&cx, 2).expect("read page 2");
5452 assert_eq!(result, Some(page2));
5453 }
5454
5455 #[test]
5456 fn test_adapter_read_page_not_found() {
5457 let cx = test_cx();
5458 let vfs = MemoryVfs::new();
5459 let mut adapter = make_adapter(&vfs, &cx);
5460
5461 adapter
5462 .append_frame(&cx, 1, &sample_page(0x10), 1)
5463 .expect("append");
5464
5465 let result = adapter.read_page(&cx, 99).expect("read missing page");
5466 assert_eq!(result, None);
5467 }
5468
5469 #[test]
5470 fn test_adapter_read_page_returns_latest_version() {
5471 let cx = test_cx();
5472 let vfs = MemoryVfs::new();
5473 let mut adapter = make_adapter(&vfs, &cx);
5474
5475 let old_data = sample_page(0xAA);
5476 let new_data = sample_page(0xBB);
5477
5478 adapter
5480 .append_frame(&cx, 5, &old_data, 0)
5481 .expect("append old");
5482 adapter
5483 .append_frame(&cx, 5, &new_data, 1)
5484 .expect("append new (commit)");
5485
5486 let result = adapter.read_page(&cx, 5).expect("read page 5");
5487 assert_eq!(
5488 result,
5489 Some(new_data),
5490 "adapter should return the latest WAL version"
5491 );
5492 }
5493
5494 #[test]
5495 fn test_adapter_refreshes_cross_handle_visibility_and_append_position() {
5496 let cx = test_cx();
5497 let vfs = MemoryVfs::new();
5498
5499 let file1 = open_wal_file(&vfs, &cx);
5500 let wal1 = WalFile::create(&cx, file1, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5501 let mut adapter1 = WalBackendAdapter::new(wal1);
5502
5503 let file2 = open_wal_file(&vfs, &cx);
5504 let wal2 = WalFile::open(&cx, file2).expect("open WAL");
5505 let mut adapter2 = WalBackendAdapter::new(wal2);
5506
5507 let page1 = sample_page(0x11);
5508 adapter1
5509 .append_frame(&cx, 1, &page1, 1)
5510 .expect("adapter1 append commit");
5511 adapter1.sync(&cx).expect("adapter1 sync");
5512 adapter2
5513 .begin_transaction(&cx)
5514 .expect("adapter2 begin transaction");
5515 assert_eq!(
5516 adapter2.read_page(&cx, 1).expect("adapter2 read page1"),
5517 Some(page1.clone()),
5518 "adapter2 should observe adapter1 commit at transaction begin"
5519 );
5520
5521 let page2 = sample_page(0x22);
5522 adapter2
5523 .append_frame(&cx, 2, &page2, 2)
5524 .expect("adapter2 append commit");
5525 adapter2.sync(&cx).expect("adapter2 sync");
5526 adapter1
5527 .begin_transaction(&cx)
5528 .expect("adapter1 begin transaction");
5529 assert_eq!(
5530 adapter1.read_page(&cx, 2).expect("adapter1 read page2"),
5531 Some(page2.clone()),
5532 "adapter1 should observe adapter2 commit at transaction begin"
5533 );
5534
5535 assert_eq!(
5537 adapter1.frame_count(),
5538 2,
5539 "shared WAL should contain both commit frames"
5540 );
5541 assert_eq!(
5542 adapter2.frame_count(),
5543 2,
5544 "shared WAL should contain both commit frames"
5545 );
5546 }
5547
5548 #[test]
5549 fn test_path_refresh_rejects_replacement_wal_page_size_mismatch() {
5550 let cx = test_cx();
5551 let vfs = MemoryVfs::new();
5552 let wal_path = std::path::Path::new("test.db-wal");
5553
5554 let file = open_wal_file(&vfs, &cx);
5555 let wal = WalFile::create(&cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5556 let mut backend = PathRefreshingWalBackend::new(
5557 vfs.clone(),
5558 std::path::Path::new("test.db"),
5559 wal_path,
5560 PAGE_SIZE,
5561 wal,
5562 true,
5563 #[cfg(all(feature = "native", any(unix, windows)))]
5564 None,
5565 );
5566
5567 backend
5568 .append_frame(&cx, 1, &sample_page(0x31), 1)
5569 .expect("append through live backend");
5570 backend.sync(&cx).expect("sync live backend");
5571
5572 vfs.delete(&cx, wal_path, false)
5573 .expect("remove path-visible WAL");
5574 let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5575 let (replacement_file, _) = vfs
5576 .open(&cx, Some(wal_path), flags)
5577 .expect("open replacement WAL path");
5578 let replacement_page_size = PAGE_SIZE
5579 .checked_mul(2)
5580 .expect("test replacement page size fits u32");
5581 let replacement_wal = WalFile::create(
5582 &cx,
5583 replacement_file,
5584 replacement_page_size,
5585 0,
5586 test_salts(),
5587 )
5588 .expect("create mismatched replacement WAL");
5589 replacement_wal.close(&cx).expect("close replacement WAL");
5590
5591 let err = backend
5592 .begin_transaction(&cx)
5593 .expect_err("path refresh should reject mismatched WAL page size");
5594 assert!(
5595 matches!(
5596 err,
5597 FrankenError::WalCorrupt { ref detail }
5598 if detail.contains("does not match database page size")
5599 && detail.contains("during path refresh")
5600 ),
5601 "unexpected error: {err:?}"
5602 );
5603 }
5604
5605 #[test]
5606 fn test_generation_change_allows_identical_full_page_baseline() {
5607 let cx = test_cx();
5608 let vfs = MemoryVfs::new();
5609 let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5610 let baseline = TransactionConflictPageBaseline {
5611 page_number: 2,
5612 page_hash: *blake3::hash(&page_two).as_bytes(),
5613 };
5614
5615 let conflicts = backend
5616 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5617 .expect("validate checkpoint-only generation transition");
5618 assert!(
5619 conflicts.is_empty(),
5620 "byte-identical checkpoint-only reset must not create a false conflict"
5621 );
5622 }
5623
5624 #[test]
5625 fn test_generation_change_rejects_changed_candidate_page() {
5626 let cx = test_cx();
5627 let vfs = MemoryVfs::new();
5628 let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5629 let changed_page_two = sample_page(0x33);
5630 write_main_db_pages(
5631 &vfs,
5632 &cx,
5633 &[
5634 sqlite_page_one(u16::try_from(PAGE_SIZE).expect("page size fits u16")),
5635 changed_page_two,
5636 ],
5637 );
5638 let baseline = TransactionConflictPageBaseline {
5639 page_number: 2,
5640 page_hash: *blake3::hash(&page_two).as_bytes(),
5641 };
5642
5643 let conflicts = backend
5644 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5645 .expect("validate changed page across generation transition");
5646 assert_eq!(conflicts, vec![2]);
5647 }
5648
5649 #[test]
5650 fn test_generation_change_rejects_changed_candidate_from_replacement_wal() {
5651 let cx = test_cx();
5652 let vfs = MemoryVfs::new();
5653 let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5654 append_replacement_wal_page(&vfs, &cx, 2, &sample_page(0x44), 2);
5655 let baseline = TransactionConflictPageBaseline {
5656 page_number: 2,
5657 page_hash: *blake3::hash(&page_two).as_bytes(),
5658 };
5659
5660 let conflicts = backend
5661 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5662 .expect("replacement WAL page must take precedence over identical main page");
5663 assert_eq!(conflicts, vec![2]);
5664 }
5665
5666 #[test]
5667 fn test_generation_change_rejects_missing_baseline() {
5668 let cx = test_cx();
5669 let vfs = MemoryVfs::new();
5670 let (mut backend, snapshot, _) = make_generation_transition_backend(&vfs, &cx);
5671
5672 let conflicts = backend
5673 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[])
5674 .expect("missing baseline must fail closed");
5675 assert_eq!(conflicts, vec![2]);
5676 }
5677
5678 #[test]
5679 fn test_generation_change_rejects_conflicting_duplicate_baselines() {
5680 let cx = test_cx();
5681 let vfs = MemoryVfs::new();
5682 let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5683 let baselines = [
5684 TransactionConflictPageBaseline {
5685 page_number: 2,
5686 page_hash: *blake3::hash(&page_two).as_bytes(),
5687 },
5688 TransactionConflictPageBaseline {
5689 page_number: 2,
5690 page_hash: *blake3::hash(&sample_page(0x55)).as_bytes(),
5691 },
5692 ];
5693
5694 let conflicts = backend
5695 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &baselines)
5696 .expect("conflicting duplicate baselines must fail closed");
5697 assert_eq!(conflicts, vec![2]);
5698 }
5699
5700 #[test]
5701 fn test_generation_change_rejects_short_candidate_page() {
5702 let cx = test_cx();
5703 let vfs = MemoryVfs::new();
5704 let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5705 write_main_db_pages(
5706 &vfs,
5707 &cx,
5708 &[sqlite_page_one(
5709 u16::try_from(PAGE_SIZE).expect("page size fits u16"),
5710 )],
5711 );
5712 let baseline = TransactionConflictPageBaseline {
5713 page_number: 2,
5714 page_hash: *blake3::hash(&page_two).as_bytes(),
5715 };
5716
5717 let conflicts = backend
5718 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5719 .expect("short page must fail closed");
5720 assert_eq!(conflicts, vec![2]);
5721 }
5722
5723 #[test]
5724 fn test_generation_change_rejects_database_page_size_change() {
5725 let cx = test_cx();
5726 let vfs = MemoryVfs::new();
5727 let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5728 write_main_db_pages(&vfs, &cx, &[sqlite_page_one(8192), page_two.clone()]);
5729 let baseline = TransactionConflictPageBaseline {
5730 page_number: 2,
5731 page_hash: *blake3::hash(&page_two).as_bytes(),
5732 };
5733
5734 let conflicts = backend
5735 .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5736 .expect("page-size change must fail closed");
5737 assert_eq!(conflicts, vec![2]);
5738 }
5739
5740 #[test]
5741 fn test_generation_change_decodes_64k_database_header_sentinel() {
5742 assert_eq!(
5743 sqlite_database_header_page_size(&sqlite_page_one(1)),
5744 Some(65_536)
5745 );
5746 }
5747
5748 #[test]
5749 fn test_adapter_batch_append_checksum_chain_matches_single_append() {
5750 let cx = test_cx();
5751 let vfs_single = MemoryVfs::new();
5752 let vfs_batch = MemoryVfs::new();
5753
5754 let mut adapter_single = make_adapter(&vfs_single, &cx);
5755 let mut adapter_batch = make_adapter(&vfs_batch, &cx);
5756
5757 let pages: Vec<Vec<u8>> = (0..4u8).map(sample_page).collect();
5758 let commit_sizes = [0_u32, 0, 0, 4];
5759
5760 for (index, page) in pages.iter().enumerate() {
5761 adapter_single
5762 .append_frame(
5763 &cx,
5764 u32::try_from(index + 1).expect("page number fits u32"),
5765 page,
5766 commit_sizes[index],
5767 )
5768 .expect("single append");
5769 }
5770
5771 let batch_frames: Vec<_> = pages
5772 .iter()
5773 .enumerate()
5774 .map(|(index, page)| WalFrameRef {
5775 page_number: u32::try_from(index + 1).expect("page number fits u32"),
5776 page_data: page,
5777 db_size_if_commit: commit_sizes[index],
5778 })
5779 .collect();
5780 adapter_batch
5781 .append_frames(&cx, &batch_frames)
5782 .expect("batch append");
5783
5784 assert_eq!(
5785 adapter_single.frame_count(),
5786 adapter_batch.frame_count(),
5787 "batch adapter append must preserve frame count"
5788 );
5789 assert_eq!(
5790 adapter_single.wal.running_checksum(),
5791 adapter_batch.wal.running_checksum(),
5792 "batch adapter append must preserve checksum chain"
5793 );
5794
5795 for frame_index in 0..pages.len() {
5796 let (single_header, single_data) = adapter_single
5797 .wal
5798 .read_frame(&cx, frame_index)
5799 .expect("read single frame");
5800 let (batch_header, batch_data) = adapter_batch
5801 .wal
5802 .read_frame(&cx, frame_index)
5803 .expect("read batch frame");
5804 assert_eq!(
5805 single_header, batch_header,
5806 "frame header {frame_index} must match"
5807 );
5808 assert_eq!(
5809 single_data, batch_data,
5810 "frame payload {frame_index} must match"
5811 );
5812 }
5813 }
5814
5815 #[test]
5816 fn test_adapter_prepared_batch_append_checksum_chain_matches_single_append() {
5817 let cx = test_cx();
5818 let vfs_single = MemoryVfs::new();
5819 let vfs_prepared = MemoryVfs::new();
5820
5821 let mut adapter_single = make_adapter(&vfs_single, &cx);
5822 let mut adapter_prepared = make_adapter(&vfs_prepared, &cx);
5823
5824 let pages: Vec<Vec<u8>> = (0..4u8).map(sample_page).collect();
5825 let commit_sizes = [0_u32, 0, 0, 4];
5826
5827 for (index, page) in pages.iter().enumerate() {
5828 adapter_single
5829 .append_frame(
5830 &cx,
5831 u32::try_from(index + 1).expect("page number fits u32"),
5832 page,
5833 commit_sizes[index],
5834 )
5835 .expect("single append");
5836 }
5837
5838 let batch_frames: Vec<_> = pages
5839 .iter()
5840 .enumerate()
5841 .map(|(index, page)| WalFrameRef {
5842 page_number: u32::try_from(index + 1).expect("page number fits u32"),
5843 page_data: page,
5844 db_size_if_commit: commit_sizes[index],
5845 })
5846 .collect();
5847 let mut prepared = adapter_prepared
5848 .prepare_append_frames(&batch_frames)
5849 .expect("prepare append")
5850 .expect("prepared batch");
5851 adapter_prepared
5852 .append_prepared_frames(&cx, &mut prepared)
5853 .expect("append prepared");
5854
5855 assert_eq!(
5856 adapter_single.frame_count(),
5857 adapter_prepared.frame_count(),
5858 "prepared adapter append must preserve frame count"
5859 );
5860 assert_eq!(
5861 adapter_single.wal.running_checksum(),
5862 adapter_prepared.wal.running_checksum(),
5863 "prepared adapter append must preserve checksum chain"
5864 );
5865
5866 for frame_index in 0..pages.len() {
5867 let (single_header, single_data) = adapter_single
5868 .wal
5869 .read_frame(&cx, frame_index)
5870 .expect("read single frame");
5871 let (prepared_header, prepared_data) = adapter_prepared
5872 .wal
5873 .read_frame(&cx, frame_index)
5874 .expect("read prepared frame");
5875 assert_eq!(
5876 single_header, prepared_header,
5877 "frame header {frame_index} must match"
5878 );
5879 assert_eq!(
5880 single_data, prepared_data,
5881 "frame payload {frame_index} must match"
5882 );
5883 }
5884 }
5885
5886 #[test]
5887 fn test_adapter_pre_finalize_reused_when_append_window_is_stable() {
5888 let cx = test_cx();
5889 let vfs_single = MemoryVfs::new();
5890 let vfs_prepared = MemoryVfs::new();
5891
5892 let mut adapter_single = make_adapter(&vfs_single, &cx);
5893 let mut adapter_prepared = make_adapter(&vfs_prepared, &cx);
5894
5895 let pages: Vec<Vec<u8>> = (0..3u8).map(sample_page).collect();
5896 let commit_sizes = [0_u32, 0, 3];
5897
5898 for (index, page) in pages.iter().enumerate() {
5899 adapter_single
5900 .append_frame(
5901 &cx,
5902 u32::try_from(index + 1).expect("page number fits u32"),
5903 page,
5904 commit_sizes[index],
5905 )
5906 .expect("single append");
5907 }
5908
5909 let batch_frames: Vec<_> = pages
5910 .iter()
5911 .enumerate()
5912 .map(|(index, page)| WalFrameRef {
5913 page_number: u32::try_from(index + 1).expect("page number fits u32"),
5914 page_data: page,
5915 db_size_if_commit: commit_sizes[index],
5916 })
5917 .collect();
5918 let mut prepared = adapter_prepared
5919 .prepare_append_frames(&batch_frames)
5920 .expect("prepare append")
5921 .expect("prepared batch");
5922 adapter_prepared
5923 .finalize_prepared_frames(&cx, &mut prepared)
5924 .expect("pre-finalize prepared batch");
5925 let finalized_for = prepared.finalized_for.expect("finalization state");
5926 let finalized_running_checksum = prepared
5927 .finalized_running_checksum
5928 .expect("finalized checksum");
5929
5930 adapter_prepared
5931 .append_prepared_frames(&cx, &mut prepared)
5932 .expect("append prepared");
5933
5934 assert_eq!(
5935 prepared.finalized_for,
5936 Some(finalized_for),
5937 "stable append window should reuse the pre-lock finalization state"
5938 );
5939 assert_eq!(
5940 prepared.finalized_running_checksum,
5941 Some(finalized_running_checksum),
5942 "stable append window should reuse the pre-lock finalized checksum"
5943 );
5944 assert_eq!(
5945 adapter_single.wal.running_checksum(),
5946 adapter_prepared.wal.running_checksum(),
5947 "stable reuse path must preserve checksum chain"
5948 );
5949 }
5950
5951 #[test]
5952 fn test_adapter_pre_finalize_reseeds_after_intervening_external_append() {
5953 let cx = test_cx();
5954 let baseline_vfs = MemoryVfs::new();
5955 let shared_vfs = MemoryVfs::new();
5956
5957 let mut baseline = make_adapter(&baseline_vfs, &cx);
5958 let mut prepared_writer = make_adapter(&shared_vfs, &cx);
5959 let intruder_file = open_wal_file(&shared_vfs, &cx);
5960 let intruder_wal = WalFile::open(&cx, intruder_file).expect("open shared WAL");
5961 let mut intruder = WalBackendAdapter::new(intruder_wal);
5962
5963 let pages: Vec<Vec<u8>> = (0..3u8).map(sample_page).collect();
5964 let commit_sizes = [0_u32, 0, 3];
5965 let intruder_page = sample_page(0xEE);
5966
5967 baseline
5968 .append_frame(&cx, 99, &intruder_page, 1)
5969 .expect("baseline intruder append");
5970 for (index, page) in pages.iter().enumerate() {
5971 baseline
5972 .append_frame(
5973 &cx,
5974 u32::try_from(index + 1).expect("page number fits u32"),
5975 page,
5976 commit_sizes[index],
5977 )
5978 .expect("baseline append");
5979 }
5980
5981 let batch_frames: Vec<_> = pages
5982 .iter()
5983 .enumerate()
5984 .map(|(index, page)| WalFrameRef {
5985 page_number: u32::try_from(index + 1).expect("page number fits u32"),
5986 page_data: page,
5987 db_size_if_commit: commit_sizes[index],
5988 })
5989 .collect();
5990 let mut prepared = prepared_writer
5991 .prepare_append_frames(&batch_frames)
5992 .expect("prepare append")
5993 .expect("prepared batch");
5994 prepared_writer
5995 .finalize_prepared_frames(&cx, &mut prepared)
5996 .expect("pre-finalize prepared batch");
5997 let stale_finalization_state = prepared.finalized_for;
5998
5999 intruder
6000 .append_frame(&cx, 99, &intruder_page, 1)
6001 .expect("intruder append");
6002 intruder.sync(&cx).expect("intruder sync");
6003
6004 prepared_writer
6005 .append_prepared_frames(&cx, &mut prepared)
6006 .expect("append prepared after external growth");
6007
6008 assert_ne!(
6009 prepared.finalized_for, stale_finalization_state,
6010 "intervening external growth should force prepared batch reseeding"
6011 );
6012 assert_eq!(
6013 baseline.wal.running_checksum(),
6014 prepared_writer.wal.running_checksum(),
6015 "reseeding path must preserve checksum chain"
6016 );
6017 assert_eq!(
6018 baseline.frame_count(),
6019 prepared_writer.frame_count(),
6020 "reseeding path must preserve frame count"
6021 );
6022 }
6023
6024 #[test]
6025 fn test_adapter_pins_read_snapshot_until_next_begin() {
6026 init_wal_publication_test_tracing();
6027 let cx = test_cx();
6028 let vfs = MemoryVfs::new();
6029
6030 let file_writer = open_wal_file(&vfs, &cx);
6031 let wal_writer =
6032 WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6033 let mut writer = WalBackendAdapter::new(wal_writer);
6034
6035 let file_reader = open_wal_file(&vfs, &cx);
6036 let wal_reader = WalFile::open(&cx, file_reader).expect("open WAL");
6037 let mut reader = WalBackendAdapter::new(wal_reader);
6038
6039 let v1 = sample_page(0x41);
6040 writer.append_frame(&cx, 3, &v1, 3).expect("append v1");
6041 writer.sync(&cx).expect("sync v1");
6042
6043 reader
6044 .begin_transaction(&cx)
6045 .expect("begin reader snapshot 1");
6046 let pinned_v1 = reader
6047 .pinned_read_snapshot()
6048 .expect("reader pins publication snapshot");
6049 assert_eq!(pinned_v1.last_commit_frame, Some(0));
6050 assert_eq!(pinned_v1.commit_count, 1);
6051 assert_eq!(pinned_v1.latest_frame_entries, 1);
6052 assert!(pinned_v1.lookup_contract_is_authoritative());
6053 assert_eq!(
6054 reader.read_page(&cx, 3).expect("reader sees v1"),
6055 Some(v1.clone())
6056 );
6057
6058 let v2 = sample_page(0x42);
6059 writer.append_frame(&cx, 3, &v2, 3).expect("append v2");
6060 writer.sync(&cx).expect("sync v2");
6061
6062 assert_eq!(
6064 reader
6065 .read_page(&cx, 3)
6066 .expect("reader remains on pinned snapshot"),
6067 Some(v1.clone())
6068 );
6069 assert_eq!(
6070 reader
6071 .pinned_read_snapshot()
6072 .expect("reader keeps the same pinned snapshot"),
6073 pinned_v1,
6074 "pinned publication metadata must stay stable until the next begin"
6075 );
6076
6077 reader
6079 .begin_transaction(&cx)
6080 .expect("begin reader snapshot 2");
6081 let pinned_v2 = reader
6082 .pinned_read_snapshot()
6083 .expect("reader repins publication snapshot");
6084 assert!(pinned_v2.publication_seq > pinned_v1.publication_seq);
6085 assert_eq!(pinned_v2.commit_count, 2);
6086 assert_eq!(pinned_v2.latest_frame_entries, 1);
6087 assert_eq!(reader.read_page(&cx, 3).expect("reader sees v2"), Some(v2));
6088 }
6089
6090 #[test]
6091 fn test_adapter_read_page_hides_uncommitted_frames() {
6092 let cx = test_cx();
6093 let vfs = MemoryVfs::new();
6094 let mut adapter = make_adapter(&vfs, &cx);
6095
6096 let committed = sample_page(0x31);
6097 let uncommitted = sample_page(0x32);
6098
6099 adapter
6100 .append_frame(&cx, 7, &committed, 7)
6101 .expect("append committed frame");
6102 adapter
6103 .append_frame(&cx, 7, &uncommitted, 0)
6104 .expect("append uncommitted frame");
6105
6106 let result = adapter.read_page(&cx, 7).expect("read committed page");
6107 assert_eq!(
6108 result,
6109 Some(committed),
6110 "reader must ignore uncommitted tail frames"
6111 );
6112 }
6113
6114 #[test]
6115 fn test_adapter_read_page_none_when_wal_has_no_commit_frame() {
6116 let cx = test_cx();
6117 let vfs = MemoryVfs::new();
6118 let mut adapter = make_adapter(&vfs, &cx);
6119
6120 adapter
6121 .append_frame(&cx, 3, &sample_page(0x44), 0)
6122 .expect("append uncommitted frame");
6123
6124 let result = adapter.read_page(&cx, 3).expect("read page");
6125 assert_eq!(result, None, "uncommitted WAL frames must stay invisible");
6126 }
6127
6128 #[test]
6129 fn test_adapter_read_page_empty_wal() {
6130 let cx = test_cx();
6131 let vfs = MemoryVfs::new();
6132 let mut adapter = make_adapter(&vfs, &cx);
6133
6134 let result = adapter.read_page(&cx, 1).expect("read from empty WAL");
6135 assert_eq!(result, None);
6136 }
6137
6138 #[test]
6139 fn test_adapter_sync() {
6140 let cx = test_cx();
6141 let vfs = MemoryVfs::new();
6142 let mut adapter = make_adapter(&vfs, &cx);
6143
6144 adapter
6145 .append_frame(&cx, 1, &sample_page(0), 1)
6146 .expect("append");
6147 adapter.sync(&cx).expect("sync should not fail");
6148 }
6149
6150 #[test]
6151 fn test_adapter_into_inner_fails_closed_until_sync() {
6152 let cx = test_cx();
6153 let staged_vfs = MemoryVfs::new();
6154 let mut staged = make_adapter(&staged_vfs, &cx);
6155
6156 staged
6157 .append_frame(&cx, 1, &sample_page(0), 1)
6158 .expect("append");
6159 assert!(
6160 matches!(staged.into_inner(), Err(FrankenError::Busy)),
6161 "an unsynced commit must prevent consuming the adapter"
6162 );
6163
6164 let synced_vfs = MemoryVfs::new();
6165 let mut synced = make_adapter(&synced_vfs, &cx);
6166 synced
6167 .append_frame(&cx, 1, &sample_page(0), 1)
6168 .expect("append");
6169 synced.sync(&cx).expect("sync staged commit");
6170
6171 assert_eq!(synced.inner().frame_count(), 1);
6172
6173 let wal = synced.into_inner().expect("sync drained the staged frames");
6174 assert_eq!(wal.frame_count(), 1);
6175 }
6176
6177 #[test]
6178 fn test_adapter_as_dyn_wal_backend() {
6179 let cx = test_cx();
6180 let vfs = MemoryVfs::new();
6181 let mut adapter = make_adapter(&vfs, &cx);
6182
6183 let backend: &mut dyn WalBackend = &mut adapter;
6185 backend
6186 .append_frame(&cx, 1, &sample_page(0x77), 1)
6187 .expect("append via dyn");
6188 assert_eq!(backend.frame_count(), 1);
6189
6190 let page = backend.read_page(&cx, 1).expect("read via dyn");
6191 assert_eq!(page, Some(sample_page(0x77)));
6192 }
6193
6194 #[test]
6195 fn test_publication_snapshots_are_visible_through_wal_backend_trait() {
6196 init_wal_publication_test_tracing();
6197 let cx = test_cx();
6198 let vfs = MemoryVfs::new();
6199
6200 let file_writer = open_wal_file(&vfs, &cx);
6201 let wal_writer =
6202 WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6203 let mut writer = WalBackendAdapter::new(wal_writer);
6204
6205 writer
6206 .append_frame(&cx, 4, &sample_page(0x84), 4)
6207 .expect("append committed frame");
6208 writer.sync(&cx).expect("sync committed frame");
6209
6210 let file_reader = open_wal_file(&vfs, &cx);
6211 let wal_reader = WalFile::open(&cx, file_reader).expect("open WAL");
6212 let mut reader = WalBackendAdapter::new(wal_reader);
6213 let backend: &mut dyn WalBackend = &mut reader;
6214
6215 let published_before = backend
6216 .published_snapshot()
6217 .expect("trait should expose the adapter publication summary");
6218 assert_eq!(published_before.last_commit_frame, None);
6219 assert_eq!(published_before.commit_count, 0);
6220
6221 let refreshed = backend
6222 .refresh_published_snapshot(&cx)
6223 .expect("refresh through trait should succeed")
6224 .expect("adapter should republish an existing committed prefix");
6225 assert_eq!(refreshed.last_commit_frame, Some(0));
6226 assert_eq!(refreshed.commit_count, 1);
6227 assert_eq!(refreshed.latest_frame_entries, 1);
6228
6229 backend
6230 .begin_transaction(&cx)
6231 .expect("begin_transaction through trait should pin snapshot");
6232 let pinned = backend
6233 .pinned_read_snapshot()
6234 .expect("trait should expose the pinned read snapshot");
6235 assert_eq!(pinned, refreshed);
6236 }
6237
6238 #[test]
6241 fn test_page_index_returns_correct_data() {
6242 let cx = test_cx();
6244 let vfs = MemoryVfs::new();
6245 let mut adapter = make_adapter(&vfs, &cx);
6246
6247 let page1 = sample_page(0x01);
6248 let page2 = sample_page(0x02);
6249 let page3 = sample_page(0x03);
6250
6251 adapter.append_frame(&cx, 1, &page1, 0).expect("append");
6252 adapter.append_frame(&cx, 2, &page2, 0).expect("append");
6253 adapter
6254 .append_frame(&cx, 3, &page3, 3)
6255 .expect("append commit");
6256
6257 assert_eq!(adapter.read_page(&cx, 1).expect("read"), Some(page1));
6259 assert_eq!(adapter.read_page(&cx, 2).expect("read"), Some(page2));
6260 assert_eq!(adapter.read_page(&cx, 3).expect("read"), Some(page3));
6261
6262 assert_eq!(adapter.read_page(&cx, 99).expect("read"), None);
6264 }
6265
6266 #[test]
6267 fn test_page_index_returns_latest_version() {
6268 let cx = test_cx();
6270 let vfs = MemoryVfs::new();
6271 let mut adapter = make_adapter(&vfs, &cx);
6272
6273 let old_data = sample_page(0xAA);
6274 let new_data = sample_page(0xBB);
6275
6276 adapter
6277 .append_frame(&cx, 5, &old_data, 0)
6278 .expect("append old");
6279 adapter
6280 .append_frame(&cx, 5, &new_data, 1)
6281 .expect("append new (commit)");
6282
6283 assert_eq!(
6284 adapter.read_page(&cx, 5).expect("read"),
6285 Some(new_data),
6286 "page index must return the latest frame for a page"
6287 );
6288 }
6289
6290 #[test]
6291 fn test_page_index_invalidated_on_wal_reset() {
6292 let cx = test_cx();
6295 let vfs = MemoryVfs::new();
6296 let mut adapter = make_adapter(&vfs, &cx);
6297
6298 let old_data = sample_page(0x11);
6299 adapter
6300 .append_frame(&cx, 1, &old_data, 1)
6301 .expect("append commit");
6302
6303 assert_eq!(adapter.read_page(&cx, 1).expect("read old"), Some(old_data));
6305
6306 let new_salts = WalSalts {
6308 salt1: 0xAAAA_BBBB,
6309 salt2: 0xCCCC_DDDD,
6310 };
6311 adapter
6312 .inner_mut()
6313 .expect("no staged batch blocks inner access")
6314 .reset(&cx, 1, new_salts, false)
6315 .expect("WAL reset");
6316
6317 let new_data = sample_page(0x22);
6319 adapter
6320 .append_frame(&cx, 1, &new_data, 1)
6321 .expect("append new generation commit");
6322
6323 let result = adapter.read_page(&cx, 1).expect("read after reset");
6325 assert_eq!(
6326 result,
6327 Some(new_data),
6328 "after WAL reset, page index must return new-generation data, not stale cached data"
6329 );
6330
6331 let old_only = sample_page(0x33);
6333 assert_eq!(
6335 adapter.read_page(&cx, 99).expect("read non-existent"),
6336 None,
6337 "pages from old WAL generation must not appear after reset"
6338 );
6339 drop(old_only);
6341 }
6342
6343 #[test]
6344 fn test_page_index_invalidated_on_same_salt_generation_change() {
6345 init_wal_publication_test_tracing();
6346 let cx = test_cx();
6349 let vfs = MemoryVfs::new();
6350 let mut adapter = make_adapter(&vfs, &cx);
6351
6352 let reused_salts = adapter.inner().header().salts;
6353 let old_data = sample_page(0x11);
6354 adapter
6355 .append_frame(&cx, 1, &old_data, 1)
6356 .expect("append commit");
6357 assert_eq!(adapter.read_page(&cx, 1).expect("read old"), Some(old_data));
6358
6359 adapter
6360 .inner_mut()
6361 .expect("no staged batch blocks inner access")
6362 .reset(&cx, 1, reused_salts, false)
6363 .expect("reset with same salts");
6364 let new_data = sample_page(0x22);
6365 adapter
6366 .append_frame(&cx, 2, &new_data, 2)
6367 .expect("append new generation commit");
6368 let refreshed = adapter
6369 .refresh_published_snapshot(&cx)
6370 .expect("refresh published snapshot after same-salt reset");
6371 assert_eq!(refreshed.generation.checkpoint_seq, 1);
6372 assert_eq!(refreshed.generation.salts, reused_salts);
6373 assert_eq!(refreshed.last_commit_frame, Some(0));
6374 assert_eq!(refreshed.commit_count, 1);
6375 assert_eq!(refreshed.latest_frame_entries, 1);
6376
6377 assert_eq!(
6378 adapter.read_page(&cx, 1).expect("old page should be gone"),
6379 None,
6380 "cached index entries from the previous generation must be invalidated"
6381 );
6382 assert_eq!(
6383 adapter.read_page(&cx, 2).expect("read new page"),
6384 Some(new_data),
6385 "adapter must resolve pages from the new generation even when salts are reused"
6386 );
6387 }
6388
6389 #[test]
6390 fn test_refresh_published_snapshot_materializes_existing_committed_prefix() {
6391 init_wal_publication_test_tracing();
6392 let cx = test_cx();
6393 let vfs = MemoryVfs::new();
6394
6395 let file_writer = open_wal_file(&vfs, &cx);
6396 let wal_writer =
6397 WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6398 let mut writer = WalBackendAdapter::new(wal_writer);
6399
6400 let p1 = sample_page(0x71);
6401 let p2 = sample_page(0x72);
6402 writer.append_frame(&cx, 1, &p1, 0).expect("append p1");
6403 writer
6404 .append_frame(&cx, 2, &p2, 2)
6405 .expect("append p2 commit");
6406 writer.sync(&cx).expect("sync writer");
6407
6408 let file_reader = open_wal_file(&vfs, &cx);
6409 let wal_reader = WalFile::open(&cx, file_reader).expect("open reader WAL");
6410 let mut reader = WalBackendAdapter::new(wal_reader);
6411
6412 let before = reader.published_snapshot();
6413 assert_eq!(before.last_commit_frame, None);
6414 assert_eq!(before.commit_count, 0);
6415 assert_eq!(before.latest_frame_entries, 0);
6416
6417 let refreshed = reader
6418 .refresh_published_snapshot(&cx)
6419 .expect("refresh published snapshot");
6420 assert_eq!(refreshed.last_commit_frame, Some(1));
6421 assert_eq!(refreshed.commit_count, 1);
6422 assert_eq!(refreshed.latest_frame_entries, 2);
6423 assert!(refreshed.lookup_contract_is_authoritative());
6424 assert_eq!(reader.read_page(&cx, 1).expect("read p1"), Some(p1));
6425 assert_eq!(reader.read_page(&cx, 2).expect("read p2"), Some(p2));
6426 }
6427
6428 #[test]
6429 fn test_page_index_incremental_extend_after_durable_sync() {
6430 let cx = test_cx();
6433 let vfs = MemoryVfs::new();
6434 let mut adapter = make_adapter(&vfs, &cx);
6435
6436 let page1 = sample_page(0x10);
6437 adapter
6438 .append_frame(&cx, 1, &page1, 1)
6439 .expect("append commit 1");
6440 adapter.sync(&cx).expect("durably publish commit 1");
6441
6442 assert_eq!(
6444 adapter.read_page(&cx, 1).expect("read"),
6445 Some(page1.clone())
6446 );
6447
6448 let page2 = sample_page(0x20);
6450 let page1_v2 = sample_page(0x30);
6451 adapter
6452 .append_frame(&cx, 2, &page2, 0)
6453 .expect("append page 2");
6454 adapter
6455 .append_frame(&cx, 1, &page1_v2, 3)
6456 .expect("append page 1 v2 (commit)");
6457 adapter.sync(&cx).expect("durably publish commit 2");
6458
6459 assert_eq!(
6461 adapter.read_page(&cx, 1).expect("read page 1 v2"),
6462 Some(page1_v2),
6463 "incremental index extend should pick up the updated page"
6464 );
6465 assert_eq!(adapter.read_page(&cx, 2).expect("read page 2"), Some(page2));
6466 }
6467
6468 fn commit_batch_pages() -> (Vec<u8>, Vec<u8>) {
6470 (sample_page(0x71), sample_page(0x72))
6471 }
6472
6473 fn assert_publication_unchanged(adapter: &WalBackendAdapter<impl VfsFile>, context: &str) {
6475 assert_eq!(
6476 adapter.published_snapshot.last_commit_frame, None,
6477 "{context}: publication must not advance before a successful sync"
6478 );
6479 assert_eq!(
6480 adapter.published_snapshot.commit_count, 0,
6481 "{context}: commit count must not advance before a successful sync"
6482 );
6483 assert!(
6484 adapter.published_snapshot.page_index.is_empty(),
6485 "{context}: no page may be visible before a successful sync"
6486 );
6487 }
6488
6489 #[test]
6490 fn test_append_frame_without_sync_leaves_publication_unchanged() {
6491 let cx = test_cx();
6492 let vfs = MemoryVfs::new();
6493 let mut adapter = make_adapter(&vfs, &cx);
6494
6495 let (p1, p2) = commit_batch_pages();
6496 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6497 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6498
6499 assert_publication_unchanged(&adapter, "append_frame");
6500 assert_eq!(
6501 adapter.pending_publication_commit,
6502 Some(1),
6503 "append_frame must stage the commit horizon for a later sync"
6504 );
6505 }
6506
6507 #[test]
6508 fn test_append_frames_without_sync_leaves_publication_unchanged() {
6509 let cx = test_cx();
6510 let vfs = MemoryVfs::new();
6511 let mut adapter = make_adapter(&vfs, &cx);
6512
6513 let (p1, p2) = commit_batch_pages();
6514 let frames = [
6515 WalFrameRef {
6516 page_number: 1,
6517 page_data: &p1,
6518 db_size_if_commit: 0,
6519 },
6520 WalFrameRef {
6521 page_number: 2,
6522 page_data: &p2,
6523 db_size_if_commit: 2,
6524 },
6525 ];
6526 adapter
6527 .append_frames(&cx, &frames)
6528 .expect("append frames batch");
6529
6530 assert_publication_unchanged(&adapter, "append_frames");
6531 assert_eq!(
6532 adapter.pending_publication_commit,
6533 Some(1),
6534 "append_frames must stage the commit horizon for a later sync"
6535 );
6536 }
6537
6538 #[test]
6539 fn test_append_frames_tracked_without_sync_leaves_publication_unchanged() {
6540 let cx = test_cx();
6541 let vfs = MemoryVfs::new();
6542 let mut adapter = make_adapter(&vfs, &cx);
6543
6544 let (p1, p2) = commit_batch_pages();
6545 let frames = [
6546 WalFrameRef {
6547 page_number: 1,
6548 page_data: &p1,
6549 db_size_if_commit: 0,
6550 },
6551 WalFrameRef {
6552 page_number: 2,
6553 page_data: &p2,
6554 db_size_if_commit: 2,
6555 },
6556 ];
6557 adapter
6558 .append_frames_tracked(&cx, &frames, VfsWriteCompletion::new())
6559 .expect("append tracked frames batch");
6560
6561 assert_publication_unchanged(&adapter, "append_frames_tracked");
6562 assert_eq!(
6563 adapter.pending_publication_commit,
6564 Some(1),
6565 "append_frames_tracked must stage the commit horizon for a later sync"
6566 );
6567 }
6568
6569 #[test]
6570 fn test_append_prepared_frames_without_sync_leaves_publication_unchanged() {
6571 let cx = test_cx();
6572 let vfs = MemoryVfs::new();
6573 let mut adapter = make_adapter(&vfs, &cx);
6574
6575 let (p1, p2) = commit_batch_pages();
6576 let frames = [
6577 WalFrameRef {
6578 page_number: 1,
6579 page_data: &p1,
6580 db_size_if_commit: 0,
6581 },
6582 WalFrameRef {
6583 page_number: 2,
6584 page_data: &p2,
6585 db_size_if_commit: 2,
6586 },
6587 ];
6588 let mut prepared = adapter
6589 .prepare_append_frames(&frames)
6590 .expect("prepare append")
6591 .expect("prepared batch");
6592 adapter
6593 .append_prepared_frames(&cx, &mut prepared)
6594 .expect("append prepared");
6595
6596 assert_publication_unchanged(&adapter, "append_prepared_frames");
6597 assert_eq!(
6598 adapter.pending_publication_commit,
6599 Some(1),
6600 "append_prepared_frames must stage the commit horizon for a later sync"
6601 );
6602 }
6603
6604 #[test]
6605 fn test_append_prepared_frames_tracked_without_sync_leaves_publication_unchanged() {
6606 let cx = test_cx();
6607 let vfs = MemoryVfs::new();
6608 let mut adapter = make_adapter(&vfs, &cx);
6609
6610 let (p1, p2) = commit_batch_pages();
6611 let frames = [
6612 WalFrameRef {
6613 page_number: 1,
6614 page_data: &p1,
6615 db_size_if_commit: 0,
6616 },
6617 WalFrameRef {
6618 page_number: 2,
6619 page_data: &p2,
6620 db_size_if_commit: 2,
6621 },
6622 ];
6623 let mut prepared = adapter
6624 .prepare_append_frames(&frames)
6625 .expect("prepare append")
6626 .expect("prepared batch");
6627 adapter
6628 .append_prepared_frames_tracked(&cx, &mut prepared, VfsWriteCompletion::new())
6629 .expect("append prepared tracked");
6630
6631 assert_publication_unchanged(&adapter, "append_prepared_frames_tracked");
6632 assert_eq!(
6633 adapter.pending_publication_commit,
6634 Some(1),
6635 "append_prepared_frames_tracked must stage the commit horizon for a later sync"
6636 );
6637 }
6638
6639 #[test]
6640 fn test_successful_sync_publishes_staged_commit_horizon() {
6641 let cx = test_cx();
6642 let vfs = MemoryVfs::new();
6643 let mut adapter = make_adapter(&vfs, &cx);
6644
6645 let (p1, p2) = commit_batch_pages();
6646 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6647 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6648 assert_publication_unchanged(&adapter, "before sync");
6649
6650 adapter.sync(&cx).expect("sync must succeed");
6651
6652 assert_eq!(
6653 adapter.published_snapshot.last_commit_frame,
6654 Some(1),
6655 "a successful sync must publish the staged commit horizon"
6656 );
6657 assert_eq!(
6658 adapter.published_snapshot.commit_count, 1,
6659 "a successful sync must publish the staged commit count"
6660 );
6661 assert_eq!(
6662 adapter.published_snapshot.page_index.len(),
6663 2,
6664 "a successful sync must publish every staged page"
6665 );
6666 assert_eq!(
6667 adapter.pending_publication_commit, None,
6668 "a published batch must no longer be staged"
6669 );
6670 assert!(
6671 adapter.pending_publication_frames.is_empty(),
6672 "a published batch must drain its staged frames"
6673 );
6674 }
6675
6676 #[test]
6677 fn test_failed_sync_advances_no_publication_and_retry_publishes() {
6678 let cx = test_cx();
6679 let vfs = CheckpointHandoffFaultVfs::new();
6680 let mut adapter = make_fault_adapter(&vfs, &cx);
6681
6682 let (p1, p2) = commit_batch_pages();
6683 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6684 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6685
6686 vfs.fail_next_wal_sync();
6687 let failure = adapter
6688 .sync(&cx)
6689 .expect_err("injected WAL sync failure must surface");
6690 assert!(
6691 failure.to_string().contains("injected WAL sync failure"),
6692 "sync must report the injected durability failure, got: {failure}"
6693 );
6694
6695 assert_publication_unchanged(&adapter, "after failed sync");
6696 assert_eq!(
6697 adapter.pending_publication_commit,
6698 Some(1),
6699 "a failed sync must preserve the staged horizon for retry"
6700 );
6701 assert!(
6702 !adapter.pending_publication_frames.is_empty(),
6703 "a failed sync must preserve staged frames for retry"
6704 );
6705
6706 adapter.sync(&cx).expect("retry sync must succeed");
6708
6709 assert_eq!(
6710 adapter.published_snapshot.last_commit_frame,
6711 Some(1),
6712 "retrying sync must publish the preserved commit horizon"
6713 );
6714 assert_eq!(
6715 adapter.published_snapshot.commit_count, 1,
6716 "retrying sync must publish the preserved commit count"
6717 );
6718 assert_eq!(
6719 adapter.published_snapshot.page_index.len(),
6720 2,
6721 "retrying sync must publish every preserved page"
6722 );
6723 assert_eq!(
6724 adapter.pending_publication_commit, None,
6725 "a retried publication must clear the staged horizon"
6726 );
6727 }
6728
6729 #[test]
6730 fn test_failed_sync_then_append_cannot_drop_or_publish_pending() {
6731 let cx = test_cx();
6732 let vfs = CheckpointHandoffFaultVfs::new();
6733 let mut adapter = make_fault_adapter(&vfs, &cx);
6734
6735 let (p1, p2) = commit_batch_pages();
6736 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6737 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6738
6739 vfs.fail_next_wal_sync();
6740 adapter
6741 .sync(&cx)
6742 .expect_err("injected WAL sync failure must surface");
6743
6744 let staged_after_failure = adapter.pending_publication_commit;
6745 let staged_frames_after_failure = adapter.pending_publication_frames.len();
6746 assert_eq!(
6747 staged_after_failure,
6748 Some(1),
6749 "failed sync must preserve the staged horizon"
6750 );
6751
6752 let p3 = sample_page(0x73);
6755 adapter
6756 .append_frame(&cx, 3, &p3, 3)
6757 .expect("append after failed sync");
6758
6759 assert_publication_unchanged(&adapter, "append after failed sync");
6760 assert!(
6761 adapter.pending_publication_frames.len() > staged_frames_after_failure,
6762 "append after a failed sync must extend, never discard, the staged batch"
6763 );
6764 assert_eq!(
6765 adapter.pending_publication_commit,
6766 Some(2),
6767 "append after a failed sync must carry the staged horizon forward"
6768 );
6769
6770 adapter.sync(&cx).expect("sync after failed attempt");
6772 assert_eq!(
6773 adapter.published_snapshot.last_commit_frame,
6774 Some(2),
6775 "recovered sync must publish the full preserved horizon"
6776 );
6777 assert_eq!(
6778 adapter.pending_publication_commit, None,
6779 "recovered sync must clear the staged horizon"
6780 );
6781 }
6782
6783 #[test]
6784 fn test_failed_sync_then_begin_transaction_then_append_fails_closed() {
6785 let cx = test_cx();
6786 let vfs = CheckpointHandoffFaultVfs::new();
6787 let mut adapter = make_fault_adapter(&vfs, &cx);
6788
6789 let (p1, p2) = commit_batch_pages();
6790 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6791 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6792
6793 vfs.fail_next_wal_sync();
6794 adapter
6795 .sync(&cx)
6796 .expect_err("injected WAL sync failure must surface");
6797 assert_eq!(
6798 adapter.pending_publication_commit,
6799 Some(1),
6800 "failed sync must preserve the staged horizon"
6801 );
6802
6803 let begin_error = adapter
6807 .begin_transaction(&cx)
6808 .expect_err("begin_transaction must fail closed while frames are staged");
6809 assert!(
6810 matches!(begin_error, FrankenError::Busy),
6811 "staged-state rejection must be retryable Busy, not corruption: {begin_error:?}"
6812 );
6813 assert_publication_unchanged(&adapter, "begin_transaction refused after failed sync");
6814 assert_eq!(
6815 adapter.pending_publication_commit,
6816 Some(1),
6817 "a refused begin_transaction must not drop the staged horizon"
6818 );
6819 assert!(
6820 adapter.pinned_read_snapshot().is_none(),
6821 "a refused begin_transaction must not pin a read snapshot"
6822 );
6823
6824 adapter.refresh_before_append = true;
6827 let p3 = sample_page(0x74);
6828 let append_error = adapter
6829 .append_frame(&cx, 3, &p3, 3)
6830 .expect_err("append must fail closed while frames are staged");
6831 assert!(
6832 matches!(append_error, FrankenError::Busy),
6833 "append rejection must be retryable Busy: {append_error:?}"
6834 );
6835 assert_publication_unchanged(&adapter, "append refused after failed sync");
6836 assert_eq!(
6837 adapter.pending_publication_commit,
6838 Some(1),
6839 "a refused append must leave the staged horizon intact"
6840 );
6841 assert!(
6842 !adapter.pending_publication_frames.is_empty(),
6843 "a refused append must leave the staged frames intact"
6844 );
6845 adapter.refresh_before_append = false;
6846
6847 adapter.sync(&cx).expect("sync after failed attempt");
6849 assert_eq!(
6850 adapter.published_snapshot.last_commit_frame,
6851 Some(1),
6852 "recovered sync must publish the preserved horizon"
6853 );
6854 }
6855
6856 #[test]
6857 fn test_failed_sync_then_checkpoint_fails_closed_and_preserves_state() {
6858 let cx = test_cx();
6859 let vfs = CheckpointHandoffFaultVfs::new();
6860 let mut adapter = make_fault_adapter(&vfs, &cx);
6861
6862 let (p1, p2) = commit_batch_pages();
6863 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6864 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6865
6866 vfs.fail_next_wal_sync();
6867 adapter
6868 .sync(&cx)
6869 .expect_err("injected WAL sync failure must surface");
6870
6871 let frames_before = adapter.frame_count();
6872 let staged_before = adapter.pending_publication_commit;
6873 let staged_frame_count_before = adapter.pending_publication_frames.len();
6874
6875 let mut writer = MockCheckpointPageWriter;
6879 let checkpoint_error = adapter
6880 .checkpoint(&cx, CheckpointMode::Passive, &mut writer, 0, None)
6881 .expect_err("checkpoint must fail closed while frames are staged");
6882 assert!(
6883 matches!(checkpoint_error, FrankenError::CheckpointFailed { .. }),
6884 "checkpoint rejection must be CheckpointFailed, not corruption: {checkpoint_error:?}"
6885 );
6886
6887 assert_eq!(
6888 adapter.frame_count(),
6889 frames_before,
6890 "a refused checkpoint must not mutate WAL bytes"
6891 );
6892 assert_publication_unchanged(&adapter, "checkpoint refused");
6893 assert_eq!(
6894 adapter.pending_publication_commit, staged_before,
6895 "a refused checkpoint must preserve the staged horizon"
6896 );
6897 assert_eq!(
6898 adapter.pending_publication_frames.len(),
6899 staged_frame_count_before,
6900 "a refused checkpoint must preserve the staged frames"
6901 );
6902
6903 adapter.sync(&cx).expect("retry sync must succeed");
6905 assert_eq!(
6906 adapter.published_snapshot.last_commit_frame,
6907 Some(1),
6908 "retry sync must publish the preserved horizon"
6909 );
6910 assert_eq!(
6911 adapter.pending_publication_commit, None,
6912 "a published batch must no longer be staged"
6913 );
6914 }
6915
6916 #[test]
6917 fn test_midtransaction_sync_preserves_uncommitted_frames_and_allows_continuation() {
6918 let cx = test_cx();
6919 let vfs = MemoryVfs::new();
6920 let mut adapter = make_adapter(&vfs, &cx);
6921
6922 let (p1, p2) = commit_batch_pages();
6923
6924 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6927 assert_eq!(
6928 adapter.pending_publication_commit, None,
6929 "a non-commit append stages no commit horizon"
6930 );
6931 adapter
6932 .sync(&cx)
6933 .expect("mid-transaction sync must succeed");
6934
6935 assert_publication_unchanged(&adapter, "sync of uncommitted frames");
6936 assert!(
6937 !adapter.pending_publication_frames.is_empty(),
6938 "a mid-transaction sync must preserve durable-but-uncommitted frames"
6939 );
6940
6941 adapter
6943 .append_frame(&cx, 2, &p2, 2)
6944 .expect("commit append after mid-transaction sync must be allowed");
6945 assert_eq!(
6946 adapter.pending_publication_commit,
6947 Some(1),
6948 "the commit append must stage the horizon for the whole batch"
6949 );
6950 assert_publication_unchanged(&adapter, "commit staged but not yet synced");
6951
6952 adapter.sync(&cx).expect("commit sync must succeed");
6953
6954 assert_eq!(
6955 adapter.published_snapshot.last_commit_frame,
6956 Some(1),
6957 "the commit sync must publish the whole batch"
6958 );
6959 assert_eq!(
6960 adapter.published_snapshot.commit_count, 1,
6961 "the batch must publish exactly one commit"
6962 );
6963 assert_eq!(
6964 adapter.published_snapshot.page_index.len(),
6965 2,
6966 "both pages must be published exactly once"
6967 );
6968 assert_eq!(
6969 adapter.published_snapshot.page_index.get(&1),
6970 Some(&0),
6971 "page 1 must map to its frame from before the mid-transaction sync"
6972 );
6973 assert_eq!(
6974 adapter.published_snapshot.page_index.get(&2),
6975 Some(&1),
6976 "page 2 must map to the commit frame"
6977 );
6978 assert!(
6979 !adapter.has_pending_publication(),
6980 "a published batch must leave nothing staged"
6981 );
6982 }
6983
6984 #[test]
6985 fn test_inner_mut_fails_closed_while_batch_is_staged() {
6986 let cx = test_cx();
6987 let vfs = CheckpointHandoffFaultVfs::new();
6988 let mut adapter = make_fault_adapter(&vfs, &cx);
6989
6990 let (p1, p2) = commit_batch_pages();
6991 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6992 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6993
6994 assert!(
6995 adapter.has_pending_publication(),
6996 "an appended-but-unsynced batch must report as pending"
6997 );
6998 assert!(
7001 matches!(adapter.inner_mut(), Err(FrankenError::Busy)),
7002 "inner_mut must fail closed with retryable Busy while frames are staged"
7003 );
7004 assert_eq!(
7005 adapter.pending_publication_commit,
7006 Some(1),
7007 "a refused inner_mut must preserve the staged horizon"
7008 );
7009
7010 adapter.sync(&cx).expect("sync staged batch");
7012 assert!(
7013 !adapter.has_pending_publication(),
7014 "a published batch must clear the pending flag"
7015 );
7016 adapter
7017 .inner_mut()
7018 .expect("inner_mut must succeed once the batch is drained");
7019 }
7020
7021 #[test]
7022 fn test_unpinned_refresh_does_not_expose_staged_horizon_before_sync() {
7023 let cx = test_cx();
7024 let vfs = MemoryVfs::new();
7025 let mut adapter = make_adapter(&vfs, &cx);
7026
7027 let (p1, p2) = commit_batch_pages();
7028 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7029 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7030
7031 adapter
7034 .refresh_published_snapshot(&cx)
7035 .expect("refresh published snapshot");
7036 assert_publication_unchanged(&adapter, "refresh with staged frames");
7037 assert_eq!(
7038 adapter.pending_publication_commit,
7039 Some(1),
7040 "refresh must leave the staged horizon intact"
7041 );
7042
7043 adapter.sync(&cx).expect("sync staged batch");
7044 assert_eq!(
7045 adapter.published_snapshot.last_commit_frame,
7046 Some(1),
7047 "sync must publish once the staged batch is durable"
7048 );
7049 }
7050
7051 #[test]
7052 fn test_authorized_deferred_commit_publishes_without_claiming_fsync() {
7053 let cx = test_cx();
7054 let vfs = MemoryVfs::new();
7055 let mut adapter = make_adapter(&vfs, &cx);
7056
7057 let (p1, p2) = commit_batch_pages();
7058 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7059 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7060 let fsynced_before = adapter.wal.last_fsynced_frame_count();
7061
7062 adapter
7063 .publish_authorized_deferred_commit(&cx)
7064 .expect("parallel-WAL authorization must publish the deferred commit");
7065
7066 assert_eq!(
7067 adapter.published_snapshot.last_commit_frame,
7068 Some(1),
7069 "the authorized commit marker must become visible"
7070 );
7071 assert_eq!(
7072 adapter.published_snapshot.commit_count, 1,
7073 "the authorized batch must publish exactly one commit"
7074 );
7075 assert!(
7076 !adapter.has_pending_publication(),
7077 "authorization must drain the staged publication horizon"
7078 );
7079 assert_eq!(
7080 adapter.wal.last_fsynced_frame_count(),
7081 fsynced_before,
7082 "deferred authorization must not claim or force an fsync"
7083 );
7084 adapter
7085 .begin_transaction(&cx)
7086 .expect("the next transaction must not see a stale Busy");
7087 }
7088
7089 #[test]
7090 fn test_commit_append_publishes_visibility_snapshot() {
7091 init_wal_publication_test_tracing();
7092 let cx = test_cx();
7093 let vfs = MemoryVfs::new();
7094 let mut adapter = make_adapter(&vfs, &cx);
7095
7096 let p1 = sample_page(0x41);
7097 let p2 = sample_page(0x42);
7098 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7099 adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7100 adapter.sync(&cx).expect("sync commit batch");
7103
7104 assert_eq!(
7105 adapter.published_snapshot.last_commit_frame,
7106 Some(1),
7107 "synced commit should publish the visible commit horizon"
7108 );
7109 assert_eq!(
7110 adapter.published_snapshot.commit_count, 1,
7111 "synced commit should track the visible WAL commit count"
7112 );
7113 assert_eq!(
7114 adapter.published_snapshot.page_index.len(),
7115 2,
7116 "published snapshot should track both committed pages"
7117 );
7118 assert_eq!(
7119 adapter.published_snapshot.page_index.get(&2),
7120 Some(&1),
7121 "published snapshot must map each page to its latest committed frame"
7122 );
7123 }
7124
7125 #[test]
7126 fn test_prepared_append_publishes_visibility_snapshot() {
7127 init_wal_publication_test_tracing();
7128 let cx = test_cx();
7129 let vfs = MemoryVfs::new();
7130 let mut adapter = make_adapter(&vfs, &cx);
7131
7132 let p1 = sample_page(0x51);
7133 let p2 = sample_page(0x52);
7134 let frames = [
7135 WalFrameRef {
7136 page_number: 1,
7137 page_data: &p1,
7138 db_size_if_commit: 0,
7139 },
7140 WalFrameRef {
7141 page_number: 2,
7142 page_data: &p2,
7143 db_size_if_commit: 2,
7144 },
7145 ];
7146 let mut prepared = adapter
7147 .prepare_append_frames(&frames)
7148 .expect("prepare append")
7149 .expect("prepared batch");
7150 adapter
7151 .append_prepared_frames(&cx, &mut prepared)
7152 .expect("append prepared");
7153 adapter.sync(&cx).expect("sync prepared commit batch");
7155
7156 assert_eq!(
7157 adapter.published_snapshot.last_commit_frame,
7158 Some(1),
7159 "synced prepared commit should publish the visible commit horizon"
7160 );
7161 assert_eq!(
7162 adapter.published_snapshot.commit_count, 1,
7163 "synced prepared commit should track the visible WAL commit count"
7164 );
7165 assert_eq!(
7166 adapter.published_snapshot.page_index.len(),
7167 2,
7168 "synced prepared commit should publish all committed pages"
7169 );
7170 assert_eq!(
7171 adapter.published_snapshot.page_index.get(&2),
7172 Some(&1),
7173 "prepared commit append must map each page to its latest committed frame"
7174 );
7175 }
7176
7177 #[test]
7178 fn test_commit_publication_refreshes_external_prefix_before_local_commit() {
7179 let cx = test_cx();
7180 let vfs = MemoryVfs::new();
7181
7182 let file_writer = open_wal_file(&vfs, &cx);
7183 let wal_writer =
7184 WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
7185 let mut writer = WalBackendAdapter::new(wal_writer);
7186
7187 let file_follower = open_wal_file(&vfs, &cx);
7188 let wal_follower = WalFile::open(&cx, file_follower).expect("open WAL");
7189 let mut follower = WalBackendAdapter::new(wal_follower);
7190
7191 let p1 = sample_page(0x61);
7192 writer
7193 .append_frame(&cx, 1, &p1, 1)
7194 .expect("writer commit 1");
7195 writer.sync(&cx).expect("sync writer commit 1");
7196
7197 let p2 = sample_page(0x62);
7198 writer
7199 .append_frame(&cx, 2, &p2, 2)
7200 .expect("writer commit 2");
7201 writer.sync(&cx).expect("sync writer commit 2");
7202
7203 let p3 = sample_page(0x63);
7204 follower
7205 .append_frame(&cx, 3, &p3, 3)
7206 .expect("follower local commit");
7207
7208 assert_eq!(
7209 follower.published_snapshot.last_commit_frame,
7210 Some(2),
7211 "local commit should publish on top of refreshed external WAL state"
7212 );
7213 assert_eq!(
7214 follower.published_snapshot.commit_count, 3,
7215 "local commit publication should include refreshed external commits"
7216 );
7217 assert_eq!(
7218 follower.published_snapshot.page_index.get(&1),
7219 Some(&0),
7220 "refresh-before-append should preserve earlier committed pages"
7221 );
7222 assert_eq!(
7223 follower.published_snapshot.page_index.get(&2),
7224 Some(&1),
7225 "refresh-before-append should publish externally committed pages"
7226 );
7227 assert_eq!(
7228 follower.published_snapshot.page_index.get(&3),
7229 Some(&2),
7230 "local commit should extend the published WAL visibility map"
7231 );
7232 assert_eq!(follower.read_page(&cx, 1).expect("read p1"), Some(p1));
7233 assert_eq!(follower.read_page(&cx, 2).expect("read p2"), Some(p2));
7234 assert_eq!(follower.read_page(&cx, 3).expect("read p3"), Some(p3));
7235 }
7236
7237 #[test]
7238 fn test_truncate_checkpoint_republishes_empty_generation_snapshot() {
7239 init_wal_publication_test_tracing();
7240 let cx = test_cx();
7241 let vfs = MemoryVfs::new();
7242 let mut adapter = make_adapter(&vfs, &cx);
7243 let mut writer = MockCheckpointPageWriter;
7244
7245 adapter
7246 .append_frame(&cx, 1, &sample_page(0x61), 1)
7247 .expect("append committed frame");
7248 adapter.sync(&cx).expect("sync committed frame");
7252 let before = adapter.published_snapshot();
7253 assert_eq!(before.last_commit_frame, Some(0));
7254 assert_eq!(before.commit_count, 1);
7255 assert_eq!(before.latest_frame_entries, 1);
7256
7257 let result = adapter
7258 .checkpoint(&cx, CheckpointMode::Truncate, &mut writer, 0, None)
7259 .expect("truncate checkpoint");
7260 assert!(result.completed);
7261 assert!(result.wal_was_reset);
7262
7263 let after = adapter.published_snapshot();
7264 assert_ne!(
7265 before.generation, after.generation,
7266 "truncate checkpoint should publish a new WAL generation"
7267 );
7268 assert_eq!(after.last_commit_frame, None);
7269 assert_eq!(after.commit_count, 0);
7270 assert_eq!(after.latest_frame_entries, 0);
7271 assert!(after.lookup_contract_is_authoritative());
7272 }
7273
7274 #[test]
7277 fn test_partial_index_falls_back_to_linear_scan() {
7278 init_wal_publication_test_tracing();
7279 let cx = test_cx();
7282 let vfs = MemoryVfs::new();
7283 let mut adapter = make_adapter(&vfs, &cx);
7284
7285 adapter.set_page_index_cap(2);
7288
7289 let p1 = sample_page(0x01);
7292 let p2 = sample_page(0x02);
7293 let p3 = sample_page(0x03);
7294 let p4 = sample_page(0x04);
7295 let p5 = sample_page(0x05);
7296
7297 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7298 adapter.append_frame(&cx, 2, &p2, 0).expect("append p2");
7299 adapter.append_frame(&cx, 3, &p3, 0).expect("append p3");
7300 adapter.append_frame(&cx, 4, &p4, 0).expect("append p4");
7301 adapter
7302 .append_frame(&cx, 5, &p5, 5)
7303 .expect("append p5 (commit)");
7304
7305 assert_eq!(
7307 adapter.read_page(&cx, 1).expect("read p1"),
7308 Some(p1),
7309 "indexed page should be found via HashMap"
7310 );
7311 assert_eq!(
7312 adapter.read_page(&cx, 2).expect("read p2"),
7313 Some(p2),
7314 "indexed page should be found via HashMap"
7315 );
7316
7317 assert_eq!(
7320 adapter.read_page(&cx, 3).expect("read p3"),
7321 Some(p3),
7322 "non-indexed page must be found via linear scan fallback"
7323 );
7324 assert_eq!(
7325 adapter.read_page(&cx, 4).expect("read p4"),
7326 Some(p4),
7327 "non-indexed page must be found via linear scan fallback"
7328 );
7329 assert_eq!(
7330 adapter.read_page(&cx, 5).expect("read p5"),
7331 Some(p5),
7332 "non-indexed page must be found via linear scan fallback"
7333 );
7334
7335 assert_eq!(
7337 adapter.read_page(&cx, 99).expect("read non-existent"),
7338 None,
7339 "non-existent page must return None even with partial index"
7340 );
7341
7342 assert!(
7344 adapter.published_snapshot.index_is_partial,
7345 "index_is_partial should be true when cap is exceeded"
7346 );
7347 }
7348
7349 #[test]
7350 fn test_partial_index_returns_latest_version_via_fallback() {
7351 let cx = test_cx();
7355 let vfs = MemoryVfs::new();
7356 let mut adapter = make_adapter(&vfs, &cx);
7357
7358 adapter.set_page_index_cap(1);
7360
7361 let old_p2 = sample_page(0xAA);
7362 let new_p2 = sample_page(0xBB);
7363
7364 adapter
7366 .append_frame(&cx, 1, &sample_page(0x01), 0)
7367 .expect("append p1");
7368 adapter
7370 .append_frame(&cx, 2, &old_p2, 0)
7371 .expect("append p2 old");
7372 adapter
7375 .append_frame(&cx, 2, &new_p2, 3)
7376 .expect("append p2 new (commit)");
7377
7378 assert_eq!(
7380 adapter.read_page(&cx, 2).expect("read p2"),
7381 Some(new_p2),
7382 "backwards scan must return the most recent frame for the page"
7383 );
7384 }
7385
7386 #[test]
7387 fn test_lookup_contract_distinguishes_authoritative_and_fallback_paths() {
7388 init_wal_publication_test_tracing();
7389 let cx = test_cx();
7390 let vfs = MemoryVfs::new();
7391 let mut adapter = make_adapter(&vfs, &cx);
7392 adapter.set_page_index_cap(1);
7393
7394 let p1 = sample_page(0x01);
7395 let p2 = sample_page(0x02);
7396 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7397 adapter
7398 .append_frame(&cx, 2, &p2, 2)
7399 .expect("append p2 commit");
7400
7401 let last_commit = adapter
7402 .inner_mut()
7403 .expect("no staged batch blocks inner access")
7404 .last_commit_frame(&cx)
7405 .expect("last commit")
7406 .expect("commit exists");
7407 adapter
7408 .publish_visible_snapshot(&cx, Some(last_commit), "lookup_contract_test")
7409 .expect("build published snapshot");
7410 let snapshot = adapter.published_snapshot.clone();
7411
7412 assert_eq!(
7413 adapter
7414 .resolve_visible_frame(&cx, &snapshot, 1)
7415 .expect("resolve indexed page"),
7416 WalPageLookupResolution::AuthoritativeHit { frame_index: 0 }
7417 );
7418 assert_eq!(
7419 adapter
7420 .resolve_visible_frame(&cx, &snapshot, 2)
7421 .expect("resolve fallback page"),
7422 WalPageLookupResolution::PartialIndexFallbackHit { frame_index: 1 }
7423 );
7424 assert_eq!(
7425 adapter
7426 .resolve_visible_frame(&cx, &snapshot, 99)
7427 .expect("resolve missing page"),
7428 WalPageLookupResolution::PartialIndexFallbackMiss
7429 );
7430 }
7431
7432 #[test]
7433 fn test_lookup_contract_is_authoritative_by_default() {
7434 let cx = test_cx();
7435 let vfs = MemoryVfs::new();
7436 let mut adapter = make_adapter(&vfs, &cx);
7437
7438 let p1 = sample_page(0x11);
7439 let p2 = sample_page(0x22);
7440 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7441 adapter
7442 .append_frame(&cx, 2, &p2, 2)
7443 .expect("append p2 commit");
7444
7445 let last_commit = adapter
7446 .inner_mut()
7447 .expect("no staged batch blocks inner access")
7448 .last_commit_frame(&cx)
7449 .expect("last commit")
7450 .expect("commit exists");
7451 adapter
7452 .publish_visible_snapshot(&cx, Some(last_commit), "lookup_contract_default")
7453 .expect("build published snapshot");
7454 let snapshot = adapter.published_snapshot.clone();
7455
7456 assert!(
7457 !snapshot.index_is_partial,
7458 "default index should be authoritative"
7459 );
7460 assert_eq!(
7461 adapter
7462 .resolve_visible_frame(&cx, &snapshot, 1)
7463 .expect("resolve page 1"),
7464 WalPageLookupResolution::AuthoritativeHit { frame_index: 0 }
7465 );
7466 assert_eq!(
7467 adapter
7468 .resolve_visible_frame(&cx, &snapshot, 2)
7469 .expect("resolve page 2"),
7470 WalPageLookupResolution::AuthoritativeHit { frame_index: 1 }
7471 );
7472 assert_eq!(
7473 adapter
7474 .resolve_visible_frame(&cx, &snapshot, 99)
7475 .expect("resolve missing page"),
7476 WalPageLookupResolution::AuthoritativeMiss
7477 );
7478 }
7479
7480 #[test]
7481 fn test_committed_txns_since_page_uses_visible_frame_horizon() {
7482 let cx = test_cx();
7483 let vfs = MemoryVfs::new();
7484 let mut adapter = make_adapter(&vfs, &cx);
7485
7486 let p1 = sample_page(0x31);
7487 let p2 = sample_page(0x32);
7488 let p3 = sample_page(0x33);
7489
7490 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7491 adapter.append_frame(&cx, 2, &p2, 2).expect("commit tx1");
7492 adapter.append_frame(&cx, 3, &p3, 0).expect("append p3");
7493 adapter.append_frame(&cx, 2, &p2, 3).expect("commit tx2");
7494
7495 assert_eq!(
7496 adapter
7497 .committed_txns_since_page(&cx, 1)
7498 .expect("count txns since page 1"),
7499 1
7500 );
7501 assert_eq!(
7502 adapter
7503 .committed_txns_since_page(&cx, 2)
7504 .expect("count txns since page 2"),
7505 0
7506 );
7507 assert_eq!(
7508 adapter
7509 .committed_txns_since_page(&cx, 99)
7510 .expect("count txns since missing page"),
7511 2
7512 );
7513 assert_eq!(
7514 adapter
7515 .committed_txn_count(&cx)
7516 .expect("count visible transactions"),
7517 2
7518 );
7519 }
7520
7521 #[test]
7522 fn test_conflicting_pages_since_snapshot_detects_later_wal_writes() {
7523 let cx = test_cx();
7524 let vfs = MemoryVfs::new();
7525 let mut adapter = make_adapter(&vfs, &cx);
7526
7527 let p1 = sample_page(0x41);
7528 let p2_before = sample_page(0x42);
7529 let p2_after = sample_page(0x43);
7530 let p3 = sample_page(0x44);
7531
7532 adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7533 adapter
7534 .append_frame(&cx, 2, &p2_before, 2)
7535 .expect("commit tx1");
7536 adapter
7537 .begin_transaction(&cx)
7538 .expect("pin transaction snapshot");
7539 let pinned = adapter
7540 .pinned_read_snapshot()
7541 .expect("transaction should expose pinned WAL snapshot");
7542 let conflict_snapshot = TransactionConflictSnapshot {
7543 generation: pinned.generation,
7544 last_commit_frame: pinned.last_commit_frame,
7545 commit_count: pinned.commit_count,
7546 };
7547
7548 adapter
7549 .append_frame(&cx, 3, &p3, 0)
7550 .expect("append unrelated later page");
7551 adapter
7552 .append_frame(&cx, 2, &p2_after, 3)
7553 .expect("commit later page 2 update");
7554
7555 let conflicts = adapter
7556 .conflicting_pages_since_snapshot(&cx, conflict_snapshot, &[2, 99], &[])
7557 .expect("conflict check should scan later committed frames");
7558 assert_eq!(conflicts, vec![2]);
7559
7560 let unrelated = adapter
7561 .conflicting_pages_since_snapshot(&cx, conflict_snapshot, &[99], &[])
7562 .expect("unrelated page should stay conflict-free");
7563 assert!(unrelated.is_empty());
7564 }
7565
7566 #[test]
7569 fn test_checkpoint_adapter_write_page() {
7570 let cx = test_cx();
7571 let mut writer = MockCheckpointPageWriter;
7572 let mut adapter = CheckpointTargetAdapterRef {
7573 writer: &mut writer,
7574 };
7575
7576 let page_no = PageNumber::new(1).expect("valid page number");
7577 adapter
7578 .write_page(&cx, page_no, &[0u8; 4096])
7579 .expect("write_page");
7580 }
7581
7582 #[test]
7583 fn test_checkpoint_adapter_truncate_db() {
7584 let cx = test_cx();
7585 let mut writer = MockCheckpointPageWriter;
7586 let mut adapter = CheckpointTargetAdapterRef {
7587 writer: &mut writer,
7588 };
7589
7590 adapter.truncate_db(&cx, 10).expect("truncate_db");
7591 }
7592
7593 #[test]
7594 fn test_checkpoint_adapter_sync_db() {
7595 let cx = test_cx();
7596 let mut writer = MockCheckpointPageWriter;
7597 let mut adapter = CheckpointTargetAdapterRef {
7598 writer: &mut writer,
7599 };
7600
7601 adapter.sync_db(&cx).expect("sync_db");
7602 }
7603
7604 #[test]
7605 fn test_checkpoint_adapter_as_dyn_target() {
7606 let cx = test_cx();
7607 let mut writer = MockCheckpointPageWriter;
7608 let mut adapter = CheckpointTargetAdapterRef {
7609 writer: &mut writer,
7610 };
7611
7612 let target: &mut dyn CheckpointTarget = &mut adapter;
7614 let page_no = PageNumber::new(3).expect("valid page number");
7615 target
7616 .write_page(&cx, page_no, &[0u8; 4096])
7617 .expect("write via dyn");
7618 target.truncate_db(&cx, 5).expect("truncate via dyn");
7619 target.sync_db(&cx).expect("sync via dyn");
7620 }
7621}