1use std::{
36 collections::VecDeque,
37 fmt::Debug,
38 sync::{
39 Arc, Mutex, PoisonError,
40 atomic::{AtomicBool, AtomicU64, Ordering},
41 },
42};
43
44use ahash::AHashSet;
45use nautilus_core::{UUID4, UnixNanos};
46
47use crate::{
48 capture::{encoder::EncodeError, registry::EncoderRegistry},
49 entry::Topic,
50 headers::Headers,
51 writer::{EntryDraft, EventStoreWriter, HaltCallback, HaltReason, SubmitError},
52};
53
54const RECENT_IDENTITY_CAPACITY: usize = 128;
57
58#[derive(Debug, thiserror::Error)]
64pub enum CaptureError {
65 #[error("encode failure: {0}")]
67 Encode(#[from] EncodeError),
68 #[error("writer submit failed: {0}")]
73 Submit(#[from] SubmitError),
74 #[error("capture adapter halted")]
80 Halted,
81}
82
83pub struct BusCaptureAdapter {
90 writer: Arc<EventStoreWriter>,
91 registry: Arc<EncoderRegistry>,
92 halt: HaltCallback,
93 halted: AtomicBool,
94 submit_counter: Option<Arc<AtomicU64>>,
95 recent_identities: Mutex<RecentIdentities>,
96}
97
98#[derive(Debug, Default)]
100struct RecentIdentities {
101 order: VecDeque<UUID4>,
102 seen: AHashSet<UUID4>,
103}
104
105impl RecentIdentities {
106 fn note_fresh(&mut self, identity: UUID4) -> bool {
108 if self.seen.contains(&identity) {
109 return false;
110 }
111
112 if self.order.len() == RECENT_IDENTITY_CAPACITY
113 && let Some(evicted) = self.order.pop_front()
114 {
115 self.seen.remove(&evicted);
116 }
117 self.order.push_back(identity);
118 self.seen.insert(identity);
119 true
120 }
121}
122
123impl Debug for BusCaptureAdapter {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_struct(stringify!(BusCaptureAdapter))
126 .field("registered_encoders", &self.registry.len())
127 .field("halted", &self.halted.load(Ordering::Acquire))
128 .finish_non_exhaustive()
129 }
130}
131
132impl BusCaptureAdapter {
133 #[must_use]
140 pub fn new(
141 writer: Arc<EventStoreWriter>,
142 registry: Arc<EncoderRegistry>,
143 halt: HaltCallback,
144 ) -> Self {
145 Self {
146 writer,
147 registry,
148 halt,
149 halted: AtomicBool::new(false),
150 submit_counter: None,
151 recent_identities: Mutex::new(RecentIdentities::default()),
152 }
153 }
154
155 #[must_use]
157 pub fn with_submit_counter(mut self, submit_counter: Arc<AtomicU64>) -> Self {
158 self.submit_counter = Some(submit_counter);
159 self
160 }
161
162 #[must_use]
164 pub fn is_halted(&self) -> bool {
165 self.halted.load(Ordering::Acquire)
166 }
167
168 #[must_use]
170 pub fn registry(&self) -> &EncoderRegistry {
171 &self.registry
172 }
173
174 #[must_use]
176 pub fn high_watermark(&self) -> u64 {
177 self.writer.high_watermark()
178 }
179
180 pub fn capture<T: 'static>(
205 &self,
206 topic: Topic,
207 message: &T,
208 headers: Headers,
209 ts_init: UnixNanos,
210 ) -> Result<bool, CaptureError> {
211 self.capture_any(topic, message as &dyn std::any::Any, headers, ts_init)
212 }
213
214 pub fn capture_any(
225 &self,
226 topic: Topic,
227 message: &dyn std::any::Any,
228 headers: Headers,
229 ts_init: UnixNanos,
230 ) -> Result<bool, CaptureError> {
231 if self.halted.load(Ordering::Acquire) {
232 return Err(CaptureError::Halted);
233 }
234
235 if let Some(identity) = self.registry.identity_for_any(message)
236 && !self.note_fresh_identity(identity)
237 {
238 return Ok(false);
239 }
240
241 let Some((payload_type, encoded)) = self.registry.encode_any(message)? else {
242 return Ok(false);
243 };
244
245 let draft = EntryDraft {
246 headers,
247 topic,
248 payload_type,
249 payload: encoded.payload,
250 ts_init,
251 index_keys: encoded.index_keys,
252 };
253
254 match self.writer.submit(draft) {
255 Ok(()) => {
256 if let Some(submit_counter) = self.submit_counter.as_ref() {
257 submit_counter.fetch_add(1, Ordering::AcqRel);
258 }
259 Ok(true)
260 }
261 Err(e) => {
262 self.fail_stop(&e);
263 Err(CaptureError::Submit(e))
264 }
265 }
266 }
267
268 fn note_fresh_identity(&self, identity: UUID4) -> bool {
269 self.recent_identities
272 .lock()
273 .unwrap_or_else(PoisonError::into_inner)
274 .note_fresh(identity)
275 }
276
277 fn fail_stop(&self, err: &SubmitError) {
278 if self
279 .halted
280 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
281 .is_ok()
282 {
283 (self.halt)(halt_reason_from_submit(err));
284 }
285 }
286}
287
288fn halt_reason_from_submit(err: &SubmitError) -> HaltReason {
296 match err {
297 SubmitError::HaltSignaled {
298 stalled_for,
299 threshold,
300 } => HaltReason::BackpressureStall {
301 stalled_for: *stalled_for,
302 threshold: *threshold,
303 },
304 SubmitError::Closed => HaltReason::BackendError("event store writer closed".to_string()),
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use std::{
311 sync::{
312 Arc, Mutex,
313 atomic::{AtomicU64, Ordering},
314 },
315 time::Duration,
316 };
317
318 use bytes::Bytes;
319 use indexmap::IndexMap;
320 use nautilus_core::{UnixNanos, time::get_atomic_clock_static};
321 use rstest::{fixture, rstest};
322 use ustr::Ustr;
323
324 use super::*;
325 use crate::{
326 backend::{AppendEntry, EventStore, IndexKey, IndexKind, MemoryBackend, ScanDirection},
327 capture::encoder::EncodedPayload,
328 entry::EventStoreEntry,
329 error::EventStoreError,
330 manifest::{RegisteredComponents, RunManifest, RunStatus},
331 writer::WriterConfig,
332 };
333
334 #[derive(Debug)]
335 struct StubCommand {
336 client_order_id: String,
337 }
338
339 #[derive(Debug)]
340 struct StubEvent {
341 client_order_id: String,
342 venue_order_id: String,
343 }
344
345 #[derive(Debug)]
346 struct UnknownMessage;
347
348 #[derive(Debug)]
349 struct FailingMessage;
350
351 fn manifest(run_id: &str) -> RunManifest {
352 RunManifest {
353 run_id: run_id.to_string(),
354 parent_run_id: None,
355 instance_id: "trader-001".to_string(),
356 binary_hash: "deadbeef".to_string(),
357 schema_version: 1,
358 crate_versions: "feedface".to_string(),
359 feature_flags: Vec::new(),
360 adapter_versions: IndexMap::new(),
361 config_hash: "cafebabe".to_string(),
362 registered_components: RegisteredComponents::default(),
363 seed: None,
364 start_ts_init: UnixNanos::from(0),
365 end_ts_init: None,
366 high_watermark: 0,
367 status: RunStatus::Running,
368 }
369 }
370
371 fn stub_registry() -> Arc<EncoderRegistry> {
372 let mut registry = EncoderRegistry::new();
373 registry.register::<StubCommand, _>(Ustr::from("StubCommand"), |c| {
374 Ok(EncodedPayload::new(
375 Bytes::copy_from_slice(c.client_order_id.as_bytes()),
376 vec![IndexKey::new(
377 IndexKind::ClientOrderId,
378 c.client_order_id.clone(),
379 )],
380 ))
381 });
382 registry.register::<StubEvent, _>(Ustr::from("StubEvent"), |e| {
383 Ok(EncodedPayload::new(
384 Bytes::copy_from_slice(e.client_order_id.as_bytes()),
385 vec![
386 IndexKey::new(IndexKind::ClientOrderId, e.client_order_id.clone()),
387 IndexKey::new(IndexKind::VenueOrderId, e.venue_order_id.clone()),
388 ],
389 ))
390 });
391 registry.register::<FailingMessage, _>(Ustr::from("FailingMessage"), |_| {
392 Err(EncodeError::Serialize(
393 "encoder rejected message".to_string(),
394 ))
395 });
396 Arc::new(registry)
397 }
398
399 #[fixture]
400 fn captured_halt() -> (HaltCallback, Arc<Mutex<Vec<HaltReason>>>) {
401 let captured: Arc<Mutex<Vec<HaltReason>>> = Arc::new(Mutex::new(Vec::new()));
402 let captured_for_cb = Arc::clone(&captured);
403 let halt: HaltCallback = Arc::new(move |reason| {
404 captured_for_cb
405 .lock()
406 .expect("captured halt poisoned")
407 .push(reason);
408 });
409 (halt, captured)
410 }
411
412 fn writer_with_open_run(
413 run_id: &str,
414 halt: HaltCallback,
415 ) -> (Arc<EventStoreWriter>, Arc<Mutex<MemoryBackend>>) {
416 let backend_arc: Arc<Mutex<MemoryBackend>> = Arc::new(Mutex::new(MemoryBackend::new()));
417 backend_arc
418 .lock()
419 .expect("inner")
420 .open_run(manifest(run_id))
421 .expect("open run");
422
423 let wrapper = SharedMemory(Arc::clone(&backend_arc));
424 let writer = EventStoreWriter::spawn(
425 Box::new(wrapper),
426 get_atomic_clock_static(),
427 halt,
428 WriterConfig::default(),
429 )
430 .expect("spawn");
431 (Arc::new(writer), backend_arc)
432 }
433
434 #[derive(Debug)]
437 struct SharedMemory(Arc<Mutex<MemoryBackend>>);
438
439 impl EventStore for SharedMemory {
440 fn open_run(&mut self, _: RunManifest) -> Result<(), EventStoreError> {
441 unreachable!("test wrapper does not forward open_run")
442 }
443
444 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
445 self.0.lock().expect("shared").append_batch(entries)
446 }
447
448 fn scan_range(
449 &self,
450 from: u64,
451 to: u64,
452 direction: ScanDirection,
453 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
454 self.0
455 .lock()
456 .expect("shared")
457 .scan_range(from, to, direction)
458 }
459
460 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
461 self.0.lock().expect("shared").scan_seq(seq)
462 }
463
464 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
465 self.0.lock().expect("shared").lookup(kind, key)
466 }
467
468 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
469 self.0.lock().expect("shared").iter_index_keys(kind)
470 }
471
472 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
473 self.0.lock().expect("shared").seal(status)
474 }
475
476 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
477 self.0.lock().expect("shared").manifest()
478 }
479
480 fn high_watermark(&self) -> Result<u64, EventStoreError> {
481 self.0.lock().expect("shared").high_watermark()
482 }
483 }
484
485 fn drain(writer: &Arc<EventStoreWriter>, target_hwm: u64) {
486 let mut waited = Duration::ZERO;
487 let deadline = Duration::from_secs(2);
488 while writer.high_watermark() < target_hwm && waited < deadline {
489 std::thread::sleep(Duration::from_millis(5));
490 waited += Duration::from_millis(5);
491 }
492 assert!(
493 writer.high_watermark() >= target_hwm,
494 "writer high_watermark {} did not reach {target_hwm} within {:?}",
495 writer.high_watermark(),
496 deadline,
497 );
498 }
499
500 #[rstest]
501 fn capture_records_registered_command_and_returns_true(
502 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
503 ) {
504 let (halt, captured) = captured_halt;
505 let (writer, backend) = writer_with_open_run("run-cmd", Arc::clone(&halt));
506 let adapter = BusCaptureAdapter::new(Arc::clone(&writer), stub_registry(), halt);
507
508 let cmd = StubCommand {
509 client_order_id: "O-1".to_string(),
510 };
511 let captured_flag = adapter
512 .capture::<StubCommand>(
513 Topic::from("exec.command.SubmitOrder"),
514 &cmd,
515 Headers::empty(),
516 UnixNanos::from(100),
517 )
518 .expect("capture");
519
520 assert!(captured_flag);
521 drain(&writer, 1);
522
523 let backend = backend.lock().expect("backend");
524 let entry = backend.scan_seq(1).expect("scan").expect("present");
525 assert_eq!(entry.payload_type.as_str(), "StubCommand");
526 assert_eq!(entry.topic.as_ref(), "exec.command.SubmitOrder");
527 assert_eq!(entry.payload.as_ref(), b"O-1");
528
529 let seq = backend
530 .lookup(IndexKind::ClientOrderId, "O-1")
531 .expect("lookup")
532 .expect("indexed");
533 assert_eq!(seq, 1);
534
535 assert!(captured.lock().expect("captured").is_empty());
536 assert!(!adapter.is_halted());
537 }
538
539 #[rstest]
540 fn capture_returns_false_for_unknown_type(
541 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
542 ) {
543 let (halt, _captured) = captured_halt;
544 let (writer, _backend) = writer_with_open_run("run-unknown", Arc::clone(&halt));
545 let adapter = BusCaptureAdapter::new(Arc::clone(&writer), stub_registry(), halt);
546
547 let captured_flag = adapter
548 .capture::<UnknownMessage>(
549 Topic::from("data.market.unknown"),
550 &UnknownMessage,
551 Headers::empty(),
552 UnixNanos::from(50),
553 )
554 .expect("capture");
555
556 assert!(!captured_flag);
557 assert_eq!(writer.high_watermark(), 0);
558 assert!(!adapter.is_halted());
559 }
560
561 #[rstest]
562 fn submit_counter_increments_on_each_captured_entry(
563 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
564 ) {
565 let (halt, _captured) = captured_halt;
566 let (writer, _backend) = writer_with_open_run("run-submit-counter", Arc::clone(&halt));
567 let submit_counter = Arc::new(AtomicU64::new(1));
568 let adapter = BusCaptureAdapter::new(Arc::clone(&writer), stub_registry(), halt)
569 .with_submit_counter(Arc::clone(&submit_counter));
570
571 adapter
572 .capture::<StubCommand>(
573 Topic::from("exec.command.SubmitOrder"),
574 &StubCommand {
575 client_order_id: "O-counter-1".to_string(),
576 },
577 Headers::empty(),
578 UnixNanos::from(100),
579 )
580 .expect("first capture");
581 adapter
582 .capture::<UnknownMessage>(
583 Topic::from("data.market.unknown"),
584 &UnknownMessage,
585 Headers::empty(),
586 UnixNanos::from(101),
587 )
588 .expect("unknown type");
589 adapter
590 .capture::<StubEvent>(
591 Topic::from("exec.event.OrderFilled"),
592 &StubEvent {
593 client_order_id: "O-counter-1".to_string(),
594 venue_order_id: "V-counter-1".to_string(),
595 },
596 Headers::empty(),
597 UnixNanos::from(102),
598 )
599 .expect("second capture");
600
601 assert_eq!(submit_counter.load(Ordering::Acquire), 3);
602 }
603
604 #[rstest]
605 fn capture_records_event_indices_atomically(
606 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
607 ) {
608 let (halt, _captured) = captured_halt;
609 let (writer, backend) = writer_with_open_run("run-event", Arc::clone(&halt));
610 let adapter = BusCaptureAdapter::new(Arc::clone(&writer), stub_registry(), halt);
611
612 let event = StubEvent {
613 client_order_id: "O-2".to_string(),
614 venue_order_id: "V-9".to_string(),
615 };
616 adapter
617 .capture::<StubEvent>(
618 Topic::from("exec.event.OrderFilled"),
619 &event,
620 Headers::empty(),
621 UnixNanos::from(200),
622 )
623 .expect("capture");
624 drain(&writer, 1);
625
626 let backend = backend.lock().expect("backend");
627 let by_client = backend
628 .lookup(IndexKind::ClientOrderId, "O-2")
629 .expect("lookup")
630 .expect("indexed");
631 let by_venue = backend
632 .lookup(IndexKind::VenueOrderId, "V-9")
633 .expect("lookup")
634 .expect("indexed");
635 assert_eq!(by_client, 1);
636 assert_eq!(by_venue, 1);
637 }
638
639 #[rstest]
640 fn capture_propagates_encoder_error_without_halting(
641 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
642 ) {
643 let (halt, captured) = captured_halt;
647 let (writer, backend) = writer_with_open_run("run-encode-err", Arc::clone(&halt));
648 let adapter = BusCaptureAdapter::new(Arc::clone(&writer), stub_registry(), halt);
649
650 let err = adapter
651 .capture::<FailingMessage>(
652 Topic::from("exec.command.Failing"),
653 &FailingMessage,
654 Headers::empty(),
655 UnixNanos::from(500),
656 )
657 .expect_err("encoder must reject");
658
659 match err {
660 CaptureError::Encode(EncodeError::Serialize(msg)) => {
661 assert!(msg.contains("rejected"), "msg was: {msg}");
662 }
663 other => panic!("expected Encode(Serialize), was {other:?}"),
664 }
665 assert!(
666 !adapter.is_halted(),
667 "encoder failure must not fail-stop the adapter",
668 );
669 assert!(captured.lock().expect("captured").is_empty());
670
671 adapter
673 .capture::<StubCommand>(
674 Topic::from("exec.command.SubmitOrder"),
675 &StubCommand {
676 client_order_id: "O-after-encode-err".to_string(),
677 },
678 Headers::empty(),
679 UnixNanos::from(501),
680 )
681 .expect("capture after encoder error");
682 drain(&writer, 1);
683 let backend = backend.lock().expect("backend");
684 assert_eq!(backend.high_watermark().expect("hwm"), 1);
685 }
686
687 #[rstest]
688 #[case::backpressure(
689 SubmitError::HaltSignaled {
690 stalled_for: Duration::from_millis(750),
691 threshold: Duration::from_millis(250),
692 },
693 HaltReason::BackpressureStall {
694 stalled_for: Duration::from_millis(750),
695 threshold: Duration::from_millis(250),
696 },
697 )]
698 #[case::closed(
699 SubmitError::Closed,
700 HaltReason::BackendError("event store writer closed".to_string()),
701 )]
702 fn halt_reason_from_submit_preserves_failure_context(
703 #[case] err: SubmitError,
704 #[case] expected: HaltReason,
705 ) {
706 let actual = halt_reason_from_submit(&err);
707
708 match (actual, expected) {
709 (
710 HaltReason::BackpressureStall {
711 stalled_for: a_s,
712 threshold: a_t,
713 },
714 HaltReason::BackpressureStall {
715 stalled_for: e_s,
716 threshold: e_t,
717 },
718 ) => {
719 assert_eq!(a_s, e_s);
720 assert_eq!(a_t, e_t);
721 }
722 (HaltReason::BackendError(a), HaltReason::BackendError(e)) => {
723 assert_eq!(a, e);
724 }
725 (actual, expected) => {
726 panic!("variant mismatch: actual={actual:?} expected={expected:?}")
727 }
728 }
729 }
730
731 #[rstest]
732 fn submit_failure_halts_adapter_and_fires_callback_once(
733 captured_halt: (HaltCallback, Arc<Mutex<Vec<HaltReason>>>),
734 ) {
735 let (halt, captured) = captured_halt;
739 let (writer, _backend) = writer_with_open_run("run-halt", Arc::clone(&halt));
740
741 let writer_clone = Arc::clone(&writer);
743 let adapter = BusCaptureAdapter::new(writer_clone, stub_registry(), halt);
746
747 drop(writer);
752
753 let halt_for_stub: HaltCallback = adapter_halt_for(&captured);
756 let stub_adapter = StubFailAdapter::new(halt_for_stub);
757
758 let err = stub_adapter
759 .capture::<StubCommand>(
760 Topic::from("exec.command.SubmitOrder"),
761 &StubCommand {
762 client_order_id: "O-fail".to_string(),
763 },
764 Headers::empty(),
765 UnixNanos::from(1),
766 )
767 .expect_err("first submit fails");
768 assert!(matches!(err, CaptureError::Submit(SubmitError::Closed)));
769 assert!(stub_adapter.is_halted());
770 assert_eq!(captured.lock().expect("captured").len(), 1);
771
772 let err2 = stub_adapter
773 .capture::<StubCommand>(
774 Topic::from("exec.command.SubmitOrder"),
775 &StubCommand {
776 client_order_id: "O-fail-2".to_string(),
777 },
778 Headers::empty(),
779 UnixNanos::from(2),
780 )
781 .expect_err("second submit short-circuits");
782 assert!(matches!(err2, CaptureError::Halted));
783 assert_eq!(
784 captured.lock().expect("captured").len(),
785 1,
786 "halt callback must not refire after the first failure",
787 );
788
789 drop(adapter);
791 }
792
793 fn adapter_halt_for(captured: &Arc<Mutex<Vec<HaltReason>>>) -> HaltCallback {
794 let captured_for_cb = Arc::clone(captured);
795 Arc::new(move |reason| {
796 captured_for_cb
797 .lock()
798 .expect("captured halt poisoned")
799 .push(reason);
800 })
801 }
802
803 struct StubFailAdapter {
807 registry: Arc<EncoderRegistry>,
808 halt: HaltCallback,
809 halted: AtomicBool,
810 }
811
812 impl StubFailAdapter {
813 fn new(halt: HaltCallback) -> Self {
814 Self {
815 registry: stub_registry(),
816 halt,
817 halted: AtomicBool::new(false),
818 }
819 }
820
821 fn is_halted(&self) -> bool {
822 self.halted.load(Ordering::Acquire)
823 }
824
825 fn capture<T: 'static>(
826 &self,
827 _topic: Topic,
828 message: &T,
829 _headers: Headers,
830 _ts_init: UnixNanos,
831 ) -> Result<bool, CaptureError> {
832 if self.halted.load(Ordering::Acquire) {
833 return Err(CaptureError::Halted);
834 }
835 let Some((_pt, _encoded)) = self.registry.encode(message)? else {
836 return Ok(false);
837 };
838 let err = SubmitError::Closed;
839
840 if self
841 .halted
842 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
843 .is_ok()
844 {
845 (self.halt)(super::halt_reason_from_submit(&err));
846 }
847 Err(CaptureError::Submit(err))
848 }
849 }
850}