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