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}
281
282struct WalQueueState {
283 next_lsn: u64,
284 entries: Vec<(u64, Vec<u8>)>,
285}
286
287impl WalAppendQueue {
288 fn new(initial_lsn: u64) -> Self {
289 Self {
290 pending: parking_lot::Mutex::new(WalQueueState {
291 next_lsn: initial_lsn,
292 entries: Vec::with_capacity(64),
293 }),
294 }
295 }
296
297 fn enqueue(&self, bytes: Vec<u8>) -> u64 {
303 let len = bytes.len() as u64;
304 let mut state = self.pending.lock();
305 let start_lsn = state.next_lsn;
306 state.next_lsn = start_lsn + len;
307 state.entries.push((start_lsn, bytes));
308 start_lsn + len
309 }
310
311 fn drain_sorted(&self) -> Vec<(u64, Vec<u8>)> {
315 let mut state = self.pending.lock();
316 let mut v = std::mem::take(&mut state.entries);
317 drop(state);
318 v.sort_by_key(|(lsn, _)| *lsn);
319 v
320 }
321
322 fn has_pending(&self) -> bool {
325 !self.pending.lock().entries.is_empty()
326 }
327
328 fn reset(&self, next_lsn: u64) {
334 let mut state = self.pending.lock();
335 state.next_lsn = next_lsn;
336 state.entries.clear();
337 }
338}
339
340pub(crate) struct StoreCommitCoordinator {
341 mode: DurabilityMode,
342 config: crate::api::GroupCommitOptions,
343 wal_path: PathBuf,
344 wal: Arc<WalMutex>,
345 queue: Arc<WalAppendQueue>,
351 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
352 fsync_count: Arc<AtomicU64>,
358}
359
360impl StoreCommitCoordinator {
361 pub(crate) fn should_open(path: &Path, mode: DurabilityMode) -> bool {
362 matches!(
363 mode,
364 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
365 ) || path.exists()
366 }
367
368 pub(crate) fn open(
369 wal_path: impl Into<PathBuf>,
370 mode: DurabilityMode,
371 config: crate::api::GroupCommitOptions,
372 ) -> io::Result<Self> {
373 let wal_path = wal_path.into();
374 let wal = WalWriter::open(&wal_path)?;
375 let initial_durable_lsn = wal.durable_lsn();
376 let initial_current_lsn = wal.current_lsn();
377 let wal = Arc::new(WalMutex::new(wal));
378 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
379 let state = Arc::new((
380 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
381 CommitStateCondvar::new(),
382 ));
383 let fsync_count = Arc::new(AtomicU64::new(0));
384
385 if matches!(
386 mode,
387 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
388 ) {
389 let wal_bg = Arc::clone(&wal);
390 let queue_bg = Arc::clone(&queue);
391 let state_bg = Arc::clone(&state);
392 let fsync_bg = Arc::clone(&fsync_count);
393 let window = Self::resolve_window(&config);
408 let max_statements = config.max_statements.max(1);
409 let max_wal_bytes = config.max_wal_bytes.max(1);
410 std::thread::spawn(move || {
411 Self::run_group_commit_loop(
412 wal_bg,
413 queue_bg,
414 state_bg,
415 fsync_bg,
416 window,
417 max_statements,
418 max_wal_bytes,
419 );
420 });
421 }
422
423 Ok(Self {
424 mode,
425 config,
426 wal_path,
427 wal,
428 queue,
429 state,
430 fsync_count,
431 })
432 }
433
434 fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
443 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
444 if let Ok(parsed) = raw.parse::<u64>() {
445 return Duration::from_micros(parsed);
446 }
447 }
448 if config.window_ms != 0 {
449 return Duration::from_millis(config.window_ms);
450 }
451 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
452 }
453
454 #[cfg(test)]
458 pub(crate) fn fsync_count(&self) -> u64 {
459 self.fsync_count.load(Ordering::Relaxed)
460 }
461
462 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
463 if actions.is_empty() {
464 return Ok(());
465 }
466
467 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
468
469 if matches!(self.mode, DurabilityMode::Strict) {
474 let commit_lsn = {
475 let mut wal = self.wal.lock();
476 wal.append(&WalRecord::TxCommitBatch {
477 tx_id,
478 actions: actions.iter().map(StoreWalAction::encode).collect(),
479 })?;
480 wal.current_lsn()
481 };
482 self.force_sync()?;
483 let _ = commit_lsn;
484 return Ok(());
485 }
486
487 let encoded_actions: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
491 let wal_bytes = encoded_actions.iter().fold(0u64, |total, payload| {
492 total.saturating_add(payload.len() as u64)
493 });
494 let blob = WalRecord::TxCommitBatch {
495 tx_id,
496 actions: encoded_actions,
497 }
498 .encode();
499
500 let commit_lsn = self.queue.enqueue(blob);
501 self.wait_until_durable(commit_lsn, wal_bytes)?;
502 Ok(())
503 }
504
505 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
510 let blob = record.encode();
511 let wal_bytes = blob.len() as u64;
512 if matches!(self.mode, DurabilityMode::Strict) {
513 {
514 let mut wal = self.wal.lock();
515 wal.append(&record)?;
516 }
517 self.force_sync()?;
518 return Ok(());
519 }
520 let commit_lsn = self.queue.enqueue(blob);
521 self.wait_until_durable(commit_lsn, wal_bytes)?;
522 Ok(())
523 }
524
525 pub(crate) fn force_sync(&self) -> io::Result<()> {
526 {
527 let mut wal = self.wal.lock();
528 wal.sync()?;
529 self.fsync_count.fetch_add(1, Ordering::Relaxed);
530 let durable = wal.durable_lsn();
531 drop(wal);
532 let (state_lock, cond) = &*self.state;
533 let mut state = state_lock.lock();
534 state.durable_lsn = durable;
535 state.pending_target_lsn = durable.max(state.pending_target_lsn);
536 state.pending_statements = 0;
537 state.pending_wal_bytes = 0;
538 state.first_pending_at = None;
539 state.last_error = None;
540 cond.notify_all();
541 }
542 Ok(())
543 }
544
545 pub(crate) fn truncate(&self) -> io::Result<()> {
546 let mut wal = self.wal.lock();
547 wal.truncate()?;
548 let durable = wal.durable_lsn();
549 let current = wal.current_lsn();
550 drop(wal);
551
552 self.queue.reset(current);
557
558 let (state_lock, cond) = &*self.state;
559 let mut state = state_lock.lock();
560 state.durable_lsn = durable;
561 state.pending_target_lsn = durable;
562 state.pending_statements = 0;
563 state.pending_wal_bytes = 0;
564 state.first_pending_at = None;
565 state.last_error = None;
566 cond.notify_all();
567 Ok(())
568 }
569
570 pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
571 if !self.wal_path.exists() {
572 return Ok(());
573 }
574
575 let reader = match WalReader::open(&self.wal_path) {
576 Ok(reader) => reader,
577 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
578 Err(err) => return Err(err),
579 };
580
581 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
582 let mut pending = Vec::<(u64, Vec<u8>)>::new();
583
584 for record in reader.iter() {
585 let (_lsn, record) = match record {
586 Ok(record) => record,
587 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
588 Err(err) => return Err(err),
589 };
590 match record {
591 WalRecord::TxCommitBatch { actions, .. } => {
592 for payload in actions {
593 let action = StoreWalAction::decode(&payload)?;
594 store.apply_replayed_action(&action).map_err(|err| {
595 io::Error::other(format!("failed to replay store wal action: {err}"))
596 })?;
597 }
598 }
599 WalRecord::Begin { tx_id } => {
600 tx_states.insert(tx_id, false);
601 }
602 WalRecord::Commit { tx_id } => {
603 tx_states.insert(tx_id, true);
604 }
605 WalRecord::Rollback { tx_id } => {
606 tx_states.remove(&tx_id);
607 }
608 WalRecord::PageWrite {
609 tx_id,
610 page_id: _,
611 data,
612 } => pending.push((tx_id, data)),
613 WalRecord::Checkpoint { .. } => {}
614 WalRecord::VectorInsert {
615 collection,
616 entity_id,
617 vector,
618 } => {
619 let mut map = store.replayed_turbo_inserts.lock();
625 map.entry(collection).or_default().push((entity_id, vector));
626 }
627 WalRecord::ProbabilisticDelta {
628 kind,
629 operation,
630 name,
631 operands,
632 } => {
633 let mut deltas = store.replayed_probabilistic_deltas.lock();
634 deltas.push((kind, operation, name, operands));
635 }
636 WalRecord::FullPageImage { .. } => {
637 }
639 }
640 }
641
642 for (tx_id, payload) in pending {
643 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
644 continue;
645 }
646 let action = StoreWalAction::decode(&payload)?;
647 store.apply_replayed_action(&action).map_err(|err| {
648 io::Error::other(format!("failed to replay store wal action: {err}"))
649 })?;
650 }
651
652 Ok(())
653 }
654
655 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
656 match self.mode {
657 DurabilityMode::Strict => self.force_sync(),
658 DurabilityMode::Async => {
663 let (state_lock, cond) = &*self.state;
664 let mut state = state_lock.lock();
665 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
666 state.pending_statements = state.pending_statements.saturating_add(1);
667 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
668 state.first_pending_at.get_or_insert_with(Instant::now);
669 cond.notify_all();
670 Ok(())
671 }
672 DurabilityMode::WalDurableGrouped => {
673 let (state_lock, cond) = &*self.state;
674 let mut state = state_lock.lock();
675 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
676 state.pending_statements = state.pending_statements.saturating_add(1);
677 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
678 state.first_pending_at.get_or_insert_with(Instant::now);
679 cond.notify_all();
680
681 loop {
682 if let Some(err) = state.last_error.clone() {
683 return Err(io::Error::other(err));
684 }
685 if state.durable_lsn >= target_lsn {
686 return Ok(());
687 }
688 cond.wait(&mut state);
691 }
692 }
693 }
694 }
695
696 fn run_group_commit_loop(
697 wal: Arc<WalMutex>,
698 queue: Arc<WalAppendQueue>,
699 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
700 fsync_count: Arc<AtomicU64>,
701 window: Duration,
702 max_statements: usize,
703 max_wal_bytes: u64,
704 ) {
705 let (state_lock, cond) = &*state;
706 loop {
707 let target_lsn = {
708 let mut guard = state_lock.lock();
709
710 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
711 cond.wait(&mut guard);
712 }
713
714 if guard.shutdown {
715 return;
716 }
717
718 let immediate = window.is_zero()
719 || guard.pending_statements >= max_statements
720 || guard.pending_wal_bytes >= max_wal_bytes;
721
722 if !immediate {
723 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
724 let now = Instant::now();
725 if now < deadline {
726 let timeout = deadline.saturating_duration_since(now);
727 let _ = cond.wait_for(&mut guard, timeout);
728 if guard.shutdown {
729 return;
730 }
731 if guard.pending_target_lsn <= guard.durable_lsn {
732 continue;
733 }
734 let should_wait_again = guard.pending_statements < max_statements
735 && guard.pending_wal_bytes < max_wal_bytes
736 && guard
737 .first_pending_at
738 .map(|first| first.elapsed() < window)
739 .unwrap_or(false);
740 if should_wait_again {
741 continue;
742 }
743 }
744 }
745
746 guard.pending_target_lsn
747 };
748
749 let batches = queue.drain_sorted();
755
756 let sync_result = {
757 let mut wal = wal.lock();
758 let mut write_err: Option<io::Error> = None;
759 for (_lsn, bytes) in batches {
760 if let Err(e) = wal.append_bytes(&bytes) {
761 write_err = Some(e);
762 break;
763 }
764 }
765 match write_err {
766 Some(e) => Err(e),
767 None => wal.sync().map(|_| {
768 fsync_count.fetch_add(1, Ordering::Relaxed);
773 wal.durable_lsn()
774 }),
775 }
776 };
777
778 let more_pending = queue.has_pending();
783
784 let mut guard = state_lock.lock();
785 match sync_result {
786 Ok(durable_lsn) => {
787 guard.durable_lsn = durable_lsn;
788 if !more_pending {
789 guard.pending_statements = 0;
790 guard.pending_wal_bytes = 0;
791 guard.first_pending_at = None;
792 }
793 guard.last_error = None;
794 let _ = target_lsn;
795 }
796 Err(err) => {
797 guard.last_error = Some(err.to_string());
798 }
799 }
800 cond.notify_all();
801 }
802 }
803}
804
805impl Drop for StoreCommitCoordinator {
806 fn drop(&mut self) {
807 let (state_lock, cond) = &*self.state;
808 let mut state = state_lock.lock();
810 state.shutdown = true;
811 cond.notify_all();
812 }
813}
814
815impl UnifiedStore {
816 pub(crate) fn begin_deferred_store_wal_capture() {
817 begin_deferred_store_wal_capture();
818 }
819
820 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
821 take_deferred_store_wal_capture()
822 }
823
824 pub(crate) fn append_deferred_store_wal_actions(
825 &self,
826 actions: DeferredStoreWalActions,
827 ) -> Result<(), StoreError> {
828 if actions.actions.is_empty() {
829 return Ok(());
830 }
831 if self.config.embedded_wal_path.is_some() {
832 return self.append_embedded_store_wal_actions(&actions.actions);
833 }
834 match self.config.durability_mode {
835 DurabilityMode::Strict => self.flush_paged_state(),
836 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
837 if let Some(commit) = &self.commit {
838 commit
839 .append_actions(&actions.actions)
840 .map_err(StoreError::Io)
841 } else {
842 self.flush_paged_state()
843 }
844 }
845 }
846 }
847
848 pub(crate) fn append_vector_insert_record(
855 &self,
856 collection: &str,
857 entity_id: u64,
858 vector: &[f32],
859 ) -> std::io::Result<()> {
860 let Some(commit) = &self.commit else {
861 return Ok(());
862 };
863 let record = WalRecord::VectorInsert {
864 collection: collection.to_string(),
865 entity_id,
866 vector: vector.to_vec(),
867 };
868 commit.append_single_record(record)
869 }
870
871 pub(crate) fn append_probabilistic_delta_record(
872 &self,
873 kind: u8,
874 operation: u8,
875 name: &str,
876 operands: Vec<Vec<u8>>,
877 ) -> std::io::Result<()> {
878 let Some(commit) = &self.commit else {
879 return Ok(());
880 };
881 let record = WalRecord::ProbabilisticDelta {
882 kind,
883 operation,
884 name: name.to_string(),
885 operands,
886 };
887 commit.append_single_record(record)
888 }
889
890 pub(crate) fn finish_paged_write(
891 &self,
892 actions: impl IntoIterator<Item = StoreWalAction>,
893 ) -> Result<(), StoreError> {
894 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
895 if deferred_store_wal_capture_active() {
896 let captured = capture_deferred_store_wal_actions(actions);
897 debug_assert!(captured);
898 return Ok(());
899 }
900 if self.config.embedded_wal_path.is_some() {
901 return self.append_embedded_store_wal_actions(&actions);
902 }
903 match self.config.durability_mode {
904 DurabilityMode::Strict => self.flush_paged_state(),
905 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
906 if let Some(commit) = &self.commit {
907 commit.append_actions(&actions).map_err(StoreError::Io)?;
908 Ok(())
909 } else {
910 self.flush_paged_state()
911 }
912 }
913 }
914 }
915
916 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
917 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
918 self.apply_replayed_action(&action)
919 }
920
921 fn append_embedded_store_wal_actions(
922 &self,
923 actions: &[StoreWalAction],
924 ) -> Result<(), StoreError> {
925 let Some(path) = self.config.embedded_wal_path.as_deref() else {
926 return Ok(());
927 };
928 if actions.is_empty() {
929 return Ok(());
930 }
931 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
932 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
933 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
934 match crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads) {
935 Ok(_) => Ok(()),
936 Err(crate::api::RedDBError::InvalidOperation(msg))
937 if msg.contains("embedded wal region full") =>
938 {
939 let snapshot = self.to_binary_dump_bytes();
940 crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
941 path,
942 &snapshot,
943 encoded_len,
944 )
945 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
946 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads)
947 .map(|_| ())
948 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
949 }
950 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
951 }
952 }
953
954 pub(crate) fn apply_replayed_action(&self, action: &StoreWalAction) -> Result<(), StoreError> {
955 match action {
956 StoreWalAction::CreateCollection { name } => {
957 if self.get_collection(name).is_none() {
958 let _ = self.create_collection_in_memory(name);
959 }
960 Ok(())
961 }
962 StoreWalAction::DropCollection { name } => self.drop_collection_in_memory(name),
963 StoreWalAction::UpsertEntityRecord { collection, record } => {
964 self.apply_replayed_upsert(collection, record)
965 }
966 StoreWalAction::DeleteEntityRecord {
967 collection,
968 entity_id,
969 } => self.apply_replayed_delete(collection, EntityId::new(*entity_id)),
970 StoreWalAction::BulkUpsertEntityRecords {
971 collection,
972 records,
973 } => {
974 for record in records {
975 self.apply_replayed_upsert(collection, record)?;
976 }
977 Ok(())
978 }
979 StoreWalAction::RefreshCollection {
980 collection,
981 records,
982 } => self.apply_replayed_refresh_collection(collection, records),
983 }
984 }
985
986 pub fn refresh_collection(
1003 &self,
1004 name: &str,
1005 entities: Vec<UnifiedEntity>,
1006 ) -> Result<Vec<Vec<u8>>, StoreError> {
1007 let fv = self.format_version();
1008
1009 let new_manager = Arc::new(SegmentManager::with_config(
1012 name,
1013 self.config.manager_config.clone(),
1014 ));
1015
1016 let mut prepared = entities;
1017 for entity in &mut prepared {
1018 if entity.id.raw() == 0 {
1019 entity.id = self.next_entity_id();
1020 } else {
1021 self.register_entity_id(entity.id);
1022 }
1023 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1024 if *row_id == 0 {
1025 *row_id = new_manager.next_row_id();
1026 } else {
1027 new_manager.register_row_id(*row_id);
1028 }
1029 }
1030 entity.ensure_table_logical_id();
1031 }
1032
1033 let serialized: Vec<Vec<u8>> = prepared
1034 .iter()
1035 .map(|e| Self::serialize_entity_record(e, None, fv))
1036 .collect();
1037
1038 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1039 StoreError::Io(std::io::Error::other(format!(
1040 "refresh_collection: bulk_insert into new manager failed: {e}"
1041 )))
1042 })?;
1043
1044 self.swap_collection_state(name, new_manager, &prepared, &serialized);
1045
1046 self.finish_paged_write([StoreWalAction::RefreshCollection {
1047 collection: name.to_string(),
1048 records: serialized.clone(),
1049 }])?;
1050
1051 Ok(serialized)
1052 }
1053
1054 fn swap_collection_state(
1060 &self,
1061 name: &str,
1062 new_manager: Arc<SegmentManager>,
1063 prepared: &[UnifiedEntity],
1064 serialized: &[Vec<u8>],
1065 ) {
1066 {
1067 let mut collections = self.collections.write();
1068 collections.insert(name.to_string(), new_manager);
1069 }
1070
1071 self.entity_cache
1072 .retain(|_, (collection, _)| collection != name);
1073 self.remove_from_graph_label_index_batch(
1074 name,
1075 &prepared.iter().map(|e| e.id).collect::<Vec<_>>(),
1076 );
1077
1078 if let Some(pager) = &self.pager {
1079 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1080 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1081 .iter()
1082 .zip(serialized.iter())
1083 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1084 .collect();
1085 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1086 if !sorted.is_empty() {
1087 let _ = new_btree.bulk_insert_sorted(&sorted);
1088 }
1089 self.btree_indices
1090 .write()
1091 .insert(name.to_string(), new_btree);
1092 self.mark_paged_registry_dirty();
1093 }
1094 }
1095
1096 fn apply_replayed_refresh_collection(
1097 &self,
1098 collection: &str,
1099 records: &[Vec<u8>],
1100 ) -> Result<(), StoreError> {
1101 let new_manager = Arc::new(SegmentManager::with_config(
1102 collection,
1103 self.config.manager_config.clone(),
1104 ));
1105
1106 let mut prepared: Vec<UnifiedEntity> = Vec::with_capacity(records.len());
1107 for record in records {
1108 let (entity, _metadata) =
1109 Self::deserialize_entity_record(record, self.format_version())?;
1110 self.register_entity_id(entity.id);
1111 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1112 new_manager.register_row_id(*row_id);
1113 }
1114 prepared.push(entity);
1115 }
1116
1117 if !prepared.is_empty() {
1118 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1119 StoreError::Io(std::io::Error::other(format!(
1120 "replay refresh_collection: bulk_insert failed: {e}"
1121 )))
1122 })?;
1123 }
1124
1125 self.swap_collection_state(collection, new_manager, &prepared, records);
1126 Ok(())
1127 }
1128
1129 pub fn refresh_collection_from_records(
1143 &self,
1144 name: &str,
1145 records: Vec<Vec<u8>>,
1146 ) -> Result<(), StoreError> {
1147 self.apply_replayed_refresh_collection(name, &records)?;
1148 self.finish_paged_write([StoreWalAction::RefreshCollection {
1149 collection: name.to_string(),
1150 records,
1151 }])?;
1152 Ok(())
1153 }
1154
1155 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1156 let mut collections = self.collections.write();
1157 if collections.contains_key(name) {
1158 return Ok(());
1159 }
1160 let manager = SegmentManager::with_config(name, self.config.manager_config.clone());
1161 collections.insert(name.to_string(), Arc::new(manager));
1162 self.mark_paged_registry_dirty();
1163 Ok(())
1164 }
1165
1166 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1167 let manager = {
1168 let mut collections = self.collections.write();
1169 match collections.remove(name) {
1170 Some(manager) => manager,
1171 None => return Ok(()),
1172 }
1173 };
1174
1175 let entities = manager.query_all(|_| true);
1176 let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
1177 for entity_id in &entity_ids {
1178 self.context_index.remove_entity(*entity_id);
1179 let _ = self.unindex_cross_refs(*entity_id);
1180 }
1181 self.btree_indices.write().remove(name);
1182 self.entity_cache.retain(|entity_id, (collection, _)| {
1183 collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
1184 });
1185 self.remove_from_graph_label_index_batch(name, &entity_ids);
1186 self.mark_paged_registry_dirty();
1187 Ok(())
1188 }
1189
1190 fn apply_replayed_upsert(&self, collection: &str, record: &[u8]) -> Result<(), StoreError> {
1191 self.create_collection_in_memory(collection)?;
1192 let (entity, metadata) = Self::deserialize_entity_record(record, self.format_version())?;
1193 let manager = self
1194 .get_collection(collection)
1195 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1196
1197 self.register_entity_id(entity.id);
1198 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1199 manager.register_row_id(*row_id);
1200 }
1201
1202 self.context_index.remove_entity(entity.id);
1203 let _ = self.unindex_cross_refs(entity.id);
1204 self.remove_from_graph_label_index(collection, entity.id);
1205
1206 if manager.get(entity.id).is_some() {
1207 manager
1208 .update_with_metadata(entity.clone(), metadata.as_ref())
1209 .map_err(StoreError::from)?;
1210 } else {
1211 manager.insert(entity.clone())?;
1212 if let Some(metadata) = metadata.as_ref() {
1213 manager.set_metadata(entity.id, metadata.clone())?;
1214 }
1215 }
1216
1217 self.context_index.index_entity(collection, &entity);
1218 if let EntityKind::GraphNode(node) = &entity.kind {
1219 self.update_graph_label_index(collection, &node.label, entity.id);
1220 }
1221 self.index_cross_refs(&entity, collection)?;
1222
1223 if let Some(pager) = &self.pager {
1224 let mut btree_indices = self.btree_indices.write();
1225 let btree = btree_indices
1226 .entry(collection.to_string())
1227 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1228 let root_before = btree.root_page_id();
1229 let key = entity.id.raw().to_be_bytes();
1230 match btree.insert(&key, record) {
1231 Ok(_) => {}
1232 Err(BTreeError::DuplicateKey) => {
1233 let _ = btree.delete(&key);
1234 let _ = btree.insert(&key, record);
1235 }
1236 Err(err) => {
1237 return Err(StoreError::Io(io::Error::other(format!(
1238 "replay upsert btree error: {err}"
1239 ))));
1240 }
1241 }
1242 if root_before != btree.root_page_id() {
1243 self.mark_paged_registry_dirty();
1244 }
1245 }
1246
1247 Ok(())
1248 }
1249
1250 fn apply_replayed_delete(&self, collection: &str, id: EntityId) -> Result<(), StoreError> {
1251 self.entity_cache.remove(id.raw());
1252 if let Some(manager) = self.get_collection(collection) {
1253 let deleted = manager.delete(id)?;
1254 if !deleted {
1255 return Ok(());
1256 }
1257 } else {
1258 return Ok(());
1259 }
1260
1261 if let Some(_pager) = &self.pager {
1262 let btree_indices = self.btree_indices.read();
1263 if let Some(btree) = btree_indices.get(collection) {
1264 let root_before = btree.root_page_id();
1265 let key = id.raw().to_be_bytes();
1266 let _ = btree.delete(&key);
1267 if root_before != btree.root_page_id() {
1268 self.mark_paged_registry_dirty();
1269 }
1270 }
1271 }
1272
1273 let _ = self.unindex_cross_refs(id);
1274 self.remove_from_graph_label_index(collection, id);
1275 self.context_index.remove_entity(id);
1276 Ok(())
1277 }
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283 use crate::api::{DurabilityMode, GroupCommitOptions};
1284 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1285 use std::time::SystemTime;
1286
1287 fn env_lock() -> &'static StdMutex<()> {
1292 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
1293 LOCK.get_or_init(|| StdMutex::new(()))
1294 }
1295
1296 fn temp_wal(name: &str) -> PathBuf {
1297 let nanos = SystemTime::now()
1298 .duration_since(std::time::UNIX_EPOCH)
1299 .unwrap()
1300 .as_nanos();
1301 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
1302 &std::env::temp_dir(),
1303 name,
1304 std::process::id(),
1305 nanos,
1306 );
1307 let _ = std::fs::remove_file(&path);
1308 path
1309 }
1310
1311 #[test]
1322 fn group_commit_coalesces_concurrent_autocommits() {
1323 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1324 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1327
1328 let path = temp_wal("coalesce");
1329 let coord = Arc::new(
1330 StoreCommitCoordinator::open(
1331 path.clone(),
1332 DurabilityMode::WalDurableGrouped,
1333 GroupCommitOptions::default(),
1334 )
1335 .expect("open commit coordinator"),
1336 );
1337
1338 const WRITERS: usize = 32;
1339 let barrier = Arc::new(Barrier::new(WRITERS));
1340 let mut handles = Vec::with_capacity(WRITERS);
1341 for tx in 0..WRITERS {
1342 let coord_c = Arc::clone(&coord);
1343 let barrier_c = Arc::clone(&barrier);
1344 handles.push(std::thread::spawn(move || {
1345 barrier_c.wait();
1349 let action = StoreWalAction::CreateCollection {
1350 name: format!("c{tx}"),
1351 };
1352 coord_c
1353 .append_actions(std::slice::from_ref(&action))
1354 .expect("append_actions");
1355 }));
1356 }
1357 for h in handles {
1358 h.join().expect("writer thread");
1359 }
1360
1361 let fsyncs = coord.fsync_count();
1362 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
1363 assert!(
1364 fsyncs < WRITERS as u64,
1365 "expected fsyncs ({fsyncs}) to be strictly less than \
1366 concurrent writers ({WRITERS}); coalescing failed"
1367 );
1368
1369 drop(coord);
1370 let _ = std::fs::remove_file(&path);
1371 }
1372
1373 #[test]
1379 fn zero_window_disables_coalescing_floor() {
1380 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1381 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1382
1383 let path = temp_wal("zero_window");
1384 let coord = Arc::new(
1385 StoreCommitCoordinator::open(
1386 path.clone(),
1387 DurabilityMode::WalDurableGrouped,
1388 GroupCommitOptions::default(),
1389 )
1390 .expect("open commit coordinator"),
1391 );
1392
1393 const WRITERS: usize = 8;
1394 let barrier = Arc::new(Barrier::new(WRITERS));
1395 let mut handles = Vec::with_capacity(WRITERS);
1396 for tx in 0..WRITERS {
1397 let coord_c = Arc::clone(&coord);
1398 let barrier_c = Arc::clone(&barrier);
1399 handles.push(std::thread::spawn(move || {
1400 barrier_c.wait();
1401 let action = StoreWalAction::CreateCollection {
1402 name: format!("z{tx}"),
1403 };
1404 coord_c
1405 .append_actions(std::slice::from_ref(&action))
1406 .expect("append_actions");
1407 }));
1408 }
1409 for h in handles {
1410 h.join().expect("writer thread");
1411 }
1412
1413 let fsyncs = coord.fsync_count();
1419 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
1420
1421 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1422 drop(coord);
1423 let _ = std::fs::remove_file(&path);
1424 }
1425
1426 #[test]
1428 fn resolve_window_precedence() {
1429 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1430 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1432 let cfg = GroupCommitOptions::default();
1433 assert_eq!(
1434 StoreCommitCoordinator::resolve_window(&cfg),
1435 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
1436 );
1437
1438 let cfg_ms = GroupCommitOptions {
1440 window_ms: 5,
1441 ..GroupCommitOptions::default()
1442 };
1443 assert_eq!(
1444 StoreCommitCoordinator::resolve_window(&cfg_ms),
1445 Duration::from_millis(5)
1446 );
1447
1448 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
1450 assert_eq!(
1451 StoreCommitCoordinator::resolve_window(&cfg),
1452 Duration::from_micros(750)
1453 );
1454 assert_eq!(
1455 StoreCommitCoordinator::resolve_window(&cfg_ms),
1456 Duration::from_micros(750)
1457 );
1458
1459 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1461 assert_eq!(
1462 StoreCommitCoordinator::resolve_window(&cfg),
1463 Duration::from_micros(0)
1464 );
1465
1466 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1467 }
1468}