1mod batcher;
32pub mod halt;
33
34use std::time::Duration;
35
36use bytes::Bytes;
37pub use halt::{HaltCallback, HaltReason, noop_halt};
38use nautilus_core::UnixNanos;
39
40use crate::{
41 backend::IndexKey,
42 entry::{PayloadType, Topic},
43 headers::Headers,
44 snapshot::SnapshotAnchor,
45};
46
47pub const DEFAULT_CHANNEL_CAPACITY: usize = 10_000;
49pub const DEFAULT_MAX_BATCH_ENTRIES: usize = 100;
51pub const DEFAULT_MAX_BATCH_LATENCY: Duration = Duration::from_millis(5);
53pub const DEFAULT_HALT_THRESHOLD: Duration = Duration::from_millis(250);
55
56#[derive(Clone, Debug)]
58pub struct WriterConfig {
59 pub channel_capacity: usize,
61 pub max_batch_entries: usize,
63 pub max_batch_latency: Duration,
65 pub halt_threshold: Duration,
68}
69
70impl Default for WriterConfig {
71 fn default() -> Self {
72 Self {
73 channel_capacity: DEFAULT_CHANNEL_CAPACITY,
74 max_batch_entries: DEFAULT_MAX_BATCH_ENTRIES,
75 max_batch_latency: DEFAULT_MAX_BATCH_LATENCY,
76 halt_threshold: DEFAULT_HALT_THRESHOLD,
77 }
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct EntryDraft {
87 pub headers: Headers,
89 pub topic: Topic,
91 pub payload_type: PayloadType,
93 pub payload: Bytes,
95 pub ts_init: UnixNanos,
97 pub index_keys: Vec<IndexKey>,
99}
100
101impl EntryDraft {
102 #[must_use]
104 pub const fn without_indices(
105 headers: Headers,
106 topic: Topic,
107 payload_type: PayloadType,
108 payload: Bytes,
109 ts_init: UnixNanos,
110 ) -> Self {
111 Self {
112 headers,
113 topic,
114 payload_type,
115 payload,
116 ts_init,
117 index_keys: Vec::new(),
118 }
119 }
120}
121
122#[derive(Debug, thiserror::Error)]
124pub enum SubmitError {
125 #[error("writer is closed")]
127 Closed,
128 #[error("submit stalled for {stalled_for:?}, halt threshold {threshold:?}")]
131 HaltSignaled {
132 stalled_for: Duration,
134 threshold: Duration,
136 },
137}
138
139#[cfg(not(madsim))]
140mod imp {
141 use std::{
142 fmt::Debug,
143 sync::{
144 Arc,
145 atomic::{AtomicBool, AtomicU64, Ordering},
146 mpsc::{self, RecvTimeoutError, SyncSender, TrySendError},
147 },
148 thread::{self, JoinHandle},
149 time::{Duration, Instant},
150 };
151
152 use nautilus_core::time::AtomicTime;
153
154 use super::{
155 EntryDraft, SnapshotAnchor, SubmitError, WriterConfig,
156 batcher::{self, WriterMessage},
157 halt::{HaltCallback, HaltReason},
158 };
159 use crate::{backend::EventStore, error::EventStoreError};
160
161 const WRITER_THREAD_NAME: &str = "event-store-writer";
162 const SUBMIT_RETRY_INTERVAL: Duration = Duration::from_micros(100);
163
164 pub struct EventStoreWriter {
166 tx: Option<SyncSender<WriterMessage>>,
167 handle: Option<JoinHandle<()>>,
168 high_watermark: Arc<AtomicU64>,
169 halt: HaltCallback,
170 halt_threshold: Duration,
171 halted: 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 (tx, rx) = mpsc::sync_channel::<WriterMessage>(config.channel_capacity);
211
212 let watermark_for_thread = Arc::clone(&high_watermark);
213 let halt_for_thread = Arc::clone(&halt);
214 let halt_threshold = config.halt_threshold;
215 let config_for_thread = config;
216
217 let handle = thread::Builder::new()
218 .name(WRITER_THREAD_NAME.to_string())
219 .spawn(move || {
220 batcher::run(
221 backend,
222 rx,
223 config_for_thread,
224 halt_for_thread,
225 watermark_for_thread,
226 clock,
227 );
228 })
229 .map_err(|e| EventStoreError::Backend(format!("spawn writer thread: {e}")))?;
230
231 Ok(Self {
232 tx: Some(tx),
233 handle: Some(handle),
234 high_watermark,
235 halt,
236 halt_threshold,
237 halted: AtomicBool::new(false),
238 clock,
239 })
240 }
241
242 pub fn submit(&self, draft: EntryDraft) -> Result<(), SubmitError> {
263 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 let elapsed = start.elapsed();
365
366 if elapsed >= self.halt_threshold {
367 self.signal_backpressure_stall(elapsed);
368 return Err(EnqueueFailure::Stalled(elapsed));
369 }
370
371 match tx.try_send(pending) {
372 Ok(()) => return Ok(()),
373 Err(TrySendError::Full(returned)) => {
374 pending = returned;
375 thread::sleep(SUBMIT_RETRY_INTERVAL);
376 }
377 Err(TrySendError::Disconnected(_)) => return Err(EnqueueFailure::Closed),
378 }
379 }
380 }
381
382 fn signal_backpressure_stall(&self, stalled_for: Duration) {
383 self.halted.store(true, Ordering::Release);
384 (self.halt)(HaltReason::BackpressureStall {
385 stalled_for,
386 threshold: self.halt_threshold,
387 });
388 }
389
390 pub fn close(mut self, run_ended: EntryDraft) -> Result<u64, EventStoreError> {
400 let tx = self
401 .tx
402 .take()
403 .ok_or_else(|| EventStoreError::Backend("writer already closed".to_string()))?;
404
405 let (ack_tx, ack_rx) = mpsc::sync_channel::<Result<u64, EventStoreError>>(1);
406 tx.send(WriterMessage::Close {
407 run_ended,
408 ack: ack_tx,
409 })
410 .map_err(|_| EventStoreError::Backend("writer thread disconnected".to_string()))?;
411 drop(tx);
414
415 let result = ack_rx.recv().map_err(|_| {
416 EventStoreError::Backend("writer ack channel disconnected".to_string())
417 })?;
418
419 if let Some(handle) = self.handle.take() {
420 handle
421 .join()
422 .map_err(|_| EventStoreError::Backend("writer thread panicked".to_string()))?;
423 }
424 result
425 }
426 }
427
428 enum EnqueueFailure {
429 Stalled(Duration),
430 Closed,
431 }
432
433 impl Drop for EventStoreWriter {
434 fn drop(&mut self) {
435 self.tx.take();
439
440 if let Some(handle) = self.handle.take() {
441 let _ = handle.join();
442 }
443 }
444 }
445}
446
447#[cfg(madsim)]
448mod imp {
449 use std::{
450 fmt::Debug,
451 sync::{
452 Arc, Mutex,
453 atomic::{AtomicU64, Ordering},
454 },
455 };
456
457 use nautilus_core::time::AtomicTime;
458
459 use super::{
460 EntryDraft, SnapshotAnchor, SubmitError, WriterConfig, batcher,
461 halt::{HaltCallback, HaltReason},
462 };
463 use crate::{backend::EventStore, error::EventStoreError, manifest::RunStatus};
464
465 pub struct EventStoreWriter {
471 inner: Mutex<Inner>,
472 high_watermark: Arc<AtomicU64>,
473 halt: HaltCallback,
474 clock: &'static AtomicTime,
475 }
476
477 impl Debug for EventStoreWriter {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 f.debug_struct(stringify!(EventStoreWriter))
480 .field(
481 "high_watermark",
482 &self.high_watermark.load(Ordering::Acquire),
483 )
484 .finish_non_exhaustive()
485 }
486 }
487
488 struct Inner {
489 backend: Box<dyn EventStore + Send>,
490 next_seq: u64,
491 closed: bool,
492 }
493
494 impl EventStoreWriter {
495 #[expect(
501 clippy::needless_pass_by_value,
502 reason = "synchronous writer keeps ownership of the backend and halt callback"
503 )]
504 pub fn spawn(
505 backend: Box<dyn EventStore + Send>,
506 clock: &'static AtomicTime,
507 halt: HaltCallback,
508 _config: WriterConfig,
509 ) -> Result<Self, EventStoreError> {
510 let initial_hwm = backend.high_watermark()?;
511 let high_watermark = Arc::new(AtomicU64::new(initial_hwm));
512 let inner = Inner {
513 backend,
514 next_seq: initial_hwm + 1,
515 closed: false,
516 };
517
518 Ok(Self {
519 inner: Mutex::new(inner),
520 high_watermark,
521 halt,
522 clock,
523 })
524 }
525
526 pub fn submit(&self, draft: EntryDraft) -> Result<(), SubmitError> {
536 let mut inner = self.inner.lock().expect("writer mutex poisoned");
537
538 if inner.closed {
539 return Err(SubmitError::Closed);
540 }
541
542 let ts_publish = self.clock.get_time_ns();
543 let seq = inner.next_seq;
544 let append = batcher::build_append_entry(draft, ts_publish, seq);
545
546 match inner.backend.append_batch(std::slice::from_ref(&append)) {
547 Ok(new_hwm) => {
548 inner.next_seq = seq + 1;
549 self.high_watermark.store(new_hwm, Ordering::Release);
550 Ok(())
551 }
552 Err(e) => {
553 (self.halt)(HaltReason::from_backend_error(&e));
554 inner.closed = true;
555 Err(SubmitError::Closed)
556 }
557 }
558 }
559
560 #[must_use]
562 pub fn high_watermark(&self) -> u64 {
563 self.high_watermark.load(Ordering::Acquire)
564 }
565
566 pub fn record_snapshot_anchor(
577 &self,
578 blob_ref: impl Into<String>,
579 content_hash: impl Into<String>,
580 ) -> Result<SnapshotAnchor, EventStoreError> {
581 let mut inner = self.inner.lock().expect("writer mutex poisoned");
582
583 if inner.closed {
584 return Err(EventStoreError::Closed);
585 }
586
587 let anchor = SnapshotAnchor::new(
588 self.high_watermark.load(Ordering::Acquire),
589 blob_ref,
590 content_hash,
591 );
592
593 match inner.backend.record_snapshot_anchor(anchor.clone()) {
594 Ok(()) => Ok(anchor),
595 Err(e) => {
596 (self.halt)(HaltReason::from_backend_error(&e));
597 inner.closed = true;
598 Err(e)
599 }
600 }
601 }
602
603 pub fn close(self, run_ended: EntryDraft) -> Result<u64, EventStoreError> {
613 let mut inner = self.inner.lock().expect("writer mutex poisoned");
614
615 if inner.closed {
616 return Err(EventStoreError::Backend(
617 "writer already closed".to_string(),
618 ));
619 }
620
621 let ts_publish = self.clock.get_time_ns();
622 let seq = inner.next_seq;
623 let append = batcher::build_append_entry(run_ended, ts_publish, seq);
624
625 match inner.backend.append_batch(std::slice::from_ref(&append)) {
626 Ok(new_hwm) => {
627 inner.next_seq = seq + 1;
628 self.high_watermark.store(new_hwm, Ordering::Release);
629 }
630 Err(e) => {
631 (self.halt)(HaltReason::from_backend_error(&e));
632 inner.closed = true;
633 return Err(e);
634 }
635 }
636
637 match inner.backend.seal(RunStatus::Ended) {
638 Ok(()) => {
639 inner.closed = true;
640 Ok(self.high_watermark.load(Ordering::Acquire))
641 }
642 Err(e) => {
643 (self.halt)(HaltReason::from_backend_error(&e));
644 inner.closed = true;
645 Err(e)
646 }
647 }
648 }
649 }
650}
651
652pub use imp::EventStoreWriter;
653
654#[cfg(test)]
655#[cfg(not(madsim))]
656mod tests {
657 use std::sync::{
658 Arc, Mutex,
659 atomic::{AtomicUsize, Ordering},
660 };
661
662 use bytes::Bytes;
663 use indexmap::IndexMap;
664 use nautilus_core::{UnixNanos, time::get_atomic_clock_static};
665 use rstest::{fixture, rstest};
666 use ustr::Ustr;
667
668 use super::*;
669 use crate::{
670 backend::{AppendEntry, EventStore, IndexKind, MemoryBackend, ScanDirection},
671 entry::EventStoreEntry,
672 error::EventStoreError,
673 manifest::{RegisteredComponents, RunManifest, RunStatus},
674 };
675
676 fn manifest(run_id: &str) -> RunManifest {
677 RunManifest {
678 run_id: run_id.to_string(),
679 parent_run_id: None,
680 instance_id: "trader-001".to_string(),
681 binary_hash: "deadbeef".to_string(),
682 schema_version: 1,
683 crate_versions: "feedface".to_string(),
684 feature_flags: Vec::new(),
685 adapter_versions: IndexMap::new(),
686 config_hash: "cafebabe".to_string(),
687 registered_components: RegisteredComponents::default(),
688 seed: None,
689 start_ts_init: UnixNanos::from(0),
690 end_ts_init: None,
691 high_watermark: 0,
692 status: RunStatus::Running,
693 }
694 }
695
696 fn entry_draft(ts_init: u64) -> EntryDraft {
697 EntryDraft {
698 headers: Headers::empty(),
699 topic: "exec.command.SubmitOrder".into(),
700 payload_type: Ustr::from("SubmitOrder"),
701 payload: Bytes::from_static(b"\x01\x02\x03\x04"),
702 ts_init: UnixNanos::from(ts_init),
703 index_keys: Vec::new(),
704 }
705 }
706
707 fn run_ended_draft() -> EntryDraft {
708 EntryDraft {
709 headers: Headers::empty(),
710 topic: "run.lifecycle.RunEnded".into(),
711 payload_type: Ustr::from("RunEnded"),
712 payload: Bytes::new(),
713 ts_init: UnixNanos::from(9_999),
714 index_keys: Vec::new(),
715 }
716 }
717
718 #[derive(Debug)]
721 struct SharedMemory(Arc<Mutex<MemoryBackend>>);
722
723 impl SharedMemory {
724 fn new() -> (Self, Arc<Mutex<MemoryBackend>>) {
725 let arc = Arc::new(Mutex::new(MemoryBackend::new()));
726 (Self(Arc::clone(&arc)), arc)
727 }
728 }
729
730 impl EventStore for SharedMemory {
731 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
732 unreachable!("test wrapper does not forward open_run")
734 }
735
736 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
737 self.0
738 .lock()
739 .expect("shared memory poisoned")
740 .append_batch(entries)
741 }
742
743 fn scan_range(
744 &self,
745 from: u64,
746 to: u64,
747 direction: ScanDirection,
748 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
749 self.0
750 .lock()
751 .expect("shared memory poisoned")
752 .scan_range(from, to, direction)
753 }
754
755 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
756 self.0.lock().expect("shared memory poisoned").scan_seq(seq)
757 }
758
759 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
760 self.0
761 .lock()
762 .expect("shared memory poisoned")
763 .lookup(kind, key)
764 }
765
766 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
767 self.0
768 .lock()
769 .expect("shared memory poisoned")
770 .iter_index_keys(kind)
771 }
772
773 fn record_snapshot_anchor(
774 &mut self,
775 anchor: SnapshotAnchor,
776 ) -> Result<(), EventStoreError> {
777 self.0
778 .lock()
779 .expect("shared memory poisoned")
780 .record_snapshot_anchor(anchor)
781 }
782
783 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
784 self.0
785 .lock()
786 .expect("shared memory poisoned")
787 .latest_snapshot_anchor()
788 }
789
790 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
791 self.0.lock().expect("shared memory poisoned").seal(status)
792 }
793
794 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
795 self.0.lock().expect("shared memory poisoned").manifest()
796 }
797
798 fn high_watermark(&self) -> Result<u64, EventStoreError> {
799 self.0
800 .lock()
801 .expect("shared memory poisoned")
802 .high_watermark()
803 }
804 }
805
806 #[derive(Debug)]
808 struct BlockingBackend {
809 inner: Arc<Mutex<MemoryBackend>>,
810 gate: Arc<(Mutex<bool>, std::sync::Condvar)>,
811 appends_seen: Arc<AtomicUsize>,
812 }
813
814 impl BlockingBackend {
815 fn new(
816 inner: Arc<Mutex<MemoryBackend>>,
817 gate: Arc<(Mutex<bool>, std::sync::Condvar)>,
818 appends_seen: Arc<AtomicUsize>,
819 ) -> Self {
820 Self {
821 inner,
822 gate,
823 appends_seen,
824 }
825 }
826 }
827
828 impl EventStore for BlockingBackend {
829 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
830 unreachable!("test wrapper does not forward open_run")
831 }
832
833 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
834 self.appends_seen.fetch_add(1, Ordering::SeqCst);
835 let (lock, cvar) = &*self.gate;
836 let mut released = lock.lock().expect("gate poisoned");
837
838 while !*released {
839 released = cvar.wait(released).expect("gate wait");
840 }
841 self.inner
842 .lock()
843 .expect("inner poisoned")
844 .append_batch(entries)
845 }
846
847 fn scan_range(
848 &self,
849 from: u64,
850 to: u64,
851 direction: ScanDirection,
852 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
853 self.inner
854 .lock()
855 .expect("inner poisoned")
856 .scan_range(from, to, direction)
857 }
858
859 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
860 self.inner.lock().expect("inner poisoned").scan_seq(seq)
861 }
862
863 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
864 self.inner.lock().expect("inner poisoned").lookup(kind, key)
865 }
866
867 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
868 self.inner
869 .lock()
870 .expect("inner poisoned")
871 .iter_index_keys(kind)
872 }
873
874 fn record_snapshot_anchor(
875 &mut self,
876 anchor: SnapshotAnchor,
877 ) -> Result<(), EventStoreError> {
878 self.inner
879 .lock()
880 .expect("inner poisoned")
881 .record_snapshot_anchor(anchor)
882 }
883
884 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
885 self.inner.lock().expect("inner poisoned").seal(status)
886 }
887
888 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
889 self.inner.lock().expect("inner poisoned").manifest()
890 }
891
892 fn high_watermark(&self) -> Result<u64, EventStoreError> {
893 self.inner.lock().expect("inner poisoned").high_watermark()
894 }
895 }
896
897 #[derive(Debug, Default)]
899 struct DiskFailureBackend {
900 appends_seen: Arc<AtomicUsize>,
901 }
902
903 impl EventStore for DiskFailureBackend {
904 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
905 Ok(())
906 }
907
908 fn append_batch(&mut self, _: &[AppendEntry]) -> Result<u64, EventStoreError> {
909 self.appends_seen.fetch_add(1, Ordering::SeqCst);
910 Err(EventStoreError::Disk("ENOSPC".to_string()))
911 }
912
913 fn scan_range(
914 &self,
915 _: u64,
916 _: u64,
917 _: ScanDirection,
918 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
919 Ok(Vec::new())
920 }
921
922 fn scan_seq(&self, _: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
923 Ok(None)
924 }
925
926 fn lookup(&self, _: IndexKind, _: &str) -> Result<Option<u64>, EventStoreError> {
927 Ok(None)
928 }
929
930 fn iter_index_keys(&self, _: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
931 Ok(Vec::new())
932 }
933
934 fn seal(&mut self, _: RunStatus) -> Result<(), EventStoreError> {
935 Ok(())
936 }
937
938 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
939 Err(EventStoreError::Backend("disk failure".to_string()))
940 }
941
942 fn high_watermark(&self) -> Result<u64, EventStoreError> {
943 Ok(0)
944 }
945 }
946
947 #[fixture]
948 fn captured_halt() -> (HaltCallback, Arc<Mutex<Vec<HaltReason>>>) {
949 let captured: Arc<Mutex<Vec<HaltReason>>> = Arc::new(Mutex::new(Vec::new()));
950 let captured_for_cb = Arc::clone(&captured);
951 let halt: HaltCallback = Arc::new(move |reason| {
952 captured_for_cb
953 .lock()
954 .expect("captured halt poisoned")
955 .push(reason);
956 });
957 (halt, captured)
958 }
959
960 #[rstest]
961 fn submit_then_close_records_entries_and_seals(
962 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
963 ) {
964 let (halt, captured) = captured_halt;
965 let (wrapper, shared) = SharedMemory::new();
966 shared
967 .lock()
968 .expect("shared")
969 .open_run(manifest("run-1"))
970 .expect("open");
971
972 let writer = EventStoreWriter::spawn(
973 Box::new(wrapper),
974 get_atomic_clock_static(),
975 halt,
976 WriterConfig::default(),
977 )
978 .expect("spawn");
979
980 for ts in 10_u64..15_u64 {
981 writer.submit(entry_draft(ts)).expect("submit");
982 }
983
984 let final_hwm = writer.close(run_ended_draft()).expect("close");
985
986 assert_eq!(final_hwm, 6);
988 let backend = shared.lock().expect("shared");
989 let m = backend.manifest().expect("manifest");
990 assert_eq!(m.status, RunStatus::Ended);
991 assert_eq!(m.high_watermark, 6);
992
993 let last = backend.scan_seq(6).expect("scan").expect("present");
994 assert_eq!(last.payload_type.as_str(), "RunEnded");
995 assert!(captured.lock().expect("captured").is_empty());
996 }
997
998 #[rstest]
999 fn record_snapshot_anchor_flushes_pending_entries_and_replay_tail_starts_after_anchor(
1000 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1001 ) {
1002 let (halt, captured) = captured_halt;
1003 let (wrapper, shared) = SharedMemory::new();
1004 shared
1005 .lock()
1006 .expect("shared")
1007 .open_run(manifest("run-anchor"))
1008 .expect("open");
1009
1010 let writer = EventStoreWriter::spawn(
1011 Box::new(wrapper),
1012 get_atomic_clock_static(),
1013 halt,
1014 WriterConfig::default(),
1015 )
1016 .expect("spawn");
1017
1018 writer.submit(entry_draft(10)).expect("submit first");
1019 writer.submit(entry_draft(11)).expect("submit second");
1020 let anchor = writer
1021 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1022 .expect("record anchor");
1023
1024 assert_eq!(anchor.high_watermark, 2);
1025
1026 writer.submit(entry_draft(12)).expect("submit third");
1027 writer.submit(entry_draft(13)).expect("submit fourth");
1028 let final_hwm = writer.close(run_ended_draft()).expect("close");
1029
1030 let backend = shared.lock().expect("shared");
1031 assert_eq!(
1032 backend.latest_snapshot_anchor().expect("latest anchor"),
1033 Some(anchor.clone()),
1034 );
1035
1036 let tail_seqs: Vec<_> = backend
1037 .scan_range(anchor.high_watermark + 1, final_hwm, ScanDirection::Forward)
1038 .expect("scan tail")
1039 .into_iter()
1040 .map(|entry| entry.seq)
1041 .collect();
1042
1043 assert_eq!(tail_seqs, vec![3, 4, 5]);
1044 assert!(captured.lock().expect("captured").is_empty());
1045 }
1046
1047 #[rstest]
1048 fn batches_respect_max_entries_threshold(
1049 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1050 ) {
1051 let (halt, _) = captured_halt;
1055 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1056 inner
1057 .lock()
1058 .expect("inner")
1059 .open_run(manifest("run-batch"))
1060 .expect("open");
1061
1062 let appends_seen = Arc::new(AtomicUsize::new(0));
1063 let gate = Arc::new((Mutex::new(true), std::sync::Condvar::new()));
1064 let backend = BlockingBackend::new(
1065 Arc::clone(&inner),
1066 Arc::clone(&gate),
1067 Arc::clone(&appends_seen),
1068 );
1069
1070 let config = WriterConfig {
1071 channel_capacity: 16,
1072 max_batch_entries: 2,
1073 max_batch_latency: Duration::from_secs(30),
1074 halt_threshold: Duration::from_secs(30),
1075 };
1076
1077 let clock = get_atomic_clock_static();
1078 let boxed = Box::new(backend);
1079
1080 let writer = EventStoreWriter::spawn(boxed, clock, halt, config).expect("spawn");
1081
1082 for ts in 10_u64..16_u64 {
1083 writer.submit(entry_draft(ts)).expect("submit");
1084 }
1085
1086 let final_hwm = writer.close(run_ended_draft()).expect("close");
1087
1088 assert_eq!(final_hwm, 7);
1090 assert_eq!(appends_seen.load(Ordering::SeqCst), 4);
1091 }
1092
1093 #[rstest]
1094 fn submit_signals_halt_when_stalled_past_threshold(
1095 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1096 ) {
1097 let (halt, captured) = captured_halt;
1101 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1102 inner
1103 .lock()
1104 .expect("inner")
1105 .open_run(manifest("run-halt"))
1106 .expect("open");
1107
1108 let gate = Arc::new((Mutex::new(false), std::sync::Condvar::new()));
1109 let appends_seen = Arc::new(AtomicUsize::new(0));
1110 let backend = BlockingBackend::new(
1111 Arc::clone(&inner),
1112 Arc::clone(&gate),
1113 Arc::clone(&appends_seen),
1114 );
1115
1116 let halt_threshold = Duration::from_millis(50);
1117 let config = WriterConfig {
1118 channel_capacity: 1,
1119 max_batch_entries: 1,
1120 max_batch_latency: Duration::from_millis(1),
1121 halt_threshold,
1122 };
1123
1124 let clock = get_atomic_clock_static();
1125 let boxed = Box::new(backend);
1126
1127 let writer = EventStoreWriter::spawn(boxed, clock, halt, config).expect("spawn");
1128
1129 writer.submit(entry_draft(10)).expect("first submit fits");
1131
1132 std::thread::sleep(Duration::from_millis(20));
1134
1135 let _ = writer.submit(entry_draft(11));
1138 let stalled = writer.submit(entry_draft(12)).expect_err("must stall");
1139
1140 match stalled {
1141 SubmitError::HaltSignaled { .. } => {}
1142 SubmitError::Closed => panic!("expected HaltSignaled, was Closed"),
1143 }
1144 let captured_reasons = captured.lock().expect("captured");
1145 assert_eq!(
1146 captured_reasons.len(),
1147 1,
1148 "halt callback must fire exactly once",
1149 );
1150 assert_backpressure_stall(captured_reasons.first(), halt_threshold);
1151 drop(captured_reasons);
1152
1153 let post_halt = writer
1157 .submit(entry_draft(13))
1158 .expect_err("post-halt submit");
1159
1160 match post_halt {
1161 SubmitError::Closed => {}
1162 SubmitError::HaltSignaled { .. } => {
1163 panic!("expected Closed after halt, was HaltSignaled")
1164 }
1165 }
1166 assert_eq!(
1168 captured.lock().expect("captured").len(),
1169 1,
1170 "halt callback must not refire after the first stall",
1171 );
1172
1173 let (lock, cvar) = &*gate;
1176 *lock.lock().expect("gate") = true;
1177 cvar.notify_all();
1178 }
1179
1180 #[rstest]
1181 fn record_snapshot_anchor_signals_halt_when_ack_stalls(
1182 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1183 ) {
1184 let (halt, captured) = captured_halt;
1185 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1186 inner
1187 .lock()
1188 .expect("inner")
1189 .open_run(manifest("run-anchor-halt"))
1190 .expect("open");
1191
1192 let gate = Arc::new((Mutex::new(false), std::sync::Condvar::new()));
1193 let appends_seen = Arc::new(AtomicUsize::new(0));
1194 let backend = BlockingBackend::new(
1195 Arc::clone(&inner),
1196 Arc::clone(&gate),
1197 Arc::clone(&appends_seen),
1198 );
1199
1200 let halt_threshold = Duration::from_millis(50);
1201
1202 let writer = EventStoreWriter::spawn(
1203 Box::new(backend),
1204 get_atomic_clock_static(),
1205 halt,
1206 WriterConfig {
1207 channel_capacity: 2,
1208 max_batch_entries: 1,
1209 max_batch_latency: Duration::from_millis(1),
1210 halt_threshold,
1211 },
1212 )
1213 .expect("spawn");
1214
1215 writer.submit(entry_draft(10)).expect("first submit fits");
1216 std::thread::sleep(Duration::from_millis(20));
1217
1218 let err = writer
1219 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1220 .expect_err("snapshot anchor ack must time out");
1221 let post_halt = writer
1222 .submit(entry_draft(11))
1223 .expect_err("post-halt submit");
1224
1225 let (lock, cvar) = &*gate;
1226 *lock.lock().expect("gate") = true;
1227 cvar.notify_all();
1228
1229 match err {
1230 EventStoreError::Backend(msg) => {
1231 assert!(
1232 msg.contains("snapshot anchor ack stalled"),
1233 "msg was: {msg}"
1234 );
1235 }
1236 other => panic!("expected Backend, was {other:?}"),
1237 }
1238
1239 match post_halt {
1240 SubmitError::Closed => {}
1241 SubmitError::HaltSignaled { .. } => {
1242 panic!("expected Closed after anchor halt, was HaltSignaled")
1243 }
1244 }
1245
1246 let captured_reasons = captured.lock().expect("captured");
1247 assert_eq!(
1248 captured_reasons.len(),
1249 1,
1250 "halt callback must fire exactly once",
1251 );
1252 assert_backpressure_stall(captured_reasons.first(), halt_threshold);
1253 }
1254
1255 #[rstest]
1256 fn record_snapshot_anchor_signals_halt_when_submit_stalls(
1257 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1258 ) {
1259 let (halt, captured) = captured_halt;
1260 let inner = Arc::new(Mutex::new(MemoryBackend::new()));
1261 inner
1262 .lock()
1263 .expect("inner")
1264 .open_run(manifest("run-anchor-submit-halt"))
1265 .expect("open");
1266
1267 let gate = Arc::new((Mutex::new(false), std::sync::Condvar::new()));
1268 let appends_seen = Arc::new(AtomicUsize::new(0));
1269 let backend = BlockingBackend::new(
1270 Arc::clone(&inner),
1271 Arc::clone(&gate),
1272 Arc::clone(&appends_seen),
1273 );
1274
1275 let halt_threshold = Duration::from_millis(50);
1276
1277 let writer = EventStoreWriter::spawn(
1278 Box::new(backend),
1279 get_atomic_clock_static(),
1280 halt,
1281 WriterConfig {
1282 channel_capacity: 1,
1283 max_batch_entries: 1,
1284 max_batch_latency: Duration::from_secs(30),
1285 halt_threshold,
1286 },
1287 )
1288 .expect("spawn");
1289
1290 writer.submit(entry_draft(10)).expect("first submit fits");
1291 let mut waited = Duration::ZERO;
1292 while appends_seen.load(Ordering::SeqCst) == 0 && waited < Duration::from_secs(1) {
1293 std::thread::sleep(Duration::from_millis(2));
1294 waited += Duration::from_millis(2);
1295 }
1296 assert_eq!(
1297 appends_seen.load(Ordering::SeqCst),
1298 1,
1299 "writer must be blocked inside the first backend append",
1300 );
1301 writer.submit(entry_draft(11)).expect("second submit fits");
1302
1303 let err = writer
1304 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1305 .expect_err("snapshot anchor submit must time out");
1306 let post_halt = writer
1307 .submit(entry_draft(12))
1308 .expect_err("post-halt submit");
1309
1310 let (lock, cvar) = &*gate;
1311 *lock.lock().expect("gate") = true;
1312 cvar.notify_all();
1313
1314 match err {
1315 EventStoreError::Backend(msg) => {
1316 assert!(
1317 msg.contains("snapshot anchor submit stalled"),
1318 "msg was: {msg}"
1319 );
1320 }
1321 other => panic!("expected Backend, was {other:?}"),
1322 }
1323
1324 match post_halt {
1325 SubmitError::Closed => {}
1326 SubmitError::HaltSignaled { .. } => {
1327 panic!("expected Closed after anchor halt, was HaltSignaled")
1328 }
1329 }
1330
1331 let captured_reasons = captured.lock().expect("captured");
1332 assert_eq!(
1333 captured_reasons.len(),
1334 1,
1335 "halt callback must fire exactly once",
1336 );
1337 assert_backpressure_stall(captured_reasons.first(), halt_threshold);
1338 }
1339
1340 #[rstest]
1341 fn backend_disk_error_fires_halt_and_closes_writer(
1342 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1343 ) {
1344 let (halt, captured) = captured_halt;
1345 let backend = DiskFailureBackend::default();
1346
1347 let writer = EventStoreWriter::spawn(
1348 Box::new(backend),
1349 get_atomic_clock_static(),
1350 halt,
1351 WriterConfig {
1352 channel_capacity: 4,
1353 max_batch_entries: 1,
1354 max_batch_latency: Duration::from_millis(1),
1355 halt_threshold: Duration::from_millis(500),
1356 },
1357 )
1358 .expect("spawn");
1359
1360 writer
1361 .submit(entry_draft(10))
1362 .expect("first submit fits in channel before writer fail-stops");
1363
1364 let mut waited = Duration::ZERO;
1366 let deadline = Duration::from_millis(500);
1367 while captured.lock().expect("captured").is_empty() && waited < deadline {
1368 std::thread::sleep(Duration::from_millis(10));
1369 waited += Duration::from_millis(10);
1370 }
1371
1372 let captured_reasons = captured.lock().expect("captured");
1373 assert!(matches!(
1374 captured_reasons.first(),
1375 Some(HaltReason::BackendDisk(_))
1376 ));
1377 drop(captured_reasons);
1378
1379 let mut closed_seen = false;
1381
1382 for _ in 0..50 {
1383 match writer.submit(entry_draft(11)) {
1384 Err(SubmitError::Closed) => {
1385 closed_seen = true;
1386 break;
1387 }
1388 _ => std::thread::sleep(Duration::from_millis(10)),
1389 }
1390 }
1391 assert!(closed_seen, "submits must surface Closed after fail-stop");
1392
1393 let close_result = writer.close(run_ended_draft());
1395 assert!(close_result.is_err());
1396 }
1397
1398 #[rstest]
1399 fn time_driven_flush_advances_watermark_before_close(
1400 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
1401 ) {
1402 let (halt, _) = captured_halt;
1406 let (wrapper, shared) = SharedMemory::new();
1407 shared
1408 .lock()
1409 .expect("shared")
1410 .open_run(manifest("run-time"))
1411 .expect("open");
1412
1413 let writer = EventStoreWriter::spawn(
1414 Box::new(wrapper),
1415 get_atomic_clock_static(),
1416 halt,
1417 WriterConfig {
1418 channel_capacity: 32,
1419 max_batch_entries: 100,
1420 max_batch_latency: Duration::from_millis(20),
1421 halt_threshold: Duration::from_secs(30),
1422 },
1423 )
1424 .expect("spawn");
1425
1426 writer.submit(entry_draft(10)).expect("submit");
1427
1428 let mut waited = Duration::ZERO;
1430 while writer.high_watermark() == 0 && waited < Duration::from_millis(500) {
1431 std::thread::sleep(Duration::from_millis(5));
1432 waited += Duration::from_millis(5);
1433 }
1434 assert_eq!(
1435 writer.high_watermark(),
1436 1,
1437 "latency window must commit a sub-batch entry before close",
1438 );
1439
1440 let final_hwm = writer.close(run_ended_draft()).expect("close");
1441 assert_eq!(final_hwm, 2);
1442 }
1443
1444 #[rstest]
1445 fn entry_draft_without_indices_constructor() {
1446 let topic: crate::entry::Topic = "exec.command.SubmitOrder".into();
1447 let payload_type = Ustr::from("SubmitOrder");
1448 let payload = Bytes::from_static(b"\x01\x02");
1449 let ts_init = UnixNanos::from(42);
1450 let draft = EntryDraft::without_indices(
1451 Headers::empty(),
1452 topic,
1453 payload_type,
1454 payload.clone(),
1455 ts_init,
1456 );
1457
1458 assert!(draft.headers.is_empty());
1459 assert_eq!(draft.topic.as_ref(), "exec.command.SubmitOrder");
1460 assert_eq!(draft.payload_type.as_str(), "SubmitOrder");
1461 assert_eq!(draft.payload, payload);
1462 assert_eq!(draft.ts_init, ts_init);
1463 assert!(draft.index_keys.is_empty());
1464 }
1465
1466 fn assert_backpressure_stall(reason: Option<&HaltReason>, expected_threshold: Duration) {
1467 match reason {
1468 Some(HaltReason::BackpressureStall {
1469 stalled_for,
1470 threshold,
1471 }) => {
1472 assert!(
1473 *stalled_for >= expected_threshold,
1474 "stalled_for {stalled_for:?} must be >= {expected_threshold:?}",
1475 );
1476 assert_eq!(*threshold, expected_threshold);
1477 }
1478 other => panic!("expected BackpressureStall, was {other:?}"),
1479 }
1480 }
1481}
1482
1483#[cfg(test)]
1484#[cfg(madsim)]
1485mod madsim_tests {
1486 use std::sync::{Arc, Mutex};
1487
1488 use bytes::Bytes;
1489 use indexmap::IndexMap;
1490 use nautilus_core::{UnixNanos, time::get_atomic_clock_static};
1491 use rstest::rstest;
1492 use ustr::Ustr;
1493
1494 use super::*;
1495 use crate::{
1496 backend::{AppendEntry, EventStore, IndexKind, MemoryBackend, ScanDirection},
1497 entry::EventStoreEntry,
1498 error::EventStoreError,
1499 manifest::{RegisteredComponents, RunManifest, RunStatus},
1500 };
1501
1502 fn manifest(run_id: &str) -> RunManifest {
1503 RunManifest {
1504 run_id: run_id.to_string(),
1505 parent_run_id: None,
1506 instance_id: "trader-001".to_string(),
1507 binary_hash: "deadbeef".to_string(),
1508 schema_version: 1,
1509 crate_versions: "feedface".to_string(),
1510 feature_flags: Vec::new(),
1511 adapter_versions: IndexMap::new(),
1512 config_hash: "cafebabe".to_string(),
1513 registered_components: RegisteredComponents::default(),
1514 seed: None,
1515 start_ts_init: UnixNanos::from(0),
1516 end_ts_init: None,
1517 high_watermark: 0,
1518 status: RunStatus::Running,
1519 }
1520 }
1521
1522 fn entry_draft(ts_init: u64) -> EntryDraft {
1523 EntryDraft {
1524 headers: Headers::empty(),
1525 topic: "exec.command.SubmitOrder".into(),
1526 payload_type: Ustr::from("SubmitOrder"),
1527 payload: Bytes::from_static(b"\x01\x02\x03\x04"),
1528 ts_init: UnixNanos::from(ts_init),
1529 index_keys: Vec::new(),
1530 }
1531 }
1532
1533 #[derive(Debug)]
1534 struct SharedMemory(Arc<Mutex<MemoryBackend>>);
1535
1536 impl SharedMemory {
1537 fn new() -> (Self, Arc<Mutex<MemoryBackend>>) {
1538 let arc = Arc::new(Mutex::new(MemoryBackend::new()));
1539 (Self(Arc::clone(&arc)), arc)
1540 }
1541 }
1542
1543 impl EventStore for SharedMemory {
1544 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
1545 unreachable!("test wrapper does not forward open_run")
1546 }
1547
1548 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
1549 self.0
1550 .lock()
1551 .expect("shared memory poisoned")
1552 .append_batch(entries)
1553 }
1554
1555 fn scan_range(
1556 &self,
1557 from: u64,
1558 to: u64,
1559 direction: ScanDirection,
1560 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
1561 self.0
1562 .lock()
1563 .expect("shared memory poisoned")
1564 .scan_range(from, to, direction)
1565 }
1566
1567 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
1568 self.0.lock().expect("shared memory poisoned").scan_seq(seq)
1569 }
1570
1571 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
1572 self.0
1573 .lock()
1574 .expect("shared memory poisoned")
1575 .lookup(kind, key)
1576 }
1577
1578 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
1579 self.0
1580 .lock()
1581 .expect("shared memory poisoned")
1582 .iter_index_keys(kind)
1583 }
1584
1585 fn record_snapshot_anchor(
1586 &mut self,
1587 anchor: SnapshotAnchor,
1588 ) -> Result<(), EventStoreError> {
1589 self.0
1590 .lock()
1591 .expect("shared memory poisoned")
1592 .record_snapshot_anchor(anchor)
1593 }
1594
1595 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
1596 self.0
1597 .lock()
1598 .expect("shared memory poisoned")
1599 .latest_snapshot_anchor()
1600 }
1601
1602 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
1603 self.0.lock().expect("shared memory poisoned").seal(status)
1604 }
1605
1606 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
1607 self.0.lock().expect("shared memory poisoned").manifest()
1608 }
1609
1610 fn high_watermark(&self) -> Result<u64, EventStoreError> {
1611 self.0
1612 .lock()
1613 .expect("shared memory poisoned")
1614 .high_watermark()
1615 }
1616 }
1617
1618 #[rstest]
1619 fn record_snapshot_anchor_records_current_watermark_under_madsim() {
1620 let (wrapper, shared) = SharedMemory::new();
1621 shared
1622 .lock()
1623 .expect("shared")
1624 .open_run(manifest("run-anchor"))
1625 .expect("open");
1626
1627 let writer = EventStoreWriter::spawn(
1628 Box::new(wrapper),
1629 get_atomic_clock_static(),
1630 noop_halt(),
1631 WriterConfig::default(),
1632 )
1633 .expect("spawn");
1634
1635 writer.submit(entry_draft(10)).expect("submit first");
1636 writer.submit(entry_draft(11)).expect("submit second");
1637 let anchor = writer
1638 .record_snapshot_anchor("cache://position-snapshots/P-1/0", "blake3:abc")
1639 .expect("record anchor");
1640
1641 let backend = shared.lock().expect("shared");
1642 assert_eq!(anchor.high_watermark, 2);
1643 assert_eq!(
1644 backend.latest_snapshot_anchor().expect("latest anchor"),
1645 Some(anchor),
1646 );
1647 }
1648}