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