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, Ordering};
9use std::sync::{Arc, Condvar, Mutex};
10
11struct NativeReplayTarget<'a> {
12 store: &'a UnifiedStore,
13 update_btrees: bool,
14}
15
16impl reddb_engine::storage::store_wal::StoreReplayTarget for NativeReplayTarget<'_> {
17 fn has_collection(&self, name: &str) -> bool {
18 self.store.get_collection(name).is_some()
19 }
20 fn create_collection(&self, name: &str) -> Result<(), StoreError> {
21 self.store.create_collection_in_memory(name)
22 }
23 fn drop_collection(&self, name: &str) -> Result<(), StoreError> {
24 self.store.drop_collection_in_memory(name)
25 }
26 fn upsert(&self, collection: &str, record: &[u8]) -> Result<(), StoreError> {
27 self.store
28 .apply_replayed_upsert(collection, record, self.update_btrees)
29 }
30 fn delete(&self, collection: &str, id: EntityId) -> Result<(), StoreError> {
31 self.store
32 .apply_replayed_delete(collection, id, self.update_btrees)
33 }
34 fn refresh(&self, collection: &str, records: &[Vec<u8>]) -> Result<(), StoreError> {
35 self.store
36 .apply_replayed_refresh_collection(collection, records, self.update_btrees)
37 }
38}
39
40const DEFAULT_ADAPTIVE_WINDOW_US: u64 = 200;
57
58type WalMutex = parking_lot::Mutex<WalWriter>;
65
66type CommitStateMutex = parking_lot::Mutex<CommitState>;
72type CommitStateCondvar = parking_lot::Condvar;
73use std::time::{Duration, Instant};
74
75static NEXT_STORE_TX_ID: AtomicU64 = AtomicU64::new(1);
76
77pub(crate) use reddb_engine::storage::store_wal::StoreWalAction;
78
79#[derive(Debug, Default)]
80pub(crate) struct DeferredStoreWalActions {
81 pub(super) actions: Vec<StoreWalAction>,
82}
83
84pub(super) struct StoreWalCaptureGuard<'a> {
85 active: bool,
86 session: Option<&'a crate::storage::embedded::EmbeddedCommitSession>,
87}
88
89impl<'a> StoreWalCaptureGuard<'a> {
90 pub(super) fn begin(
91 session: Option<&'a crate::storage::embedded::EmbeddedCommitSession>,
92 ) -> Self {
93 assert!(
94 !deferred_store_wal_capture_active(),
95 "store WAL captures cannot nest"
96 );
97 begin_deferred_store_wal_capture();
98 Self {
99 active: true,
100 session,
101 }
102 }
103
104 pub(super) fn finish(mut self) -> DeferredStoreWalActions {
105 let actions = take_deferred_store_wal_capture();
106 self.active = false;
107 actions
108 }
109}
110
111impl Drop for StoreWalCaptureGuard<'_> {
112 fn drop(&mut self) {
113 if self.active {
114 if let Some(session) = self.session {
115 session.fence_interrupted_write();
116 }
117 let _ = take_deferred_store_wal_capture();
118 }
119 }
120}
121
122impl DeferredStoreWalActions {
123 pub(crate) fn is_empty(&self) -> bool {
124 self.actions.is_empty()
125 }
126
127 pub(crate) fn extend(&mut self, other: Self) {
128 self.actions.extend(other.actions);
129 }
130}
131
132thread_local! {
133 static DEFERRED_STORE_WAL_ACTIONS: RefCell<Option<Vec<StoreWalAction>>> =
134 const { RefCell::new(None) };
135}
136
137fn begin_deferred_store_wal_capture() {
138 DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
139 let mut guard = cell.borrow_mut();
140 debug_assert!(guard.is_none());
141 *guard = Some(Vec::new());
142 });
143}
144
145fn capture_deferred_store_wal_actions(actions: Vec<StoreWalAction>) -> bool {
146 DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
147 let mut guard = cell.borrow_mut();
148 if let Some(pending) = guard.as_mut() {
149 pending.extend(actions);
150 true
151 } else {
152 false
153 }
154 })
155}
156
157fn deferred_store_wal_capture_active() -> bool {
158 DEFERRED_STORE_WAL_ACTIONS.with(|cell| cell.borrow().is_some())
159}
160
161fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
162 DEFERRED_STORE_WAL_ACTIONS.with(|cell| DeferredStoreWalActions {
163 actions: cell.borrow_mut().take().unwrap_or_default(),
164 })
165}
166
167#[derive(Debug)]
168struct CommitState {
169 durable_lsn: u64,
170 pending_target_lsn: u64,
171 pending_statements: usize,
172 pending_wal_bytes: u64,
173 first_pending_at: Option<Instant>,
174 shutdown: bool,
175 last_error: Option<String>,
176}
177
178impl CommitState {
179 fn new(initial_durable_lsn: u64) -> Self {
180 Self {
181 durable_lsn: initial_durable_lsn,
182 pending_target_lsn: initial_durable_lsn,
183 pending_statements: 0,
184 pending_wal_bytes: 0,
185 first_pending_at: None,
186 shutdown: false,
187 last_error: None,
188 }
189 }
190}
191
192pub(crate) struct WalAppendQueue {
206 epoch: parking_lot::RwLock<()>,
209 pending: parking_lot::Mutex<WalQueueState>,
224 queued_bytes: AtomicU64,
229}
230
231struct WalQueueState {
232 next_lsn: u64,
233 entries: Vec<(u64, Vec<u8>)>,
234}
235
236impl WalAppendQueue {
237 fn new(initial_lsn: u64) -> Self {
238 Self {
239 epoch: parking_lot::RwLock::new(()),
240 pending: parking_lot::Mutex::new(WalQueueState {
241 next_lsn: initial_lsn,
242 entries: Vec::with_capacity(64),
243 }),
244 queued_bytes: AtomicU64::new(0),
245 }
246 }
247
248 fn enqueue(&self, bytes: Vec<u8>) -> u64 {
254 let len = bytes.len() as u64;
255 let mut state = self.pending.lock();
256 let start_lsn = state.next_lsn;
257 state.next_lsn = start_lsn + len;
258 state.entries.push((start_lsn, bytes));
259 self.queued_bytes.fetch_add(len, Ordering::Relaxed);
260 start_lsn + len
261 }
262
263 fn drain_sorted(&self) -> Vec<(u64, Vec<u8>)> {
267 let mut state = self.pending.lock();
268 let mut v = std::mem::take(&mut state.entries);
269 let drained: u64 = v.iter().map(|(_, bytes)| bytes.len() as u64).sum();
270 drop(state);
271 let _ = self
275 .queued_bytes
276 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |queued| {
277 Some(queued.saturating_sub(drained))
278 });
279 v.sort_by_key(|(lsn, _)| *lsn);
280 v
281 }
282
283 fn has_pending(&self) -> bool {
286 !self.pending.lock().entries.is_empty()
287 }
288
289 fn queued_bytes(&self) -> u64 {
291 self.queued_bytes.load(Ordering::Relaxed)
292 }
293
294 fn reset(&self, next_lsn: u64) {
300 let mut state = self.pending.lock();
301 state.next_lsn = next_lsn;
302 state.entries.clear();
303 drop(state);
304 self.queued_bytes.store(0, Ordering::Relaxed);
305 }
306}
307
308pub(crate) struct StoreCommitCoordinator {
309 mode: DurabilityMode,
310 config: crate::api::GroupCommitOptions,
311 wal_path: PathBuf,
312 wal: Arc<WalMutex>,
313 queue: Arc<WalAppendQueue>,
319 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
320 fsync_count: Arc<AtomicU64>,
326 wal_generation: AtomicU64,
327}
328
329impl StoreCommitCoordinator {
330 pub(crate) fn open(
331 wal_path: impl Into<PathBuf>,
332 mode: DurabilityMode,
333 config: crate::api::GroupCommitOptions,
334 ) -> io::Result<Self> {
335 let wal_path = wal_path.into();
336 let wal = WalWriter::open(&wal_path)?;
337 let initial_durable_lsn = wal.durable_lsn();
338 let initial_current_lsn = wal.current_lsn();
339 let wal = Arc::new(WalMutex::new(wal));
340 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
341 let state = Arc::new((
342 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
343 CommitStateCondvar::new(),
344 ));
345 let fsync_count = Arc::new(AtomicU64::new(0));
346
347 if matches!(
348 mode,
349 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
350 ) {
351 let wal_bg = Arc::clone(&wal);
352 let queue_bg = Arc::clone(&queue);
353 let state_bg = Arc::clone(&state);
354 let fsync_bg = Arc::clone(&fsync_count);
355 let window = Self::resolve_window(&config);
370 let max_statements = config.max_statements.max(1);
371 let max_wal_bytes = config.max_wal_bytes.max(1);
372 std::thread::spawn(move || {
373 Self::run_group_commit_loop(
374 wal_bg,
375 queue_bg,
376 state_bg,
377 fsync_bg,
378 window,
379 max_statements,
380 max_wal_bytes,
381 );
382 });
383 }
384
385 Ok(Self {
386 mode,
387 config,
388 wal_path,
389 wal,
390 queue,
391 state,
392 fsync_count,
393 wal_generation: AtomicU64::new(0),
394 })
395 }
396
397 fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
406 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
407 if let Ok(parsed) = raw.parse::<u64>() {
408 return Duration::from_micros(parsed);
409 }
410 }
411 if config.window_ms != 0 {
412 return Duration::from_millis(config.window_ms);
413 }
414 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
415 }
416
417 #[cfg(test)]
421 pub(crate) fn fsync_count(&self) -> u64 {
422 self.fsync_count.load(Ordering::Relaxed)
423 }
424
425 pub(crate) fn buffered_bytes(&self) -> u64 {
429 self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
430 }
431
432 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
433 self.append_actions_impl(actions, false).map(|_| ())
434 }
435
436 pub(super) fn append_checkpoint_actions(
437 &self,
438 actions: &[StoreWalAction],
439 ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
440 self.append_actions_impl(actions, true)
441 }
442
443 fn append_actions_impl(
444 &self,
445 actions: &[StoreWalAction],
446 retain: bool,
447 ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
448 use super::checkpoint_wal::CheckpointWalBatch;
449 if actions.is_empty() {
450 return Ok(CheckpointWalBatch::default());
451 }
452 let _epoch = self.queue.epoch.read();
453 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
454 let encoded: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
455 let wal_bytes = encoded.iter().fold(0u64, |total, action| {
456 total.saturating_add(action.len() as u64)
457 });
458 let (blob, mut references) = if retain {
459 CheckpointWalBatch::encode(
460 tx_id,
461 &encoded,
462 self.wal_generation.load(Ordering::Acquire),
463 )?
464 } else {
465 (
466 WalRecord::TxCommitBatch {
467 tx_id,
468 actions: encoded,
469 }
470 .encode(),
471 CheckpointWalBatch::default(),
472 )
473 };
474 let start_lsn = if matches!(self.mode, DurabilityMode::Strict) {
475 let start = self.wal.lock().append_bytes(&blob)?;
476 self.force_sync()?;
477 start
478 } else {
479 let length = blob.len() as u64;
480 let end = self.queue.enqueue(blob);
481 self.wait_until_durable(end, wal_bytes)?;
482 end.checked_sub(length).ok_or_else(|| {
483 io::Error::new(io::ErrorKind::InvalidData, "invalid WAL batch offset")
484 })?
485 };
486 references.relocate(start_lsn)?;
487 Ok(references)
488 }
489
490 pub(super) fn checkpoint_row_matches(
493 &self,
494 reference: super::checkpoint_wal::WalRowReference,
495 expected: &[u8],
496 ) -> io::Result<bool> {
497 if expected.len() != reference.length as usize {
498 return Ok(false);
499 }
500 let mut position = 0;
501 let mut matches = true;
502 let present = self.visit_checkpoint_row(reference, |chunk| {
503 matches &= chunk == &expected[position..position + chunk.len()];
504 position += chunk.len();
505 })?;
506 Ok(present && matches)
507 }
508
509 pub(super) fn checkpoint_row_bytes(
511 &self,
512 reference: super::checkpoint_wal::WalRowReference,
513 ) -> io::Result<Vec<u8>> {
514 let mut bytes = Vec::new();
515 let present = self.visit_checkpoint_row(reference, |chunk| {
516 if bytes.is_empty() {
517 bytes.reserve_exact(reference.length as usize);
518 }
519 bytes.extend_from_slice(chunk);
520 })?;
521 if !present {
522 return Err(io::Error::new(
523 io::ErrorKind::InvalidData,
524 "checkpoint WAL reference changed during projection",
525 ));
526 }
527 Ok(bytes)
528 }
529
530 fn visit_checkpoint_row(
531 &self,
532 reference: super::checkpoint_wal::WalRowReference,
533 mut visit: impl FnMut(&[u8]),
534 ) -> io::Result<bool> {
535 use std::io::{Read, Seek, SeekFrom};
536 let _epoch = self.queue.epoch.read_recursive();
537 if reference.generation != self.wal_generation.load(Ordering::Acquire) {
538 return Ok(false);
539 }
540 let end = reference
541 .offset
542 .checked_add(u64::from(reference.length))
543 .ok_or_else(|| {
544 io::Error::new(io::ErrorKind::InvalidData, "WAL proof offset overflow")
545 })?;
546 let (state_lock, _) = &*self.state;
547 if end > state_lock.lock().durable_lsn {
548 return Ok(false);
549 }
550 let mut file = std::fs::File::open(&self.wal_path)?;
551 file.seek(SeekFrom::Start(reference.offset))?;
552 let mut buffer = [0u8; 16 * 1024];
553 let mut checksum = crc32fast::Hasher::new();
554 let mut remaining = reference.length as usize;
555 while remaining != 0 {
556 let length = remaining.min(buffer.len());
557 let chunk = &mut buffer[..length];
558 file.read_exact(chunk)?;
559 checksum.update(chunk);
560 visit(chunk);
561 remaining -= length;
562 }
563 if checksum.finalize() != reference.checksum {
564 return Err(io::Error::new(
565 io::ErrorKind::InvalidData,
566 "checkpoint WAL proof checksum mismatch",
567 ));
568 }
569 Ok(true)
570 }
571
572 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
577 let _epoch = self.queue.epoch.read();
578 let blob = record.encode();
579 let wal_bytes = blob.len() as u64;
580 if matches!(self.mode, DurabilityMode::Strict) {
581 {
582 let mut wal = self.wal.lock();
583 wal.append(&record)?;
584 }
585 self.force_sync()?;
586 return Ok(());
587 }
588 let commit_lsn = self.queue.enqueue(blob);
589 self.wait_until_durable(commit_lsn, wal_bytes)?;
590 Ok(())
591 }
592
593 pub(crate) fn force_sync(&self) -> io::Result<()> {
594 let _epoch = self.queue.epoch.read_recursive();
597 {
598 let mut wal = self.wal.lock();
599 wal.sync()?;
600 self.fsync_count.fetch_add(1, Ordering::Relaxed);
601 let durable = wal.durable_lsn();
602 drop(wal);
603 let (state_lock, cond) = &*self.state;
604 let mut state = state_lock.lock();
605 state.durable_lsn = durable;
606 state.pending_target_lsn = durable.max(state.pending_target_lsn);
607 state.pending_statements = 0;
608 state.pending_wal_bytes = 0;
609 state.first_pending_at = None;
610 state.last_error = None;
611 cond.notify_all();
612 }
613 Ok(())
614 }
615
616 pub(crate) fn truncate(&self) -> io::Result<()> {
617 let _epoch = self.queue.epoch.write();
618 self.wal_generation.fetch_add(1, Ordering::AcqRel);
619 let mut wal = self.wal.lock();
620 wal.truncate()?;
621 let durable = wal.durable_lsn();
622 let current = wal.current_lsn();
623 drop(wal);
624
625 self.queue.reset(current);
630
631 let (state_lock, cond) = &*self.state;
632 let mut state = state_lock.lock();
633 state.durable_lsn = durable;
634 state.pending_target_lsn = durable;
635 state.pending_statements = 0;
636 state.pending_wal_bytes = 0;
637 state.first_pending_at = None;
638 state.last_error = None;
639 cond.notify_all();
640 Ok(())
641 }
642
643 pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
644 if !self.wal_path.exists() {
645 return Ok(());
646 }
647
648 let reader = match WalReader::open(&self.wal_path) {
649 Ok(reader) => reader,
650 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
651 Err(err) => return Err(err),
652 };
653
654 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
655 let mut pending = Vec::<(u64, Vec<u8>)>::new();
656 let mut applied = false;
657
658 for record in reader.iter() {
659 let (_lsn, record) = match record {
660 Ok(record) => record,
661 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
662 Err(err) => return Err(err),
663 };
664 match record {
665 WalRecord::TxCommitBatch { actions, .. } => {
666 for payload in actions {
667 let action = StoreWalAction::decode(&payload)?;
668 applied = true;
669 store.apply_replayed_action(&action, false).map_err(|err| {
670 io::Error::other(format!("failed to replay store wal action: {err}"))
671 })?;
672 }
673 }
674 WalRecord::Begin { tx_id } => {
675 tx_states.insert(tx_id, false);
676 }
677 WalRecord::Commit { tx_id } => {
678 tx_states.insert(tx_id, true);
679 }
680 WalRecord::Rollback { tx_id } => {
681 tx_states.remove(&tx_id);
682 }
683 WalRecord::PageWrite {
684 tx_id,
685 page_id: _,
686 data,
687 } => pending.push((tx_id, data)),
688 WalRecord::Checkpoint { .. } => {}
689 WalRecord::VectorInsert {
690 collection,
691 entity_id,
692 vector,
693 } => {
694 let mut map = store.replayed_turbo_inserts.lock();
700 map.entry(collection).or_default().push((entity_id, vector));
701 }
702 WalRecord::ProbabilisticDelta {
703 kind,
704 operation,
705 name,
706 operands,
707 } => {
708 let mut deltas = store.replayed_probabilistic_deltas.lock();
709 deltas.push((kind, operation, name, operands));
710 }
711 WalRecord::FullPageImage { .. } => {
712 }
714 }
715 }
716
717 for (tx_id, payload) in pending {
718 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
719 continue;
720 }
721 let action = StoreWalAction::decode(&payload)?;
722 applied = true;
723 store.apply_replayed_action(&action, false).map_err(|err| {
724 io::Error::other(format!("failed to replay store wal action: {err}"))
725 })?;
726 }
727
728 if applied {
729 store
730 .reconcile_replayed_btrees()
731 .map_err(|err| io::Error::other(err.to_string()))?;
732 }
733 Ok(())
734 }
735
736 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
737 match self.mode {
738 DurabilityMode::Strict => self.force_sync(),
739 DurabilityMode::Async => {
744 let (state_lock, cond) = &*self.state;
745 let mut state = state_lock.lock();
746 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
747 state.pending_statements = state.pending_statements.saturating_add(1);
748 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
749 state.first_pending_at.get_or_insert_with(Instant::now);
750 cond.notify_all();
751 Ok(())
752 }
753 DurabilityMode::WalDurableGrouped => {
754 let (state_lock, cond) = &*self.state;
755 let mut state = state_lock.lock();
756 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
757 state.pending_statements = state.pending_statements.saturating_add(1);
758 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
759 state.first_pending_at.get_or_insert_with(Instant::now);
760 cond.notify_all();
761
762 loop {
763 if let Some(err) = state.last_error.clone() {
764 return Err(io::Error::other(err));
765 }
766 if state.durable_lsn >= target_lsn {
767 return Ok(());
768 }
769 cond.wait(&mut state);
772 }
773 }
774 }
775 }
776
777 fn run_group_commit_loop(
778 wal: Arc<WalMutex>,
779 queue: Arc<WalAppendQueue>,
780 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
781 fsync_count: Arc<AtomicU64>,
782 window: Duration,
783 max_statements: usize,
784 max_wal_bytes: u64,
785 ) {
786 let (state_lock, cond) = &*state;
787 loop {
788 let target_lsn = {
789 let mut guard = state_lock.lock();
790
791 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
792 cond.wait(&mut guard);
793 }
794
795 if guard.shutdown {
796 return;
797 }
798
799 let immediate = window.is_zero()
800 || guard.pending_statements >= max_statements
801 || guard.pending_wal_bytes >= max_wal_bytes;
802
803 if !immediate {
804 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
805 let now = Instant::now();
806 if now < deadline {
807 let timeout = deadline.saturating_duration_since(now);
808 let _ = cond.wait_for(&mut guard, timeout);
809 if guard.shutdown {
810 return;
811 }
812 if guard.pending_target_lsn <= guard.durable_lsn {
813 continue;
814 }
815 let should_wait_again = guard.pending_statements < max_statements
816 && guard.pending_wal_bytes < max_wal_bytes
817 && guard
818 .first_pending_at
819 .map(|first| first.elapsed() < window)
820 .unwrap_or(false);
821 if should_wait_again {
822 continue;
823 }
824 }
825 }
826
827 guard.pending_target_lsn
828 };
829
830 let _epoch = queue.epoch.read_recursive();
835
836 let batches = queue.drain_sorted();
842
843 let sync_result = {
844 let mut wal = wal.lock();
845 let mut write_err: Option<io::Error> = None;
846 for (_lsn, bytes) in batches {
847 if let Err(e) = wal.append_bytes(&bytes) {
848 write_err = Some(e);
849 break;
850 }
851 }
852 match write_err {
853 Some(e) => Err(e),
854 None => wal.sync().map(|_| {
855 fsync_count.fetch_add(1, Ordering::Relaxed);
860 wal.durable_lsn()
861 }),
862 }
863 };
864
865 let more_pending = queue.has_pending();
870
871 let mut guard = state_lock.lock();
872 match sync_result {
873 Ok(durable_lsn) => {
874 guard.durable_lsn = durable_lsn;
875 if !more_pending {
876 guard.pending_statements = 0;
877 guard.pending_wal_bytes = 0;
878 guard.first_pending_at = None;
879 }
880 guard.last_error = None;
881 let _ = target_lsn;
882 }
883 Err(err) => {
884 guard.last_error = Some(err.to_string());
885 }
886 }
887 cond.notify_all();
888 }
889 }
890}
891
892impl Drop for StoreCommitCoordinator {
893 fn drop(&mut self) {
894 let (state_lock, cond) = &*self.state;
895 let mut state = state_lock.lock();
897 state.shutdown = true;
898 cond.notify_all();
899 }
900}
901
902impl UnifiedStore {
903 pub(crate) fn deferred_store_wal_capture_active() -> bool {
904 deferred_store_wal_capture_active()
905 }
906
907 pub(crate) fn begin_deferred_store_wal_capture() {
908 begin_deferred_store_wal_capture();
909 }
910
911 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
912 take_deferred_store_wal_capture()
913 }
914
915 pub(crate) fn append_deferred_store_wal_actions(
916 &self,
917 actions: DeferredStoreWalActions,
918 ) -> Result<(), StoreError> {
919 if actions.actions.is_empty() {
920 return Ok(());
921 }
922 if self.config.embedded_wal_path.is_some() {
923 return self.append_embedded_store_wal_actions(&actions.actions);
924 }
925 if let Some(commit) = &self.commit {
928 let references = commit
929 .append_checkpoint_actions(&actions.actions)
930 .map_err(StoreError::Io)?;
931 self.remember_checkpoint_rows(actions.actions, references);
932 Ok(())
933 } else {
934 self.flush_paged_state()
935 }
936 }
937
938 pub(crate) fn append_vector_insert_record(
945 &self,
946 collection: &str,
947 entity_id: u64,
948 vector: &[f32],
949 ) -> std::io::Result<()> {
950 let Some(commit) = &self.commit else {
951 return Ok(());
952 };
953 let record = WalRecord::VectorInsert {
954 collection: collection.to_string(),
955 entity_id,
956 vector: vector.to_vec(),
957 };
958 commit.append_single_record(record)
959 }
960
961 pub(crate) fn append_probabilistic_delta_record(
962 &self,
963 kind: u8,
964 operation: u8,
965 name: &str,
966 operands: Vec<Vec<u8>>,
967 ) -> std::io::Result<()> {
968 let Some(commit) = &self.commit else {
969 return Ok(());
970 };
971 let record = WalRecord::ProbabilisticDelta {
972 kind,
973 operation,
974 name: name.to_string(),
975 operands,
976 };
977 commit.append_single_record(record)
978 }
979
980 pub(crate) fn finish_paged_write(
981 &self,
982 actions: impl IntoIterator<Item = StoreWalAction>,
983 ) -> Result<(), StoreError> {
984 self.finish_paged_write_for_projection(actions, false)
985 }
986
987 pub(crate) fn finish_paged_write_for_projection(
988 &self,
989 actions: impl IntoIterator<Item = StoreWalAction>,
990 track: bool,
991 ) -> Result<(), StoreError> {
992 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
993 if deferred_store_wal_capture_active() {
994 let captured = capture_deferred_store_wal_actions(actions);
995 debug_assert!(captured);
996 return Ok(());
997 }
998 if self.config.embedded_wal_path.is_some() {
999 return self.append_embedded_store_wal_actions(&actions);
1000 }
1001 let mut references = super::checkpoint_wal::CheckpointWalBatch::default();
1002 let result = match self.config.durability_mode {
1003 DurabilityMode::Strict => {
1004 if let Some(commit) = &self.commit {
1005 if track {
1006 references = commit
1007 .append_checkpoint_actions(&actions)
1008 .map_err(StoreError::Io)?;
1009 } else {
1010 commit.append_actions(&actions).map_err(StoreError::Io)?;
1011 }
1012 }
1013 self.flush_paged_state()
1014 }
1015 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
1016 if let Some(commit) = &self.commit {
1017 if track {
1018 references = commit
1019 .append_checkpoint_actions(&actions)
1020 .map_err(StoreError::Io)?;
1021 } else {
1022 commit.append_actions(&actions).map_err(StoreError::Io)?;
1023 }
1024 Ok(())
1025 } else {
1026 self.flush_paged_state()
1027 }
1028 }
1029 };
1030 result?;
1031 if track {
1032 self.remember_checkpoint_rows(actions, references);
1033 }
1034 Ok(())
1035 }
1036
1037 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
1038 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
1039 self.apply_replayed_action(&action, true)
1040 }
1041
1042 fn append_embedded_store_wal_actions(
1043 &self,
1044 actions: &[StoreWalAction],
1045 ) -> Result<(), StoreError> {
1046 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1047 return Ok(());
1048 };
1049 if actions.is_empty() {
1050 return Ok(());
1051 }
1052 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1053 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1054 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1055 match self.append_embedded_payloads(path, &payloads) {
1056 Ok(_) => Ok(()),
1057 Err(crate::api::RedDBError::InvalidOperation(msg))
1058 if msg.contains("embedded wal region full") =>
1059 {
1060 self.recover_full_embedded_wal(path, &payloads, encoded_len)
1061 }
1062 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1063 }
1064 }
1065
1066 fn recover_full_embedded_wal(
1067 &self,
1068 path: &Path,
1069 payloads: &[Vec<u8>],
1070 encoded_len: u64,
1071 ) -> Result<(), StoreError> {
1072 let _vault_guard = self.vault_publication_lock.lock();
1073 match self.append_embedded_payloads(path, payloads) {
1077 Ok(_) => return Ok(()),
1078 Err(crate::api::RedDBError::InvalidOperation(message))
1079 if message.contains("embedded wal region full") => {}
1080 Err(error) => return Err(StoreError::Io(std::io::Error::other(error.to_string()))),
1081 }
1082 struct CheckpointFence<'a>(Option<&'a crate::storage::embedded::EmbeddedCommitSession>);
1086 impl Drop for CheckpointFence<'_> {
1087 fn drop(&mut self) {
1088 if let Some(session) = self.0 {
1089 session.fence_interrupted_write();
1090 }
1091 }
1092 }
1093 let mut fence = CheckpointFence(Some(
1094 self.embedded_commit_session
1095 .get()
1096 .expect("WAL-full recovery follows an append attempt"),
1097 ));
1098 let started = Instant::now();
1099 let snapshot = self.to_binary_dump_bytes();
1100 let capacity = encoded_len.max((snapshot.len() as u64).min(EMBEDDED_WAL_HEADROOM_LIMIT));
1101 let checkpoint = crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
1102 path, &snapshot, capacity,
1103 )
1104 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1105 fence.0 = None;
1106 tracing::debug!(
1107 target: "reddb::embedded_checkpoint",
1108 snapshot_bytes = snapshot.len(),
1109 wal_region_bytes = checkpoint.manifest.wal_region_bytes,
1110 elapsed_us = started.elapsed().as_micros() as u64,
1111 "embedded WAL overflow checkpoint"
1112 );
1113 self.append_embedded_payloads(path, payloads)
1114 .map(|_| ())
1115 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1116 }
1117
1118 fn append_embedded_payloads(
1119 &self,
1120 path: &Path,
1121 payloads: &[Vec<u8>],
1122 ) -> crate::RedDBResult<crate::storage::EmbeddedRdbOpen> {
1123 let session = self
1124 .embedded_commit_session
1125 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1126 let options = self.config.group_commit;
1127 let grouped = self.config.durability_mode == DurabilityMode::WalDurableGrouped;
1128 let window = if grouped {
1129 StoreCommitCoordinator::resolve_window(&options)
1130 } else {
1131 Duration::ZERO
1132 };
1133 session.append(
1134 payloads,
1135 reddb_file::embedded::EmbeddedGroupCommitOptions {
1136 window,
1137 max_statements: if grouped { options.max_statements } else { 1 },
1138 max_wal_bytes: options.max_wal_bytes,
1139 },
1140 )
1141 }
1142
1143 pub(super) fn enqueue_grouped_actions(
1144 &self,
1145 actions: &[StoreWalAction],
1146 ) -> Result<Option<crate::storage::embedded::PendingEmbeddedAppend>, StoreError> {
1147 if self.config.durability_mode != DurabilityMode::WalDurableGrouped
1148 || deferred_store_wal_capture_active()
1149 {
1150 return Ok(None);
1151 }
1152 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1153 return Ok(None);
1154 };
1155 let session = self
1156 .embedded_commit_session
1157 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1158 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1159 let options = self.config.group_commit;
1160 session
1161 .enqueue(
1162 &payloads,
1163 reddb_file::embedded::EmbeddedGroupCommitOptions {
1164 window: StoreCommitCoordinator::resolve_window(&options),
1165 max_statements: options.max_statements,
1166 max_wal_bytes: options.max_wal_bytes,
1167 },
1168 )
1169 .map(Some)
1170 .map_err(|error| StoreError::Io(io::Error::other(error.to_string())))
1171 }
1172
1173 pub(super) fn complete_grouped_actions(
1174 &self,
1175 pending: &crate::storage::embedded::PendingEmbeddedAppend,
1176 actions: &[StoreWalAction],
1177 ) -> Result<(), StoreError> {
1178 match pending.wait() {
1179 Ok(_) => Ok(()),
1180 Err(crate::RedDBError::InvalidOperation(message))
1181 if message.contains("embedded wal region full") =>
1182 {
1183 self.append_embedded_store_wal_actions(actions)
1184 }
1185 Err(error) => Err(StoreError::Io(io::Error::other(error.to_string()))),
1186 }
1187 }
1188
1189 pub(crate) fn apply_replayed_action(
1190 &self,
1191 action: &StoreWalAction,
1192 update_btrees: bool,
1193 ) -> Result<(), StoreError> {
1194 reddb_engine::storage::store_wal::apply_replayed_action(
1195 &NativeReplayTarget {
1196 store: self,
1197 update_btrees,
1198 },
1199 action,
1200 )
1201 }
1202
1203 pub fn refresh_collection(
1220 &self,
1221 name: &str,
1222 entities: Vec<UnifiedEntity>,
1223 ) -> Result<Vec<Vec<u8>>, StoreError> {
1224 let fv = self.format_version();
1225
1226 let new_manager = Arc::new(SegmentManager::with_config(
1229 name,
1230 self.config.manager_config.clone(),
1231 ));
1232
1233 let mut prepared = entities;
1234 for entity in &mut prepared {
1235 if entity.id.raw() == 0 {
1236 entity.id = self.next_entity_id();
1237 } else {
1238 self.register_entity_id(entity.id);
1239 }
1240 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1241 if *row_id == 0 {
1242 *row_id = new_manager.next_row_id();
1243 } else {
1244 new_manager.register_row_id(*row_id);
1245 }
1246 }
1247 entity.ensure_table_logical_id();
1248 }
1249
1250 let serialized: Vec<Vec<u8>> = prepared
1251 .iter()
1252 .map(|e| Self::serialize_entity_record(e, None, fv))
1253 .collect();
1254
1255 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1256 StoreError::Io(std::io::Error::other(format!(
1257 "refresh_collection: bulk_insert into new manager failed: {e}"
1258 )))
1259 })?;
1260
1261 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
1262
1263 self.finish_paged_write([StoreWalAction::RefreshCollection {
1264 collection: name.to_string(),
1265 records: serialized.clone(),
1266 }])?;
1267
1268 Ok(serialized)
1269 }
1270
1271 fn swap_collection_state(
1277 &self,
1278 name: &str,
1279 new_manager: Arc<SegmentManager>,
1280 prepared: &[UnifiedEntity],
1281 serialized: &[Vec<u8>],
1282 update_btrees: bool,
1283 ) {
1284 self.resident
1285 .swap_collection_state(name, new_manager, prepared);
1286
1287 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1288 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1289 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1290 .iter()
1291 .zip(serialized.iter())
1292 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1293 .collect();
1294 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1295 if !sorted.is_empty() {
1296 let _ = new_btree.bulk_insert_sorted(&sorted);
1297 }
1298 self.btree_indices
1299 .write()
1300 .insert(name.to_string(), new_btree);
1301 self.mark_paged_registry_dirty();
1302 }
1303 }
1304
1305 fn apply_replayed_refresh_collection(
1306 &self,
1307 collection: &str,
1308 records: &[Vec<u8>],
1309 update_btrees: bool,
1310 ) -> Result<(), StoreError> {
1311 let (new_manager, prepared) = self.resident.prepare_replayed_refresh_collection(
1312 collection,
1313 records,
1314 self.format_version(),
1315 &self.config.manager_config,
1316 )?;
1317 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
1318 Ok(())
1319 }
1320
1321 pub fn refresh_collection_from_records(
1335 &self,
1336 name: &str,
1337 records: Vec<Vec<u8>>,
1338 ) -> Result<(), StoreError> {
1339 self.apply_replayed_refresh_collection(name, &records, true)?;
1340 self.finish_paged_write([StoreWalAction::RefreshCollection {
1341 collection: name.to_string(),
1342 records,
1343 }])?;
1344 Ok(())
1345 }
1346
1347 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1348 self.resident
1349 .create_collection_in_memory(name, &self.config.manager_config)
1350 }
1351
1352 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1353 self.resident.drop_collection_in_memory(name, || {
1354 if let Some(tree) = self.btree_indices.write().remove(name) {
1355 self.retire_btree(tree);
1356 }
1357 })
1358 }
1359
1360 fn apply_replayed_upsert(
1361 &self,
1362 collection: &str,
1363 record: &[u8],
1364 update_btrees: bool,
1365 ) -> Result<(), StoreError> {
1366 let id = self.resident.apply_replayed_upsert(
1367 collection,
1368 record,
1369 self.format_version(),
1370 &self.config.manager_config,
1371 )?;
1372
1373 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1374 let mut btree_indices = self.btree_indices.write();
1375 let btree = btree_indices
1376 .entry(collection.to_string())
1377 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1378 let root_before = btree.root_page_id();
1379 let key = id.raw().to_be_bytes();
1380 match btree.insert(&key, record) {
1381 Ok(_) => {}
1382 Err(BTreeError::DuplicateKey) => {
1383 let _ = btree.delete(&key);
1384 let _ = btree.insert(&key, record);
1385 }
1386 Err(err) => {
1387 return Err(StoreError::Io(io::Error::other(format!(
1388 "replay upsert btree error: {err}"
1389 ))));
1390 }
1391 }
1392 if root_before != btree.root_page_id() {
1393 self.mark_paged_registry_dirty();
1394 }
1395 }
1396
1397 Ok(())
1398 }
1399
1400 fn apply_replayed_delete(
1401 &self,
1402 collection: &str,
1403 id: EntityId,
1404 update_btrees: bool,
1405 ) -> Result<(), StoreError> {
1406 self.resident.apply_replayed_delete(collection, id, || {
1407 if self.pager.is_some() && update_btrees {
1408 let btree_indices = self.btree_indices.read();
1409 if let Some(btree) = btree_indices.get(collection) {
1410 let root_before = btree.root_page_id();
1411 let key = id.raw().to_be_bytes();
1412 let _ = btree.delete(&key);
1413 if root_before != btree.root_page_id() {
1414 self.mark_paged_registry_dirty();
1415 }
1416 }
1417 }
1418 })
1419 }
1420}
1421
1422#[cfg(test)]
1423mod tests {
1424 use super::*;
1425 use crate::api::{DurabilityMode, GroupCommitOptions};
1426 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1427 use std::time::SystemTime;
1428
1429 #[test]
1430 fn logical_wal_replay_restores_a_deleted_physical_identity() {
1431 for flat in [false, true] {
1432 let store = UnifiedStore::new();
1433 store.create_collection("replay").expect("collection");
1434 let id = EntityId::new(42);
1435 let entity = UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(1)]);
1436 if flat {
1437 store
1438 .bulk_insert("replay", vec![entity])
1439 .expect("flat seed");
1440 } else {
1441 store.insert_auto("replay", entity).expect("hash seed");
1442 }
1443 let metadata = Metadata::with_fields(HashMap::from([(
1444 "source".into(),
1445 MetadataValue::String("wal".into()),
1446 )]));
1447 for sequence in 100..104 {
1448 let delete = StoreWalAction::DeleteEntityRecord {
1449 collection: "replay".into(),
1450 entity_id: id.raw(),
1451 };
1452 store
1453 .apply_encoded_store_wal_action(&delete.encode())
1454 .expect("replay delete");
1455 let mut restored =
1456 UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(2)]);
1457 restored.ensure_table_logical_id();
1458 restored.sequence_id = sequence;
1459 let upsert = StoreWalAction::upsert_entity(
1460 "replay",
1461 &restored,
1462 Some(&metadata),
1463 store.format_version(),
1464 );
1465 store
1466 .apply_encoded_store_wal_action(&upsert.encode())
1467 .expect("replay upsert");
1468 let manager = store.get_collection("replay").expect("restored collection");
1469 let stored = manager.get(id).expect("restored entity is readable");
1470 assert_eq!(store.entity_cache_stats().entries, 0);
1471 assert_eq!(manager.count(), 1);
1472 assert_eq!(manager.query_all(|_| true).len(), 1);
1473 assert_eq!(stored.sequence_id, sequence);
1474 assert_eq!(
1475 UnifiedStore::serialize_entity_record(
1476 &stored,
1477 manager.get_metadata(id).as_ref(),
1478 store.format_version()
1479 ),
1480 UnifiedStore::serialize_entity_record(
1481 &restored,
1482 Some(&metadata),
1483 store.format_version()
1484 )
1485 );
1486 assert!(store.get_any(id).is_some());
1487 }
1488 }
1489 }
1490
1491 fn embedded_fixture(
1492 mode: DurabilityMode,
1493 background_bytes: usize,
1494 ) -> (tempfile::TempDir, PathBuf, UnifiedStore) {
1495 let directory = tempfile::tempdir().expect("directory");
1496 let path = directory.path().join("data.rdb");
1497 let mut store =
1498 UnifiedStore::with_config(UnifiedStoreConfig::default().with_durability_mode(mode));
1499 store.create_collection("keys").expect("collection");
1500 store.set_aux_metadata(vec![42; background_bytes]);
1501 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(
1502 &path,
1503 &store.to_binary_dump_bytes(),
1504 )
1505 .expect("snapshot");
1506 store.config.embedded_wal_path = Some(path.clone());
1507 (directory, path, store)
1508 }
1509
1510 #[test]
1511 fn embedded_stale_full_result_does_not_checkpoint_reclaimed_space() {
1512 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
1513 let (_directory, path, store) = embedded_fixture(mode, 0);
1514 store
1515 .append_embedded_store_wal_actions(&[StoreWalAction::CreateCollection {
1516 name: "keys".into(),
1517 }])
1518 .expect("another writer appended after reclaiming space");
1519 let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
1520 let payloads = vec![StoreWalAction::CreateCollection {
1521 name: "after".into(),
1522 }
1523 .encode()];
1524 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1525 .expect("encoded length");
1526 store
1528 .recover_full_embedded_wal(&path, &payloads, length)
1529 .expect("revalidate after acquiring publication");
1530 let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
1531 assert_eq!(
1532 after.selected_superblock.generation,
1533 before.selected_superblock.generation + 1,
1534 "only the retry is published, without an unnecessary checkpoint"
1535 );
1536 assert_eq!(
1537 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&after)
1538 .expect("both appends retained")
1539 .len(),
1540 2
1541 );
1542 }
1543 }
1544
1545 #[test]
1546 fn embedded_full_wal_reserves_bounded_headroom_for_populated_store() {
1547 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
1548 let (_directory, path, store) = embedded_fixture(mode, 2 * 1024 * 1024);
1549 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial");
1550 let action = StoreWalAction::CreateCollection {
1551 name: "keys".into(),
1552 };
1553 let payloads = vec![action.encode()];
1554 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1555 .expect("encoded length");
1556 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(
1557 &path,
1558 &vec![payloads[0].clone(); (initial.manifest.wal_region_bytes / length) as usize],
1559 )
1560 .expect("fill the initial region with valid records");
1561 store
1562 .append_embedded_store_wal_actions(&[action])
1563 .expect("overflow");
1564 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("checkpoint");
1565 assert_eq!(open.manifest.wal_region_bytes, 1024 * 1024);
1566 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
1567 .expect("snapshot")
1568 .expect("populated image");
1569 let recovered =
1570 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
1571 .expect("recover snapshot");
1572 for payload in
1573 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL")
1574 {
1575 recovered
1576 .apply_encoded_store_wal_action(&payload)
1577 .expect("replay");
1578 }
1579 assert_eq!(
1580 recovered.resident.aux_metadata.read().len(),
1581 2 * 1024 * 1024
1582 );
1583 assert!(recovered.get_collection("keys").is_some());
1584 }
1585 }
1586
1587 #[test]
1588 fn embedded_grouped_overwrites_recover_across_repeated_full_wal_regions() {
1589 fn row(writer: usize, round: usize) -> UnifiedEntity {
1590 let mut state = (writer as u64 + 1) * 31 + round as u64 * 13;
1591 let payload: String = (0..4096)
1592 .map(|_| {
1593 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1594 char::from(b'!' + ((state >> 24) % 90) as u8)
1595 })
1596 .collect();
1597 let mut entity = UnifiedEntity::table_row(EntityId::new(0), "keys", 0, vec![]);
1598 entity.data = EntityData::Row(RowData::with_names(
1599 vec![
1600 Value::text(format!("k{writer}")),
1601 Value::text(format!("{round}:{payload}")),
1602 ],
1603 vec!["key".into(), "value".into()],
1604 ));
1605 entity
1606 }
1607 let (_directory, path, store) =
1608 embedded_fixture(DurabilityMode::WalDurableGrouped, 2 * 1024 * 1024);
1609 assert!(
1610 StoreWalAction::upsert_entity("keys", &row(0, 0), None, store.format_version())
1611 .encode()
1612 .len()
1613 >= 2048,
1614 "fixture must exert physical WAL pressure after value compression"
1615 );
1616 let mut ids: Vec<_> = (0..8)
1617 .map(|writer| {
1618 store
1619 .write_keyed_row("keys", row(writer, 0), None, None)
1620 .expect("seed")
1621 })
1622 .collect();
1623 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial roots");
1624 let mut previous_snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&initial)
1625 .expect("initial snapshot")
1626 .expect("image");
1627 for wave in 0..3 {
1631 let barrier = Barrier::new(ids.len());
1632 ids = std::thread::scope(|scope| {
1633 let workers: Vec<_> = ids
1634 .iter()
1635 .enumerate()
1636 .map(|(writer, previous)| {
1637 let store = &store;
1638 let barrier = &barrier;
1639 scope.spawn(move || {
1640 barrier.wait();
1641 let mut id = *previous;
1642 for round in 0..64 {
1643 id = store
1644 .write_keyed_row(
1645 "keys",
1646 row(writer, wave * 64 + round),
1647 None,
1648 Some(id),
1649 )
1650 .expect("durable overwrite");
1651 }
1652 id
1653 })
1654 })
1655 .collect();
1656 workers
1657 .into_iter()
1658 .map(|worker| worker.join().expect("writer"))
1659 .collect()
1660 });
1661 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("wave roots");
1662 assert_eq!(open.manifest.wal_region_bytes, EMBEDDED_WAL_HEADROOM_LIMIT);
1663 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
1664 .expect("wave snapshot")
1665 .expect("image");
1666 assert!(snapshot != previous_snapshot, "wave {wave} must checkpoint");
1667 previous_snapshot = snapshot;
1668 }
1669 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("recover roots");
1670 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
1671 .expect("snapshot")
1672 .expect("image");
1673 let recovered =
1674 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
1675 .expect("snapshot replay");
1676 for payload in crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL") {
1677 recovered
1678 .apply_encoded_store_wal_action(&payload)
1679 .expect("WAL replay");
1680 }
1681 assert_eq!(recovered.get_collection("keys").expect("keys").count(), 8);
1682 for (writer, id) in ids.iter().enumerate() {
1683 let entity = recovered
1684 .get("keys", *id)
1685 .expect("last acknowledged version");
1686 assert_eq!(
1687 entity.data.as_row().expect("row").get_field("value"),
1688 row(writer, 191)
1689 .data
1690 .as_row()
1691 .expect("expected row")
1692 .get_field("value")
1693 );
1694 }
1695 }
1696
1697 fn env_lock() -> &'static StdMutex<()> {
1702 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
1703 LOCK.get_or_init(|| StdMutex::new(()))
1704 }
1705
1706 fn temp_wal(name: &str) -> PathBuf {
1707 let nanos = SystemTime::now()
1708 .duration_since(std::time::UNIX_EPOCH)
1709 .unwrap()
1710 .as_nanos();
1711 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
1712 &std::env::temp_dir(),
1713 name,
1714 std::process::id(),
1715 nanos,
1716 );
1717 let _ = std::fs::remove_file(&path);
1718 path
1719 }
1720
1721 fn replay_crash_payload(id: u64, round: u64) -> String {
1722 let mut state = id * 31 + round * 13 + 1;
1723 (0..600)
1724 .map(|_| {
1725 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1726 char::from(b'!' + ((state >> 24) % 90) as u8)
1727 })
1728 .collect()
1729 }
1730
1731 #[test]
1732 fn paged_wal_replay_survives_immediate_process_exit() {
1733 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
1734 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
1735 if let Some(path) = std::env::var_os(CHILD_PATH) {
1736 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
1737 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
1738 std::process::exit(0);
1740 }
1741 let directory = tempfile::tempdir().unwrap();
1742 let path = directory.path().join("data.rdb");
1743 {
1744 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
1745 store.create_collection("rows").unwrap();
1746 for id in 1..=400 {
1747 store
1748 .insert(
1749 "rows",
1750 UnifiedEntity::table_row(
1751 EntityId::new(id),
1752 "rows",
1753 id,
1754 vec![Value::text(format!("initial-{id}"))],
1755 ),
1756 )
1757 .unwrap();
1758 }
1759 store.persist().unwrap();
1760 }
1761 {
1762 let coordinator = StoreCommitCoordinator::open(
1763 reddb_file::layout::unified_wal_path(&path),
1764 DurabilityMode::Strict,
1765 GroupCommitOptions::default(),
1766 )
1767 .unwrap();
1768 for round in 0..3 {
1769 let actions = (1..=400)
1770 .map(|id| {
1771 let payload = replay_crash_payload(id, round);
1772 let entity = UnifiedEntity::table_row(
1773 EntityId::new(id),
1774 "rows",
1775 id,
1776 vec![Value::text(payload)],
1777 );
1778 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
1779 })
1780 .collect::<Vec<_>>();
1781 coordinator.append_actions(&actions).unwrap();
1782 }
1783 }
1784 let status = std::process::Command::new(std::env::current_exe().unwrap())
1785 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
1786 .env(CHILD_PATH, &path).status().unwrap();
1787 assert!(status.success(), "replay process failed");
1788 let reopened = UnifiedStore::open_with_config(&path, config())
1789 .expect("cold reopen after replay crash");
1790 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
1791 for id in 1..=400 {
1792 let entity = reopened
1793 .get("rows", EntityId::new(id))
1794 .expect("retained entity");
1795 let EntityData::Row(row) = entity.data else {
1796 panic!("expected row")
1797 };
1798 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
1799 }
1800 drop(reopened);
1801 let stable_bytes = std::fs::metadata(&path).unwrap().len();
1802 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
1803 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
1804 assert_eq!(
1805 std::fs::metadata(&path).unwrap().len(),
1806 stable_bytes,
1807 "idempotent replay must not allocate another tree generation"
1808 );
1809 }
1810
1811 #[test]
1812 fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
1813 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1814 let path = temp_wal("checkpoint-ack");
1815 let coordinator = Arc::new(
1816 StoreCommitCoordinator::open(
1817 path.clone(),
1818 DurabilityMode::WalDurableGrouped,
1819 GroupCommitOptions::default(),
1820 )
1821 .unwrap(),
1822 );
1823 let state = coordinator.state.0.lock();
1826 let (written_tx, written_rx) = std::sync::mpsc::channel();
1827 let writer_coordinator = Arc::clone(&coordinator);
1828 let writer = std::thread::spawn(move || {
1829 let result = writer_coordinator.append_actions(&[StoreWalAction::CreateCollection {
1830 name: "acknowledged".into(),
1831 }]);
1832 written_tx.send(result).unwrap();
1833 });
1834 let deadline = Instant::now() + Duration::from_secs(2);
1835 while !coordinator.queue.has_pending() {
1836 assert!(Instant::now() < deadline, "writer never enqueued");
1837 std::thread::yield_now();
1838 }
1839 let (started_tx, started_rx) = std::sync::mpsc::channel();
1840 let (reset_tx, reset_rx) = std::sync::mpsc::channel();
1841 let reset_coordinator = Arc::clone(&coordinator);
1842 let reset = std::thread::spawn(move || {
1843 started_tx.send(()).unwrap();
1844 reset_tx.send(reset_coordinator.truncate()).unwrap();
1845 });
1846 started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1847 let deadline = Instant::now() + Duration::from_millis(100);
1848 while coordinator.queue.has_pending() && Instant::now() < deadline {
1849 std::thread::sleep(Duration::from_millis(1));
1850 }
1851 let discarded_before_ack = !coordinator.queue.has_pending();
1852 drop(state);
1853 let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
1854 if acknowledged.is_err() {
1855 let mut state = coordinator.state.0.lock();
1858 state.shutdown = true;
1859 state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
1860 coordinator.state.1.notify_all();
1861 }
1862 writer.join().unwrap();
1863 reset_rx
1864 .recv_timeout(Duration::from_secs(2))
1865 .unwrap()
1866 .unwrap();
1867 reset.join().unwrap();
1868 assert!(
1869 !discarded_before_ack,
1870 "checkpoint discarded an unacknowledged WAL record"
1871 );
1872 assert!(
1873 acknowledged.unwrap().is_ok(),
1874 "durable writer did not finish"
1875 );
1876 drop(coordinator);
1877 let _ = std::fs::remove_file(path);
1878 }
1879
1880 #[test]
1891 fn group_commit_coalesces_concurrent_autocommits() {
1892 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1893 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1896
1897 let path = temp_wal("coalesce");
1898 let coord = Arc::new(
1899 StoreCommitCoordinator::open(
1900 path.clone(),
1901 DurabilityMode::WalDurableGrouped,
1902 GroupCommitOptions::default(),
1903 )
1904 .expect("open commit coordinator"),
1905 );
1906
1907 const WRITERS: usize = 32;
1908 let barrier = Arc::new(Barrier::new(WRITERS));
1909 let mut handles = Vec::with_capacity(WRITERS);
1910 for tx in 0..WRITERS {
1911 let coord_c = Arc::clone(&coord);
1912 let barrier_c = Arc::clone(&barrier);
1913 handles.push(std::thread::spawn(move || {
1914 barrier_c.wait();
1918 let action = StoreWalAction::CreateCollection {
1919 name: format!("c{tx}"),
1920 };
1921 coord_c
1922 .append_actions(std::slice::from_ref(&action))
1923 .expect("append_actions");
1924 }));
1925 }
1926 for h in handles {
1927 h.join().expect("writer thread");
1928 }
1929
1930 let fsyncs = coord.fsync_count();
1931 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
1932 assert!(
1933 fsyncs < WRITERS as u64,
1934 "expected fsyncs ({fsyncs}) to be strictly less than \
1935 concurrent writers ({WRITERS}); coalescing failed"
1936 );
1937
1938 drop(coord);
1939 let _ = std::fs::remove_file(&path);
1940 }
1941
1942 #[test]
1948 fn zero_window_disables_coalescing_floor() {
1949 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1950 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1951
1952 let path = temp_wal("zero_window");
1953 let coord = Arc::new(
1954 StoreCommitCoordinator::open(
1955 path.clone(),
1956 DurabilityMode::WalDurableGrouped,
1957 GroupCommitOptions::default(),
1958 )
1959 .expect("open commit coordinator"),
1960 );
1961
1962 const WRITERS: usize = 8;
1963 let barrier = Arc::new(Barrier::new(WRITERS));
1964 let mut handles = Vec::with_capacity(WRITERS);
1965 for tx in 0..WRITERS {
1966 let coord_c = Arc::clone(&coord);
1967 let barrier_c = Arc::clone(&barrier);
1968 handles.push(std::thread::spawn(move || {
1969 barrier_c.wait();
1970 let action = StoreWalAction::CreateCollection {
1971 name: format!("z{tx}"),
1972 };
1973 coord_c
1974 .append_actions(std::slice::from_ref(&action))
1975 .expect("append_actions");
1976 }));
1977 }
1978 for h in handles {
1979 h.join().expect("writer thread");
1980 }
1981
1982 let fsyncs = coord.fsync_count();
1988 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
1989
1990 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1991 drop(coord);
1992 let _ = std::fs::remove_file(&path);
1993 }
1994
1995 #[test]
1997 fn resolve_window_precedence() {
1998 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1999 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
2001 let cfg = GroupCommitOptions::default();
2002 assert_eq!(
2003 StoreCommitCoordinator::resolve_window(&cfg),
2004 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
2005 );
2006
2007 let cfg_ms = GroupCommitOptions {
2009 window_ms: 5,
2010 ..GroupCommitOptions::default()
2011 };
2012 assert_eq!(
2013 StoreCommitCoordinator::resolve_window(&cfg_ms),
2014 Duration::from_millis(5)
2015 );
2016
2017 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
2019 assert_eq!(
2020 StoreCommitCoordinator::resolve_window(&cfg),
2021 Duration::from_micros(750)
2022 );
2023 assert_eq!(
2024 StoreCommitCoordinator::resolve_window(&cfg_ms),
2025 Duration::from_micros(750)
2026 );
2027
2028 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
2030 assert_eq!(
2031 StoreCommitCoordinator::resolve_window(&cfg),
2032 Duration::from_micros(0)
2033 );
2034
2035 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
2036 }
2037}