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 should_open(path: &Path, mode: DurabilityMode) -> bool {
385 matches!(
386 mode,
387 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
388 ) || path.exists()
389 }
390
391 pub(crate) fn open(
392 wal_path: impl Into<PathBuf>,
393 mode: DurabilityMode,
394 config: crate::api::GroupCommitOptions,
395 ) -> io::Result<Self> {
396 let wal_path = wal_path.into();
397 let wal = WalWriter::open(&wal_path)?;
398 let initial_durable_lsn = wal.durable_lsn();
399 let initial_current_lsn = wal.current_lsn();
400 let wal = Arc::new(WalMutex::new(wal));
401 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
402 let state = Arc::new((
403 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
404 CommitStateCondvar::new(),
405 ));
406 let fsync_count = Arc::new(AtomicU64::new(0));
407
408 if matches!(
409 mode,
410 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
411 ) {
412 let wal_bg = Arc::clone(&wal);
413 let queue_bg = Arc::clone(&queue);
414 let state_bg = Arc::clone(&state);
415 let fsync_bg = Arc::clone(&fsync_count);
416 let window = Self::resolve_window(&config);
431 let max_statements = config.max_statements.max(1);
432 let max_wal_bytes = config.max_wal_bytes.max(1);
433 std::thread::spawn(move || {
434 Self::run_group_commit_loop(
435 wal_bg,
436 queue_bg,
437 state_bg,
438 fsync_bg,
439 window,
440 max_statements,
441 max_wal_bytes,
442 );
443 });
444 }
445
446 Ok(Self {
447 mode,
448 config,
449 wal_path,
450 wal,
451 queue,
452 state,
453 fsync_count,
454 })
455 }
456
457 fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
466 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
467 if let Ok(parsed) = raw.parse::<u64>() {
468 return Duration::from_micros(parsed);
469 }
470 }
471 if config.window_ms != 0 {
472 return Duration::from_millis(config.window_ms);
473 }
474 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
475 }
476
477 #[cfg(test)]
481 pub(crate) fn fsync_count(&self) -> u64 {
482 self.fsync_count.load(Ordering::Relaxed)
483 }
484
485 pub(crate) fn buffered_bytes(&self) -> u64 {
489 self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
490 }
491
492 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
493 if actions.is_empty() {
494 return Ok(());
495 }
496
497 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
498
499 if matches!(self.mode, DurabilityMode::Strict) {
504 let commit_lsn = {
505 let mut wal = self.wal.lock();
506 wal.append(&WalRecord::TxCommitBatch {
507 tx_id,
508 actions: actions.iter().map(StoreWalAction::encode).collect(),
509 })?;
510 wal.current_lsn()
511 };
512 self.force_sync()?;
513 let _ = commit_lsn;
514 return Ok(());
515 }
516
517 let encoded_actions: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
521 let wal_bytes = encoded_actions.iter().fold(0u64, |total, payload| {
522 total.saturating_add(payload.len() as u64)
523 });
524 let blob = WalRecord::TxCommitBatch {
525 tx_id,
526 actions: encoded_actions,
527 }
528 .encode();
529
530 let commit_lsn = self.queue.enqueue(blob);
531 self.wait_until_durable(commit_lsn, wal_bytes)?;
532 Ok(())
533 }
534
535 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
540 let blob = record.encode();
541 let wal_bytes = blob.len() as u64;
542 if matches!(self.mode, DurabilityMode::Strict) {
543 {
544 let mut wal = self.wal.lock();
545 wal.append(&record)?;
546 }
547 self.force_sync()?;
548 return Ok(());
549 }
550 let commit_lsn = self.queue.enqueue(blob);
551 self.wait_until_durable(commit_lsn, wal_bytes)?;
552 Ok(())
553 }
554
555 pub(crate) fn force_sync(&self) -> io::Result<()> {
556 {
557 let mut wal = self.wal.lock();
558 wal.sync()?;
559 self.fsync_count.fetch_add(1, Ordering::Relaxed);
560 let durable = wal.durable_lsn();
561 drop(wal);
562 let (state_lock, cond) = &*self.state;
563 let mut state = state_lock.lock();
564 state.durable_lsn = durable;
565 state.pending_target_lsn = durable.max(state.pending_target_lsn);
566 state.pending_statements = 0;
567 state.pending_wal_bytes = 0;
568 state.first_pending_at = None;
569 state.last_error = None;
570 cond.notify_all();
571 }
572 Ok(())
573 }
574
575 pub(crate) fn truncate(&self) -> io::Result<()> {
576 let mut wal = self.wal.lock();
577 wal.truncate()?;
578 let durable = wal.durable_lsn();
579 let current = wal.current_lsn();
580 drop(wal);
581
582 self.queue.reset(current);
587
588 let (state_lock, cond) = &*self.state;
589 let mut state = state_lock.lock();
590 state.durable_lsn = durable;
591 state.pending_target_lsn = durable;
592 state.pending_statements = 0;
593 state.pending_wal_bytes = 0;
594 state.first_pending_at = None;
595 state.last_error = None;
596 cond.notify_all();
597 Ok(())
598 }
599
600 pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
601 if !self.wal_path.exists() {
602 return Ok(());
603 }
604
605 let reader = match WalReader::open(&self.wal_path) {
606 Ok(reader) => reader,
607 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
608 Err(err) => return Err(err),
609 };
610
611 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
612 let mut pending = Vec::<(u64, Vec<u8>)>::new();
613
614 for record in reader.iter() {
615 let (_lsn, record) = match record {
616 Ok(record) => record,
617 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
618 Err(err) => return Err(err),
619 };
620 match record {
621 WalRecord::TxCommitBatch { actions, .. } => {
622 for payload in actions {
623 let action = StoreWalAction::decode(&payload)?;
624 store.apply_replayed_action(&action).map_err(|err| {
625 io::Error::other(format!("failed to replay store wal action: {err}"))
626 })?;
627 }
628 }
629 WalRecord::Begin { tx_id } => {
630 tx_states.insert(tx_id, false);
631 }
632 WalRecord::Commit { tx_id } => {
633 tx_states.insert(tx_id, true);
634 }
635 WalRecord::Rollback { tx_id } => {
636 tx_states.remove(&tx_id);
637 }
638 WalRecord::PageWrite {
639 tx_id,
640 page_id: _,
641 data,
642 } => pending.push((tx_id, data)),
643 WalRecord::Checkpoint { .. } => {}
644 WalRecord::VectorInsert {
645 collection,
646 entity_id,
647 vector,
648 } => {
649 let mut map = store.replayed_turbo_inserts.lock();
655 map.entry(collection).or_default().push((entity_id, vector));
656 }
657 WalRecord::ProbabilisticDelta {
658 kind,
659 operation,
660 name,
661 operands,
662 } => {
663 let mut deltas = store.replayed_probabilistic_deltas.lock();
664 deltas.push((kind, operation, name, operands));
665 }
666 WalRecord::FullPageImage { .. } => {
667 }
669 }
670 }
671
672 for (tx_id, payload) in pending {
673 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
674 continue;
675 }
676 let action = StoreWalAction::decode(&payload)?;
677 store.apply_replayed_action(&action).map_err(|err| {
678 io::Error::other(format!("failed to replay store wal action: {err}"))
679 })?;
680 }
681
682 Ok(())
683 }
684
685 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
686 match self.mode {
687 DurabilityMode::Strict => self.force_sync(),
688 DurabilityMode::Async => {
693 let (state_lock, cond) = &*self.state;
694 let mut state = state_lock.lock();
695 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
696 state.pending_statements = state.pending_statements.saturating_add(1);
697 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
698 state.first_pending_at.get_or_insert_with(Instant::now);
699 cond.notify_all();
700 Ok(())
701 }
702 DurabilityMode::WalDurableGrouped => {
703 let (state_lock, cond) = &*self.state;
704 let mut state = state_lock.lock();
705 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
706 state.pending_statements = state.pending_statements.saturating_add(1);
707 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
708 state.first_pending_at.get_or_insert_with(Instant::now);
709 cond.notify_all();
710
711 loop {
712 if let Some(err) = state.last_error.clone() {
713 return Err(io::Error::other(err));
714 }
715 if state.durable_lsn >= target_lsn {
716 return Ok(());
717 }
718 cond.wait(&mut state);
721 }
722 }
723 }
724 }
725
726 fn run_group_commit_loop(
727 wal: Arc<WalMutex>,
728 queue: Arc<WalAppendQueue>,
729 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
730 fsync_count: Arc<AtomicU64>,
731 window: Duration,
732 max_statements: usize,
733 max_wal_bytes: u64,
734 ) {
735 let (state_lock, cond) = &*state;
736 loop {
737 let target_lsn = {
738 let mut guard = state_lock.lock();
739
740 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
741 cond.wait(&mut guard);
742 }
743
744 if guard.shutdown {
745 return;
746 }
747
748 let immediate = window.is_zero()
749 || guard.pending_statements >= max_statements
750 || guard.pending_wal_bytes >= max_wal_bytes;
751
752 if !immediate {
753 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
754 let now = Instant::now();
755 if now < deadline {
756 let timeout = deadline.saturating_duration_since(now);
757 let _ = cond.wait_for(&mut guard, timeout);
758 if guard.shutdown {
759 return;
760 }
761 if guard.pending_target_lsn <= guard.durable_lsn {
762 continue;
763 }
764 let should_wait_again = guard.pending_statements < max_statements
765 && guard.pending_wal_bytes < max_wal_bytes
766 && guard
767 .first_pending_at
768 .map(|first| first.elapsed() < window)
769 .unwrap_or(false);
770 if should_wait_again {
771 continue;
772 }
773 }
774 }
775
776 guard.pending_target_lsn
777 };
778
779 let batches = queue.drain_sorted();
785
786 let sync_result = {
787 let mut wal = wal.lock();
788 let mut write_err: Option<io::Error> = None;
789 for (_lsn, bytes) in batches {
790 if let Err(e) = wal.append_bytes(&bytes) {
791 write_err = Some(e);
792 break;
793 }
794 }
795 match write_err {
796 Some(e) => Err(e),
797 None => wal.sync().map(|_| {
798 fsync_count.fetch_add(1, Ordering::Relaxed);
803 wal.durable_lsn()
804 }),
805 }
806 };
807
808 let more_pending = queue.has_pending();
813
814 let mut guard = state_lock.lock();
815 match sync_result {
816 Ok(durable_lsn) => {
817 guard.durable_lsn = durable_lsn;
818 if !more_pending {
819 guard.pending_statements = 0;
820 guard.pending_wal_bytes = 0;
821 guard.first_pending_at = None;
822 }
823 guard.last_error = None;
824 let _ = target_lsn;
825 }
826 Err(err) => {
827 guard.last_error = Some(err.to_string());
828 }
829 }
830 cond.notify_all();
831 }
832 }
833}
834
835impl Drop for StoreCommitCoordinator {
836 fn drop(&mut self) {
837 let (state_lock, cond) = &*self.state;
838 let mut state = state_lock.lock();
840 state.shutdown = true;
841 cond.notify_all();
842 }
843}
844
845impl UnifiedStore {
846 pub(crate) fn deferred_store_wal_capture_active() -> bool {
847 deferred_store_wal_capture_active()
848 }
849
850 pub(crate) fn begin_deferred_store_wal_capture() {
851 begin_deferred_store_wal_capture();
852 }
853
854 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
855 take_deferred_store_wal_capture()
856 }
857
858 pub(crate) fn append_deferred_store_wal_actions(
859 &self,
860 actions: DeferredStoreWalActions,
861 ) -> Result<(), StoreError> {
862 if actions.actions.is_empty() {
863 return Ok(());
864 }
865 if self.config.embedded_wal_path.is_some() {
866 return self.append_embedded_store_wal_actions(&actions.actions);
867 }
868 match self.config.durability_mode {
869 DurabilityMode::Strict => self.flush_paged_state(),
870 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
871 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 }
881
882 pub(crate) fn append_vector_insert_record(
889 &self,
890 collection: &str,
891 entity_id: u64,
892 vector: &[f32],
893 ) -> std::io::Result<()> {
894 let Some(commit) = &self.commit else {
895 return Ok(());
896 };
897 let record = WalRecord::VectorInsert {
898 collection: collection.to_string(),
899 entity_id,
900 vector: vector.to_vec(),
901 };
902 commit.append_single_record(record)
903 }
904
905 pub(crate) fn append_probabilistic_delta_record(
906 &self,
907 kind: u8,
908 operation: u8,
909 name: &str,
910 operands: Vec<Vec<u8>>,
911 ) -> std::io::Result<()> {
912 let Some(commit) = &self.commit else {
913 return Ok(());
914 };
915 let record = WalRecord::ProbabilisticDelta {
916 kind,
917 operation,
918 name: name.to_string(),
919 operands,
920 };
921 commit.append_single_record(record)
922 }
923
924 pub(crate) fn finish_paged_write(
925 &self,
926 actions: impl IntoIterator<Item = StoreWalAction>,
927 ) -> Result<(), StoreError> {
928 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
929 if deferred_store_wal_capture_active() {
930 let captured = capture_deferred_store_wal_actions(actions);
931 debug_assert!(captured);
932 return Ok(());
933 }
934 if self.config.embedded_wal_path.is_some() {
935 return self.append_embedded_store_wal_actions(&actions);
936 }
937 match self.config.durability_mode {
938 DurabilityMode::Strict => self.flush_paged_state(),
939 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
940 if let Some(commit) = &self.commit {
941 commit.append_actions(&actions).map_err(StoreError::Io)?;
942 Ok(())
943 } else {
944 self.flush_paged_state()
945 }
946 }
947 }
948 }
949
950 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
951 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
952 self.apply_replayed_action(&action)
953 }
954
955 fn append_embedded_store_wal_actions(
956 &self,
957 actions: &[StoreWalAction],
958 ) -> Result<(), StoreError> {
959 let Some(path) = self.config.embedded_wal_path.as_deref() else {
960 return Ok(());
961 };
962 if actions.is_empty() {
963 return Ok(());
964 }
965 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
966 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
967 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
968 match crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads) {
969 Ok(_) => Ok(()),
970 Err(crate::api::RedDBError::InvalidOperation(msg))
971 if msg.contains("embedded wal region full") =>
972 {
973 let snapshot = self.to_binary_dump_bytes();
974 crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
975 path,
976 &snapshot,
977 encoded_len,
978 )
979 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
980 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads)
981 .map(|_| ())
982 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
983 }
984 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
985 }
986 }
987
988 pub(crate) fn apply_replayed_action(&self, action: &StoreWalAction) -> Result<(), StoreError> {
989 match action {
990 StoreWalAction::CreateCollection { name } => {
991 if self.get_collection(name).is_none() {
992 let _ = self.create_collection_in_memory(name);
993 }
994 Ok(())
995 }
996 StoreWalAction::DropCollection { name } => self.drop_collection_in_memory(name),
997 StoreWalAction::UpsertEntityRecord { collection, record } => {
998 self.apply_replayed_upsert(collection, record)
999 }
1000 StoreWalAction::DeleteEntityRecord {
1001 collection,
1002 entity_id,
1003 } => self.apply_replayed_delete(collection, EntityId::new(*entity_id)),
1004 StoreWalAction::BulkUpsertEntityRecords {
1005 collection,
1006 records,
1007 } => {
1008 for record in records {
1009 self.apply_replayed_upsert(collection, record)?;
1010 }
1011 Ok(())
1012 }
1013 StoreWalAction::RefreshCollection {
1014 collection,
1015 records,
1016 } => self.apply_replayed_refresh_collection(collection, records),
1017 }
1018 }
1019
1020 pub fn refresh_collection(
1037 &self,
1038 name: &str,
1039 entities: Vec<UnifiedEntity>,
1040 ) -> Result<Vec<Vec<u8>>, StoreError> {
1041 let fv = self.format_version();
1042
1043 let new_manager = Arc::new(SegmentManager::with_config(
1046 name,
1047 self.config.manager_config.clone(),
1048 ));
1049
1050 let mut prepared = entities;
1051 for entity in &mut prepared {
1052 if entity.id.raw() == 0 {
1053 entity.id = self.next_entity_id();
1054 } else {
1055 self.register_entity_id(entity.id);
1056 }
1057 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1058 if *row_id == 0 {
1059 *row_id = new_manager.next_row_id();
1060 } else {
1061 new_manager.register_row_id(*row_id);
1062 }
1063 }
1064 entity.ensure_table_logical_id();
1065 }
1066
1067 let serialized: Vec<Vec<u8>> = prepared
1068 .iter()
1069 .map(|e| Self::serialize_entity_record(e, None, fv))
1070 .collect();
1071
1072 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1073 StoreError::Io(std::io::Error::other(format!(
1074 "refresh_collection: bulk_insert into new manager failed: {e}"
1075 )))
1076 })?;
1077
1078 self.swap_collection_state(name, new_manager, &prepared, &serialized);
1079
1080 self.finish_paged_write([StoreWalAction::RefreshCollection {
1081 collection: name.to_string(),
1082 records: serialized.clone(),
1083 }])?;
1084
1085 Ok(serialized)
1086 }
1087
1088 fn swap_collection_state(
1094 &self,
1095 name: &str,
1096 new_manager: Arc<SegmentManager>,
1097 prepared: &[UnifiedEntity],
1098 serialized: &[Vec<u8>],
1099 ) {
1100 {
1101 let mut collections = self.collections.write();
1102 collections.insert(name.to_string(), new_manager);
1103 }
1104
1105 self.entity_cache
1106 .retain(|_, (collection, _)| collection != name);
1107 self.remove_from_graph_label_index_batch(
1108 name,
1109 &prepared.iter().map(|e| e.id).collect::<Vec<_>>(),
1110 );
1111
1112 if let Some(pager) = &self.pager {
1113 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1114 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1115 .iter()
1116 .zip(serialized.iter())
1117 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1118 .collect();
1119 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1120 if !sorted.is_empty() {
1121 let _ = new_btree.bulk_insert_sorted(&sorted);
1122 }
1123 self.btree_indices
1124 .write()
1125 .insert(name.to_string(), new_btree);
1126 self.mark_paged_registry_dirty();
1127 }
1128 }
1129
1130 fn apply_replayed_refresh_collection(
1131 &self,
1132 collection: &str,
1133 records: &[Vec<u8>],
1134 ) -> Result<(), StoreError> {
1135 let new_manager = Arc::new(SegmentManager::with_config(
1136 collection,
1137 self.config.manager_config.clone(),
1138 ));
1139
1140 let mut prepared: Vec<UnifiedEntity> = Vec::with_capacity(records.len());
1141 for record in records {
1142 let (entity, _metadata) =
1143 Self::deserialize_entity_record(record, self.format_version())?;
1144 self.register_entity_id(entity.id);
1145 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1146 new_manager.register_row_id(*row_id);
1147 }
1148 prepared.push(entity);
1149 }
1150
1151 if !prepared.is_empty() {
1152 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1153 StoreError::Io(std::io::Error::other(format!(
1154 "replay refresh_collection: bulk_insert failed: {e}"
1155 )))
1156 })?;
1157 }
1158
1159 self.swap_collection_state(collection, new_manager, &prepared, records);
1160 Ok(())
1161 }
1162
1163 pub fn refresh_collection_from_records(
1177 &self,
1178 name: &str,
1179 records: Vec<Vec<u8>>,
1180 ) -> Result<(), StoreError> {
1181 self.apply_replayed_refresh_collection(name, &records)?;
1182 self.finish_paged_write([StoreWalAction::RefreshCollection {
1183 collection: name.to_string(),
1184 records,
1185 }])?;
1186 Ok(())
1187 }
1188
1189 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1190 let mut collections = self.collections.write();
1191 if collections.contains_key(name) {
1192 return Ok(());
1193 }
1194 let manager = SegmentManager::with_config(name, self.config.manager_config.clone());
1195 collections.insert(name.to_string(), Arc::new(manager));
1196 self.mark_paged_registry_dirty();
1197 Ok(())
1198 }
1199
1200 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1201 let manager = {
1202 let mut collections = self.collections.write();
1203 match collections.remove(name) {
1204 Some(manager) => manager,
1205 None => return Ok(()),
1206 }
1207 };
1208
1209 let entities = manager.query_all(|_| true);
1210 let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
1211 for entity_id in &entity_ids {
1212 self.context_index.remove_entity(*entity_id);
1213 let _ = self.unindex_cross_refs(*entity_id);
1214 }
1215 self.btree_indices.write().remove(name);
1216 self.entity_cache.retain(|entity_id, (collection, _)| {
1217 collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
1218 });
1219 self.remove_from_graph_label_index_batch(name, &entity_ids);
1220 self.mark_paged_registry_dirty();
1221 Ok(())
1222 }
1223
1224 fn apply_replayed_upsert(&self, collection: &str, record: &[u8]) -> Result<(), StoreError> {
1225 self.create_collection_in_memory(collection)?;
1226 let (entity, metadata) = Self::deserialize_entity_record(record, self.format_version())?;
1227 let manager = self
1228 .get_collection(collection)
1229 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1230
1231 self.register_entity_id(entity.id);
1232 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1233 manager.register_row_id(*row_id);
1234 }
1235
1236 self.context_index.remove_entity(entity.id);
1237 let _ = self.unindex_cross_refs(entity.id);
1238 self.remove_from_graph_label_index(collection, entity.id);
1239
1240 if manager.get(entity.id).is_some() {
1241 manager
1242 .update_with_metadata(entity.clone(), metadata.as_ref())
1243 .map_err(StoreError::from)?;
1244 } else {
1245 manager.insert(entity.clone())?;
1246 if let Some(metadata) = metadata.as_ref() {
1247 manager.set_metadata(entity.id, metadata.clone())?;
1248 }
1249 }
1250
1251 self.context_index.index_entity(collection, &entity);
1252 if let EntityKind::GraphNode(node) = &entity.kind {
1253 self.update_graph_label_index(collection, &node.label, entity.id);
1254 }
1255 self.index_cross_refs(&entity, collection)?;
1256
1257 if let Some(pager) = &self.pager {
1258 let mut btree_indices = self.btree_indices.write();
1259 let btree = btree_indices
1260 .entry(collection.to_string())
1261 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1262 let root_before = btree.root_page_id();
1263 let key = entity.id.raw().to_be_bytes();
1264 match btree.insert(&key, record) {
1265 Ok(_) => {}
1266 Err(BTreeError::DuplicateKey) => {
1267 let _ = btree.delete(&key);
1268 let _ = btree.insert(&key, record);
1269 }
1270 Err(err) => {
1271 return Err(StoreError::Io(io::Error::other(format!(
1272 "replay upsert btree error: {err}"
1273 ))));
1274 }
1275 }
1276 if root_before != btree.root_page_id() {
1277 self.mark_paged_registry_dirty();
1278 }
1279 }
1280
1281 Ok(())
1282 }
1283
1284 fn apply_replayed_delete(&self, collection: &str, id: EntityId) -> Result<(), StoreError> {
1285 self.entity_cache.remove(id.raw());
1286 if let Some(manager) = self.get_collection(collection) {
1287 let deleted = manager.delete(id)?;
1288 if !deleted {
1289 return Ok(());
1290 }
1291 } else {
1292 return Ok(());
1293 }
1294
1295 if let Some(_pager) = &self.pager {
1296 let btree_indices = self.btree_indices.read();
1297 if let Some(btree) = btree_indices.get(collection) {
1298 let root_before = btree.root_page_id();
1299 let key = id.raw().to_be_bytes();
1300 let _ = btree.delete(&key);
1301 if root_before != btree.root_page_id() {
1302 self.mark_paged_registry_dirty();
1303 }
1304 }
1305 }
1306
1307 let _ = self.unindex_cross_refs(id);
1308 self.remove_from_graph_label_index(collection, id);
1309 self.context_index.remove_entity(id);
1310 Ok(())
1311 }
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use super::*;
1317 use crate::api::{DurabilityMode, GroupCommitOptions};
1318 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1319 use std::time::SystemTime;
1320
1321 fn env_lock() -> &'static StdMutex<()> {
1326 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
1327 LOCK.get_or_init(|| StdMutex::new(()))
1328 }
1329
1330 fn temp_wal(name: &str) -> PathBuf {
1331 let nanos = SystemTime::now()
1332 .duration_since(std::time::UNIX_EPOCH)
1333 .unwrap()
1334 .as_nanos();
1335 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
1336 &std::env::temp_dir(),
1337 name,
1338 std::process::id(),
1339 nanos,
1340 );
1341 let _ = std::fs::remove_file(&path);
1342 path
1343 }
1344
1345 #[test]
1356 fn group_commit_coalesces_concurrent_autocommits() {
1357 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1358 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1361
1362 let path = temp_wal("coalesce");
1363 let coord = Arc::new(
1364 StoreCommitCoordinator::open(
1365 path.clone(),
1366 DurabilityMode::WalDurableGrouped,
1367 GroupCommitOptions::default(),
1368 )
1369 .expect("open commit coordinator"),
1370 );
1371
1372 const WRITERS: usize = 32;
1373 let barrier = Arc::new(Barrier::new(WRITERS));
1374 let mut handles = Vec::with_capacity(WRITERS);
1375 for tx in 0..WRITERS {
1376 let coord_c = Arc::clone(&coord);
1377 let barrier_c = Arc::clone(&barrier);
1378 handles.push(std::thread::spawn(move || {
1379 barrier_c.wait();
1383 let action = StoreWalAction::CreateCollection {
1384 name: format!("c{tx}"),
1385 };
1386 coord_c
1387 .append_actions(std::slice::from_ref(&action))
1388 .expect("append_actions");
1389 }));
1390 }
1391 for h in handles {
1392 h.join().expect("writer thread");
1393 }
1394
1395 let fsyncs = coord.fsync_count();
1396 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
1397 assert!(
1398 fsyncs < WRITERS as u64,
1399 "expected fsyncs ({fsyncs}) to be strictly less than \
1400 concurrent writers ({WRITERS}); coalescing failed"
1401 );
1402
1403 drop(coord);
1404 let _ = std::fs::remove_file(&path);
1405 }
1406
1407 #[test]
1413 fn zero_window_disables_coalescing_floor() {
1414 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1415 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1416
1417 let path = temp_wal("zero_window");
1418 let coord = Arc::new(
1419 StoreCommitCoordinator::open(
1420 path.clone(),
1421 DurabilityMode::WalDurableGrouped,
1422 GroupCommitOptions::default(),
1423 )
1424 .expect("open commit coordinator"),
1425 );
1426
1427 const WRITERS: usize = 8;
1428 let barrier = Arc::new(Barrier::new(WRITERS));
1429 let mut handles = Vec::with_capacity(WRITERS);
1430 for tx in 0..WRITERS {
1431 let coord_c = Arc::clone(&coord);
1432 let barrier_c = Arc::clone(&barrier);
1433 handles.push(std::thread::spawn(move || {
1434 barrier_c.wait();
1435 let action = StoreWalAction::CreateCollection {
1436 name: format!("z{tx}"),
1437 };
1438 coord_c
1439 .append_actions(std::slice::from_ref(&action))
1440 .expect("append_actions");
1441 }));
1442 }
1443 for h in handles {
1444 h.join().expect("writer thread");
1445 }
1446
1447 let fsyncs = coord.fsync_count();
1453 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
1454
1455 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1456 drop(coord);
1457 let _ = std::fs::remove_file(&path);
1458 }
1459
1460 #[test]
1462 fn resolve_window_precedence() {
1463 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1464 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1466 let cfg = GroupCommitOptions::default();
1467 assert_eq!(
1468 StoreCommitCoordinator::resolve_window(&cfg),
1469 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
1470 );
1471
1472 let cfg_ms = GroupCommitOptions {
1474 window_ms: 5,
1475 ..GroupCommitOptions::default()
1476 };
1477 assert_eq!(
1478 StoreCommitCoordinator::resolve_window(&cfg_ms),
1479 Duration::from_millis(5)
1480 );
1481
1482 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
1484 assert_eq!(
1485 StoreCommitCoordinator::resolve_window(&cfg),
1486 Duration::from_micros(750)
1487 );
1488 assert_eq!(
1489 StoreCommitCoordinator::resolve_window(&cfg_ms),
1490 Duration::from_micros(750)
1491 );
1492
1493 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1495 assert_eq!(
1496 StoreCommitCoordinator::resolve_window(&cfg),
1497 Duration::from_micros(0)
1498 );
1499
1500 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1501 }
1502}