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