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
28type WalMutex = parking_lot::Mutex<WalWriter>;
35
36type CommitStateMutex = parking_lot::Mutex<CommitState>;
42type CommitStateCondvar = parking_lot::Condvar;
43use std::time::{Duration, Instant};
44
45static NEXT_STORE_TX_ID: AtomicU64 = AtomicU64::new(1);
46
47#[derive(Debug, Clone)]
48pub(crate) enum StoreWalAction {
49 CreateCollection {
50 name: String,
51 },
52 DropCollection {
53 name: String,
54 },
55 UpsertEntityRecord {
56 collection: String,
57 record: Vec<u8>,
58 },
59 DeleteEntityRecord {
60 collection: String,
61 entity_id: u64,
62 },
63 BulkUpsertEntityRecords {
68 collection: String,
69 records: Vec<Vec<u8>>,
70 },
71 RefreshCollection {
79 collection: String,
80 records: Vec<Vec<u8>>,
81 },
82}
83
84#[derive(Debug, Default)]
85pub(crate) struct DeferredStoreWalActions {
86 actions: Vec<StoreWalAction>,
87}
88
89impl DeferredStoreWalActions {
90 pub(crate) fn is_empty(&self) -> bool {
91 self.actions.is_empty()
92 }
93
94 pub(crate) fn extend(&mut self, other: Self) {
95 self.actions.extend(other.actions);
96 }
97}
98
99thread_local! {
100 static DEFERRED_STORE_WAL_ACTIONS: RefCell<Option<Vec<StoreWalAction>>> =
101 const { RefCell::new(None) };
102}
103
104fn begin_deferred_store_wal_capture() {
105 DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
106 let mut guard = cell.borrow_mut();
107 debug_assert!(guard.is_none());
108 *guard = Some(Vec::new());
109 });
110}
111
112fn capture_deferred_store_wal_actions(actions: Vec<StoreWalAction>) -> bool {
113 DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
114 let mut guard = cell.borrow_mut();
115 if let Some(pending) = guard.as_mut() {
116 pending.extend(actions);
117 true
118 } else {
119 false
120 }
121 })
122}
123
124fn deferred_store_wal_capture_active() -> bool {
125 DEFERRED_STORE_WAL_ACTIONS.with(|cell| cell.borrow().is_some())
126}
127
128fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
129 DEFERRED_STORE_WAL_ACTIONS.with(|cell| DeferredStoreWalActions {
130 actions: cell.borrow_mut().take().unwrap_or_default(),
131 })
132}
133
134impl StoreWalAction {
135 pub(crate) fn upsert_entity(
136 collection: &str,
137 entity: &UnifiedEntity,
138 metadata: Option<&Metadata>,
139 format_version: u32,
140 ) -> Self {
141 Self::UpsertEntityRecord {
142 collection: collection.to_string(),
143 record: UnifiedStore::serialize_entity_record(entity, metadata, format_version),
144 }
145 }
146
147 fn encode(&self) -> Vec<u8> {
148 reddb_file::encode_store_wal_action_frame(&self.to_frame())
149 .expect("encode store wal action frame")
150 }
151
152 fn to_frame(&self) -> reddb_file::StoreWalActionFrame {
153 match self {
154 Self::CreateCollection { name } => {
155 reddb_file::StoreWalActionFrame::CreateCollection { name: name.clone() }
156 }
157 Self::DropCollection { name } => {
158 reddb_file::StoreWalActionFrame::DropCollection { name: name.clone() }
159 }
160 Self::UpsertEntityRecord { collection, record } => {
161 reddb_file::StoreWalActionFrame::UpsertEntityRecord {
162 collection: collection.clone(),
163 record: record.clone(),
164 }
165 }
166 Self::DeleteEntityRecord {
167 collection,
168 entity_id,
169 } => reddb_file::StoreWalActionFrame::DeleteEntityRecord {
170 collection: collection.clone(),
171 entity_id: *entity_id,
172 },
173 Self::BulkUpsertEntityRecords {
174 collection,
175 records,
176 } => reddb_file::StoreWalActionFrame::BulkUpsertEntityRecords {
177 collection: collection.clone(),
178 records: records.clone(),
179 },
180 Self::RefreshCollection {
181 collection,
182 records,
183 } => reddb_file::StoreWalActionFrame::RefreshCollection {
184 collection: collection.clone(),
185 records: records.clone(),
186 },
187 }
188 }
189
190 fn decode(bytes: &[u8]) -> io::Result<Self> {
191 match reddb_file::decode_store_wal_action_frame(bytes)? {
192 reddb_file::StoreWalActionFrame::CreateCollection { name } => {
193 Ok(Self::CreateCollection { name })
194 }
195 reddb_file::StoreWalActionFrame::DropCollection { name } => {
196 Ok(Self::DropCollection { name })
197 }
198 reddb_file::StoreWalActionFrame::UpsertEntityRecord { collection, record } => {
199 Ok(Self::UpsertEntityRecord { collection, record })
200 }
201 reddb_file::StoreWalActionFrame::DeleteEntityRecord {
202 collection,
203 entity_id,
204 } => Ok(Self::DeleteEntityRecord {
205 collection,
206 entity_id,
207 }),
208 reddb_file::StoreWalActionFrame::BulkUpsertEntityRecords {
209 collection,
210 records,
211 } => Ok(Self::BulkUpsertEntityRecords {
212 collection,
213 records,
214 }),
215 reddb_file::StoreWalActionFrame::RefreshCollection {
216 collection,
217 records,
218 } => Ok(Self::RefreshCollection {
219 collection,
220 records,
221 }),
222 }
223 }
224}
225
226#[derive(Debug)]
227struct CommitState {
228 durable_lsn: u64,
229 pending_target_lsn: u64,
230 pending_statements: usize,
231 pending_wal_bytes: u64,
232 first_pending_at: Option<Instant>,
233 shutdown: bool,
234 last_error: Option<String>,
235}
236
237impl CommitState {
238 fn new(initial_durable_lsn: u64) -> Self {
239 Self {
240 durable_lsn: initial_durable_lsn,
241 pending_target_lsn: initial_durable_lsn,
242 pending_statements: 0,
243 pending_wal_bytes: 0,
244 first_pending_at: None,
245 shutdown: false,
246 last_error: None,
247 }
248 }
249}
250
251pub(crate) struct WalAppendQueue {
265 pending: parking_lot::Mutex<WalQueueState>,
280 queued_bytes: AtomicU64,
285}
286
287struct WalQueueState {
288 next_lsn: u64,
289 entries: Vec<(u64, Vec<u8>)>,
290}
291
292impl WalAppendQueue {
293 fn new(initial_lsn: u64) -> Self {
294 Self {
295 pending: parking_lot::Mutex::new(WalQueueState {
296 next_lsn: initial_lsn,
297 entries: Vec::with_capacity(64),
298 }),
299 queued_bytes: AtomicU64::new(0),
300 }
301 }
302
303 fn enqueue(&self, bytes: Vec<u8>) -> u64 {
309 let len = bytes.len() as u64;
310 let mut state = self.pending.lock();
311 let start_lsn = state.next_lsn;
312 state.next_lsn = start_lsn + len;
313 state.entries.push((start_lsn, bytes));
314 self.queued_bytes.fetch_add(len, Ordering::Relaxed);
315 start_lsn + len
316 }
317
318 fn drain_sorted(&self) -> Vec<(u64, Vec<u8>)> {
322 let mut state = self.pending.lock();
323 let mut v = std::mem::take(&mut state.entries);
324 let drained: u64 = v.iter().map(|(_, bytes)| bytes.len() as u64).sum();
325 drop(state);
326 let _ = self
330 .queued_bytes
331 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |queued| {
332 Some(queued.saturating_sub(drained))
333 });
334 v.sort_by_key(|(lsn, _)| *lsn);
335 v
336 }
337
338 fn has_pending(&self) -> bool {
341 !self.pending.lock().entries.is_empty()
342 }
343
344 fn queued_bytes(&self) -> u64 {
346 self.queued_bytes.load(Ordering::Relaxed)
347 }
348
349 fn reset(&self, next_lsn: u64) {
355 let mut state = self.pending.lock();
356 state.next_lsn = next_lsn;
357 state.entries.clear();
358 drop(state);
359 self.queued_bytes.store(0, Ordering::Relaxed);
360 }
361}
362
363pub(crate) struct StoreCommitCoordinator {
364 mode: DurabilityMode,
365 config: crate::api::GroupCommitOptions,
366 wal_path: PathBuf,
367 wal: Arc<WalMutex>,
368 queue: Arc<WalAppendQueue>,
374 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
375 fsync_count: Arc<AtomicU64>,
381}
382
383impl StoreCommitCoordinator {
384 pub(crate) fn open(
385 wal_path: impl Into<PathBuf>,
386 mode: DurabilityMode,
387 config: crate::api::GroupCommitOptions,
388 ) -> io::Result<Self> {
389 let wal_path = wal_path.into();
390 let wal = WalWriter::open(&wal_path)?;
391 let initial_durable_lsn = wal.durable_lsn();
392 let initial_current_lsn = wal.current_lsn();
393 let wal = Arc::new(WalMutex::new(wal));
394 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
395 let state = Arc::new((
396 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
397 CommitStateCondvar::new(),
398 ));
399 let fsync_count = Arc::new(AtomicU64::new(0));
400
401 if matches!(
402 mode,
403 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
404 ) {
405 let wal_bg = Arc::clone(&wal);
406 let queue_bg = Arc::clone(&queue);
407 let state_bg = Arc::clone(&state);
408 let fsync_bg = Arc::clone(&fsync_count);
409 let window = Self::resolve_window(&config);
424 let max_statements = config.max_statements.max(1);
425 let max_wal_bytes = config.max_wal_bytes.max(1);
426 std::thread::spawn(move || {
427 Self::run_group_commit_loop(
428 wal_bg,
429 queue_bg,
430 state_bg,
431 fsync_bg,
432 window,
433 max_statements,
434 max_wal_bytes,
435 );
436 });
437 }
438
439 Ok(Self {
440 mode,
441 config,
442 wal_path,
443 wal,
444 queue,
445 state,
446 fsync_count,
447 })
448 }
449
450 fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
459 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
460 if let Ok(parsed) = raw.parse::<u64>() {
461 return Duration::from_micros(parsed);
462 }
463 }
464 if config.window_ms != 0 {
465 return Duration::from_millis(config.window_ms);
466 }
467 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
468 }
469
470 #[cfg(test)]
474 pub(crate) fn fsync_count(&self) -> u64 {
475 self.fsync_count.load(Ordering::Relaxed)
476 }
477
478 pub(crate) fn buffered_bytes(&self) -> u64 {
482 self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
483 }
484
485 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
486 if actions.is_empty() {
487 return Ok(());
488 }
489
490 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
491
492 if matches!(self.mode, DurabilityMode::Strict) {
497 let commit_lsn = {
498 let mut wal = self.wal.lock();
499 wal.append(&WalRecord::TxCommitBatch {
500 tx_id,
501 actions: actions.iter().map(StoreWalAction::encode).collect(),
502 })?;
503 wal.current_lsn()
504 };
505 self.force_sync()?;
506 let _ = commit_lsn;
507 return Ok(());
508 }
509
510 let encoded_actions: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
514 let wal_bytes = encoded_actions.iter().fold(0u64, |total, payload| {
515 total.saturating_add(payload.len() as u64)
516 });
517 let blob = WalRecord::TxCommitBatch {
518 tx_id,
519 actions: encoded_actions,
520 }
521 .encode();
522
523 let commit_lsn = self.queue.enqueue(blob);
524 self.wait_until_durable(commit_lsn, wal_bytes)?;
525 Ok(())
526 }
527
528 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
533 let blob = record.encode();
534 let wal_bytes = blob.len() as u64;
535 if matches!(self.mode, DurabilityMode::Strict) {
536 {
537 let mut wal = self.wal.lock();
538 wal.append(&record)?;
539 }
540 self.force_sync()?;
541 return Ok(());
542 }
543 let commit_lsn = self.queue.enqueue(blob);
544 self.wait_until_durable(commit_lsn, wal_bytes)?;
545 Ok(())
546 }
547
548 pub(crate) fn force_sync(&self) -> io::Result<()> {
549 {
550 let mut wal = self.wal.lock();
551 wal.sync()?;
552 self.fsync_count.fetch_add(1, Ordering::Relaxed);
553 let durable = wal.durable_lsn();
554 drop(wal);
555 let (state_lock, cond) = &*self.state;
556 let mut state = state_lock.lock();
557 state.durable_lsn = durable;
558 state.pending_target_lsn = durable.max(state.pending_target_lsn);
559 state.pending_statements = 0;
560 state.pending_wal_bytes = 0;
561 state.first_pending_at = None;
562 state.last_error = None;
563 cond.notify_all();
564 }
565 Ok(())
566 }
567
568 pub(crate) fn truncate(&self) -> io::Result<()> {
569 let mut wal = self.wal.lock();
570 wal.truncate()?;
571 let durable = wal.durable_lsn();
572 let current = wal.current_lsn();
573 drop(wal);
574
575 self.queue.reset(current);
580
581 let (state_lock, cond) = &*self.state;
582 let mut state = state_lock.lock();
583 state.durable_lsn = durable;
584 state.pending_target_lsn = durable;
585 state.pending_statements = 0;
586 state.pending_wal_bytes = 0;
587 state.first_pending_at = None;
588 state.last_error = None;
589 cond.notify_all();
590 Ok(())
591 }
592
593 pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
594 if !self.wal_path.exists() {
595 return Ok(());
596 }
597
598 let reader = match WalReader::open(&self.wal_path) {
599 Ok(reader) => reader,
600 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
601 Err(err) => return Err(err),
602 };
603
604 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
605 let mut pending = Vec::<(u64, Vec<u8>)>::new();
606 let mut applied = false;
607
608 for record in reader.iter() {
609 let (_lsn, record) = match record {
610 Ok(record) => record,
611 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
612 Err(err) => return Err(err),
613 };
614 match record {
615 WalRecord::TxCommitBatch { actions, .. } => {
616 for payload in actions {
617 let action = StoreWalAction::decode(&payload)?;
618 applied = true;
619 store.apply_replayed_action(&action, false).map_err(|err| {
620 io::Error::other(format!("failed to replay store wal action: {err}"))
621 })?;
622 }
623 }
624 WalRecord::Begin { tx_id } => {
625 tx_states.insert(tx_id, false);
626 }
627 WalRecord::Commit { tx_id } => {
628 tx_states.insert(tx_id, true);
629 }
630 WalRecord::Rollback { tx_id } => {
631 tx_states.remove(&tx_id);
632 }
633 WalRecord::PageWrite {
634 tx_id,
635 page_id: _,
636 data,
637 } => pending.push((tx_id, data)),
638 WalRecord::Checkpoint { .. } => {}
639 WalRecord::VectorInsert {
640 collection,
641 entity_id,
642 vector,
643 } => {
644 let mut map = store.replayed_turbo_inserts.lock();
650 map.entry(collection).or_default().push((entity_id, vector));
651 }
652 WalRecord::ProbabilisticDelta {
653 kind,
654 operation,
655 name,
656 operands,
657 } => {
658 let mut deltas = store.replayed_probabilistic_deltas.lock();
659 deltas.push((kind, operation, name, operands));
660 }
661 WalRecord::FullPageImage { .. } => {
662 }
664 }
665 }
666
667 for (tx_id, payload) in pending {
668 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
669 continue;
670 }
671 let action = StoreWalAction::decode(&payload)?;
672 applied = true;
673 store.apply_replayed_action(&action, false).map_err(|err| {
674 io::Error::other(format!("failed to replay store wal action: {err}"))
675 })?;
676 }
677
678 if applied {
679 store
680 .reconcile_replayed_btrees()
681 .map_err(|err| io::Error::other(err.to_string()))?;
682 }
683 Ok(())
684 }
685
686 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
687 match self.mode {
688 DurabilityMode::Strict => self.force_sync(),
689 DurabilityMode::Async => {
694 let (state_lock, cond) = &*self.state;
695 let mut state = state_lock.lock();
696 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
697 state.pending_statements = state.pending_statements.saturating_add(1);
698 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
699 state.first_pending_at.get_or_insert_with(Instant::now);
700 cond.notify_all();
701 Ok(())
702 }
703 DurabilityMode::WalDurableGrouped => {
704 let (state_lock, cond) = &*self.state;
705 let mut state = state_lock.lock();
706 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
707 state.pending_statements = state.pending_statements.saturating_add(1);
708 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
709 state.first_pending_at.get_or_insert_with(Instant::now);
710 cond.notify_all();
711
712 loop {
713 if let Some(err) = state.last_error.clone() {
714 return Err(io::Error::other(err));
715 }
716 if state.durable_lsn >= target_lsn {
717 return Ok(());
718 }
719 cond.wait(&mut state);
722 }
723 }
724 }
725 }
726
727 fn run_group_commit_loop(
728 wal: Arc<WalMutex>,
729 queue: Arc<WalAppendQueue>,
730 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
731 fsync_count: Arc<AtomicU64>,
732 window: Duration,
733 max_statements: usize,
734 max_wal_bytes: u64,
735 ) {
736 let (state_lock, cond) = &*state;
737 loop {
738 let target_lsn = {
739 let mut guard = state_lock.lock();
740
741 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
742 cond.wait(&mut guard);
743 }
744
745 if guard.shutdown {
746 return;
747 }
748
749 let immediate = window.is_zero()
750 || guard.pending_statements >= max_statements
751 || guard.pending_wal_bytes >= max_wal_bytes;
752
753 if !immediate {
754 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
755 let now = Instant::now();
756 if now < deadline {
757 let timeout = deadline.saturating_duration_since(now);
758 let _ = cond.wait_for(&mut guard, timeout);
759 if guard.shutdown {
760 return;
761 }
762 if guard.pending_target_lsn <= guard.durable_lsn {
763 continue;
764 }
765 let should_wait_again = guard.pending_statements < max_statements
766 && guard.pending_wal_bytes < max_wal_bytes
767 && guard
768 .first_pending_at
769 .map(|first| first.elapsed() < window)
770 .unwrap_or(false);
771 if should_wait_again {
772 continue;
773 }
774 }
775 }
776
777 guard.pending_target_lsn
778 };
779
780 let batches = queue.drain_sorted();
786
787 let sync_result = {
788 let mut wal = wal.lock();
789 let mut write_err: Option<io::Error> = None;
790 for (_lsn, bytes) in batches {
791 if let Err(e) = wal.append_bytes(&bytes) {
792 write_err = Some(e);
793 break;
794 }
795 }
796 match write_err {
797 Some(e) => Err(e),
798 None => wal.sync().map(|_| {
799 fsync_count.fetch_add(1, Ordering::Relaxed);
804 wal.durable_lsn()
805 }),
806 }
807 };
808
809 let more_pending = queue.has_pending();
814
815 let mut guard = state_lock.lock();
816 match sync_result {
817 Ok(durable_lsn) => {
818 guard.durable_lsn = durable_lsn;
819 if !more_pending {
820 guard.pending_statements = 0;
821 guard.pending_wal_bytes = 0;
822 guard.first_pending_at = None;
823 }
824 guard.last_error = None;
825 let _ = target_lsn;
826 }
827 Err(err) => {
828 guard.last_error = Some(err.to_string());
829 }
830 }
831 cond.notify_all();
832 }
833 }
834}
835
836impl Drop for StoreCommitCoordinator {
837 fn drop(&mut self) {
838 let (state_lock, cond) = &*self.state;
839 let mut state = state_lock.lock();
841 state.shutdown = true;
842 cond.notify_all();
843 }
844}
845
846impl UnifiedStore {
847 pub(crate) fn deferred_store_wal_capture_active() -> bool {
848 deferred_store_wal_capture_active()
849 }
850
851 pub(crate) fn begin_deferred_store_wal_capture() {
852 begin_deferred_store_wal_capture();
853 }
854
855 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
856 take_deferred_store_wal_capture()
857 }
858
859 pub(crate) fn append_deferred_store_wal_actions(
860 &self,
861 actions: DeferredStoreWalActions,
862 ) -> Result<(), StoreError> {
863 if actions.actions.is_empty() {
864 return Ok(());
865 }
866 if self.config.embedded_wal_path.is_some() {
867 return self.append_embedded_store_wal_actions(&actions.actions);
868 }
869 if let Some(commit) = &self.commit {
872 commit
873 .append_actions(&actions.actions)
874 .map_err(StoreError::Io)
875 } else {
876 self.flush_paged_state()
877 }
878 }
879
880 pub(crate) fn append_vector_insert_record(
887 &self,
888 collection: &str,
889 entity_id: u64,
890 vector: &[f32],
891 ) -> std::io::Result<()> {
892 let Some(commit) = &self.commit else {
893 return Ok(());
894 };
895 let record = WalRecord::VectorInsert {
896 collection: collection.to_string(),
897 entity_id,
898 vector: vector.to_vec(),
899 };
900 commit.append_single_record(record)
901 }
902
903 pub(crate) fn append_probabilistic_delta_record(
904 &self,
905 kind: u8,
906 operation: u8,
907 name: &str,
908 operands: Vec<Vec<u8>>,
909 ) -> std::io::Result<()> {
910 let Some(commit) = &self.commit else {
911 return Ok(());
912 };
913 let record = WalRecord::ProbabilisticDelta {
914 kind,
915 operation,
916 name: name.to_string(),
917 operands,
918 };
919 commit.append_single_record(record)
920 }
921
922 pub(crate) fn finish_paged_write(
923 &self,
924 actions: impl IntoIterator<Item = StoreWalAction>,
925 ) -> Result<(), StoreError> {
926 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
927 if deferred_store_wal_capture_active() {
928 let captured = capture_deferred_store_wal_actions(actions);
929 debug_assert!(captured);
930 return Ok(());
931 }
932 if self.config.embedded_wal_path.is_some() {
933 return self.append_embedded_store_wal_actions(&actions);
934 }
935 match self.config.durability_mode {
936 DurabilityMode::Strict => {
937 if let Some(commit) = &self.commit {
938 commit.append_actions(&actions).map_err(StoreError::Io)?;
939 }
940 self.flush_paged_state()
941 }
942 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
943 if let Some(commit) = &self.commit {
944 commit.append_actions(&actions).map_err(StoreError::Io)?;
945 Ok(())
946 } else {
947 self.flush_paged_state()
948 }
949 }
950 }
951 }
952
953 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
954 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
955 self.apply_replayed_action(&action, true)
956 }
957
958 fn append_embedded_store_wal_actions(
959 &self,
960 actions: &[StoreWalAction],
961 ) -> Result<(), StoreError> {
962 let Some(path) = self.config.embedded_wal_path.as_deref() else {
963 return Ok(());
964 };
965 if actions.is_empty() {
966 return Ok(());
967 }
968 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
969 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
970 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
971 match crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads) {
972 Ok(_) => Ok(()),
973 Err(crate::api::RedDBError::InvalidOperation(msg))
974 if msg.contains("embedded wal region full") =>
975 {
976 let _vault_guard = self.vault_publication_lock.lock();
977 let snapshot = self.to_binary_dump_bytes();
978 crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
979 path,
980 &snapshot,
981 encoded_len,
982 )
983 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
984 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads)
985 .map(|_| ())
986 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
987 }
988 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
989 }
990 }
991
992 pub(crate) fn apply_replayed_action(
993 &self,
994 action: &StoreWalAction,
995 update_btrees: bool,
996 ) -> Result<(), StoreError> {
997 match action {
998 StoreWalAction::CreateCollection { name } => {
999 if self.get_collection(name).is_none() {
1000 let _ = self.create_collection_in_memory(name);
1001 }
1002 Ok(())
1003 }
1004 StoreWalAction::DropCollection { name } => self.drop_collection_in_memory(name),
1005 StoreWalAction::UpsertEntityRecord { collection, record } => {
1006 self.apply_replayed_upsert(collection, record, update_btrees)
1007 }
1008 StoreWalAction::DeleteEntityRecord {
1009 collection,
1010 entity_id,
1011 } => self.apply_replayed_delete(collection, EntityId::new(*entity_id), update_btrees),
1012 StoreWalAction::BulkUpsertEntityRecords {
1013 collection,
1014 records,
1015 } => {
1016 for record in records {
1017 self.apply_replayed_upsert(collection, record, update_btrees)?;
1018 }
1019 Ok(())
1020 }
1021 StoreWalAction::RefreshCollection {
1022 collection,
1023 records,
1024 } => self.apply_replayed_refresh_collection(collection, records, update_btrees),
1025 }
1026 }
1027
1028 pub fn refresh_collection(
1045 &self,
1046 name: &str,
1047 entities: Vec<UnifiedEntity>,
1048 ) -> Result<Vec<Vec<u8>>, StoreError> {
1049 let fv = self.format_version();
1050
1051 let new_manager = Arc::new(SegmentManager::with_config(
1054 name,
1055 self.config.manager_config.clone(),
1056 ));
1057
1058 let mut prepared = entities;
1059 for entity in &mut prepared {
1060 if entity.id.raw() == 0 {
1061 entity.id = self.next_entity_id();
1062 } else {
1063 self.register_entity_id(entity.id);
1064 }
1065 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1066 if *row_id == 0 {
1067 *row_id = new_manager.next_row_id();
1068 } else {
1069 new_manager.register_row_id(*row_id);
1070 }
1071 }
1072 entity.ensure_table_logical_id();
1073 }
1074
1075 let serialized: Vec<Vec<u8>> = prepared
1076 .iter()
1077 .map(|e| Self::serialize_entity_record(e, None, fv))
1078 .collect();
1079
1080 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1081 StoreError::Io(std::io::Error::other(format!(
1082 "refresh_collection: bulk_insert into new manager failed: {e}"
1083 )))
1084 })?;
1085
1086 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
1087
1088 self.finish_paged_write([StoreWalAction::RefreshCollection {
1089 collection: name.to_string(),
1090 records: serialized.clone(),
1091 }])?;
1092
1093 Ok(serialized)
1094 }
1095
1096 fn swap_collection_state(
1102 &self,
1103 name: &str,
1104 new_manager: Arc<SegmentManager>,
1105 prepared: &[UnifiedEntity],
1106 serialized: &[Vec<u8>],
1107 update_btrees: bool,
1108 ) {
1109 {
1110 let mut collections = self.collections.write();
1111 collections.insert(name.to_string(), new_manager);
1112 }
1113
1114 self.entity_cache
1115 .retain(|_, (collection, _)| collection != name);
1116 self.remove_from_graph_label_index_batch(
1117 name,
1118 &prepared.iter().map(|e| e.id).collect::<Vec<_>>(),
1119 );
1120
1121 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1122 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1123 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1124 .iter()
1125 .zip(serialized.iter())
1126 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1127 .collect();
1128 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1129 if !sorted.is_empty() {
1130 let _ = new_btree.bulk_insert_sorted(&sorted);
1131 }
1132 self.btree_indices
1133 .write()
1134 .insert(name.to_string(), new_btree);
1135 self.mark_paged_registry_dirty();
1136 }
1137 }
1138
1139 fn apply_replayed_refresh_collection(
1140 &self,
1141 collection: &str,
1142 records: &[Vec<u8>],
1143 update_btrees: bool,
1144 ) -> Result<(), StoreError> {
1145 let new_manager = Arc::new(SegmentManager::with_config(
1146 collection,
1147 self.config.manager_config.clone(),
1148 ));
1149
1150 let mut prepared: Vec<UnifiedEntity> = Vec::with_capacity(records.len());
1151 for record in records {
1152 let (entity, _metadata) =
1153 Self::deserialize_entity_record(record, self.format_version())?;
1154 self.register_entity_id(entity.id);
1155 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1156 new_manager.register_row_id(*row_id);
1157 }
1158 prepared.push(entity);
1159 }
1160
1161 if !prepared.is_empty() {
1162 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1163 StoreError::Io(std::io::Error::other(format!(
1164 "replay refresh_collection: bulk_insert failed: {e}"
1165 )))
1166 })?;
1167 }
1168
1169 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
1170 Ok(())
1171 }
1172
1173 pub fn refresh_collection_from_records(
1187 &self,
1188 name: &str,
1189 records: Vec<Vec<u8>>,
1190 ) -> Result<(), StoreError> {
1191 self.apply_replayed_refresh_collection(name, &records, true)?;
1192 self.finish_paged_write([StoreWalAction::RefreshCollection {
1193 collection: name.to_string(),
1194 records,
1195 }])?;
1196 Ok(())
1197 }
1198
1199 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1200 let mut collections = self.collections.write();
1201 if collections.contains_key(name) {
1202 return Ok(());
1203 }
1204 let manager = SegmentManager::with_config(name, self.config.manager_config.clone());
1205 collections.insert(name.to_string(), Arc::new(manager));
1206 self.mark_paged_registry_dirty();
1207 Ok(())
1208 }
1209
1210 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1211 let manager = {
1212 let mut collections = self.collections.write();
1213 match collections.remove(name) {
1214 Some(manager) => manager,
1215 None => return Ok(()),
1216 }
1217 };
1218
1219 let entities = manager.query_all(|_| true);
1220 let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
1221 for entity_id in &entity_ids {
1222 self.context_index.remove_entity(*entity_id);
1223 let _ = self.unindex_cross_refs(*entity_id);
1224 }
1225 self.btree_indices.write().remove(name);
1226 self.entity_cache.retain(|entity_id, (collection, _)| {
1227 collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
1228 });
1229 self.remove_from_graph_label_index_batch(name, &entity_ids);
1230 self.mark_paged_registry_dirty();
1231 Ok(())
1232 }
1233
1234 fn apply_replayed_upsert(
1235 &self,
1236 collection: &str,
1237 record: &[u8],
1238 update_btrees: bool,
1239 ) -> Result<(), StoreError> {
1240 self.create_collection_in_memory(collection)?;
1241 let (entity, metadata) = Self::deserialize_entity_record(record, self.format_version())?;
1242 let manager = self
1243 .get_collection(collection)
1244 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1245
1246 self.register_entity_id(entity.id);
1247 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1248 manager.register_row_id(*row_id);
1249 }
1250
1251 self.context_index.remove_entity(entity.id);
1252 let _ = self.unindex_cross_refs(entity.id);
1253 self.remove_from_graph_label_index(collection, entity.id);
1254
1255 if manager.get(entity.id).is_some() {
1256 manager
1257 .update_with_metadata(entity.clone(), metadata.as_ref())
1258 .map_err(StoreError::from)?;
1259 } else {
1260 manager.insert(entity.clone())?;
1261 if let Some(metadata) = metadata.as_ref() {
1262 manager.set_metadata(entity.id, metadata.clone())?;
1263 }
1264 }
1265
1266 self.context_index.index_entity(collection, &entity);
1267 if let EntityKind::GraphNode(node) = &entity.kind {
1268 self.update_graph_label_index(collection, &node.label, entity.id);
1269 }
1270 self.index_cross_refs(&entity, collection)?;
1271
1272 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1273 let mut btree_indices = self.btree_indices.write();
1274 let btree = btree_indices
1275 .entry(collection.to_string())
1276 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1277 let root_before = btree.root_page_id();
1278 let key = entity.id.raw().to_be_bytes();
1279 match btree.insert(&key, record) {
1280 Ok(_) => {}
1281 Err(BTreeError::DuplicateKey) => {
1282 let _ = btree.delete(&key);
1283 let _ = btree.insert(&key, record);
1284 }
1285 Err(err) => {
1286 return Err(StoreError::Io(io::Error::other(format!(
1287 "replay upsert btree error: {err}"
1288 ))));
1289 }
1290 }
1291 if root_before != btree.root_page_id() {
1292 self.mark_paged_registry_dirty();
1293 }
1294 }
1295
1296 Ok(())
1297 }
1298
1299 fn apply_replayed_delete(
1300 &self,
1301 collection: &str,
1302 id: EntityId,
1303 update_btrees: bool,
1304 ) -> Result<(), StoreError> {
1305 self.entity_cache.remove(id.raw());
1306 if let Some(manager) = self.get_collection(collection) {
1307 let deleted = manager.delete(id)?;
1308 if !deleted {
1309 return Ok(());
1310 }
1311 } else {
1312 return Ok(());
1313 }
1314
1315 if self.pager.is_some() && update_btrees {
1316 let btree_indices = self.btree_indices.read();
1317 if let Some(btree) = btree_indices.get(collection) {
1318 let root_before = btree.root_page_id();
1319 let key = id.raw().to_be_bytes();
1320 let _ = btree.delete(&key);
1321 if root_before != btree.root_page_id() {
1322 self.mark_paged_registry_dirty();
1323 }
1324 }
1325 }
1326
1327 let _ = self.unindex_cross_refs(id);
1328 self.remove_from_graph_label_index(collection, id);
1329 self.context_index.remove_entity(id);
1330 Ok(())
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337 use crate::api::{DurabilityMode, GroupCommitOptions};
1338 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1339 use std::time::SystemTime;
1340
1341 fn env_lock() -> &'static StdMutex<()> {
1346 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
1347 LOCK.get_or_init(|| StdMutex::new(()))
1348 }
1349
1350 fn temp_wal(name: &str) -> PathBuf {
1351 let nanos = SystemTime::now()
1352 .duration_since(std::time::UNIX_EPOCH)
1353 .unwrap()
1354 .as_nanos();
1355 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
1356 &std::env::temp_dir(),
1357 name,
1358 std::process::id(),
1359 nanos,
1360 );
1361 let _ = std::fs::remove_file(&path);
1362 path
1363 }
1364
1365 fn replay_crash_payload(id: u64, round: u64) -> String {
1366 let mut state = id * 31 + round * 13 + 1;
1367 (0..600)
1368 .map(|_| {
1369 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1370 char::from(b'!' + ((state >> 24) % 90) as u8)
1371 })
1372 .collect()
1373 }
1374
1375 #[test]
1376 fn paged_wal_replay_survives_immediate_process_exit() {
1377 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
1378 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
1379 if let Some(path) = std::env::var_os(CHILD_PATH) {
1380 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
1381 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
1382 std::process::exit(0);
1384 }
1385 let directory = tempfile::tempdir().unwrap();
1386 let path = directory.path().join("data.rdb");
1387 {
1388 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
1389 store.create_collection("rows").unwrap();
1390 for id in 1..=400 {
1391 store
1392 .insert(
1393 "rows",
1394 UnifiedEntity::table_row(
1395 EntityId::new(id),
1396 "rows",
1397 id,
1398 vec![Value::text(format!("initial-{id}"))],
1399 ),
1400 )
1401 .unwrap();
1402 }
1403 store.persist().unwrap();
1404 }
1405 {
1406 let coordinator = StoreCommitCoordinator::open(
1407 reddb_file::layout::unified_wal_path(&path),
1408 DurabilityMode::Strict,
1409 GroupCommitOptions::default(),
1410 )
1411 .unwrap();
1412 for round in 0..3 {
1413 let actions = (1..=400)
1414 .map(|id| {
1415 let payload = replay_crash_payload(id, round);
1416 let entity = UnifiedEntity::table_row(
1417 EntityId::new(id),
1418 "rows",
1419 id,
1420 vec![Value::text(payload)],
1421 );
1422 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
1423 })
1424 .collect::<Vec<_>>();
1425 coordinator.append_actions(&actions).unwrap();
1426 }
1427 }
1428 let status = std::process::Command::new(std::env::current_exe().unwrap())
1429 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
1430 .env(CHILD_PATH, &path).status().unwrap();
1431 assert!(status.success(), "replay process failed");
1432 let reopened = UnifiedStore::open_with_config(&path, config())
1433 .expect("cold reopen after replay crash");
1434 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
1435 for id in 1..=400 {
1436 let entity = reopened
1437 .get("rows", EntityId::new(id))
1438 .expect("retained entity");
1439 let EntityData::Row(row) = entity.data else {
1440 panic!("expected row")
1441 };
1442 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
1443 }
1444 drop(reopened);
1445 let stable_bytes = std::fs::metadata(&path).unwrap().len();
1446 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
1447 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
1448 assert_eq!(
1449 std::fs::metadata(&path).unwrap().len(),
1450 stable_bytes,
1451 "idempotent replay must not allocate another tree generation"
1452 );
1453 }
1454
1455 #[test]
1466 fn group_commit_coalesces_concurrent_autocommits() {
1467 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1468 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1471
1472 let path = temp_wal("coalesce");
1473 let coord = Arc::new(
1474 StoreCommitCoordinator::open(
1475 path.clone(),
1476 DurabilityMode::WalDurableGrouped,
1477 GroupCommitOptions::default(),
1478 )
1479 .expect("open commit coordinator"),
1480 );
1481
1482 const WRITERS: usize = 32;
1483 let barrier = Arc::new(Barrier::new(WRITERS));
1484 let mut handles = Vec::with_capacity(WRITERS);
1485 for tx in 0..WRITERS {
1486 let coord_c = Arc::clone(&coord);
1487 let barrier_c = Arc::clone(&barrier);
1488 handles.push(std::thread::spawn(move || {
1489 barrier_c.wait();
1493 let action = StoreWalAction::CreateCollection {
1494 name: format!("c{tx}"),
1495 };
1496 coord_c
1497 .append_actions(std::slice::from_ref(&action))
1498 .expect("append_actions");
1499 }));
1500 }
1501 for h in handles {
1502 h.join().expect("writer thread");
1503 }
1504
1505 let fsyncs = coord.fsync_count();
1506 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
1507 assert!(
1508 fsyncs < WRITERS as u64,
1509 "expected fsyncs ({fsyncs}) to be strictly less than \
1510 concurrent writers ({WRITERS}); coalescing failed"
1511 );
1512
1513 drop(coord);
1514 let _ = std::fs::remove_file(&path);
1515 }
1516
1517 #[test]
1523 fn zero_window_disables_coalescing_floor() {
1524 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1525 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1526
1527 let path = temp_wal("zero_window");
1528 let coord = Arc::new(
1529 StoreCommitCoordinator::open(
1530 path.clone(),
1531 DurabilityMode::WalDurableGrouped,
1532 GroupCommitOptions::default(),
1533 )
1534 .expect("open commit coordinator"),
1535 );
1536
1537 const WRITERS: usize = 8;
1538 let barrier = Arc::new(Barrier::new(WRITERS));
1539 let mut handles = Vec::with_capacity(WRITERS);
1540 for tx in 0..WRITERS {
1541 let coord_c = Arc::clone(&coord);
1542 let barrier_c = Arc::clone(&barrier);
1543 handles.push(std::thread::spawn(move || {
1544 barrier_c.wait();
1545 let action = StoreWalAction::CreateCollection {
1546 name: format!("z{tx}"),
1547 };
1548 coord_c
1549 .append_actions(std::slice::from_ref(&action))
1550 .expect("append_actions");
1551 }));
1552 }
1553 for h in handles {
1554 h.join().expect("writer thread");
1555 }
1556
1557 let fsyncs = coord.fsync_count();
1563 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
1564
1565 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1566 drop(coord);
1567 let _ = std::fs::remove_file(&path);
1568 }
1569
1570 #[test]
1572 fn resolve_window_precedence() {
1573 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1574 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1576 let cfg = GroupCommitOptions::default();
1577 assert_eq!(
1578 StoreCommitCoordinator::resolve_window(&cfg),
1579 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
1580 );
1581
1582 let cfg_ms = GroupCommitOptions {
1584 window_ms: 5,
1585 ..GroupCommitOptions::default()
1586 };
1587 assert_eq!(
1588 StoreCommitCoordinator::resolve_window(&cfg_ms),
1589 Duration::from_millis(5)
1590 );
1591
1592 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
1594 assert_eq!(
1595 StoreCommitCoordinator::resolve_window(&cfg),
1596 Duration::from_micros(750)
1597 );
1598 assert_eq!(
1599 StoreCommitCoordinator::resolve_window(&cfg_ms),
1600 Duration::from_micros(750)
1601 );
1602
1603 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1605 assert_eq!(
1606 StoreCommitCoordinator::resolve_window(&cfg),
1607 Duration::from_micros(0)
1608 );
1609
1610 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1611 }
1612}