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