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}
429
430impl StoreCommitCoordinator {
431 pub(crate) fn open(
432 wal_path: impl Into<PathBuf>,
433 mode: DurabilityMode,
434 config: crate::api::GroupCommitOptions,
435 ) -> io::Result<Self> {
436 let wal_path = wal_path.into();
437 let wal = WalWriter::open(&wal_path)?;
438 let initial_durable_lsn = wal.durable_lsn();
439 let initial_current_lsn = wal.current_lsn();
440 let wal = Arc::new(WalMutex::new(wal));
441 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
442 let state = Arc::new((
443 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
444 CommitStateCondvar::new(),
445 ));
446 let fsync_count = Arc::new(AtomicU64::new(0));
447
448 if matches!(
449 mode,
450 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
451 ) {
452 let wal_bg = Arc::clone(&wal);
453 let queue_bg = Arc::clone(&queue);
454 let state_bg = Arc::clone(&state);
455 let fsync_bg = Arc::clone(&fsync_count);
456 let window = Self::resolve_window(&config);
471 let max_statements = config.max_statements.max(1);
472 let max_wal_bytes = config.max_wal_bytes.max(1);
473 std::thread::spawn(move || {
474 Self::run_group_commit_loop(
475 wal_bg,
476 queue_bg,
477 state_bg,
478 fsync_bg,
479 window,
480 max_statements,
481 max_wal_bytes,
482 );
483 });
484 }
485
486 Ok(Self {
487 mode,
488 config,
489 wal_path,
490 wal,
491 queue,
492 state,
493 fsync_count,
494 })
495 }
496
497 fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
506 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
507 if let Ok(parsed) = raw.parse::<u64>() {
508 return Duration::from_micros(parsed);
509 }
510 }
511 if config.window_ms != 0 {
512 return Duration::from_millis(config.window_ms);
513 }
514 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
515 }
516
517 #[cfg(test)]
521 pub(crate) fn fsync_count(&self) -> u64 {
522 self.fsync_count.load(Ordering::Relaxed)
523 }
524
525 pub(crate) fn buffered_bytes(&self) -> u64 {
529 self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
530 }
531
532 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
533 if actions.is_empty() {
534 return Ok(());
535 }
536 let _epoch = self.queue.epoch.read();
537
538 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
539
540 if matches!(self.mode, DurabilityMode::Strict) {
545 let commit_lsn = {
546 let mut wal = self.wal.lock();
547 wal.append(&WalRecord::TxCommitBatch {
548 tx_id,
549 actions: actions.iter().map(StoreWalAction::encode).collect(),
550 })?;
551 wal.current_lsn()
552 };
553 self.force_sync()?;
554 let _ = commit_lsn;
555 return Ok(());
556 }
557
558 let encoded_actions: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
562 let wal_bytes = encoded_actions.iter().fold(0u64, |total, payload| {
563 total.saturating_add(payload.len() as u64)
564 });
565 let blob = WalRecord::TxCommitBatch {
566 tx_id,
567 actions: encoded_actions,
568 }
569 .encode();
570
571 let commit_lsn = self.queue.enqueue(blob);
572 self.wait_until_durable(commit_lsn, wal_bytes)?;
573 Ok(())
574 }
575
576 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
581 let _epoch = self.queue.epoch.read();
582 let blob = record.encode();
583 let wal_bytes = blob.len() as u64;
584 if matches!(self.mode, DurabilityMode::Strict) {
585 {
586 let mut wal = self.wal.lock();
587 wal.append(&record)?;
588 }
589 self.force_sync()?;
590 return Ok(());
591 }
592 let commit_lsn = self.queue.enqueue(blob);
593 self.wait_until_durable(commit_lsn, wal_bytes)?;
594 Ok(())
595 }
596
597 pub(crate) fn force_sync(&self) -> io::Result<()> {
598 let _epoch = self.queue.epoch.read_recursive();
601 {
602 let mut wal = self.wal.lock();
603 wal.sync()?;
604 self.fsync_count.fetch_add(1, Ordering::Relaxed);
605 let durable = wal.durable_lsn();
606 drop(wal);
607 let (state_lock, cond) = &*self.state;
608 let mut state = state_lock.lock();
609 state.durable_lsn = durable;
610 state.pending_target_lsn = durable.max(state.pending_target_lsn);
611 state.pending_statements = 0;
612 state.pending_wal_bytes = 0;
613 state.first_pending_at = None;
614 state.last_error = None;
615 cond.notify_all();
616 }
617 Ok(())
618 }
619
620 pub(crate) fn truncate(&self) -> io::Result<()> {
621 let _epoch = self.queue.epoch.write();
622 let mut wal = self.wal.lock();
623 wal.truncate()?;
624 let durable = wal.durable_lsn();
625 let current = wal.current_lsn();
626 drop(wal);
627
628 self.queue.reset(current);
633
634 let (state_lock, cond) = &*self.state;
635 let mut state = state_lock.lock();
636 state.durable_lsn = durable;
637 state.pending_target_lsn = durable;
638 state.pending_statements = 0;
639 state.pending_wal_bytes = 0;
640 state.first_pending_at = None;
641 state.last_error = None;
642 cond.notify_all();
643 Ok(())
644 }
645
646 pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
647 if !self.wal_path.exists() {
648 return Ok(());
649 }
650
651 let reader = match WalReader::open(&self.wal_path) {
652 Ok(reader) => reader,
653 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
654 Err(err) => return Err(err),
655 };
656
657 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
658 let mut pending = Vec::<(u64, Vec<u8>)>::new();
659 let mut applied = false;
660
661 for record in reader.iter() {
662 let (_lsn, record) = match record {
663 Ok(record) => record,
664 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
665 Err(err) => return Err(err),
666 };
667 match record {
668 WalRecord::TxCommitBatch { actions, .. } => {
669 for payload in actions {
670 let action = StoreWalAction::decode(&payload)?;
671 applied = true;
672 store.apply_replayed_action(&action, false).map_err(|err| {
673 io::Error::other(format!("failed to replay store wal action: {err}"))
674 })?;
675 }
676 }
677 WalRecord::Begin { tx_id } => {
678 tx_states.insert(tx_id, false);
679 }
680 WalRecord::Commit { tx_id } => {
681 tx_states.insert(tx_id, true);
682 }
683 WalRecord::Rollback { tx_id } => {
684 tx_states.remove(&tx_id);
685 }
686 WalRecord::PageWrite {
687 tx_id,
688 page_id: _,
689 data,
690 } => pending.push((tx_id, data)),
691 WalRecord::Checkpoint { .. } => {}
692 WalRecord::VectorInsert {
693 collection,
694 entity_id,
695 vector,
696 } => {
697 let mut map = store.replayed_turbo_inserts.lock();
703 map.entry(collection).or_default().push((entity_id, vector));
704 }
705 WalRecord::ProbabilisticDelta {
706 kind,
707 operation,
708 name,
709 operands,
710 } => {
711 let mut deltas = store.replayed_probabilistic_deltas.lock();
712 deltas.push((kind, operation, name, operands));
713 }
714 WalRecord::FullPageImage { .. } => {
715 }
717 }
718 }
719
720 for (tx_id, payload) in pending {
721 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
722 continue;
723 }
724 let action = StoreWalAction::decode(&payload)?;
725 applied = true;
726 store.apply_replayed_action(&action, false).map_err(|err| {
727 io::Error::other(format!("failed to replay store wal action: {err}"))
728 })?;
729 }
730
731 if applied {
732 store
733 .reconcile_replayed_btrees()
734 .map_err(|err| io::Error::other(err.to_string()))?;
735 }
736 Ok(())
737 }
738
739 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
740 match self.mode {
741 DurabilityMode::Strict => self.force_sync(),
742 DurabilityMode::Async => {
747 let (state_lock, cond) = &*self.state;
748 let mut state = state_lock.lock();
749 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
750 state.pending_statements = state.pending_statements.saturating_add(1);
751 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
752 state.first_pending_at.get_or_insert_with(Instant::now);
753 cond.notify_all();
754 Ok(())
755 }
756 DurabilityMode::WalDurableGrouped => {
757 let (state_lock, cond) = &*self.state;
758 let mut state = state_lock.lock();
759 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
760 state.pending_statements = state.pending_statements.saturating_add(1);
761 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
762 state.first_pending_at.get_or_insert_with(Instant::now);
763 cond.notify_all();
764
765 loop {
766 if let Some(err) = state.last_error.clone() {
767 return Err(io::Error::other(err));
768 }
769 if state.durable_lsn >= target_lsn {
770 return Ok(());
771 }
772 cond.wait(&mut state);
775 }
776 }
777 }
778 }
779
780 fn run_group_commit_loop(
781 wal: Arc<WalMutex>,
782 queue: Arc<WalAppendQueue>,
783 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
784 fsync_count: Arc<AtomicU64>,
785 window: Duration,
786 max_statements: usize,
787 max_wal_bytes: u64,
788 ) {
789 let (state_lock, cond) = &*state;
790 loop {
791 let target_lsn = {
792 let mut guard = state_lock.lock();
793
794 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
795 cond.wait(&mut guard);
796 }
797
798 if guard.shutdown {
799 return;
800 }
801
802 let immediate = window.is_zero()
803 || guard.pending_statements >= max_statements
804 || guard.pending_wal_bytes >= max_wal_bytes;
805
806 if !immediate {
807 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
808 let now = Instant::now();
809 if now < deadline {
810 let timeout = deadline.saturating_duration_since(now);
811 let _ = cond.wait_for(&mut guard, timeout);
812 if guard.shutdown {
813 return;
814 }
815 if guard.pending_target_lsn <= guard.durable_lsn {
816 continue;
817 }
818 let should_wait_again = guard.pending_statements < max_statements
819 && guard.pending_wal_bytes < max_wal_bytes
820 && guard
821 .first_pending_at
822 .map(|first| first.elapsed() < window)
823 .unwrap_or(false);
824 if should_wait_again {
825 continue;
826 }
827 }
828 }
829
830 guard.pending_target_lsn
831 };
832
833 let _epoch = queue.epoch.read_recursive();
838
839 let batches = queue.drain_sorted();
845
846 let sync_result = {
847 let mut wal = wal.lock();
848 let mut write_err: Option<io::Error> = None;
849 for (_lsn, bytes) in batches {
850 if let Err(e) = wal.append_bytes(&bytes) {
851 write_err = Some(e);
852 break;
853 }
854 }
855 match write_err {
856 Some(e) => Err(e),
857 None => wal.sync().map(|_| {
858 fsync_count.fetch_add(1, Ordering::Relaxed);
863 wal.durable_lsn()
864 }),
865 }
866 };
867
868 let more_pending = queue.has_pending();
873
874 let mut guard = state_lock.lock();
875 match sync_result {
876 Ok(durable_lsn) => {
877 guard.durable_lsn = durable_lsn;
878 if !more_pending {
879 guard.pending_statements = 0;
880 guard.pending_wal_bytes = 0;
881 guard.first_pending_at = None;
882 }
883 guard.last_error = None;
884 let _ = target_lsn;
885 }
886 Err(err) => {
887 guard.last_error = Some(err.to_string());
888 }
889 }
890 cond.notify_all();
891 }
892 }
893}
894
895impl Drop for StoreCommitCoordinator {
896 fn drop(&mut self) {
897 let (state_lock, cond) = &*self.state;
898 let mut state = state_lock.lock();
900 state.shutdown = true;
901 cond.notify_all();
902 }
903}
904
905impl UnifiedStore {
906 pub(crate) fn deferred_store_wal_capture_active() -> bool {
907 deferred_store_wal_capture_active()
908 }
909
910 pub(crate) fn begin_deferred_store_wal_capture() {
911 begin_deferred_store_wal_capture();
912 }
913
914 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
915 take_deferred_store_wal_capture()
916 }
917
918 pub(crate) fn append_deferred_store_wal_actions(
919 &self,
920 actions: DeferredStoreWalActions,
921 ) -> Result<(), StoreError> {
922 if actions.actions.is_empty() {
923 return Ok(());
924 }
925 if self.config.embedded_wal_path.is_some() {
926 return self.append_embedded_store_wal_actions(&actions.actions);
927 }
928 if let Some(commit) = &self.commit {
931 commit
932 .append_actions(&actions.actions)
933 .map_err(StoreError::Io)?;
934 self.remember_checkpoint_rows(actions.actions);
935 Ok(())
936 } else {
937 self.flush_paged_state()
938 }
939 }
940
941 pub(crate) fn append_vector_insert_record(
948 &self,
949 collection: &str,
950 entity_id: u64,
951 vector: &[f32],
952 ) -> std::io::Result<()> {
953 let Some(commit) = &self.commit else {
954 return Ok(());
955 };
956 let record = WalRecord::VectorInsert {
957 collection: collection.to_string(),
958 entity_id,
959 vector: vector.to_vec(),
960 };
961 commit.append_single_record(record)
962 }
963
964 pub(crate) fn append_probabilistic_delta_record(
965 &self,
966 kind: u8,
967 operation: u8,
968 name: &str,
969 operands: Vec<Vec<u8>>,
970 ) -> std::io::Result<()> {
971 let Some(commit) = &self.commit else {
972 return Ok(());
973 };
974 let record = WalRecord::ProbabilisticDelta {
975 kind,
976 operation,
977 name: name.to_string(),
978 operands,
979 };
980 commit.append_single_record(record)
981 }
982
983 pub(crate) fn finish_paged_write(
984 &self,
985 actions: impl IntoIterator<Item = StoreWalAction>,
986 ) -> Result<(), StoreError> {
987 self.finish_paged_write_for_projection(actions, false)
988 }
989
990 pub(crate) fn finish_paged_write_for_projection(
991 &self,
992 actions: impl IntoIterator<Item = StoreWalAction>,
993 track: bool,
994 ) -> Result<(), StoreError> {
995 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
996 if deferred_store_wal_capture_active() {
997 let captured = capture_deferred_store_wal_actions(actions);
998 debug_assert!(captured);
999 return Ok(());
1000 }
1001 if self.config.embedded_wal_path.is_some() {
1002 return self.append_embedded_store_wal_actions(&actions);
1003 }
1004 let result = match self.config.durability_mode {
1005 DurabilityMode::Strict => {
1006 if let Some(commit) = &self.commit {
1007 commit.append_actions(&actions).map_err(StoreError::Io)?;
1008 }
1009 self.flush_paged_state()
1010 }
1011 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
1012 if let Some(commit) = &self.commit {
1013 commit.append_actions(&actions).map_err(StoreError::Io)?;
1014 Ok(())
1015 } else {
1016 self.flush_paged_state()
1017 }
1018 }
1019 };
1020 result?;
1021 if track {
1022 self.remember_checkpoint_rows(actions);
1023 }
1024 Ok(())
1025 }
1026
1027 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
1028 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
1029 self.apply_replayed_action(&action, true)
1030 }
1031
1032 fn append_embedded_store_wal_actions(
1033 &self,
1034 actions: &[StoreWalAction],
1035 ) -> Result<(), StoreError> {
1036 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1037 return Ok(());
1038 };
1039 if actions.is_empty() {
1040 return Ok(());
1041 }
1042 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1043 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1044 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1045 match self.append_embedded_payloads(path, &payloads) {
1046 Ok(_) => Ok(()),
1047 Err(crate::api::RedDBError::InvalidOperation(msg))
1048 if msg.contains("embedded wal region full") =>
1049 {
1050 self.recover_full_embedded_wal(path, &payloads, encoded_len)
1051 }
1052 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1053 }
1054 }
1055
1056 fn recover_full_embedded_wal(
1057 &self,
1058 path: &Path,
1059 payloads: &[Vec<u8>],
1060 encoded_len: u64,
1061 ) -> Result<(), StoreError> {
1062 let _vault_guard = self.vault_publication_lock.lock();
1063 match self.append_embedded_payloads(path, payloads) {
1067 Ok(_) => return Ok(()),
1068 Err(crate::api::RedDBError::InvalidOperation(message))
1069 if message.contains("embedded wal region full") => {}
1070 Err(error) => return Err(StoreError::Io(std::io::Error::other(error.to_string()))),
1071 }
1072 struct CheckpointFence<'a>(Option<&'a crate::storage::embedded::EmbeddedCommitSession>);
1076 impl Drop for CheckpointFence<'_> {
1077 fn drop(&mut self) {
1078 if let Some(session) = self.0 {
1079 session.fence_interrupted_write();
1080 }
1081 }
1082 }
1083 let mut fence = CheckpointFence(Some(
1084 self.embedded_commit_session
1085 .get()
1086 .expect("WAL-full recovery follows an append attempt"),
1087 ));
1088 let started = Instant::now();
1089 let snapshot = self.to_binary_dump_bytes();
1090 let capacity = encoded_len.max((snapshot.len() as u64).min(EMBEDDED_WAL_HEADROOM_LIMIT));
1091 let checkpoint = crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
1092 path, &snapshot, capacity,
1093 )
1094 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1095 fence.0 = None;
1096 tracing::debug!(
1097 target: "reddb::embedded_checkpoint",
1098 snapshot_bytes = snapshot.len(),
1099 wal_region_bytes = checkpoint.manifest.wal_region_bytes,
1100 elapsed_us = started.elapsed().as_micros() as u64,
1101 "embedded WAL overflow checkpoint"
1102 );
1103 self.append_embedded_payloads(path, payloads)
1104 .map(|_| ())
1105 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1106 }
1107
1108 fn append_embedded_payloads(
1109 &self,
1110 path: &Path,
1111 payloads: &[Vec<u8>],
1112 ) -> crate::RedDBResult<crate::storage::EmbeddedRdbOpen> {
1113 let session = self
1114 .embedded_commit_session
1115 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1116 let options = self.config.group_commit;
1117 let grouped = self.config.durability_mode == DurabilityMode::WalDurableGrouped;
1118 let window = if grouped {
1119 StoreCommitCoordinator::resolve_window(&options)
1120 } else {
1121 Duration::ZERO
1122 };
1123 session.append(
1124 payloads,
1125 reddb_file::embedded::EmbeddedGroupCommitOptions {
1126 window,
1127 max_statements: if grouped { options.max_statements } else { 1 },
1128 max_wal_bytes: options.max_wal_bytes,
1129 },
1130 )
1131 }
1132
1133 pub(super) fn enqueue_grouped_actions(
1134 &self,
1135 actions: &[StoreWalAction],
1136 ) -> Result<Option<crate::storage::embedded::PendingEmbeddedAppend>, StoreError> {
1137 if self.config.durability_mode != DurabilityMode::WalDurableGrouped
1138 || deferred_store_wal_capture_active()
1139 {
1140 return Ok(None);
1141 }
1142 let Some(path) = self.config.embedded_wal_path.as_deref() else {
1143 return Ok(None);
1144 };
1145 let session = self
1146 .embedded_commit_session
1147 .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1148 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1149 let options = self.config.group_commit;
1150 session
1151 .enqueue(
1152 &payloads,
1153 reddb_file::embedded::EmbeddedGroupCommitOptions {
1154 window: StoreCommitCoordinator::resolve_window(&options),
1155 max_statements: options.max_statements,
1156 max_wal_bytes: options.max_wal_bytes,
1157 },
1158 )
1159 .map(Some)
1160 .map_err(|error| StoreError::Io(io::Error::other(error.to_string())))
1161 }
1162
1163 pub(super) fn complete_grouped_actions(
1164 &self,
1165 pending: &crate::storage::embedded::PendingEmbeddedAppend,
1166 actions: &[StoreWalAction],
1167 ) -> Result<(), StoreError> {
1168 match pending.wait() {
1169 Ok(_) => Ok(()),
1170 Err(crate::RedDBError::InvalidOperation(message))
1171 if message.contains("embedded wal region full") =>
1172 {
1173 self.append_embedded_store_wal_actions(actions)
1174 }
1175 Err(error) => Err(StoreError::Io(io::Error::other(error.to_string()))),
1176 }
1177 }
1178
1179 pub(crate) fn apply_replayed_action(
1180 &self,
1181 action: &StoreWalAction,
1182 update_btrees: bool,
1183 ) -> Result<(), StoreError> {
1184 match action {
1185 StoreWalAction::CreateCollection { name } => {
1186 if self.get_collection(name).is_none() {
1187 let _ = self.create_collection_in_memory(name);
1188 }
1189 Ok(())
1190 }
1191 StoreWalAction::DropCollection { name } => self.drop_collection_in_memory(name),
1192 StoreWalAction::UpsertEntityRecord { collection, record } => {
1193 self.apply_replayed_upsert(collection, record, update_btrees)
1194 }
1195 StoreWalAction::DeleteEntityRecord {
1196 collection,
1197 entity_id,
1198 } => self.apply_replayed_delete(collection, EntityId::new(*entity_id), update_btrees),
1199 StoreWalAction::BulkUpsertEntityRecords {
1200 collection,
1201 records,
1202 } => {
1203 for record in records {
1204 self.apply_replayed_upsert(collection, record, update_btrees)?;
1205 }
1206 Ok(())
1207 }
1208 StoreWalAction::RefreshCollection {
1209 collection,
1210 records,
1211 } => self.apply_replayed_refresh_collection(collection, records, update_btrees),
1212 }
1213 }
1214
1215 pub fn refresh_collection(
1232 &self,
1233 name: &str,
1234 entities: Vec<UnifiedEntity>,
1235 ) -> Result<Vec<Vec<u8>>, StoreError> {
1236 let fv = self.format_version();
1237
1238 let new_manager = Arc::new(SegmentManager::with_config(
1241 name,
1242 self.config.manager_config.clone(),
1243 ));
1244
1245 let mut prepared = entities;
1246 for entity in &mut prepared {
1247 if entity.id.raw() == 0 {
1248 entity.id = self.next_entity_id();
1249 } else {
1250 self.register_entity_id(entity.id);
1251 }
1252 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1253 if *row_id == 0 {
1254 *row_id = new_manager.next_row_id();
1255 } else {
1256 new_manager.register_row_id(*row_id);
1257 }
1258 }
1259 entity.ensure_table_logical_id();
1260 }
1261
1262 let serialized: Vec<Vec<u8>> = prepared
1263 .iter()
1264 .map(|e| Self::serialize_entity_record(e, None, fv))
1265 .collect();
1266
1267 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1268 StoreError::Io(std::io::Error::other(format!(
1269 "refresh_collection: bulk_insert into new manager failed: {e}"
1270 )))
1271 })?;
1272
1273 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
1274
1275 self.finish_paged_write([StoreWalAction::RefreshCollection {
1276 collection: name.to_string(),
1277 records: serialized.clone(),
1278 }])?;
1279
1280 Ok(serialized)
1281 }
1282
1283 fn swap_collection_state(
1289 &self,
1290 name: &str,
1291 new_manager: Arc<SegmentManager>,
1292 prepared: &[UnifiedEntity],
1293 serialized: &[Vec<u8>],
1294 update_btrees: bool,
1295 ) {
1296 {
1297 let mut collections = self.collections.write();
1298 collections.insert(name.to_string(), new_manager);
1299 }
1300
1301 self.entity_cache
1302 .retain(|_, (collection, _)| collection != name);
1303 self.remove_from_graph_label_index_batch(
1304 name,
1305 &prepared.iter().map(|e| e.id).collect::<Vec<_>>(),
1306 );
1307
1308 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1309 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1310 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1311 .iter()
1312 .zip(serialized.iter())
1313 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1314 .collect();
1315 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1316 if !sorted.is_empty() {
1317 let _ = new_btree.bulk_insert_sorted(&sorted);
1318 }
1319 self.btree_indices
1320 .write()
1321 .insert(name.to_string(), new_btree);
1322 self.mark_paged_registry_dirty();
1323 }
1324 }
1325
1326 fn apply_replayed_refresh_collection(
1327 &self,
1328 collection: &str,
1329 records: &[Vec<u8>],
1330 update_btrees: bool,
1331 ) -> Result<(), StoreError> {
1332 let new_manager = Arc::new(SegmentManager::with_config(
1333 collection,
1334 self.config.manager_config.clone(),
1335 ));
1336
1337 let mut prepared: Vec<UnifiedEntity> = Vec::with_capacity(records.len());
1338 for record in records {
1339 let (entity, _metadata) =
1340 Self::deserialize_entity_record(record, self.format_version())?;
1341 self.register_entity_id(entity.id);
1342 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1343 new_manager.register_row_id(*row_id);
1344 }
1345 prepared.push(entity);
1346 }
1347
1348 if !prepared.is_empty() {
1349 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1350 StoreError::Io(std::io::Error::other(format!(
1351 "replay refresh_collection: bulk_insert failed: {e}"
1352 )))
1353 })?;
1354 }
1355
1356 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
1357 Ok(())
1358 }
1359
1360 pub fn refresh_collection_from_records(
1374 &self,
1375 name: &str,
1376 records: Vec<Vec<u8>>,
1377 ) -> Result<(), StoreError> {
1378 self.apply_replayed_refresh_collection(name, &records, true)?;
1379 self.finish_paged_write([StoreWalAction::RefreshCollection {
1380 collection: name.to_string(),
1381 records,
1382 }])?;
1383 Ok(())
1384 }
1385
1386 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1387 let mut collections = self.collections.write();
1388 if collections.contains_key(name) {
1389 return Ok(());
1390 }
1391 let manager = SegmentManager::with_config(name, self.config.manager_config.clone());
1392 collections.insert(name.to_string(), Arc::new(manager));
1393 self.mark_paged_registry_dirty();
1394 Ok(())
1395 }
1396
1397 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1398 let manager = {
1399 let mut collections = self.collections.write();
1400 match collections.remove(name) {
1401 Some(manager) => manager,
1402 None => return Ok(()),
1403 }
1404 };
1405
1406 let entities = manager.query_all(|_| true);
1407 let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
1408 for entity_id in &entity_ids {
1409 self.context_index.remove_entity(*entity_id);
1410 let _ = self.unindex_cross_refs(*entity_id);
1411 }
1412 if let Some(tree) = self.btree_indices.write().remove(name) {
1413 self.retire_btree(tree);
1414 }
1415 self.entity_cache.retain(|entity_id, (collection, _)| {
1416 collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
1417 });
1418 self.remove_from_graph_label_index_batch(name, &entity_ids);
1419 self.mark_paged_registry_dirty();
1420 Ok(())
1421 }
1422
1423 fn apply_replayed_upsert(
1424 &self,
1425 collection: &str,
1426 record: &[u8],
1427 update_btrees: bool,
1428 ) -> Result<(), StoreError> {
1429 self.create_collection_in_memory(collection)?;
1430 let (entity, metadata) = Self::deserialize_entity_record(record, self.format_version())?;
1431 let manager = self
1432 .get_collection(collection)
1433 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1434
1435 self.register_entity_id(entity.id);
1436 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1437 manager.register_row_id(*row_id);
1438 }
1439
1440 self.context_index.remove_entity(entity.id);
1441 let _ = self.unindex_cross_refs(entity.id);
1442 self.remove_from_graph_label_index(collection, entity.id);
1443
1444 if manager.get(entity.id).is_some() {
1445 manager
1446 .update_with_metadata(entity.clone(), metadata.as_ref())
1447 .map_err(StoreError::from)?;
1448 } else {
1449 manager.restore_snapshot_entity(entity.clone())?;
1450 if let Some(metadata) = metadata.as_ref() {
1451 manager.set_metadata(entity.id, metadata.clone())?;
1452 }
1453 }
1454
1455 self.context_index.index_entity(collection, &entity);
1456 if let EntityKind::GraphNode(node) = &entity.kind {
1457 self.update_graph_label_index(collection, &node.label, entity.id);
1458 }
1459 self.index_cross_refs(&entity, collection)?;
1460
1461 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1462 let mut btree_indices = self.btree_indices.write();
1463 let btree = btree_indices
1464 .entry(collection.to_string())
1465 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1466 let root_before = btree.root_page_id();
1467 let key = entity.id.raw().to_be_bytes();
1468 match btree.insert(&key, record) {
1469 Ok(_) => {}
1470 Err(BTreeError::DuplicateKey) => {
1471 let _ = btree.delete(&key);
1472 let _ = btree.insert(&key, record);
1473 }
1474 Err(err) => {
1475 return Err(StoreError::Io(io::Error::other(format!(
1476 "replay upsert btree error: {err}"
1477 ))));
1478 }
1479 }
1480 if root_before != btree.root_page_id() {
1481 self.mark_paged_registry_dirty();
1482 }
1483 }
1484
1485 Ok(())
1486 }
1487
1488 fn apply_replayed_delete(
1489 &self,
1490 collection: &str,
1491 id: EntityId,
1492 update_btrees: bool,
1493 ) -> Result<(), StoreError> {
1494 self.entity_cache.remove(id.raw());
1495 if let Some(manager) = self.get_collection(collection) {
1496 let deleted = manager.delete(id)?;
1497 if !deleted {
1498 return Ok(());
1499 }
1500 } else {
1501 return Ok(());
1502 }
1503
1504 if self.pager.is_some() && update_btrees {
1505 let btree_indices = self.btree_indices.read();
1506 if let Some(btree) = btree_indices.get(collection) {
1507 let root_before = btree.root_page_id();
1508 let key = id.raw().to_be_bytes();
1509 let _ = btree.delete(&key);
1510 if root_before != btree.root_page_id() {
1511 self.mark_paged_registry_dirty();
1512 }
1513 }
1514 }
1515
1516 let _ = self.unindex_cross_refs(id);
1517 self.remove_from_graph_label_index(collection, id);
1518 self.context_index.remove_entity(id);
1519 Ok(())
1520 }
1521}
1522
1523#[cfg(test)]
1524mod tests {
1525 use super::*;
1526 use crate::api::{DurabilityMode, GroupCommitOptions};
1527 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1528 use std::time::SystemTime;
1529
1530 fn embedded_fixture(
1531 mode: DurabilityMode,
1532 background_bytes: usize,
1533 ) -> (tempfile::TempDir, PathBuf, UnifiedStore) {
1534 let directory = tempfile::tempdir().expect("directory");
1535 let path = directory.path().join("data.rdb");
1536 let mut store =
1537 UnifiedStore::with_config(UnifiedStoreConfig::default().with_durability_mode(mode));
1538 store.create_collection("keys").expect("collection");
1539 store.set_aux_metadata(vec![42; background_bytes]);
1540 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(
1541 &path,
1542 &store.to_binary_dump_bytes(),
1543 )
1544 .expect("snapshot");
1545 store.config.embedded_wal_path = Some(path.clone());
1546 (directory, path, store)
1547 }
1548
1549 #[test]
1550 fn embedded_stale_full_result_does_not_checkpoint_reclaimed_space() {
1551 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
1552 let (_directory, path, store) = embedded_fixture(mode, 0);
1553 store
1554 .append_embedded_store_wal_actions(&[StoreWalAction::CreateCollection {
1555 name: "keys".into(),
1556 }])
1557 .expect("another writer appended after reclaiming space");
1558 let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
1559 let payloads = vec![StoreWalAction::CreateCollection {
1560 name: "after".into(),
1561 }
1562 .encode()];
1563 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1564 .expect("encoded length");
1565 store
1567 .recover_full_embedded_wal(&path, &payloads, length)
1568 .expect("revalidate after acquiring publication");
1569 let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
1570 assert_eq!(
1571 after.selected_superblock.generation,
1572 before.selected_superblock.generation + 1,
1573 "only the retry is published, without an unnecessary checkpoint"
1574 );
1575 assert_eq!(
1576 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&after)
1577 .expect("both appends retained")
1578 .len(),
1579 2
1580 );
1581 }
1582 }
1583
1584 #[test]
1585 fn embedded_full_wal_reserves_bounded_headroom_for_populated_store() {
1586 for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
1587 let (_directory, path, store) = embedded_fixture(mode, 2 * 1024 * 1024);
1588 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial");
1589 let action = StoreWalAction::CreateCollection {
1590 name: "keys".into(),
1591 };
1592 let payloads = vec![action.encode()];
1593 let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1594 .expect("encoded length");
1595 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(
1596 &path,
1597 &vec![payloads[0].clone(); (initial.manifest.wal_region_bytes / length) as usize],
1598 )
1599 .expect("fill the initial region with valid records");
1600 store
1601 .append_embedded_store_wal_actions(&[action])
1602 .expect("overflow");
1603 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("checkpoint");
1604 assert_eq!(open.manifest.wal_region_bytes, 1024 * 1024);
1605 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
1606 .expect("snapshot")
1607 .expect("populated image");
1608 let recovered =
1609 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
1610 .expect("recover snapshot");
1611 for payload in
1612 crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL")
1613 {
1614 recovered
1615 .apply_encoded_store_wal_action(&payload)
1616 .expect("replay");
1617 }
1618 assert_eq!(recovered.aux_metadata.read().len(), 2 * 1024 * 1024);
1619 assert!(recovered.get_collection("keys").is_some());
1620 }
1621 }
1622
1623 #[test]
1624 fn embedded_grouped_overwrites_recover_across_repeated_full_wal_regions() {
1625 fn row(writer: usize, round: usize) -> UnifiedEntity {
1626 let mut state = (writer as u64 + 1) * 31 + round as u64 * 13;
1627 let payload: String = (0..4096)
1628 .map(|_| {
1629 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1630 char::from(b'!' + ((state >> 24) % 90) as u8)
1631 })
1632 .collect();
1633 let mut entity = UnifiedEntity::table_row(EntityId::new(0), "keys", 0, vec![]);
1634 entity.data = EntityData::Row(RowData::with_names(
1635 vec![
1636 Value::text(format!("k{writer}")),
1637 Value::text(format!("{round}:{payload}")),
1638 ],
1639 vec!["key".into(), "value".into()],
1640 ));
1641 entity
1642 }
1643 let (_directory, path, store) =
1644 embedded_fixture(DurabilityMode::WalDurableGrouped, 2 * 1024 * 1024);
1645 assert!(
1646 StoreWalAction::upsert_entity("keys", &row(0, 0), None, store.format_version())
1647 .encode()
1648 .len()
1649 >= 2048,
1650 "fixture must exert physical WAL pressure after value compression"
1651 );
1652 let mut ids: Vec<_> = (0..8)
1653 .map(|writer| {
1654 store
1655 .write_keyed_row("keys", row(writer, 0), None, None)
1656 .expect("seed")
1657 })
1658 .collect();
1659 let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial roots");
1660 let mut previous_snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&initial)
1661 .expect("initial snapshot")
1662 .expect("image");
1663 for wave in 0..3 {
1667 let barrier = Barrier::new(ids.len());
1668 ids = std::thread::scope(|scope| {
1669 let workers: Vec<_> = ids
1670 .iter()
1671 .enumerate()
1672 .map(|(writer, previous)| {
1673 let store = &store;
1674 let barrier = &barrier;
1675 scope.spawn(move || {
1676 barrier.wait();
1677 let mut id = *previous;
1678 for round in 0..64 {
1679 id = store
1680 .write_keyed_row(
1681 "keys",
1682 row(writer, wave * 64 + round),
1683 None,
1684 Some(id),
1685 )
1686 .expect("durable overwrite");
1687 }
1688 id
1689 })
1690 })
1691 .collect();
1692 workers
1693 .into_iter()
1694 .map(|worker| worker.join().expect("writer"))
1695 .collect()
1696 });
1697 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("wave roots");
1698 assert_eq!(open.manifest.wal_region_bytes, EMBEDDED_WAL_HEADROOM_LIMIT);
1699 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
1700 .expect("wave snapshot")
1701 .expect("image");
1702 assert!(snapshot != previous_snapshot, "wave {wave} must checkpoint");
1703 previous_snapshot = snapshot;
1704 }
1705 let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("recover roots");
1706 let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
1707 .expect("snapshot")
1708 .expect("image");
1709 let recovered =
1710 UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
1711 .expect("snapshot replay");
1712 for payload in crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL") {
1713 recovered
1714 .apply_encoded_store_wal_action(&payload)
1715 .expect("WAL replay");
1716 }
1717 assert_eq!(recovered.get_collection("keys").expect("keys").count(), 8);
1718 for (writer, id) in ids.iter().enumerate() {
1719 let entity = recovered
1720 .get("keys", *id)
1721 .expect("last acknowledged version");
1722 assert_eq!(
1723 entity.data.as_row().expect("row").get_field("value"),
1724 row(writer, 191)
1725 .data
1726 .as_row()
1727 .expect("expected row")
1728 .get_field("value")
1729 );
1730 }
1731 }
1732
1733 fn env_lock() -> &'static StdMutex<()> {
1738 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
1739 LOCK.get_or_init(|| StdMutex::new(()))
1740 }
1741
1742 fn temp_wal(name: &str) -> PathBuf {
1743 let nanos = SystemTime::now()
1744 .duration_since(std::time::UNIX_EPOCH)
1745 .unwrap()
1746 .as_nanos();
1747 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
1748 &std::env::temp_dir(),
1749 name,
1750 std::process::id(),
1751 nanos,
1752 );
1753 let _ = std::fs::remove_file(&path);
1754 path
1755 }
1756
1757 fn replay_crash_payload(id: u64, round: u64) -> String {
1758 let mut state = id * 31 + round * 13 + 1;
1759 (0..600)
1760 .map(|_| {
1761 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1762 char::from(b'!' + ((state >> 24) % 90) as u8)
1763 })
1764 .collect()
1765 }
1766
1767 #[test]
1768 fn paged_wal_replay_survives_immediate_process_exit() {
1769 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
1770 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
1771 if let Some(path) = std::env::var_os(CHILD_PATH) {
1772 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
1773 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
1774 std::process::exit(0);
1776 }
1777 let directory = tempfile::tempdir().unwrap();
1778 let path = directory.path().join("data.rdb");
1779 {
1780 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
1781 store.create_collection("rows").unwrap();
1782 for id in 1..=400 {
1783 store
1784 .insert(
1785 "rows",
1786 UnifiedEntity::table_row(
1787 EntityId::new(id),
1788 "rows",
1789 id,
1790 vec![Value::text(format!("initial-{id}"))],
1791 ),
1792 )
1793 .unwrap();
1794 }
1795 store.persist().unwrap();
1796 }
1797 {
1798 let coordinator = StoreCommitCoordinator::open(
1799 reddb_file::layout::unified_wal_path(&path),
1800 DurabilityMode::Strict,
1801 GroupCommitOptions::default(),
1802 )
1803 .unwrap();
1804 for round in 0..3 {
1805 let actions = (1..=400)
1806 .map(|id| {
1807 let payload = replay_crash_payload(id, round);
1808 let entity = UnifiedEntity::table_row(
1809 EntityId::new(id),
1810 "rows",
1811 id,
1812 vec![Value::text(payload)],
1813 );
1814 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
1815 })
1816 .collect::<Vec<_>>();
1817 coordinator.append_actions(&actions).unwrap();
1818 }
1819 }
1820 let status = std::process::Command::new(std::env::current_exe().unwrap())
1821 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
1822 .env(CHILD_PATH, &path).status().unwrap();
1823 assert!(status.success(), "replay process failed");
1824 let reopened = UnifiedStore::open_with_config(&path, config())
1825 .expect("cold reopen after replay crash");
1826 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
1827 for id in 1..=400 {
1828 let entity = reopened
1829 .get("rows", EntityId::new(id))
1830 .expect("retained entity");
1831 let EntityData::Row(row) = entity.data else {
1832 panic!("expected row")
1833 };
1834 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
1835 }
1836 drop(reopened);
1837 let stable_bytes = std::fs::metadata(&path).unwrap().len();
1838 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
1839 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
1840 assert_eq!(
1841 std::fs::metadata(&path).unwrap().len(),
1842 stable_bytes,
1843 "idempotent replay must not allocate another tree generation"
1844 );
1845 }
1846
1847 #[test]
1848 fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
1849 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1850 let path = temp_wal("checkpoint-ack");
1851 let coordinator = Arc::new(
1852 StoreCommitCoordinator::open(
1853 path.clone(),
1854 DurabilityMode::WalDurableGrouped,
1855 GroupCommitOptions::default(),
1856 )
1857 .unwrap(),
1858 );
1859 let state = coordinator.state.0.lock();
1862 let (written_tx, written_rx) = std::sync::mpsc::channel();
1863 let writer_coordinator = Arc::clone(&coordinator);
1864 let writer = std::thread::spawn(move || {
1865 let result = writer_coordinator.append_actions(&[StoreWalAction::CreateCollection {
1866 name: "acknowledged".into(),
1867 }]);
1868 written_tx.send(result).unwrap();
1869 });
1870 let deadline = Instant::now() + Duration::from_secs(2);
1871 while !coordinator.queue.has_pending() {
1872 assert!(Instant::now() < deadline, "writer never enqueued");
1873 std::thread::yield_now();
1874 }
1875 let (started_tx, started_rx) = std::sync::mpsc::channel();
1876 let (reset_tx, reset_rx) = std::sync::mpsc::channel();
1877 let reset_coordinator = Arc::clone(&coordinator);
1878 let reset = std::thread::spawn(move || {
1879 started_tx.send(()).unwrap();
1880 reset_tx.send(reset_coordinator.truncate()).unwrap();
1881 });
1882 started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1883 let deadline = Instant::now() + Duration::from_millis(100);
1884 while coordinator.queue.has_pending() && Instant::now() < deadline {
1885 std::thread::sleep(Duration::from_millis(1));
1886 }
1887 let discarded_before_ack = !coordinator.queue.has_pending();
1888 drop(state);
1889 let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
1890 if acknowledged.is_err() {
1891 let mut state = coordinator.state.0.lock();
1894 state.shutdown = true;
1895 state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
1896 coordinator.state.1.notify_all();
1897 }
1898 writer.join().unwrap();
1899 reset_rx
1900 .recv_timeout(Duration::from_secs(2))
1901 .unwrap()
1902 .unwrap();
1903 reset.join().unwrap();
1904 assert!(
1905 !discarded_before_ack,
1906 "checkpoint discarded an unacknowledged WAL record"
1907 );
1908 assert!(
1909 acknowledged.unwrap().is_ok(),
1910 "durable writer did not finish"
1911 );
1912 drop(coordinator);
1913 let _ = std::fs::remove_file(path);
1914 }
1915
1916 #[test]
1927 fn group_commit_coalesces_concurrent_autocommits() {
1928 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1929 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1932
1933 let path = temp_wal("coalesce");
1934 let coord = Arc::new(
1935 StoreCommitCoordinator::open(
1936 path.clone(),
1937 DurabilityMode::WalDurableGrouped,
1938 GroupCommitOptions::default(),
1939 )
1940 .expect("open commit coordinator"),
1941 );
1942
1943 const WRITERS: usize = 32;
1944 let barrier = Arc::new(Barrier::new(WRITERS));
1945 let mut handles = Vec::with_capacity(WRITERS);
1946 for tx in 0..WRITERS {
1947 let coord_c = Arc::clone(&coord);
1948 let barrier_c = Arc::clone(&barrier);
1949 handles.push(std::thread::spawn(move || {
1950 barrier_c.wait();
1954 let action = StoreWalAction::CreateCollection {
1955 name: format!("c{tx}"),
1956 };
1957 coord_c
1958 .append_actions(std::slice::from_ref(&action))
1959 .expect("append_actions");
1960 }));
1961 }
1962 for h in handles {
1963 h.join().expect("writer thread");
1964 }
1965
1966 let fsyncs = coord.fsync_count();
1967 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
1968 assert!(
1969 fsyncs < WRITERS as u64,
1970 "expected fsyncs ({fsyncs}) to be strictly less than \
1971 concurrent writers ({WRITERS}); coalescing failed"
1972 );
1973
1974 drop(coord);
1975 let _ = std::fs::remove_file(&path);
1976 }
1977
1978 #[test]
1984 fn zero_window_disables_coalescing_floor() {
1985 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1986 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1987
1988 let path = temp_wal("zero_window");
1989 let coord = Arc::new(
1990 StoreCommitCoordinator::open(
1991 path.clone(),
1992 DurabilityMode::WalDurableGrouped,
1993 GroupCommitOptions::default(),
1994 )
1995 .expect("open commit coordinator"),
1996 );
1997
1998 const WRITERS: usize = 8;
1999 let barrier = Arc::new(Barrier::new(WRITERS));
2000 let mut handles = Vec::with_capacity(WRITERS);
2001 for tx in 0..WRITERS {
2002 let coord_c = Arc::clone(&coord);
2003 let barrier_c = Arc::clone(&barrier);
2004 handles.push(std::thread::spawn(move || {
2005 barrier_c.wait();
2006 let action = StoreWalAction::CreateCollection {
2007 name: format!("z{tx}"),
2008 };
2009 coord_c
2010 .append_actions(std::slice::from_ref(&action))
2011 .expect("append_actions");
2012 }));
2013 }
2014 for h in handles {
2015 h.join().expect("writer thread");
2016 }
2017
2018 let fsyncs = coord.fsync_count();
2024 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
2025
2026 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
2027 drop(coord);
2028 let _ = std::fs::remove_file(&path);
2029 }
2030
2031 #[test]
2033 fn resolve_window_precedence() {
2034 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
2035 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
2037 let cfg = GroupCommitOptions::default();
2038 assert_eq!(
2039 StoreCommitCoordinator::resolve_window(&cfg),
2040 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
2041 );
2042
2043 let cfg_ms = GroupCommitOptions {
2045 window_ms: 5,
2046 ..GroupCommitOptions::default()
2047 };
2048 assert_eq!(
2049 StoreCommitCoordinator::resolve_window(&cfg_ms),
2050 Duration::from_millis(5)
2051 );
2052
2053 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
2055 assert_eq!(
2056 StoreCommitCoordinator::resolve_window(&cfg),
2057 Duration::from_micros(750)
2058 );
2059 assert_eq!(
2060 StoreCommitCoordinator::resolve_window(&cfg_ms),
2061 Duration::from_micros(750)
2062 );
2063
2064 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
2066 assert_eq!(
2067 StoreCommitCoordinator::resolve_window(&cfg),
2068 Duration::from_micros(0)
2069 );
2070
2071 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
2072 }
2073}