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 pager
1174 .recover_unowned_pages(&plan.release)
1175 .map_err(|error| io::Error::other(error.to_string()))?;
1176 #[cfg(test)]
1177 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY").is_some() {
1178 std::process::exit(187);
1179 }
1180 pager
1181 .sync_recovered_allocator()
1182 .map_err(|error| io::Error::other(error.to_string()))?;
1183 #[cfg(test)]
1184 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC").is_some() {
1185 std::process::exit(188);
1186 }
1187 coordinator.append_durable_page_ownership(PageOwnershipRecord::Finish {
1188 epoch: pending.epoch,
1189 })?;
1190 #[cfg(test)]
1191 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH").is_some() {
1192 std::process::exit(189);
1193 }
1194 }
1195 coordinator.map_or(Ok(()), Self::release_completed_ownership_wal)
1196 }
1197
1198 pub(super) fn next_page_ownership_epoch(&self, store: &UnifiedStore) -> io::Result<u64> {
1199 self.ownership_last_epoch
1200 .load(Ordering::Acquire)
1201 .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1202 .checked_add(1)
1203 .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))
1204 }
1205
1206 pub(super) fn begin_page_ownership_epoch(
1208 &self,
1209 store: &UnifiedStore,
1210 epoch: u64,
1211 ) -> io::Result<()> {
1212 if store.ownership_active_epoch.load(Ordering::Acquire) != 0
1213 || epoch <= store.ownership_catalog_epoch.load(Ordering::Acquire)
1214 || epoch <= self.ownership_last_epoch.load(Ordering::Acquire)
1215 {
1216 return Err(io::Error::other("invalid page ownership epoch"));
1217 }
1218 let pager = store
1219 .pager
1220 .as_ref()
1221 .ok_or_else(|| io::Error::other("ownership epoch requires a pager"))?;
1222 let format_version = if !store.secondary_index_specs.read().is_empty() {
1223 reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1224 } else if store.config.resident_memory_bytes_max.is_some() {
1225 reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1226 } else {
1227 reddb_file::file_format::PAGE_CHECKPOINT_UNDO_FILE_VERSION
1228 };
1229 let format_version = format_version.max(
1232 pager
1233 .header()
1234 .map_err(|error| io::Error::other(error.to_string()))?
1235 .version,
1236 );
1237 pager
1238 .fence_file_format_version(format_version)
1239 .map_err(|error| io::Error::other(error.to_string()))?;
1240 let coordinator = Arc::clone(
1241 store
1242 .commit
1243 .as_ref()
1244 .ok_or_else(|| io::Error::other("ownership epoch requires a WAL coordinator"))?,
1245 );
1246 let recorder = Arc::new(StoreOwnershipRecorder::begin(coordinator, epoch)?);
1247 pager.install_ownership_recorder(recorder)?;
1248 store.ownership_active_epoch.store(epoch, Ordering::Release);
1249 Ok(())
1250 }
1251
1252 pub(super) fn finish_page_ownership_epoch(
1254 &self,
1255 store: &UnifiedStore,
1256 epoch: u64,
1257 ) -> io::Result<()> {
1258 self.finish_page_ownership_epoch_inner(store, epoch, true)
1259 }
1260
1261 pub(super) fn finish_count_batch_ownership_epoch(
1265 &self,
1266 store: &UnifiedStore,
1267 epoch: u64,
1268 ) -> io::Result<()> {
1269 self.finish_page_ownership_epoch_inner(store, epoch, false)
1270 }
1271
1272 fn finish_page_ownership_epoch_inner(
1273 &self,
1274 store: &UnifiedStore,
1275 epoch: u64,
1276 validate_for_truncation: bool,
1277 ) -> io::Result<()> {
1278 if epoch == 0 || store.ownership_active_epoch.load(Ordering::Acquire) != epoch {
1279 return Err(io::Error::other("page ownership epoch is not active"));
1280 }
1281 if store.ownership_catalog_epoch.load(Ordering::Acquire) != epoch {
1282 return Err(io::Error::other("page ownership epoch is not published"));
1283 }
1284 let pager = store
1285 .pager
1286 .as_ref()
1287 .ok_or_else(|| io::Error::other("ownership epoch requires a pager"))?;
1288 pager
1289 .sync_recovered_allocator()
1290 .map_err(|error| io::Error::other(error.to_string()))?;
1291 self.append_durable_page_ownership(
1292 reddb_file::page_ownership_journal::PageOwnershipRecord::Finish { epoch },
1293 )?;
1294 pager.clear_ownership_recorder()?;
1295 store.ownership_active_epoch.store(0, Ordering::Release);
1296 if validate_for_truncation {
1297 self.release_completed_ownership_wal()
1298 } else {
1299 Ok(())
1300 }
1301 }
1302
1303 pub(crate) fn replay_into(
1304 &self,
1305 store: &UnifiedStore,
1306 boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1307 ) -> io::Result<()> {
1308 Self::replay_from_wal(store, &self.wal_path, Some(self), boot_wal)
1309 }
1310
1311 pub(crate) fn replay_read_only(store: &UnifiedStore, wal_path: &Path) -> io::Result<()> {
1312 Self::replay_from_wal(store, wal_path, None, None)
1313 }
1314
1315 fn replay_from_wal(
1316 store: &UnifiedStore,
1317 wal_path: &Path,
1318 coordinator: Option<&Self>,
1319 boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1320 ) -> io::Result<()> {
1321 if !wal_path.exists() {
1322 return Ok(());
1323 }
1324
1325 let reader = match WalReader::open(wal_path) {
1326 Ok(reader) => reader,
1327 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
1328 Err(err) => return Err(err),
1329 };
1330
1331 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
1332 let mut has_page_writes = false;
1333 let mut ownership_journal = Vec::new();
1334 let mut latest_ownership_epoch = 0;
1335 let mut incomplete_wal_tail = None;
1336 let mut applied = false;
1337
1338 let mut records = reader.iter();
1339 while let Some(record) = records.next() {
1340 let (lsn, record) = match record {
1341 Ok(record) => record,
1342 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
1343 incomplete_wal_tail = Some(records.next_record_lsn());
1344 break;
1345 }
1346 Err(err) => return Err(err),
1347 };
1348 match record {
1349 WalRecord::PageOwnership { record } => {
1350 latest_ownership_epoch = latest_ownership_epoch.max(record.epoch());
1351 if let Some(coordinator) = coordinator {
1352 coordinator
1353 .ownership_last_epoch
1354 .fetch_max(record.epoch(), Ordering::AcqRel);
1355 }
1356 ownership_journal
1357 .extend_from_slice(&reddb_file::page_ownership_journal::encode(record)?);
1358 }
1359 WalRecord::TxCommitBatch { actions, .. } => {
1360 for (index, payload) in actions.into_iter().enumerate() {
1361 if boot_wal.is_some_and(|proof| proof.skips_action(lsn, index)) {
1362 continue;
1363 }
1364 let action = StoreWalAction::decode(&payload)?;
1365 applied = true;
1366 store.apply_replayed_action(&action, false).map_err(|err| {
1367 io::Error::other(format!("failed to replay store wal action: {err}"))
1368 })?;
1369 store.remember_checkpoint_rows(vec![action], Default::default());
1373 }
1374 }
1375 WalRecord::Begin { tx_id } => {
1376 tx_states.insert(tx_id, false);
1377 }
1378 WalRecord::Commit { tx_id } => {
1379 tx_states.insert(tx_id, true);
1380 }
1381 WalRecord::Rollback { tx_id } => {
1382 tx_states.remove(&tx_id);
1383 }
1384 WalRecord::PageWrite { .. } => has_page_writes = true,
1385 WalRecord::Checkpoint { .. } => {}
1386 WalRecord::VectorInsert {
1387 collection,
1388 entity_id,
1389 vector,
1390 } => {
1391 if let Some(coordinator) = coordinator {
1392 coordinator.runtime_wal_state.store(2, Ordering::Release);
1393 }
1394 let mut map = store.replayed_turbo_inserts.lock();
1400 map.entry(
1401 reddb_engine::storage::system_collection_names::canonical_owned(collection),
1402 )
1403 .or_default()
1404 .push((entity_id, vector));
1405 }
1406 WalRecord::ProbabilisticDelta {
1407 kind,
1408 operation,
1409 name,
1410 operands,
1411 } => {
1412 if let Some(coordinator) = coordinator {
1413 coordinator.runtime_wal_state.store(2, Ordering::Release);
1414 }
1415 let mut deltas = store.replayed_probabilistic_deltas.lock();
1416 deltas.push((kind, operation, name, operands));
1417 }
1418 WalRecord::FullPageImage { .. } => {
1419 }
1421 }
1422 }
1423
1424 if incomplete_wal_tail.is_some() && !ownership_journal.is_empty() {
1425 return Err(io::Error::new(
1426 io::ErrorKind::InvalidData,
1427 "incomplete WAL prevents ownership recovery",
1428 ));
1429 }
1430 if let (Some(coordinator), Some(valid_lsn)) = (coordinator, incomplete_wal_tail) {
1431 coordinator.truncate_incomplete_tail(valid_lsn)?;
1432 }
1433 Self::recover_page_ownership(coordinator, store, &ownership_journal)?;
1434
1435 if has_page_writes {
1436 for item in WalReader::open(wal_path)?.iter() {
1440 let record = match item {
1441 Ok((_, record)) => record,
1442 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
1443 Err(err) => return Err(err),
1444 };
1445 let WalRecord::PageWrite { tx_id, data, .. } = record else {
1446 continue;
1447 };
1448 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
1449 continue;
1450 }
1451 let action = StoreWalAction::decode(&data)?;
1452 applied = true;
1453 store.apply_replayed_action(&action, false).map_err(|err| {
1454 io::Error::other(format!("failed to replay store wal action: {err}"))
1455 })?;
1456 store.remember_checkpoint_rows(vec![action], Default::default());
1457 }
1458 }
1459
1460 let Some(coordinator) = coordinator else {
1461 return Ok(());
1462 };
1463 if applied && store.config.resident_memory_bytes_max.is_some() {
1464 return Ok(());
1468 }
1469 if applied {
1470 #[cfg(test)]
1473 if let Ok(fault) = std::env::var("REDDB_TEST_PAGE_REPLAY_IO") {
1474 use crate::storage::engine::pager;
1475 match fault.as_str() {
1476 "empty" => pager::test_fail_next_pager_write(None, 0),
1477 "partial" => pager::test_fail_next_pager_write(None, 17),
1478 "sync" => pager::test_fail_pager_sync_after(0),
1479 _ => panic!("unknown replay publication I/O fault {fault}"),
1480 }
1481 }
1482 let epoch = latest_ownership_epoch
1483 .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1484 .checked_add(1)
1485 .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))?;
1486 coordinator.begin_page_ownership_epoch(store, epoch)?;
1487 store.mark_paged_registry_dirty();
1490 store
1491 .reconcile_replayed_btrees()
1492 .map_err(|err| io::Error::other(err.to_string()))?;
1493 coordinator.finish_page_ownership_epoch(store, epoch)?;
1494 }
1495 Ok(())
1496 }
1497
1498 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
1499 self.wait_until_durable_batch(target_lsn, wal_bytes, false)
1500 }
1501
1502 fn check_write_error(&self) -> io::Result<()> {
1503 if let Some(error) = &self.state.0.lock().last_error {
1504 return Err(io::Error::other(error.clone()));
1505 }
1506 Ok(())
1507 }
1508
1509 fn wait_until_durable_batch(
1510 &self,
1511 target_lsn: u64,
1512 wal_bytes: u64,
1513 ready: bool,
1514 ) -> io::Result<()> {
1515 match self.mode {
1516 DurabilityMode::Strict => self.force_sync(),
1517 DurabilityMode::Async => {
1522 let (state_lock, cond) = &*self.state;
1523 let mut state = state_lock.lock();
1524 if let Some(error) = &state.last_error {
1525 return Err(io::Error::other(error.clone()));
1526 }
1527 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1528 state.pending_statements = state.pending_statements.saturating_add(1);
1529 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1530 state.first_pending_at.get_or_insert_with(Instant::now);
1531 cond.notify_all();
1532 Ok(())
1533 }
1534 DurabilityMode::WalDurableGrouped => {
1535 let (state_lock, cond) = &*self.state;
1536 let mut state = state_lock.lock();
1537 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1538 state.pending_statements = state.pending_statements.saturating_add(1);
1539 if ready {
1540 state.pending_statements = state
1541 .pending_statements
1542 .max(self.config.max_statements.max(1));
1543 }
1544 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1545 state.first_pending_at.get_or_insert_with(Instant::now);
1546 cond.notify_all();
1547
1548 loop {
1549 if let Some(err) = state.last_error.clone() {
1550 return Err(io::Error::other(err));
1551 }
1552 if state.durable_lsn >= target_lsn {
1553 return Ok(());
1554 }
1555 cond.wait(&mut state);
1558 }
1559 }
1560 }
1561 }
1562
1563 fn run_group_commit_loop(
1564 wal: Arc<WalMutex>,
1565 queue: Arc<WalAppendQueue>,
1566 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
1567 fsync_count: Arc<AtomicU64>,
1568 window: Duration,
1569 max_statements: usize,
1570 max_wal_bytes: u64,
1571 ) {
1572 let (state_lock, cond) = &*state;
1573 loop {
1574 let target_lsn = {
1575 let mut guard = state_lock.lock();
1576
1577 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
1578 cond.wait(&mut guard);
1579 }
1580
1581 if guard.shutdown {
1582 return;
1583 }
1584
1585 let immediate = window.is_zero()
1586 || guard.pending_statements >= max_statements
1587 || guard.pending_wal_bytes >= max_wal_bytes;
1588
1589 if !immediate {
1590 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
1591 let now = Instant::now();
1592 if now < deadline {
1593 let timeout = deadline.saturating_duration_since(now);
1594 let _ = cond.wait_for(&mut guard, timeout);
1595 if guard.shutdown {
1596 return;
1597 }
1598 if guard.pending_target_lsn <= guard.durable_lsn {
1599 continue;
1600 }
1601 let should_wait_again = guard.pending_statements < max_statements
1602 && guard.pending_wal_bytes < max_wal_bytes
1603 && guard
1604 .first_pending_at
1605 .map(|first| first.elapsed() < window)
1606 .unwrap_or(false);
1607 if should_wait_again {
1608 continue;
1609 }
1610 }
1611 }
1612
1613 guard.pending_target_lsn
1614 };
1615
1616 let _epoch = queue.epoch.read_recursive();
1621
1622 let batches = queue.drain_sorted();
1628
1629 let sync_result = {
1630 let mut wal = wal.lock();
1631 let mut write_err: Option<io::Error> = None;
1632 for (_lsn, bytes) in batches {
1633 if let Err(e) = wal.append_bytes(&bytes) {
1634 write_err = Some(e);
1635 break;
1636 }
1637 }
1638 match write_err {
1639 Some(e) => Err(e),
1640 None => wal.sync().map(|_| {
1641 fsync_count.fetch_add(1, Ordering::Relaxed);
1646 wal.durable_lsn()
1647 }),
1648 }
1649 };
1650
1651 let more_pending = queue.has_pending();
1656
1657 let mut guard = state_lock.lock();
1658 match sync_result {
1659 Ok(durable_lsn) => {
1660 guard.durable_lsn = durable_lsn;
1661 if !more_pending {
1662 guard.pending_statements = 0;
1663 guard.pending_wal_bytes = 0;
1664 guard.first_pending_at = None;
1665 }
1666 guard.last_error = None;
1667 let _ = target_lsn;
1668 }
1669 Err(err) => {
1670 guard.last_error = Some(format!("WAL failed; reopen required: {err}"));
1671 cond.notify_all();
1674 return;
1675 }
1676 }
1677 cond.notify_all();
1678 }
1679 }
1680}
1681
1682impl Drop for StoreCommitCoordinator {
1683 fn drop(&mut self) {
1684 let (state_lock, cond) = &*self.state;
1685 let mut state = state_lock.lock();
1687 state.shutdown = true;
1688 cond.notify_all();
1689 }
1690}
1691
1692pub(super) struct PendingResultCacheAppend {
1693 pending: crate::storage::embedded::PendingEmbeddedAppend,
1694 epoch_guard: Option<super::result_cache_epoch::ResultCacheEpochGuard>,
1695}
1696
1697impl UnifiedStore {
1698 #[cfg(test)]
1699 pub(crate) fn test_fail_wal_flush_after(&self, bytes: usize) {
1700 self.commit
1701 .as_ref()
1702 .expect("fixture requires an external WAL")
1703 .wal
1704 .lock()
1705 .test_fail_flush_after(bytes);
1706 }
1707
1708 pub(crate) fn sync_control_metadata(&self) -> Result<(), StoreError> {
1709 if let Some(commit) = &self.commit {
1710 commit.sync_pending().map_err(StoreError::Io)?;
1711 }
1712 Ok(())
1714 }
1715
1716 pub(crate) fn deferred_store_wal_capture_active() -> bool {
1717 deferred_store_wal_capture_active()
1718 }
1719
1720 pub(crate) fn begin_deferred_store_wal_capture() {
1721 begin_deferred_store_wal_capture();
1722 }
1723
1724 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
1725 take_deferred_store_wal_capture()
1726 }
1727
1728 pub(crate) fn append_deferred_store_wal_actions(
1729 &self,
1730 actions: DeferredStoreWalActions,
1731 ) -> Result<(), StoreError> {
1732 if actions.actions.is_empty() {
1733 return Ok(());
1734 }
1735 self.ensure_pager_writeback_available()?;
1736 #[cfg(test)]
1737 if self
1738 .test_require_deferred_publication_fence
1739 .swap(false, Ordering::AcqRel)
1740 && !self.vault_publication_lock.is_owned_by_current_thread()
1741 {
1742 return Err(StoreError::Io(io::Error::other(
1743 "deferred WAL missing checkpoint publication fence",
1744 )));
1745 }
1746 #[cfg(test)]
1747 if self
1748 .test_fail_deferred_wal_append
1749 .swap(false, Ordering::AcqRel)
1750 {
1751 return Err(StoreError::Io(io::Error::other(
1752 "injected deferred WAL append failure",
1753 )));
1754 }
1755 if self.config.embedded_wal_path.is_some() {
1756 return self.append_embedded_store_wal_actions(&actions.actions);
1757 }
1758 let _publication = self.vault_publication_lock.lock();
1759 let mut actions = actions.actions;
1760 let epoch_guard = self.append_result_cache_epoch_action(&mut actions)?;
1761 if let Some(commit) = &self.commit {
1764 let references = commit
1765 .append_checkpoint_actions(&actions)
1766 .map_err(StoreError::Io)?;
1767 self.remember_checkpoint_rows(actions, references);
1768 } else {
1769 self.flush_paged_state()?;
1770 }
1771 if let Some(guard) = epoch_guard {
1772 guard.commit();
1773 }
1774 Ok(())
1775 }
1776
1777 pub(crate) fn append_vector_insert_record(
1784 &self,
1785 collection: &str,
1786 entity_id: u64,
1787 vector: &[f32],
1788 ) -> std::io::Result<()> {
1789 let Some(commit) = &self.commit else {
1790 return Ok(());
1791 };
1792 let record = WalRecord::VectorInsert {
1793 collection: collection.to_string(),
1794 entity_id,
1795 vector: vector.to_vec(),
1796 };
1797 commit.append_single_record(record)
1798 }
1799
1800 pub(crate) fn append_probabilistic_delta_record(
1801 &self,
1802 kind: u8,
1803 operation: u8,
1804 name: &str,
1805 operands: Vec<Vec<u8>>,
1806 ) -> std::io::Result<()> {
1807 let Some(commit) = &self.commit else {
1808 return Ok(());
1809 };
1810 let record = WalRecord::ProbabilisticDelta {
1811 kind,
1812 operation,
1813 name: name.to_string(),
1814 operands,
1815 };
1816 commit.append_single_record(record)
1817 }
1818
1819 pub(crate) fn finish_paged_write(
1820 &self,
1821 actions: impl IntoIterator<Item = StoreWalAction>,
1822 ) -> Result<(), StoreError> {
1823 self.finish_paged_write_for_projection(
1824 actions,
1825 self.config.resident_memory_bytes_max.is_some(),
1826 )
1827 }
1828
1829 pub(crate) fn finish_paged_write_for_projection(
1830 &self,
1831 actions: impl IntoIterator<Item = StoreWalAction>,
1832 track: bool,
1833 ) -> Result<(), StoreError> {
1834 if self.is_read_only() {
1835 return Ok(());
1836 }
1837 self.ensure_pager_writeback_available()?;
1838 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
1839 if deferred_store_wal_capture_active() {
1840 let captured = capture_deferred_store_wal_actions(actions);
1841 debug_assert!(captured);
1842 return Ok(());
1843 }
1844 if self.config.embedded_wal_path.is_some() {
1845 return self.append_embedded_store_wal_actions(&actions);
1846 }
1847 let _publication = self.vault_publication_lock.lock();
1848 let mut actions = actions;
1849 let epoch_guard = self.append_result_cache_epoch_action(&mut actions)?;
1850 let mut references = super::checkpoint_wal::CheckpointWalBatch::default();
1851 let result = match self.config.durability_mode {
1852 DurabilityMode::Strict => {
1853 if let Some(commit) = &self.commit {
1854 if track {
1855 references = commit
1856 .append_checkpoint_actions(&actions)
1857 .map_err(StoreError::Io)?;
1858 } else {
1859 commit.append_actions(&actions).map_err(StoreError::Io)?;
1860 }
1861 }
1862 self.flush_paged_state()
1863 }
1864 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
1865 if let Some(commit) = &self.commit {
1866 if track {
1867 references = commit
1868 .append_checkpoint_actions(&actions)
1869 .map_err(StoreError::Io)?;
1870 } else {
1871 commit.append_actions(&actions).map_err(StoreError::Io)?;
1872 }
1873 Ok(())
1874 } else {
1875 self.flush_paged_state()
1876 }
1877 }
1878 };
1879 result?;
1880 if track {
1881 self.remember_checkpoint_rows(actions, references);
1882 }
1883 if let Some(guard) = epoch_guard {
1884 guard.commit();
1885 }
1886 Ok(())
1887 }
1888
1889 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
1890 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
1891 self.apply_replayed_action(&action, true)
1892 }
1893
1894 fn append_embedded_store_wal_actions(
1895 &self,
1896 actions: &[StoreWalAction],
1897 ) -> Result<(), StoreError> {
1898 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1899 return Ok(());
1900 };
1901 if actions.is_empty() {
1902 return Ok(());
1903 }
1904 let _publication = self.vault_publication_lock.lock();
1905 let mut actions = actions.to_vec();
1906 let epoch_guard = self.append_result_cache_epoch_action(&mut actions)?;
1907 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1908 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1909 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1910 let result = match self.append_embedded_payloads(path, &payloads) {
1911 Ok(_) => Ok(()),
1912 Err(crate::api::RedDBError::InvalidOperation(msg))
1913 if msg.contains("embedded wal region full") =>
1914 {
1915 self.recover_full_embedded_wal(path, &payloads, encoded_len)
1916 }
1917 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1918 };
1919 if result.is_ok() {
1920 if let Some(guard) = epoch_guard {
1921 guard.commit();
1922 }
1923 }
1924 result
1925 }
1926
1927 fn recover_full_embedded_wal(
1928 &self,
1929 path: &Path,
1930 payloads: &[Vec<u8>],
1931 encoded_len: u64,
1932 ) -> Result<(), StoreError> {
1933 let _vault_guard = self.vault_publication_lock.lock();
1934 match self.append_embedded_payloads(path, payloads) {
1938 Ok(_) => return Ok(()),
1939 Err(crate::api::RedDBError::InvalidOperation(message))
1940 if message.contains("embedded wal region full") => {}
1941 Err(error) => return Err(StoreError::Io(std::io::Error::other(error.to_string()))),
1942 }
1943 struct CheckpointFence<'a>(Option<&'a crate::storage::embedded::EmbeddedCommitSession>);
1947 impl Drop for CheckpointFence<'_> {
1948 fn drop(&mut self) {
1949 if let Some(session) = self.0 {
1950 session.fence_interrupted_write();
1951 }
1952 }
1953 }
1954 let mut fence = CheckpointFence(Some(
1955 self.embedded_commit_session
1956 .get()
1957 .expect("WAL-full recovery follows an append attempt"),
1958 ));
1959 let started = Instant::now();
1960 let snapshot = self.to_binary_dump_bytes();
1961 let capacity = encoded_len.max((snapshot.len() as u64).min(EMBEDDED_WAL_HEADROOM_LIMIT));
1962 let checkpoint = crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
1963 path, &snapshot, capacity,
1964 )
1965 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1966 fence.0 = None;
1967 tracing::debug!(
1968 target: "reddb::embedded_checkpoint",
1969 snapshot_bytes = snapshot.len(),
1970 wal_region_bytes = checkpoint.manifest.wal_region_bytes,
1971 elapsed_us = started.elapsed().as_micros() as u64,
1972 "embedded WAL overflow checkpoint"
1973 );
1974 self.append_embedded_payloads(path, payloads)
1975 .map(|_| ())
1976 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1977 }
1978
1979 fn append_embedded_payloads(
1980 &self,
1981 path: &Path,
1982 payloads: &[Vec<u8>],
1983 ) -> crate::RedDBResult<crate::storage::EmbeddedRdbOpen> {
1984 let session = self
1985 .embedded_commit_session
1986 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1987 let options = self.config.group_commit;
1988 let grouped = self.config.durability_mode == DurabilityMode::WalDurableGrouped;
1989 let window = if grouped {
1990 StoreCommitCoordinator::resolve_window(&options)
1991 } else {
1992 Duration::ZERO
1993 };
1994 session.append(
1995 payloads,
1996 reddb_file::embedded::EmbeddedGroupCommitOptions {
1997 window,
1998 max_statements: if grouped { options.max_statements } else { 1 },
1999 max_wal_bytes: options.max_wal_bytes,
2000 },
2001 )
2002 }
2003
2004 pub(super) fn enqueue_grouped_actions(
2005 &self,
2006 actions: &[StoreWalAction],
2007 ) -> Result<Option<PendingResultCacheAppend>, StoreError> {
2008 if self.config.durability_mode != DurabilityMode::WalDurableGrouped
2009 || deferred_store_wal_capture_active()
2010 {
2011 return Ok(None);
2012 }
2013 let Some(path) = self.config.embedded_wal_path.as_deref() else {
2014 return Ok(None);
2015 };
2016 let session = self
2017 .embedded_commit_session
2018 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
2019 let _publication = self.vault_publication_lock.lock();
2020 let mut actions = actions.to_vec();
2021 let epoch_guard = self.append_result_cache_epoch_action(&mut actions)?;
2022 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
2023 let options = self.config.group_commit;
2024 session
2025 .enqueue(
2026 &payloads,
2027 reddb_file::embedded::EmbeddedGroupCommitOptions {
2028 window: StoreCommitCoordinator::resolve_window(&options),
2029 max_statements: options.max_statements,
2030 max_wal_bytes: options.max_wal_bytes,
2031 },
2032 )
2033 .map(|pending| {
2034 Some(PendingResultCacheAppend {
2035 pending,
2036 epoch_guard,
2037 })
2038 })
2039 .map_err(|error| StoreError::Io(io::Error::other(error.to_string())))
2040 }
2041
2042 pub(super) fn complete_grouped_actions(
2043 &self,
2044 pending: &mut PendingResultCacheAppend,
2045 actions: &[StoreWalAction],
2046 ) -> Result<(), StoreError> {
2047 let result = match pending.pending.wait() {
2048 Ok(_) => Ok(()),
2049 Err(crate::RedDBError::InvalidOperation(message))
2050 if message.contains("embedded wal region full") =>
2051 {
2052 self.append_embedded_store_wal_actions(actions)
2053 }
2054 Err(error) => Err(StoreError::Io(io::Error::other(error.to_string()))),
2055 };
2056 if result.is_ok() {
2057 if let Some(guard) = pending.epoch_guard.take() {
2058 guard.commit();
2059 }
2060 }
2061 result
2062 }
2063
2064 pub(crate) fn apply_replayed_action(
2065 &self,
2066 action: &StoreWalAction,
2067 update_btrees: bool,
2068 ) -> Result<(), StoreError> {
2069 reddb_engine::storage::store_wal::apply_replayed_action(
2070 &NativeReplayTarget {
2071 store: self,
2072 update_btrees,
2073 },
2074 action,
2075 )
2076 }
2077
2078 pub fn refresh_collection(
2095 &self,
2096 name: &str,
2097 entities: Vec<UnifiedEntity>,
2098 ) -> Result<Vec<Vec<u8>>, StoreError> {
2099 let name = Self::canonical_collection(name);
2100 let fv = self.format_version();
2101
2102 let new_manager = Arc::new(SegmentManager::with_config(
2105 name,
2106 self.config.manager_config.clone(),
2107 ));
2108
2109 let mut prepared = entities;
2110 for entity in &mut prepared {
2111 entity.canonicalize_system_collection_names();
2112 if entity.id.raw() == 0 {
2113 entity.id = self.next_entity_id();
2114 } else {
2115 self.register_entity_id(entity.id);
2116 }
2117 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
2118 if *row_id == 0 {
2119 *row_id = new_manager.next_row_id();
2120 } else {
2121 new_manager.register_row_id(*row_id);
2122 }
2123 }
2124 entity.ensure_table_logical_id();
2125 }
2126
2127 let serialized: Vec<Vec<u8>> = prepared
2128 .iter()
2129 .map(|e| Self::serialize_entity_record(e, None, fv))
2130 .collect();
2131
2132 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
2133 StoreError::Io(std::io::Error::other(format!(
2134 "refresh_collection: bulk_insert into new manager failed: {e}"
2135 )))
2136 })?;
2137
2138 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
2139
2140 self.finish_paged_write([StoreWalAction::RefreshCollection {
2141 collection: name.to_string(),
2142 records: serialized.clone(),
2143 }])?;
2144
2145 Ok(serialized)
2146 }
2147
2148 fn swap_collection_state(
2154 &self,
2155 name: &str,
2156 new_manager: Arc<SegmentManager>,
2157 prepared: &[UnifiedEntity],
2158 serialized: &[Vec<u8>],
2159 update_btrees: bool,
2160 ) {
2161 self.resident
2162 .swap_collection_state(name, new_manager, prepared);
2163
2164 if let Some(pager) = self
2165 .pager
2166 .as_ref()
2167 .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
2168 {
2169 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
2170 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
2171 .iter()
2172 .zip(serialized.iter())
2173 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
2174 .collect();
2175 sorted.sort_by(|a, b| a.0.cmp(&b.0));
2176 if !sorted.is_empty() {
2177 let _ = new_btree.bulk_insert_sorted(&sorted);
2178 }
2179 self.btree_indices
2180 .write()
2181 .insert(name.to_string(), new_btree);
2182 self.mark_paged_registry_dirty();
2183 }
2184 }
2185
2186 fn apply_replayed_refresh_collection(
2187 &self,
2188 collection: &str,
2189 records: &[Vec<u8>],
2190 update_btrees: bool,
2191 ) -> Result<(), StoreError> {
2192 let collection = Self::canonical_collection(collection);
2193 let (new_manager, prepared) = self.resident.prepare_replayed_refresh_collection(
2194 collection,
2195 records,
2196 self.format_version(),
2197 &self.config.manager_config,
2198 )?;
2199 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
2200 Ok(())
2201 }
2202
2203 pub fn refresh_collection_from_records(
2217 &self,
2218 name: &str,
2219 records: Vec<Vec<u8>>,
2220 ) -> Result<(), StoreError> {
2221 let name = Self::canonical_collection(name);
2222 self.apply_replayed_refresh_collection(name, &records, true)?;
2223 self.finish_paged_write([StoreWalAction::RefreshCollection {
2224 collection: name.to_string(),
2225 records,
2226 }])?;
2227 Ok(())
2228 }
2229
2230 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
2231 let name = Self::canonical_collection(name);
2232 self.resident
2233 .create_collection_in_memory(name, &self.config.manager_config)
2234 }
2235
2236 pub(super) fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
2237 let name = Self::canonical_collection(name);
2238 self.resident.drop_collection_in_memory(name, || {
2239 if let Some(tree) = self.btree_indices.write().remove(name) {
2240 self.retire_btree(tree);
2241 }
2242 })
2243 }
2244
2245 fn apply_replayed_upsert(
2246 &self,
2247 collection: &str,
2248 record: &[u8],
2249 update_btrees: bool,
2250 ) -> Result<(), StoreError> {
2251 let collection = Self::canonical_collection(collection);
2252 let id = self.resident.apply_replayed_upsert(
2253 collection,
2254 record,
2255 self.format_version(),
2256 &self.config.manager_config,
2257 )?;
2258
2259 if let Some(pager) = self
2260 .pager
2261 .as_ref()
2262 .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
2263 {
2264 let mut btree_indices = self.btree_indices.write();
2265 let btree = btree_indices
2266 .entry(collection.to_string())
2267 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
2268 let root_before = btree.root_page_id();
2269 let key = id.raw().to_be_bytes();
2270 match btree.insert(&key, record) {
2271 Ok(_) => {}
2272 Err(BTreeError::DuplicateKey) => {
2273 let _ = btree.delete(&key);
2274 let _ = btree.insert(&key, record);
2275 }
2276 Err(err) => {
2277 return Err(StoreError::Io(io::Error::other(format!(
2278 "replay upsert btree error: {err}"
2279 ))));
2280 }
2281 }
2282 if root_before != btree.root_page_id() {
2283 self.mark_paged_registry_dirty();
2284 }
2285 }
2286
2287 Ok(())
2288 }
2289
2290 fn apply_replayed_delete(
2291 &self,
2292 collection: &str,
2293 id: EntityId,
2294 update_btrees: bool,
2295 ) -> Result<(), StoreError> {
2296 let collection = Self::canonical_collection(collection);
2297 self.resident.apply_replayed_delete(collection, id, || {
2298 if self.pager.is_some()
2299 && update_btrees
2300 && self.config.resident_memory_bytes_max.is_none()
2301 {
2302 let btree_indices = self.btree_indices.read();
2303 if let Some(btree) = btree_indices.get(collection) {
2304 let root_before = btree.root_page_id();
2305 let key = id.raw().to_be_bytes();
2306 let _ = btree.delete(&key);
2307 if root_before != btree.root_page_id() {
2308 self.mark_paged_registry_dirty();
2309 }
2310 }
2311 }
2312 })
2313 }
2314}
2315
2316#[cfg(all(test, target_os = "linux"))]
2317#[path = "storage_apocalypse.rs"]
2318mod storage_apocalypse;
2319
2320#[cfg(test)]
2321mod tests {
2322 use super::*;
2323 use crate::api::{DurabilityMode, GroupCommitOptions};
2324 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
2325 use std::time::SystemTime;
2326
2327 #[test]
2328 fn runtime_wal_retention_serializes_concurrent_appends() {
2329 let directory = tempfile::tempdir().expect("temporary WAL directory");
2330 let path = directory.path().join("data.wal");
2331 let coordinator = Arc::new(
2332 StoreCommitCoordinator::open(
2333 &path,
2334 DurabilityMode::WalDurableGrouped,
2335 GroupCommitOptions::default(),
2336 )
2337 .expect("coordinator"),
2338 );
2339 let barrier = Arc::new(Barrier::new(5));
2340 let mut workers = Vec::new();
2341 for worker in 0..4u8 {
2342 let coordinator = Arc::clone(&coordinator);
2343 let barrier = Arc::clone(&barrier);
2344 workers.push(std::thread::spawn(move || {
2345 barrier.wait();
2346 for index in 0..20u8 {
2347 coordinator
2348 .append_single_record(WalRecord::ProbabilisticDelta {
2349 kind: 1,
2350 operation: 1,
2351 name: "visitors".into(),
2352 operands: vec![vec![worker, index]],
2353 })
2354 .expect("append delta");
2355 }
2356 }));
2357 }
2358 barrier.wait();
2359 for _ in 0..5 {
2360 coordinator.truncate().expect("retain while appending");
2361 }
2362 for worker in workers {
2363 worker.join().expect("worker");
2364 }
2365 coordinator.truncate().expect("final retention");
2366 let entries: std::collections::HashSet<_> = WalReader::open(&path)
2367 .expect("reader")
2368 .iter()
2369 .map(|entry| match entry.expect("record").1 {
2370 WalRecord::ProbabilisticDelta { operands, .. } => operands[0].clone(),
2371 _ => panic!("unexpected retained record"),
2372 })
2373 .collect();
2374 assert_eq!(entries.len(), 80);
2375 }
2376
2377 #[test]
2378 fn runtime_wal_retention_preserves_exact_frames_and_append_order() {
2379 use crate::storage::wal::record::WalRecordAuthority;
2380 for mode in [
2381 DurabilityMode::Strict,
2382 DurabilityMode::WalDurableGrouped,
2383 DurabilityMode::Async,
2384 ] {
2385 let directory = tempfile::tempdir().expect("temporary WAL directory");
2386 let path = directory.path().join("data.wal");
2387 let record = WalRecord::ProbabilisticDelta {
2388 kind: 1,
2389 operation: 1,
2390 name: "visitors".into(),
2391 operands: vec![b"alice".to_vec()],
2392 };
2393 let mut frame = Vec::new();
2394 record.encode_with_authority_into(
2395 &mut frame,
2396 WalRecordAuthority {
2397 term: 42,
2398 ownership_epoch: Some(9),
2399 },
2400 );
2401 {
2402 let mut writer = WalWriter::open(&path).expect("WAL writer");
2403 writer
2404 .append(&WalRecord::Begin { tx_id: 1 })
2405 .expect("logical begin");
2406 writer.append_bytes(&frame).expect("runtime record");
2407 writer
2408 .append(&WalRecord::Commit { tx_id: 1 })
2409 .expect("logical commit");
2410 writer.sync().expect("sync");
2411 }
2412 let coordinator =
2413 StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2414 .expect("coordinator");
2415 coordinator.truncate().expect("retain runtime records");
2416 let mut expected = reddb_file::encode_wal_file_header().to_vec();
2417 expected.extend_from_slice(&frame);
2418 assert_eq!(std::fs::read(&path).expect("retained WAL"), expected);
2419 let later = WalRecord::ProbabilisticDelta {
2420 kind: 1,
2421 operation: 1,
2422 name: "visitors".into(),
2423 operands: vec![b"bob".to_vec()],
2424 };
2425 coordinator
2426 .append_single_record(later.clone())
2427 .expect("append after rewrite");
2428 coordinator.truncate().expect("retain queued record");
2429 let records: Vec<_> = WalReader::open(&path)
2430 .expect("reader")
2431 .iter()
2432 .map(|entry| entry.expect("record").1)
2433 .collect();
2434 assert_eq!(records, vec![record, later]);
2435 coordinator
2436 .truncate_after_runtime_checkpoint()
2437 .expect("runtime snapshot permits truncation");
2438 assert_eq!(
2439 std::fs::read(&path).expect("checkpointed WAL"),
2440 reddb_file::encode_wal_file_header()
2441 );
2442 }
2443 }
2444
2445 #[test]
2446 fn runtime_wal_rewrite_survives_interruption() {
2447 const TEST: &str =
2448 "storage::unified::store::commit::tests::runtime_wal_rewrite_survives_interruption";
2449 const CHILD: &str = "REDDB_TEST_RUNTIME_WAL_REWRITE_PATH";
2450 if let Some(path) = std::env::var_os(CHILD) {
2451 let coordinator = StoreCommitCoordinator::open(
2452 path,
2453 DurabilityMode::Strict,
2454 GroupCommitOptions::default(),
2455 )
2456 .expect("child coordinator");
2457 coordinator.truncate().expect("fault injection");
2458 panic!("interruption point was not reached");
2459 }
2460 for (hook, code, replaced) in [
2461 ("REDDB_TEST_EXIT_BEFORE_RUNTIME_WAL_RENAME", 211, false),
2462 ("REDDB_TEST_EXIT_AFTER_RUNTIME_WAL_RENAME", 212, true),
2463 ] {
2464 let directory = tempfile::tempdir().expect("temporary WAL directory");
2465 let path = directory.path().join("data.wal");
2466 let record = WalRecord::ProbabilisticDelta {
2467 kind: 1,
2468 operation: 1,
2469 name: "visitors".into(),
2470 operands: vec![b"alice".to_vec()],
2471 };
2472 {
2473 let mut writer = WalWriter::open(&path).expect("WAL writer");
2474 writer
2475 .append(&WalRecord::Begin { tx_id: 1 })
2476 .expect("begin");
2477 writer.append(&record).expect("runtime record");
2478 writer
2479 .append(&WalRecord::Commit { tx_id: 1 })
2480 .expect("commit");
2481 writer.sync().expect("sync");
2482 }
2483 let original = std::fs::read(&path).expect("original WAL");
2484 let child =
2485 std::process::Command::new(std::env::current_exe().expect("test executable"))
2486 .args(["--exact", TEST, "--nocapture"])
2487 .env(CHILD, &path)
2488 .env(hook, "1")
2489 .output()
2490 .expect("child");
2491 assert_eq!(
2492 child.status.code(),
2493 Some(code),
2494 "{}",
2495 String::from_utf8_lossy(&child.stderr)
2496 );
2497 let after = std::fs::read(&path).expect("WAL after interruption");
2498 if replaced {
2499 let mut expected = reddb_file::encode_wal_file_header().to_vec();
2500 expected.extend_from_slice(&record.encode());
2501 assert_eq!(after, expected);
2502 } else {
2503 assert_eq!(after, original);
2504 }
2505 let coordinator = StoreCommitCoordinator::open(
2506 &path,
2507 DurabilityMode::Strict,
2508 GroupCommitOptions::default(),
2509 )
2510 .expect("reopen");
2511 coordinator.truncate().expect("retry rewrite");
2512 let records: Vec<_> = WalReader::open(&path)
2513 .expect("reader")
2514 .iter()
2515 .map(|entry| entry.expect("record").1)
2516 .collect();
2517 assert_eq!(records, vec![record]);
2518 }
2519 }
2520
2521 #[test]
2522 fn ownership_epoch_rejects_nested_begin_and_wrong_finish() {
2523 let directory = tempfile::tempdir().expect("temporary store");
2524 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("store");
2525 store.create_collection("rows").expect("collection");
2526 store.persist().expect("baseline");
2527 let coordinator = store.commit.as_ref().expect("WAL coordinator");
2528 assert!(coordinator.begin_page_ownership_epoch(&store, 0).is_err());
2529 assert!(coordinator.finish_page_ownership_epoch(&store, 1).is_err());
2530 coordinator
2531 .begin_page_ownership_epoch(&store, 2)
2532 .expect("begin epoch");
2533 assert!(coordinator.begin_page_ownership_epoch(&store, 3).is_err());
2534 assert!(coordinator.finish_page_ownership_epoch(&store, 3).is_err());
2535 assert!(coordinator.finish_page_ownership_epoch(&store, 2).is_err());
2536 store.flush_paged_registry().expect("publish epoch");
2537 coordinator
2538 .finish_page_ownership_epoch(&store, 2)
2539 .expect("finish epoch");
2540 assert_eq!(store.ownership_active_epoch.load(Ordering::Acquire), 0);
2541 }
2542
2543 #[test]
2544 fn native_pager_records_page_ownership_in_durable_wal() {
2545 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2546
2547 let directory = tempfile::tempdir().expect("temporary store");
2548 let path = directory.path().join("data.rdb");
2549 let wal_path = reddb_file::layout::unified_wal_path(&path);
2550 let pager = crate::storage::engine::Pager::open(&path, Default::default())
2551 .expect("open native pager");
2552 let coordinator = Arc::new(
2553 StoreCommitCoordinator::open(
2554 &wal_path,
2555 DurabilityMode::Async,
2556 GroupCommitOptions::default(),
2557 )
2558 .expect("open WAL"),
2559 );
2560 let recorder = Arc::new(
2561 StoreOwnershipRecorder::begin(Arc::clone(&coordinator), 7).expect("begin epoch"),
2562 );
2563 pager
2564 .install_ownership_recorder(recorder)
2565 .expect("install recorder");
2566 let page = pager
2567 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2568 .expect("allocate")
2569 .page_id();
2570 pager
2571 .record_retirement_intent(&[page])
2572 .expect("record retirement");
2573 pager.free_retired_pages(&[page]).expect("retire");
2574 let records: Vec<_> = WalReader::open(&wal_path)
2575 .expect("read WAL")
2576 .iter()
2577 .map(|item| item.expect("WAL record").1)
2578 .collect();
2579 assert_eq!(
2580 records,
2581 vec![
2582 WalRecord::PageOwnership {
2583 record: PageOwnershipRecord::Begin { epoch: 7 }
2584 },
2585 WalRecord::PageOwnership {
2586 record: PageOwnershipRecord::Reserve {
2587 epoch: 7,
2588 page_id: page
2589 }
2590 },
2591 WalRecord::PageOwnership {
2592 record: PageOwnershipRecord::Retire {
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 ]
2604 );
2605 assert!(
2606 coordinator.fsync_count() >= 3,
2607 "async ownership writes must sync"
2608 );
2609 assert!(
2610 coordinator.truncate().is_err(),
2611 "ownership WAL remains protected"
2612 );
2613 }
2614
2615 #[test]
2616 fn replay_reclaims_unpublished_ownership_reservations() {
2617 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2618
2619 let directory = tempfile::tempdir().expect("temporary store");
2620 let path = directory.path().join("data.rdb");
2621 {
2622 let store = UnifiedStore::open(&path).expect("create store");
2623 store.create_collection("rows").expect("collection");
2624 store.persist().expect("persist baseline");
2625 }
2626 let wal_path = reddb_file::layout::unified_wal_path(&path);
2627 let ids;
2628 {
2629 let pager =
2630 crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2631 let first = pager.page_count().expect("page count");
2632 let coordinator = StoreCommitCoordinator::open(
2633 &wal_path,
2634 DurabilityMode::Strict,
2635 GroupCommitOptions::default(),
2636 )
2637 .expect("WAL");
2638 coordinator
2639 .append_durable_page_ownership_batch(&[
2640 PageOwnershipRecord::Begin { epoch: 2 },
2641 PageOwnershipRecord::Reserve {
2642 epoch: 2,
2643 page_id: first,
2644 },
2645 PageOwnershipRecord::Reserve {
2646 epoch: 2,
2647 page_id: first + 1,
2648 },
2649 ])
2650 .expect("reserve pages durably");
2651 ids = [
2652 pager
2653 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2654 .expect("allocate")
2655 .page_id(),
2656 pager
2657 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2658 .expect("allocate")
2659 .page_id(),
2660 ];
2661 assert_eq!(ids, [first, first + 1]);
2662 pager.sync().expect("persist abandoned pages");
2663 }
2664 let reopened = UnifiedStore::open(&path).expect("recover ownership");
2665 let pager = reopened.pager.as_ref().expect("pager");
2666 let a = pager
2667 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2668 .expect("reuse")
2669 .page_id();
2670 let b = pager
2671 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2672 .expect("reuse")
2673 .page_id();
2674 assert_ne!(a, b);
2675 assert!(ids.contains(&a) && ids.contains(&b));
2676 let records: Vec<_> = WalReader::open(&wal_path)
2677 .expect("read WAL")
2678 .iter()
2679 .collect::<io::Result<Vec<_>>>()
2680 .expect("records");
2681 assert!(matches!(
2682 records.last(),
2683 Some((
2684 _,
2685 WalRecord::PageOwnership {
2686 record: PageOwnershipRecord::Finish { epoch: 2 }
2687 }
2688 ))
2689 ));
2690 reopened
2691 .commit
2692 .as_ref()
2693 .expect("coordinator")
2694 .truncate()
2695 .expect("completed ownership can be truncated");
2696 }
2697
2698 #[cfg(target_os = "linux")]
2699 #[test]
2700 fn ownership_recovery_survives_interruption_and_retry() {
2701 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2702
2703 const CHILD: &str = "REDDB_EXPERIMENTAL_OWNERSHIP_RECOVERY_PATH";
2704 if let Some(path) = std::env::var_os(CHILD) {
2705 let _store = UnifiedStore::open(path).expect("recover store");
2706 panic!("recovery interruption hook was not reached");
2707 }
2708 for (published, hook, exit_code) in [
2709 (
2710 false,
2711 "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2712 187,
2713 ),
2714 (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2715 (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2716 (
2717 true,
2718 "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2719 187,
2720 ),
2721 (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2722 (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2723 ] {
2724 let directory = tempfile::tempdir().expect("temporary store");
2725 let path = directory.path().join("data.rdb");
2726 {
2727 let store = UnifiedStore::open(&path).expect("create store");
2728 store.create_collection("rows").expect("collection");
2729 store.persist().expect("baseline");
2730 if published {
2731 store.ownership_active_epoch.store(2, Ordering::Release);
2732 store.flush_paged_registry().expect("publish epoch");
2733 store.ownership_active_epoch.store(0, Ordering::Release);
2734 }
2735 }
2736 let wal_path = reddb_file::layout::unified_wal_path(&path);
2737 let ids;
2738 {
2739 let pager =
2740 crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2741 let first = pager.page_count().expect("page count");
2742 let coordinator = StoreCommitCoordinator::open(
2743 &wal_path,
2744 DurabilityMode::Strict,
2745 GroupCommitOptions::default(),
2746 )
2747 .expect("WAL");
2748 let intent = |page_id| {
2749 if published {
2750 PageOwnershipRecord::Retire { epoch: 2, page_id }
2751 } else {
2752 PageOwnershipRecord::Reserve { epoch: 2, page_id }
2753 }
2754 };
2755 coordinator
2756 .append_durable_page_ownership_batch(&[
2757 PageOwnershipRecord::Begin { epoch: 2 },
2758 intent(first),
2759 intent(first + 1),
2760 ])
2761 .expect("reserve");
2762 ids = [
2763 pager
2764 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2765 .expect("allocate")
2766 .page_id(),
2767 pager
2768 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2769 .expect("allocate")
2770 .page_id(),
2771 ];
2772 pager.sync().expect("persist abandoned pages");
2773 }
2774 for _ in 0..if exit_code == 189 { 1 } else { 2 } {
2775 let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
2776 .args(["--exact", "storage::unified::store::commit::tests::ownership_recovery_survives_interruption_and_retry", "--nocapture"])
2777 .env(CHILD, &path)
2778 .env(hook, "1")
2779 .output().expect("run interrupted recovery");
2780 assert_eq!(
2781 output.status.code(),
2782 Some(exit_code),
2783 "published={published} {hook}: {}",
2784 String::from_utf8_lossy(&output.stderr)
2785 );
2786 }
2787 let reopened = UnifiedStore::open(&path).expect("recover after interruptions");
2788 let pager = reopened.pager.as_ref().expect("pager");
2789 let a = pager
2790 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2791 .expect("reuse")
2792 .page_id();
2793 let b = pager
2794 .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2795 .expect("reuse")
2796 .page_id();
2797 assert_ne!(a, b, "published={published} {hook}: duplicate page");
2798 assert!(
2799 ids.contains(&a) && ids.contains(&b),
2800 "published={published} {hook}: lost recovered page"
2801 );
2802 }
2803 }
2804
2805 #[test]
2806 fn ownership_intents_are_durable_in_every_mode() {
2807 use reddb_file::page_ownership_journal::PageOwnershipRecord;
2808
2809 for mode in [
2810 DurabilityMode::Strict,
2811 DurabilityMode::WalDurableGrouped,
2812 DurabilityMode::Async,
2813 ] {
2814 let directory = tempfile::tempdir().expect("temporary WAL");
2815 let path = directory.path().join("ownership");
2816 let coordinator =
2817 StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2818 .expect("open coordinator");
2819 let intent = PageOwnershipRecord::Reserve {
2820 epoch: 1,
2821 page_id: 67,
2822 };
2823 let detach = PageOwnershipRecord::DetachFree {
2824 epoch: 1,
2825 page_id: 68,
2826 };
2827 coordinator
2828 .append_durable_page_ownership_batch(&[intent, detach])
2829 .expect("durable intent");
2830 assert!(
2831 coordinator.fsync_count() > 0,
2832 "ownership intent must be synced in {mode:?}"
2833 );
2834 let records: Vec<_> = WalReader::open(&path)
2835 .expect("open WAL")
2836 .iter()
2837 .collect::<io::Result<Vec<_>>>()
2838 .expect("read records");
2839 assert!(
2840 matches!(records.as_slice(), [(_, WalRecord::PageOwnership { record: first }), (_, WalRecord::PageOwnership { record: second })] if *first == intent && *second == detach)
2841 );
2842 assert!(coordinator
2843 .append_single_record(WalRecord::PageOwnership { record: intent })
2844 .is_err());
2845 assert!(coordinator
2846 .append_durable_page_ownership(PageOwnershipRecord::Begin { epoch: 0 })
2847 .is_err());
2848 assert!(
2849 coordinator.truncate().is_err(),
2850 "active ownership must prevent truncation"
2851 );
2852 let before = std::fs::read(&path).expect("ownership WAL");
2853 drop(coordinator);
2854 let reopened = StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2855 .expect("reopen coordinator");
2856 assert!(
2857 reopened.truncate().is_err(),
2858 "reopen must rediscover ownership records"
2859 );
2860 assert_eq!(std::fs::read(&path).expect("preserved WAL"), before);
2861 }
2862 }
2863
2864 #[test]
2865 fn unresolved_ownership_intent_blocks_store_replay() {
2866 for truncated in [false, true] {
2867 let directory = tempfile::tempdir().expect("temporary store");
2868 let path = directory.path().join("data.rdb");
2869 {
2870 let store = UnifiedStore::open(&path).expect("create store");
2871 store.create_collection("rows").expect("create collection");
2872 store.persist().expect("persist baseline");
2873 }
2874 let wal_path = reddb_file::layout::unified_wal_path(&path);
2875 {
2876 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
2877 writer
2878 .append(&WalRecord::PageOwnership {
2879 record: reddb_file::page_ownership_journal::PageOwnershipRecord::Reserve {
2880 epoch: 1,
2881 page_id: 67,
2882 },
2883 })
2884 .expect("append ownership intent");
2885 writer.sync().expect("sync ownership intent");
2886 }
2887 if truncated {
2888 let wal = std::fs::OpenOptions::new()
2889 .write(true)
2890 .open(&wal_path)
2891 .expect("WAL");
2892 let length = wal.metadata().expect("length").len();
2893 wal.set_len(length - 1).expect("truncate last byte");
2894 wal.sync_all().expect("sync truncation");
2895 }
2896 let before = std::fs::read(&path).expect("baseline data");
2897 let error = match UnifiedStore::open(&path) {
2898 Ok(_) => panic!("unresolved ownership must prevent opening"),
2899 Err(error) => error,
2900 };
2901 let expected = if truncated {
2902 "incomplete page ownership WAL record"
2903 } else {
2904 "page ownership journal requires recovery"
2905 };
2906 assert!(error.to_string().contains(expected), "{error}");
2907 assert_eq!(std::fs::read(&path).expect("preserved data"), before);
2908 }
2909 }
2910
2911 #[test]
2912 fn logical_wal_replay_restores_a_deleted_physical_identity() {
2913 for flat in [false, true] {
2914 let store = UnifiedStore::new();
2915 store.create_collection("replay").expect("collection");
2916 let id = EntityId::new(42);
2917 let entity = UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(1)]);
2918 if flat {
2919 store
2920 .bulk_insert("replay", vec![entity])
2921 .expect("flat seed");
2922 } else {
2923 store.insert_auto("replay", entity).expect("hash seed");
2924 }
2925 let metadata = Metadata::with_fields(HashMap::from([(
2926 "source".into(),
2927 MetadataValue::String("wal".into()),
2928 )]));
2929 for sequence in 100..104 {
2930 let delete = StoreWalAction::DeleteEntityRecord {
2931 collection: "replay".into(),
2932 entity_id: id.raw(),
2933 };
2934 store
2935 .apply_encoded_store_wal_action(&delete.encode())
2936 .expect("replay delete");
2937 let mut restored =
2938 UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(2)]);
2939 restored.ensure_table_logical_id();
2940 restored.sequence_id = sequence;
2941 let upsert = StoreWalAction::upsert_entity(
2942 "replay",
2943 &restored,
2944 Some(&metadata),
2945 store.format_version(),
2946 );
2947 store
2948 .apply_encoded_store_wal_action(&upsert.encode())
2949 .expect("replay upsert");
2950 let manager = store.get_collection("replay").expect("restored collection");
2951 let stored = manager.get(id).expect("restored entity is readable");
2952 assert_eq!(store.entity_cache_stats().entries, 0);
2953 assert_eq!(manager.count(), 1);
2954 assert_eq!(manager.query_all(|_| true).len(), 1);
2955 assert_eq!(stored.sequence_id, sequence);
2956 assert_eq!(
2957 UnifiedStore::serialize_entity_record(
2958 &stored,
2959 manager.get_metadata(id).as_ref(),
2960 store.format_version()
2961 ),
2962 UnifiedStore::serialize_entity_record(
2963 &restored,
2964 Some(&metadata),
2965 store.format_version()
2966 )
2967 );
2968 assert!(store.get_any(id).is_some());
2969 }
2970 }
2971 }
2972
2973 fn embedded_fixture(
2974 mode: DurabilityMode,
2975 background_bytes: usize,
2976 ) -> (tempfile::TempDir, PathBuf, UnifiedStore) {
2977 let directory = tempfile::tempdir().expect("directory");
2978 let path = directory.path().join("data.rdb");
2979 let mut store =
2980 UnifiedStore::with_config(UnifiedStoreConfig::default().with_durability_mode(mode));
2981 store.create_collection("keys").expect("collection");
2982 store.set_aux_metadata(vec![42; background_bytes]);
2983 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(
2984 &path,
2985 &store.to_binary_dump_bytes(),
2986 )
2987 .expect("snapshot");
2988 store.config.embedded_wal_path = Some(path.clone());
2989 (directory, path, store)
2990 }
2991
2992 #[test]
2993 fn embedded_stale_full_result_does_not_checkpoint_reclaimed_space() {
2994 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2995 let (_directory, path, store) = embedded_fixture(mode, 0);
2996 store
2997 .append_embedded_store_wal_actions(&[StoreWalAction::CreateCollection {
2998 name: "keys".into(),
2999 }])
3000 .expect("another writer appended after reclaiming space");
3001 let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
3002 let payloads = vec![StoreWalAction::CreateCollection {
3003 name: "after".into(),
3004 }
3005 .encode()];
3006 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
3007 .expect("encoded length");
3008 store
3010 .recover_full_embedded_wal(&path, &payloads, length)
3011 .expect("revalidate after acquiring publication");
3012 let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
3013 assert_eq!(
3014 after.selected_superblock.generation,
3015 before.selected_superblock.generation + 1,
3016 "only the retry is published, without an unnecessary checkpoint"
3017 );
3018 assert_eq!(
3019 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&after)
3020 .expect("both appends retained")
3021 .len(),
3022 2
3023 );
3024 }
3025 }
3026
3027 #[test]
3028 fn embedded_full_wal_reserves_bounded_headroom_for_populated_store() {
3029 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
3030 let (_directory, path, store) = embedded_fixture(mode, 2 * 1024 * 1024);
3031 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial");
3032 let action = StoreWalAction::CreateCollection {
3033 name: "keys".into(),
3034 };
3035 let payloads = vec![action.encode()];
3036 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
3037 .expect("encoded length");
3038 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(
3039 &path,
3040 &vec![payloads[0].clone(); (initial.manifest.wal_region_bytes / length) as usize],
3041 )
3042 .expect("fill the initial region with valid records");
3043 store
3044 .append_embedded_store_wal_actions(&[action])
3045 .expect("overflow");
3046 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("checkpoint");
3047 assert_eq!(open.manifest.wal_region_bytes, 1024 * 1024);
3048 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
3049 .expect("snapshot")
3050 .expect("populated image");
3051 let recovered =
3052 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
3053 .expect("recover snapshot");
3054 for payload in
3055 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL")
3056 {
3057 recovered
3058 .apply_encoded_store_wal_action(&payload)
3059 .expect("replay");
3060 }
3061 assert_eq!(
3062 recovered.resident.aux_metadata.read().len(),
3063 2 * 1024 * 1024
3064 );
3065 assert!(recovered.get_collection("keys").is_some());
3066 }
3067 }
3068
3069 #[test]
3070 fn embedded_grouped_overwrites_recover_across_repeated_full_wal_regions() {
3071 fn row(writer: usize, round: usize) -> UnifiedEntity {
3072 let mut state = (writer as u64 + 1) * 31 + round as u64 * 13;
3073 let payload: String = (0..4096)
3074 .map(|_| {
3075 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
3076 char::from(b'!' + ((state >> 24) % 90) as u8)
3077 })
3078 .collect();
3079 let mut entity = UnifiedEntity::table_row(EntityId::new(0), "keys", 0, vec![]);
3080 entity.data = EntityData::Row(RowData::with_names(
3081 vec![
3082 Value::text(format!("k{writer}")),
3083 Value::text(format!("{round}:{payload}")),
3084 ],
3085 vec!["key".into(), "value".into()],
3086 ));
3087 entity
3088 }
3089 let (_directory, path, store) =
3090 embedded_fixture(DurabilityMode::WalDurableGrouped, 2 * 1024 * 1024);
3091 assert!(
3092 StoreWalAction::upsert_entity("keys", &row(0, 0), None, store.format_version())
3093 .encode()
3094 .len()
3095 >= 2048,
3096 "fixture must exert physical WAL pressure after value compression"
3097 );
3098 let mut ids: Vec<_> = (0..8)
3099 .map(|writer| {
3100 store
3101 .write_keyed_row("keys", row(writer, 0), None, None)
3102 .expect("seed")
3103 })
3104 .collect();
3105 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial roots");
3106 let mut previous_snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&initial)
3107 .expect("initial snapshot")
3108 .expect("image");
3109 for wave in 0..3 {
3113 let barrier = Barrier::new(ids.len());
3114 ids = std::thread::scope(|scope| {
3115 let workers: Vec<_> = ids
3116 .iter()
3117 .enumerate()
3118 .map(|(writer, previous)| {
3119 let store = &store;
3120 let barrier = &barrier;
3121 scope.spawn(move || {
3122 barrier.wait();
3123 let mut id = *previous;
3124 for round in 0..64 {
3125 id = store
3126 .write_keyed_row(
3127 "keys",
3128 row(writer, wave * 64 + round),
3129 None,
3130 Some(id),
3131 )
3132 .expect("durable overwrite");
3133 }
3134 id
3135 })
3136 })
3137 .collect();
3138 workers
3139 .into_iter()
3140 .map(|worker| worker.join().expect("writer"))
3141 .collect()
3142 });
3143 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("wave roots");
3144 assert_eq!(open.manifest.wal_region_bytes, EMBEDDED_WAL_HEADROOM_LIMIT);
3145 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
3146 .expect("wave snapshot")
3147 .expect("image");
3148 assert!(snapshot != previous_snapshot, "wave {wave} must checkpoint");
3149 previous_snapshot = snapshot;
3150 }
3151 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("recover roots");
3152 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
3153 .expect("snapshot")
3154 .expect("image");
3155 let recovered =
3156 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
3157 .expect("snapshot replay");
3158 for payload in crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL") {
3159 recovered
3160 .apply_encoded_store_wal_action(&payload)
3161 .expect("WAL replay");
3162 }
3163 assert_eq!(recovered.get_collection("keys").expect("keys").count(), 8);
3164 for (writer, id) in ids.iter().enumerate() {
3165 let entity = recovered
3166 .get("keys", *id)
3167 .expect("last acknowledged version");
3168 assert_eq!(
3169 entity.data.as_row().expect("row").get_field("value"),
3170 row(writer, 191)
3171 .data
3172 .as_row()
3173 .expect("expected row")
3174 .get_field("value")
3175 );
3176 }
3177 }
3178
3179 fn env_lock() -> &'static StdMutex<()> {
3184 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
3185 LOCK.get_or_init(|| StdMutex::new(()))
3186 }
3187
3188 fn temp_wal(name: &str) -> PathBuf {
3189 let nanos = SystemTime::now()
3190 .duration_since(std::time::UNIX_EPOCH)
3191 .unwrap()
3192 .as_nanos();
3193 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
3194 &std::env::temp_dir(),
3195 name,
3196 std::process::id(),
3197 nanos,
3198 );
3199 let _ = std::fs::remove_file(&path);
3200 path
3201 }
3202
3203 fn replay_crash_payload(id: u64, round: u64) -> String {
3204 let mut state = id * 31 + round * 13 + 1;
3205 (0..600)
3206 .map(|_| {
3207 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
3208 char::from(b'!' + ((state >> 24) % 90) as u8)
3209 })
3210 .collect()
3211 }
3212
3213 #[cfg(target_os = "linux")]
3214 #[test]
3215 fn interrupted_replay_reclaims_unpublished_pages() {
3216 interrupted_replay_reclaims_pages(false, false);
3217 }
3218
3219 #[cfg(target_os = "linux")]
3220 #[test]
3221 fn interrupted_replay_reclaims_durable_unpublished_pages() {
3222 interrupted_replay_reclaims_pages(false, true);
3223 }
3224
3225 #[cfg(target_os = "linux")]
3226 #[test]
3227 fn interrupted_replay_preserves_published_pages() {
3228 interrupted_replay_reclaims_pages(true, false);
3229 }
3230
3231 #[cfg(target_os = "linux")]
3232 #[test]
3233 fn interrupted_checkpoint_reclaims_unpublished_pages() {
3234 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_REBUILD_PATH";
3235 let config = || {
3236 UnifiedStoreConfig::default()
3237 .with_durability_mode(DurabilityMode::Strict)
3238 .with_page_cache_slots(4)
3239 };
3240 if let Some(path) = std::env::var_os(CHILD_PATH) {
3241 let store = UnifiedStore::open_with_config(path, config()).unwrap();
3242 for id in 1..=200 {
3245 let entity = UnifiedEntity::table_row(
3246 EntityId::new(id),
3247 "rows",
3248 id,
3249 vec![Value::text(replay_crash_payload(id, 8))],
3250 );
3251 let record =
3252 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3253 store.apply_replayed_upsert("rows", &record, false).unwrap();
3254 }
3255 store.persist().expect("must exit during checkpoint");
3256 panic!("checkpoint rebuild hook was not reached");
3257 }
3258 let directory = tempfile::tempdir().unwrap();
3259 let path = directory.path().join("data.rdb");
3260 {
3261 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3262 store.create_collection("rows").unwrap();
3263 for id in 1..=200 {
3264 store
3265 .insert(
3266 "rows",
3267 UnifiedEntity::table_row(
3268 EntityId::new(id),
3269 "rows",
3270 id,
3271 vec![Value::text(format!("initial-{id}"))],
3272 ),
3273 )
3274 .unwrap();
3275 }
3276 store.persist().unwrap();
3277 }
3278 let mut first = 0;
3279 let mut last = 0;
3280 for cycle in 1..=12 {
3281 let output = std::process::Command::new(std::env::current_exe().unwrap())
3282 .args([
3283 "--exact",
3284 "storage::unified::store::commit::tests::interrupted_checkpoint_reclaims_unpublished_pages",
3285 "--nocapture",
3286 ])
3287 .env(CHILD_PATH, &path)
3288 .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
3289 .output()
3290 .unwrap();
3291 assert_eq!(
3292 output.status.code(),
3293 Some(183),
3294 "{}",
3295 String::from_utf8_lossy(&output.stderr)
3296 );
3297 assert!(String::from_utf8_lossy(&output.stderr)
3298 .contains("experimental checkpoint exit before manifest"));
3299 last = std::fs::metadata(&path).unwrap().len();
3300 if cycle == 1 {
3301 first = last;
3302 }
3303 eprintln!("checkpoint_exit_cycle={cycle} data_logical={last}");
3304 }
3305 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3306 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3307 for id in 1..=200 {
3308 assert_eq!(
3309 recovered
3310 .get("rows", EntityId::new(id))
3311 .unwrap()
3312 .data
3313 .as_row()
3314 .unwrap()
3315 .columns,
3316 vec![Value::text(format!("initial-{id}"))]
3317 );
3318 }
3319 assert!(last <= first + 2 * crate::storage::engine::PAGE_SIZE as u64,
3320 "repeated interrupted checkpoint must bound page growth: first={first}, after_12={last}");
3321 }
3322
3323 #[cfg(target_os = "linux")]
3324 #[test]
3325 fn interrupted_checkpoint_with_wal_reclaims_unpublished_pages() {
3326 use std::os::unix::fs::MetadataExt;
3327 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_PATH";
3328 const ROUND: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_ROUND";
3329 let config = || {
3330 UnifiedStoreConfig::default()
3331 .with_durability_mode(DurabilityMode::Strict)
3332 .with_page_cache_slots(4)
3333 };
3334 if let Some(path) = std::env::var_os(CHILD_PATH) {
3335 let round: u64 = std::env::var(ROUND).unwrap().parse().unwrap();
3336 let store = UnifiedStore::open_with_config(path, config()).unwrap();
3337 if round > 1 {
3338 for id in 1..=200 {
3339 assert_eq!(
3340 store
3341 .get("rows", EntityId::new(id))
3342 .unwrap()
3343 .data
3344 .as_row()
3345 .unwrap()
3346 .columns,
3347 vec![Value::text(replay_crash_payload(id, round - 1))]
3348 );
3349 }
3350 }
3351 let actions: Vec<_> = (1..=200)
3352 .map(|id| {
3353 let entity = UnifiedEntity::table_row(
3354 EntityId::new(id),
3355 "rows",
3356 id,
3357 vec![Value::text(replay_crash_payload(id, round))],
3358 );
3359 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3360 })
3361 .collect();
3362 for action in &actions {
3363 store.apply_replayed_action(action, false).unwrap();
3364 }
3365 store
3368 .commit
3369 .as_ref()
3370 .unwrap()
3371 .append_actions(&actions)
3372 .unwrap();
3373 store.persist().expect("must exit during checkpoint");
3374 panic!("checkpoint rebuild hook was not reached");
3375 }
3376 let directory = tempfile::tempdir().unwrap();
3377 let path = directory.path().join("data.rdb");
3378 {
3379 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3380 store.create_collection("rows").unwrap();
3381 for id in 1..=200 {
3382 store
3383 .insert(
3384 "rows",
3385 UnifiedEntity::table_row(
3386 EntityId::new(id),
3387 "rows",
3388 id,
3389 vec![Value::text(format!("initial-{id}"))],
3390 ),
3391 )
3392 .unwrap();
3393 }
3394 store.persist().unwrap();
3395 }
3396 let mut fourth = 0;
3397 let mut last = 0;
3398 for round in 1..=12 {
3399 let output = std::process::Command::new(std::env::current_exe().unwrap())
3400 .args([
3401 "--exact",
3402 "storage::unified::store::commit::tests::interrupted_checkpoint_with_wal_reclaims_unpublished_pages",
3403 "--nocapture",
3404 ])
3405 .env(CHILD_PATH, &path)
3406 .env(ROUND, round.to_string())
3407 .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
3408 .output().unwrap();
3409 assert_eq!(
3410 output.status.code(),
3411 Some(183),
3412 "round {round}: {}",
3413 String::from_utf8_lossy(&output.stderr)
3414 );
3415 assert!(String::from_utf8_lossy(&output.stderr)
3416 .contains("experimental checkpoint exit before manifest"));
3417 let meta = std::fs::metadata(&path).unwrap();
3418 last = meta.len();
3419 if round == 4 {
3420 fourth = last;
3421 }
3422 eprintln!(
3423 "checkpoint_wal_exit_round={round} data_logical={last} data_allocated={}",
3424 meta.blocks() * 512
3425 );
3426 }
3427 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3428 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3429 for id in 1..=200 {
3430 assert_eq!(
3431 recovered
3432 .get("rows", EntityId::new(id))
3433 .unwrap()
3434 .data
3435 .as_row()
3436 .unwrap()
3437 .columns,
3438 vec![Value::text(replay_crash_payload(id, 12))]
3439 );
3440 }
3441 recovered.persist().unwrap();
3442 drop(recovered);
3443 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3444 for id in 1..=200 {
3445 assert_eq!(
3446 reopened
3447 .get("rows", EntityId::new(id))
3448 .unwrap()
3449 .data
3450 .as_row()
3451 .unwrap()
3452 .columns,
3453 vec![Value::text(replay_crash_payload(id, 12))]
3454 );
3455 }
3456 assert!(last <= fourth + 2 * crate::storage::engine::PAGE_SIZE as u64,
3457 "repeated interrupted WAL-backed checkpoints must bound page growth: round4={fourth}, round12={last}");
3458 }
3459
3460 #[cfg(target_os = "linux")]
3461 #[test]
3462 fn interrupted_retired_reclamation_preserves_reuse() {
3463 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_RETIRED_PATH";
3464 fn mutate(store: &UnifiedStore, round: u64) {
3465 for id in 1..=200 {
3466 let entity = UnifiedEntity::table_row(
3467 EntityId::new(id),
3468 "rows",
3469 id,
3470 vec![Value::text(replay_crash_payload(id, round))],
3471 );
3472 let record =
3473 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3474 store.apply_replayed_upsert("rows", &record, false).unwrap();
3475 }
3476 }
3477 let config = || {
3478 UnifiedStoreConfig::default()
3479 .with_durability_mode(DurabilityMode::Strict)
3480 .with_page_cache_slots(4)
3481 };
3482 if let Some(path) = std::env::var_os(CHILD_PATH) {
3483 let store = UnifiedStore::open_with_config(path, config()).unwrap();
3484 mutate(&store, 2);
3485 store.persist().expect("must exit at retired free boundary");
3486 panic!("retired free hook was not reached");
3487 }
3488 let mut reuse = Vec::new();
3489 for (label, env_name, exit_code, message) in [
3490 (
3491 "before",
3492 "REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE",
3493 184,
3494 "experimental exit before retired free",
3495 ),
3496 (
3497 "enqueued",
3498 "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE",
3499 186,
3500 "experimental exit after retired enqueue",
3501 ),
3502 (
3503 "after",
3504 "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE",
3505 185,
3506 "experimental exit after retired free",
3507 ),
3508 ] {
3509 let directory = tempfile::tempdir().unwrap();
3510 let path = directory.path().join("data.rdb");
3511 {
3512 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3513 store.create_collection("rows").unwrap();
3514 for id in 1..=200 {
3515 store
3516 .insert(
3517 "rows",
3518 UnifiedEntity::table_row(
3519 EntityId::new(id),
3520 "rows",
3521 id,
3522 vec![Value::text(format!("initial-{id}"))],
3523 ),
3524 )
3525 .unwrap();
3526 }
3527 store.persist().unwrap();
3528 mutate(&store, 1);
3529 store.persist().unwrap();
3530 }
3531 let output = std::process::Command::new(std::env::current_exe().unwrap())
3532 .args([
3533 "--exact",
3534 "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse",
3535 "--nocapture",
3536 ])
3537 .env(CHILD_PATH, &path)
3538 .env(env_name, "1")
3539 .output().unwrap();
3540 assert_eq!(
3541 output.status.code(),
3542 Some(exit_code),
3543 "{label}: {}",
3544 String::from_utf8_lossy(&output.stderr)
3545 );
3546 assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3547 eprintln!(
3548 "retired_reclaim_{label} child={}",
3549 String::from_utf8_lossy(&output.stderr).trim()
3550 );
3551 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3552 for id in 1..=200 {
3553 assert_eq!(
3554 store
3555 .get("rows", EntityId::new(id))
3556 .unwrap()
3557 .data
3558 .as_row()
3559 .unwrap()
3560 .columns,
3561 vec![Value::text(replay_crash_payload(id, 2))]
3562 );
3563 }
3564 let pager = store.pager.as_ref().unwrap();
3565 let before_pages = pager.page_count().unwrap();
3566 let mut ids = std::collections::HashSet::new();
3567 let mut reused = 0;
3568 for _ in 0..256 {
3569 let page = pager
3570 .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3571 .unwrap();
3572 let id = page.page_id();
3573 assert!(ids.insert(id), "duplicate allocation");
3574 reused += u32::from(id < before_pages);
3575 pager.write_page(id, page).unwrap();
3576 }
3577 pager.sync().unwrap();
3578 eprintln!(
3579 "retired_reclaim_{label} before_pages={before_pages} reused={reused} appended={}",
3580 256 - reused
3581 );
3582 drop(store);
3583 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3584 for id in 1..=200 {
3585 assert_eq!(
3586 reopened
3587 .get("rows", EntityId::new(id))
3588 .unwrap()
3589 .data
3590 .as_row()
3591 .unwrap()
3592 .columns,
3593 vec![Value::text(replay_crash_payload(id, 2))]
3594 );
3595 }
3596 reuse.push(reused);
3597 }
3598 assert!(
3599 reuse[0] >= reuse[2] && reuse[1] >= reuse[2],
3600 "recovery must not lose retired pages before allocator publication: before={} enqueued={} after={}",
3601 reuse[0],
3602 reuse[1],
3603 reuse[2]
3604 );
3605 }
3606
3607 #[cfg(target_os = "linux")]
3608 #[test]
3609 fn experimental_retaining_released_root_is_not_recoverable() {
3610 let directory = tempfile::tempdir().unwrap();
3611 let path = directory.path().join("data.rdb");
3612 let config = || {
3613 UnifiedStoreConfig::default()
3614 .with_durability_mode(DurabilityMode::Strict)
3615 .with_page_cache_slots(4)
3616 };
3617 {
3618 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3619 store.create_collection("rows").unwrap();
3620 for id in 1..=200 {
3621 store
3622 .insert(
3623 "rows",
3624 UnifiedEntity::table_row(
3625 EntityId::new(id),
3626 "rows",
3627 id,
3628 vec![Value::text(format!("initial-{id}"))],
3629 ),
3630 )
3631 .unwrap();
3632 }
3633 store.persist().unwrap();
3634 for id in 1..=200 {
3635 let entity = UnifiedEntity::table_row(
3636 EntityId::new(id),
3637 "rows",
3638 id,
3639 vec![Value::text(replay_crash_payload(id, 1))],
3640 );
3641 let record =
3642 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3643 store.apply_replayed_upsert("rows", &record, false).unwrap();
3644 }
3645 store.persist().unwrap();
3646 }
3647 let output = std::process::Command::new(std::env::current_exe().unwrap())
3648 .args(["--exact", "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse", "--nocapture"])
3649 .env("REDDB_EXPERIMENTAL_RETIRED_PATH", &path)
3650 .env("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT", "1")
3651 .env("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE", "1")
3652 .output().unwrap();
3653 assert_eq!(
3654 output.status.code(),
3655 Some(185),
3656 "{}",
3657 String::from_utf8_lossy(&output.stderr)
3658 );
3659 assert!(String::from_utf8_lossy(&output.stderr)
3660 .contains("experimental exit after retired free"));
3661
3662 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3663 for id in 1..=200 {
3664 assert_eq!(
3665 reopened
3666 .get("rows", EntityId::new(id))
3667 .unwrap()
3668 .data
3669 .as_row()
3670 .unwrap()
3671 .columns,
3672 vec![Value::text(replay_crash_payload(id, 2))]
3673 );
3674 }
3675 let retired_roots: Vec<_> = reopened
3676 .retired_btrees
3677 .lock()
3678 .iter()
3679 .map(|entry| entry.tree.root_page_id())
3680 .collect();
3681 let pager = reopened.pager.as_ref().unwrap();
3682 let root_types: Vec<_> = retired_roots
3683 .iter()
3684 .map(|id| pager.read_page(*id).unwrap().page_type().unwrap())
3685 .collect();
3686 assert!(
3687 root_types.contains(&crate::storage::engine::page::PageType::FreelistTrunk),
3688 "expected a retained root to have become a freelist trunk: {root_types:?}"
3689 );
3690 let error = reopened
3691 .persist()
3692 .expect_err("retained root was reused as allocator metadata");
3693 assert!(
3694 error
3695 .to_string()
3696 .contains("unexpected retired tree page type"),
3697 "{error}"
3698 );
3699 eprintln!("retained_root_types={root_types:?} persist_error={error}");
3700 }
3701
3702 #[cfg(target_os = "linux")]
3703 fn interrupted_replay_reclaims_pages(after_publication: bool, force_sync: bool) {
3704 use std::os::unix::fs::MetadataExt;
3705 const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_REPLAY_REBUILD_PATH";
3706 let config = || {
3707 UnifiedStoreConfig::default()
3708 .with_durability_mode(DurabilityMode::Strict)
3709 .with_page_cache_slots(4)
3710 };
3711 if let Some(path) = std::env::var_os(CHILD_PATH) {
3712 let _store = UnifiedStore::open_with_config(path, config())
3713 .expect("must exit while replay rebuilds");
3714 panic!("replay rebuild hook was not reached");
3715 }
3716 let directory = tempfile::tempdir().unwrap();
3717 let path = directory.path().join("data.rdb");
3718 {
3719 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3720 store.create_collection("rows").unwrap();
3721 for id in 1..=200 {
3722 store
3723 .insert(
3724 "rows",
3725 UnifiedEntity::table_row(
3726 EntityId::new(id),
3727 "rows",
3728 id,
3729 vec![Value::text(format!("initial-{id}"))],
3730 ),
3731 )
3732 .unwrap();
3733 }
3734 store.persist().unwrap();
3735 }
3736 {
3737 let coordinator = StoreCommitCoordinator::open(
3738 reddb_file::layout::unified_wal_path(&path),
3739 DurabilityMode::Strict,
3740 GroupCommitOptions::default(),
3741 )
3742 .unwrap();
3743 let actions = (1..=200)
3744 .map(|id| {
3745 let entity = UnifiedEntity::table_row(
3746 EntityId::new(id),
3747 "rows",
3748 id,
3749 vec![Value::text(replay_crash_payload(id, 8))],
3750 );
3751 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3752 })
3753 .collect::<Vec<_>>();
3754 coordinator.append_actions(&actions).unwrap();
3755 }
3756 let snapshot = || {
3757 let data = std::fs::metadata(&path).unwrap();
3758 let wal = std::fs::metadata(reddb_file::layout::unified_wal_path(&path)).unwrap();
3759 (
3760 data.len(),
3761 data.blocks() * 512,
3762 wal.len(),
3763 wal.blocks() * 512,
3764 )
3765 };
3766 let baseline = snapshot();
3767 let mut previous = baseline;
3768 let mut first_failure = baseline;
3769 for cycle in 1..=12 {
3770 let test_name = if after_publication {
3771 "storage::unified::store::commit::tests::interrupted_replay_preserves_published_pages"
3772 } else if force_sync {
3773 "storage::unified::store::commit::tests::interrupted_replay_reclaims_durable_unpublished_pages"
3774 } else {
3775 "storage::unified::store::commit::tests::interrupted_replay_reclaims_unpublished_pages"
3776 };
3777 let mut command = std::process::Command::new(std::env::current_exe().unwrap());
3778 command
3779 .args(["--exact", test_name, "--nocapture"])
3780 .env(CHILD_PATH, &path);
3781 if !force_sync {
3782 command.env("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC", "1");
3783 } else {
3784 command.env_remove("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC");
3785 }
3786 let message = if after_publication {
3787 command.env("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", "1");
3788 "experimental replay exit after manifest"
3789 } else {
3790 command.env("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD", "1");
3791 "experimental replay rebuild exit before manifest"
3792 };
3793 let output = command.output().unwrap();
3794 assert_eq!(
3795 output.status.code(),
3796 Some(182),
3797 "cycle {cycle}: {}",
3798 String::from_utf8_lossy(&output.stderr)
3799 );
3800 assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3801 let now = snapshot();
3802 eprintln!("replay_exit_cycle={cycle} data_logical={} data_allocated={} wal_logical={} wal_allocated={}",now.0,now.1,now.2,now.3);
3803 if cycle == 1 {
3804 first_failure = now;
3805 }
3806 previous = now;
3807 }
3808 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3809 assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3810 for id in 1..=200 {
3811 assert_eq!(
3812 recovered
3813 .get("rows", EntityId::new(id))
3814 .unwrap()
3815 .data
3816 .as_row()
3817 .unwrap()
3818 .columns,
3819 vec![Value::text(replay_crash_payload(id, 8))]
3820 );
3821 }
3822 let pages = recovered.pager.as_ref().unwrap().page_count().unwrap();
3823 let free = recovered.pager.as_ref().unwrap().free_page_count().unwrap();
3824 recovered.persist().unwrap();
3825 drop(recovered);
3826 let final_sizes = snapshot();
3827 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3828 assert_eq!(reopened.get_collection("rows").unwrap().count(), 200);
3829 for id in 1..=200 {
3830 assert_eq!(
3831 reopened
3832 .get("rows", EntityId::new(id))
3833 .unwrap()
3834 .data
3835 .as_row()
3836 .unwrap()
3837 .columns,
3838 vec![Value::text(replay_crash_payload(id, 8))]
3839 );
3840 }
3841 eprintln!("replay_rebuild baseline={baseline:?} after_12={previous:?} after_recovery_checkpoint={final_sizes:?} pages={pages} free={free}");
3842 let pager = reopened.pager.as_ref().unwrap();
3846 let before_probe = pager.page_count().unwrap();
3847 let mut ids = std::collections::HashSet::new();
3848 let mut reused = 0;
3849 for _ in 0..256 {
3850 let page = pager
3851 .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3852 .unwrap();
3853 let id = page.page_id();
3854 assert!(ids.insert(id), "allocator returned the same page twice");
3855 reused += u32::from(id < before_probe);
3856 pager.write_page(id, page).unwrap();
3857 }
3858 pager.sync().unwrap();
3859 let after_probe = pager.page_count().unwrap();
3860 eprintln!("replay_reuse_probe count=256 before_pages={before_probe} after_pages={after_probe} reused={reused} appended={}", 256 - reused);
3861 drop(reopened);
3862 let verified = UnifiedStore::open_with_config(&path, config()).unwrap();
3863 for id in 1..=200 {
3864 assert_eq!(
3865 verified
3866 .get("rows", EntityId::new(id))
3867 .unwrap()
3868 .data
3869 .as_row()
3870 .unwrap()
3871 .columns,
3872 vec![Value::text(replay_crash_payload(id, 8))]
3873 );
3874 }
3875 assert!(
3876 previous.0 <= first_failure.0 + 2 * crate::storage::engine::PAGE_SIZE as u64,
3877 "repeated interrupted replay must bound page growth: first={}, after_12={}",
3878 first_failure.0,
3879 previous.0
3880 );
3881 }
3882
3883 #[test]
3884 fn paged_wal_replay_survives_immediate_process_exit() {
3885 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
3886 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3887 if let Some(path) = std::env::var_os(CHILD_PATH) {
3888 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
3889 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
3890 std::process::exit(0);
3892 }
3893 let directory = tempfile::tempdir().unwrap();
3894 let path = directory.path().join("data.rdb");
3895 {
3896 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3897 store.create_collection("rows").unwrap();
3898 for id in 1..=400 {
3899 store
3900 .insert(
3901 "rows",
3902 UnifiedEntity::table_row(
3903 EntityId::new(id),
3904 "rows",
3905 id,
3906 vec![Value::text(format!("initial-{id}"))],
3907 ),
3908 )
3909 .unwrap();
3910 }
3911 store.persist().unwrap();
3912 }
3913 {
3914 let coordinator = StoreCommitCoordinator::open(
3915 reddb_file::layout::unified_wal_path(&path),
3916 DurabilityMode::Strict,
3917 GroupCommitOptions::default(),
3918 )
3919 .unwrap();
3920 for round in 0..3 {
3921 let actions = (1..=400)
3922 .map(|id| {
3923 let payload = replay_crash_payload(id, round);
3924 let entity = UnifiedEntity::table_row(
3925 EntityId::new(id),
3926 "rows",
3927 id,
3928 vec![Value::text(payload)],
3929 );
3930 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3931 })
3932 .collect::<Vec<_>>();
3933 coordinator.append_actions(&actions).unwrap();
3934 }
3935 }
3936 let status = std::process::Command::new(std::env::current_exe().unwrap())
3937 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
3938 .env(CHILD_PATH, &path).status().unwrap();
3939 assert!(status.success(), "replay process failed");
3940 let reopened = UnifiedStore::open_with_config(&path, config())
3941 .expect("cold reopen after replay crash");
3942 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
3943 for id in 1..=400 {
3944 let entity = reopened
3945 .get("rows", EntityId::new(id))
3946 .expect("retained entity");
3947 let EntityData::Row(row) = entity.data else {
3948 panic!("expected row")
3949 };
3950 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
3951 }
3952 drop(reopened);
3953 let stable_bytes = std::fs::metadata(&path).unwrap().len();
3954 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
3955 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
3956 assert_eq!(
3957 std::fs::metadata(&path).unwrap().len(),
3958 stable_bytes,
3959 "idempotent replay must not allocate another tree generation"
3960 );
3961 }
3962
3963 #[test]
3964 fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
3965 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3966 let path = temp_wal("checkpoint-ack");
3967 let coordinator = Arc::new(
3968 StoreCommitCoordinator::open(
3969 path.clone(),
3970 DurabilityMode::WalDurableGrouped,
3971 GroupCommitOptions::default(),
3972 )
3973 .unwrap(),
3974 );
3975 let (written_tx, written_rx) = std::sync::mpsc::channel();
3979 let (enqueued_tx, enqueued_rx) = std::sync::mpsc::channel();
3980 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
3981 let writer_coordinator = Arc::clone(&coordinator);
3982 let writer = std::thread::spawn(move || {
3983 writer_coordinator.check_write_error().unwrap();
3984 let _epoch = writer_coordinator.queue.epoch.read();
3985 let action = StoreWalAction::CreateCollection {
3986 name: "acknowledged".into(),
3987 }
3988 .encode();
3989 let wal_bytes = action.len() as u64;
3990 let blob = WalRecord::TxCommitBatch {
3991 tx_id: 1,
3992 actions: vec![action],
3993 }
3994 .encode();
3995 let target_lsn = writer_coordinator.queue.enqueue(blob);
3996 enqueued_tx.send(()).unwrap();
3997 resume_rx.recv().unwrap();
3998 let result = writer_coordinator.wait_until_durable_batch(target_lsn, wal_bytes, false);
3999 written_tx.send(result).unwrap();
4000 });
4001 enqueued_rx.recv_timeout(Duration::from_secs(2)).unwrap();
4002 let state = coordinator.state.0.lock();
4003 resume_tx.send(()).unwrap();
4004 let (started_tx, started_rx) = std::sync::mpsc::channel();
4005 let (reset_tx, reset_rx) = std::sync::mpsc::channel();
4006 let reset_coordinator = Arc::clone(&coordinator);
4007 let reset = std::thread::spawn(move || {
4008 started_tx.send(()).unwrap();
4009 reset_tx.send(reset_coordinator.truncate()).unwrap();
4010 });
4011 started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
4012 let deadline = Instant::now() + Duration::from_millis(100);
4013 while coordinator.queue.has_pending() && Instant::now() < deadline {
4014 std::thread::sleep(Duration::from_millis(1));
4015 }
4016 let discarded_before_ack = !coordinator.queue.has_pending();
4017 drop(state);
4018 let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
4019 if acknowledged.is_err() {
4020 let mut state = coordinator.state.0.lock();
4023 state.shutdown = true;
4024 state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
4025 coordinator.state.1.notify_all();
4026 }
4027 writer.join().unwrap();
4028 reset_rx
4029 .recv_timeout(Duration::from_secs(2))
4030 .unwrap()
4031 .unwrap();
4032 reset.join().unwrap();
4033 assert!(
4034 !discarded_before_ack,
4035 "checkpoint discarded an unacknowledged WAL record"
4036 );
4037 assert!(
4038 acknowledged.unwrap().is_ok(),
4039 "durable writer did not finish"
4040 );
4041 drop(coordinator);
4042 let _ = std::fs::remove_file(path);
4043 }
4044
4045 #[test]
4046 fn ready_publication_requests_flush_but_waits_for_durability() {
4047 let path = temp_wal("ready-publication");
4048 let options = GroupCommitOptions {
4049 max_statements: 8,
4050 window_ms: 1000,
4051 ..Default::default()
4052 };
4053 let coordinator = Arc::new(
4054 StoreCommitCoordinator::open(path.clone(), DurabilityMode::WalDurableGrouped, options)
4055 .expect("coordinator"),
4056 );
4057 let wal = coordinator.wal.lock();
4058 let acknowledged = Arc::new(std::sync::atomic::AtomicBool::new(false));
4059 let writer = {
4060 let coordinator = Arc::clone(&coordinator);
4061 let acknowledged = Arc::clone(&acknowledged);
4062 std::thread::spawn(move || {
4063 coordinator
4064 .append_ready_actions(
4065 &[StoreWalAction::CreateCollection {
4066 name: "ready".into(),
4067 }],
4068 false,
4069 )
4070 .expect("durable publication");
4071 acknowledged.store(true, Ordering::Release);
4072 })
4073 };
4074 let deadline = Instant::now() + Duration::from_secs(10);
4075 while coordinator.state.0.lock().pending_statements == 0 {
4076 assert!(Instant::now() < deadline, "writer did not enqueue");
4077 std::thread::yield_now();
4078 }
4079 assert_eq!(
4080 coordinator.state.0.lock().pending_statements,
4081 options.max_statements
4082 );
4083 assert!(
4084 !acknowledged.load(Ordering::Acquire),
4085 "must wait for WAL sync"
4086 );
4087 drop(wal);
4088 writer.join().expect("writer");
4089 assert!(acknowledged.load(Ordering::Acquire));
4090 assert_eq!(coordinator.fsync_count(), 1);
4091 drop(coordinator);
4092 let _ = std::fs::remove_file(path);
4093 }
4094
4095 #[test]
4106 fn group_commit_coalesces_concurrent_autocommits() {
4107 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
4108 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4111
4112 let path = temp_wal("coalesce");
4113 let coord = Arc::new(
4114 StoreCommitCoordinator::open(
4115 path.clone(),
4116 DurabilityMode::WalDurableGrouped,
4117 GroupCommitOptions::default(),
4118 )
4119 .expect("open commit coordinator"),
4120 );
4121
4122 const WRITERS: usize = 32;
4123 let barrier = Arc::new(Barrier::new(WRITERS));
4124 let mut handles = Vec::with_capacity(WRITERS);
4125 for tx in 0..WRITERS {
4126 let coord_c = Arc::clone(&coord);
4127 let barrier_c = Arc::clone(&barrier);
4128 handles.push(std::thread::spawn(move || {
4129 barrier_c.wait();
4133 let action = StoreWalAction::CreateCollection {
4134 name: format!("c{tx}"),
4135 };
4136 coord_c
4137 .append_actions(std::slice::from_ref(&action))
4138 .expect("append_actions");
4139 }));
4140 }
4141 for h in handles {
4142 h.join().expect("writer thread");
4143 }
4144
4145 let fsyncs = coord.fsync_count();
4146 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
4147 assert!(
4148 fsyncs < WRITERS as u64,
4149 "expected fsyncs ({fsyncs}) to be strictly less than \
4150 concurrent writers ({WRITERS}); coalescing failed"
4151 );
4152
4153 drop(coord);
4154 let _ = std::fs::remove_file(&path);
4155 }
4156
4157 #[test]
4163 fn zero_window_disables_coalescing_floor() {
4164 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
4165 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
4166
4167 let path = temp_wal("zero_window");
4168 let coord = Arc::new(
4169 StoreCommitCoordinator::open(
4170 path.clone(),
4171 DurabilityMode::WalDurableGrouped,
4172 GroupCommitOptions::default(),
4173 )
4174 .expect("open commit coordinator"),
4175 );
4176
4177 const WRITERS: usize = 8;
4178 let barrier = Arc::new(Barrier::new(WRITERS));
4179 let mut handles = Vec::with_capacity(WRITERS);
4180 for tx in 0..WRITERS {
4181 let coord_c = Arc::clone(&coord);
4182 let barrier_c = Arc::clone(&barrier);
4183 handles.push(std::thread::spawn(move || {
4184 barrier_c.wait();
4185 let action = StoreWalAction::CreateCollection {
4186 name: format!("z{tx}"),
4187 };
4188 coord_c
4189 .append_actions(std::slice::from_ref(&action))
4190 .expect("append_actions");
4191 }));
4192 }
4193 for h in handles {
4194 h.join().expect("writer thread");
4195 }
4196
4197 let fsyncs = coord.fsync_count();
4203 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
4204
4205 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4206 drop(coord);
4207 let _ = std::fs::remove_file(&path);
4208 }
4209
4210 #[test]
4212 fn resolve_window_precedence() {
4213 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
4214 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4216 let cfg = GroupCommitOptions::default();
4217 assert_eq!(
4218 StoreCommitCoordinator::resolve_window(&cfg),
4219 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
4220 );
4221
4222 let cfg_ms = GroupCommitOptions {
4224 window_ms: 5,
4225 ..GroupCommitOptions::default()
4226 };
4227 assert_eq!(
4228 StoreCommitCoordinator::resolve_window(&cfg_ms),
4229 Duration::from_millis(5)
4230 );
4231
4232 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
4234 assert_eq!(
4235 StoreCommitCoordinator::resolve_window(&cfg),
4236 Duration::from_micros(750)
4237 );
4238 assert_eq!(
4239 StoreCommitCoordinator::resolve_window(&cfg_ms),
4240 Duration::from_micros(750)
4241 );
4242
4243 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
4245 assert_eq!(
4246 StoreCommitCoordinator::resolve_window(&cfg),
4247 Duration::from_micros(0)
4248 );
4249
4250 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
4251 }
4252}