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