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