1pub mod halt;
29
30mod batcher;
34
35use std::time::Duration;
36
37use bytes::Bytes;
38pub use halt::{HaltCallback, HaltReason, noop_halt};
39use nautilus_core::UnixNanos;
40
41use crate::{
42 backend::IndexKey,
43 entry::{PayloadType, Topic},
44 headers::Headers,
45 snapshot::SnapshotAnchor,
46};
47
48pub const DEFAULT_CHANNEL_CAPACITY: usize = 10_000;
50pub const DEFAULT_MAX_BATCH_ENTRIES: usize = 100;
52pub const DEFAULT_MAX_BATCH_LATENCY: Duration = Duration::from_millis(5);
54pub const DEFAULT_HALT_THRESHOLD: Duration = Duration::from_millis(250);
56
57#[derive(Clone, Debug)]
59pub struct WriterConfig {
60 pub channel_capacity: usize,
62 pub max_batch_entries: usize,
64 pub max_batch_latency: Duration,
66 pub halt_threshold: Duration,
69}
70
71impl Default for WriterConfig {
72 fn default() -> Self {
73 Self {
74 channel_capacity: DEFAULT_CHANNEL_CAPACITY,
75 max_batch_entries: DEFAULT_MAX_BATCH_ENTRIES,
76 max_batch_latency: DEFAULT_MAX_BATCH_LATENCY,
77 halt_threshold: DEFAULT_HALT_THRESHOLD,
78 }
79 }
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct EntryDraft {
88 pub headers: Headers,
90 pub topic: Topic,
92 pub payload_type: PayloadType,
94 pub payload: Bytes,
96 pub ts_init: UnixNanos,
98 pub index_keys: Vec<IndexKey>,
100}
101
102impl EntryDraft {
103 #[must_use]
105 pub const fn without_indices(
106 headers: Headers,
107 topic: Topic,
108 payload_type: PayloadType,
109 payload: Bytes,
110 ts_init: UnixNanos,
111 ) -> Self {
112 Self {
113 headers,
114 topic,
115 payload_type,
116 payload,
117 ts_init,
118 index_keys: Vec::new(),
119 }
120 }
121}
122
123#[derive(Debug, thiserror::Error)]
125pub enum SubmitError {
126 #[error("writer is closed")]
128 Closed,
129 #[error("submit stalled for {stalled_for:?}, halt threshold {threshold:?}")]
132 HaltSignaled {
133 stalled_for: Duration,
135 threshold: Duration,
137 },
138}
139
140#[cfg(not(madsim))]
141mod imp {
142 use std::{
143 fmt::Debug,
144 sync::{
145 Arc,
146 atomic::{AtomicBool, AtomicU64, Ordering},
147 mpsc::{self, RecvTimeoutError, SyncSender, TrySendError},
148 },
149 thread::{self, JoinHandle},
150 time::{Duration, Instant},
151 };
152
153 use nautilus_core::time::AtomicTime;
154
155 use super::{
156 EntryDraft, SnapshotAnchor, SubmitError, WriterConfig,
157 batcher::{self, HaltSink, WriterMessage},
158 halt::{self, HaltCallback, HaltReason},
159 };
160 use crate::{backend::EventStore, error::EventStoreError};
161
162 const WRITER_THREAD_NAME: &str = "event-store-writer";
163 const SUBMIT_RETRY_INTERVAL: Duration = Duration::from_micros(100);
164
165 pub struct EventStoreWriter {
167 tx: Option<SyncSender<WriterMessage>>,
168 handle: Option<JoinHandle<()>>,
169 high_watermark: Arc<AtomicU64>,
170 halt: HaltCallback,
171 halt_threshold: Duration,
172 halted: Arc<AtomicBool>,
175 clock: &'static AtomicTime,
176 }
177
178 impl Debug for EventStoreWriter {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct(stringify!(EventStoreWriter))
181 .field(
182 "high_watermark",
183 &self.high_watermark.load(Ordering::Acquire),
184 )
185 .field("halt_threshold", &self.halt_threshold)
186 .field("halted", &self.halted.load(Ordering::Acquire))
187 .field("tx_attached", &self.tx.is_some())
188 .finish_non_exhaustive()
189 }
190 }
191
192 impl EventStoreWriter {
193 pub fn spawn(
203 backend: Box<dyn EventStore + Send>,
204 clock: &'static AtomicTime,
205 halt: HaltCallback,
206 config: WriterConfig,
207 ) -> Result<Self, EventStoreError> {
208 let initial_hwm = backend.high_watermark()?;
209 let high_watermark = Arc::new(AtomicU64::new(initial_hwm));
210 let halted = Arc::new(AtomicBool::new(false));
211 let (tx, rx) = mpsc::sync_channel::<WriterMessage>(config.channel_capacity.max(1));
214
215 let watermark_for_thread = Arc::clone(&high_watermark);
216 let halt_for_thread = Arc::clone(&halt);
217 let halted_for_thread = Arc::clone(&halted);
218 let halt_threshold = config.halt_threshold;
219 let config_for_thread = config;
220
221 let handle = thread::Builder::new()
222 .name(WRITER_THREAD_NAME.to_string())
223 .spawn(move || {
224 batcher::run(
225 backend,
226 rx,
227 config_for_thread,
228 HaltSink::new(halt_for_thread, halted_for_thread),
229 watermark_for_thread,
230 clock,
231 );
232 })
233 .map_err(|e| EventStoreError::Backend(format!("spawn writer thread: {e}")))?;
234
235 Ok(Self {
236 tx: Some(tx),
237 handle: Some(handle),
238 high_watermark,
239 halt,
240 halt_threshold,
241 halted,
242 clock,
243 })
244 }
245
246 pub fn submit(&self, draft: EntryDraft) -> Result<(), SubmitError> {
265 if self.halted.load(Ordering::Acquire) {
268 return Err(SubmitError::Closed);
269 }
270
271 let tx = self.tx.as_ref().ok_or(SubmitError::Closed)?;
272 let ts_publish = self.clock.get_time_ns();
273 let pending = WriterMessage::Entry { draft, ts_publish };
274 let start = Instant::now();
275
276 match self.enqueue_with_backpressure(tx, pending, start) {
277 Ok(()) => Ok(()),
278 Err(EnqueueFailure::Stalled(elapsed)) => Err(SubmitError::HaltSignaled {
279 stalled_for: elapsed,
280 threshold: self.halt_threshold,
281 }),
282 Err(EnqueueFailure::Closed) => Err(SubmitError::Closed),
283 }
284 }
285
286 #[must_use]
291 pub fn high_watermark(&self) -> u64 {
292 self.high_watermark.load(Ordering::Acquire)
293 }
294
295 pub fn record_snapshot_anchor(
308 &self,
309 blob_ref: impl Into<String>,
310 content_hash: impl Into<String>,
311 ) -> Result<SnapshotAnchor, EventStoreError> {
312 if self.halted.load(Ordering::Acquire) {
313 return Err(EventStoreError::Closed);
314 }
315
316 let tx = self.tx.as_ref().ok_or(EventStoreError::Closed)?;
317 let (ack_tx, ack_rx) = mpsc::sync_channel::<Result<SnapshotAnchor, EventStoreError>>(1);
318 let pending = WriterMessage::RecordSnapshotAnchor {
319 blob_ref: blob_ref.into(),
320 content_hash: content_hash.into(),
321 ack: ack_tx,
322 };
323 let start = Instant::now();
324
325 if let Err(e) = self.enqueue_with_backpressure(tx, pending, start) {
326 match e {
327 EnqueueFailure::Stalled(elapsed) => {
328 return Err(EventStoreError::Backend(format!(
329 "snapshot anchor submit stalled for {elapsed:?}, halt threshold {:?}",
330 self.halt_threshold,
331 )));
332 }
333 EnqueueFailure::Closed => return Err(EventStoreError::Closed),
334 }
335 }
336
337 match ack_rx.recv_timeout(self.halt_threshold) {
338 Ok(result) => result,
339 Err(RecvTimeoutError::Timeout) => {
340 let elapsed = start.elapsed();
341 self.signal_backpressure_stall(elapsed);
342 Err(EventStoreError::Backend(format!(
343 "snapshot anchor ack stalled for {elapsed:?}, halt threshold {:?}",
344 self.halt_threshold,
345 )))
346 }
347 Err(RecvTimeoutError::Disconnected) => Err(EventStoreError::Backend(
348 "snapshot anchor ack channel disconnected".to_string(),
349 )),
350 }
351 }
352
353 fn enqueue_with_backpressure(
354 &self,
355 tx: &SyncSender<WriterMessage>,
356 mut pending: WriterMessage,
357 start: Instant,
358 ) -> Result<(), EnqueueFailure> {
359 loop {
364 if self.halted.load(Ordering::Acquire) {
367 return Err(EnqueueFailure::Closed);
368 }
369
370 let elapsed = start.elapsed();
371
372 if elapsed >= self.halt_threshold {
373 self.signal_backpressure_stall(elapsed);
374 return Err(EnqueueFailure::Stalled(elapsed));
375 }
376
377 match tx.try_send(pending) {
378 Ok(()) => return Ok(()),
379 Err(TrySendError::Full(returned)) => {
380 pending = returned;
381 thread::sleep(SUBMIT_RETRY_INTERVAL);
382 }
383 Err(TrySendError::Disconnected(_)) => return Err(EnqueueFailure::Closed),
384 }
385 }
386 }
387
388 fn signal_backpressure_stall(&self, stalled_for: Duration) {
389 halt::fire_once(
390 &self.halt,
391 &self.halted,
392 HaltReason::BackpressureStall {
393 stalled_for,
394 threshold: self.halt_threshold,
395 },
396 );
397 }
398
399 pub fn close(mut self, run_ended: EntryDraft) -> Result<u64, EventStoreError> {
409 let tx = self
410 .tx
411 .take()
412 .ok_or_else(|| EventStoreError::Backend("writer already closed".to_string()))?;
413
414 let (ack_tx, ack_rx) = mpsc::sync_channel::<Result<u64, EventStoreError>>(1);
415 tx.send(WriterMessage::Close {
416 run_ended,
417 ack: ack_tx,
418 })
419 .map_err(|_| EventStoreError::Backend("writer thread disconnected".to_string()))?;
420 drop(tx);
423
424 let result = ack_rx.recv().map_err(|_| {
425 EventStoreError::Backend("writer ack channel disconnected".to_string())
426 })?;
427
428 if let Some(handle) = self.handle.take() {
429 handle
430 .join()
431 .map_err(|_| EventStoreError::Backend("writer thread panicked".to_string()))?;
432 }
433 result
434 }
435 }
436
437 enum EnqueueFailure {
438 Stalled(Duration),
439 Closed,
440 }
441
442 impl Drop for EventStoreWriter {
443 fn drop(&mut self) {
444 self.tx.take();
448
449 if let Some(handle) = self.handle.take() {
450 let _ = handle.join();
451 }
452 }
453 }
454}
455
456#[cfg(madsim)]
457mod imp {
458 use std::{
459 fmt::Debug,
460 sync::{
461 Arc,
462 atomic::{AtomicU64, Ordering},
463 },
464 };
465
466 use nautilus_core::time::AtomicTime;
467 use parking_lot::Mutex;
468
469 use super::{
470 EntryDraft, SnapshotAnchor, SubmitError, WriterConfig, batcher,
471 halt::{HaltCallback, HaltReason},
472 };
473 use crate::{backend::EventStore, error::EventStoreError, manifest::RunStatus};
474
475 pub struct EventStoreWriter {
481 inner: Mutex<Inner>,
482 high_watermark: Arc<AtomicU64>,
483 halt: HaltCallback,
484 clock: &'static AtomicTime,
485 }
486
487 impl Debug for EventStoreWriter {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.debug_struct(stringify!(EventStoreWriter))
490 .field(
491 "high_watermark",
492 &self.high_watermark.load(Ordering::Acquire),
493 )
494 .finish_non_exhaustive()
495 }
496 }
497
498 struct Inner {
499 backend: Box<dyn EventStore + Send>,
500 next_seq: u64,
501 closed: bool,
502 }
503
504 impl EventStoreWriter {
505 pub fn spawn(
511 backend: Box<dyn EventStore + Send>,
512 clock: &'static AtomicTime,
513 halt: HaltCallback,
514 _config: WriterConfig,
515 ) -> Result<Self, EventStoreError> {
516 let initial_hwm = backend.high_watermark()?;
517 let high_watermark = Arc::new(AtomicU64::new(initial_hwm));
518 let inner = Inner {
519 backend,
520 next_seq: initial_hwm + 1,
521 closed: false,
522 };
523
524 Ok(Self {
525 inner: Mutex::new(inner),
526 high_watermark,
527 halt,
528 clock,
529 })
530 }
531
532 pub fn submit(&self, draft: EntryDraft) -> Result<(), SubmitError> {
538 let mut inner = self.inner.lock();
539
540 if inner.closed {
541 return Err(SubmitError::Closed);
542 }
543
544 let ts_publish = self.clock.get_time_ns();
545 let seq = inner.next_seq;
546 let append = batcher::build_append_entry(draft, ts_publish, seq);
547
548 match inner.backend.append_batch(std::slice::from_ref(&append)) {
549 Ok(new_hwm) => {
550 inner.next_seq = seq + 1;
551 self.high_watermark.store(new_hwm, Ordering::Release);
552 Ok(())
553 }
554 Err(e) => {
555 (self.halt)(HaltReason::from_backend_error(&e));
556 inner.closed = true;
557 Err(SubmitError::Closed)
558 }
559 }
560 }
561
562 #[must_use]
564 pub fn high_watermark(&self) -> u64 {
565 self.high_watermark.load(Ordering::Acquire)
566 }
567
568 pub fn record_snapshot_anchor(
575 &self,
576 blob_ref: impl Into<String>,
577 content_hash: impl Into<String>,
578 ) -> Result<SnapshotAnchor, EventStoreError> {
579 let mut inner = self.inner.lock();
580
581 if inner.closed {
582 return Err(EventStoreError::Closed);
583 }
584
585 let anchor = SnapshotAnchor::new(
586 self.high_watermark.load(Ordering::Acquire),
587 blob_ref,
588 content_hash,
589 );
590
591 match inner.backend.record_snapshot_anchor(anchor.clone()) {
592 Ok(()) => Ok(anchor),
593 Err(e) => {
594 (self.halt)(HaltReason::from_backend_error(&e));
595 inner.closed = true;
596 Err(e)
597 }
598 }
599 }
600
601 pub fn close(self, run_ended: EntryDraft) -> Result<u64, EventStoreError> {
607 let mut inner = self.inner.lock();
608
609 if inner.closed {
610 return Err(EventStoreError::Backend(
611 "writer already closed".to_string(),
612 ));
613 }
614
615 let ts_publish = self.clock.get_time_ns();
616 let seq = inner.next_seq;
617 let append = batcher::build_append_entry(run_ended, ts_publish, seq);
618
619 match inner.backend.append_batch(std::slice::from_ref(&append)) {
620 Ok(new_hwm) => {
621 inner.next_seq = seq + 1;
622 self.high_watermark.store(new_hwm, Ordering::Release);
623 }
624 Err(e) => {
625 (self.halt)(HaltReason::from_backend_error(&e));
626 inner.closed = true;
627 return Err(e);
628 }
629 }
630
631 match inner.backend.seal(RunStatus::Ended) {
632 Ok(()) => {
633 inner.closed = true;
634 Ok(self.high_watermark.load(Ordering::Acquire))
635 }
636 Err(e) => {
637 (self.halt)(HaltReason::from_backend_error(&e));
638 inner.closed = true;
639 Err(e)
640 }
641 }
642 }
643 }
644}
645
646pub use imp::EventStoreWriter;
647
648#[cfg(test)]
649#[cfg(not(madsim))]
650mod tests {
651 use std::sync::{
652 Arc,
653 atomic::{AtomicUsize, Ordering},
654 };
655
656 use bytes::Bytes;
657 use indexmap::IndexMap;
658 use nautilus_core::{UnixNanos, time::get_atomic_clock_static};
659 use parking_lot::Mutex;
660 use rstest::{fixture, rstest};
661 use ustr::Ustr;
662
663 use super::*;
664 use crate::{
665 backend::{AppendEntry, EventStore, IndexKind, MemoryBackend, ScanDirection},
666 entry::EventStoreEntry,
667 error::EventStoreError,
668 manifest::{RegisteredComponents, RunManifest, RunStatus},
669 };
670
671 fn manifest(run_id: &str) -> RunManifest {
672 RunManifest {
673 run_id: run_id.to_string(),
674 parent_run_id: None,
675 instance_id: "trader-001".to_string(),
676 binary_hash: "deadbeef".to_string(),
677 schema_version: 1,
678 crate_versions: "feedface".to_string(),
679 feature_flags: Vec::new(),
680 adapter_versions: IndexMap::new(),
681 config_hash: "cafebabe".to_string(),
682 registered_components: RegisteredComponents::default(),
683 seed: None,
684 start_ts_init: UnixNanos::from(0),
685 end_ts_init: None,
686 high_watermark: 0,
687 status: RunStatus::Running,
688 }
689 }
690
691 fn entry_draft(ts_init: u64) -> EntryDraft {
692 EntryDraft {
693 headers: Headers::empty(),
694 topic: "exec.command.SubmitOrder".into(),
695 payload_type: Ustr::from("SubmitOrder"),
696 payload: Bytes::from_static(b"\x01\x02\x03\x04"),
697 ts_init: UnixNanos::from(ts_init),
698 index_keys: Vec::new(),
699 }
700 }
701
702 fn run_ended_draft() -> EntryDraft {
703 EntryDraft {
704 headers: Headers::empty(),
705 topic: "run.lifecycle.RunEnded".into(),
706 payload_type: Ustr::from("RunEnded"),
707 payload: Bytes::new(),
708 ts_init: UnixNanos::from(9_999),
709 index_keys: Vec::new(),
710 }
711 }
712
713 #[derive(Debug)]
716 struct SharedMemory(Arc<Mutex<MemoryBackend>>);
717
718 impl SharedMemory {
719 fn new() -> (Self, Arc<Mutex<MemoryBackend>>) {
720 let arc = Arc::new(Mutex::new(MemoryBackend::new()));
721 (Self(Arc::clone(&arc)), arc)
722 }
723 }
724
725 impl EventStore for SharedMemory {
726 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
727 unreachable!("test wrapper does not forward open_run")
729 }
730
731 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
732 self.0.lock().append_batch(entries)
733 }
734
735 fn scan_range(
736 &self,
737 from: u64,
738 to: u64,
739 direction: ScanDirection,
740 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
741 self.0.lock().scan_range(from, to, direction)
742 }
743
744 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
745 self.0.lock().scan_seq(seq)
746 }
747
748 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
749 self.0.lock().lookup(kind, key)
750 }
751
752 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
753 self.0.lock().iter_index_keys(kind)
754 }
755
756 fn record_snapshot_anchor(
757 &mut self,
758 anchor: SnapshotAnchor,
759 ) -> Result<(), EventStoreError> {
760 self.0.lock().record_snapshot_anchor(anchor)
761 }
762
763 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
764 self.0.lock().latest_snapshot_anchor()
765 }
766
767 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
768 self.0.lock().seal(status)
769 }
770
771 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
772 self.0.lock().manifest()
773 }
774
775 fn high_watermark(&self) -> Result<u64, EventStoreError> {
776 self.0.lock().high_watermark()
777 }
778 }
779
780 #[derive(Debug)]
782 struct BlockingBackend {
783 inner: Arc<Mutex<MemoryBackend>>,
784 gate: Arc<(Mutex<bool>, parking_lot::Condvar)>,
785 appends_seen: Arc<AtomicUsize>,
786 }
787
788 impl BlockingBackend {
789 fn new(
790 inner: Arc<Mutex<MemoryBackend>>,
791 gate: Arc<(Mutex<bool>, parking_lot::Condvar)>,
792 appends_seen: Arc<AtomicUsize>,
793 ) -> Self {
794 Self {
795 inner,
796 gate,
797 appends_seen,
798 }
799 }
800 }
801
802 impl EventStore for BlockingBackend {
803 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
804 unreachable!("test wrapper does not forward open_run")
805 }
806
807 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
808 self.appends_seen.fetch_add(1, Ordering::SeqCst);
809 let (lock, cvar) = &*self.gate;
810 let mut released = lock.lock();
811
812 while !*released {
813 cvar.wait(&mut released);
814 }
815 self.inner.lock().append_batch(entries)
816 }
817
818 fn scan_range(
819 &self,
820 from: u64,
821 to: u64,
822 direction: ScanDirection,
823 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
824 self.inner.lock().scan_range(from, to, direction)
825 }
826
827 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
828 self.inner.lock().scan_seq(seq)
829 }
830
831 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
832 self.inner.lock().lookup(kind, key)
833 }
834
835 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
836 self.inner.lock().iter_index_keys(kind)
837 }
838
839 fn record_snapshot_anchor(
840 &mut self,
841 anchor: SnapshotAnchor,
842 ) -> Result<(), EventStoreError> {
843 self.inner.lock().record_snapshot_anchor(anchor)
844 }
845
846 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
847 self.inner.lock().seal(status)
848 }
849
850 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
851 self.inner.lock().manifest()
852 }
853
854 fn high_watermark(&self) -> Result<u64, EventStoreError> {
855 self.inner.lock().high_watermark()
856 }
857 }
858
859 #[derive(Debug, Default)]
861 struct DiskFailureBackend {
862 appends_seen: Arc<AtomicUsize>,
863 }
864
865 impl EventStore for DiskFailureBackend {
866 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
867 Ok(())
868 }
869
870 fn append_batch(&mut self, _: &[AppendEntry]) -> Result<u64, EventStoreError> {
871 self.appends_seen.fetch_add(1, Ordering::SeqCst);
872 Err(EventStoreError::Disk("ENOSPC".to_string()))
873 }
874
875 fn scan_range(
876 &self,
877 _: u64,
878 _: u64,
879 _: ScanDirection,
880 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
881 Ok(Vec::new())
882 }
883
884 fn scan_seq(&self, _: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
885 Ok(None)
886 }
887
888 fn lookup(&self, _: IndexKind, _: &str) -> Result<Option<u64>, EventStoreError> {
889 Ok(None)
890 }
891
892 fn iter_index_keys(&self, _: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
893 Ok(Vec::new())
894 }
895
896 fn seal(&mut self, _: RunStatus) -> Result<(), EventStoreError> {
897 Ok(())
898 }
899
900 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
901 Err(EventStoreError::Backend("disk failure".to_string()))
902 }
903
904 fn high_watermark(&self) -> Result<u64, EventStoreError> {
905 Ok(0)
906 }
907 }
908
909 #[derive(Debug)]
912 struct GatedDiskFailureBackend {
913 gate: Arc<(Mutex<bool>, parking_lot::Condvar)>,
914 appends_seen: Arc<AtomicUsize>,
915 }
916
917 impl EventStore for GatedDiskFailureBackend {
918 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
919 Ok(())
920 }
921
922 fn append_batch(&mut self, _: &[AppendEntry]) -> Result<u64, EventStoreError> {
923 self.appends_seen.fetch_add(1, Ordering::SeqCst);
924 let (lock, cvar) = &*self.gate;
925 let mut released = lock.lock();
926
927 while !*released {
928 cvar.wait(&mut released);
929 }
930 Err(EventStoreError::Disk("ENOSPC".to_string()))
931 }
932
933 fn scan_range(
934 &self,
935 _: u64,
936 _: u64,
937 _: ScanDirection,
938 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
939 Ok(Vec::new())
940 }
941
942 fn scan_seq(&self, _: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
943 Ok(None)
944 }
945
946 fn lookup(&self, _: IndexKind, _: &str) -> Result<Option<u64>, EventStoreError> {
947 Ok(None)
948 }
949
950 fn iter_index_keys(&self, _: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
951 Ok(Vec::new())
952 }
953
954 fn seal(&mut self, _: RunStatus) -> Result<(), EventStoreError> {
955 Ok(())
956 }
957
958 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
959 Err(EventStoreError::Backend("disk failure".to_string()))
960 }
961
962 fn high_watermark(&self) -> Result<u64, EventStoreError> {
963 Ok(0)
964 }
965 }
966
967 #[fixture]
968 fn captured_halt() -> (HaltCallback, Arc<Mutex<Vec<HaltReason>>>) {
969 let captured: Arc<Mutex<Vec<HaltReason>>> = Arc::new(Mutex::new(Vec::new()));
970 let captured_for_cb = Arc::clone(&captured);
971 let halt: HaltCallback = Arc::new(move |reason| {
972 captured_for_cb.lock().push(reason);
973 });
974 (halt, captured)
975 }
976
977 #[rstest]
978 fn submit_then_close_records_entries_and_seals(
979 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
980 ) {
981 let (halt, captured) = captured_halt;
982 let (wrapper, shared) = SharedMemory::new();
983 shared.lock().open_run(manifest("run-1")).expect("open");
984
985 let writer = EventStoreWriter::spawn(
986 Box::new(wrapper),
987 get_atomic_clock_static(),
988 halt,
989 WriterConfig::default(),
990 )
991 .expect("spawn");
992
993 for ts in 10_u64..15_u64 {
994 writer.submit(entry_draft(ts)).expect("submit");
995 }
996
997 let final_hwm = writer.close(run_ended_draft()).expect("close");
998
999 assert_eq!(final_hwm, 6);
1001 let backend = shared.lock();
1002 let m = backend.manifest().expect("manifest");
1003 assert_eq!(m.status, RunStatus::Ended);
1004 assert_eq!(m.high_watermark, 6);
1005
1006 let last = backend.scan_seq(6).expect("scan").expect("present");
1007 assert_eq!(last.payload_type.as_str(), "RunEnded");
1008 assert!(captured.lock().is_empty());
1009 }
1010
1011 #[rstest]
1012 fn record_snapshot_anchor_flushes_pending_entries_and_replay_tail_starts_after_anchor(
1013 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1014 ) {
1015 let (halt, captured) = captured_halt;
1016 let (wrapper, shared) = SharedMemory::new();
1017 shared
1018 .lock()
1019 .open_run(manifest("run-anchor"))
1020 .expect("open");
1021
1022 let writer = EventStoreWriter::spawn(
1023 Box::new(wrapper),
1024 get_atomic_clock_static(),
1025 halt,
1026 WriterConfig::default(),
1027 )
1028 .expect("spawn");
1029
1030 writer.submit(entry_draft(10)).expect("submit first");
1031 writer.submit(entry_draft(11)).expect("submit second");
1032 let anchor = writer
1033 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1034 .expect("record anchor");
1035
1036 assert_eq!(anchor.high_watermark, 2);
1037
1038 writer.submit(entry_draft(12)).expect("submit third");
1039 writer.submit(entry_draft(13)).expect("submit fourth");
1040 let final_hwm = writer.close(run_ended_draft()).expect("close");
1041
1042 let backend = shared.lock();
1043 assert_eq!(
1044 backend.latest_snapshot_anchor().expect("latest anchor"),
1045 Some(anchor.clone()),
1046 );
1047
1048 let tail_seqs: Vec<_> = backend
1049 .scan_range(anchor.high_watermark + 1, final_hwm, ScanDirection::Forward)
1050 .expect("scan tail")
1051 .into_iter()
1052 .map(|entry| entry.seq)
1053 .collect();
1054
1055 assert_eq!(tail_seqs, vec![3, 4, 5]);
1056 assert!(captured.lock().is_empty());
1057 }
1058
1059 #[rstest]
1060 fn batches_respect_max_entries_threshold(
1061 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1062 ) {
1063 let (halt, _) = captured_halt;
1067 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1068 inner.lock().open_run(manifest("run-batch")).expect("open");
1069
1070 let appends_seen = Arc::new(AtomicUsize::new(0));
1071 let gate = Arc::new((Mutex::new(true), parking_lot::Condvar::new()));
1072 let backend = BlockingBackend::new(
1073 Arc::clone(&inner),
1074 Arc::clone(&gate),
1075 Arc::clone(&appends_seen),
1076 );
1077
1078 let config = WriterConfig {
1079 channel_capacity: 16,
1080 max_batch_entries: 2,
1081 max_batch_latency: Duration::from_secs(30),
1082 halt_threshold: Duration::from_secs(30),
1083 };
1084
1085 let clock = get_atomic_clock_static();
1086 let boxed = Box::new(backend);
1087
1088 let writer = EventStoreWriter::spawn(boxed, clock, halt, config).expect("spawn");
1089
1090 for ts in 10_u64..16_u64 {
1091 writer.submit(entry_draft(ts)).expect("submit");
1092 }
1093
1094 let final_hwm = writer.close(run_ended_draft()).expect("close");
1095
1096 assert_eq!(final_hwm, 7);
1098 assert_eq!(appends_seen.load(Ordering::SeqCst), 4);
1099 }
1100
1101 #[rstest]
1102 fn submit_signals_halt_when_stalled_past_threshold(
1103 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1104 ) {
1105 let (halt, captured) = captured_halt;
1109 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1110 inner.lock().open_run(manifest("run-halt")).expect("open");
1111
1112 let gate = Arc::new((Mutex::new(false), parking_lot::Condvar::new()));
1113 let appends_seen = Arc::new(AtomicUsize::new(0));
1114 let backend = BlockingBackend::new(
1115 Arc::clone(&inner),
1116 Arc::clone(&gate),
1117 Arc::clone(&appends_seen),
1118 );
1119
1120 let halt_threshold = Duration::from_millis(50);
1121 let config = WriterConfig {
1122 channel_capacity: 1,
1123 max_batch_entries: 1,
1124 max_batch_latency: Duration::from_millis(1),
1125 halt_threshold,
1126 };
1127
1128 let clock = get_atomic_clock_static();
1129 let boxed = Box::new(backend);
1130
1131 let writer = EventStoreWriter::spawn(boxed, clock, halt, config).expect("spawn");
1132
1133 writer.submit(entry_draft(10)).expect("first submit fits");
1135
1136 std::thread::sleep(Duration::from_millis(20));
1138
1139 let _ = writer.submit(entry_draft(11));
1142 let stalled = writer.submit(entry_draft(12)).expect_err("must stall");
1143
1144 match stalled {
1145 SubmitError::HaltSignaled { .. } => {}
1146 SubmitError::Closed => panic!("expected HaltSignaled, was Closed"),
1147 }
1148 let captured_reasons = captured.lock();
1149 assert_eq!(
1150 captured_reasons.len(),
1151 1,
1152 "halt callback must fire exactly once",
1153 );
1154 assert_backpressure_stall(captured_reasons.first(), halt_threshold);
1155 drop(captured_reasons);
1156
1157 let post_halt = writer
1161 .submit(entry_draft(13))
1162 .expect_err("post-halt submit");
1163
1164 match post_halt {
1165 SubmitError::Closed => {}
1166 SubmitError::HaltSignaled { .. } => {
1167 panic!("expected Closed after halt, was HaltSignaled")
1168 }
1169 }
1170 assert_eq!(
1172 captured.lock().len(),
1173 1,
1174 "halt callback must not refire after the first stall",
1175 );
1176
1177 let (lock, cvar) = &*gate;
1180 *lock.lock() = true;
1181 cvar.notify_all();
1182 }
1183
1184 #[rstest]
1185 fn halt_fires_once_across_stall_and_backend_failure(
1186 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1187 ) {
1188 let (halt, captured) = captured_halt;
1191 let gate = Arc::new((Mutex::new(false), parking_lot::Condvar::new()));
1192 let appends_seen = Arc::new(AtomicUsize::new(0));
1193 let backend = GatedDiskFailureBackend {
1194 gate: Arc::clone(&gate),
1195 appends_seen: Arc::clone(&appends_seen),
1196 };
1197
1198 let halt_threshold = Duration::from_millis(50);
1199 let config = WriterConfig {
1200 channel_capacity: 1,
1201 max_batch_entries: 1,
1202 max_batch_latency: Duration::from_millis(1),
1203 halt_threshold,
1204 };
1205
1206 let clock = get_atomic_clock_static();
1207 let boxed = Box::new(backend);
1208
1209 let writer = EventStoreWriter::spawn(boxed, clock, halt, config).expect("spawn");
1210
1211 writer.submit(entry_draft(10)).expect("first submit fits");
1212
1213 let mut waited = Duration::ZERO;
1214 while appends_seen.load(Ordering::SeqCst) == 0 && waited < Duration::from_secs(2) {
1215 std::thread::sleep(Duration::from_millis(5));
1216 waited += Duration::from_millis(5);
1217 }
1218 assert_eq!(
1219 appends_seen.load(Ordering::SeqCst),
1220 1,
1221 "writer thread did not reach the gated append",
1222 );
1223
1224 let _ = writer.submit(entry_draft(11));
1226 let stalled = writer.submit(entry_draft(12)).expect_err("must stall");
1227 assert!(
1228 matches!(stalled, SubmitError::HaltSignaled { .. }),
1229 "was {stalled:?}",
1230 );
1231
1232 let (lock, cvar) = &*gate;
1235 *lock.lock() = true;
1236 cvar.notify_all();
1237
1238 drop(writer);
1240
1241 let reasons = captured.lock();
1242 assert_eq!(
1243 reasons.len(),
1244 1,
1245 "halt must fire exactly once across stall and backend failure",
1246 );
1247 assert_backpressure_stall(reasons.first(), halt_threshold);
1248 }
1249
1250 #[rstest]
1251 fn zero_channel_capacity_is_clamped_and_submit_buffers(
1252 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1253 ) {
1254 let (halt, captured) = captured_halt;
1255 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1256 inner
1257 .lock()
1258 .open_run(manifest("run-zero-capacity"))
1259 .expect("open");
1260
1261 let gate = Arc::new((Mutex::new(false), parking_lot::Condvar::new()));
1262 let appends_seen = Arc::new(AtomicUsize::new(0));
1263 let backend = BlockingBackend::new(
1264 Arc::clone(&inner),
1265 Arc::clone(&gate),
1266 Arc::clone(&appends_seen),
1267 );
1268
1269 let config = WriterConfig {
1270 channel_capacity: 0,
1271 max_batch_entries: 1,
1272 max_batch_latency: Duration::from_millis(1),
1273 halt_threshold: Duration::from_millis(250),
1274 };
1275
1276 let clock = get_atomic_clock_static();
1277 let boxed = Box::new(backend);
1278
1279 let writer = EventStoreWriter::spawn(boxed, clock, halt, config).expect("spawn");
1280
1281 writer.submit(entry_draft(10)).expect("first submit fits");
1282
1283 let mut waited = Duration::ZERO;
1284 while appends_seen.load(Ordering::SeqCst) == 0 && waited < Duration::from_secs(2) {
1285 std::thread::sleep(Duration::from_millis(5));
1286 waited += Duration::from_millis(5);
1287 }
1288 assert_eq!(
1289 appends_seen.load(Ordering::SeqCst),
1290 1,
1291 "writer thread did not reach the gated append",
1292 );
1293
1294 let second_submit = writer.submit(entry_draft(11));
1297
1298 let (lock, cvar) = &*gate;
1299 *lock.lock() = true;
1300 cvar.notify_all();
1301
1302 let final_hwm = writer.close(run_ended_draft()).expect("close");
1303 second_submit.expect("second submit must be buffered by the clamped capacity");
1304 assert_eq!(final_hwm, 3);
1305 assert!(captured.lock().is_empty());
1306 }
1307
1308 #[rstest]
1309 fn submit_after_writer_thread_halt_returns_closed(
1310 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1311 ) {
1312 let (halt, captured) = captured_halt;
1315 let config = WriterConfig {
1316 max_batch_entries: 1,
1317 ..WriterConfig::default()
1318 };
1319
1320 let writer = EventStoreWriter::spawn(
1321 Box::new(DiskFailureBackend::default()),
1322 get_atomic_clock_static(),
1323 halt,
1324 config,
1325 )
1326 .expect("spawn");
1327
1328 writer.submit(entry_draft(10)).expect("submit accepted");
1329
1330 let mut waited = Duration::ZERO;
1331 while waited < Duration::from_secs(2) {
1332 if !captured.lock().is_empty() {
1333 break;
1334 }
1335 std::thread::sleep(Duration::from_millis(5));
1336 waited += Duration::from_millis(5);
1337 }
1338
1339 let reasons = captured.lock();
1340 assert_eq!(reasons.len(), 1, "writer-thread halt did not fire");
1341 assert!(
1342 matches!(reasons.first(), Some(HaltReason::BackendDisk(_))),
1343 "was {:?}",
1344 reasons.first(),
1345 );
1346 drop(reasons);
1347
1348 let post_halt = writer
1349 .submit(entry_draft(11))
1350 .expect_err("post-halt submit must reject");
1351 assert!(
1352 matches!(post_halt, SubmitError::Closed),
1353 "was {post_halt:?}",
1354 );
1355 assert_eq!(
1356 captured.lock().len(),
1357 1,
1358 "halt must not refire on post-halt submits",
1359 );
1360 }
1361
1362 #[rstest]
1363 fn retrying_submit_returns_closed_after_stall_halt_latches(
1364 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1365 ) {
1366 let (halt, captured) = captured_halt;
1369 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1370 inner
1371 .lock()
1372 .open_run(manifest("run-retry-latch"))
1373 .expect("open");
1374
1375 let gate = Arc::new((Mutex::new(false), parking_lot::Condvar::new()));
1376 let appends_seen = Arc::new(AtomicUsize::new(0));
1377 let backend = BlockingBackend::new(
1378 Arc::clone(&inner),
1379 Arc::clone(&gate),
1380 Arc::clone(&appends_seen),
1381 );
1382
1383 let config = WriterConfig {
1384 channel_capacity: 1,
1385 max_batch_entries: 1,
1386 max_batch_latency: Duration::from_millis(1),
1387 halt_threshold: Duration::from_millis(50),
1388 };
1389
1390 let clock = get_atomic_clock_static();
1391 let writer = Arc::new(
1392 EventStoreWriter::spawn(Box::new(backend), clock, halt, config).expect("spawn"),
1393 );
1394
1395 writer.submit(entry_draft(10)).expect("first submit fits");
1396
1397 let mut waited = Duration::ZERO;
1398 while appends_seen.load(Ordering::SeqCst) == 0 && waited < Duration::from_secs(2) {
1399 std::thread::sleep(Duration::from_millis(5));
1400 waited += Duration::from_millis(5);
1401 }
1402 assert_eq!(
1403 appends_seen.load(Ordering::SeqCst),
1404 1,
1405 "writer thread did not reach the gated append",
1406 );
1407
1408 writer
1409 .submit(entry_draft(11))
1410 .expect("second submit fills the slot");
1411
1412 let stalled = writer.submit(entry_draft(12)).expect_err("must stall");
1414 assert!(
1415 matches!(stalled, SubmitError::HaltSignaled { .. }),
1416 "was {stalled:?}",
1417 );
1418
1419 let writer_for_thread = Arc::clone(&writer);
1421 let retrying = std::thread::spawn(move || writer_for_thread.submit(entry_draft(13)));
1422 std::thread::sleep(Duration::from_millis(20));
1423
1424 let (lock, cvar) = &*gate;
1426 *lock.lock() = true;
1427 cvar.notify_all();
1428
1429 let result = retrying.join().expect("retrying thread panicked");
1430 assert!(matches!(result, Err(SubmitError::Closed)), "was {result:?}");
1431
1432 let mut waited = Duration::ZERO;
1434 while writer.high_watermark() < 2 && waited < Duration::from_secs(2) {
1435 std::thread::sleep(Duration::from_millis(5));
1436 waited += Duration::from_millis(5);
1437 }
1438 assert_eq!(writer.high_watermark(), 2);
1439 assert_eq!(
1440 captured.lock().len(),
1441 1,
1442 "halt must not refire for the refused submit",
1443 );
1444 }
1445
1446 #[rstest]
1447 fn record_snapshot_anchor_signals_halt_when_ack_stalls(
1448 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1449 ) {
1450 let (halt, captured) = captured_halt;
1451 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1452 inner
1453 .lock()
1454 .open_run(manifest("run-anchor-halt"))
1455 .expect("open");
1456
1457 let gate = Arc::new((Mutex::new(false), parking_lot::Condvar::new()));
1458 let appends_seen = Arc::new(AtomicUsize::new(0));
1459 let backend = BlockingBackend::new(
1460 Arc::clone(&inner),
1461 Arc::clone(&gate),
1462 Arc::clone(&appends_seen),
1463 );
1464
1465 let halt_threshold = Duration::from_millis(50);
1466
1467 let writer = EventStoreWriter::spawn(
1468 Box::new(backend),
1469 get_atomic_clock_static(),
1470 halt,
1471 WriterConfig {
1472 channel_capacity: 2,
1473 max_batch_entries: 1,
1474 max_batch_latency: Duration::from_millis(1),
1475 halt_threshold,
1476 },
1477 )
1478 .expect("spawn");
1479
1480 writer.submit(entry_draft(10)).expect("first submit fits");
1481 std::thread::sleep(Duration::from_millis(20));
1482
1483 let err = writer
1484 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1485 .expect_err("snapshot anchor ack must time out");
1486 let post_halt = writer
1487 .submit(entry_draft(11))
1488 .expect_err("post-halt submit");
1489
1490 let (lock, cvar) = &*gate;
1491 *lock.lock() = true;
1492 cvar.notify_all();
1493
1494 match err {
1495 EventStoreError::Backend(msg) => {
1496 assert!(
1497 msg.contains("snapshot anchor ack stalled"),
1498 "msg was: {msg}"
1499 );
1500 }
1501 other => panic!("expected Backend, was {other:?}"),
1502 }
1503
1504 match post_halt {
1505 SubmitError::Closed => {}
1506 SubmitError::HaltSignaled { .. } => {
1507 panic!("expected Closed after anchor halt, was HaltSignaled")
1508 }
1509 }
1510
1511 let captured_reasons = captured.lock();
1512 assert_eq!(
1513 captured_reasons.len(),
1514 1,
1515 "halt callback must fire exactly once",
1516 );
1517 assert_backpressure_stall(captured_reasons.first(), halt_threshold);
1518 }
1519
1520 #[rstest]
1521 fn record_snapshot_anchor_signals_halt_when_submit_stalls(
1522 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1523 ) {
1524 let (halt, captured) = captured_halt;
1525 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1526 inner
1527 .lock()
1528 .open_run(manifest("run-anchor-submit-halt"))
1529 .expect("open");
1530
1531 let gate = Arc::new((Mutex::new(false), parking_lot::Condvar::new()));
1532 let appends_seen = Arc::new(AtomicUsize::new(0));
1533 let backend = BlockingBackend::new(
1534 Arc::clone(&inner),
1535 Arc::clone(&gate),
1536 Arc::clone(&appends_seen),
1537 );
1538
1539 let halt_threshold = Duration::from_millis(50);
1540
1541 let writer = EventStoreWriter::spawn(
1542 Box::new(backend),
1543 get_atomic_clock_static(),
1544 halt,
1545 WriterConfig {
1546 channel_capacity: 1,
1547 max_batch_entries: 1,
1548 max_batch_latency: Duration::from_secs(30),
1549 halt_threshold,
1550 },
1551 )
1552 .expect("spawn");
1553
1554 writer.submit(entry_draft(10)).expect("first submit fits");
1555 let mut waited = Duration::ZERO;
1556 while appends_seen.load(Ordering::SeqCst) == 0 && waited < Duration::from_secs(1) {
1557 std::thread::sleep(Duration::from_millis(2));
1558 waited += Duration::from_millis(2);
1559 }
1560 assert_eq!(
1561 appends_seen.load(Ordering::SeqCst),
1562 1,
1563 "writer must be blocked inside the first backend append",
1564 );
1565 writer.submit(entry_draft(11)).expect("second submit fits");
1566
1567 let err = writer
1568 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1569 .expect_err("snapshot anchor submit must time out");
1570 let post_halt = writer
1571 .submit(entry_draft(12))
1572 .expect_err("post-halt submit");
1573
1574 let (lock, cvar) = &*gate;
1575 *lock.lock() = true;
1576 cvar.notify_all();
1577
1578 match err {
1579 EventStoreError::Backend(msg) => {
1580 assert!(
1581 msg.contains("snapshot anchor submit stalled"),
1582 "msg was: {msg}"
1583 );
1584 }
1585 other => panic!("expected Backend, was {other:?}"),
1586 }
1587
1588 match post_halt {
1589 SubmitError::Closed => {}
1590 SubmitError::HaltSignaled { .. } => {
1591 panic!("expected Closed after anchor halt, was HaltSignaled")
1592 }
1593 }
1594
1595 let captured_reasons = captured.lock();
1596 assert_eq!(
1597 captured_reasons.len(),
1598 1,
1599 "halt callback must fire exactly once",
1600 );
1601 assert_backpressure_stall(captured_reasons.first(), halt_threshold);
1602 }
1603
1604 #[rstest]
1605 fn backend_disk_error_fires_halt_and_closes_writer(
1606 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1607 ) {
1608 let (halt, captured) = captured_halt;
1609 let backend = DiskFailureBackend::default();
1610
1611 let writer = EventStoreWriter::spawn(
1612 Box::new(backend),
1613 get_atomic_clock_static(),
1614 halt,
1615 WriterConfig {
1616 channel_capacity: 4,
1617 max_batch_entries: 1,
1618 max_batch_latency: Duration::from_millis(1),
1619 halt_threshold: Duration::from_millis(500),
1620 },
1621 )
1622 .expect("spawn");
1623
1624 writer
1625 .submit(entry_draft(10))
1626 .expect("first submit fits in channel before writer fail-stops");
1627
1628 let mut waited = Duration::ZERO;
1630 let deadline = Duration::from_millis(500);
1631 while captured.lock().is_empty() && waited < deadline {
1632 std::thread::sleep(Duration::from_millis(10));
1633 waited += Duration::from_millis(10);
1634 }
1635
1636 let captured_reasons = captured.lock();
1637 assert!(matches!(
1638 captured_reasons.first(),
1639 Some(HaltReason::BackendDisk(_))
1640 ));
1641 drop(captured_reasons);
1642
1643 let mut closed_seen = false;
1645
1646 for _ in 0..50 {
1647 match writer.submit(entry_draft(11)) {
1648 Err(SubmitError::Closed) => {
1649 closed_seen = true;
1650 break;
1651 }
1652 _ => std::thread::sleep(Duration::from_millis(10)),
1653 }
1654 }
1655 assert!(closed_seen, "submits must surface Closed after fail-stop");
1656
1657 let close_result = writer.close(run_ended_draft());
1659 assert!(close_result.is_err());
1660 }
1661
1662 #[rstest]
1663 fn time_driven_flush_advances_watermark_before_close(
1664 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1665 ) {
1666 let (halt, _) = captured_halt;
1670 let (wrapper, shared) = SharedMemory::new();
1671 shared.lock().open_run(manifest("run-time")).expect("open");
1672
1673 let writer = EventStoreWriter::spawn(
1674 Box::new(wrapper),
1675 get_atomic_clock_static(),
1676 halt,
1677 WriterConfig {
1678 channel_capacity: 32,
1679 max_batch_entries: 100,
1680 max_batch_latency: Duration::from_millis(20),
1681 halt_threshold: Duration::from_secs(30),
1682 },
1683 )
1684 .expect("spawn");
1685
1686 writer.submit(entry_draft(10)).expect("submit");
1687
1688 let mut waited = Duration::ZERO;
1690 while writer.high_watermark() == 0 && waited < Duration::from_millis(500) {
1691 std::thread::sleep(Duration::from_millis(5));
1692 waited += Duration::from_millis(5);
1693 }
1694 assert_eq!(
1695 writer.high_watermark(),
1696 1,
1697 "latency window must commit a sub-batch entry before close",
1698 );
1699
1700 let final_hwm = writer.close(run_ended_draft()).expect("close");
1701 assert_eq!(final_hwm, 2);
1702 }
1703
1704 #[rstest]
1705 fn entry_draft_without_indices_constructor() {
1706 let topic: crate::entry::Topic = "exec.command.SubmitOrder".into();
1707 let payload_type = Ustr::from("SubmitOrder");
1708 let payload = Bytes::from_static(b"\x01\x02");
1709 let ts_init = UnixNanos::from(42);
1710 let draft = EntryDraft::without_indices(
1711 Headers::empty(),
1712 topic,
1713 payload_type,
1714 payload.clone(),
1715 ts_init,
1716 );
1717
1718 assert!(draft.headers.is_empty());
1719 assert_eq!(draft.topic.as_ref(), "exec.command.SubmitOrder");
1720 assert_eq!(draft.payload_type.as_str(), "SubmitOrder");
1721 assert_eq!(draft.payload, payload);
1722 assert_eq!(draft.ts_init, ts_init);
1723 assert!(draft.index_keys.is_empty());
1724 }
1725
1726 fn assert_backpressure_stall(reason: Option<&HaltReason>, expected_threshold: Duration) {
1727 match reason {
1728 Some(HaltReason::BackpressureStall {
1729 stalled_for,
1730 threshold,
1731 }) => {
1732 assert!(
1733 *stalled_for >= expected_threshold,
1734 "stalled_for {stalled_for:?} must be >= {expected_threshold:?}",
1735 );
1736 assert_eq!(*threshold, expected_threshold);
1737 }
1738 other => panic!("expected BackpressureStall, was {other:?}"),
1739 }
1740 }
1741}
1742
1743#[cfg(test)]
1744#[cfg(madsim)]
1745mod madsim_tests {
1746 use std::sync::Arc;
1747
1748 use bytes::Bytes;
1749 use indexmap::IndexMap;
1750 use nautilus_core::{UnixNanos, time::get_atomic_clock_static};
1751 use parking_lot::Mutex;
1752 use rstest::rstest;
1753 use ustr::Ustr;
1754
1755 use super::*;
1756 use crate::{
1757 backend::{AppendEntry, EventStore, IndexKind, MemoryBackend, ScanDirection},
1758 entry::EventStoreEntry,
1759 error::EventStoreError,
1760 manifest::{RegisteredComponents, RunManifest, RunStatus},
1761 };
1762
1763 fn manifest(run_id: &str) -> RunManifest {
1764 RunManifest {
1765 run_id: run_id.to_string(),
1766 parent_run_id: None,
1767 instance_id: "trader-001".to_string(),
1768 binary_hash: "deadbeef".to_string(),
1769 schema_version: 1,
1770 crate_versions: "feedface".to_string(),
1771 feature_flags: Vec::new(),
1772 adapter_versions: IndexMap::new(),
1773 config_hash: "cafebabe".to_string(),
1774 registered_components: RegisteredComponents::default(),
1775 seed: None,
1776 start_ts_init: UnixNanos::from(0),
1777 end_ts_init: None,
1778 high_watermark: 0,
1779 status: RunStatus::Running,
1780 }
1781 }
1782
1783 fn entry_draft(ts_init: u64) -> EntryDraft {
1784 EntryDraft {
1785 headers: Headers::empty(),
1786 topic: "exec.command.SubmitOrder".into(),
1787 payload_type: Ustr::from("SubmitOrder"),
1788 payload: Bytes::from_static(b"\x01\x02\x03\x04"),
1789 ts_init: UnixNanos::from(ts_init),
1790 index_keys: Vec::new(),
1791 }
1792 }
1793
1794 #[derive(Debug)]
1795 struct SharedMemory(Arc<Mutex<MemoryBackend>>);
1796
1797 impl SharedMemory {
1798 fn new() -> (Self, Arc<Mutex<MemoryBackend>>) {
1799 let arc = Arc::new(Mutex::new(MemoryBackend::new()));
1800 (Self(Arc::clone(&arc)), arc)
1801 }
1802 }
1803
1804 impl EventStore for SharedMemory {
1805 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
1806 unreachable!("test wrapper does not forward open_run")
1807 }
1808
1809 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
1810 self.0.lock().append_batch(entries)
1811 }
1812
1813 fn scan_range(
1814 &self,
1815 from: u64,
1816 to: u64,
1817 direction: ScanDirection,
1818 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
1819 self.0.lock().scan_range(from, to, direction)
1820 }
1821
1822 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
1823 self.0.lock().scan_seq(seq)
1824 }
1825
1826 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
1827 self.0.lock().lookup(kind, key)
1828 }
1829
1830 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
1831 self.0.lock().iter_index_keys(kind)
1832 }
1833
1834 fn record_snapshot_anchor(
1835 &mut self,
1836 anchor: SnapshotAnchor,
1837 ) -> Result<(), EventStoreError> {
1838 self.0.lock().record_snapshot_anchor(anchor)
1839 }
1840
1841 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
1842 self.0.lock().latest_snapshot_anchor()
1843 }
1844
1845 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
1846 self.0.lock().seal(status)
1847 }
1848
1849 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
1850 self.0.lock().manifest()
1851 }
1852
1853 fn high_watermark(&self) -> Result<u64, EventStoreError> {
1854 self.0.lock().high_watermark()
1855 }
1856 }
1857
1858 #[rstest]
1859 fn record_snapshot_anchor_records_current_watermark_under_madsim() {
1860 let (wrapper, shared) = SharedMemory::new();
1861 shared
1862 .lock()
1863 .open_run(manifest("run-anchor"))
1864 .expect("open");
1865
1866 let writer = EventStoreWriter::spawn(
1867 Box::new(wrapper),
1868 get_atomic_clock_static(),
1869 noop_halt(),
1870 WriterConfig::default(),
1871 )
1872 .expect("spawn");
1873
1874 writer.submit(entry_draft(10)).expect("submit first");
1875 writer.submit(entry_draft(11)).expect("submit second");
1876 let anchor = writer
1877 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1878 .expect("record anchor");
1879
1880 let backend = shared.lock();
1881 assert_eq!(anchor.high_watermark, 2);
1882 assert_eq!(
1883 backend.latest_snapshot_anchor().expect("latest anchor"),
1884 Some(anchor),
1885 );
1886 }
1887}