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 pending = Vec::<(u64, Vec<u8>)>::new();
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 {
1111 tx_id,
1112 page_id: _,
1113 data,
1114 } => pending.push((tx_id, data)),
1115 WalRecord::Checkpoint { .. } => {}
1116 WalRecord::VectorInsert {
1117 collection,
1118 entity_id,
1119 vector,
1120 } => {
1121 let mut map = store.replayed_turbo_inserts.lock();
1127 map.entry(
1128 reddb_engine::storage::system_collection_names::canonical_owned(collection),
1129 )
1130 .or_default()
1131 .push((entity_id, vector));
1132 }
1133 WalRecord::ProbabilisticDelta {
1134 kind,
1135 operation,
1136 name,
1137 operands,
1138 } => {
1139 let mut deltas = store.replayed_probabilistic_deltas.lock();
1140 deltas.push((kind, operation, name, operands));
1141 }
1142 WalRecord::FullPageImage { .. } => {
1143 }
1145 }
1146 }
1147
1148 if incomplete_wal_tail && !ownership_journal.is_empty() {
1149 return Err(io::Error::new(
1150 io::ErrorKind::InvalidData,
1151 "incomplete WAL prevents ownership recovery",
1152 ));
1153 }
1154 self.recover_page_ownership(store, &ownership_journal)?;
1155
1156 for (tx_id, payload) in pending {
1157 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
1158 continue;
1159 }
1160 let action = StoreWalAction::decode(&payload)?;
1161 applied = true;
1162 store.apply_replayed_action(&action, false).map_err(|err| {
1163 io::Error::other(format!("failed to replay store wal action: {err}"))
1164 })?;
1165 store.remember_checkpoint_rows(vec![action], Default::default());
1166 }
1167
1168 if applied && store.config.resident_memory_bytes_max.is_some() {
1169 return Ok(());
1173 }
1174 if applied {
1175 let epoch = latest_ownership_epoch
1178 .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1179 .checked_add(1)
1180 .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))?;
1181 self.begin_page_ownership_epoch(store, epoch)?;
1182 store.mark_paged_registry_dirty();
1185 store
1186 .reconcile_replayed_btrees()
1187 .map_err(|err| io::Error::other(err.to_string()))?;
1188 self.finish_page_ownership_epoch(store, epoch)?;
1189 }
1190 Ok(())
1191 }
1192
1193 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
1194 match self.mode {
1195 DurabilityMode::Strict => self.force_sync(),
1196 DurabilityMode::Async => {
1201 let (state_lock, cond) = &*self.state;
1202 let mut state = state_lock.lock();
1203 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1204 state.pending_statements = state.pending_statements.saturating_add(1);
1205 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1206 state.first_pending_at.get_or_insert_with(Instant::now);
1207 cond.notify_all();
1208 Ok(())
1209 }
1210 DurabilityMode::WalDurableGrouped => {
1211 let (state_lock, cond) = &*self.state;
1212 let mut state = state_lock.lock();
1213 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1214 state.pending_statements = state.pending_statements.saturating_add(1);
1215 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1216 state.first_pending_at.get_or_insert_with(Instant::now);
1217 cond.notify_all();
1218
1219 loop {
1220 if let Some(err) = state.last_error.clone() {
1221 return Err(io::Error::other(err));
1222 }
1223 if state.durable_lsn >= target_lsn {
1224 return Ok(());
1225 }
1226 cond.wait(&mut state);
1229 }
1230 }
1231 }
1232 }
1233
1234 fn run_group_commit_loop(
1235 wal: Arc<WalMutex>,
1236 queue: Arc<WalAppendQueue>,
1237 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
1238 fsync_count: Arc<AtomicU64>,
1239 window: Duration,
1240 max_statements: usize,
1241 max_wal_bytes: u64,
1242 ) {
1243 let (state_lock, cond) = &*state;
1244 loop {
1245 let target_lsn = {
1246 let mut guard = state_lock.lock();
1247
1248 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
1249 cond.wait(&mut guard);
1250 }
1251
1252 if guard.shutdown {
1253 return;
1254 }
1255
1256 let immediate = window.is_zero()
1257 || guard.pending_statements >= max_statements
1258 || guard.pending_wal_bytes >= max_wal_bytes;
1259
1260 if !immediate {
1261 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
1262 let now = Instant::now();
1263 if now < deadline {
1264 let timeout = deadline.saturating_duration_since(now);
1265 let _ = cond.wait_for(&mut guard, timeout);
1266 if guard.shutdown {
1267 return;
1268 }
1269 if guard.pending_target_lsn <= guard.durable_lsn {
1270 continue;
1271 }
1272 let should_wait_again = guard.pending_statements < max_statements
1273 && guard.pending_wal_bytes < max_wal_bytes
1274 && guard
1275 .first_pending_at
1276 .map(|first| first.elapsed() < window)
1277 .unwrap_or(false);
1278 if should_wait_again {
1279 continue;
1280 }
1281 }
1282 }
1283
1284 guard.pending_target_lsn
1285 };
1286
1287 let _epoch = queue.epoch.read_recursive();
1292
1293 let batches = queue.drain_sorted();
1299
1300 let sync_result = {
1301 let mut wal = wal.lock();
1302 let mut write_err: Option<io::Error> = None;
1303 for (_lsn, bytes) in batches {
1304 if let Err(e) = wal.append_bytes(&bytes) {
1305 write_err = Some(e);
1306 break;
1307 }
1308 }
1309 match write_err {
1310 Some(e) => Err(e),
1311 None => wal.sync().map(|_| {
1312 fsync_count.fetch_add(1, Ordering::Relaxed);
1317 wal.durable_lsn()
1318 }),
1319 }
1320 };
1321
1322 let more_pending = queue.has_pending();
1327
1328 let mut guard = state_lock.lock();
1329 match sync_result {
1330 Ok(durable_lsn) => {
1331 guard.durable_lsn = durable_lsn;
1332 if !more_pending {
1333 guard.pending_statements = 0;
1334 guard.pending_wal_bytes = 0;
1335 guard.first_pending_at = None;
1336 }
1337 guard.last_error = None;
1338 let _ = target_lsn;
1339 }
1340 Err(err) => {
1341 guard.last_error = Some(err.to_string());
1342 }
1343 }
1344 cond.notify_all();
1345 }
1346 }
1347}
1348
1349impl Drop for StoreCommitCoordinator {
1350 fn drop(&mut self) {
1351 let (state_lock, cond) = &*self.state;
1352 let mut state = state_lock.lock();
1354 state.shutdown = true;
1355 cond.notify_all();
1356 }
1357}
1358
1359impl UnifiedStore {
1360 pub(crate) fn deferred_store_wal_capture_active() -> bool {
1361 deferred_store_wal_capture_active()
1362 }
1363
1364 pub(crate) fn begin_deferred_store_wal_capture() {
1365 begin_deferred_store_wal_capture();
1366 }
1367
1368 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
1369 take_deferred_store_wal_capture()
1370 }
1371
1372 pub(crate) fn append_deferred_store_wal_actions(
1373 &self,
1374 actions: DeferredStoreWalActions,
1375 ) -> Result<(), StoreError> {
1376 if actions.actions.is_empty() {
1377 return Ok(());
1378 }
1379 #[cfg(test)]
1380 if self
1381 .test_require_deferred_publication_fence
1382 .swap(false, Ordering::AcqRel)
1383 && !self.vault_publication_lock.is_owned_by_current_thread()
1384 {
1385 return Err(StoreError::Io(io::Error::other(
1386 "deferred WAL missing checkpoint publication fence",
1387 )));
1388 }
1389 #[cfg(test)]
1390 if self
1391 .test_fail_deferred_wal_append
1392 .swap(false, Ordering::AcqRel)
1393 {
1394 return Err(StoreError::Io(io::Error::other(
1395 "injected deferred WAL append failure",
1396 )));
1397 }
1398 if self.config.embedded_wal_path.is_some() {
1399 return self.append_embedded_store_wal_actions(&actions.actions);
1400 }
1401 if let Some(commit) = &self.commit {
1404 let references = commit
1405 .append_checkpoint_actions(&actions.actions)
1406 .map_err(StoreError::Io)?;
1407 self.remember_checkpoint_rows(actions.actions, references);
1408 Ok(())
1409 } else {
1410 self.flush_paged_state()
1411 }
1412 }
1413
1414 pub(crate) fn append_vector_insert_record(
1421 &self,
1422 collection: &str,
1423 entity_id: u64,
1424 vector: &[f32],
1425 ) -> std::io::Result<()> {
1426 let Some(commit) = &self.commit else {
1427 return Ok(());
1428 };
1429 let record = WalRecord::VectorInsert {
1430 collection: collection.to_string(),
1431 entity_id,
1432 vector: vector.to_vec(),
1433 };
1434 commit.append_single_record(record)
1435 }
1436
1437 pub(crate) fn append_probabilistic_delta_record(
1438 &self,
1439 kind: u8,
1440 operation: u8,
1441 name: &str,
1442 operands: Vec<Vec<u8>>,
1443 ) -> std::io::Result<()> {
1444 let Some(commit) = &self.commit else {
1445 return Ok(());
1446 };
1447 let record = WalRecord::ProbabilisticDelta {
1448 kind,
1449 operation,
1450 name: name.to_string(),
1451 operands,
1452 };
1453 commit.append_single_record(record)
1454 }
1455
1456 pub(crate) fn finish_paged_write(
1457 &self,
1458 actions: impl IntoIterator<Item = StoreWalAction>,
1459 ) -> Result<(), StoreError> {
1460 self.finish_paged_write_for_projection(
1461 actions,
1462 self.config.resident_memory_bytes_max.is_some(),
1463 )
1464 }
1465
1466 pub(crate) fn finish_paged_write_for_projection(
1467 &self,
1468 actions: impl IntoIterator<Item = StoreWalAction>,
1469 track: bool,
1470 ) -> Result<(), StoreError> {
1471 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
1472 if deferred_store_wal_capture_active() {
1473 let captured = capture_deferred_store_wal_actions(actions);
1474 debug_assert!(captured);
1475 return Ok(());
1476 }
1477 if self.config.embedded_wal_path.is_some() {
1478 return self.append_embedded_store_wal_actions(&actions);
1479 }
1480 let mut references = super::checkpoint_wal::CheckpointWalBatch::default();
1481 let result = match self.config.durability_mode {
1482 DurabilityMode::Strict => {
1483 if let Some(commit) = &self.commit {
1484 if track {
1485 references = commit
1486 .append_checkpoint_actions(&actions)
1487 .map_err(StoreError::Io)?;
1488 } else {
1489 commit.append_actions(&actions).map_err(StoreError::Io)?;
1490 }
1491 }
1492 self.flush_paged_state()
1493 }
1494 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
1495 if let Some(commit) = &self.commit {
1496 if track {
1497 references = commit
1498 .append_checkpoint_actions(&actions)
1499 .map_err(StoreError::Io)?;
1500 } else {
1501 commit.append_actions(&actions).map_err(StoreError::Io)?;
1502 }
1503 Ok(())
1504 } else {
1505 self.flush_paged_state()
1506 }
1507 }
1508 };
1509 result?;
1510 if track {
1511 self.remember_checkpoint_rows(actions, references);
1512 }
1513 Ok(())
1514 }
1515
1516 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
1517 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
1518 self.apply_replayed_action(&action, true)
1519 }
1520
1521 fn append_embedded_store_wal_actions(
1522 &self,
1523 actions: &[StoreWalAction],
1524 ) -> Result<(), StoreError> {
1525 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1526 return Ok(());
1527 };
1528 if actions.is_empty() {
1529 return Ok(());
1530 }
1531 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1532 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1533 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1534 match self.append_embedded_payloads(path, &payloads) {
1535 Ok(_) => Ok(()),
1536 Err(crate::api::RedDBError::InvalidOperation(msg))
1537 if msg.contains("embedded wal region full") =>
1538 {
1539 self.recover_full_embedded_wal(path, &payloads, encoded_len)
1540 }
1541 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1542 }
1543 }
1544
1545 fn recover_full_embedded_wal(
1546 &self,
1547 path: &Path,
1548 payloads: &[Vec<u8>],
1549 encoded_len: u64,
1550 ) -> Result<(), StoreError> {
1551 let _vault_guard = self.vault_publication_lock.lock();
1552 match self.append_embedded_payloads(path, payloads) {
1556 Ok(_) => return Ok(()),
1557 Err(crate::api::RedDBError::InvalidOperation(message))
1558 if message.contains("embedded wal region full") => {}
1559 Err(error) => return Err(StoreError::Io(std::io::Error::other(error.to_string()))),
1560 }
1561 struct CheckpointFence<'a>(Option<&'a crate::storage::embedded::EmbeddedCommitSession>);
1565 impl Drop for CheckpointFence<'_> {
1566 fn drop(&mut self) {
1567 if let Some(session) = self.0 {
1568 session.fence_interrupted_write();
1569 }
1570 }
1571 }
1572 let mut fence = CheckpointFence(Some(
1573 self.embedded_commit_session
1574 .get()
1575 .expect("WAL-full recovery follows an append attempt"),
1576 ));
1577 let started = Instant::now();
1578 let snapshot = self.to_binary_dump_bytes();
1579 let capacity = encoded_len.max((snapshot.len() as u64).min(EMBEDDED_WAL_HEADROOM_LIMIT));
1580 let checkpoint = crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
1581 path, &snapshot, capacity,
1582 )
1583 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1584 fence.0 = None;
1585 tracing::debug!(
1586 target: "reddb::embedded_checkpoint",
1587 snapshot_bytes = snapshot.len(),
1588 wal_region_bytes = checkpoint.manifest.wal_region_bytes,
1589 elapsed_us = started.elapsed().as_micros() as u64,
1590 "embedded WAL overflow checkpoint"
1591 );
1592 self.append_embedded_payloads(path, payloads)
1593 .map(|_| ())
1594 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1595 }
1596
1597 fn append_embedded_payloads(
1598 &self,
1599 path: &Path,
1600 payloads: &[Vec<u8>],
1601 ) -> crate::RedDBResult<crate::storage::EmbeddedRdbOpen> {
1602 let session = self
1603 .embedded_commit_session
1604 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1605 let options = self.config.group_commit;
1606 let grouped = self.config.durability_mode == DurabilityMode::WalDurableGrouped;
1607 let window = if grouped {
1608 StoreCommitCoordinator::resolve_window(&options)
1609 } else {
1610 Duration::ZERO
1611 };
1612 session.append(
1613 payloads,
1614 reddb_file::embedded::EmbeddedGroupCommitOptions {
1615 window,
1616 max_statements: if grouped { options.max_statements } else { 1 },
1617 max_wal_bytes: options.max_wal_bytes,
1618 },
1619 )
1620 }
1621
1622 pub(super) fn enqueue_grouped_actions(
1623 &self,
1624 actions: &[StoreWalAction],
1625 ) -> Result<Option<crate::storage::embedded::PendingEmbeddedAppend>, StoreError> {
1626 if self.config.durability_mode != DurabilityMode::WalDurableGrouped
1627 || deferred_store_wal_capture_active()
1628 {
1629 return Ok(None);
1630 }
1631 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1632 return Ok(None);
1633 };
1634 let session = self
1635 .embedded_commit_session
1636 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1637 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1638 let options = self.config.group_commit;
1639 session
1640 .enqueue(
1641 &payloads,
1642 reddb_file::embedded::EmbeddedGroupCommitOptions {
1643 window: StoreCommitCoordinator::resolve_window(&options),
1644 max_statements: options.max_statements,
1645 max_wal_bytes: options.max_wal_bytes,
1646 },
1647 )
1648 .map(Some)
1649 .map_err(|error| StoreError::Io(io::Error::other(error.to_string())))
1650 }
1651
1652 pub(super) fn complete_grouped_actions(
1653 &self,
1654 pending: &crate::storage::embedded::PendingEmbeddedAppend,
1655 actions: &[StoreWalAction],
1656 ) -> Result<(), StoreError> {
1657 match pending.wait() {
1658 Ok(_) => Ok(()),
1659 Err(crate::RedDBError::InvalidOperation(message))
1660 if message.contains("embedded wal region full") =>
1661 {
1662 self.append_embedded_store_wal_actions(actions)
1663 }
1664 Err(error) => Err(StoreError::Io(io::Error::other(error.to_string()))),
1665 }
1666 }
1667
1668 pub(crate) fn apply_replayed_action(
1669 &self,
1670 action: &StoreWalAction,
1671 update_btrees: bool,
1672 ) -> Result<(), StoreError> {
1673 reddb_engine::storage::store_wal::apply_replayed_action(
1674 &NativeReplayTarget {
1675 store: self,
1676 update_btrees,
1677 },
1678 action,
1679 )
1680 }
1681
1682 pub fn refresh_collection(
1699 &self,
1700 name: &str,
1701 entities: Vec<UnifiedEntity>,
1702 ) -> Result<Vec<Vec<u8>>, StoreError> {
1703 let name = Self::canonical_collection(name);
1704 let fv = self.format_version();
1705
1706 let new_manager = Arc::new(SegmentManager::with_config(
1709 name,
1710 self.config.manager_config.clone(),
1711 ));
1712
1713 let mut prepared = entities;
1714 for entity in &mut prepared {
1715 entity.canonicalize_system_collection_names();
1716 if entity.id.raw() == 0 {
1717 entity.id = self.next_entity_id();
1718 } else {
1719 self.register_entity_id(entity.id);
1720 }
1721 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1722 if *row_id == 0 {
1723 *row_id = new_manager.next_row_id();
1724 } else {
1725 new_manager.register_row_id(*row_id);
1726 }
1727 }
1728 entity.ensure_table_logical_id();
1729 }
1730
1731 let serialized: Vec<Vec<u8>> = prepared
1732 .iter()
1733 .map(|e| Self::serialize_entity_record(e, None, fv))
1734 .collect();
1735
1736 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1737 StoreError::Io(std::io::Error::other(format!(
1738 "refresh_collection: bulk_insert into new manager failed: {e}"
1739 )))
1740 })?;
1741
1742 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
1743
1744 self.finish_paged_write([StoreWalAction::RefreshCollection {
1745 collection: name.to_string(),
1746 records: serialized.clone(),
1747 }])?;
1748
1749 Ok(serialized)
1750 }
1751
1752 fn swap_collection_state(
1758 &self,
1759 name: &str,
1760 new_manager: Arc<SegmentManager>,
1761 prepared: &[UnifiedEntity],
1762 serialized: &[Vec<u8>],
1763 update_btrees: bool,
1764 ) {
1765 self.resident
1766 .swap_collection_state(name, new_manager, prepared);
1767
1768 if let Some(pager) = self
1769 .pager
1770 .as_ref()
1771 .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
1772 {
1773 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1774 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1775 .iter()
1776 .zip(serialized.iter())
1777 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1778 .collect();
1779 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1780 if !sorted.is_empty() {
1781 let _ = new_btree.bulk_insert_sorted(&sorted);
1782 }
1783 self.btree_indices
1784 .write()
1785 .insert(name.to_string(), new_btree);
1786 self.mark_paged_registry_dirty();
1787 }
1788 }
1789
1790 fn apply_replayed_refresh_collection(
1791 &self,
1792 collection: &str,
1793 records: &[Vec<u8>],
1794 update_btrees: bool,
1795 ) -> Result<(), StoreError> {
1796 let collection = Self::canonical_collection(collection);
1797 let (new_manager, prepared) = self.resident.prepare_replayed_refresh_collection(
1798 collection,
1799 records,
1800 self.format_version(),
1801 &self.config.manager_config,
1802 )?;
1803 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
1804 Ok(())
1805 }
1806
1807 pub fn refresh_collection_from_records(
1821 &self,
1822 name: &str,
1823 records: Vec<Vec<u8>>,
1824 ) -> Result<(), StoreError> {
1825 let name = Self::canonical_collection(name);
1826 self.apply_replayed_refresh_collection(name, &records, true)?;
1827 self.finish_paged_write([StoreWalAction::RefreshCollection {
1828 collection: name.to_string(),
1829 records,
1830 }])?;
1831 Ok(())
1832 }
1833
1834 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1835 let name = Self::canonical_collection(name);
1836 self.resident
1837 .create_collection_in_memory(name, &self.config.manager_config)
1838 }
1839
1840 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1841 let name = Self::canonical_collection(name);
1842 self.resident.drop_collection_in_memory(name, || {
1843 if let Some(tree) = self.btree_indices.write().remove(name) {
1844 self.retire_btree(tree);
1845 }
1846 })
1847 }
1848
1849 fn apply_replayed_upsert(
1850 &self,
1851 collection: &str,
1852 record: &[u8],
1853 update_btrees: bool,
1854 ) -> Result<(), StoreError> {
1855 let collection = Self::canonical_collection(collection);
1856 let id = self.resident.apply_replayed_upsert(
1857 collection,
1858 record,
1859 self.format_version(),
1860 &self.config.manager_config,
1861 )?;
1862
1863 if let Some(pager) = self
1864 .pager
1865 .as_ref()
1866 .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
1867 {
1868 let mut btree_indices = self.btree_indices.write();
1869 let btree = btree_indices
1870 .entry(collection.to_string())
1871 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1872 let root_before = btree.root_page_id();
1873 let key = id.raw().to_be_bytes();
1874 match btree.insert(&key, record) {
1875 Ok(_) => {}
1876 Err(BTreeError::DuplicateKey) => {
1877 let _ = btree.delete(&key);
1878 let _ = btree.insert(&key, record);
1879 }
1880 Err(err) => {
1881 return Err(StoreError::Io(io::Error::other(format!(
1882 "replay upsert btree error: {err}"
1883 ))));
1884 }
1885 }
1886 if root_before != btree.root_page_id() {
1887 self.mark_paged_registry_dirty();
1888 }
1889 }
1890
1891 Ok(())
1892 }
1893
1894 fn apply_replayed_delete(
1895 &self,
1896 collection: &str,
1897 id: EntityId,
1898 update_btrees: bool,
1899 ) -> Result<(), StoreError> {
1900 let collection = Self::canonical_collection(collection);
1901 self.resident.apply_replayed_delete(collection, id, || {
1902 if self.pager.is_some()
1903 && update_btrees
1904 && self.config.resident_memory_bytes_max.is_none()
1905 {
1906 let btree_indices = self.btree_indices.read();
1907 if let Some(btree) = btree_indices.get(collection) {
1908 let root_before = btree.root_page_id();
1909 let key = id.raw().to_be_bytes();
1910 let _ = btree.delete(&key);
1911 if root_before != btree.root_page_id() {
1912 self.mark_paged_registry_dirty();
1913 }
1914 }
1915 }
1916 })
1917 }
1918}
1919
1920#[cfg(all(test, target_os = "linux"))]
1921#[path = "storage_apocalypse.rs"]
1922mod storage_apocalypse;
1923
1924#[cfg(test)]
1925mod tests {
1926 use super::*;
1927 use crate::api::{DurabilityMode, GroupCommitOptions};
1928 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1929 use std::time::SystemTime;
1930
1931 #[test]
1932 fn ownership_epoch_rejects_nested_begin_and_wrong_finish() {
1933 let directory = tempfile::tempdir().expect("temporary store");
1934 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("store");
1935 store.create_collection("rows").expect("collection");
1936 store.persist().expect("baseline");
1937 let coordinator = store.commit.as_ref().expect("WAL coordinator");
1938 assert!(coordinator.begin_page_ownership_epoch(&store, 0).is_err());
1939 assert!(coordinator.finish_page_ownership_epoch(&store, 1).is_err());
1940 coordinator
1941 .begin_page_ownership_epoch(&store, 2)
1942 .expect("begin epoch");
1943 assert!(coordinator.begin_page_ownership_epoch(&store, 3).is_err());
1944 assert!(coordinator.finish_page_ownership_epoch(&store, 3).is_err());
1945 assert!(coordinator.finish_page_ownership_epoch(&store, 2).is_err());
1946 store.flush_paged_registry().expect("publish epoch");
1947 coordinator
1948 .finish_page_ownership_epoch(&store, 2)
1949 .expect("finish epoch");
1950 assert_eq!(store.ownership_active_epoch.load(Ordering::Acquire), 0);
1951 }
1952
1953 #[test]
1954 fn native_pager_records_page_ownership_in_durable_wal() {
1955 use reddb_file::page_ownership_journal::PageOwnershipRecord;
1956
1957 let directory = tempfile::tempdir().expect("temporary store");
1958 let path = directory.path().join("data.rdb");
1959 let wal_path = reddb_file::layout::unified_wal_path(&path);
1960 let pager = crate::storage::engine::Pager::open(&path, Default::default())
1961 .expect("open native pager");
1962 let coordinator = Arc::new(
1963 StoreCommitCoordinator::open(
1964 &wal_path,
1965 DurabilityMode::Async,
1966 GroupCommitOptions::default(),
1967 )
1968 .expect("open WAL"),
1969 );
1970 let recorder = Arc::new(
1971 StoreOwnershipRecorder::begin(Arc::clone(&coordinator), 7).expect("begin epoch"),
1972 );
1973 pager
1974 .install_ownership_recorder(recorder)
1975 .expect("install recorder");
1976 let page = pager
1977 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
1978 .expect("allocate")
1979 .page_id();
1980 pager
1981 .record_retirement_intent(&[page])
1982 .expect("record retirement");
1983 pager.free_retired_pages(&[page]).expect("retire");
1984 let records: Vec<_> = WalReader::open(&wal_path)
1985 .expect("read WAL")
1986 .iter()
1987 .map(|item| item.expect("WAL record").1)
1988 .collect();
1989 assert_eq!(
1990 records,
1991 vec![
1992 WalRecord::PageOwnership {
1993 record: PageOwnershipRecord::Begin { epoch: 7 }
1994 },
1995 WalRecord::PageOwnership {
1996 record: PageOwnershipRecord::Reserve {
1997 epoch: 7,
1998 page_id: page
1999 }
2000 },
2001 WalRecord::PageOwnership {
2002 record: PageOwnershipRecord::Retire {
2003 epoch: 7,
2004 page_id: page
2005 }
2006 },
2007 WalRecord::PageOwnership {
2008 record: PageOwnershipRecord::Retire {
2009 epoch: 7,
2010 page_id: page
2011 }
2012 },
2013 ]
2014 );
2015 assert!(
2016 coordinator.fsync_count() >= 3,
2017 "async ownership writes must sync"
2018 );
2019 assert!(
2020 coordinator.truncate().is_err(),
2021 "ownership WAL remains protected"
2022 );
2023 }
2024
2025 #[test]
2026 fn replay_reclaims_unpublished_ownership_reservations() {
2027 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2028
2029 let directory = tempfile::tempdir().expect("temporary store");
2030 let path = directory.path().join("data.rdb");
2031 {
2032 let store = UnifiedStore::open(&path).expect("create store");
2033 store.create_collection("rows").expect("collection");
2034 store.persist().expect("persist baseline");
2035 }
2036 let wal_path = reddb_file::layout::unified_wal_path(&path);
2037 let ids;
2038 {
2039 let pager =
2040 crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2041 let first = pager.page_count().expect("page count");
2042 let coordinator = StoreCommitCoordinator::open(
2043 &wal_path,
2044 DurabilityMode::Strict,
2045 GroupCommitOptions::default(),
2046 )
2047 .expect("WAL");
2048 coordinator
2049 .append_durable_page_ownership_batch(&[
2050 PageOwnershipRecord::Begin { epoch: 2 },
2051 PageOwnershipRecord::Reserve {
2052 epoch: 2,
2053 page_id: first,
2054 },
2055 PageOwnershipRecord::Reserve {
2056 epoch: 2,
2057 page_id: first + 1,
2058 },
2059 ])
2060 .expect("reserve pages durably");
2061 ids = [
2062 pager
2063 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2064 .expect("allocate")
2065 .page_id(),
2066 pager
2067 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2068 .expect("allocate")
2069 .page_id(),
2070 ];
2071 assert_eq!(ids, [first, first + 1]);
2072 pager.sync().expect("persist abandoned pages");
2073 }
2074 let reopened = UnifiedStore::open(&path).expect("recover ownership");
2075 let pager = reopened.pager.as_ref().expect("pager");
2076 let a = pager
2077 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2078 .expect("reuse")
2079 .page_id();
2080 let b = pager
2081 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2082 .expect("reuse")
2083 .page_id();
2084 assert_ne!(a, b);
2085 assert!(ids.contains(&a) && ids.contains(&b));
2086 let records: Vec<_> = WalReader::open(&wal_path)
2087 .expect("read WAL")
2088 .iter()
2089 .collect::<io::Result<Vec<_>>>()
2090 .expect("records");
2091 assert!(matches!(
2092 records.last(),
2093 Some((
2094 _,
2095 WalRecord::PageOwnership {
2096 record: PageOwnershipRecord::Finish { epoch: 2 }
2097 }
2098 ))
2099 ));
2100 reopened
2101 .commit
2102 .as_ref()
2103 .expect("coordinator")
2104 .truncate()
2105 .expect("completed ownership can be truncated");
2106 }
2107
2108 #[cfg(target_os = "linux")]
2109 #[test]
2110 fn ownership_recovery_survives_interruption_and_retry() {
2111 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2112
2113 const CHILD: &str = "REDDB_EXPERIMENTAL_OWNERSHIP_RECOVERY_PATH";
2114 if let Some(path) = std::env::var_os(CHILD) {
2115 let _store = UnifiedStore::open(path).expect("recover store");
2116 panic!("recovery interruption hook was not reached");
2117 }
2118 for (published, hook, exit_code) in [
2119 (
2120 false,
2121 "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2122 187,
2123 ),
2124 (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2125 (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2126 (
2127 true,
2128 "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2129 187,
2130 ),
2131 (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2132 (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2133 ] {
2134 let directory = tempfile::tempdir().expect("temporary store");
2135 let path = directory.path().join("data.rdb");
2136 {
2137 let store = UnifiedStore::open(&path).expect("create store");
2138 store.create_collection("rows").expect("collection");
2139 store.persist().expect("baseline");
2140 if published {
2141 store.ownership_active_epoch.store(2, Ordering::Release);
2142 store.flush_paged_registry().expect("publish epoch");
2143 store.ownership_active_epoch.store(0, Ordering::Release);
2144 }
2145 }
2146 let wal_path = reddb_file::layout::unified_wal_path(&path);
2147 let ids;
2148 {
2149 let pager =
2150 crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2151 let first = pager.page_count().expect("page count");
2152 let coordinator = StoreCommitCoordinator::open(
2153 &wal_path,
2154 DurabilityMode::Strict,
2155 GroupCommitOptions::default(),
2156 )
2157 .expect("WAL");
2158 let intent = |page_id| {
2159 if published {
2160 PageOwnershipRecord::Retire { epoch: 2, page_id }
2161 } else {
2162 PageOwnershipRecord::Reserve { epoch: 2, page_id }
2163 }
2164 };
2165 coordinator
2166 .append_durable_page_ownership_batch(&[
2167 PageOwnershipRecord::Begin { epoch: 2 },
2168 intent(first),
2169 intent(first + 1),
2170 ])
2171 .expect("reserve");
2172 ids = [
2173 pager
2174 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2175 .expect("allocate")
2176 .page_id(),
2177 pager
2178 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2179 .expect("allocate")
2180 .page_id(),
2181 ];
2182 pager.sync().expect("persist abandoned pages");
2183 }
2184 for _ in 0..if exit_code == 189 { 1 } else { 2 } {
2185 let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
2186 .args(["--exact", "storage::unified::store::commit::tests::ownership_recovery_survives_interruption_and_retry", "--nocapture"])
2187 .env(CHILD, &path)
2188 .env(hook, "1")
2189 .output().expect("run interrupted recovery");
2190 assert_eq!(
2191 output.status.code(),
2192 Some(exit_code),
2193 "published={published} {hook}: {}",
2194 String::from_utf8_lossy(&output.stderr)
2195 );
2196 }
2197 let reopened = UnifiedStore::open(&path).expect("recover after interruptions");
2198 let pager = reopened.pager.as_ref().expect("pager");
2199 let a = pager
2200 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2201 .expect("reuse")
2202 .page_id();
2203 let b = pager
2204 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2205 .expect("reuse")
2206 .page_id();
2207 assert_ne!(a, b, "published={published} {hook}: duplicate page");
2208 assert!(
2209 ids.contains(&a) && ids.contains(&b),
2210 "published={published} {hook}: lost recovered page"
2211 );
2212 }
2213 }
2214
2215 #[test]
2216 fn ownership_intents_are_durable_in_every_mode() {
2217 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2218
2219 for mode in [
2220 DurabilityMode::Strict,
2221 DurabilityMode::WalDurableGrouped,
2222 DurabilityMode::Async,
2223 ] {
2224 let directory = tempfile::tempdir().expect("temporary WAL");
2225 let path = directory.path().join("ownership");
2226 let coordinator =
2227 StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2228 .expect("open coordinator");
2229 let intent = PageOwnershipRecord::Reserve {
2230 epoch: 1,
2231 page_id: 67,
2232 };
2233 let detach = PageOwnershipRecord::DetachFree {
2234 epoch: 1,
2235 page_id: 68,
2236 };
2237 coordinator
2238 .append_durable_page_ownership_batch(&[intent, detach])
2239 .expect("durable intent");
2240 assert!(
2241 coordinator.fsync_count() > 0,
2242 "ownership intent must be synced in {mode:?}"
2243 );
2244 let records: Vec<_> = WalReader::open(&path)
2245 .expect("open WAL")
2246 .iter()
2247 .collect::<io::Result<Vec<_>>>()
2248 .expect("read records");
2249 assert!(
2250 matches!(records.as_slice(), [(_, WalRecord::PageOwnership { record: first }), (_, WalRecord::PageOwnership { record: second })] if *first == intent && *second == detach)
2251 );
2252 assert!(coordinator
2253 .append_single_record(WalRecord::PageOwnership { record: intent })
2254 .is_err());
2255 assert!(coordinator
2256 .append_durable_page_ownership(PageOwnershipRecord::Begin { epoch: 0 })
2257 .is_err());
2258 assert!(
2259 coordinator.truncate().is_err(),
2260 "active ownership must prevent truncation"
2261 );
2262 let before = std::fs::read(&path).expect("ownership WAL");
2263 drop(coordinator);
2264 let reopened = StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2265 .expect("reopen coordinator");
2266 assert!(
2267 reopened.truncate().is_err(),
2268 "reopen must rediscover ownership records"
2269 );
2270 assert_eq!(std::fs::read(&path).expect("preserved WAL"), before);
2271 }
2272 }
2273
2274 #[test]
2275 fn unresolved_ownership_intent_blocks_store_replay() {
2276 for truncated in [false, true] {
2277 let directory = tempfile::tempdir().expect("temporary store");
2278 let path = directory.path().join("data.rdb");
2279 {
2280 let store = UnifiedStore::open(&path).expect("create store");
2281 store.create_collection("rows").expect("create collection");
2282 store.persist().expect("persist baseline");
2283 }
2284 let wal_path = reddb_file::layout::unified_wal_path(&path);
2285 {
2286 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
2287 writer
2288 .append(&WalRecord::PageOwnership {
2289 record: reddb_file::page_ownership_journal::PageOwnershipRecord::Reserve {
2290 epoch: 1,
2291 page_id: 67,
2292 },
2293 })
2294 .expect("append ownership intent");
2295 writer.sync().expect("sync ownership intent");
2296 }
2297 if truncated {
2298 let wal = std::fs::OpenOptions::new()
2299 .write(true)
2300 .open(&wal_path)
2301 .expect("WAL");
2302 let length = wal.metadata().expect("length").len();
2303 wal.set_len(length - 1).expect("truncate last byte");
2304 wal.sync_all().expect("sync truncation");
2305 }
2306 let before = std::fs::read(&path).expect("baseline data");
2307 let error = match UnifiedStore::open(&path) {
2308 Ok(_) => panic!("unresolved ownership must prevent opening"),
2309 Err(error) => error,
2310 };
2311 let expected = if truncated {
2312 "incomplete page ownership WAL record"
2313 } else {
2314 "page ownership journal requires recovery"
2315 };
2316 assert!(error.to_string().contains(expected), "{error}");
2317 assert_eq!(std::fs::read(&path).expect("preserved data"), before);
2318 }
2319 }
2320
2321 #[test]
2322 fn logical_wal_replay_restores_a_deleted_physical_identity() {
2323 for flat in [false, true] {
2324 let store = UnifiedStore::new();
2325 store.create_collection("replay").expect("collection");
2326 let id = EntityId::new(42);
2327 let entity = UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(1)]);
2328 if flat {
2329 store
2330 .bulk_insert("replay", vec![entity])
2331 .expect("flat seed");
2332 } else {
2333 store.insert_auto("replay", entity).expect("hash seed");
2334 }
2335 let metadata = Metadata::with_fields(HashMap::from([(
2336 "source".into(),
2337 MetadataValue::String("wal".into()),
2338 )]));
2339 for sequence in 100..104 {
2340 let delete = StoreWalAction::DeleteEntityRecord {
2341 collection: "replay".into(),
2342 entity_id: id.raw(),
2343 };
2344 store
2345 .apply_encoded_store_wal_action(&delete.encode())
2346 .expect("replay delete");
2347 let mut restored =
2348 UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(2)]);
2349 restored.ensure_table_logical_id();
2350 restored.sequence_id = sequence;
2351 let upsert = StoreWalAction::upsert_entity(
2352 "replay",
2353 &restored,
2354 Some(&metadata),
2355 store.format_version(),
2356 );
2357 store
2358 .apply_encoded_store_wal_action(&upsert.encode())
2359 .expect("replay upsert");
2360 let manager = store.get_collection("replay").expect("restored collection");
2361 let stored = manager.get(id).expect("restored entity is readable");
2362 assert_eq!(store.entity_cache_stats().entries, 0);
2363 assert_eq!(manager.count(), 1);
2364 assert_eq!(manager.query_all(|_| true).len(), 1);
2365 assert_eq!(stored.sequence_id, sequence);
2366 assert_eq!(
2367 UnifiedStore::serialize_entity_record(
2368 &stored,
2369 manager.get_metadata(id).as_ref(),
2370 store.format_version()
2371 ),
2372 UnifiedStore::serialize_entity_record(
2373 &restored,
2374 Some(&metadata),
2375 store.format_version()
2376 )
2377 );
2378 assert!(store.get_any(id).is_some());
2379 }
2380 }
2381 }
2382
2383 fn embedded_fixture(
2384 mode: DurabilityMode,
2385 background_bytes: usize,
2386 ) -> (tempfile::TempDir, PathBuf, UnifiedStore) {
2387 let directory = tempfile::tempdir().expect("directory");
2388 let path = directory.path().join("data.rdb");
2389 let mut store =
2390 UnifiedStore::with_config(UnifiedStoreConfig::default().with_durability_mode(mode));
2391 store.create_collection("keys").expect("collection");
2392 store.set_aux_metadata(vec![42; background_bytes]);
2393 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(
2394 &path,
2395 &store.to_binary_dump_bytes(),
2396 )
2397 .expect("snapshot");
2398 store.config.embedded_wal_path = Some(path.clone());
2399 (directory, path, store)
2400 }
2401
2402 #[test]
2403 fn embedded_stale_full_result_does_not_checkpoint_reclaimed_space() {
2404 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2405 let (_directory, path, store) = embedded_fixture(mode, 0);
2406 store
2407 .append_embedded_store_wal_actions(&[StoreWalAction::CreateCollection {
2408 name: "keys".into(),
2409 }])
2410 .expect("another writer appended after reclaiming space");
2411 let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
2412 let payloads = vec![StoreWalAction::CreateCollection {
2413 name: "after".into(),
2414 }
2415 .encode()];
2416 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
2417 .expect("encoded length");
2418 store
2420 .recover_full_embedded_wal(&path, &payloads, length)
2421 .expect("revalidate after acquiring publication");
2422 let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
2423 assert_eq!(
2424 after.selected_superblock.generation,
2425 before.selected_superblock.generation + 1,
2426 "only the retry is published, without an unnecessary checkpoint"
2427 );
2428 assert_eq!(
2429 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&after)
2430 .expect("both appends retained")
2431 .len(),
2432 2
2433 );
2434 }
2435 }
2436
2437 #[test]
2438 fn embedded_full_wal_reserves_bounded_headroom_for_populated_store() {
2439 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2440 let (_directory, path, store) = embedded_fixture(mode, 2 * 1024 * 1024);
2441 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial");
2442 let action = StoreWalAction::CreateCollection {
2443 name: "keys".into(),
2444 };
2445 let payloads = vec![action.encode()];
2446 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
2447 .expect("encoded length");
2448 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(
2449 &path,
2450 &vec![payloads[0].clone(); (initial.manifest.wal_region_bytes / length) as usize],
2451 )
2452 .expect("fill the initial region with valid records");
2453 store
2454 .append_embedded_store_wal_actions(&[action])
2455 .expect("overflow");
2456 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("checkpoint");
2457 assert_eq!(open.manifest.wal_region_bytes, 1024 * 1024);
2458 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2459 .expect("snapshot")
2460 .expect("populated image");
2461 let recovered =
2462 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
2463 .expect("recover snapshot");
2464 for payload in
2465 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL")
2466 {
2467 recovered
2468 .apply_encoded_store_wal_action(&payload)
2469 .expect("replay");
2470 }
2471 assert_eq!(
2472 recovered.resident.aux_metadata.read().len(),
2473 2 * 1024 * 1024
2474 );
2475 assert!(recovered.get_collection("keys").is_some());
2476 }
2477 }
2478
2479 #[test]
2480 fn embedded_grouped_overwrites_recover_across_repeated_full_wal_regions() {
2481 fn row(writer: usize, round: usize) -> UnifiedEntity {
2482 let mut state = (writer as u64 + 1) * 31 + round as u64 * 13;
2483 let payload: String = (0..4096)
2484 .map(|_| {
2485 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
2486 char::from(b'!' + ((state >> 24) % 90) as u8)
2487 })
2488 .collect();
2489 let mut entity = UnifiedEntity::table_row(EntityId::new(0), "keys", 0, vec![]);
2490 entity.data = EntityData::Row(RowData::with_names(
2491 vec![
2492 Value::text(format!("k{writer}")),
2493 Value::text(format!("{round}:{payload}")),
2494 ],
2495 vec!["key".into(), "value".into()],
2496 ));
2497 entity
2498 }
2499 let (_directory, path, store) =
2500 embedded_fixture(DurabilityMode::WalDurableGrouped, 2 * 1024 * 1024);
2501 assert!(
2502 StoreWalAction::upsert_entity("keys", &row(0, 0), None, store.format_version())
2503 .encode()
2504 .len()
2505 >= 2048,
2506 "fixture must exert physical WAL pressure after value compression"
2507 );
2508 let mut ids: Vec<_> = (0..8)
2509 .map(|writer| {
2510 store
2511 .write_keyed_row("keys", row(writer, 0), None, None)
2512 .expect("seed")
2513 })
2514 .collect();
2515 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial roots");
2516 let mut previous_snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&initial)
2517 .expect("initial snapshot")
2518 .expect("image");
2519 for wave in 0..3 {
2523 let barrier = Barrier::new(ids.len());
2524 ids = std::thread::scope(|scope| {
2525 let workers: Vec<_> = ids
2526 .iter()
2527 .enumerate()
2528 .map(|(writer, previous)| {
2529 let store = &store;
2530 let barrier = &barrier;
2531 scope.spawn(move || {
2532 barrier.wait();
2533 let mut id = *previous;
2534 for round in 0..64 {
2535 id = store
2536 .write_keyed_row(
2537 "keys",
2538 row(writer, wave * 64 + round),
2539 None,
2540 Some(id),
2541 )
2542 .expect("durable overwrite");
2543 }
2544 id
2545 })
2546 })
2547 .collect();
2548 workers
2549 .into_iter()
2550 .map(|worker| worker.join().expect("writer"))
2551 .collect()
2552 });
2553 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("wave roots");
2554 assert_eq!(open.manifest.wal_region_bytes, EMBEDDED_WAL_HEADROOM_LIMIT);
2555 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2556 .expect("wave snapshot")
2557 .expect("image");
2558 assert!(snapshot != previous_snapshot, "wave {wave} must checkpoint");
2559 previous_snapshot = snapshot;
2560 }
2561 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("recover roots");
2562 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2563 .expect("snapshot")
2564 .expect("image");
2565 let recovered =
2566 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
2567 .expect("snapshot replay");
2568 for payload in crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL") {
2569 recovered
2570 .apply_encoded_store_wal_action(&payload)
2571 .expect("WAL replay");
2572 }
2573 assert_eq!(recovered.get_collection("keys").expect("keys").count(), 8);
2574 for (writer, id) in ids.iter().enumerate() {
2575 let entity = recovered
2576 .get("keys", *id)
2577 .expect("last acknowledged version");
2578 assert_eq!(
2579 entity.data.as_row().expect("row").get_field("value"),
2580 row(writer, 191)
2581 .data
2582 .as_row()
2583 .expect("expected row")
2584 .get_field("value")
2585 );
2586 }
2587 }
2588
2589 fn env_lock() -> &'static StdMutex<()> {
2594 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
2595 LOCK.get_or_init(|| StdMutex::new(()))
2596 }
2597
2598 fn temp_wal(name: &str) -> PathBuf {
2599 let nanos = SystemTime::now()
2600 .duration_since(std::time::UNIX_EPOCH)
2601 .unwrap()
2602 .as_nanos();
2603 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
2604 &std::env::temp_dir(),
2605 name,
2606 std::process::id(),
2607 nanos,
2608 );
2609 let _ = std::fs::remove_file(&path);
2610 path
2611 }
2612
2613 fn replay_crash_payload(id: u64, round: u64) -> String {
2614 let mut state = id * 31 + round * 13 + 1;
2615 (0..600)
2616 .map(|_| {
2617 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
2618 char::from(b'!' + ((state >> 24) % 90) as u8)
2619 })
2620 .collect()
2621 }
2622
2623 #[cfg(target_os = "linux")]
2624 #[test]
2625 fn interrupted_replay_reclaims_unpublished_pages() {
2626 interrupted_replay_reclaims_pages(false, false);
2627 }
2628
2629 #[cfg(target_os = "linux")]
2630 #[test]
2631 fn interrupted_replay_reclaims_durable_unpublished_pages() {
2632 interrupted_replay_reclaims_pages(false, true);
2633 }
2634
2635 #[cfg(target_os = "linux")]
2636 #[test]
2637 fn interrupted_replay_preserves_published_pages() {
2638 interrupted_replay_reclaims_pages(true, false);
2639 }
2640
2641 #[cfg(target_os = "linux")]
2642 #[test]
2643 fn interrupted_checkpoint_reclaims_unpublished_pages() {
2644 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_REBUILD_PATH";
2645 let config = || {
2646 UnifiedStoreConfig::default()
2647 .with_durability_mode(DurabilityMode::Strict)
2648 .with_page_cache_slots(4)
2649 };
2650 if let Some(path) = std::env::var_os(CHILD_PATH) {
2651 let store = UnifiedStore::open_with_config(path, config()).unwrap();
2652 for id in 1..=200 {
2655 let entity = UnifiedEntity::table_row(
2656 EntityId::new(id),
2657 "rows",
2658 id,
2659 vec![Value::text(replay_crash_payload(id, 8))],
2660 );
2661 let record =
2662 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
2663 store.apply_replayed_upsert("rows", &record, false).unwrap();
2664 }
2665 store.persist().expect("must exit during checkpoint");
2666 panic!("checkpoint rebuild hook was not reached");
2667 }
2668 let directory = tempfile::tempdir().unwrap();
2669 let path = directory.path().join("data.rdb");
2670 {
2671 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2672 store.create_collection("rows").unwrap();
2673 for id in 1..=200 {
2674 store
2675 .insert(
2676 "rows",
2677 UnifiedEntity::table_row(
2678 EntityId::new(id),
2679 "rows",
2680 id,
2681 vec![Value::text(format!("initial-{id}"))],
2682 ),
2683 )
2684 .unwrap();
2685 }
2686 store.persist().unwrap();
2687 }
2688 let mut first = 0;
2689 let mut last = 0;
2690 for cycle in 1..=12 {
2691 let output = std::process::Command::new(std::env::current_exe().unwrap())
2692 .args([
2693 "--exact",
2694 "storage::unified::store::commit::tests::interrupted_checkpoint_reclaims_unpublished_pages",
2695 "--nocapture",
2696 ])
2697 .env(CHILD_PATH, &path)
2698 .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
2699 .output()
2700 .unwrap();
2701 assert_eq!(
2702 output.status.code(),
2703 Some(183),
2704 "{}",
2705 String::from_utf8_lossy(&output.stderr)
2706 );
2707 assert!(String::from_utf8_lossy(&output.stderr)
2708 .contains("experimental checkpoint exit before manifest"));
2709 last = std::fs::metadata(&path).unwrap().len();
2710 if cycle == 1 {
2711 first = last;
2712 }
2713 eprintln!("checkpoint_exit_cycle={cycle} data_logical={last}");
2714 }
2715 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2716 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
2717 for id in 1..=200 {
2718 assert_eq!(
2719 recovered
2720 .get("rows", EntityId::new(id))
2721 .unwrap()
2722 .data
2723 .as_row()
2724 .unwrap()
2725 .columns,
2726 vec![Value::text(format!("initial-{id}"))]
2727 );
2728 }
2729 assert!(last <= first + 2 * crate::storage::engine::PAGE_SIZE as u64,
2730 "repeated interrupted checkpoint must bound page growth: first={first}, after_12={last}");
2731 }
2732
2733 #[cfg(target_os = "linux")]
2734 #[test]
2735 fn interrupted_checkpoint_with_wal_reclaims_unpublished_pages() {
2736 use std::os::unix::fs::MetadataExt;
2737 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_PATH";
2738 const ROUND: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_ROUND";
2739 let config = || {
2740 UnifiedStoreConfig::default()
2741 .with_durability_mode(DurabilityMode::Strict)
2742 .with_page_cache_slots(4)
2743 };
2744 if let Some(path) = std::env::var_os(CHILD_PATH) {
2745 let round: u64 = std::env::var(ROUND).unwrap().parse().unwrap();
2746 let store = UnifiedStore::open_with_config(path, config()).unwrap();
2747 if round > 1 {
2748 for id in 1..=200 {
2749 assert_eq!(
2750 store
2751 .get("rows", EntityId::new(id))
2752 .unwrap()
2753 .data
2754 .as_row()
2755 .unwrap()
2756 .columns,
2757 vec![Value::text(replay_crash_payload(id, round - 1))]
2758 );
2759 }
2760 }
2761 let actions: Vec<_> = (1..=200)
2762 .map(|id| {
2763 let entity = UnifiedEntity::table_row(
2764 EntityId::new(id),
2765 "rows",
2766 id,
2767 vec![Value::text(replay_crash_payload(id, round))],
2768 );
2769 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
2770 })
2771 .collect();
2772 for action in &actions {
2773 store.apply_replayed_action(action, false).unwrap();
2774 }
2775 store
2778 .commit
2779 .as_ref()
2780 .unwrap()
2781 .append_actions(&actions)
2782 .unwrap();
2783 store.persist().expect("must exit during checkpoint");
2784 panic!("checkpoint rebuild hook was not reached");
2785 }
2786 let directory = tempfile::tempdir().unwrap();
2787 let path = directory.path().join("data.rdb");
2788 {
2789 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2790 store.create_collection("rows").unwrap();
2791 for id in 1..=200 {
2792 store
2793 .insert(
2794 "rows",
2795 UnifiedEntity::table_row(
2796 EntityId::new(id),
2797 "rows",
2798 id,
2799 vec![Value::text(format!("initial-{id}"))],
2800 ),
2801 )
2802 .unwrap();
2803 }
2804 store.persist().unwrap();
2805 }
2806 let mut fourth = 0;
2807 let mut last = 0;
2808 for round in 1..=12 {
2809 let output = std::process::Command::new(std::env::current_exe().unwrap())
2810 .args([
2811 "--exact",
2812 "storage::unified::store::commit::tests::interrupted_checkpoint_with_wal_reclaims_unpublished_pages",
2813 "--nocapture",
2814 ])
2815 .env(CHILD_PATH, &path)
2816 .env(ROUND, round.to_string())
2817 .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
2818 .output().unwrap();
2819 assert_eq!(
2820 output.status.code(),
2821 Some(183),
2822 "round {round}: {}",
2823 String::from_utf8_lossy(&output.stderr)
2824 );
2825 assert!(String::from_utf8_lossy(&output.stderr)
2826 .contains("experimental checkpoint exit before manifest"));
2827 let meta = std::fs::metadata(&path).unwrap();
2828 last = meta.len();
2829 if round == 4 {
2830 fourth = last;
2831 }
2832 eprintln!(
2833 "checkpoint_wal_exit_round={round} data_logical={last} data_allocated={}",
2834 meta.blocks() * 512
2835 );
2836 }
2837 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2838 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
2839 for id in 1..=200 {
2840 assert_eq!(
2841 recovered
2842 .get("rows", EntityId::new(id))
2843 .unwrap()
2844 .data
2845 .as_row()
2846 .unwrap()
2847 .columns,
2848 vec![Value::text(replay_crash_payload(id, 12))]
2849 );
2850 }
2851 recovered.persist().unwrap();
2852 drop(recovered);
2853 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2854 for id in 1..=200 {
2855 assert_eq!(
2856 reopened
2857 .get("rows", EntityId::new(id))
2858 .unwrap()
2859 .data
2860 .as_row()
2861 .unwrap()
2862 .columns,
2863 vec![Value::text(replay_crash_payload(id, 12))]
2864 );
2865 }
2866 assert!(last <= fourth + 2 * crate::storage::engine::PAGE_SIZE as u64,
2867 "repeated interrupted WAL-backed checkpoints must bound page growth: round4={fourth}, round12={last}");
2868 }
2869
2870 #[cfg(target_os = "linux")]
2871 #[test]
2872 fn interrupted_retired_reclamation_preserves_reuse() {
2873 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_RETIRED_PATH";
2874 fn mutate(store: &UnifiedStore, round: u64) {
2875 for id in 1..=200 {
2876 let entity = UnifiedEntity::table_row(
2877 EntityId::new(id),
2878 "rows",
2879 id,
2880 vec![Value::text(replay_crash_payload(id, round))],
2881 );
2882 let record =
2883 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
2884 store.apply_replayed_upsert("rows", &record, false).unwrap();
2885 }
2886 }
2887 let config = || {
2888 UnifiedStoreConfig::default()
2889 .with_durability_mode(DurabilityMode::Strict)
2890 .with_page_cache_slots(4)
2891 };
2892 if let Some(path) = std::env::var_os(CHILD_PATH) {
2893 let store = UnifiedStore::open_with_config(path, config()).unwrap();
2894 mutate(&store, 2);
2895 store.persist().expect("must exit at retired free boundary");
2896 panic!("retired free hook was not reached");
2897 }
2898 let mut reuse = Vec::new();
2899 for (label, env_name, exit_code, message) in [
2900 (
2901 "before",
2902 "REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE",
2903 184,
2904 "experimental exit before retired free",
2905 ),
2906 (
2907 "enqueued",
2908 "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE",
2909 186,
2910 "experimental exit after retired enqueue",
2911 ),
2912 (
2913 "after",
2914 "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE",
2915 185,
2916 "experimental exit after retired free",
2917 ),
2918 ] {
2919 let directory = tempfile::tempdir().unwrap();
2920 let path = directory.path().join("data.rdb");
2921 {
2922 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2923 store.create_collection("rows").unwrap();
2924 for id in 1..=200 {
2925 store
2926 .insert(
2927 "rows",
2928 UnifiedEntity::table_row(
2929 EntityId::new(id),
2930 "rows",
2931 id,
2932 vec![Value::text(format!("initial-{id}"))],
2933 ),
2934 )
2935 .unwrap();
2936 }
2937 store.persist().unwrap();
2938 mutate(&store, 1);
2939 store.persist().unwrap();
2940 }
2941 let output = std::process::Command::new(std::env::current_exe().unwrap())
2942 .args([
2943 "--exact",
2944 "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse",
2945 "--nocapture",
2946 ])
2947 .env(CHILD_PATH, &path)
2948 .env(env_name, "1")
2949 .output().unwrap();
2950 assert_eq!(
2951 output.status.code(),
2952 Some(exit_code),
2953 "{label}: {}",
2954 String::from_utf8_lossy(&output.stderr)
2955 );
2956 assert!(String::from_utf8_lossy(&output.stderr).contains(message));
2957 eprintln!(
2958 "retired_reclaim_{label} child={}",
2959 String::from_utf8_lossy(&output.stderr).trim()
2960 );
2961 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2962 for id in 1..=200 {
2963 assert_eq!(
2964 store
2965 .get("rows", EntityId::new(id))
2966 .unwrap()
2967 .data
2968 .as_row()
2969 .unwrap()
2970 .columns,
2971 vec![Value::text(replay_crash_payload(id, 2))]
2972 );
2973 }
2974 let pager = store.pager.as_ref().unwrap();
2975 let before_pages = pager.page_count().unwrap();
2976 let mut ids = std::collections::HashSet::new();
2977 let mut reused = 0;
2978 for _ in 0..256 {
2979 let page = pager
2980 .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
2981 .unwrap();
2982 let id = page.page_id();
2983 assert!(ids.insert(id), "duplicate allocation");
2984 reused += u32::from(id < before_pages);
2985 pager.write_page(id, page).unwrap();
2986 }
2987 pager.sync().unwrap();
2988 eprintln!(
2989 "retired_reclaim_{label} before_pages={before_pages} reused={reused} appended={}",
2990 256 - reused
2991 );
2992 drop(store);
2993 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2994 for id in 1..=200 {
2995 assert_eq!(
2996 reopened
2997 .get("rows", EntityId::new(id))
2998 .unwrap()
2999 .data
3000 .as_row()
3001 .unwrap()
3002 .columns,
3003 vec![Value::text(replay_crash_payload(id, 2))]
3004 );
3005 }
3006 reuse.push(reused);
3007 }
3008 assert!(
3009 reuse[0] >= reuse[2] && reuse[1] >= reuse[2],
3010 "recovery must not lose retired pages before allocator publication: before={} enqueued={} after={}",
3011 reuse[0],
3012 reuse[1],
3013 reuse[2]
3014 );
3015 }
3016
3017 #[cfg(target_os = "linux")]
3018 #[test]
3019 fn experimental_retaining_released_root_is_not_recoverable() {
3020 let directory = tempfile::tempdir().unwrap();
3021 let path = directory.path().join("data.rdb");
3022 let config = || {
3023 UnifiedStoreConfig::default()
3024 .with_durability_mode(DurabilityMode::Strict)
3025 .with_page_cache_slots(4)
3026 };
3027 {
3028 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3029 store.create_collection("rows").unwrap();
3030 for id in 1..=200 {
3031 store
3032 .insert(
3033 "rows",
3034 UnifiedEntity::table_row(
3035 EntityId::new(id),
3036 "rows",
3037 id,
3038 vec![Value::text(format!("initial-{id}"))],
3039 ),
3040 )
3041 .unwrap();
3042 }
3043 store.persist().unwrap();
3044 for id in 1..=200 {
3045 let entity = UnifiedEntity::table_row(
3046 EntityId::new(id),
3047 "rows",
3048 id,
3049 vec![Value::text(replay_crash_payload(id, 1))],
3050 );
3051 let record =
3052 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3053 store.apply_replayed_upsert("rows", &record, false).unwrap();
3054 }
3055 store.persist().unwrap();
3056 }
3057 let output = std::process::Command::new(std::env::current_exe().unwrap())
3058 .args(["--exact", "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse", "--nocapture"])
3059 .env("REDDB_EXPERIMENTAL_RETIRED_PATH", &path)
3060 .env("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT", "1")
3061 .env("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE", "1")
3062 .output().unwrap();
3063 assert_eq!(
3064 output.status.code(),
3065 Some(185),
3066 "{}",
3067 String::from_utf8_lossy(&output.stderr)
3068 );
3069 assert!(String::from_utf8_lossy(&output.stderr)
3070 .contains("experimental exit after retired free"));
3071
3072 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3073 for id in 1..=200 {
3074 assert_eq!(
3075 reopened
3076 .get("rows", EntityId::new(id))
3077 .unwrap()
3078 .data
3079 .as_row()
3080 .unwrap()
3081 .columns,
3082 vec![Value::text(replay_crash_payload(id, 2))]
3083 );
3084 }
3085 let retired_roots: Vec<_> = reopened
3086 .retired_btrees
3087 .lock()
3088 .iter()
3089 .map(|entry| entry.tree.root_page_id())
3090 .collect();
3091 let pager = reopened.pager.as_ref().unwrap();
3092 let root_types: Vec<_> = retired_roots
3093 .iter()
3094 .map(|id| pager.read_page(*id).unwrap().page_type().unwrap())
3095 .collect();
3096 assert!(
3097 root_types.contains(&crate::storage::engine::page::PageType::FreelistTrunk),
3098 "expected a retained root to have become a freelist trunk: {root_types:?}"
3099 );
3100 let error = reopened
3101 .persist()
3102 .expect_err("retained root was reused as allocator metadata");
3103 assert!(
3104 error
3105 .to_string()
3106 .contains("unexpected retired tree page type"),
3107 "{error}"
3108 );
3109 eprintln!("retained_root_types={root_types:?} persist_error={error}");
3110 }
3111
3112 #[cfg(target_os = "linux")]
3113 fn interrupted_replay_reclaims_pages(after_publication: bool, force_sync: bool) {
3114 use std::os::unix::fs::MetadataExt;
3115 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_REPLAY_REBUILD_PATH";
3116 let config = || {
3117 UnifiedStoreConfig::default()
3118 .with_durability_mode(DurabilityMode::Strict)
3119 .with_page_cache_slots(4)
3120 };
3121 if let Some(path) = std::env::var_os(CHILD_PATH) {
3122 let _store = UnifiedStore::open_with_config(path, config())
3123 .expect("must exit while replay rebuilds");
3124 panic!("replay rebuild hook was not reached");
3125 }
3126 let directory = tempfile::tempdir().unwrap();
3127 let path = directory.path().join("data.rdb");
3128 {
3129 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3130 store.create_collection("rows").unwrap();
3131 for id in 1..=200 {
3132 store
3133 .insert(
3134 "rows",
3135 UnifiedEntity::table_row(
3136 EntityId::new(id),
3137 "rows",
3138 id,
3139 vec![Value::text(format!("initial-{id}"))],
3140 ),
3141 )
3142 .unwrap();
3143 }
3144 store.persist().unwrap();
3145 }
3146 {
3147 let coordinator = StoreCommitCoordinator::open(
3148 reddb_file::layout::unified_wal_path(&path),
3149 DurabilityMode::Strict,
3150 GroupCommitOptions::default(),
3151 )
3152 .unwrap();
3153 let actions = (1..=200)
3154 .map(|id| {
3155 let entity = UnifiedEntity::table_row(
3156 EntityId::new(id),
3157 "rows",
3158 id,
3159 vec![Value::text(replay_crash_payload(id, 8))],
3160 );
3161 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3162 })
3163 .collect::<Vec<_>>();
3164 coordinator.append_actions(&actions).unwrap();
3165 }
3166 let snapshot = || {
3167 let data = std::fs::metadata(&path).unwrap();
3168 let wal = std::fs::metadata(reddb_file::layout::unified_wal_path(&path)).unwrap();
3169 (
3170 data.len(),
3171 data.blocks() * 512,
3172 wal.len(),
3173 wal.blocks() * 512,
3174 )
3175 };
3176 let baseline = snapshot();
3177 let mut previous = baseline;
3178 let mut first_failure = baseline;
3179 for cycle in 1..=12 {
3180 let test_name = if after_publication {
3181 "storage::unified::store::commit::tests::interrupted_replay_preserves_published_pages"
3182 } else if force_sync {
3183 "storage::unified::store::commit::tests::interrupted_replay_reclaims_durable_unpublished_pages"
3184 } else {
3185 "storage::unified::store::commit::tests::interrupted_replay_reclaims_unpublished_pages"
3186 };
3187 let mut command = std::process::Command::new(std::env::current_exe().unwrap());
3188 command
3189 .args(["--exact", test_name, "--nocapture"])
3190 .env(CHILD_PATH, &path);
3191 if !force_sync {
3192 command.env("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC", "1");
3193 } else {
3194 command.env_remove("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC");
3195 }
3196 let message = if after_publication {
3197 command.env("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", "1");
3198 "experimental replay exit after manifest"
3199 } else {
3200 command.env("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD", "1");
3201 "experimental replay rebuild exit before manifest"
3202 };
3203 let output = command.output().unwrap();
3204 assert_eq!(
3205 output.status.code(),
3206 Some(182),
3207 "cycle {cycle}: {}",
3208 String::from_utf8_lossy(&output.stderr)
3209 );
3210 assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3211 let now = snapshot();
3212 eprintln!("replay_exit_cycle={cycle} data_logical={} data_allocated={} wal_logical={} wal_allocated={}",now.0,now.1,now.2,now.3);
3213 if cycle == 1 {
3214 first_failure = now;
3215 }
3216 previous = now;
3217 }
3218 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3219 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3220 for id in 1..=200 {
3221 assert_eq!(
3222 recovered
3223 .get("rows", EntityId::new(id))
3224 .unwrap()
3225 .data
3226 .as_row()
3227 .unwrap()
3228 .columns,
3229 vec![Value::text(replay_crash_payload(id, 8))]
3230 );
3231 }
3232 let pages = recovered.pager.as_ref().unwrap().page_count().unwrap();
3233 let free = recovered.pager.as_ref().unwrap().free_page_count().unwrap();
3234 recovered.persist().unwrap();
3235 drop(recovered);
3236 let final_sizes = snapshot();
3237 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3238 assert_eq!(reopened.get_collection("rows").unwrap().count(), 200);
3239 for id in 1..=200 {
3240 assert_eq!(
3241 reopened
3242 .get("rows", EntityId::new(id))
3243 .unwrap()
3244 .data
3245 .as_row()
3246 .unwrap()
3247 .columns,
3248 vec![Value::text(replay_crash_payload(id, 8))]
3249 );
3250 }
3251 eprintln!("replay_rebuild baseline={baseline:?} after_12={previous:?} after_recovery_checkpoint={final_sizes:?} pages={pages} free={free}");
3252 let pager = reopened.pager.as_ref().unwrap();
3256 let before_probe = pager.page_count().unwrap();
3257 let mut ids = std::collections::HashSet::new();
3258 let mut reused = 0;
3259 for _ in 0..256 {
3260 let page = pager
3261 .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3262 .unwrap();
3263 let id = page.page_id();
3264 assert!(ids.insert(id), "allocator returned the same page twice");
3265 reused += u32::from(id < before_probe);
3266 pager.write_page(id, page).unwrap();
3267 }
3268 pager.sync().unwrap();
3269 let after_probe = pager.page_count().unwrap();
3270 eprintln!("replay_reuse_probe count=256 before_pages={before_probe} after_pages={after_probe} reused={reused} appended={}", 256 - reused);
3271 drop(reopened);
3272 let verified = UnifiedStore::open_with_config(&path, config()).unwrap();
3273 for id in 1..=200 {
3274 assert_eq!(
3275 verified
3276 .get("rows", EntityId::new(id))
3277 .unwrap()
3278 .data
3279 .as_row()
3280 .unwrap()
3281 .columns,
3282 vec![Value::text(replay_crash_payload(id, 8))]
3283 );
3284 }
3285 assert!(
3286 previous.0 <= first_failure.0 + 2 * crate::storage::engine::PAGE_SIZE as u64,
3287 "repeated interrupted replay must bound page growth: first={}, after_12={}",
3288 first_failure.0,
3289 previous.0
3290 );
3291 }
3292
3293 #[test]
3294 fn paged_wal_replay_survives_immediate_process_exit() {
3295 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
3296 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3297 if let Some(path) = std::env::var_os(CHILD_PATH) {
3298 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
3299 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
3300 std::process::exit(0);
3302 }
3303 let directory = tempfile::tempdir().unwrap();
3304 let path = directory.path().join("data.rdb");
3305 {
3306 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3307 store.create_collection("rows").unwrap();
3308 for id in 1..=400 {
3309 store
3310 .insert(
3311 "rows",
3312 UnifiedEntity::table_row(
3313 EntityId::new(id),
3314 "rows",
3315 id,
3316 vec![Value::text(format!("initial-{id}"))],
3317 ),
3318 )
3319 .unwrap();
3320 }
3321 store.persist().unwrap();
3322 }
3323 {
3324 let coordinator = StoreCommitCoordinator::open(
3325 reddb_file::layout::unified_wal_path(&path),
3326 DurabilityMode::Strict,
3327 GroupCommitOptions::default(),
3328 )
3329 .unwrap();
3330 for round in 0..3 {
3331 let actions = (1..=400)
3332 .map(|id| {
3333 let payload = replay_crash_payload(id, round);
3334 let entity = UnifiedEntity::table_row(
3335 EntityId::new(id),
3336 "rows",
3337 id,
3338 vec![Value::text(payload)],
3339 );
3340 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3341 })
3342 .collect::<Vec<_>>();
3343 coordinator.append_actions(&actions).unwrap();
3344 }
3345 }
3346 let status = std::process::Command::new(std::env::current_exe().unwrap())
3347 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
3348 .env(CHILD_PATH, &path).status().unwrap();
3349 assert!(status.success(), "replay process failed");
3350 let reopened = UnifiedStore::open_with_config(&path, config())
3351 .expect("cold reopen after replay crash");
3352 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
3353 for id in 1..=400 {
3354 let entity = reopened
3355 .get("rows", EntityId::new(id))
3356 .expect("retained entity");
3357 let EntityData::Row(row) = entity.data else {
3358 panic!("expected row")
3359 };
3360 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
3361 }
3362 drop(reopened);
3363 let stable_bytes = std::fs::metadata(&path).unwrap().len();
3364 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
3365 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
3366 assert_eq!(
3367 std::fs::metadata(&path).unwrap().len(),
3368 stable_bytes,
3369 "idempotent replay must not allocate another tree generation"
3370 );
3371 }
3372
3373 #[test]
3374 fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
3375 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3376 let path = temp_wal("checkpoint-ack");
3377 let coordinator = Arc::new(
3378 StoreCommitCoordinator::open(
3379 path.clone(),
3380 DurabilityMode::WalDurableGrouped,
3381 GroupCommitOptions::default(),
3382 )
3383 .unwrap(),
3384 );
3385 let state = coordinator.state.0.lock();
3388 let (written_tx, written_rx) = std::sync::mpsc::channel();
3389 let writer_coordinator = Arc::clone(&coordinator);
3390 let writer = std::thread::spawn(move || {
3391 let result = writer_coordinator.append_actions(&[StoreWalAction::CreateCollection {
3392 name: "acknowledged".into(),
3393 }]);
3394 written_tx.send(result).unwrap();
3395 });
3396 let deadline = Instant::now() + Duration::from_secs(2);
3397 while !coordinator.queue.has_pending() {
3398 assert!(Instant::now() < deadline, "writer never enqueued");
3399 std::thread::yield_now();
3400 }
3401 let (started_tx, started_rx) = std::sync::mpsc::channel();
3402 let (reset_tx, reset_rx) = std::sync::mpsc::channel();
3403 let reset_coordinator = Arc::clone(&coordinator);
3404 let reset = std::thread::spawn(move || {
3405 started_tx.send(()).unwrap();
3406 reset_tx.send(reset_coordinator.truncate()).unwrap();
3407 });
3408 started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
3409 let deadline = Instant::now() + Duration::from_millis(100);
3410 while coordinator.queue.has_pending() && Instant::now() < deadline {
3411 std::thread::sleep(Duration::from_millis(1));
3412 }
3413 let discarded_before_ack = !coordinator.queue.has_pending();
3414 drop(state);
3415 let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
3416 if acknowledged.is_err() {
3417 let mut state = coordinator.state.0.lock();
3420 state.shutdown = true;
3421 state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
3422 coordinator.state.1.notify_all();
3423 }
3424 writer.join().unwrap();
3425 reset_rx
3426 .recv_timeout(Duration::from_secs(2))
3427 .unwrap()
3428 .unwrap();
3429 reset.join().unwrap();
3430 assert!(
3431 !discarded_before_ack,
3432 "checkpoint discarded an unacknowledged WAL record"
3433 );
3434 assert!(
3435 acknowledged.unwrap().is_ok(),
3436 "durable writer did not finish"
3437 );
3438 drop(coordinator);
3439 let _ = std::fs::remove_file(path);
3440 }
3441
3442 #[test]
3453 fn group_commit_coalesces_concurrent_autocommits() {
3454 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3455 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3458
3459 let path = temp_wal("coalesce");
3460 let coord = Arc::new(
3461 StoreCommitCoordinator::open(
3462 path.clone(),
3463 DurabilityMode::WalDurableGrouped,
3464 GroupCommitOptions::default(),
3465 )
3466 .expect("open commit coordinator"),
3467 );
3468
3469 const WRITERS: usize = 32;
3470 let barrier = Arc::new(Barrier::new(WRITERS));
3471 let mut handles = Vec::with_capacity(WRITERS);
3472 for tx in 0..WRITERS {
3473 let coord_c = Arc::clone(&coord);
3474 let barrier_c = Arc::clone(&barrier);
3475 handles.push(std::thread::spawn(move || {
3476 barrier_c.wait();
3480 let action = StoreWalAction::CreateCollection {
3481 name: format!("c{tx}"),
3482 };
3483 coord_c
3484 .append_actions(std::slice::from_ref(&action))
3485 .expect("append_actions");
3486 }));
3487 }
3488 for h in handles {
3489 h.join().expect("writer thread");
3490 }
3491
3492 let fsyncs = coord.fsync_count();
3493 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
3494 assert!(
3495 fsyncs < WRITERS as u64,
3496 "expected fsyncs ({fsyncs}) to be strictly less than \
3497 concurrent writers ({WRITERS}); coalescing failed"
3498 );
3499
3500 drop(coord);
3501 let _ = std::fs::remove_file(&path);
3502 }
3503
3504 #[test]
3510 fn zero_window_disables_coalescing_floor() {
3511 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3512 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
3513
3514 let path = temp_wal("zero_window");
3515 let coord = Arc::new(
3516 StoreCommitCoordinator::open(
3517 path.clone(),
3518 DurabilityMode::WalDurableGrouped,
3519 GroupCommitOptions::default(),
3520 )
3521 .expect("open commit coordinator"),
3522 );
3523
3524 const WRITERS: usize = 8;
3525 let barrier = Arc::new(Barrier::new(WRITERS));
3526 let mut handles = Vec::with_capacity(WRITERS);
3527 for tx in 0..WRITERS {
3528 let coord_c = Arc::clone(&coord);
3529 let barrier_c = Arc::clone(&barrier);
3530 handles.push(std::thread::spawn(move || {
3531 barrier_c.wait();
3532 let action = StoreWalAction::CreateCollection {
3533 name: format!("z{tx}"),
3534 };
3535 coord_c
3536 .append_actions(std::slice::from_ref(&action))
3537 .expect("append_actions");
3538 }));
3539 }
3540 for h in handles {
3541 h.join().expect("writer thread");
3542 }
3543
3544 let fsyncs = coord.fsync_count();
3550 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
3551
3552 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3553 drop(coord);
3554 let _ = std::fs::remove_file(&path);
3555 }
3556
3557 #[test]
3559 fn resolve_window_precedence() {
3560 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3561 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3563 let cfg = GroupCommitOptions::default();
3564 assert_eq!(
3565 StoreCommitCoordinator::resolve_window(&cfg),
3566 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
3567 );
3568
3569 let cfg_ms = GroupCommitOptions {
3571 window_ms: 5,
3572 ..GroupCommitOptions::default()
3573 };
3574 assert_eq!(
3575 StoreCommitCoordinator::resolve_window(&cfg_ms),
3576 Duration::from_millis(5)
3577 );
3578
3579 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
3581 assert_eq!(
3582 StoreCommitCoordinator::resolve_window(&cfg),
3583 Duration::from_micros(750)
3584 );
3585 assert_eq!(
3586 StoreCommitCoordinator::resolve_window(&cfg_ms),
3587 Duration::from_micros(750)
3588 );
3589
3590 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
3592 assert_eq!(
3593 StoreCommitCoordinator::resolve_window(&cfg),
3594 Duration::from_micros(0)
3595 );
3596
3597 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3598 }
3599}