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