1use super::*;
2use crate::api::DurabilityMode;
3use crate::storage::wal::{WalReader, WalRecord, WalWriter};
4use reddb_engine::storage::resident_file::EMBEDDED_WAL_HEADROOM_LIMIT;
5use std::cell::RefCell;
6use std::io;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
9use std::sync::{Arc, Condvar, Mutex};
10
11#[path = "checkpoint_undo.rs"]
12mod checkpoint_undo;
13
14struct NativeReplayTarget<'a> {
15 store: &'a UnifiedStore,
16 update_btrees: bool,
17}
18
19impl reddb_engine::storage::store_wal::StoreReplayTarget for NativeReplayTarget<'_> {
20 fn set_aux_metadata(&self, bytes: &[u8]) -> Result<(), StoreError> {
21 let bytes = reddb_engine::catalog::codec::canonicalize_collection_contract_blob(bytes)
22 .map_err(|err| StoreError::Serialization(err.to_string()))?;
23 self.store.resident.set_aux_metadata(bytes);
24 Ok(())
25 }
26 fn has_collection(&self, name: &str) -> bool {
27 self.store.get_collection(name).is_some()
28 }
29 fn create_collection(&self, name: &str) -> Result<(), StoreError> {
30 self.store.create_collection_in_memory(name)
31 }
32 fn drop_collection(&self, name: &str) -> Result<(), StoreError> {
33 self.store.drop_collection_in_memory(name)
34 }
35 fn upsert(&self, collection: &str, record: &[u8]) -> Result<(), StoreError> {
36 self.store
37 .apply_replayed_upsert(collection, record, self.update_btrees)
38 }
39 fn delete(&self, collection: &str, id: EntityId) -> Result<(), StoreError> {
40 self.store
41 .apply_replayed_delete(collection, id, self.update_btrees)
42 }
43 fn refresh(&self, collection: &str, records: &[Vec<u8>]) -> Result<(), StoreError> {
44 self.store
45 .apply_replayed_refresh_collection(collection, records, self.update_btrees)
46 }
47}
48
49const DEFAULT_ADAPTIVE_WINDOW_US: u64 = 200;
66
67type WalMutex = parking_lot::Mutex<WalWriter>;
74
75type CommitStateMutex = parking_lot::Mutex<CommitState>;
81type CommitStateCondvar = parking_lot::Condvar;
82use std::time::{Duration, Instant};
83
84static NEXT_STORE_TX_ID: AtomicU64 = AtomicU64::new(1);
85
86pub(crate) use reddb_engine::storage::store_wal::StoreWalAction;
87
88#[derive(Debug, Default)]
89pub(crate) struct DeferredStoreWalActions {
90 pub(super) actions: Vec<StoreWalAction>,
91}
92
93pub(crate) struct StoreWalCaptureGuard<'a> {
94 active: bool,
95 session: Option<&'a crate::storage::embedded::EmbeddedCommitSession>,
96}
97
98impl<'a> StoreWalCaptureGuard<'a> {
99 pub(crate) fn begin(
100 session: Option<&'a crate::storage::embedded::EmbeddedCommitSession>,
101 ) -> Self {
102 assert!(
103 !deferred_store_wal_capture_active(),
104 "store WAL captures cannot nest"
105 );
106 begin_deferred_store_wal_capture();
107 Self {
108 active: true,
109 session,
110 }
111 }
112
113 pub(crate) fn finish(mut self) -> DeferredStoreWalActions {
114 let actions = take_deferred_store_wal_capture();
115 self.active = false;
116 actions
117 }
118}
119
120impl Drop for StoreWalCaptureGuard<'_> {
121 fn drop(&mut self) {
122 if self.active {
123 if let Some(session) = self.session {
124 session.fence_interrupted_write();
125 }
126 let _ = take_deferred_store_wal_capture();
127 }
128 }
129}
130
131impl DeferredStoreWalActions {
132 pub(crate) fn push(&mut self, action: StoreWalAction) {
133 self.actions.push(action);
134 }
135
136 pub(crate) fn is_empty(&self) -> bool {
137 self.actions.is_empty()
138 }
139
140 pub(crate) fn extend(&mut self, other: Self) {
141 self.actions.extend(other.actions);
142 }
143}
144
145thread_local! {
146 static DEFERRED_STORE_WAL_ACTIONS: RefCell<Option<Vec<StoreWalAction>>> =
147 const { RefCell::new(None) };
148}
149
150fn begin_deferred_store_wal_capture() {
151 DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
152 let mut guard = cell.borrow_mut();
153 debug_assert!(guard.is_none());
154 *guard = Some(Vec::new());
155 });
156}
157
158fn capture_deferred_store_wal_actions(actions: Vec<StoreWalAction>) -> bool {
159 DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
160 let mut guard = cell.borrow_mut();
161 if let Some(pending) = guard.as_mut() {
162 pending.extend(actions);
163 true
164 } else {
165 false
166 }
167 })
168}
169
170fn deferred_store_wal_capture_active() -> bool {
171 DEFERRED_STORE_WAL_ACTIONS.with(|cell| cell.borrow().is_some())
172}
173
174fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
175 DEFERRED_STORE_WAL_ACTIONS.with(|cell| DeferredStoreWalActions {
176 actions: cell.borrow_mut().take().unwrap_or_default(),
177 })
178}
179
180#[derive(Debug)]
181struct CommitState {
182 durable_lsn: u64,
183 pending_target_lsn: u64,
184 pending_statements: usize,
185 pending_wal_bytes: u64,
186 first_pending_at: Option<Instant>,
187 shutdown: bool,
188 last_error: Option<String>,
189}
190
191impl CommitState {
192 fn new(initial_durable_lsn: u64) -> Self {
193 Self {
194 durable_lsn: initial_durable_lsn,
195 pending_target_lsn: initial_durable_lsn,
196 pending_statements: 0,
197 pending_wal_bytes: 0,
198 first_pending_at: None,
199 shutdown: false,
200 last_error: None,
201 }
202 }
203}
204
205pub(crate) struct WalAppendQueue {
219 epoch: parking_lot::RwLock<()>,
222 pending: parking_lot::Mutex<WalQueueState>,
237 queued_bytes: AtomicU64,
242}
243
244struct WalQueueState {
245 next_lsn: u64,
246 entries: Vec<(u64, Vec<u8>)>,
247}
248
249impl WalAppendQueue {
250 fn new(initial_lsn: u64) -> Self {
251 Self {
252 epoch: parking_lot::RwLock::new(()),
253 pending: parking_lot::Mutex::new(WalQueueState {
254 next_lsn: initial_lsn,
255 entries: Vec::with_capacity(64),
256 }),
257 queued_bytes: AtomicU64::new(0),
258 }
259 }
260
261 fn enqueue(&self, bytes: Vec<u8>) -> u64 {
267 let len = bytes.len() as u64;
268 let mut state = self.pending.lock();
269 let start_lsn = state.next_lsn;
270 state.next_lsn = start_lsn + len;
271 state.entries.push((start_lsn, bytes));
272 self.queued_bytes.fetch_add(len, Ordering::Relaxed);
273 start_lsn + len
274 }
275
276 fn drain_sorted(&self) -> Vec<(u64, Vec<u8>)> {
280 let mut state = self.pending.lock();
281 let mut v = std::mem::take(&mut state.entries);
282 let drained: u64 = v.iter().map(|(_, bytes)| bytes.len() as u64).sum();
283 drop(state);
284 let _ = self
288 .queued_bytes
289 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |queued| {
290 Some(queued.saturating_sub(drained))
291 });
292 v.sort_by_key(|(lsn, _)| *lsn);
293 v
294 }
295
296 fn has_pending(&self) -> bool {
299 !self.pending.lock().entries.is_empty()
300 }
301
302 fn queued_bytes(&self) -> u64 {
304 self.queued_bytes.load(Ordering::Relaxed)
305 }
306
307 fn reset(&self, next_lsn: u64) {
313 let mut state = self.pending.lock();
314 state.next_lsn = next_lsn;
315 state.entries.clear();
316 drop(state);
317 self.queued_bytes.store(0, Ordering::Relaxed);
318 }
319}
320
321pub(crate) struct StoreCommitCoordinator {
322 mode: DurabilityMode,
323 config: crate::api::GroupCommitOptions,
324 wal_path: PathBuf,
325 wal: Arc<WalMutex>,
326 queue: Arc<WalAppendQueue>,
332 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
333 fsync_count: Arc<AtomicU64>,
339 wal_generation: AtomicU64,
340 ownership_last_epoch: AtomicU64,
342 ownership_wal_state: AtomicU8,
344 runtime_wal_state: AtomicU8,
346}
347
348#[allow(dead_code)] struct StoreOwnershipRecorder {
351 coordinator: Arc<StoreCommitCoordinator>,
352 epoch: u64,
353 images: parking_lot::Mutex<CheckpointPageImages>,
354}
355
356#[derive(Default)]
357struct CheckpointPageImages {
358 reserved: roaring::RoaringBitmap,
359 captured: roaring::RoaringBitmap,
360}
361
362impl StoreOwnershipRecorder {
363 #[allow(dead_code)]
364 fn begin(coordinator: Arc<StoreCommitCoordinator>, epoch: u64) -> io::Result<Self> {
365 coordinator.append_durable_page_ownership(
366 reddb_file::page_ownership_journal::PageOwnershipRecord::Begin { epoch },
367 )?;
368 Ok(Self {
369 coordinator,
370 epoch,
371 images: parking_lot::Mutex::new(CheckpointPageImages::default()),
372 })
373 }
374
375 fn append_pages(
376 &self,
377 pages: &[u32],
378 make_record: impl Fn(u64, u32) -> reddb_file::page_ownership_journal::PageOwnershipRecord,
379 ) -> io::Result<()> {
380 for chunk in pages.chunks(256) {
381 let records: Vec<_> = chunk
382 .iter()
383 .map(|&id| make_record(self.epoch, id))
384 .collect();
385 self.coordinator
386 .append_durable_page_ownership_batch(&records)?;
387 }
388 Ok(())
389 }
390}
391
392impl crate::storage::engine::pager::PageOwnershipRecorder for StoreOwnershipRecorder {
393 fn needs_page_image(&self, page_id: u32) -> io::Result<bool> {
394 if page_id < reddb_engine::storage::pager_io::FIRST_ALLOCATABLE_PAGE {
397 return Ok(false);
398 }
399 let images = self.images.lock();
400 Ok(!images.reserved.contains(page_id) && !images.captured.contains(page_id))
401 }
402
403 fn capture_page_image(&self, page: &crate::storage::engine::Page) -> io::Result<()> {
404 let mut images = self.images.lock();
405 let page_id = page.page_id();
406 if images.reserved.contains(page_id) || images.captured.contains(page_id) {
407 return Ok(());
408 }
409 let mut bytes = Vec::new();
410 reddb_file::encode_main_wal_record_frame_into(
411 reddb_file::MainWalRecordFrameRef::FullPageImage {
412 tx_id: 0,
413 page_id,
414 ckpt_epoch: self.epoch,
415 data: page.as_bytes(),
416 },
417 crate::storage::wal::record::WAL_DEFAULT_TERM,
418 &mut bytes,
419 )?;
420 self.coordinator.append_durable_ownership_bytes(bytes)?;
421 images.captured.insert(page_id);
422 Ok(())
423 }
424
425 fn reserve_pages(&self, start: u32, count: u32) -> io::Result<()> {
426 use reddb_file::page_ownership_journal::PageOwnershipRecord;
427 let end = start.checked_add(count).ok_or_else(|| {
428 io::Error::new(io::ErrorKind::InvalidInput, "ownership range overflow")
429 })?;
430 let mut next = start;
431 while next < end {
432 let chunk_end = next.saturating_add(256).min(end);
433 let records: Vec<_> = (next..chunk_end)
434 .map(|page_id| PageOwnershipRecord::Reserve {
435 epoch: self.epoch,
436 page_id,
437 })
438 .collect();
439 self.coordinator
440 .append_durable_page_ownership_batch(&records)?;
441 self.images.lock().reserved.insert_range(next..chunk_end);
442 next = chunk_end;
443 }
444 Ok(())
445 }
446
447 fn detach_free_pages(&self, pages: &[u32]) -> io::Result<()> {
448 self.append_pages(pages, |epoch, page_id| {
449 reddb_file::page_ownership_journal::PageOwnershipRecord::DetachFree { epoch, page_id }
450 })
451 }
452
453 fn retire_pages(&self, pages: &[u32]) -> io::Result<()> {
454 self.append_pages(pages, |epoch, page_id| {
455 reddb_file::page_ownership_journal::PageOwnershipRecord::Retire { epoch, page_id }
456 })
457 }
458}
459
460impl StoreCommitCoordinator {
461 pub(crate) fn open(
462 wal_path: impl Into<PathBuf>,
463 mode: DurabilityMode,
464 config: crate::api::GroupCommitOptions,
465 ) -> io::Result<Self> {
466 let wal_path = wal_path.into();
467 let wal = WalWriter::open(&wal_path)?;
468 let initial_durable_lsn = wal.durable_lsn();
469 let initial_current_lsn = wal.current_lsn();
470 let wal = Arc::new(WalMutex::new(wal));
471 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
472 let state = Arc::new((
473 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
474 CommitStateCondvar::new(),
475 ));
476 let fsync_count = Arc::new(AtomicU64::new(0));
477
478 if matches!(
479 mode,
480 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
481 ) {
482 let wal_bg = Arc::clone(&wal);
483 let queue_bg = Arc::clone(&queue);
484 let state_bg = Arc::clone(&state);
485 let fsync_bg = Arc::clone(&fsync_count);
486 let window = Self::resolve_window(&config);
501 let max_statements = config.max_statements.max(1);
502 let max_wal_bytes = config.max_wal_bytes.max(1);
503 std::thread::spawn(move || {
504 Self::run_group_commit_loop(
505 wal_bg,
506 queue_bg,
507 state_bg,
508 fsync_bg,
509 window,
510 max_statements,
511 max_wal_bytes,
512 );
513 });
514 }
515
516 Ok(Self {
517 mode,
518 config,
519 wal_path,
520 wal,
521 queue,
522 state,
523 fsync_count,
524 wal_generation: AtomicU64::new(0),
525 ownership_last_epoch: AtomicU64::new(0),
526 ownership_wal_state: AtomicU8::new(0),
527 runtime_wal_state: AtomicU8::new(0),
528 })
529 }
530
531 pub(super) fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
540 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
541 if let Ok(parsed) = raw.parse::<u64>() {
542 return Duration::from_micros(parsed);
543 }
544 }
545 if config.window_ms != 0 {
546 return Duration::from_millis(config.window_ms);
547 }
548 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
549 }
550
551 #[cfg(test)]
555 pub(crate) fn fsync_count(&self) -> u64 {
556 self.fsync_count.load(Ordering::Relaxed)
557 }
558
559 pub(crate) fn buffered_bytes(&self) -> u64 {
563 self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
564 }
565
566 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
567 self.append_actions_impl(actions, false, false).map(|_| ())
568 }
569
570 pub(super) fn append_checkpoint_actions(
571 &self,
572 actions: &[StoreWalAction],
573 ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
574 self.append_actions_impl(actions, true, false)
575 }
576
577 pub(super) fn append_ready_actions(
580 &self,
581 actions: &[StoreWalAction],
582 retain: bool,
583 ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
584 self.append_actions_impl(actions, retain, true)
585 }
586
587 fn append_actions_impl(
588 &self,
589 actions: &[StoreWalAction],
590 retain: bool,
591 ready: bool,
592 ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
593 use super::checkpoint_wal::CheckpointWalBatch;
594 if actions.is_empty() {
595 return Ok(CheckpointWalBatch::default());
596 }
597 let _epoch = self.queue.epoch.read();
598 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
599 let encoded: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
600 let wal_bytes = encoded.iter().fold(0u64, |total, action| {
601 total.saturating_add(action.len() as u64)
602 });
603 let (blob, mut references) = if retain {
604 CheckpointWalBatch::encode(
605 tx_id,
606 &encoded,
607 self.wal_generation.load(Ordering::Acquire),
608 )?
609 } else {
610 (
611 WalRecord::TxCommitBatch {
612 tx_id,
613 actions: encoded,
614 }
615 .encode(),
616 CheckpointWalBatch::default(),
617 )
618 };
619 let start_lsn = if matches!(self.mode, DurabilityMode::Strict) {
620 let start = self.wal.lock().append_bytes(&blob)?;
621 self.force_sync()?;
622 start
623 } else {
624 let length = blob.len() as u64;
625 let end = self.queue.enqueue(blob);
626 self.wait_until_durable_batch(end, wal_bytes, ready)?;
627 end.checked_sub(length).ok_or_else(|| {
628 io::Error::new(io::ErrorKind::InvalidData, "invalid WAL batch offset")
629 })?
630 };
631 references.relocate(start_lsn)?;
632 Ok(references)
633 }
634
635 pub(super) fn checkpoint_row_matches(
638 &self,
639 reference: super::checkpoint_wal::WalRowReference,
640 expected: &[u8],
641 ) -> io::Result<bool> {
642 if expected.len() != reference.length as usize {
643 return Ok(false);
644 }
645 let mut position = 0;
646 let mut matches = true;
647 let present = self.visit_checkpoint_row(reference, |chunk| {
648 matches &= chunk == &expected[position..position + chunk.len()];
649 position += chunk.len();
650 })?;
651 Ok(present && matches)
652 }
653
654 pub(super) fn checkpoint_row_bytes(
656 &self,
657 reference: super::checkpoint_wal::WalRowReference,
658 ) -> io::Result<Vec<u8>> {
659 let mut bytes = Vec::new();
660 let present = self.visit_checkpoint_row(reference, |chunk| {
661 if bytes.is_empty() {
662 bytes.reserve_exact(reference.length as usize);
663 }
664 bytes.extend_from_slice(chunk);
665 })?;
666 if !present {
667 return Err(io::Error::new(
668 io::ErrorKind::InvalidData,
669 "checkpoint WAL reference changed during projection",
670 ));
671 }
672 Ok(bytes)
673 }
674
675 fn visit_checkpoint_row(
676 &self,
677 reference: super::checkpoint_wal::WalRowReference,
678 mut visit: impl FnMut(&[u8]),
679 ) -> io::Result<bool> {
680 use std::io::{Read, Seek, SeekFrom};
681 let _epoch = self.queue.epoch.read_recursive();
682 if reference.generation != self.wal_generation.load(Ordering::Acquire) {
683 return Ok(false);
684 }
685 let end = reference
686 .offset
687 .checked_add(u64::from(reference.length))
688 .ok_or_else(|| {
689 io::Error::new(io::ErrorKind::InvalidData, "WAL proof offset overflow")
690 })?;
691 let (state_lock, _) = &*self.state;
692 if end > state_lock.lock().durable_lsn {
693 return Ok(false);
694 }
695 let mut file = std::fs::File::open(&self.wal_path)?;
696 file.seek(SeekFrom::Start(reference.offset))?;
697 let mut buffer = [0u8; 16 * 1024];
698 let mut checksum = crc32fast::Hasher::new();
699 let mut remaining = reference.length as usize;
700 while remaining != 0 {
701 let length = remaining.min(buffer.len());
702 let chunk = &mut buffer[..length];
703 file.read_exact(chunk)?;
704 checksum.update(chunk);
705 visit(chunk);
706 remaining -= length;
707 }
708 if checksum.finalize() != reference.checksum {
709 return Err(io::Error::new(
710 io::ErrorKind::InvalidData,
711 "checkpoint WAL proof checksum mismatch",
712 ));
713 }
714 Ok(true)
715 }
716
717 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
722 if matches!(record, WalRecord::PageOwnership { .. }) {
723 return Err(io::Error::new(
724 io::ErrorKind::InvalidInput,
725 "page ownership requires a durable append",
726 ));
727 }
728 let _epoch = self.queue.epoch.read();
729 if matches!(
730 record,
731 WalRecord::ProbabilisticDelta { .. } | WalRecord::VectorInsert { .. }
732 ) {
733 self.runtime_wal_state.store(2, Ordering::Release);
734 }
735 let blob = record.encode();
736 let wal_bytes = blob.len() as u64;
737 if matches!(self.mode, DurabilityMode::Strict) {
738 {
739 let mut wal = self.wal.lock();
740 wal.append(&record)?;
741 }
742 self.force_sync()?;
743 return Ok(());
744 }
745 let commit_lsn = self.queue.enqueue(blob);
746 self.wait_until_durable(commit_lsn, wal_bytes)?;
747 Ok(())
748 }
749
750 #[allow(dead_code)] pub(super) fn append_durable_page_ownership(
754 &self,
755 record: reddb_file::page_ownership_journal::PageOwnershipRecord,
756 ) -> io::Result<()> {
757 self.append_durable_page_ownership_batch(&[record])
758 }
759
760 #[allow(dead_code)] pub(super) fn append_durable_page_ownership_batch(
763 &self,
764 records: &[reddb_file::page_ownership_journal::PageOwnershipRecord],
765 ) -> io::Result<()> {
766 if records.is_empty() {
767 return Ok(());
768 }
769 let mut bytes = Vec::new();
770 for &record in records {
771 reddb_file::page_ownership_journal::encode(record)?;
773 WalRecord::PageOwnership { record }.encode_into(&mut bytes);
774 }
775 for record in records {
776 self.ownership_last_epoch
777 .fetch_max(record.epoch(), Ordering::AcqRel);
778 }
779 self.append_durable_ownership_bytes(bytes)
780 }
781
782 fn append_durable_ownership_bytes(&self, bytes: Vec<u8>) -> io::Result<()> {
783 let _epoch = self.queue.epoch.read();
784 self.ownership_wal_state.store(2, Ordering::Release);
787 if matches!(self.mode, DurabilityMode::Strict) {
788 self.wal.lock().append_bytes(&bytes)?;
789 return self.force_sync();
790 }
791
792 let length = bytes.len() as u64;
793 let target = self.queue.enqueue(bytes);
794 let (state_lock, cond) = &*self.state;
795 let mut state = state_lock.lock();
796 state.pending_target_lsn = state.pending_target_lsn.max(target);
797 state.pending_statements = state.pending_statements.saturating_add(1);
798 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(length);
799 state.first_pending_at.get_or_insert_with(Instant::now);
800 cond.notify_all();
801 loop {
802 if let Some(error) = state.last_error.clone() {
803 return Err(io::Error::other(error));
804 }
805 if state.durable_lsn >= target {
806 return Ok(());
807 }
808 cond.wait(&mut state);
809 }
810 }
811
812 pub(crate) fn force_sync(&self) -> io::Result<()> {
813 let _epoch = self.queue.epoch.read_recursive();
816 {
817 let mut wal = self.wal.lock();
818 wal.sync()?;
819 self.fsync_count.fetch_add(1, Ordering::Relaxed);
820 let durable = wal.durable_lsn();
821 drop(wal);
822 let (state_lock, cond) = &*self.state;
823 let mut state = state_lock.lock();
824 state.durable_lsn = durable;
825 state.pending_target_lsn = durable.max(state.pending_target_lsn);
826 state.pending_statements = 0;
827 state.pending_wal_bytes = 0;
828 state.first_pending_at = None;
829 state.last_error = None;
830 cond.notify_all();
831 }
832 Ok(())
833 }
834
835 fn sync_pending(&self) -> io::Result<()> {
839 if matches!(self.mode, DurabilityMode::Strict) {
840 return self.force_sync();
841 }
842 let _epoch = self.queue.epoch.read();
843 let target = self.queue.pending.lock().next_lsn;
844 let (state_lock, cond) = &*self.state;
845 let mut state = state_lock.lock();
846 state.pending_target_lsn = state.pending_target_lsn.max(target);
847 state.pending_statements = state.pending_statements.saturating_add(1);
848 state.first_pending_at.get_or_insert_with(Instant::now);
849 cond.notify_all();
850 loop {
851 if let Some(error) = &state.last_error {
852 return Err(io::Error::other(error.clone()));
853 }
854 if state.durable_lsn >= target {
855 return Ok(());
856 }
857 cond.wait(&mut state);
858 }
859 }
860
861 pub(crate) fn truncate(&self) -> io::Result<()> {
862 self.truncate_inner(false)
863 }
864
865 pub(super) fn truncate_after_runtime_checkpoint(&self) -> io::Result<()> {
866 self.truncate_inner(true)
867 }
868
869 fn truncate_inner(&self, runtime_snapshots_durable: bool) -> io::Result<()> {
870 let _epoch = self.queue.epoch.write();
871 {
874 let mut wal = self.wal.lock();
875 for (offset, bytes) in self.queue.drain_sorted() {
876 if offset != wal.current_lsn() {
877 return Err(io::Error::other("WAL queue offset differs from writer"));
878 }
879 wal.append_bytes(&bytes)?;
880 }
881 wal.sync()?;
882 }
883 if self.ownership_wal_state.load(Ordering::Acquire) == 0 {
884 let reader = WalReader::open(&self.wal_path)?;
885 let mut protected = false;
886 for item in reader.iter() {
887 match item {
888 Ok((_, WalRecord::PageOwnership { .. })) => {
889 protected = true;
890 break;
891 }
892 Ok(_) => {}
893 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break,
894 Err(error) => return Err(error),
895 }
896 }
897 self.ownership_wal_state
898 .store(if protected { 2 } else { 1 }, Ordering::Release);
899 }
900 if self.ownership_wal_state.load(Ordering::Acquire) == 2 {
901 return Err(io::Error::other(
904 "cannot truncate WAL while page ownership records require recovery",
905 ));
906 }
907 if self.runtime_wal_state.load(Ordering::Acquire) == 0 {
908 let mut protected = false;
909 for item in WalReader::open(&self.wal_path)?.iter() {
910 if matches!(
911 item?.1,
912 WalRecord::ProbabilisticDelta { .. } | WalRecord::VectorInsert { .. }
913 ) {
914 protected = true;
915 }
916 }
917 self.runtime_wal_state
918 .store(if protected { 2 } else { 1 }, Ordering::Release);
919 }
920 self.wal_generation.fetch_add(1, Ordering::AcqRel);
921 let mut wal = self.wal.lock();
922 let retain_runtime =
923 !runtime_snapshots_durable && self.runtime_wal_state.load(Ordering::Acquire) == 2;
924 if retain_runtime {
925 self.retain_runtime_wal(&mut wal)?;
926 } else {
927 wal.truncate()?;
928 self.runtime_wal_state.store(1, Ordering::Release);
929 }
930 let durable = wal.durable_lsn();
931 let current = wal.current_lsn();
932 drop(wal);
933
934 self.queue.reset(current);
939
940 let (state_lock, cond) = &*self.state;
941 let mut state = state_lock.lock();
942 state.durable_lsn = durable;
943 state.pending_target_lsn = durable;
944 state.pending_statements = 0;
945 state.pending_wal_bytes = 0;
946 state.first_pending_at = None;
947 state.last_error = None;
948 cond.notify_all();
949 drop(state);
950 if retain_runtime {
951 use reddb_file::Vfs;
952 let parent = self
953 .wal_path
954 .parent()
955 .filter(|p| !p.as_os_str().is_empty())
956 .unwrap_or_else(|| Path::new("."));
957 reddb_file::StdVfs.sync_dir(parent)?;
958 }
959 Ok(())
960 }
961
962 fn retain_runtime_wal(&self, wal: &mut WalWriter) -> io::Result<()> {
966 use std::io::{Read, Seek, SeekFrom, Write};
967 static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
968 let mut name = self
969 .wal_path
970 .file_name()
971 .ok_or_else(|| io::Error::other("WAL filename missing"))?
972 .to_os_string();
973 name.push(format!(
974 ".{}.{}.runtime.tmp",
975 std::process::id(),
976 NEXT_TEMP.fetch_add(1, Ordering::Relaxed)
977 ));
978 let temporary = self.wal_path.with_file_name(name);
979 let mut options = std::fs::OpenOptions::new();
980 options.write(true).create_new(true);
981 #[cfg(unix)]
982 {
983 use std::os::unix::fs::OpenOptionsExt;
984 options.mode(0o600);
985 }
986 let mut temp_file = options.open(&temporary)?;
987 let result = (|| -> io::Result<()> {
988 let mut source = std::fs::File::open(&self.wal_path)?;
989 let mut header = [0u8; reddb_file::WAL_FILE_HEADER_BYTES];
990 source.read_exact(&mut header)?;
991 reddb_file::decode_wal_file_header(&header)
992 .map_err(|error| io::Error::other(error.to_string()))?;
993 temp_file.write_all(&header)?;
994 temp_file.sync_all()?;
995 drop(temp_file);
996 let mut retained = WalWriter::open(&temporary)?;
997 let mut previous = None;
998 let end = wal.current_lsn();
999 let mut buffer = [0u8; 64 * 1024];
1000 let mut copy_frame = |start: u64, end: u64| -> io::Result<()> {
1001 source.seek(SeekFrom::Start(start))?;
1002 let mut remaining = end
1003 .checked_sub(start)
1004 .ok_or_else(|| io::Error::other("invalid WAL frame boundary"))?;
1005 while remaining > 0 {
1006 let length = usize::try_from(remaining.min(buffer.len() as u64))
1007 .map_err(|_| io::Error::other("invalid WAL frame length"))?;
1008 source.read_exact(&mut buffer[..length])?;
1009 retained.append_bytes(&buffer[..length])?;
1010 remaining -= length as u64;
1011 }
1012 Ok(())
1013 };
1014 for item in WalReader::open(&self.wal_path)?.iter() {
1015 let (offset, record) = item?;
1016 if let Some((start, true)) = previous.take() {
1017 copy_frame(start, offset)?;
1018 }
1019 previous = Some((
1020 offset,
1021 matches!(
1022 record,
1023 WalRecord::ProbabilisticDelta { .. } | WalRecord::VectorInsert { .. }
1024 ),
1025 ));
1026 }
1027 if let Some((start, true)) = previous {
1028 copy_frame(start, end)?;
1029 }
1030 drop(source);
1031 retained.sync()?;
1032 #[cfg(test)]
1033 if std::env::var_os("REDDB_TEST_EXIT_BEFORE_RUNTIME_WAL_RENAME").is_some() {
1034 std::process::exit(211);
1035 }
1036 std::fs::rename(&temporary, &self.wal_path)?;
1037 #[cfg(test)]
1038 if std::env::var_os("REDDB_TEST_EXIT_AFTER_RUNTIME_WAL_RENAME").is_some() {
1039 std::process::exit(212);
1040 }
1041 *wal = retained;
1042 Ok(())
1043 })();
1044 if result.is_err() {
1045 let _ = std::fs::remove_file(&temporary);
1046 }
1047 result
1048 }
1049
1050 fn release_completed_ownership_wal(&self) -> io::Result<()> {
1051 use reddb_file::page_ownership_journal::{encode, pending_epoch, PageOwnershipRecord};
1052 let _epoch = self.queue.epoch.write();
1053 let mut journal = Vec::new();
1054 let mut last = None;
1055 for item in WalReader::open(&self.wal_path)?.iter() {
1056 match item {
1057 Ok((_, WalRecord::PageOwnership { record })) => {
1058 journal.extend_from_slice(&encode(record)?);
1059 last = Some(record);
1060 }
1061 Ok(_) => {}
1062 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
1063 return Err(io::Error::new(
1064 io::ErrorKind::InvalidData,
1065 "incomplete WAL before ownership completion",
1066 ));
1067 }
1068 Err(error) => return Err(error),
1069 }
1070 }
1071 if pending_epoch(&journal)?.is_some()
1072 || !matches!(last, Some(PageOwnershipRecord::Finish { .. }))
1073 {
1074 return Err(io::Error::other("page ownership WAL is not complete"));
1075 }
1076 self.ownership_wal_state.store(1, Ordering::Release);
1077 Ok(())
1078 }
1079
1080 fn recover_page_ownership(
1081 coordinator: Option<&Self>,
1082 store: &UnifiedStore,
1083 journal: &[u8],
1084 ) -> io::Result<()> {
1085 use reddb_file::page_ownership_journal::{
1086 pending_epoch, recovery_plan, PageOwnershipRecord,
1087 };
1088 if journal.is_empty() {
1089 return Ok(());
1090 }
1091 let pager = store
1092 .pager
1093 .as_ref()
1094 .ok_or_else(|| io::Error::other("ownership WAL requires a pager"))?;
1095 let header = pager
1096 .header()
1097 .map_err(|error| io::Error::other(error.to_string()))?;
1098 if header.version < reddb_file::file_format::PAGE_OWNERSHIP_FILE_VERSION {
1099 return Err(io::Error::other(
1100 "page ownership journal requires recovery with a fenced file version",
1101 ));
1102 }
1103 let pending = pending_epoch(journal).map_err(|error| {
1104 io::Error::other(format!("page ownership journal requires recovery: {error}"))
1105 })?;
1106 if let Some(mut pending) = pending {
1107 let catalog_epoch = store.ownership_catalog_epoch.load(Ordering::Acquire);
1108 let (_, retained_pages) = pager
1109 .deferred_reclamation_pages()
1110 .map_err(|error| io::Error::other(error.to_string()))?;
1111 if catalog_epoch == pending.epoch {
1115 for page in &retained_pages {
1116 pending.retired_pages.remove(page);
1117 }
1118 }
1119 let mut plan = recovery_plan(
1120 &pending,
1121 catalog_epoch,
1122 reddb_engine::storage::pager_io::FIRST_ALLOCATABLE_PAGE,
1123 header.page_count,
1124 )?;
1125 if retained_pages
1126 .iter()
1127 .any(|page| plan.release.binary_search(page).is_ok())
1128 {
1129 return Err(io::Error::other(
1130 "ownership recovery would release a catalog-retained page",
1131 ));
1132 }
1133 plan.preserve.extend(retained_pages);
1134 plan.preserve.sort_unstable();
1135 plan.preserve.dedup();
1136 pager
1137 .validate_owned_pages(&plan.preserve)
1138 .map_err(|error| io::Error::other(error.to_string()))?;
1139 let Some(coordinator) = coordinator else {
1140 return Ok(());
1142 };
1143 pager
1144 .recover_unowned_pages(&plan.release)
1145 .map_err(|error| io::Error::other(error.to_string()))?;
1146 #[cfg(test)]
1147 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY").is_some() {
1148 std::process::exit(187);
1149 }
1150 pager
1151 .sync_recovered_allocator()
1152 .map_err(|error| io::Error::other(error.to_string()))?;
1153 #[cfg(test)]
1154 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC").is_some() {
1155 std::process::exit(188);
1156 }
1157 coordinator.append_durable_page_ownership(PageOwnershipRecord::Finish {
1158 epoch: pending.epoch,
1159 })?;
1160 #[cfg(test)]
1161 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH").is_some() {
1162 std::process::exit(189);
1163 }
1164 }
1165 coordinator.map_or(Ok(()), Self::release_completed_ownership_wal)
1166 }
1167
1168 pub(super) fn next_page_ownership_epoch(&self, store: &UnifiedStore) -> io::Result<u64> {
1169 self.ownership_last_epoch
1170 .load(Ordering::Acquire)
1171 .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1172 .checked_add(1)
1173 .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))
1174 }
1175
1176 pub(super) fn begin_page_ownership_epoch(
1178 &self,
1179 store: &UnifiedStore,
1180 epoch: u64,
1181 ) -> io::Result<()> {
1182 if store.ownership_active_epoch.load(Ordering::Acquire) != 0
1183 || epoch <= store.ownership_catalog_epoch.load(Ordering::Acquire)
1184 || epoch <= self.ownership_last_epoch.load(Ordering::Acquire)
1185 {
1186 return Err(io::Error::other("invalid page ownership epoch"));
1187 }
1188 let pager = store
1189 .pager
1190 .as_ref()
1191 .ok_or_else(|| io::Error::other("ownership epoch requires a pager"))?;
1192 let format_version = if !store.secondary_index_specs.read().is_empty() {
1193 reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1194 } else if store.config.resident_memory_bytes_max.is_some() {
1195 reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1196 } else {
1197 reddb_file::file_format::PAGE_CHECKPOINT_UNDO_FILE_VERSION
1198 };
1199 let format_version = format_version.max(
1202 pager
1203 .header()
1204 .map_err(|error| io::Error::other(error.to_string()))?
1205 .version,
1206 );
1207 pager
1208 .fence_file_format_version(format_version)
1209 .map_err(|error| io::Error::other(error.to_string()))?;
1210 let coordinator = Arc::clone(
1211 store
1212 .commit
1213 .as_ref()
1214 .ok_or_else(|| io::Error::other("ownership epoch requires a WAL coordinator"))?,
1215 );
1216 let recorder = Arc::new(StoreOwnershipRecorder::begin(coordinator, epoch)?);
1217 pager.install_ownership_recorder(recorder)?;
1218 store.ownership_active_epoch.store(epoch, Ordering::Release);
1219 Ok(())
1220 }
1221
1222 pub(super) fn finish_page_ownership_epoch(
1224 &self,
1225 store: &UnifiedStore,
1226 epoch: u64,
1227 ) -> io::Result<()> {
1228 self.finish_page_ownership_epoch_inner(store, epoch, true)
1229 }
1230
1231 pub(super) fn finish_count_batch_ownership_epoch(
1235 &self,
1236 store: &UnifiedStore,
1237 epoch: u64,
1238 ) -> io::Result<()> {
1239 self.finish_page_ownership_epoch_inner(store, epoch, false)
1240 }
1241
1242 fn finish_page_ownership_epoch_inner(
1243 &self,
1244 store: &UnifiedStore,
1245 epoch: u64,
1246 validate_for_truncation: bool,
1247 ) -> io::Result<()> {
1248 if epoch == 0 || store.ownership_active_epoch.load(Ordering::Acquire) != epoch {
1249 return Err(io::Error::other("page ownership epoch is not active"));
1250 }
1251 if store.ownership_catalog_epoch.load(Ordering::Acquire) != epoch {
1252 return Err(io::Error::other("page ownership epoch is not published"));
1253 }
1254 let pager = store
1255 .pager
1256 .as_ref()
1257 .ok_or_else(|| io::Error::other("ownership epoch requires a pager"))?;
1258 pager
1259 .sync_recovered_allocator()
1260 .map_err(|error| io::Error::other(error.to_string()))?;
1261 self.append_durable_page_ownership(
1262 reddb_file::page_ownership_journal::PageOwnershipRecord::Finish { epoch },
1263 )?;
1264 pager.clear_ownership_recorder()?;
1265 store.ownership_active_epoch.store(0, Ordering::Release);
1266 if validate_for_truncation {
1267 self.release_completed_ownership_wal()
1268 } else {
1269 Ok(())
1270 }
1271 }
1272
1273 pub(crate) fn replay_into(
1274 &self,
1275 store: &UnifiedStore,
1276 boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1277 ) -> io::Result<()> {
1278 Self::replay_from_wal(store, &self.wal_path, Some(self), boot_wal)
1279 }
1280
1281 pub(crate) fn replay_read_only(store: &UnifiedStore, wal_path: &Path) -> io::Result<()> {
1282 Self::replay_from_wal(store, wal_path, None, None)
1283 }
1284
1285 fn replay_from_wal(
1286 store: &UnifiedStore,
1287 wal_path: &Path,
1288 coordinator: Option<&Self>,
1289 boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1290 ) -> io::Result<()> {
1291 if !wal_path.exists() {
1292 return Ok(());
1293 }
1294
1295 let reader = match WalReader::open(wal_path) {
1296 Ok(reader) => reader,
1297 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
1298 Err(err) => return Err(err),
1299 };
1300
1301 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
1302 let mut has_page_writes = false;
1303 let mut ownership_journal = Vec::new();
1304 let mut latest_ownership_epoch = 0;
1305 let mut incomplete_wal_tail = false;
1306 let mut applied = false;
1307
1308 for record in reader.iter() {
1309 let (lsn, record) = match record {
1310 Ok(record) => record,
1311 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
1312 incomplete_wal_tail = true;
1313 break;
1314 }
1315 Err(err) => return Err(err),
1316 };
1317 match record {
1318 WalRecord::PageOwnership { record } => {
1319 latest_ownership_epoch = latest_ownership_epoch.max(record.epoch());
1320 if let Some(coordinator) = coordinator {
1321 coordinator
1322 .ownership_last_epoch
1323 .fetch_max(record.epoch(), Ordering::AcqRel);
1324 }
1325 ownership_journal
1326 .extend_from_slice(&reddb_file::page_ownership_journal::encode(record)?);
1327 }
1328 WalRecord::TxCommitBatch { actions, .. } => {
1329 for (index, payload) in actions.into_iter().enumerate() {
1330 if boot_wal.is_some_and(|proof| proof.skips_action(lsn, index)) {
1331 continue;
1332 }
1333 let action = StoreWalAction::decode(&payload)?;
1334 applied = true;
1335 store.apply_replayed_action(&action, false).map_err(|err| {
1336 io::Error::other(format!("failed to replay store wal action: {err}"))
1337 })?;
1338 store.remember_checkpoint_rows(vec![action], Default::default());
1342 }
1343 }
1344 WalRecord::Begin { tx_id } => {
1345 tx_states.insert(tx_id, false);
1346 }
1347 WalRecord::Commit { tx_id } => {
1348 tx_states.insert(tx_id, true);
1349 }
1350 WalRecord::Rollback { tx_id } => {
1351 tx_states.remove(&tx_id);
1352 }
1353 WalRecord::PageWrite { .. } => has_page_writes = true,
1354 WalRecord::Checkpoint { .. } => {}
1355 WalRecord::VectorInsert {
1356 collection,
1357 entity_id,
1358 vector,
1359 } => {
1360 if let Some(coordinator) = coordinator {
1361 coordinator.runtime_wal_state.store(2, Ordering::Release);
1362 }
1363 let mut map = store.replayed_turbo_inserts.lock();
1369 map.entry(
1370 reddb_engine::storage::system_collection_names::canonical_owned(collection),
1371 )
1372 .or_default()
1373 .push((entity_id, vector));
1374 }
1375 WalRecord::ProbabilisticDelta {
1376 kind,
1377 operation,
1378 name,
1379 operands,
1380 } => {
1381 if let Some(coordinator) = coordinator {
1382 coordinator.runtime_wal_state.store(2, Ordering::Release);
1383 }
1384 let mut deltas = store.replayed_probabilistic_deltas.lock();
1385 deltas.push((kind, operation, name, operands));
1386 }
1387 WalRecord::FullPageImage { .. } => {
1388 }
1390 }
1391 }
1392
1393 if incomplete_wal_tail && !ownership_journal.is_empty() {
1394 return Err(io::Error::new(
1395 io::ErrorKind::InvalidData,
1396 "incomplete WAL prevents ownership recovery",
1397 ));
1398 }
1399 Self::recover_page_ownership(coordinator, store, &ownership_journal)?;
1400
1401 if has_page_writes {
1402 for item in WalReader::open(wal_path)?.iter() {
1406 let record = match item {
1407 Ok((_, record)) => record,
1408 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
1409 Err(err) => return Err(err),
1410 };
1411 let WalRecord::PageWrite { tx_id, data, .. } = record else {
1412 continue;
1413 };
1414 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
1415 continue;
1416 }
1417 let action = StoreWalAction::decode(&data)?;
1418 applied = true;
1419 store.apply_replayed_action(&action, false).map_err(|err| {
1420 io::Error::other(format!("failed to replay store wal action: {err}"))
1421 })?;
1422 store.remember_checkpoint_rows(vec![action], Default::default());
1423 }
1424 }
1425
1426 let Some(coordinator) = coordinator else {
1427 return Ok(());
1428 };
1429 if applied && store.config.resident_memory_bytes_max.is_some() {
1430 return Ok(());
1434 }
1435 if applied {
1436 let epoch = latest_ownership_epoch
1439 .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1440 .checked_add(1)
1441 .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))?;
1442 coordinator.begin_page_ownership_epoch(store, epoch)?;
1443 store.mark_paged_registry_dirty();
1446 store
1447 .reconcile_replayed_btrees()
1448 .map_err(|err| io::Error::other(err.to_string()))?;
1449 coordinator.finish_page_ownership_epoch(store, epoch)?;
1450 }
1451 Ok(())
1452 }
1453
1454 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
1455 self.wait_until_durable_batch(target_lsn, wal_bytes, false)
1456 }
1457
1458 fn wait_until_durable_batch(
1459 &self,
1460 target_lsn: u64,
1461 wal_bytes: u64,
1462 ready: bool,
1463 ) -> io::Result<()> {
1464 match self.mode {
1465 DurabilityMode::Strict => self.force_sync(),
1466 DurabilityMode::Async => {
1471 let (state_lock, cond) = &*self.state;
1472 let mut state = state_lock.lock();
1473 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1474 state.pending_statements = state.pending_statements.saturating_add(1);
1475 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1476 state.first_pending_at.get_or_insert_with(Instant::now);
1477 cond.notify_all();
1478 Ok(())
1479 }
1480 DurabilityMode::WalDurableGrouped => {
1481 let (state_lock, cond) = &*self.state;
1482 let mut state = state_lock.lock();
1483 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1484 state.pending_statements = state.pending_statements.saturating_add(1);
1485 if ready {
1486 state.pending_statements = state
1487 .pending_statements
1488 .max(self.config.max_statements.max(1));
1489 }
1490 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1491 state.first_pending_at.get_or_insert_with(Instant::now);
1492 cond.notify_all();
1493
1494 loop {
1495 if let Some(err) = state.last_error.clone() {
1496 return Err(io::Error::other(err));
1497 }
1498 if state.durable_lsn >= target_lsn {
1499 return Ok(());
1500 }
1501 cond.wait(&mut state);
1504 }
1505 }
1506 }
1507 }
1508
1509 fn run_group_commit_loop(
1510 wal: Arc<WalMutex>,
1511 queue: Arc<WalAppendQueue>,
1512 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
1513 fsync_count: Arc<AtomicU64>,
1514 window: Duration,
1515 max_statements: usize,
1516 max_wal_bytes: u64,
1517 ) {
1518 let (state_lock, cond) = &*state;
1519 loop {
1520 let target_lsn = {
1521 let mut guard = state_lock.lock();
1522
1523 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
1524 cond.wait(&mut guard);
1525 }
1526
1527 if guard.shutdown {
1528 return;
1529 }
1530
1531 let immediate = window.is_zero()
1532 || guard.pending_statements >= max_statements
1533 || guard.pending_wal_bytes >= max_wal_bytes;
1534
1535 if !immediate {
1536 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
1537 let now = Instant::now();
1538 if now < deadline {
1539 let timeout = deadline.saturating_duration_since(now);
1540 let _ = cond.wait_for(&mut guard, timeout);
1541 if guard.shutdown {
1542 return;
1543 }
1544 if guard.pending_target_lsn <= guard.durable_lsn {
1545 continue;
1546 }
1547 let should_wait_again = guard.pending_statements < max_statements
1548 && guard.pending_wal_bytes < max_wal_bytes
1549 && guard
1550 .first_pending_at
1551 .map(|first| first.elapsed() < window)
1552 .unwrap_or(false);
1553 if should_wait_again {
1554 continue;
1555 }
1556 }
1557 }
1558
1559 guard.pending_target_lsn
1560 };
1561
1562 let _epoch = queue.epoch.read_recursive();
1567
1568 let batches = queue.drain_sorted();
1574
1575 let sync_result = {
1576 let mut wal = wal.lock();
1577 let mut write_err: Option<io::Error> = None;
1578 for (_lsn, bytes) in batches {
1579 if let Err(e) = wal.append_bytes(&bytes) {
1580 write_err = Some(e);
1581 break;
1582 }
1583 }
1584 match write_err {
1585 Some(e) => Err(e),
1586 None => wal.sync().map(|_| {
1587 fsync_count.fetch_add(1, Ordering::Relaxed);
1592 wal.durable_lsn()
1593 }),
1594 }
1595 };
1596
1597 let more_pending = queue.has_pending();
1602
1603 let mut guard = state_lock.lock();
1604 match sync_result {
1605 Ok(durable_lsn) => {
1606 guard.durable_lsn = durable_lsn;
1607 if !more_pending {
1608 guard.pending_statements = 0;
1609 guard.pending_wal_bytes = 0;
1610 guard.first_pending_at = None;
1611 }
1612 guard.last_error = None;
1613 let _ = target_lsn;
1614 }
1615 Err(err) => {
1616 guard.last_error = Some(err.to_string());
1617 }
1618 }
1619 cond.notify_all();
1620 }
1621 }
1622}
1623
1624impl Drop for StoreCommitCoordinator {
1625 fn drop(&mut self) {
1626 let (state_lock, cond) = &*self.state;
1627 let mut state = state_lock.lock();
1629 state.shutdown = true;
1630 cond.notify_all();
1631 }
1632}
1633
1634impl UnifiedStore {
1635 pub(crate) fn sync_control_metadata(&self) -> Result<(), StoreError> {
1636 if let Some(commit) = &self.commit {
1637 commit.sync_pending().map_err(StoreError::Io)?;
1638 }
1639 Ok(())
1641 }
1642
1643 pub(crate) fn deferred_store_wal_capture_active() -> bool {
1644 deferred_store_wal_capture_active()
1645 }
1646
1647 pub(crate) fn begin_deferred_store_wal_capture() {
1648 begin_deferred_store_wal_capture();
1649 }
1650
1651 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
1652 take_deferred_store_wal_capture()
1653 }
1654
1655 pub(crate) fn append_deferred_store_wal_actions(
1656 &self,
1657 actions: DeferredStoreWalActions,
1658 ) -> Result<(), StoreError> {
1659 if actions.actions.is_empty() {
1660 return Ok(());
1661 }
1662 #[cfg(test)]
1663 if self
1664 .test_require_deferred_publication_fence
1665 .swap(false, Ordering::AcqRel)
1666 && !self.vault_publication_lock.is_owned_by_current_thread()
1667 {
1668 return Err(StoreError::Io(io::Error::other(
1669 "deferred WAL missing checkpoint publication fence",
1670 )));
1671 }
1672 #[cfg(test)]
1673 if self
1674 .test_fail_deferred_wal_append
1675 .swap(false, Ordering::AcqRel)
1676 {
1677 return Err(StoreError::Io(io::Error::other(
1678 "injected deferred WAL append failure",
1679 )));
1680 }
1681 if self.config.embedded_wal_path.is_some() {
1682 return self.append_embedded_store_wal_actions(&actions.actions);
1683 }
1684 if let Some(commit) = &self.commit {
1687 let references = commit
1688 .append_checkpoint_actions(&actions.actions)
1689 .map_err(StoreError::Io)?;
1690 self.remember_checkpoint_rows(actions.actions, references);
1691 Ok(())
1692 } else {
1693 self.flush_paged_state()
1694 }
1695 }
1696
1697 pub(crate) fn append_vector_insert_record(
1704 &self,
1705 collection: &str,
1706 entity_id: u64,
1707 vector: &[f32],
1708 ) -> std::io::Result<()> {
1709 let Some(commit) = &self.commit else {
1710 return Ok(());
1711 };
1712 let record = WalRecord::VectorInsert {
1713 collection: collection.to_string(),
1714 entity_id,
1715 vector: vector.to_vec(),
1716 };
1717 commit.append_single_record(record)
1718 }
1719
1720 pub(crate) fn append_probabilistic_delta_record(
1721 &self,
1722 kind: u8,
1723 operation: u8,
1724 name: &str,
1725 operands: Vec<Vec<u8>>,
1726 ) -> std::io::Result<()> {
1727 let Some(commit) = &self.commit else {
1728 return Ok(());
1729 };
1730 let record = WalRecord::ProbabilisticDelta {
1731 kind,
1732 operation,
1733 name: name.to_string(),
1734 operands,
1735 };
1736 commit.append_single_record(record)
1737 }
1738
1739 pub(crate) fn finish_paged_write(
1740 &self,
1741 actions: impl IntoIterator<Item = StoreWalAction>,
1742 ) -> Result<(), StoreError> {
1743 self.finish_paged_write_for_projection(
1744 actions,
1745 self.config.resident_memory_bytes_max.is_some(),
1746 )
1747 }
1748
1749 pub(crate) fn finish_paged_write_for_projection(
1750 &self,
1751 actions: impl IntoIterator<Item = StoreWalAction>,
1752 track: bool,
1753 ) -> Result<(), StoreError> {
1754 if self.is_read_only() {
1755 return Ok(());
1756 }
1757 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
1758 if deferred_store_wal_capture_active() {
1759 let captured = capture_deferred_store_wal_actions(actions);
1760 debug_assert!(captured);
1761 return Ok(());
1762 }
1763 if self.config.embedded_wal_path.is_some() {
1764 return self.append_embedded_store_wal_actions(&actions);
1765 }
1766 let mut references = super::checkpoint_wal::CheckpointWalBatch::default();
1767 let result = match self.config.durability_mode {
1768 DurabilityMode::Strict => {
1769 if let Some(commit) = &self.commit {
1770 if track {
1771 references = commit
1772 .append_checkpoint_actions(&actions)
1773 .map_err(StoreError::Io)?;
1774 } else {
1775 commit.append_actions(&actions).map_err(StoreError::Io)?;
1776 }
1777 }
1778 self.flush_paged_state()
1779 }
1780 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
1781 if let Some(commit) = &self.commit {
1782 if track {
1783 references = commit
1784 .append_checkpoint_actions(&actions)
1785 .map_err(StoreError::Io)?;
1786 } else {
1787 commit.append_actions(&actions).map_err(StoreError::Io)?;
1788 }
1789 Ok(())
1790 } else {
1791 self.flush_paged_state()
1792 }
1793 }
1794 };
1795 result?;
1796 if track {
1797 self.remember_checkpoint_rows(actions, references);
1798 }
1799 Ok(())
1800 }
1801
1802 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
1803 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
1804 self.apply_replayed_action(&action, true)
1805 }
1806
1807 fn append_embedded_store_wal_actions(
1808 &self,
1809 actions: &[StoreWalAction],
1810 ) -> Result<(), StoreError> {
1811 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1812 return Ok(());
1813 };
1814 if actions.is_empty() {
1815 return Ok(());
1816 }
1817 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1818 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1819 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1820 match self.append_embedded_payloads(path, &payloads) {
1821 Ok(_) => Ok(()),
1822 Err(crate::api::RedDBError::InvalidOperation(msg))
1823 if msg.contains("embedded wal region full") =>
1824 {
1825 self.recover_full_embedded_wal(path, &payloads, encoded_len)
1826 }
1827 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1828 }
1829 }
1830
1831 fn recover_full_embedded_wal(
1832 &self,
1833 path: &Path,
1834 payloads: &[Vec<u8>],
1835 encoded_len: u64,
1836 ) -> Result<(), StoreError> {
1837 let _vault_guard = self.vault_publication_lock.lock();
1838 match self.append_embedded_payloads(path, payloads) {
1842 Ok(_) => return Ok(()),
1843 Err(crate::api::RedDBError::InvalidOperation(message))
1844 if message.contains("embedded wal region full") => {}
1845 Err(error) => return Err(StoreError::Io(std::io::Error::other(error.to_string()))),
1846 }
1847 struct CheckpointFence<'a>(Option<&'a crate::storage::embedded::EmbeddedCommitSession>);
1851 impl Drop for CheckpointFence<'_> {
1852 fn drop(&mut self) {
1853 if let Some(session) = self.0 {
1854 session.fence_interrupted_write();
1855 }
1856 }
1857 }
1858 let mut fence = CheckpointFence(Some(
1859 self.embedded_commit_session
1860 .get()
1861 .expect("WAL-full recovery follows an append attempt"),
1862 ));
1863 let started = Instant::now();
1864 let snapshot = self.to_binary_dump_bytes();
1865 let capacity = encoded_len.max((snapshot.len() as u64).min(EMBEDDED_WAL_HEADROOM_LIMIT));
1866 let checkpoint = crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
1867 path, &snapshot, capacity,
1868 )
1869 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1870 fence.0 = None;
1871 tracing::debug!(
1872 target: "reddb::embedded_checkpoint",
1873 snapshot_bytes = snapshot.len(),
1874 wal_region_bytes = checkpoint.manifest.wal_region_bytes,
1875 elapsed_us = started.elapsed().as_micros() as u64,
1876 "embedded WAL overflow checkpoint"
1877 );
1878 self.append_embedded_payloads(path, payloads)
1879 .map(|_| ())
1880 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1881 }
1882
1883 fn append_embedded_payloads(
1884 &self,
1885 path: &Path,
1886 payloads: &[Vec<u8>],
1887 ) -> crate::RedDBResult<crate::storage::EmbeddedRdbOpen> {
1888 let session = self
1889 .embedded_commit_session
1890 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1891 let options = self.config.group_commit;
1892 let grouped = self.config.durability_mode == DurabilityMode::WalDurableGrouped;
1893 let window = if grouped {
1894 StoreCommitCoordinator::resolve_window(&options)
1895 } else {
1896 Duration::ZERO
1897 };
1898 session.append(
1899 payloads,
1900 reddb_file::embedded::EmbeddedGroupCommitOptions {
1901 window,
1902 max_statements: if grouped { options.max_statements } else { 1 },
1903 max_wal_bytes: options.max_wal_bytes,
1904 },
1905 )
1906 }
1907
1908 pub(super) fn enqueue_grouped_actions(
1909 &self,
1910 actions: &[StoreWalAction],
1911 ) -> Result<Option<crate::storage::embedded::PendingEmbeddedAppend>, StoreError> {
1912 if self.config.durability_mode != DurabilityMode::WalDurableGrouped
1913 || deferred_store_wal_capture_active()
1914 {
1915 return Ok(None);
1916 }
1917 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1918 return Ok(None);
1919 };
1920 let session = self
1921 .embedded_commit_session
1922 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1923 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1924 let options = self.config.group_commit;
1925 session
1926 .enqueue(
1927 &payloads,
1928 reddb_file::embedded::EmbeddedGroupCommitOptions {
1929 window: StoreCommitCoordinator::resolve_window(&options),
1930 max_statements: options.max_statements,
1931 max_wal_bytes: options.max_wal_bytes,
1932 },
1933 )
1934 .map(Some)
1935 .map_err(|error| StoreError::Io(io::Error::other(error.to_string())))
1936 }
1937
1938 pub(super) fn complete_grouped_actions(
1939 &self,
1940 pending: &crate::storage::embedded::PendingEmbeddedAppend,
1941 actions: &[StoreWalAction],
1942 ) -> Result<(), StoreError> {
1943 match pending.wait() {
1944 Ok(_) => Ok(()),
1945 Err(crate::RedDBError::InvalidOperation(message))
1946 if message.contains("embedded wal region full") =>
1947 {
1948 self.append_embedded_store_wal_actions(actions)
1949 }
1950 Err(error) => Err(StoreError::Io(io::Error::other(error.to_string()))),
1951 }
1952 }
1953
1954 pub(crate) fn apply_replayed_action(
1955 &self,
1956 action: &StoreWalAction,
1957 update_btrees: bool,
1958 ) -> Result<(), StoreError> {
1959 reddb_engine::storage::store_wal::apply_replayed_action(
1960 &NativeReplayTarget {
1961 store: self,
1962 update_btrees,
1963 },
1964 action,
1965 )
1966 }
1967
1968 pub fn refresh_collection(
1985 &self,
1986 name: &str,
1987 entities: Vec<UnifiedEntity>,
1988 ) -> Result<Vec<Vec<u8>>, StoreError> {
1989 let name = Self::canonical_collection(name);
1990 let fv = self.format_version();
1991
1992 let new_manager = Arc::new(SegmentManager::with_config(
1995 name,
1996 self.config.manager_config.clone(),
1997 ));
1998
1999 let mut prepared = entities;
2000 for entity in &mut prepared {
2001 entity.canonicalize_system_collection_names();
2002 if entity.id.raw() == 0 {
2003 entity.id = self.next_entity_id();
2004 } else {
2005 self.register_entity_id(entity.id);
2006 }
2007 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
2008 if *row_id == 0 {
2009 *row_id = new_manager.next_row_id();
2010 } else {
2011 new_manager.register_row_id(*row_id);
2012 }
2013 }
2014 entity.ensure_table_logical_id();
2015 }
2016
2017 let serialized: Vec<Vec<u8>> = prepared
2018 .iter()
2019 .map(|e| Self::serialize_entity_record(e, None, fv))
2020 .collect();
2021
2022 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
2023 StoreError::Io(std::io::Error::other(format!(
2024 "refresh_collection: bulk_insert into new manager failed: {e}"
2025 )))
2026 })?;
2027
2028 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
2029
2030 self.finish_paged_write([StoreWalAction::RefreshCollection {
2031 collection: name.to_string(),
2032 records: serialized.clone(),
2033 }])?;
2034
2035 Ok(serialized)
2036 }
2037
2038 fn swap_collection_state(
2044 &self,
2045 name: &str,
2046 new_manager: Arc<SegmentManager>,
2047 prepared: &[UnifiedEntity],
2048 serialized: &[Vec<u8>],
2049 update_btrees: bool,
2050 ) {
2051 self.resident
2052 .swap_collection_state(name, new_manager, prepared);
2053
2054 if let Some(pager) = self
2055 .pager
2056 .as_ref()
2057 .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
2058 {
2059 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
2060 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
2061 .iter()
2062 .zip(serialized.iter())
2063 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
2064 .collect();
2065 sorted.sort_by(|a, b| a.0.cmp(&b.0));
2066 if !sorted.is_empty() {
2067 let _ = new_btree.bulk_insert_sorted(&sorted);
2068 }
2069 self.btree_indices
2070 .write()
2071 .insert(name.to_string(), new_btree);
2072 self.mark_paged_registry_dirty();
2073 }
2074 }
2075
2076 fn apply_replayed_refresh_collection(
2077 &self,
2078 collection: &str,
2079 records: &[Vec<u8>],
2080 update_btrees: bool,
2081 ) -> Result<(), StoreError> {
2082 let collection = Self::canonical_collection(collection);
2083 let (new_manager, prepared) = self.resident.prepare_replayed_refresh_collection(
2084 collection,
2085 records,
2086 self.format_version(),
2087 &self.config.manager_config,
2088 )?;
2089 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
2090 Ok(())
2091 }
2092
2093 pub fn refresh_collection_from_records(
2107 &self,
2108 name: &str,
2109 records: Vec<Vec<u8>>,
2110 ) -> Result<(), StoreError> {
2111 let name = Self::canonical_collection(name);
2112 self.apply_replayed_refresh_collection(name, &records, true)?;
2113 self.finish_paged_write([StoreWalAction::RefreshCollection {
2114 collection: name.to_string(),
2115 records,
2116 }])?;
2117 Ok(())
2118 }
2119
2120 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
2121 let name = Self::canonical_collection(name);
2122 self.resident
2123 .create_collection_in_memory(name, &self.config.manager_config)
2124 }
2125
2126 pub(super) fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
2127 let name = Self::canonical_collection(name);
2128 self.resident.drop_collection_in_memory(name, || {
2129 if let Some(tree) = self.btree_indices.write().remove(name) {
2130 self.retire_btree(tree);
2131 }
2132 })
2133 }
2134
2135 fn apply_replayed_upsert(
2136 &self,
2137 collection: &str,
2138 record: &[u8],
2139 update_btrees: bool,
2140 ) -> Result<(), StoreError> {
2141 let collection = Self::canonical_collection(collection);
2142 let id = self.resident.apply_replayed_upsert(
2143 collection,
2144 record,
2145 self.format_version(),
2146 &self.config.manager_config,
2147 )?;
2148
2149 if let Some(pager) = self
2150 .pager
2151 .as_ref()
2152 .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
2153 {
2154 let mut btree_indices = self.btree_indices.write();
2155 let btree = btree_indices
2156 .entry(collection.to_string())
2157 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
2158 let root_before = btree.root_page_id();
2159 let key = id.raw().to_be_bytes();
2160 match btree.insert(&key, record) {
2161 Ok(_) => {}
2162 Err(BTreeError::DuplicateKey) => {
2163 let _ = btree.delete(&key);
2164 let _ = btree.insert(&key, record);
2165 }
2166 Err(err) => {
2167 return Err(StoreError::Io(io::Error::other(format!(
2168 "replay upsert btree error: {err}"
2169 ))));
2170 }
2171 }
2172 if root_before != btree.root_page_id() {
2173 self.mark_paged_registry_dirty();
2174 }
2175 }
2176
2177 Ok(())
2178 }
2179
2180 fn apply_replayed_delete(
2181 &self,
2182 collection: &str,
2183 id: EntityId,
2184 update_btrees: bool,
2185 ) -> Result<(), StoreError> {
2186 let collection = Self::canonical_collection(collection);
2187 self.resident.apply_replayed_delete(collection, id, || {
2188 if self.pager.is_some()
2189 && update_btrees
2190 && self.config.resident_memory_bytes_max.is_none()
2191 {
2192 let btree_indices = self.btree_indices.read();
2193 if let Some(btree) = btree_indices.get(collection) {
2194 let root_before = btree.root_page_id();
2195 let key = id.raw().to_be_bytes();
2196 let _ = btree.delete(&key);
2197 if root_before != btree.root_page_id() {
2198 self.mark_paged_registry_dirty();
2199 }
2200 }
2201 }
2202 })
2203 }
2204}
2205
2206#[cfg(all(test, target_os = "linux"))]
2207#[path = "storage_apocalypse.rs"]
2208mod storage_apocalypse;
2209
2210#[cfg(test)]
2211mod tests {
2212 use super::*;
2213 use crate::api::{DurabilityMode, GroupCommitOptions};
2214 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
2215 use std::time::SystemTime;
2216
2217 #[test]
2218 fn runtime_wal_retention_serializes_concurrent_appends() {
2219 let directory = tempfile::tempdir().expect("temporary WAL directory");
2220 let path = directory.path().join("data.wal");
2221 let coordinator = Arc::new(
2222 StoreCommitCoordinator::open(
2223 &path,
2224 DurabilityMode::WalDurableGrouped,
2225 GroupCommitOptions::default(),
2226 )
2227 .expect("coordinator"),
2228 );
2229 let barrier = Arc::new(Barrier::new(5));
2230 let mut workers = Vec::new();
2231 for worker in 0..4u8 {
2232 let coordinator = Arc::clone(&coordinator);
2233 let barrier = Arc::clone(&barrier);
2234 workers.push(std::thread::spawn(move || {
2235 barrier.wait();
2236 for index in 0..20u8 {
2237 coordinator
2238 .append_single_record(WalRecord::ProbabilisticDelta {
2239 kind: 1,
2240 operation: 1,
2241 name: "visitors".into(),
2242 operands: vec![vec![worker, index]],
2243 })
2244 .expect("append delta");
2245 }
2246 }));
2247 }
2248 barrier.wait();
2249 for _ in 0..5 {
2250 coordinator.truncate().expect("retain while appending");
2251 }
2252 for worker in workers {
2253 worker.join().expect("worker");
2254 }
2255 coordinator.truncate().expect("final retention");
2256 let entries: std::collections::HashSet<_> = WalReader::open(&path)
2257 .expect("reader")
2258 .iter()
2259 .map(|entry| match entry.expect("record").1 {
2260 WalRecord::ProbabilisticDelta { operands, .. } => operands[0].clone(),
2261 _ => panic!("unexpected retained record"),
2262 })
2263 .collect();
2264 assert_eq!(entries.len(), 80);
2265 }
2266
2267 #[test]
2268 fn runtime_wal_retention_preserves_exact_frames_and_append_order() {
2269 use crate::storage::wal::record::WalRecordAuthority;
2270 for mode in [
2271 DurabilityMode::Strict,
2272 DurabilityMode::WalDurableGrouped,
2273 DurabilityMode::Async,
2274 ] {
2275 let directory = tempfile::tempdir().expect("temporary WAL directory");
2276 let path = directory.path().join("data.wal");
2277 let record = WalRecord::ProbabilisticDelta {
2278 kind: 1,
2279 operation: 1,
2280 name: "visitors".into(),
2281 operands: vec![b"alice".to_vec()],
2282 };
2283 let mut frame = Vec::new();
2284 record.encode_with_authority_into(
2285 &mut frame,
2286 WalRecordAuthority {
2287 term: 42,
2288 ownership_epoch: Some(9),
2289 },
2290 );
2291 {
2292 let mut writer = WalWriter::open(&path).expect("WAL writer");
2293 writer
2294 .append(&WalRecord::Begin { tx_id: 1 })
2295 .expect("logical begin");
2296 writer.append_bytes(&frame).expect("runtime record");
2297 writer
2298 .append(&WalRecord::Commit { tx_id: 1 })
2299 .expect("logical commit");
2300 writer.sync().expect("sync");
2301 }
2302 let coordinator =
2303 StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2304 .expect("coordinator");
2305 coordinator.truncate().expect("retain runtime records");
2306 let mut expected = reddb_file::encode_wal_file_header().to_vec();
2307 expected.extend_from_slice(&frame);
2308 assert_eq!(std::fs::read(&path).expect("retained WAL"), expected);
2309 let later = WalRecord::ProbabilisticDelta {
2310 kind: 1,
2311 operation: 1,
2312 name: "visitors".into(),
2313 operands: vec![b"bob".to_vec()],
2314 };
2315 coordinator
2316 .append_single_record(later.clone())
2317 .expect("append after rewrite");
2318 coordinator.truncate().expect("retain queued record");
2319 let records: Vec<_> = WalReader::open(&path)
2320 .expect("reader")
2321 .iter()
2322 .map(|entry| entry.expect("record").1)
2323 .collect();
2324 assert_eq!(records, vec![record, later]);
2325 coordinator
2326 .truncate_after_runtime_checkpoint()
2327 .expect("runtime snapshot permits truncation");
2328 assert_eq!(
2329 std::fs::read(&path).expect("checkpointed WAL"),
2330 reddb_file::encode_wal_file_header()
2331 );
2332 }
2333 }
2334
2335 #[test]
2336 fn runtime_wal_rewrite_survives_interruption() {
2337 const TEST: &str =
2338 "storage::unified::store::commit::tests::runtime_wal_rewrite_survives_interruption";
2339 const CHILD: &str = "REDDB_TEST_RUNTIME_WAL_REWRITE_PATH";
2340 if let Some(path) = std::env::var_os(CHILD) {
2341 let coordinator = StoreCommitCoordinator::open(
2342 path,
2343 DurabilityMode::Strict,
2344 GroupCommitOptions::default(),
2345 )
2346 .expect("child coordinator");
2347 coordinator.truncate().expect("fault injection");
2348 panic!("interruption point was not reached");
2349 }
2350 for (hook, code, replaced) in [
2351 ("REDDB_TEST_EXIT_BEFORE_RUNTIME_WAL_RENAME", 211, false),
2352 ("REDDB_TEST_EXIT_AFTER_RUNTIME_WAL_RENAME", 212, true),
2353 ] {
2354 let directory = tempfile::tempdir().expect("temporary WAL directory");
2355 let path = directory.path().join("data.wal");
2356 let record = WalRecord::ProbabilisticDelta {
2357 kind: 1,
2358 operation: 1,
2359 name: "visitors".into(),
2360 operands: vec![b"alice".to_vec()],
2361 };
2362 {
2363 let mut writer = WalWriter::open(&path).expect("WAL writer");
2364 writer
2365 .append(&WalRecord::Begin { tx_id: 1 })
2366 .expect("begin");
2367 writer.append(&record).expect("runtime record");
2368 writer
2369 .append(&WalRecord::Commit { tx_id: 1 })
2370 .expect("commit");
2371 writer.sync().expect("sync");
2372 }
2373 let original = std::fs::read(&path).expect("original WAL");
2374 let child =
2375 std::process::Command::new(std::env::current_exe().expect("test executable"))
2376 .args(["--exact", TEST, "--nocapture"])
2377 .env(CHILD, &path)
2378 .env(hook, "1")
2379 .output()
2380 .expect("child");
2381 assert_eq!(
2382 child.status.code(),
2383 Some(code),
2384 "{}",
2385 String::from_utf8_lossy(&child.stderr)
2386 );
2387 let after = std::fs::read(&path).expect("WAL after interruption");
2388 if replaced {
2389 let mut expected = reddb_file::encode_wal_file_header().to_vec();
2390 expected.extend_from_slice(&record.encode());
2391 assert_eq!(after, expected);
2392 } else {
2393 assert_eq!(after, original);
2394 }
2395 let coordinator = StoreCommitCoordinator::open(
2396 &path,
2397 DurabilityMode::Strict,
2398 GroupCommitOptions::default(),
2399 )
2400 .expect("reopen");
2401 coordinator.truncate().expect("retry rewrite");
2402 let records: Vec<_> = WalReader::open(&path)
2403 .expect("reader")
2404 .iter()
2405 .map(|entry| entry.expect("record").1)
2406 .collect();
2407 assert_eq!(records, vec![record]);
2408 }
2409 }
2410
2411 #[test]
2412 fn ownership_epoch_rejects_nested_begin_and_wrong_finish() {
2413 let directory = tempfile::tempdir().expect("temporary store");
2414 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("store");
2415 store.create_collection("rows").expect("collection");
2416 store.persist().expect("baseline");
2417 let coordinator = store.commit.as_ref().expect("WAL coordinator");
2418 assert!(coordinator.begin_page_ownership_epoch(&store, 0).is_err());
2419 assert!(coordinator.finish_page_ownership_epoch(&store, 1).is_err());
2420 coordinator
2421 .begin_page_ownership_epoch(&store, 2)
2422 .expect("begin epoch");
2423 assert!(coordinator.begin_page_ownership_epoch(&store, 3).is_err());
2424 assert!(coordinator.finish_page_ownership_epoch(&store, 3).is_err());
2425 assert!(coordinator.finish_page_ownership_epoch(&store, 2).is_err());
2426 store.flush_paged_registry().expect("publish epoch");
2427 coordinator
2428 .finish_page_ownership_epoch(&store, 2)
2429 .expect("finish epoch");
2430 assert_eq!(store.ownership_active_epoch.load(Ordering::Acquire), 0);
2431 }
2432
2433 #[test]
2434 fn native_pager_records_page_ownership_in_durable_wal() {
2435 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2436
2437 let directory = tempfile::tempdir().expect("temporary store");
2438 let path = directory.path().join("data.rdb");
2439 let wal_path = reddb_file::layout::unified_wal_path(&path);
2440 let pager = crate::storage::engine::Pager::open(&path, Default::default())
2441 .expect("open native pager");
2442 let coordinator = Arc::new(
2443 StoreCommitCoordinator::open(
2444 &wal_path,
2445 DurabilityMode::Async,
2446 GroupCommitOptions::default(),
2447 )
2448 .expect("open WAL"),
2449 );
2450 let recorder = Arc::new(
2451 StoreOwnershipRecorder::begin(Arc::clone(&coordinator), 7).expect("begin epoch"),
2452 );
2453 pager
2454 .install_ownership_recorder(recorder)
2455 .expect("install recorder");
2456 let page = pager
2457 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2458 .expect("allocate")
2459 .page_id();
2460 pager
2461 .record_retirement_intent(&[page])
2462 .expect("record retirement");
2463 pager.free_retired_pages(&[page]).expect("retire");
2464 let records: Vec<_> = WalReader::open(&wal_path)
2465 .expect("read WAL")
2466 .iter()
2467 .map(|item| item.expect("WAL record").1)
2468 .collect();
2469 assert_eq!(
2470 records,
2471 vec![
2472 WalRecord::PageOwnership {
2473 record: PageOwnershipRecord::Begin { epoch: 7 }
2474 },
2475 WalRecord::PageOwnership {
2476 record: PageOwnershipRecord::Reserve {
2477 epoch: 7,
2478 page_id: page
2479 }
2480 },
2481 WalRecord::PageOwnership {
2482 record: PageOwnershipRecord::Retire {
2483 epoch: 7,
2484 page_id: page
2485 }
2486 },
2487 WalRecord::PageOwnership {
2488 record: PageOwnershipRecord::Retire {
2489 epoch: 7,
2490 page_id: page
2491 }
2492 },
2493 ]
2494 );
2495 assert!(
2496 coordinator.fsync_count() >= 3,
2497 "async ownership writes must sync"
2498 );
2499 assert!(
2500 coordinator.truncate().is_err(),
2501 "ownership WAL remains protected"
2502 );
2503 }
2504
2505 #[test]
2506 fn replay_reclaims_unpublished_ownership_reservations() {
2507 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2508
2509 let directory = tempfile::tempdir().expect("temporary store");
2510 let path = directory.path().join("data.rdb");
2511 {
2512 let store = UnifiedStore::open(&path).expect("create store");
2513 store.create_collection("rows").expect("collection");
2514 store.persist().expect("persist baseline");
2515 }
2516 let wal_path = reddb_file::layout::unified_wal_path(&path);
2517 let ids;
2518 {
2519 let pager =
2520 crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2521 let first = pager.page_count().expect("page count");
2522 let coordinator = StoreCommitCoordinator::open(
2523 &wal_path,
2524 DurabilityMode::Strict,
2525 GroupCommitOptions::default(),
2526 )
2527 .expect("WAL");
2528 coordinator
2529 .append_durable_page_ownership_batch(&[
2530 PageOwnershipRecord::Begin { epoch: 2 },
2531 PageOwnershipRecord::Reserve {
2532 epoch: 2,
2533 page_id: first,
2534 },
2535 PageOwnershipRecord::Reserve {
2536 epoch: 2,
2537 page_id: first + 1,
2538 },
2539 ])
2540 .expect("reserve pages durably");
2541 ids = [
2542 pager
2543 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2544 .expect("allocate")
2545 .page_id(),
2546 pager
2547 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2548 .expect("allocate")
2549 .page_id(),
2550 ];
2551 assert_eq!(ids, [first, first + 1]);
2552 pager.sync().expect("persist abandoned pages");
2553 }
2554 let reopened = UnifiedStore::open(&path).expect("recover ownership");
2555 let pager = reopened.pager.as_ref().expect("pager");
2556 let a = pager
2557 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2558 .expect("reuse")
2559 .page_id();
2560 let b = pager
2561 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2562 .expect("reuse")
2563 .page_id();
2564 assert_ne!(a, b);
2565 assert!(ids.contains(&a) && ids.contains(&b));
2566 let records: Vec<_> = WalReader::open(&wal_path)
2567 .expect("read WAL")
2568 .iter()
2569 .collect::<io::Result<Vec<_>>>()
2570 .expect("records");
2571 assert!(matches!(
2572 records.last(),
2573 Some((
2574 _,
2575 WalRecord::PageOwnership {
2576 record: PageOwnershipRecord::Finish { epoch: 2 }
2577 }
2578 ))
2579 ));
2580 reopened
2581 .commit
2582 .as_ref()
2583 .expect("coordinator")
2584 .truncate()
2585 .expect("completed ownership can be truncated");
2586 }
2587
2588 #[cfg(target_os = "linux")]
2589 #[test]
2590 fn ownership_recovery_survives_interruption_and_retry() {
2591 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2592
2593 const CHILD: &str = "REDDB_EXPERIMENTAL_OWNERSHIP_RECOVERY_PATH";
2594 if let Some(path) = std::env::var_os(CHILD) {
2595 let _store = UnifiedStore::open(path).expect("recover store");
2596 panic!("recovery interruption hook was not reached");
2597 }
2598 for (published, hook, exit_code) in [
2599 (
2600 false,
2601 "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2602 187,
2603 ),
2604 (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2605 (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2606 (
2607 true,
2608 "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2609 187,
2610 ),
2611 (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2612 (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2613 ] {
2614 let directory = tempfile::tempdir().expect("temporary store");
2615 let path = directory.path().join("data.rdb");
2616 {
2617 let store = UnifiedStore::open(&path).expect("create store");
2618 store.create_collection("rows").expect("collection");
2619 store.persist().expect("baseline");
2620 if published {
2621 store.ownership_active_epoch.store(2, Ordering::Release);
2622 store.flush_paged_registry().expect("publish epoch");
2623 store.ownership_active_epoch.store(0, Ordering::Release);
2624 }
2625 }
2626 let wal_path = reddb_file::layout::unified_wal_path(&path);
2627 let ids;
2628 {
2629 let pager =
2630 crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2631 let first = pager.page_count().expect("page count");
2632 let coordinator = StoreCommitCoordinator::open(
2633 &wal_path,
2634 DurabilityMode::Strict,
2635 GroupCommitOptions::default(),
2636 )
2637 .expect("WAL");
2638 let intent = |page_id| {
2639 if published {
2640 PageOwnershipRecord::Retire { epoch: 2, page_id }
2641 } else {
2642 PageOwnershipRecord::Reserve { epoch: 2, page_id }
2643 }
2644 };
2645 coordinator
2646 .append_durable_page_ownership_batch(&[
2647 PageOwnershipRecord::Begin { epoch: 2 },
2648 intent(first),
2649 intent(first + 1),
2650 ])
2651 .expect("reserve");
2652 ids = [
2653 pager
2654 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2655 .expect("allocate")
2656 .page_id(),
2657 pager
2658 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2659 .expect("allocate")
2660 .page_id(),
2661 ];
2662 pager.sync().expect("persist abandoned pages");
2663 }
2664 for _ in 0..if exit_code == 189 { 1 } else { 2 } {
2665 let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
2666 .args(["--exact", "storage::unified::store::commit::tests::ownership_recovery_survives_interruption_and_retry", "--nocapture"])
2667 .env(CHILD, &path)
2668 .env(hook, "1")
2669 .output().expect("run interrupted recovery");
2670 assert_eq!(
2671 output.status.code(),
2672 Some(exit_code),
2673 "published={published} {hook}: {}",
2674 String::from_utf8_lossy(&output.stderr)
2675 );
2676 }
2677 let reopened = UnifiedStore::open(&path).expect("recover after interruptions");
2678 let pager = reopened.pager.as_ref().expect("pager");
2679 let a = pager
2680 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2681 .expect("reuse")
2682 .page_id();
2683 let b = pager
2684 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2685 .expect("reuse")
2686 .page_id();
2687 assert_ne!(a, b, "published={published} {hook}: duplicate page");
2688 assert!(
2689 ids.contains(&a) && ids.contains(&b),
2690 "published={published} {hook}: lost recovered page"
2691 );
2692 }
2693 }
2694
2695 #[test]
2696 fn ownership_intents_are_durable_in_every_mode() {
2697 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2698
2699 for mode in [
2700 DurabilityMode::Strict,
2701 DurabilityMode::WalDurableGrouped,
2702 DurabilityMode::Async,
2703 ] {
2704 let directory = tempfile::tempdir().expect("temporary WAL");
2705 let path = directory.path().join("ownership");
2706 let coordinator =
2707 StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2708 .expect("open coordinator");
2709 let intent = PageOwnershipRecord::Reserve {
2710 epoch: 1,
2711 page_id: 67,
2712 };
2713 let detach = PageOwnershipRecord::DetachFree {
2714 epoch: 1,
2715 page_id: 68,
2716 };
2717 coordinator
2718 .append_durable_page_ownership_batch(&[intent, detach])
2719 .expect("durable intent");
2720 assert!(
2721 coordinator.fsync_count() > 0,
2722 "ownership intent must be synced in {mode:?}"
2723 );
2724 let records: Vec<_> = WalReader::open(&path)
2725 .expect("open WAL")
2726 .iter()
2727 .collect::<io::Result<Vec<_>>>()
2728 .expect("read records");
2729 assert!(
2730 matches!(records.as_slice(), [(_, WalRecord::PageOwnership { record: first }), (_, WalRecord::PageOwnership { record: second })] if *first == intent && *second == detach)
2731 );
2732 assert!(coordinator
2733 .append_single_record(WalRecord::PageOwnership { record: intent })
2734 .is_err());
2735 assert!(coordinator
2736 .append_durable_page_ownership(PageOwnershipRecord::Begin { epoch: 0 })
2737 .is_err());
2738 assert!(
2739 coordinator.truncate().is_err(),
2740 "active ownership must prevent truncation"
2741 );
2742 let before = std::fs::read(&path).expect("ownership WAL");
2743 drop(coordinator);
2744 let reopened = StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2745 .expect("reopen coordinator");
2746 assert!(
2747 reopened.truncate().is_err(),
2748 "reopen must rediscover ownership records"
2749 );
2750 assert_eq!(std::fs::read(&path).expect("preserved WAL"), before);
2751 }
2752 }
2753
2754 #[test]
2755 fn unresolved_ownership_intent_blocks_store_replay() {
2756 for truncated in [false, true] {
2757 let directory = tempfile::tempdir().expect("temporary store");
2758 let path = directory.path().join("data.rdb");
2759 {
2760 let store = UnifiedStore::open(&path).expect("create store");
2761 store.create_collection("rows").expect("create collection");
2762 store.persist().expect("persist baseline");
2763 }
2764 let wal_path = reddb_file::layout::unified_wal_path(&path);
2765 {
2766 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
2767 writer
2768 .append(&WalRecord::PageOwnership {
2769 record: reddb_file::page_ownership_journal::PageOwnershipRecord::Reserve {
2770 epoch: 1,
2771 page_id: 67,
2772 },
2773 })
2774 .expect("append ownership intent");
2775 writer.sync().expect("sync ownership intent");
2776 }
2777 if truncated {
2778 let wal = std::fs::OpenOptions::new()
2779 .write(true)
2780 .open(&wal_path)
2781 .expect("WAL");
2782 let length = wal.metadata().expect("length").len();
2783 wal.set_len(length - 1).expect("truncate last byte");
2784 wal.sync_all().expect("sync truncation");
2785 }
2786 let before = std::fs::read(&path).expect("baseline data");
2787 let error = match UnifiedStore::open(&path) {
2788 Ok(_) => panic!("unresolved ownership must prevent opening"),
2789 Err(error) => error,
2790 };
2791 let expected = if truncated {
2792 "incomplete page ownership WAL record"
2793 } else {
2794 "page ownership journal requires recovery"
2795 };
2796 assert!(error.to_string().contains(expected), "{error}");
2797 assert_eq!(std::fs::read(&path).expect("preserved data"), before);
2798 }
2799 }
2800
2801 #[test]
2802 fn logical_wal_replay_restores_a_deleted_physical_identity() {
2803 for flat in [false, true] {
2804 let store = UnifiedStore::new();
2805 store.create_collection("replay").expect("collection");
2806 let id = EntityId::new(42);
2807 let entity = UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(1)]);
2808 if flat {
2809 store
2810 .bulk_insert("replay", vec![entity])
2811 .expect("flat seed");
2812 } else {
2813 store.insert_auto("replay", entity).expect("hash seed");
2814 }
2815 let metadata = Metadata::with_fields(HashMap::from([(
2816 "source".into(),
2817 MetadataValue::String("wal".into()),
2818 )]));
2819 for sequence in 100..104 {
2820 let delete = StoreWalAction::DeleteEntityRecord {
2821 collection: "replay".into(),
2822 entity_id: id.raw(),
2823 };
2824 store
2825 .apply_encoded_store_wal_action(&delete.encode())
2826 .expect("replay delete");
2827 let mut restored =
2828 UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(2)]);
2829 restored.ensure_table_logical_id();
2830 restored.sequence_id = sequence;
2831 let upsert = StoreWalAction::upsert_entity(
2832 "replay",
2833 &restored,
2834 Some(&metadata),
2835 store.format_version(),
2836 );
2837 store
2838 .apply_encoded_store_wal_action(&upsert.encode())
2839 .expect("replay upsert");
2840 let manager = store.get_collection("replay").expect("restored collection");
2841 let stored = manager.get(id).expect("restored entity is readable");
2842 assert_eq!(store.entity_cache_stats().entries, 0);
2843 assert_eq!(manager.count(), 1);
2844 assert_eq!(manager.query_all(|_| true).len(), 1);
2845 assert_eq!(stored.sequence_id, sequence);
2846 assert_eq!(
2847 UnifiedStore::serialize_entity_record(
2848 &stored,
2849 manager.get_metadata(id).as_ref(),
2850 store.format_version()
2851 ),
2852 UnifiedStore::serialize_entity_record(
2853 &restored,
2854 Some(&metadata),
2855 store.format_version()
2856 )
2857 );
2858 assert!(store.get_any(id).is_some());
2859 }
2860 }
2861 }
2862
2863 fn embedded_fixture(
2864 mode: DurabilityMode,
2865 background_bytes: usize,
2866 ) -> (tempfile::TempDir, PathBuf, UnifiedStore) {
2867 let directory = tempfile::tempdir().expect("directory");
2868 let path = directory.path().join("data.rdb");
2869 let mut store =
2870 UnifiedStore::with_config(UnifiedStoreConfig::default().with_durability_mode(mode));
2871 store.create_collection("keys").expect("collection");
2872 store.set_aux_metadata(vec![42; background_bytes]);
2873 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(
2874 &path,
2875 &store.to_binary_dump_bytes(),
2876 )
2877 .expect("snapshot");
2878 store.config.embedded_wal_path = Some(path.clone());
2879 (directory, path, store)
2880 }
2881
2882 #[test]
2883 fn embedded_stale_full_result_does_not_checkpoint_reclaimed_space() {
2884 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2885 let (_directory, path, store) = embedded_fixture(mode, 0);
2886 store
2887 .append_embedded_store_wal_actions(&[StoreWalAction::CreateCollection {
2888 name: "keys".into(),
2889 }])
2890 .expect("another writer appended after reclaiming space");
2891 let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
2892 let payloads = vec![StoreWalAction::CreateCollection {
2893 name: "after".into(),
2894 }
2895 .encode()];
2896 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
2897 .expect("encoded length");
2898 store
2900 .recover_full_embedded_wal(&path, &payloads, length)
2901 .expect("revalidate after acquiring publication");
2902 let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
2903 assert_eq!(
2904 after.selected_superblock.generation,
2905 before.selected_superblock.generation + 1,
2906 "only the retry is published, without an unnecessary checkpoint"
2907 );
2908 assert_eq!(
2909 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&after)
2910 .expect("both appends retained")
2911 .len(),
2912 2
2913 );
2914 }
2915 }
2916
2917 #[test]
2918 fn embedded_full_wal_reserves_bounded_headroom_for_populated_store() {
2919 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2920 let (_directory, path, store) = embedded_fixture(mode, 2 * 1024 * 1024);
2921 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial");
2922 let action = StoreWalAction::CreateCollection {
2923 name: "keys".into(),
2924 };
2925 let payloads = vec![action.encode()];
2926 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
2927 .expect("encoded length");
2928 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(
2929 &path,
2930 &vec![payloads[0].clone(); (initial.manifest.wal_region_bytes / length) as usize],
2931 )
2932 .expect("fill the initial region with valid records");
2933 store
2934 .append_embedded_store_wal_actions(&[action])
2935 .expect("overflow");
2936 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("checkpoint");
2937 assert_eq!(open.manifest.wal_region_bytes, 1024 * 1024);
2938 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2939 .expect("snapshot")
2940 .expect("populated image");
2941 let recovered =
2942 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
2943 .expect("recover snapshot");
2944 for payload in
2945 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL")
2946 {
2947 recovered
2948 .apply_encoded_store_wal_action(&payload)
2949 .expect("replay");
2950 }
2951 assert_eq!(
2952 recovered.resident.aux_metadata.read().len(),
2953 2 * 1024 * 1024
2954 );
2955 assert!(recovered.get_collection("keys").is_some());
2956 }
2957 }
2958
2959 #[test]
2960 fn embedded_grouped_overwrites_recover_across_repeated_full_wal_regions() {
2961 fn row(writer: usize, round: usize) -> UnifiedEntity {
2962 let mut state = (writer as u64 + 1) * 31 + round as u64 * 13;
2963 let payload: String = (0..4096)
2964 .map(|_| {
2965 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
2966 char::from(b'!' + ((state >> 24) % 90) as u8)
2967 })
2968 .collect();
2969 let mut entity = UnifiedEntity::table_row(EntityId::new(0), "keys", 0, vec![]);
2970 entity.data = EntityData::Row(RowData::with_names(
2971 vec![
2972 Value::text(format!("k{writer}")),
2973 Value::text(format!("{round}:{payload}")),
2974 ],
2975 vec!["key".into(), "value".into()],
2976 ));
2977 entity
2978 }
2979 let (_directory, path, store) =
2980 embedded_fixture(DurabilityMode::WalDurableGrouped, 2 * 1024 * 1024);
2981 assert!(
2982 StoreWalAction::upsert_entity("keys", &row(0, 0), None, store.format_version())
2983 .encode()
2984 .len()
2985 >= 2048,
2986 "fixture must exert physical WAL pressure after value compression"
2987 );
2988 let mut ids: Vec<_> = (0..8)
2989 .map(|writer| {
2990 store
2991 .write_keyed_row("keys", row(writer, 0), None, None)
2992 .expect("seed")
2993 })
2994 .collect();
2995 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial roots");
2996 let mut previous_snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&initial)
2997 .expect("initial snapshot")
2998 .expect("image");
2999 for wave in 0..3 {
3003 let barrier = Barrier::new(ids.len());
3004 ids = std::thread::scope(|scope| {
3005 let workers: Vec<_> = ids
3006 .iter()
3007 .enumerate()
3008 .map(|(writer, previous)| {
3009 let store = &store;
3010 let barrier = &barrier;
3011 scope.spawn(move || {
3012 barrier.wait();
3013 let mut id = *previous;
3014 for round in 0..64 {
3015 id = store
3016 .write_keyed_row(
3017 "keys",
3018 row(writer, wave * 64 + round),
3019 None,
3020 Some(id),
3021 )
3022 .expect("durable overwrite");
3023 }
3024 id
3025 })
3026 })
3027 .collect();
3028 workers
3029 .into_iter()
3030 .map(|worker| worker.join().expect("writer"))
3031 .collect()
3032 });
3033 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("wave roots");
3034 assert_eq!(open.manifest.wal_region_bytes, EMBEDDED_WAL_HEADROOM_LIMIT);
3035 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
3036 .expect("wave snapshot")
3037 .expect("image");
3038 assert!(snapshot != previous_snapshot, "wave {wave} must checkpoint");
3039 previous_snapshot = snapshot;
3040 }
3041 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("recover roots");
3042 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
3043 .expect("snapshot")
3044 .expect("image");
3045 let recovered =
3046 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
3047 .expect("snapshot replay");
3048 for payload in crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL") {
3049 recovered
3050 .apply_encoded_store_wal_action(&payload)
3051 .expect("WAL replay");
3052 }
3053 assert_eq!(recovered.get_collection("keys").expect("keys").count(), 8);
3054 for (writer, id) in ids.iter().enumerate() {
3055 let entity = recovered
3056 .get("keys", *id)
3057 .expect("last acknowledged version");
3058 assert_eq!(
3059 entity.data.as_row().expect("row").get_field("value"),
3060 row(writer, 191)
3061 .data
3062 .as_row()
3063 .expect("expected row")
3064 .get_field("value")
3065 );
3066 }
3067 }
3068
3069 fn env_lock() -> &'static StdMutex<()> {
3074 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
3075 LOCK.get_or_init(|| StdMutex::new(()))
3076 }
3077
3078 fn temp_wal(name: &str) -> PathBuf {
3079 let nanos = SystemTime::now()
3080 .duration_since(std::time::UNIX_EPOCH)
3081 .unwrap()
3082 .as_nanos();
3083 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
3084 &std::env::temp_dir(),
3085 name,
3086 std::process::id(),
3087 nanos,
3088 );
3089 let _ = std::fs::remove_file(&path);
3090 path
3091 }
3092
3093 fn replay_crash_payload(id: u64, round: u64) -> String {
3094 let mut state = id * 31 + round * 13 + 1;
3095 (0..600)
3096 .map(|_| {
3097 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
3098 char::from(b'!' + ((state >> 24) % 90) as u8)
3099 })
3100 .collect()
3101 }
3102
3103 #[cfg(target_os = "linux")]
3104 #[test]
3105 fn interrupted_replay_reclaims_unpublished_pages() {
3106 interrupted_replay_reclaims_pages(false, false);
3107 }
3108
3109 #[cfg(target_os = "linux")]
3110 #[test]
3111 fn interrupted_replay_reclaims_durable_unpublished_pages() {
3112 interrupted_replay_reclaims_pages(false, true);
3113 }
3114
3115 #[cfg(target_os = "linux")]
3116 #[test]
3117 fn interrupted_replay_preserves_published_pages() {
3118 interrupted_replay_reclaims_pages(true, false);
3119 }
3120
3121 #[cfg(target_os = "linux")]
3122 #[test]
3123 fn interrupted_checkpoint_reclaims_unpublished_pages() {
3124 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_REBUILD_PATH";
3125 let config = || {
3126 UnifiedStoreConfig::default()
3127 .with_durability_mode(DurabilityMode::Strict)
3128 .with_page_cache_slots(4)
3129 };
3130 if let Some(path) = std::env::var_os(CHILD_PATH) {
3131 let store = UnifiedStore::open_with_config(path, config()).unwrap();
3132 for id in 1..=200 {
3135 let entity = UnifiedEntity::table_row(
3136 EntityId::new(id),
3137 "rows",
3138 id,
3139 vec![Value::text(replay_crash_payload(id, 8))],
3140 );
3141 let record =
3142 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3143 store.apply_replayed_upsert("rows", &record, false).unwrap();
3144 }
3145 store.persist().expect("must exit during checkpoint");
3146 panic!("checkpoint rebuild hook was not reached");
3147 }
3148 let directory = tempfile::tempdir().unwrap();
3149 let path = directory.path().join("data.rdb");
3150 {
3151 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3152 store.create_collection("rows").unwrap();
3153 for id in 1..=200 {
3154 store
3155 .insert(
3156 "rows",
3157 UnifiedEntity::table_row(
3158 EntityId::new(id),
3159 "rows",
3160 id,
3161 vec![Value::text(format!("initial-{id}"))],
3162 ),
3163 )
3164 .unwrap();
3165 }
3166 store.persist().unwrap();
3167 }
3168 let mut first = 0;
3169 let mut last = 0;
3170 for cycle in 1..=12 {
3171 let output = std::process::Command::new(std::env::current_exe().unwrap())
3172 .args([
3173 "--exact",
3174 "storage::unified::store::commit::tests::interrupted_checkpoint_reclaims_unpublished_pages",
3175 "--nocapture",
3176 ])
3177 .env(CHILD_PATH, &path)
3178 .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
3179 .output()
3180 .unwrap();
3181 assert_eq!(
3182 output.status.code(),
3183 Some(183),
3184 "{}",
3185 String::from_utf8_lossy(&output.stderr)
3186 );
3187 assert!(String::from_utf8_lossy(&output.stderr)
3188 .contains("experimental checkpoint exit before manifest"));
3189 last = std::fs::metadata(&path).unwrap().len();
3190 if cycle == 1 {
3191 first = last;
3192 }
3193 eprintln!("checkpoint_exit_cycle={cycle} data_logical={last}");
3194 }
3195 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3196 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3197 for id in 1..=200 {
3198 assert_eq!(
3199 recovered
3200 .get("rows", EntityId::new(id))
3201 .unwrap()
3202 .data
3203 .as_row()
3204 .unwrap()
3205 .columns,
3206 vec![Value::text(format!("initial-{id}"))]
3207 );
3208 }
3209 assert!(last <= first + 2 * crate::storage::engine::PAGE_SIZE as u64,
3210 "repeated interrupted checkpoint must bound page growth: first={first}, after_12={last}");
3211 }
3212
3213 #[cfg(target_os = "linux")]
3214 #[test]
3215 fn interrupted_checkpoint_with_wal_reclaims_unpublished_pages() {
3216 use std::os::unix::fs::MetadataExt;
3217 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_PATH";
3218 const ROUND: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_ROUND";
3219 let config = || {
3220 UnifiedStoreConfig::default()
3221 .with_durability_mode(DurabilityMode::Strict)
3222 .with_page_cache_slots(4)
3223 };
3224 if let Some(path) = std::env::var_os(CHILD_PATH) {
3225 let round: u64 = std::env::var(ROUND).unwrap().parse().unwrap();
3226 let store = UnifiedStore::open_with_config(path, config()).unwrap();
3227 if round > 1 {
3228 for id in 1..=200 {
3229 assert_eq!(
3230 store
3231 .get("rows", EntityId::new(id))
3232 .unwrap()
3233 .data
3234 .as_row()
3235 .unwrap()
3236 .columns,
3237 vec![Value::text(replay_crash_payload(id, round - 1))]
3238 );
3239 }
3240 }
3241 let actions: Vec<_> = (1..=200)
3242 .map(|id| {
3243 let entity = UnifiedEntity::table_row(
3244 EntityId::new(id),
3245 "rows",
3246 id,
3247 vec![Value::text(replay_crash_payload(id, round))],
3248 );
3249 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3250 })
3251 .collect();
3252 for action in &actions {
3253 store.apply_replayed_action(action, false).unwrap();
3254 }
3255 store
3258 .commit
3259 .as_ref()
3260 .unwrap()
3261 .append_actions(&actions)
3262 .unwrap();
3263 store.persist().expect("must exit during checkpoint");
3264 panic!("checkpoint rebuild hook was not reached");
3265 }
3266 let directory = tempfile::tempdir().unwrap();
3267 let path = directory.path().join("data.rdb");
3268 {
3269 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3270 store.create_collection("rows").unwrap();
3271 for id in 1..=200 {
3272 store
3273 .insert(
3274 "rows",
3275 UnifiedEntity::table_row(
3276 EntityId::new(id),
3277 "rows",
3278 id,
3279 vec![Value::text(format!("initial-{id}"))],
3280 ),
3281 )
3282 .unwrap();
3283 }
3284 store.persist().unwrap();
3285 }
3286 let mut fourth = 0;
3287 let mut last = 0;
3288 for round in 1..=12 {
3289 let output = std::process::Command::new(std::env::current_exe().unwrap())
3290 .args([
3291 "--exact",
3292 "storage::unified::store::commit::tests::interrupted_checkpoint_with_wal_reclaims_unpublished_pages",
3293 "--nocapture",
3294 ])
3295 .env(CHILD_PATH, &path)
3296 .env(ROUND, round.to_string())
3297 .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
3298 .output().unwrap();
3299 assert_eq!(
3300 output.status.code(),
3301 Some(183),
3302 "round {round}: {}",
3303 String::from_utf8_lossy(&output.stderr)
3304 );
3305 assert!(String::from_utf8_lossy(&output.stderr)
3306 .contains("experimental checkpoint exit before manifest"));
3307 let meta = std::fs::metadata(&path).unwrap();
3308 last = meta.len();
3309 if round == 4 {
3310 fourth = last;
3311 }
3312 eprintln!(
3313 "checkpoint_wal_exit_round={round} data_logical={last} data_allocated={}",
3314 meta.blocks() * 512
3315 );
3316 }
3317 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3318 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3319 for id in 1..=200 {
3320 assert_eq!(
3321 recovered
3322 .get("rows", EntityId::new(id))
3323 .unwrap()
3324 .data
3325 .as_row()
3326 .unwrap()
3327 .columns,
3328 vec![Value::text(replay_crash_payload(id, 12))]
3329 );
3330 }
3331 recovered.persist().unwrap();
3332 drop(recovered);
3333 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3334 for id in 1..=200 {
3335 assert_eq!(
3336 reopened
3337 .get("rows", EntityId::new(id))
3338 .unwrap()
3339 .data
3340 .as_row()
3341 .unwrap()
3342 .columns,
3343 vec![Value::text(replay_crash_payload(id, 12))]
3344 );
3345 }
3346 assert!(last <= fourth + 2 * crate::storage::engine::PAGE_SIZE as u64,
3347 "repeated interrupted WAL-backed checkpoints must bound page growth: round4={fourth}, round12={last}");
3348 }
3349
3350 #[cfg(target_os = "linux")]
3351 #[test]
3352 fn interrupted_retired_reclamation_preserves_reuse() {
3353 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_RETIRED_PATH";
3354 fn mutate(store: &UnifiedStore, round: u64) {
3355 for id in 1..=200 {
3356 let entity = UnifiedEntity::table_row(
3357 EntityId::new(id),
3358 "rows",
3359 id,
3360 vec![Value::text(replay_crash_payload(id, round))],
3361 );
3362 let record =
3363 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3364 store.apply_replayed_upsert("rows", &record, false).unwrap();
3365 }
3366 }
3367 let config = || {
3368 UnifiedStoreConfig::default()
3369 .with_durability_mode(DurabilityMode::Strict)
3370 .with_page_cache_slots(4)
3371 };
3372 if let Some(path) = std::env::var_os(CHILD_PATH) {
3373 let store = UnifiedStore::open_with_config(path, config()).unwrap();
3374 mutate(&store, 2);
3375 store.persist().expect("must exit at retired free boundary");
3376 panic!("retired free hook was not reached");
3377 }
3378 let mut reuse = Vec::new();
3379 for (label, env_name, exit_code, message) in [
3380 (
3381 "before",
3382 "REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE",
3383 184,
3384 "experimental exit before retired free",
3385 ),
3386 (
3387 "enqueued",
3388 "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE",
3389 186,
3390 "experimental exit after retired enqueue",
3391 ),
3392 (
3393 "after",
3394 "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE",
3395 185,
3396 "experimental exit after retired free",
3397 ),
3398 ] {
3399 let directory = tempfile::tempdir().unwrap();
3400 let path = directory.path().join("data.rdb");
3401 {
3402 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3403 store.create_collection("rows").unwrap();
3404 for id in 1..=200 {
3405 store
3406 .insert(
3407 "rows",
3408 UnifiedEntity::table_row(
3409 EntityId::new(id),
3410 "rows",
3411 id,
3412 vec![Value::text(format!("initial-{id}"))],
3413 ),
3414 )
3415 .unwrap();
3416 }
3417 store.persist().unwrap();
3418 mutate(&store, 1);
3419 store.persist().unwrap();
3420 }
3421 let output = std::process::Command::new(std::env::current_exe().unwrap())
3422 .args([
3423 "--exact",
3424 "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse",
3425 "--nocapture",
3426 ])
3427 .env(CHILD_PATH, &path)
3428 .env(env_name, "1")
3429 .output().unwrap();
3430 assert_eq!(
3431 output.status.code(),
3432 Some(exit_code),
3433 "{label}: {}",
3434 String::from_utf8_lossy(&output.stderr)
3435 );
3436 assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3437 eprintln!(
3438 "retired_reclaim_{label} child={}",
3439 String::from_utf8_lossy(&output.stderr).trim()
3440 );
3441 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3442 for id in 1..=200 {
3443 assert_eq!(
3444 store
3445 .get("rows", EntityId::new(id))
3446 .unwrap()
3447 .data
3448 .as_row()
3449 .unwrap()
3450 .columns,
3451 vec![Value::text(replay_crash_payload(id, 2))]
3452 );
3453 }
3454 let pager = store.pager.as_ref().unwrap();
3455 let before_pages = pager.page_count().unwrap();
3456 let mut ids = std::collections::HashSet::new();
3457 let mut reused = 0;
3458 for _ in 0..256 {
3459 let page = pager
3460 .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3461 .unwrap();
3462 let id = page.page_id();
3463 assert!(ids.insert(id), "duplicate allocation");
3464 reused += u32::from(id < before_pages);
3465 pager.write_page(id, page).unwrap();
3466 }
3467 pager.sync().unwrap();
3468 eprintln!(
3469 "retired_reclaim_{label} before_pages={before_pages} reused={reused} appended={}",
3470 256 - reused
3471 );
3472 drop(store);
3473 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3474 for id in 1..=200 {
3475 assert_eq!(
3476 reopened
3477 .get("rows", EntityId::new(id))
3478 .unwrap()
3479 .data
3480 .as_row()
3481 .unwrap()
3482 .columns,
3483 vec![Value::text(replay_crash_payload(id, 2))]
3484 );
3485 }
3486 reuse.push(reused);
3487 }
3488 assert!(
3489 reuse[0] >= reuse[2] && reuse[1] >= reuse[2],
3490 "recovery must not lose retired pages before allocator publication: before={} enqueued={} after={}",
3491 reuse[0],
3492 reuse[1],
3493 reuse[2]
3494 );
3495 }
3496
3497 #[cfg(target_os = "linux")]
3498 #[test]
3499 fn experimental_retaining_released_root_is_not_recoverable() {
3500 let directory = tempfile::tempdir().unwrap();
3501 let path = directory.path().join("data.rdb");
3502 let config = || {
3503 UnifiedStoreConfig::default()
3504 .with_durability_mode(DurabilityMode::Strict)
3505 .with_page_cache_slots(4)
3506 };
3507 {
3508 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3509 store.create_collection("rows").unwrap();
3510 for id in 1..=200 {
3511 store
3512 .insert(
3513 "rows",
3514 UnifiedEntity::table_row(
3515 EntityId::new(id),
3516 "rows",
3517 id,
3518 vec![Value::text(format!("initial-{id}"))],
3519 ),
3520 )
3521 .unwrap();
3522 }
3523 store.persist().unwrap();
3524 for id in 1..=200 {
3525 let entity = UnifiedEntity::table_row(
3526 EntityId::new(id),
3527 "rows",
3528 id,
3529 vec![Value::text(replay_crash_payload(id, 1))],
3530 );
3531 let record =
3532 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3533 store.apply_replayed_upsert("rows", &record, false).unwrap();
3534 }
3535 store.persist().unwrap();
3536 }
3537 let output = std::process::Command::new(std::env::current_exe().unwrap())
3538 .args(["--exact", "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse", "--nocapture"])
3539 .env("REDDB_EXPERIMENTAL_RETIRED_PATH", &path)
3540 .env("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT", "1")
3541 .env("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE", "1")
3542 .output().unwrap();
3543 assert_eq!(
3544 output.status.code(),
3545 Some(185),
3546 "{}",
3547 String::from_utf8_lossy(&output.stderr)
3548 );
3549 assert!(String::from_utf8_lossy(&output.stderr)
3550 .contains("experimental exit after retired free"));
3551
3552 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3553 for id in 1..=200 {
3554 assert_eq!(
3555 reopened
3556 .get("rows", EntityId::new(id))
3557 .unwrap()
3558 .data
3559 .as_row()
3560 .unwrap()
3561 .columns,
3562 vec![Value::text(replay_crash_payload(id, 2))]
3563 );
3564 }
3565 let retired_roots: Vec<_> = reopened
3566 .retired_btrees
3567 .lock()
3568 .iter()
3569 .map(|entry| entry.tree.root_page_id())
3570 .collect();
3571 let pager = reopened.pager.as_ref().unwrap();
3572 let root_types: Vec<_> = retired_roots
3573 .iter()
3574 .map(|id| pager.read_page(*id).unwrap().page_type().unwrap())
3575 .collect();
3576 assert!(
3577 root_types.contains(&crate::storage::engine::page::PageType::FreelistTrunk),
3578 "expected a retained root to have become a freelist trunk: {root_types:?}"
3579 );
3580 let error = reopened
3581 .persist()
3582 .expect_err("retained root was reused as allocator metadata");
3583 assert!(
3584 error
3585 .to_string()
3586 .contains("unexpected retired tree page type"),
3587 "{error}"
3588 );
3589 eprintln!("retained_root_types={root_types:?} persist_error={error}");
3590 }
3591
3592 #[cfg(target_os = "linux")]
3593 fn interrupted_replay_reclaims_pages(after_publication: bool, force_sync: bool) {
3594 use std::os::unix::fs::MetadataExt;
3595 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_REPLAY_REBUILD_PATH";
3596 let config = || {
3597 UnifiedStoreConfig::default()
3598 .with_durability_mode(DurabilityMode::Strict)
3599 .with_page_cache_slots(4)
3600 };
3601 if let Some(path) = std::env::var_os(CHILD_PATH) {
3602 let _store = UnifiedStore::open_with_config(path, config())
3603 .expect("must exit while replay rebuilds");
3604 panic!("replay rebuild hook was not reached");
3605 }
3606 let directory = tempfile::tempdir().unwrap();
3607 let path = directory.path().join("data.rdb");
3608 {
3609 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3610 store.create_collection("rows").unwrap();
3611 for id in 1..=200 {
3612 store
3613 .insert(
3614 "rows",
3615 UnifiedEntity::table_row(
3616 EntityId::new(id),
3617 "rows",
3618 id,
3619 vec![Value::text(format!("initial-{id}"))],
3620 ),
3621 )
3622 .unwrap();
3623 }
3624 store.persist().unwrap();
3625 }
3626 {
3627 let coordinator = StoreCommitCoordinator::open(
3628 reddb_file::layout::unified_wal_path(&path),
3629 DurabilityMode::Strict,
3630 GroupCommitOptions::default(),
3631 )
3632 .unwrap();
3633 let actions = (1..=200)
3634 .map(|id| {
3635 let entity = UnifiedEntity::table_row(
3636 EntityId::new(id),
3637 "rows",
3638 id,
3639 vec![Value::text(replay_crash_payload(id, 8))],
3640 );
3641 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3642 })
3643 .collect::<Vec<_>>();
3644 coordinator.append_actions(&actions).unwrap();
3645 }
3646 let snapshot = || {
3647 let data = std::fs::metadata(&path).unwrap();
3648 let wal = std::fs::metadata(reddb_file::layout::unified_wal_path(&path)).unwrap();
3649 (
3650 data.len(),
3651 data.blocks() * 512,
3652 wal.len(),
3653 wal.blocks() * 512,
3654 )
3655 };
3656 let baseline = snapshot();
3657 let mut previous = baseline;
3658 let mut first_failure = baseline;
3659 for cycle in 1..=12 {
3660 let test_name = if after_publication {
3661 "storage::unified::store::commit::tests::interrupted_replay_preserves_published_pages"
3662 } else if force_sync {
3663 "storage::unified::store::commit::tests::interrupted_replay_reclaims_durable_unpublished_pages"
3664 } else {
3665 "storage::unified::store::commit::tests::interrupted_replay_reclaims_unpublished_pages"
3666 };
3667 let mut command = std::process::Command::new(std::env::current_exe().unwrap());
3668 command
3669 .args(["--exact", test_name, "--nocapture"])
3670 .env(CHILD_PATH, &path);
3671 if !force_sync {
3672 command.env("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC", "1");
3673 } else {
3674 command.env_remove("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC");
3675 }
3676 let message = if after_publication {
3677 command.env("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", "1");
3678 "experimental replay exit after manifest"
3679 } else {
3680 command.env("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD", "1");
3681 "experimental replay rebuild exit before manifest"
3682 };
3683 let output = command.output().unwrap();
3684 assert_eq!(
3685 output.status.code(),
3686 Some(182),
3687 "cycle {cycle}: {}",
3688 String::from_utf8_lossy(&output.stderr)
3689 );
3690 assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3691 let now = snapshot();
3692 eprintln!("replay_exit_cycle={cycle} data_logical={} data_allocated={} wal_logical={} wal_allocated={}",now.0,now.1,now.2,now.3);
3693 if cycle == 1 {
3694 first_failure = now;
3695 }
3696 previous = now;
3697 }
3698 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3699 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3700 for id in 1..=200 {
3701 assert_eq!(
3702 recovered
3703 .get("rows", EntityId::new(id))
3704 .unwrap()
3705 .data
3706 .as_row()
3707 .unwrap()
3708 .columns,
3709 vec![Value::text(replay_crash_payload(id, 8))]
3710 );
3711 }
3712 let pages = recovered.pager.as_ref().unwrap().page_count().unwrap();
3713 let free = recovered.pager.as_ref().unwrap().free_page_count().unwrap();
3714 recovered.persist().unwrap();
3715 drop(recovered);
3716 let final_sizes = snapshot();
3717 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3718 assert_eq!(reopened.get_collection("rows").unwrap().count(), 200);
3719 for id in 1..=200 {
3720 assert_eq!(
3721 reopened
3722 .get("rows", EntityId::new(id))
3723 .unwrap()
3724 .data
3725 .as_row()
3726 .unwrap()
3727 .columns,
3728 vec![Value::text(replay_crash_payload(id, 8))]
3729 );
3730 }
3731 eprintln!("replay_rebuild baseline={baseline:?} after_12={previous:?} after_recovery_checkpoint={final_sizes:?} pages={pages} free={free}");
3732 let pager = reopened.pager.as_ref().unwrap();
3736 let before_probe = pager.page_count().unwrap();
3737 let mut ids = std::collections::HashSet::new();
3738 let mut reused = 0;
3739 for _ in 0..256 {
3740 let page = pager
3741 .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3742 .unwrap();
3743 let id = page.page_id();
3744 assert!(ids.insert(id), "allocator returned the same page twice");
3745 reused += u32::from(id < before_probe);
3746 pager.write_page(id, page).unwrap();
3747 }
3748 pager.sync().unwrap();
3749 let after_probe = pager.page_count().unwrap();
3750 eprintln!("replay_reuse_probe count=256 before_pages={before_probe} after_pages={after_probe} reused={reused} appended={}", 256 - reused);
3751 drop(reopened);
3752 let verified = UnifiedStore::open_with_config(&path, config()).unwrap();
3753 for id in 1..=200 {
3754 assert_eq!(
3755 verified
3756 .get("rows", EntityId::new(id))
3757 .unwrap()
3758 .data
3759 .as_row()
3760 .unwrap()
3761 .columns,
3762 vec![Value::text(replay_crash_payload(id, 8))]
3763 );
3764 }
3765 assert!(
3766 previous.0 <= first_failure.0 + 2 * crate::storage::engine::PAGE_SIZE as u64,
3767 "repeated interrupted replay must bound page growth: first={}, after_12={}",
3768 first_failure.0,
3769 previous.0
3770 );
3771 }
3772
3773 #[test]
3774 fn paged_wal_replay_survives_immediate_process_exit() {
3775 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
3776 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3777 if let Some(path) = std::env::var_os(CHILD_PATH) {
3778 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
3779 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
3780 std::process::exit(0);
3782 }
3783 let directory = tempfile::tempdir().unwrap();
3784 let path = directory.path().join("data.rdb");
3785 {
3786 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3787 store.create_collection("rows").unwrap();
3788 for id in 1..=400 {
3789 store
3790 .insert(
3791 "rows",
3792 UnifiedEntity::table_row(
3793 EntityId::new(id),
3794 "rows",
3795 id,
3796 vec![Value::text(format!("initial-{id}"))],
3797 ),
3798 )
3799 .unwrap();
3800 }
3801 store.persist().unwrap();
3802 }
3803 {
3804 let coordinator = StoreCommitCoordinator::open(
3805 reddb_file::layout::unified_wal_path(&path),
3806 DurabilityMode::Strict,
3807 GroupCommitOptions::default(),
3808 )
3809 .unwrap();
3810 for round in 0..3 {
3811 let actions = (1..=400)
3812 .map(|id| {
3813 let payload = replay_crash_payload(id, round);
3814 let entity = UnifiedEntity::table_row(
3815 EntityId::new(id),
3816 "rows",
3817 id,
3818 vec![Value::text(payload)],
3819 );
3820 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3821 })
3822 .collect::<Vec<_>>();
3823 coordinator.append_actions(&actions).unwrap();
3824 }
3825 }
3826 let status = std::process::Command::new(std::env::current_exe().unwrap())
3827 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
3828 .env(CHILD_PATH, &path).status().unwrap();
3829 assert!(status.success(), "replay process failed");
3830 let reopened = UnifiedStore::open_with_config(&path, config())
3831 .expect("cold reopen after replay crash");
3832 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
3833 for id in 1..=400 {
3834 let entity = reopened
3835 .get("rows", EntityId::new(id))
3836 .expect("retained entity");
3837 let EntityData::Row(row) = entity.data else {
3838 panic!("expected row")
3839 };
3840 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
3841 }
3842 drop(reopened);
3843 let stable_bytes = std::fs::metadata(&path).unwrap().len();
3844 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
3845 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
3846 assert_eq!(
3847 std::fs::metadata(&path).unwrap().len(),
3848 stable_bytes,
3849 "idempotent replay must not allocate another tree generation"
3850 );
3851 }
3852
3853 #[test]
3854 fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
3855 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3856 let path = temp_wal("checkpoint-ack");
3857 let coordinator = Arc::new(
3858 StoreCommitCoordinator::open(
3859 path.clone(),
3860 DurabilityMode::WalDurableGrouped,
3861 GroupCommitOptions::default(),
3862 )
3863 .unwrap(),
3864 );
3865 let state = coordinator.state.0.lock();
3868 let (written_tx, written_rx) = std::sync::mpsc::channel();
3869 let writer_coordinator = Arc::clone(&coordinator);
3870 let writer = std::thread::spawn(move || {
3871 let result = writer_coordinator.append_actions(&[StoreWalAction::CreateCollection {
3872 name: "acknowledged".into(),
3873 }]);
3874 written_tx.send(result).unwrap();
3875 });
3876 let deadline = Instant::now() + Duration::from_secs(2);
3877 while !coordinator.queue.has_pending() {
3878 assert!(Instant::now() < deadline, "writer never enqueued");
3879 std::thread::yield_now();
3880 }
3881 let (started_tx, started_rx) = std::sync::mpsc::channel();
3882 let (reset_tx, reset_rx) = std::sync::mpsc::channel();
3883 let reset_coordinator = Arc::clone(&coordinator);
3884 let reset = std::thread::spawn(move || {
3885 started_tx.send(()).unwrap();
3886 reset_tx.send(reset_coordinator.truncate()).unwrap();
3887 });
3888 started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
3889 let deadline = Instant::now() + Duration::from_millis(100);
3890 while coordinator.queue.has_pending() && Instant::now() < deadline {
3891 std::thread::sleep(Duration::from_millis(1));
3892 }
3893 let discarded_before_ack = !coordinator.queue.has_pending();
3894 drop(state);
3895 let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
3896 if acknowledged.is_err() {
3897 let mut state = coordinator.state.0.lock();
3900 state.shutdown = true;
3901 state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
3902 coordinator.state.1.notify_all();
3903 }
3904 writer.join().unwrap();
3905 reset_rx
3906 .recv_timeout(Duration::from_secs(2))
3907 .unwrap()
3908 .unwrap();
3909 reset.join().unwrap();
3910 assert!(
3911 !discarded_before_ack,
3912 "checkpoint discarded an unacknowledged WAL record"
3913 );
3914 assert!(
3915 acknowledged.unwrap().is_ok(),
3916 "durable writer did not finish"
3917 );
3918 drop(coordinator);
3919 let _ = std::fs::remove_file(path);
3920 }
3921
3922 #[test]
3923 fn ready_publication_requests_flush_but_waits_for_durability() {
3924 let path = temp_wal("ready-publication");
3925 let options = GroupCommitOptions {
3926 max_statements: 8,
3927 window_ms: 1000,
3928 ..Default::default()
3929 };
3930 let coordinator = Arc::new(
3931 StoreCommitCoordinator::open(path.clone(), DurabilityMode::WalDurableGrouped, options)
3932 .expect("coordinator"),
3933 );
3934 let wal = coordinator.wal.lock();
3935 let acknowledged = Arc::new(std::sync::atomic::AtomicBool::new(false));
3936 let writer = {
3937 let coordinator = Arc::clone(&coordinator);
3938 let acknowledged = Arc::clone(&acknowledged);
3939 std::thread::spawn(move || {
3940 coordinator
3941 .append_ready_actions(
3942 &[StoreWalAction::CreateCollection {
3943 name: "ready".into(),
3944 }],
3945 false,
3946 )
3947 .expect("durable publication");
3948 acknowledged.store(true, Ordering::Release);
3949 })
3950 };
3951 let deadline = Instant::now() + Duration::from_secs(10);
3952 while coordinator.state.0.lock().pending_statements == 0 {
3953 assert!(Instant::now() < deadline, "writer did not enqueue");
3954 std::thread::yield_now();
3955 }
3956 assert_eq!(
3957 coordinator.state.0.lock().pending_statements,
3958 options.max_statements
3959 );
3960 assert!(
3961 !acknowledged.load(Ordering::Acquire),
3962 "must wait for WAL sync"
3963 );
3964 drop(wal);
3965 writer.join().expect("writer");
3966 assert!(acknowledged.load(Ordering::Acquire));
3967 assert_eq!(coordinator.fsync_count(), 1);
3968 drop(coordinator);
3969 let _ = std::fs::remove_file(path);
3970 }
3971
3972 #[test]
3983 fn group_commit_coalesces_concurrent_autocommits() {
3984 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3985 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3988
3989 let path = temp_wal("coalesce");
3990 let coord = Arc::new(
3991 StoreCommitCoordinator::open(
3992 path.clone(),
3993 DurabilityMode::WalDurableGrouped,
3994 GroupCommitOptions::default(),
3995 )
3996 .expect("open commit coordinator"),
3997 );
3998
3999 const WRITERS: usize = 32;
4000 let barrier = Arc::new(Barrier::new(WRITERS));
4001 let mut handles = Vec::with_capacity(WRITERS);
4002 for tx in 0..WRITERS {
4003 let coord_c = Arc::clone(&coord);
4004 let barrier_c = Arc::clone(&barrier);
4005 handles.push(std::thread::spawn(move || {
4006 barrier_c.wait();
4010 let action = StoreWalAction::CreateCollection {
4011 name: format!("c{tx}"),
4012 };
4013 coord_c
4014 .append_actions(std::slice::from_ref(&action))
4015 .expect("append_actions");
4016 }));
4017 }
4018 for h in handles {
4019 h.join().expect("writer thread");
4020 }
4021
4022 let fsyncs = coord.fsync_count();
4023 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
4024 assert!(
4025 fsyncs < WRITERS as u64,
4026 "expected fsyncs ({fsyncs}) to be strictly less than \
4027 concurrent writers ({WRITERS}); coalescing failed"
4028 );
4029
4030 drop(coord);
4031 let _ = std::fs::remove_file(&path);
4032 }
4033
4034 #[test]
4040 fn zero_window_disables_coalescing_floor() {
4041 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
4042 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
4043
4044 let path = temp_wal("zero_window");
4045 let coord = Arc::new(
4046 StoreCommitCoordinator::open(
4047 path.clone(),
4048 DurabilityMode::WalDurableGrouped,
4049 GroupCommitOptions::default(),
4050 )
4051 .expect("open commit coordinator"),
4052 );
4053
4054 const WRITERS: usize = 8;
4055 let barrier = Arc::new(Barrier::new(WRITERS));
4056 let mut handles = Vec::with_capacity(WRITERS);
4057 for tx in 0..WRITERS {
4058 let coord_c = Arc::clone(&coord);
4059 let barrier_c = Arc::clone(&barrier);
4060 handles.push(std::thread::spawn(move || {
4061 barrier_c.wait();
4062 let action = StoreWalAction::CreateCollection {
4063 name: format!("z{tx}"),
4064 };
4065 coord_c
4066 .append_actions(std::slice::from_ref(&action))
4067 .expect("append_actions");
4068 }));
4069 }
4070 for h in handles {
4071 h.join().expect("writer thread");
4072 }
4073
4074 let fsyncs = coord.fsync_count();
4080 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
4081
4082 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4083 drop(coord);
4084 let _ = std::fs::remove_file(&path);
4085 }
4086
4087 #[test]
4089 fn resolve_window_precedence() {
4090 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
4091 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4093 let cfg = GroupCommitOptions::default();
4094 assert_eq!(
4095 StoreCommitCoordinator::resolve_window(&cfg),
4096 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
4097 );
4098
4099 let cfg_ms = GroupCommitOptions {
4101 window_ms: 5,
4102 ..GroupCommitOptions::default()
4103 };
4104 assert_eq!(
4105 StoreCommitCoordinator::resolve_window(&cfg_ms),
4106 Duration::from_millis(5)
4107 );
4108
4109 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
4111 assert_eq!(
4112 StoreCommitCoordinator::resolve_window(&cfg),
4113 Duration::from_micros(750)
4114 );
4115 assert_eq!(
4116 StoreCommitCoordinator::resolve_window(&cfg_ms),
4117 Duration::from_micros(750)
4118 );
4119
4120 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
4122 assert_eq!(
4123 StoreCommitCoordinator::resolve_window(&cfg),
4124 Duration::from_micros(0)
4125 );
4126
4127 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4128 }
4129}