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 epoch: parking_lot::RwLock<()>,
268 pending: parking_lot::Mutex<WalQueueState>,
283 queued_bytes: AtomicU64,
288}
289
290struct WalQueueState {
291 next_lsn: u64,
292 entries: Vec<(u64, Vec<u8>)>,
293}
294
295impl WalAppendQueue {
296 fn new(initial_lsn: u64) -> Self {
297 Self {
298 epoch: parking_lot::RwLock::new(()),
299 pending: parking_lot::Mutex::new(WalQueueState {
300 next_lsn: initial_lsn,
301 entries: Vec::with_capacity(64),
302 }),
303 queued_bytes: AtomicU64::new(0),
304 }
305 }
306
307 fn enqueue(&self, bytes: Vec<u8>) -> u64 {
313 let len = bytes.len() as u64;
314 let mut state = self.pending.lock();
315 let start_lsn = state.next_lsn;
316 state.next_lsn = start_lsn + len;
317 state.entries.push((start_lsn, bytes));
318 self.queued_bytes.fetch_add(len, Ordering::Relaxed);
319 start_lsn + len
320 }
321
322 fn drain_sorted(&self) -> Vec<(u64, Vec<u8>)> {
326 let mut state = self.pending.lock();
327 let mut v = std::mem::take(&mut state.entries);
328 let drained: u64 = v.iter().map(|(_, bytes)| bytes.len() as u64).sum();
329 drop(state);
330 let _ = self
334 .queued_bytes
335 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |queued| {
336 Some(queued.saturating_sub(drained))
337 });
338 v.sort_by_key(|(lsn, _)| *lsn);
339 v
340 }
341
342 fn has_pending(&self) -> bool {
345 !self.pending.lock().entries.is_empty()
346 }
347
348 fn queued_bytes(&self) -> u64 {
350 self.queued_bytes.load(Ordering::Relaxed)
351 }
352
353 fn reset(&self, next_lsn: u64) {
359 let mut state = self.pending.lock();
360 state.next_lsn = next_lsn;
361 state.entries.clear();
362 drop(state);
363 self.queued_bytes.store(0, Ordering::Relaxed);
364 }
365}
366
367pub(crate) struct StoreCommitCoordinator {
368 mode: DurabilityMode,
369 config: crate::api::GroupCommitOptions,
370 wal_path: PathBuf,
371 wal: Arc<WalMutex>,
372 queue: Arc<WalAppendQueue>,
378 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
379 fsync_count: Arc<AtomicU64>,
385}
386
387impl StoreCommitCoordinator {
388 pub(crate) fn open(
389 wal_path: impl Into<PathBuf>,
390 mode: DurabilityMode,
391 config: crate::api::GroupCommitOptions,
392 ) -> io::Result<Self> {
393 let wal_path = wal_path.into();
394 let wal = WalWriter::open(&wal_path)?;
395 let initial_durable_lsn = wal.durable_lsn();
396 let initial_current_lsn = wal.current_lsn();
397 let wal = Arc::new(WalMutex::new(wal));
398 let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
399 let state = Arc::new((
400 CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
401 CommitStateCondvar::new(),
402 ));
403 let fsync_count = Arc::new(AtomicU64::new(0));
404
405 if matches!(
406 mode,
407 DurabilityMode::WalDurableGrouped | DurabilityMode::Async
408 ) {
409 let wal_bg = Arc::clone(&wal);
410 let queue_bg = Arc::clone(&queue);
411 let state_bg = Arc::clone(&state);
412 let fsync_bg = Arc::clone(&fsync_count);
413 let window = Self::resolve_window(&config);
428 let max_statements = config.max_statements.max(1);
429 let max_wal_bytes = config.max_wal_bytes.max(1);
430 std::thread::spawn(move || {
431 Self::run_group_commit_loop(
432 wal_bg,
433 queue_bg,
434 state_bg,
435 fsync_bg,
436 window,
437 max_statements,
438 max_wal_bytes,
439 );
440 });
441 }
442
443 Ok(Self {
444 mode,
445 config,
446 wal_path,
447 wal,
448 queue,
449 state,
450 fsync_count,
451 })
452 }
453
454 fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
463 if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
464 if let Ok(parsed) = raw.parse::<u64>() {
465 return Duration::from_micros(parsed);
466 }
467 }
468 if config.window_ms != 0 {
469 return Duration::from_millis(config.window_ms);
470 }
471 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
472 }
473
474 #[cfg(test)]
478 pub(crate) fn fsync_count(&self) -> u64 {
479 self.fsync_count.load(Ordering::Relaxed)
480 }
481
482 pub(crate) fn buffered_bytes(&self) -> u64 {
486 self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
487 }
488
489 pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
490 if actions.is_empty() {
491 return Ok(());
492 }
493 let _epoch = self.queue.epoch.read();
494
495 let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
496
497 if matches!(self.mode, DurabilityMode::Strict) {
502 let commit_lsn = {
503 let mut wal = self.wal.lock();
504 wal.append(&WalRecord::TxCommitBatch {
505 tx_id,
506 actions: actions.iter().map(StoreWalAction::encode).collect(),
507 })?;
508 wal.current_lsn()
509 };
510 self.force_sync()?;
511 let _ = commit_lsn;
512 return Ok(());
513 }
514
515 let encoded_actions: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
519 let wal_bytes = encoded_actions.iter().fold(0u64, |total, payload| {
520 total.saturating_add(payload.len() as u64)
521 });
522 let blob = WalRecord::TxCommitBatch {
523 tx_id,
524 actions: encoded_actions,
525 }
526 .encode();
527
528 let commit_lsn = self.queue.enqueue(blob);
529 self.wait_until_durable(commit_lsn, wal_bytes)?;
530 Ok(())
531 }
532
533 pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
538 let _epoch = self.queue.epoch.read();
539 let blob = record.encode();
540 let wal_bytes = blob.len() as u64;
541 if matches!(self.mode, DurabilityMode::Strict) {
542 {
543 let mut wal = self.wal.lock();
544 wal.append(&record)?;
545 }
546 self.force_sync()?;
547 return Ok(());
548 }
549 let commit_lsn = self.queue.enqueue(blob);
550 self.wait_until_durable(commit_lsn, wal_bytes)?;
551 Ok(())
552 }
553
554 pub(crate) fn force_sync(&self) -> io::Result<()> {
555 let _epoch = self.queue.epoch.read_recursive();
558 {
559 let mut wal = self.wal.lock();
560 wal.sync()?;
561 self.fsync_count.fetch_add(1, Ordering::Relaxed);
562 let durable = wal.durable_lsn();
563 drop(wal);
564 let (state_lock, cond) = &*self.state;
565 let mut state = state_lock.lock();
566 state.durable_lsn = durable;
567 state.pending_target_lsn = durable.max(state.pending_target_lsn);
568 state.pending_statements = 0;
569 state.pending_wal_bytes = 0;
570 state.first_pending_at = None;
571 state.last_error = None;
572 cond.notify_all();
573 }
574 Ok(())
575 }
576
577 pub(crate) fn truncate(&self) -> io::Result<()> {
578 let _epoch = self.queue.epoch.write();
579 let mut wal = self.wal.lock();
580 wal.truncate()?;
581 let durable = wal.durable_lsn();
582 let current = wal.current_lsn();
583 drop(wal);
584
585 self.queue.reset(current);
590
591 let (state_lock, cond) = &*self.state;
592 let mut state = state_lock.lock();
593 state.durable_lsn = durable;
594 state.pending_target_lsn = durable;
595 state.pending_statements = 0;
596 state.pending_wal_bytes = 0;
597 state.first_pending_at = None;
598 state.last_error = None;
599 cond.notify_all();
600 Ok(())
601 }
602
603 pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
604 if !self.wal_path.exists() {
605 return Ok(());
606 }
607
608 let reader = match WalReader::open(&self.wal_path) {
609 Ok(reader) => reader,
610 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
611 Err(err) => return Err(err),
612 };
613
614 let mut tx_states = std::collections::HashMap::<u64, bool>::new();
615 let mut pending = Vec::<(u64, Vec<u8>)>::new();
616 let mut applied = false;
617
618 for record in reader.iter() {
619 let (_lsn, record) = match record {
620 Ok(record) => record,
621 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
622 Err(err) => return Err(err),
623 };
624 match record {
625 WalRecord::TxCommitBatch { actions, .. } => {
626 for payload in actions {
627 let action = StoreWalAction::decode(&payload)?;
628 applied = true;
629 store.apply_replayed_action(&action, false).map_err(|err| {
630 io::Error::other(format!("failed to replay store wal action: {err}"))
631 })?;
632 }
633 }
634 WalRecord::Begin { tx_id } => {
635 tx_states.insert(tx_id, false);
636 }
637 WalRecord::Commit { tx_id } => {
638 tx_states.insert(tx_id, true);
639 }
640 WalRecord::Rollback { tx_id } => {
641 tx_states.remove(&tx_id);
642 }
643 WalRecord::PageWrite {
644 tx_id,
645 page_id: _,
646 data,
647 } => pending.push((tx_id, data)),
648 WalRecord::Checkpoint { .. } => {}
649 WalRecord::VectorInsert {
650 collection,
651 entity_id,
652 vector,
653 } => {
654 let mut map = store.replayed_turbo_inserts.lock();
660 map.entry(collection).or_default().push((entity_id, vector));
661 }
662 WalRecord::ProbabilisticDelta {
663 kind,
664 operation,
665 name,
666 operands,
667 } => {
668 let mut deltas = store.replayed_probabilistic_deltas.lock();
669 deltas.push((kind, operation, name, operands));
670 }
671 WalRecord::FullPageImage { .. } => {
672 }
674 }
675 }
676
677 for (tx_id, payload) in pending {
678 if !tx_states.get(&tx_id).copied().unwrap_or(false) {
679 continue;
680 }
681 let action = StoreWalAction::decode(&payload)?;
682 applied = true;
683 store.apply_replayed_action(&action, false).map_err(|err| {
684 io::Error::other(format!("failed to replay store wal action: {err}"))
685 })?;
686 }
687
688 if applied {
689 store
690 .reconcile_replayed_btrees()
691 .map_err(|err| io::Error::other(err.to_string()))?;
692 }
693 Ok(())
694 }
695
696 fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
697 match self.mode {
698 DurabilityMode::Strict => self.force_sync(),
699 DurabilityMode::Async => {
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 Ok(())
712 }
713 DurabilityMode::WalDurableGrouped => {
714 let (state_lock, cond) = &*self.state;
715 let mut state = state_lock.lock();
716 state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
717 state.pending_statements = state.pending_statements.saturating_add(1);
718 state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
719 state.first_pending_at.get_or_insert_with(Instant::now);
720 cond.notify_all();
721
722 loop {
723 if let Some(err) = state.last_error.clone() {
724 return Err(io::Error::other(err));
725 }
726 if state.durable_lsn >= target_lsn {
727 return Ok(());
728 }
729 cond.wait(&mut state);
732 }
733 }
734 }
735 }
736
737 fn run_group_commit_loop(
738 wal: Arc<WalMutex>,
739 queue: Arc<WalAppendQueue>,
740 state: Arc<(CommitStateMutex, CommitStateCondvar)>,
741 fsync_count: Arc<AtomicU64>,
742 window: Duration,
743 max_statements: usize,
744 max_wal_bytes: u64,
745 ) {
746 let (state_lock, cond) = &*state;
747 loop {
748 let target_lsn = {
749 let mut guard = state_lock.lock();
750
751 while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
752 cond.wait(&mut guard);
753 }
754
755 if guard.shutdown {
756 return;
757 }
758
759 let immediate = window.is_zero()
760 || guard.pending_statements >= max_statements
761 || guard.pending_wal_bytes >= max_wal_bytes;
762
763 if !immediate {
764 let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
765 let now = Instant::now();
766 if now < deadline {
767 let timeout = deadline.saturating_duration_since(now);
768 let _ = cond.wait_for(&mut guard, timeout);
769 if guard.shutdown {
770 return;
771 }
772 if guard.pending_target_lsn <= guard.durable_lsn {
773 continue;
774 }
775 let should_wait_again = guard.pending_statements < max_statements
776 && guard.pending_wal_bytes < max_wal_bytes
777 && guard
778 .first_pending_at
779 .map(|first| first.elapsed() < window)
780 .unwrap_or(false);
781 if should_wait_again {
782 continue;
783 }
784 }
785 }
786
787 guard.pending_target_lsn
788 };
789
790 let _epoch = queue.epoch.read_recursive();
795
796 let batches = queue.drain_sorted();
802
803 let sync_result = {
804 let mut wal = wal.lock();
805 let mut write_err: Option<io::Error> = None;
806 for (_lsn, bytes) in batches {
807 if let Err(e) = wal.append_bytes(&bytes) {
808 write_err = Some(e);
809 break;
810 }
811 }
812 match write_err {
813 Some(e) => Err(e),
814 None => wal.sync().map(|_| {
815 fsync_count.fetch_add(1, Ordering::Relaxed);
820 wal.durable_lsn()
821 }),
822 }
823 };
824
825 let more_pending = queue.has_pending();
830
831 let mut guard = state_lock.lock();
832 match sync_result {
833 Ok(durable_lsn) => {
834 guard.durable_lsn = durable_lsn;
835 if !more_pending {
836 guard.pending_statements = 0;
837 guard.pending_wal_bytes = 0;
838 guard.first_pending_at = None;
839 }
840 guard.last_error = None;
841 let _ = target_lsn;
842 }
843 Err(err) => {
844 guard.last_error = Some(err.to_string());
845 }
846 }
847 cond.notify_all();
848 }
849 }
850}
851
852impl Drop for StoreCommitCoordinator {
853 fn drop(&mut self) {
854 let (state_lock, cond) = &*self.state;
855 let mut state = state_lock.lock();
857 state.shutdown = true;
858 cond.notify_all();
859 }
860}
861
862impl UnifiedStore {
863 pub(crate) fn deferred_store_wal_capture_active() -> bool {
864 deferred_store_wal_capture_active()
865 }
866
867 pub(crate) fn begin_deferred_store_wal_capture() {
868 begin_deferred_store_wal_capture();
869 }
870
871 pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
872 take_deferred_store_wal_capture()
873 }
874
875 pub(crate) fn append_deferred_store_wal_actions(
876 &self,
877 actions: DeferredStoreWalActions,
878 ) -> Result<(), StoreError> {
879 if actions.actions.is_empty() {
880 return Ok(());
881 }
882 if self.config.embedded_wal_path.is_some() {
883 return self.append_embedded_store_wal_actions(&actions.actions);
884 }
885 if let Some(commit) = &self.commit {
888 commit
889 .append_actions(&actions.actions)
890 .map_err(StoreError::Io)
891 } else {
892 self.flush_paged_state()
893 }
894 }
895
896 pub(crate) fn append_vector_insert_record(
903 &self,
904 collection: &str,
905 entity_id: u64,
906 vector: &[f32],
907 ) -> std::io::Result<()> {
908 let Some(commit) = &self.commit else {
909 return Ok(());
910 };
911 let record = WalRecord::VectorInsert {
912 collection: collection.to_string(),
913 entity_id,
914 vector: vector.to_vec(),
915 };
916 commit.append_single_record(record)
917 }
918
919 pub(crate) fn append_probabilistic_delta_record(
920 &self,
921 kind: u8,
922 operation: u8,
923 name: &str,
924 operands: Vec<Vec<u8>>,
925 ) -> std::io::Result<()> {
926 let Some(commit) = &self.commit else {
927 return Ok(());
928 };
929 let record = WalRecord::ProbabilisticDelta {
930 kind,
931 operation,
932 name: name.to_string(),
933 operands,
934 };
935 commit.append_single_record(record)
936 }
937
938 pub(crate) fn finish_paged_write(
939 &self,
940 actions: impl IntoIterator<Item = StoreWalAction>,
941 ) -> Result<(), StoreError> {
942 let actions: Vec<StoreWalAction> = actions.into_iter().collect();
943 if deferred_store_wal_capture_active() {
944 let captured = capture_deferred_store_wal_actions(actions);
945 debug_assert!(captured);
946 return Ok(());
947 }
948 if self.config.embedded_wal_path.is_some() {
949 return self.append_embedded_store_wal_actions(&actions);
950 }
951 match self.config.durability_mode {
952 DurabilityMode::Strict => {
953 if let Some(commit) = &self.commit {
954 commit.append_actions(&actions).map_err(StoreError::Io)?;
955 }
956 self.flush_paged_state()
957 }
958 DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
959 if let Some(commit) = &self.commit {
960 commit.append_actions(&actions).map_err(StoreError::Io)?;
961 Ok(())
962 } else {
963 self.flush_paged_state()
964 }
965 }
966 }
967 }
968
969 pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
970 let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
971 self.apply_replayed_action(&action, true)
972 }
973
974 fn append_embedded_store_wal_actions(
975 &self,
976 actions: &[StoreWalAction],
977 ) -> Result<(), StoreError> {
978 let Some(path) = self.config.embedded_wal_path.as_deref() else {
979 return Ok(());
980 };
981 if actions.is_empty() {
982 return Ok(());
983 }
984 let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
985 let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
986 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
987 match crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads) {
988 Ok(_) => Ok(()),
989 Err(crate::api::RedDBError::InvalidOperation(msg))
990 if msg.contains("embedded wal region full") =>
991 {
992 let _vault_guard = self.vault_publication_lock.lock();
993 let snapshot = self.to_binary_dump_bytes();
994 crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
995 path,
996 &snapshot,
997 encoded_len,
998 )
999 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1000 crate::storage::EmbeddedRdbArtifact::append_wal_payloads(path, &payloads)
1001 .map(|_| ())
1002 .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1003 }
1004 Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1005 }
1006 }
1007
1008 pub(crate) fn apply_replayed_action(
1009 &self,
1010 action: &StoreWalAction,
1011 update_btrees: bool,
1012 ) -> Result<(), StoreError> {
1013 match action {
1014 StoreWalAction::CreateCollection { name } => {
1015 if self.get_collection(name).is_none() {
1016 let _ = self.create_collection_in_memory(name);
1017 }
1018 Ok(())
1019 }
1020 StoreWalAction::DropCollection { name } => self.drop_collection_in_memory(name),
1021 StoreWalAction::UpsertEntityRecord { collection, record } => {
1022 self.apply_replayed_upsert(collection, record, update_btrees)
1023 }
1024 StoreWalAction::DeleteEntityRecord {
1025 collection,
1026 entity_id,
1027 } => self.apply_replayed_delete(collection, EntityId::new(*entity_id), update_btrees),
1028 StoreWalAction::BulkUpsertEntityRecords {
1029 collection,
1030 records,
1031 } => {
1032 for record in records {
1033 self.apply_replayed_upsert(collection, record, update_btrees)?;
1034 }
1035 Ok(())
1036 }
1037 StoreWalAction::RefreshCollection {
1038 collection,
1039 records,
1040 } => self.apply_replayed_refresh_collection(collection, records, update_btrees),
1041 }
1042 }
1043
1044 pub fn refresh_collection(
1061 &self,
1062 name: &str,
1063 entities: Vec<UnifiedEntity>,
1064 ) -> Result<Vec<Vec<u8>>, StoreError> {
1065 let fv = self.format_version();
1066
1067 let new_manager = Arc::new(SegmentManager::with_config(
1070 name,
1071 self.config.manager_config.clone(),
1072 ));
1073
1074 let mut prepared = entities;
1075 for entity in &mut prepared {
1076 if entity.id.raw() == 0 {
1077 entity.id = self.next_entity_id();
1078 } else {
1079 self.register_entity_id(entity.id);
1080 }
1081 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1082 if *row_id == 0 {
1083 *row_id = new_manager.next_row_id();
1084 } else {
1085 new_manager.register_row_id(*row_id);
1086 }
1087 }
1088 entity.ensure_table_logical_id();
1089 }
1090
1091 let serialized: Vec<Vec<u8>> = prepared
1092 .iter()
1093 .map(|e| Self::serialize_entity_record(e, None, fv))
1094 .collect();
1095
1096 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1097 StoreError::Io(std::io::Error::other(format!(
1098 "refresh_collection: bulk_insert into new manager failed: {e}"
1099 )))
1100 })?;
1101
1102 self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
1103
1104 self.finish_paged_write([StoreWalAction::RefreshCollection {
1105 collection: name.to_string(),
1106 records: serialized.clone(),
1107 }])?;
1108
1109 Ok(serialized)
1110 }
1111
1112 fn swap_collection_state(
1118 &self,
1119 name: &str,
1120 new_manager: Arc<SegmentManager>,
1121 prepared: &[UnifiedEntity],
1122 serialized: &[Vec<u8>],
1123 update_btrees: bool,
1124 ) {
1125 {
1126 let mut collections = self.collections.write();
1127 collections.insert(name.to_string(), new_manager);
1128 }
1129
1130 self.entity_cache
1131 .retain(|_, (collection, _)| collection != name);
1132 self.remove_from_graph_label_index_batch(
1133 name,
1134 &prepared.iter().map(|e| e.id).collect::<Vec<_>>(),
1135 );
1136
1137 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1138 let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1139 let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1140 .iter()
1141 .zip(serialized.iter())
1142 .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1143 .collect();
1144 sorted.sort_by(|a, b| a.0.cmp(&b.0));
1145 if !sorted.is_empty() {
1146 let _ = new_btree.bulk_insert_sorted(&sorted);
1147 }
1148 self.btree_indices
1149 .write()
1150 .insert(name.to_string(), new_btree);
1151 self.mark_paged_registry_dirty();
1152 }
1153 }
1154
1155 fn apply_replayed_refresh_collection(
1156 &self,
1157 collection: &str,
1158 records: &[Vec<u8>],
1159 update_btrees: bool,
1160 ) -> Result<(), StoreError> {
1161 let new_manager = Arc::new(SegmentManager::with_config(
1162 collection,
1163 self.config.manager_config.clone(),
1164 ));
1165
1166 let mut prepared: Vec<UnifiedEntity> = Vec::with_capacity(records.len());
1167 for record in records {
1168 let (entity, _metadata) =
1169 Self::deserialize_entity_record(record, self.format_version())?;
1170 self.register_entity_id(entity.id);
1171 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1172 new_manager.register_row_id(*row_id);
1173 }
1174 prepared.push(entity);
1175 }
1176
1177 if !prepared.is_empty() {
1178 new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1179 StoreError::Io(std::io::Error::other(format!(
1180 "replay refresh_collection: bulk_insert failed: {e}"
1181 )))
1182 })?;
1183 }
1184
1185 self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
1186 Ok(())
1187 }
1188
1189 pub fn refresh_collection_from_records(
1203 &self,
1204 name: &str,
1205 records: Vec<Vec<u8>>,
1206 ) -> Result<(), StoreError> {
1207 self.apply_replayed_refresh_collection(name, &records, true)?;
1208 self.finish_paged_write([StoreWalAction::RefreshCollection {
1209 collection: name.to_string(),
1210 records,
1211 }])?;
1212 Ok(())
1213 }
1214
1215 pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1216 let mut collections = self.collections.write();
1217 if collections.contains_key(name) {
1218 return Ok(());
1219 }
1220 let manager = SegmentManager::with_config(name, self.config.manager_config.clone());
1221 collections.insert(name.to_string(), Arc::new(manager));
1222 self.mark_paged_registry_dirty();
1223 Ok(())
1224 }
1225
1226 fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1227 let manager = {
1228 let mut collections = self.collections.write();
1229 match collections.remove(name) {
1230 Some(manager) => manager,
1231 None => return Ok(()),
1232 }
1233 };
1234
1235 let entities = manager.query_all(|_| true);
1236 let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
1237 for entity_id in &entity_ids {
1238 self.context_index.remove_entity(*entity_id);
1239 let _ = self.unindex_cross_refs(*entity_id);
1240 }
1241 if let Some(tree) = self.btree_indices.write().remove(name) {
1242 self.retire_btree(tree);
1243 }
1244 self.entity_cache.retain(|entity_id, (collection, _)| {
1245 collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
1246 });
1247 self.remove_from_graph_label_index_batch(name, &entity_ids);
1248 self.mark_paged_registry_dirty();
1249 Ok(())
1250 }
1251
1252 fn apply_replayed_upsert(
1253 &self,
1254 collection: &str,
1255 record: &[u8],
1256 update_btrees: bool,
1257 ) -> Result<(), StoreError> {
1258 self.create_collection_in_memory(collection)?;
1259 let (entity, metadata) = Self::deserialize_entity_record(record, self.format_version())?;
1260 let manager = self
1261 .get_collection(collection)
1262 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1263
1264 self.register_entity_id(entity.id);
1265 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1266 manager.register_row_id(*row_id);
1267 }
1268
1269 self.context_index.remove_entity(entity.id);
1270 let _ = self.unindex_cross_refs(entity.id);
1271 self.remove_from_graph_label_index(collection, entity.id);
1272
1273 if manager.get(entity.id).is_some() {
1274 manager
1275 .update_with_metadata(entity.clone(), metadata.as_ref())
1276 .map_err(StoreError::from)?;
1277 } else {
1278 manager.restore_snapshot_entity(entity.clone())?;
1279 if let Some(metadata) = metadata.as_ref() {
1280 manager.set_metadata(entity.id, metadata.clone())?;
1281 }
1282 }
1283
1284 self.context_index.index_entity(collection, &entity);
1285 if let EntityKind::GraphNode(node) = &entity.kind {
1286 self.update_graph_label_index(collection, &node.label, entity.id);
1287 }
1288 self.index_cross_refs(&entity, collection)?;
1289
1290 if let Some(pager) = self.pager.as_ref().filter(|_| update_btrees) {
1291 let mut btree_indices = self.btree_indices.write();
1292 let btree = btree_indices
1293 .entry(collection.to_string())
1294 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
1295 let root_before = btree.root_page_id();
1296 let key = entity.id.raw().to_be_bytes();
1297 match btree.insert(&key, record) {
1298 Ok(_) => {}
1299 Err(BTreeError::DuplicateKey) => {
1300 let _ = btree.delete(&key);
1301 let _ = btree.insert(&key, record);
1302 }
1303 Err(err) => {
1304 return Err(StoreError::Io(io::Error::other(format!(
1305 "replay upsert btree error: {err}"
1306 ))));
1307 }
1308 }
1309 if root_before != btree.root_page_id() {
1310 self.mark_paged_registry_dirty();
1311 }
1312 }
1313
1314 Ok(())
1315 }
1316
1317 fn apply_replayed_delete(
1318 &self,
1319 collection: &str,
1320 id: EntityId,
1321 update_btrees: bool,
1322 ) -> Result<(), StoreError> {
1323 self.entity_cache.remove(id.raw());
1324 if let Some(manager) = self.get_collection(collection) {
1325 let deleted = manager.delete(id)?;
1326 if !deleted {
1327 return Ok(());
1328 }
1329 } else {
1330 return Ok(());
1331 }
1332
1333 if self.pager.is_some() && update_btrees {
1334 let btree_indices = self.btree_indices.read();
1335 if let Some(btree) = btree_indices.get(collection) {
1336 let root_before = btree.root_page_id();
1337 let key = id.raw().to_be_bytes();
1338 let _ = btree.delete(&key);
1339 if root_before != btree.root_page_id() {
1340 self.mark_paged_registry_dirty();
1341 }
1342 }
1343 }
1344
1345 let _ = self.unindex_cross_refs(id);
1346 self.remove_from_graph_label_index(collection, id);
1347 self.context_index.remove_entity(id);
1348 Ok(())
1349 }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354 use super::*;
1355 use crate::api::{DurabilityMode, GroupCommitOptions};
1356 use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
1357 use std::time::SystemTime;
1358
1359 fn env_lock() -> &'static StdMutex<()> {
1364 static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
1365 LOCK.get_or_init(|| StdMutex::new(()))
1366 }
1367
1368 fn temp_wal(name: &str) -> PathBuf {
1369 let nanos = SystemTime::now()
1370 .duration_since(std::time::UNIX_EPOCH)
1371 .unwrap()
1372 .as_nanos();
1373 let path = reddb_file::layout::store_commit_coord_temp_wal_path(
1374 &std::env::temp_dir(),
1375 name,
1376 std::process::id(),
1377 nanos,
1378 );
1379 let _ = std::fs::remove_file(&path);
1380 path
1381 }
1382
1383 fn replay_crash_payload(id: u64, round: u64) -> String {
1384 let mut state = id * 31 + round * 13 + 1;
1385 (0..600)
1386 .map(|_| {
1387 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1388 char::from(b'!' + ((state >> 24) % 90) as u8)
1389 })
1390 .collect()
1391 }
1392
1393 #[test]
1394 fn paged_wal_replay_survives_immediate_process_exit() {
1395 const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
1396 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
1397 if let Some(path) = std::env::var_os(CHILD_PATH) {
1398 let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
1399 assert_eq!(store.get_collection("rows").unwrap().count(), 400);
1400 std::process::exit(0);
1402 }
1403 let directory = tempfile::tempdir().unwrap();
1404 let path = directory.path().join("data.rdb");
1405 {
1406 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
1407 store.create_collection("rows").unwrap();
1408 for id in 1..=400 {
1409 store
1410 .insert(
1411 "rows",
1412 UnifiedEntity::table_row(
1413 EntityId::new(id),
1414 "rows",
1415 id,
1416 vec![Value::text(format!("initial-{id}"))],
1417 ),
1418 )
1419 .unwrap();
1420 }
1421 store.persist().unwrap();
1422 }
1423 {
1424 let coordinator = StoreCommitCoordinator::open(
1425 reddb_file::layout::unified_wal_path(&path),
1426 DurabilityMode::Strict,
1427 GroupCommitOptions::default(),
1428 )
1429 .unwrap();
1430 for round in 0..3 {
1431 let actions = (1..=400)
1432 .map(|id| {
1433 let payload = replay_crash_payload(id, round);
1434 let entity = UnifiedEntity::table_row(
1435 EntityId::new(id),
1436 "rows",
1437 id,
1438 vec![Value::text(payload)],
1439 );
1440 StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
1441 })
1442 .collect::<Vec<_>>();
1443 coordinator.append_actions(&actions).unwrap();
1444 }
1445 }
1446 let status = std::process::Command::new(std::env::current_exe().unwrap())
1447 .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
1448 .env(CHILD_PATH, &path).status().unwrap();
1449 assert!(status.success(), "replay process failed");
1450 let reopened = UnifiedStore::open_with_config(&path, config())
1451 .expect("cold reopen after replay crash");
1452 assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
1453 for id in 1..=400 {
1454 let entity = reopened
1455 .get("rows", EntityId::new(id))
1456 .expect("retained entity");
1457 let EntityData::Row(row) = entity.data else {
1458 panic!("expected row")
1459 };
1460 assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
1461 }
1462 drop(reopened);
1463 let stable_bytes = std::fs::metadata(&path).unwrap().len();
1464 let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
1465 assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
1466 assert_eq!(
1467 std::fs::metadata(&path).unwrap().len(),
1468 stable_bytes,
1469 "idempotent replay must not allocate another tree generation"
1470 );
1471 }
1472
1473 #[test]
1474 fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
1475 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1476 let path = temp_wal("checkpoint-ack");
1477 let coordinator = Arc::new(
1478 StoreCommitCoordinator::open(
1479 path.clone(),
1480 DurabilityMode::WalDurableGrouped,
1481 GroupCommitOptions::default(),
1482 )
1483 .unwrap(),
1484 );
1485 let state = coordinator.state.0.lock();
1488 let (written_tx, written_rx) = std::sync::mpsc::channel();
1489 let writer_coordinator = Arc::clone(&coordinator);
1490 let writer = std::thread::spawn(move || {
1491 let result = writer_coordinator.append_actions(&[StoreWalAction::CreateCollection {
1492 name: "acknowledged".into(),
1493 }]);
1494 written_tx.send(result).unwrap();
1495 });
1496 let deadline = Instant::now() + Duration::from_secs(2);
1497 while !coordinator.queue.has_pending() {
1498 assert!(Instant::now() < deadline, "writer never enqueued");
1499 std::thread::yield_now();
1500 }
1501 let (started_tx, started_rx) = std::sync::mpsc::channel();
1502 let (reset_tx, reset_rx) = std::sync::mpsc::channel();
1503 let reset_coordinator = Arc::clone(&coordinator);
1504 let reset = std::thread::spawn(move || {
1505 started_tx.send(()).unwrap();
1506 reset_tx.send(reset_coordinator.truncate()).unwrap();
1507 });
1508 started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1509 let deadline = Instant::now() + Duration::from_millis(100);
1510 while coordinator.queue.has_pending() && Instant::now() < deadline {
1511 std::thread::sleep(Duration::from_millis(1));
1512 }
1513 let discarded_before_ack = !coordinator.queue.has_pending();
1514 drop(state);
1515 let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
1516 if acknowledged.is_err() {
1517 let mut state = coordinator.state.0.lock();
1520 state.shutdown = true;
1521 state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
1522 coordinator.state.1.notify_all();
1523 }
1524 writer.join().unwrap();
1525 reset_rx
1526 .recv_timeout(Duration::from_secs(2))
1527 .unwrap()
1528 .unwrap();
1529 reset.join().unwrap();
1530 assert!(
1531 !discarded_before_ack,
1532 "checkpoint discarded an unacknowledged WAL record"
1533 );
1534 assert!(
1535 acknowledged.unwrap().is_ok(),
1536 "durable writer did not finish"
1537 );
1538 drop(coordinator);
1539 let _ = std::fs::remove_file(path);
1540 }
1541
1542 #[test]
1553 fn group_commit_coalesces_concurrent_autocommits() {
1554 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1555 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1558
1559 let path = temp_wal("coalesce");
1560 let coord = Arc::new(
1561 StoreCommitCoordinator::open(
1562 path.clone(),
1563 DurabilityMode::WalDurableGrouped,
1564 GroupCommitOptions::default(),
1565 )
1566 .expect("open commit coordinator"),
1567 );
1568
1569 const WRITERS: usize = 32;
1570 let barrier = Arc::new(Barrier::new(WRITERS));
1571 let mut handles = Vec::with_capacity(WRITERS);
1572 for tx in 0..WRITERS {
1573 let coord_c = Arc::clone(&coord);
1574 let barrier_c = Arc::clone(&barrier);
1575 handles.push(std::thread::spawn(move || {
1576 barrier_c.wait();
1580 let action = StoreWalAction::CreateCollection {
1581 name: format!("c{tx}"),
1582 };
1583 coord_c
1584 .append_actions(std::slice::from_ref(&action))
1585 .expect("append_actions");
1586 }));
1587 }
1588 for h in handles {
1589 h.join().expect("writer thread");
1590 }
1591
1592 let fsyncs = coord.fsync_count();
1593 assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
1594 assert!(
1595 fsyncs < WRITERS as u64,
1596 "expected fsyncs ({fsyncs}) to be strictly less than \
1597 concurrent writers ({WRITERS}); coalescing failed"
1598 );
1599
1600 drop(coord);
1601 let _ = std::fs::remove_file(&path);
1602 }
1603
1604 #[test]
1610 fn zero_window_disables_coalescing_floor() {
1611 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1612 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1613
1614 let path = temp_wal("zero_window");
1615 let coord = Arc::new(
1616 StoreCommitCoordinator::open(
1617 path.clone(),
1618 DurabilityMode::WalDurableGrouped,
1619 GroupCommitOptions::default(),
1620 )
1621 .expect("open commit coordinator"),
1622 );
1623
1624 const WRITERS: usize = 8;
1625 let barrier = Arc::new(Barrier::new(WRITERS));
1626 let mut handles = Vec::with_capacity(WRITERS);
1627 for tx in 0..WRITERS {
1628 let coord_c = Arc::clone(&coord);
1629 let barrier_c = Arc::clone(&barrier);
1630 handles.push(std::thread::spawn(move || {
1631 barrier_c.wait();
1632 let action = StoreWalAction::CreateCollection {
1633 name: format!("z{tx}"),
1634 };
1635 coord_c
1636 .append_actions(std::slice::from_ref(&action))
1637 .expect("append_actions");
1638 }));
1639 }
1640 for h in handles {
1641 h.join().expect("writer thread");
1642 }
1643
1644 let fsyncs = coord.fsync_count();
1650 assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
1651
1652 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1653 drop(coord);
1654 let _ = std::fs::remove_file(&path);
1655 }
1656
1657 #[test]
1659 fn resolve_window_precedence() {
1660 let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
1661 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1663 let cfg = GroupCommitOptions::default();
1664 assert_eq!(
1665 StoreCommitCoordinator::resolve_window(&cfg),
1666 Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
1667 );
1668
1669 let cfg_ms = GroupCommitOptions {
1671 window_ms: 5,
1672 ..GroupCommitOptions::default()
1673 };
1674 assert_eq!(
1675 StoreCommitCoordinator::resolve_window(&cfg_ms),
1676 Duration::from_millis(5)
1677 );
1678
1679 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
1681 assert_eq!(
1682 StoreCommitCoordinator::resolve_window(&cfg),
1683 Duration::from_micros(750)
1684 );
1685 assert_eq!(
1686 StoreCommitCoordinator::resolve_window(&cfg_ms),
1687 Duration::from_micros(750)
1688 );
1689
1690 std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
1692 assert_eq!(
1693 StoreCommitCoordinator::resolve_window(&cfg),
1694 Duration::from_micros(0)
1695 );
1696
1697 std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
1698 }
1699}