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