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