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