1use std::{fmt::Debug, future::Future, pin::Pin, time::Duration};
81
82use indexmap::IndexSet;
83use nautilus_common::{
84 actor::{Actor, DataActor, DataActorNative},
85 cache::database::CacheDatabaseAdapter,
86 component::Component,
87 enums::{Environment, LogColor},
88 live::dst,
89 log_info,
90 messages::{
91 DataEvent, ExecutionEvent, ExecutionReport,
92 data::DataCommand,
93 execution::{GenerateOrderStatusReports, GeneratePositionStatusReports, TradingCommand},
94 },
95 msgbus::{self, BusMessage},
96 timer::TimeEventHandler,
97};
98use nautilus_core::{
99 UUID4, UnixNanos,
100 datetime::{NANOSECONDS_IN_MILLISECOND, mins_to_secs, secs_to_nanos_unchecked},
101};
102use nautilus_model::{
103 events::OrderEventAny,
104 identifiers::{ClientOrderId, TraderId, Venue},
105 orders::Order,
106 reports::{OrderStatusReport, PositionStatusReport},
107};
108use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
109use nautilus_trading::{
110 ExecutionAlgorithm, ExecutionAlgorithmNative,
111 strategy::{Strategy, StrategyNative},
112};
113use tabled::{builder::Builder, settings::Style};
114
115use crate::{
116 execution::{
117 client::LiveExecutionClient,
118 manager::{
119 ExecutionManager, ExecutionManagerConfig, OpenOrderReportCheck, PositionReportCheck,
120 },
121 },
122 runner::{AsyncRunner, AsyncRunnerChannels},
123};
124
125pub mod builder;
126pub mod config;
127#[cfg(feature = "plugin")]
128pub mod plugin;
129mod state;
130
131use builder::ExternalMessageBusIngress;
132pub use builder::LiveNodeBuilder;
133use config::{LiveNodeConfig, PluginConfig};
134use state::EngineConnectionStatus;
135pub use state::{LiveNodeHandle, NodeState};
136
137#[derive(Debug)]
142#[cfg_attr(
143 feature = "python",
144 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.live", unsendable)
145)]
146#[cfg_attr(
147 feature = "python",
148 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
149)]
150pub struct LiveNode {
151 kernel: NautilusKernel,
152 runner: Option<AsyncRunner>,
153 config: LiveNodeConfig,
154 handle: LiveNodeHandle,
155 exec_manager: ExecutionManager,
156 exec_clients: Vec<LiveExecutionClient>,
157 external_msgbus: Option<ExternalMessageBusIngress>,
158 shutdown_deadline: Option<dst::time::Instant>,
159 #[cfg(feature = "plugin")]
160 plugins: plugin::NodePlugins,
161 #[cfg(feature = "python")]
162 #[allow(dead_code)] python_actors: Vec<pyo3::Py<pyo3::PyAny>>,
164}
165
166impl LiveNode {
167 #[must_use]
171 pub(crate) fn new_from_builder(
172 kernel: NautilusKernel,
173 runner: AsyncRunner,
174 config: LiveNodeConfig,
175 exec_manager: ExecutionManager,
176 exec_clients: Vec<LiveExecutionClient>,
177 external_msgbus: Option<ExternalMessageBusIngress>,
178 ) -> Self {
179 Self {
180 kernel,
181 runner: Some(runner),
182 config,
183 handle: LiveNodeHandle::new(),
184 exec_manager,
185 exec_clients,
186 external_msgbus,
187 shutdown_deadline: None,
188 #[cfg(feature = "plugin")]
189 plugins: plugin::NodePlugins,
190 #[cfg(feature = "python")]
191 python_actors: Vec::new(),
192 }
193 }
194
195 pub fn builder(
201 trader_id: TraderId,
202 environment: Environment,
203 ) -> anyhow::Result<LiveNodeBuilder> {
204 LiveNodeBuilder::new(trader_id, environment)
205 }
206
207 pub fn build(name: String, config: Option<LiveNodeConfig>) -> anyhow::Result<Self> {
217 let mut config = config.unwrap_or_default();
218 config.environment = Environment::Live;
219
220 match config.environment() {
221 Environment::Sandbox | Environment::Live => {}
222 Environment::Backtest => {
223 anyhow::bail!("LiveNode cannot be used with Backtest environment");
224 }
225 }
226
227 config.validate_runtime_support()?;
228
229 if config.event_store.is_some() {
230 anyhow::bail!(
231 "LiveNodeConfig.event_store is set but LiveNode::build cannot install a factory; \
232 use LiveNodeBuilder::with_event_store(...) instead"
233 );
234 }
235
236 let runner = AsyncRunner::new();
237 runner.bind_senders();
238
239 let kernel = NautilusKernel::new(name, config.clone())?;
240
241 let exec_manager_config =
242 ExecutionManagerConfig::from(&config.exec_engine).with_trader_id(config.trader_id);
243 let exec_manager = ExecutionManager::new(
244 kernel.clock.clone(),
245 kernel.cache.clone(),
246 exec_manager_config,
247 );
248
249 let node = Self {
250 kernel,
251 runner: Some(runner),
252 config,
253 handle: LiveNodeHandle::new(),
254 exec_manager,
255 exec_clients: Vec::new(),
256 external_msgbus: None,
257 shutdown_deadline: None,
258 #[cfg(feature = "plugin")]
259 plugins: plugin::NodePlugins,
260 #[cfg(feature = "python")]
261 python_actors: Vec::new(),
262 };
263 node.load_configured_plugins()?;
264
265 log::info!("LiveNode built successfully with kernel config");
266
267 Ok(node)
268 }
269
270 #[cfg(feature = "plugin")]
276 pub(crate) fn load_configured_plugins(&self) -> anyhow::Result<()> {
277 if self.config.plugins.is_empty() {
278 return Ok(());
279 }
280
281 anyhow::bail!(
282 "LiveNodeConfig.plugins requires host-side plug-in support; nautilus-plugin is the guest SDK only"
283 )
284 }
285
286 #[cfg(not(feature = "plugin"))]
292 pub(crate) fn load_configured_plugins(&self) -> anyhow::Result<()> {
293 if self.config.plugins.is_empty() {
294 return Ok(());
295 }
296
297 anyhow::bail!(
298 "LiveNodeConfig.plugins requires host-side plug-in support; nautilus-plugin is the guest SDK only"
299 )
300 }
301
302 #[cfg(feature = "plugin")]
308 #[expect(
309 clippy::needless_pass_by_value,
310 reason = "signature mirrors the host-enabled API"
311 )]
312 pub fn add_plugin(&mut self, config: PluginConfig) -> anyhow::Result<()> {
313 config.validate_runtime_support(self.config.plugins.len())?;
314
315 anyhow::bail!(
316 "LiveNode::add_plugin requires host-side plug-in support; nautilus-plugin is the guest SDK only"
317 )
318 }
319
320 #[cfg(not(feature = "plugin"))]
326 #[expect(
327 clippy::needless_pass_by_value,
328 reason = "signature mirrors the plugin-enabled API"
329 )]
330 pub fn add_plugin(&mut self, config: PluginConfig) -> anyhow::Result<()> {
331 let _ = config;
332 anyhow::bail!(
333 "LiveNode::add_plugin requires host-side plug-in support; nautilus-plugin is the guest SDK only"
334 )
335 }
336
337 #[must_use]
339 pub fn handle(&self) -> LiveNodeHandle {
340 self.handle.clone()
341 }
342
343 pub async fn start(&mut self) -> anyhow::Result<()> {
355 if self.state().is_running() {
356 anyhow::bail!("Already running");
357 }
358
359 if let Some(runner) = self.runner.as_ref() {
360 runner.bind_senders();
361 }
362
363 self.handle.set_state(NodeState::Starting);
364
365 self.kernel.reset_shutdown_flag();
366 self.kernel.start_async().await;
367
368 if self.kernel.is_event_store_replay() {
369 log::info!(
370 "Event-store replay loaded; skipping live client connection and reconciliation",
371 );
372 self.handle.set_state(NodeState::Running);
373 return Ok(());
374 }
375
376 if self.kernel.is_event_store_replay_configured() {
377 self.abort_startup("Event-store replay did not start")
378 .await?;
379 return Ok(());
380 }
381
382 self.kernel.connect_data_clients().await;
384
385 if let Some(runner) = self.runner.as_mut() {
386 runner.flush_pending_data();
387 }
388
389 self.kernel.connect_exec_clients().await;
390
391 if let Some(reason) = self.startup_abort_reason() {
392 self.abort_startup(reason).await?;
393 return Ok(());
394 }
395
396 match self.await_engines_connected().await {
397 EngineConnectionStatus::Connected => {}
398 EngineConnectionStatus::TimedOut => {
399 log::error!("Cannot start trader: engine client(s) not connected");
400 self.handle.set_state(NodeState::Running);
401 return Ok(());
402 }
403 EngineConnectionStatus::StopRequested => {
404 self.abort_startup("Stop signal received during startup")
405 .await?;
406 return Ok(());
407 }
408 EngineConnectionStatus::ShutdownRequested => {
409 self.abort_startup("Shutdown signal received during startup")
410 .await?;
411 return Ok(());
412 }
413 }
414
415 self.perform_startup_reconciliation().await?;
416
417 self.kernel.start_trader();
418 #[cfg(feature = "plugin")]
419 if let Err(e) = self.plugins.start_controllers() {
420 return self.abort_after_trader_start_failure(e).await;
421 }
422
423 self.handle.set_state(NodeState::Running);
424
425 Ok(())
426 }
427
428 pub async fn stop(&mut self) -> anyhow::Result<()> {
437 if !self.state().is_running() {
438 anyhow::bail!("Not running");
439 }
440
441 self.handle.set_state(NodeState::ShuttingDown);
442
443 #[cfg(feature = "plugin")]
444 let controller_stop_result = self.plugins.stop_controllers();
445 #[cfg(not(feature = "plugin"))]
446 let controller_stop_result: anyhow::Result<()> = Ok(());
447
448 self.kernel.stop_trader();
449 let delay = self.kernel.delay_post_stop();
450 log::info!("Awaiting residual events ({delay:?})...");
451
452 dst::time::sleep(delay).await;
453 let stop_result = self.finalize_stop().await;
454 match (controller_stop_result, stop_result) {
455 (Ok(()), Ok(())) => Ok(()),
456 (Err(controller_err), Ok(())) => Err(controller_err),
457 (Ok(()), Err(stop_err)) => Err(stop_err),
458 (Err(controller_err), Err(stop_err)) => {
459 log::error!("Error stopping plug-in controllers: {controller_err}");
460 Err(stop_err)
461 }
462 }
463 }
464
465 async fn await_engines_connected(&self) -> EngineConnectionStatus {
469 log::info!(
470 "Awaiting engine connections ({:?} timeout)...",
471 self.config.timeout_connection
472 );
473
474 let start = dst::time::Instant::now();
475 let timeout = self.config.timeout_connection;
476 let interval = Duration::from_millis(100);
477
478 while start.elapsed() < timeout {
479 if self.handle.should_stop() {
480 log::warn!("Stop signal received, aborting connection wait");
481 return EngineConnectionStatus::StopRequested;
482 }
483
484 if self.kernel.is_shutdown_requested() {
485 log::warn!("Shutdown signal received, aborting connection wait");
486 return EngineConnectionStatus::ShutdownRequested;
487 }
488
489 if self.kernel.check_engines_connected() {
490 log::info!("All engine clients connected");
491 return EngineConnectionStatus::Connected;
492 }
493 dst::time::sleep(interval).await;
494 }
495
496 self.log_connection_status();
497 EngineConnectionStatus::TimedOut
498 }
499
500 async fn await_engines_disconnected(&self) {
504 log::info!(
505 "Awaiting engine disconnections ({:?} timeout)...",
506 self.config.timeout_disconnection
507 );
508
509 let start = dst::time::Instant::now();
510 let timeout = self.config.timeout_disconnection;
511 let interval = Duration::from_millis(100);
512
513 while start.elapsed() < timeout {
514 if self.kernel.check_engines_disconnected() {
515 log::info!("All engine clients disconnected");
516 return;
517 }
518 dst::time::sleep(interval).await;
519 }
520
521 log::error!(
522 "Timed out ({:?}) waiting for engines to disconnect\n\
523 DataEngine.check_disconnected() == {}\n\
524 ExecEngine.check_disconnected() == {}",
525 timeout,
526 self.kernel.data_engine().check_disconnected(),
527 self.kernel.exec_engine().borrow().check_disconnected(),
528 );
529 }
530
531 fn log_connection_status(&self) {
532 let data_status = self.kernel.data_client_connection_status();
533 let exec_status = self.kernel.exec_client_connection_status();
534
535 let mut rows: Vec<ClientStatus> = Vec::new();
536
537 for (client_id, connected) in data_status {
538 rows.push(ClientStatus {
539 client: client_id.to_string(),
540 client_type: "Data",
541 connected,
542 });
543 }
544
545 for (client_id, connected) in exec_status {
546 rows.push(ClientStatus {
547 client: client_id.to_string(),
548 client_type: "Execution",
549 connected,
550 });
551 }
552
553 let table = render_client_statuses(rows);
554
555 log::warn!(
556 "Timed out ({:?}) waiting for engines to connect\n\n{table}\n\n\
557 DataEngine.check_connected() == {}\n\
558 ExecEngine.check_connected() == {}",
559 self.config.timeout_connection,
560 self.kernel.data_engine().check_connected(),
561 self.kernel.exec_engine().borrow().check_connected(),
562 );
563 }
564
565 #[expect(clippy::await_holding_refcell_ref)] async fn perform_startup_reconciliation(&mut self) -> anyhow::Result<()> {
575 if !self.config.exec_engine.reconciliation {
576 log::info!("Startup reconciliation disabled");
577 return Ok(());
578 }
579
580 log_info!(
581 "Starting execution state reconciliation...",
582 color = LogColor::Blue
583 );
584
585 let lookback_mins = self
586 .config
587 .exec_engine
588 .reconciliation_lookback_mins
589 .map(u64::from);
590
591 let timeout = self.config.timeout_reconciliation;
592 let start = dst::time::Instant::now();
593 let client_ids = self.kernel.exec_engine.borrow().client_ids();
594
595 for client_id in client_ids {
596 if start.elapsed() > timeout {
597 log::warn!("Reconciliation timeout reached, stopping early");
598 break;
599 }
600
601 log_info!(
602 "Requesting mass status from {}...",
603 client_id,
604 color = LogColor::Blue
605 );
606
607 let mass_status_result = self
608 .kernel
609 .exec_engine
610 .borrow_mut()
611 .generate_mass_status(&client_id, lookback_mins)
612 .await;
613
614 match mass_status_result {
615 Ok(Some(mass_status)) => {
616 log_info!(
617 "Reconciling ExecutionMassStatus for {}",
618 client_id,
619 color = LogColor::Blue
620 );
621
622 let exec_engine_rc = self.kernel.exec_engine.clone();
623
624 let result = self
625 .exec_manager
626 .reconcile_execution_mass_status(mass_status, exec_engine_rc)
627 .await;
628
629 if result.events.is_empty() {
630 log_info!(
631 "Reconciliation for {} succeeded",
632 client_id,
633 color = LogColor::Blue
634 );
635 } else {
636 log::info!(
637 color = LogColor::Blue as u8;
638 "Reconciliation for {} processed {} events",
639 client_id,
640 result.events.len()
641 );
642 }
643
644 if !result.external_orders.is_empty() {
646 let exec_engine = self.kernel.exec_engine.borrow();
647 for external in result.external_orders {
648 exec_engine.register_external_order(
649 external.client_order_id,
650 external.venue_order_id,
651 external.instrument_id,
652 external.strategy_id,
653 external.ts_init,
654 );
655 }
656 }
657 }
658 Ok(None) => {
659 log::warn!(
660 "No mass status available from {client_id} \
661 (likely adapter error when generating reports)"
662 );
663 }
664 Err(e) => {
665 log::warn!("Failed to get mass status from {client_id}: {e}");
666 }
667 }
668 }
669
670 self.kernel.portfolio.borrow_mut().initialize_orders();
671 self.kernel.portfolio.borrow_mut().initialize_positions();
672
673 let elapsed_secs = start.elapsed().as_secs_f64();
674 log_info!(
675 "Startup reconciliation completed in {:.2}s",
676 elapsed_secs,
677 color = LogColor::Blue
678 );
679
680 Ok(())
681 }
682
683 pub async fn run(&mut self) -> anyhow::Result<()> {
705 if self.state().is_running() {
706 anyhow::bail!("Already running");
707 }
708
709 let Some(runner) = self.runner.take() else {
710 anyhow::bail!("Runner already consumed - run() called twice");
711 };
712 runner.bind_senders();
713
714 let AsyncRunnerChannels {
715 mut time_evt_rx,
716 mut exec_evt_rx,
717 mut exec_cmd_rx,
718 mut data_evt_rx,
719 mut data_cmd_rx,
720 } = runner.take_channels();
721
722 log::info!("Event loop starting");
723
724 self.handle.set_state(NodeState::Starting);
725 self.kernel.reset_shutdown_flag();
726 self.kernel.start_async().await;
727
728 if self.kernel.is_event_store_replay() {
729 log::info!(
730 "Event-store replay loaded; skipping live client connection and reconciliation",
731 );
732 self.handle.set_state(NodeState::Running);
733 return Ok(());
734 }
735
736 if self.kernel.is_event_store_replay_configured() {
737 self.abort_startup("Event-store replay did not start")
738 .await?;
739 return Ok(());
740 }
741
742 let mut external_msgbus_rx = match self.take_external_ingress_receiver() {
743 Ok(rx) => rx,
744 Err(e) => {
745 let result = self
746 .abort_startup("External message bus ingress failed to start")
747 .await;
748 Self::drain_channels(
749 &mut time_evt_rx,
750 &mut data_evt_rx,
751 &mut data_cmd_rx,
752 &mut exec_evt_rx,
753 &mut exec_cmd_rx,
754 );
755 log::info!("Event loop stopped");
756
757 if let Err(finalize_err) = result {
758 anyhow::bail!(
759 "failed to start external message bus ingress: {e}; failed to finalize startup abort: {finalize_err}"
760 );
761 }
762
763 return Err(e);
764 }
765 };
766
767 let stop_handle = self.handle.clone();
768 let mut pending = PendingEvents::default();
769
770 drive_with_event_buffering(
775 self.kernel.connect_data_clients(),
776 &mut pending,
777 &mut time_evt_rx,
778 &mut data_evt_rx,
779 &mut data_cmd_rx,
780 &mut exec_evt_rx,
781 &mut exec_cmd_rx,
782 )
783 .await;
784
785 flush_pending_data(&mut pending, &mut data_evt_rx, &mut data_cmd_rx);
789 debug_assert!(
790 pending.data_evts.is_empty() && pending.data_cmds.is_empty(),
791 "data must be drained into cache before exec clients connect",
792 );
793
794 let engine_connection_status = drive_with_event_buffering(
796 self.connect_exec_phase(),
797 &mut pending,
798 &mut time_evt_rx,
799 &mut data_evt_rx,
800 &mut data_cmd_rx,
801 &mut exec_evt_rx,
802 &mut exec_cmd_rx,
803 )
804 .await?;
805
806 flush_all_pending(
808 &mut pending,
809 &mut time_evt_rx,
810 &mut data_evt_rx,
811 &mut data_cmd_rx,
812 &mut exec_evt_rx,
813 &mut exec_cmd_rx,
814 );
815 debug_assert!(
816 pending.is_empty(),
817 "all startup events must be processed before reconciliation",
818 );
819
820 if let Some(reason) = engine_connection_status
821 .abort_reason()
822 .or_else(|| self.startup_abort_reason())
823 {
824 self.abort_startup(reason).await?;
825 Self::drain_channels(
826 &mut time_evt_rx,
827 &mut data_evt_rx,
828 &mut data_cmd_rx,
829 &mut exec_evt_rx,
830 &mut exec_cmd_rx,
831 );
832 log::info!("Event loop stopped");
833 return Ok(());
834 }
835
836 if engine_connection_status == EngineConnectionStatus::Connected {
837 self.perform_startup_reconciliation().await?;
839 self.kernel.start_trader();
840 #[cfg(feature = "plugin")]
841 if let Err(e) = self.plugins.start_controllers() {
842 let result = self.abort_after_trader_start_failure(e).await;
843 Self::drain_channels(
844 &mut time_evt_rx,
845 &mut data_evt_rx,
846 &mut data_cmd_rx,
847 &mut exec_evt_rx,
848 &mut exec_cmd_rx,
849 );
850 log::info!("Event loop stopped");
851 return result;
852 }
853 } else {
854 log::error!("Not starting trader: engine client(s) not connected");
855 }
856
857 self.handle.set_state(NodeState::Running);
858
859 let exec_config = &self.config.exec_engine;
860 let inflight_interval_ns =
861 u64::from(exec_config.inflight_check_interval_ms) * NANOSECONDS_IN_MILLISECOND;
862 let open_interval_ns = exec_config
863 .open_check_interval_secs
864 .filter(|&s| s > 0.0)
865 .map_or(0, secs_to_nanos_unchecked);
866 let position_interval_ns = exec_config
867 .position_check_interval_secs
868 .filter(|&s| s > 0.0)
869 .map_or(0, secs_to_nanos_unchecked);
870 let has_clients = !self
871 .kernel
872 .exec_engine
873 .borrow()
874 .get_all_clients()
875 .is_empty();
876 let recon_enabled = has_clients
877 && (inflight_interval_ns > 0 || open_interval_ns > 0 || position_interval_ns > 0);
878
879 let recon_min_interval = if recon_enabled {
880 let mut intervals = Vec::new();
881
882 if exec_config.inflight_check_interval_ms > 0 {
883 intervals.push(Duration::from_millis(u64::from(
884 exec_config.inflight_check_interval_ms,
885 )));
886 }
887
888 if let Some(s) = exec_config.open_check_interval_secs.filter(|&s| s > 0.0) {
889 intervals.push(Duration::from_secs_f64(s));
890 }
891
892 if let Some(s) = exec_config
893 .position_check_interval_secs
894 .filter(|&s| s > 0.0)
895 {
896 intervals.push(Duration::from_secs_f64(s));
897 }
898
899 intervals
900 .into_iter()
901 .min()
902 .unwrap_or(Duration::from_secs(1))
903 } else {
904 Duration::from_secs(1) };
906
907 let startup_delay = if self.config.exec_engine.reconciliation {
912 Duration::from_secs_f64(exec_config.reconciliation_startup_delay_secs)
913 } else {
914 Duration::ZERO
915 };
916
917 let recon_start = dst::time::Instant::now() + startup_delay;
918
919 let mut ts_last_inflight = self.exec_manager.generate_timestamp_ns();
920 let mut ts_last_open = ts_last_inflight;
921 let mut ts_last_position = ts_last_inflight;
922
923 let far_future = Duration::from_hours(24 * 365 * 100);
926
927 let make_schedule = |opt_dur: Option<Duration>| -> (Duration, dst::time::Instant) {
928 let dur = opt_dur.unwrap_or(far_future);
929 (dur, recon_start + dur)
930 };
931
932 let (recon_interval, mut recon_next) = make_schedule(if recon_enabled {
933 Some(recon_min_interval)
934 } else {
935 None
936 });
937
938 let (purge_orders_interval, mut purge_orders_next) = make_schedule(
939 exec_config
940 .purge_closed_orders_interval_mins
941 .filter(|&m| m > 0)
942 .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
943 );
944
945 let (purge_positions_interval, mut purge_positions_next) = make_schedule(
946 exec_config
947 .purge_closed_positions_interval_mins
948 .filter(|&m| m > 0)
949 .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
950 );
951
952 let (purge_account_interval, mut purge_account_next) = make_schedule(
953 exec_config
954 .purge_account_events_interval_mins
955 .filter(|&m| m > 0)
956 .map(|m| Duration::from_secs(mins_to_secs(u64::from(m)))),
957 );
958
959 let (own_books_interval, mut own_books_next) = make_schedule(
960 exec_config
961 .own_books_audit_interval_secs
962 .filter(|&s| s > 0.0)
963 .map(Duration::from_secs_f64),
964 );
965
966 let (prune_fills_interval, mut prune_fills_next) =
967 make_schedule(Some(Duration::from_mins(1)));
968
969 let mut maintenance_timer = dst::time::interval(Duration::from_millis(100));
970 maintenance_timer.set_missed_tick_behavior(dst::time::MissedTickBehavior::Skip);
971
972 let mut stop_check_timer = dst::time::interval(Duration::from_millis(100));
977 stop_check_timer.set_missed_tick_behavior(dst::time::MissedTickBehavior::Skip);
978
979 let mut residual_events = 0usize;
981 let mut open_order_report_task: Option<OpenOrderReportTask> = None;
982 let mut position_report_task: Option<PositionReportTask> = None;
983 let ctrl_c = dst::signal::ctrl_c();
984 let terminate = dst::signal::terminate();
985 tokio::pin!(ctrl_c);
986 tokio::pin!(terminate);
987
988 loop {
989 let shutdown_deadline = self.shutdown_deadline;
990 let is_shutting_down = self.state() == NodeState::ShuttingDown;
991 let is_running = self.state() == NodeState::Running;
992
993 tokio::select! {
994 biased;
995
996 result = &mut ctrl_c, if is_running => {
998 match result {
999 Ok(()) => log::info!("Received SIGINT, shutting down"),
1000 Err(e) => log::error!("Failed to listen for SIGINT: {e}"),
1001 }
1002 self.initiate_shutdown();
1003 }
1004 result = &mut terminate, if is_running => {
1005 match result {
1006 Ok(()) => log::info!("Received SIGTERM, shutting down"),
1007 Err(e) => log::error!("Failed to listen for SIGTERM: {e}"),
1008 }
1009 self.initiate_shutdown();
1010 }
1011 _ = stop_check_timer.tick(), if is_running => {
1012 if stop_handle.should_stop() {
1013 log::info!("Received stop signal from handle");
1014 self.initiate_shutdown();
1015 } else if self.kernel.is_shutdown_requested() {
1016 log::info!("Received ShutdownSystem command, shutting down");
1017 self.initiate_shutdown();
1018 }
1019 }
1020 () = async {
1021 match shutdown_deadline {
1022 Some(deadline) => dst::time::sleep_until(deadline).await,
1023 None => std::future::pending::<()>().await,
1024 }
1025 }, if self.state() == NodeState::ShuttingDown => {
1026 break;
1027 }
1028 result = async {
1029 match open_order_report_task.as_mut() {
1030 Some(task) => task.future.as_mut().await,
1031 None => std::future::pending::<OpenOrderReportResult>().await,
1032 }
1033 }, if open_order_report_task.is_some() => {
1034 open_order_report_task = None;
1035 let events = self
1036 .exec_manager
1037 .reconcile_open_order_reports(&result.check, result.reports);
1038 self.process_reconciliation_events(&events);
1039 }
1040 result = async {
1041 match position_report_task.as_mut() {
1042 Some(task) => task.future.as_mut().await,
1043 None => std::future::pending::<PositionReportResult>().await,
1044 }
1045 }, if position_report_task.is_some() => {
1046 position_report_task = None;
1047 let events = self.exec_manager.reconcile_position_reports(
1048 &result.check,
1049 result.reports,
1050 &result.failed_venues,
1051 );
1052 self.process_reconciliation_events(&events);
1053 }
1054
1055 _ = maintenance_timer.tick(), if is_running => {
1058 let mut now = dst::time::Instant::now();
1059
1060 if recon_enabled && now >= recon_next {
1061 let recon_intervals = ReconciliationCheckIntervals {
1062 inflight: inflight_interval_ns,
1063 open: open_interval_ns,
1064 position: position_interval_ns,
1065 };
1066 let mut recon_state = ReconciliationCheckState {
1067 ts_last_inflight: &mut ts_last_inflight,
1068 ts_last_open: &mut ts_last_open,
1069 ts_last_position: &mut ts_last_position,
1070 open_order_report_task: &mut open_order_report_task,
1071 position_report_task: &mut position_report_task,
1072 };
1073
1074 self.run_reconciliation_checks(
1075 recon_intervals,
1076 &mut recon_state,
1077 );
1078
1079 now = dst::time::Instant::now();
1080 recon_next = now + recon_interval;
1081 }
1082
1083 if now >= purge_orders_next {
1084 self.exec_manager.purge_closed_orders();
1085 purge_orders_next = now + purge_orders_interval;
1086 }
1087
1088 if now >= purge_positions_next {
1089 self.exec_manager.purge_closed_positions();
1090 purge_positions_next = now + purge_positions_interval;
1091 }
1092
1093 if now >= purge_account_next {
1094 self.exec_manager.purge_account_events();
1095 purge_account_next = now + purge_account_interval;
1096 }
1097
1098 if now >= own_books_next {
1099 self.kernel.cache().borrow_mut().audit_own_order_books();
1100 own_books_next = now + own_books_interval;
1101 }
1102
1103 if now >= prune_fills_next {
1104 self.exec_manager.prune_recent_fills_cache(60.0);
1105 prune_fills_next = now + prune_fills_interval;
1106 }
1107 }
1108
1109 Some(handler) = time_evt_rx.recv() => {
1114 AsyncRunner::handle_time_event(handler);
1115
1116 if is_shutting_down {
1117 log::debug!("Residual time event");
1118 residual_events += 1;
1119 }
1120 }
1121 Some(evt) = exec_evt_rx.recv() => {
1122 if is_shutting_down {
1123 log::debug!("Residual exec event: {evt:?}");
1124 residual_events += 1;
1125 }
1126
1127 let mut close_ids: Vec<ClientOrderId> = Vec::new();
1128
1129 match &evt {
1130 ExecutionEvent::Order(order_evt) => {
1131 self.exec_manager.record_local_activity(order_evt.client_order_id());
1132 match order_evt {
1133 OrderEventAny::Filled(fill) => {
1134 self.exec_manager.record_position_activity(
1135 fill.instrument_id,
1136 fill.account_id,
1137 fill.ts_event,
1138 );
1139 self.exec_manager.mark_fill_processed(fill.trade_id);
1140 }
1141 OrderEventAny::Accepted(_)
1142 | OrderEventAny::Rejected(_)
1143 | OrderEventAny::Canceled(_)
1144 | OrderEventAny::Expired(_)
1145 | OrderEventAny::Denied(_)
1146 | OrderEventAny::Updated(_)
1147 | OrderEventAny::ModifyRejected(_)
1148 | OrderEventAny::CancelRejected(_) => {
1149 self.exec_manager.clear_recon_tracking(
1150 &order_evt.client_order_id(), true,
1151 );
1152 }
1153 _ => {}
1154 }
1155 close_ids.push(order_evt.client_order_id());
1156 }
1157 ExecutionEvent::OrderSubmittedBatch(batch) => {
1158 for submitted in &batch.events {
1159 self.exec_manager.record_local_activity(submitted.client_order_id);
1160 }
1161 }
1162 ExecutionEvent::OrderAcceptedBatch(batch) => {
1163 for accepted in &batch.events {
1164 self.exec_manager.record_local_activity(accepted.client_order_id);
1165 self.exec_manager.clear_recon_tracking(
1166 &accepted.client_order_id, true,
1167 );
1168 }
1169 }
1170 ExecutionEvent::OrderCanceledBatch(batch) => {
1171 for canceled in &batch.events {
1172 self.exec_manager.record_local_activity(canceled.client_order_id);
1173 self.exec_manager.clear_recon_tracking(
1174 &canceled.client_order_id, true,
1175 );
1176 close_ids.push(canceled.client_order_id);
1177 }
1178 }
1179 ExecutionEvent::Report(report) => {
1180 if let ExecutionReport::Fill(fill_report) = report
1181 && self.exec_manager.is_fill_recently_processed(&fill_report.trade_id) {
1182 log::debug!(
1183 "Skipping recently processed fill report: {}",
1184 fill_report.trade_id,
1185 );
1186 continue;
1187 }
1188 self.exec_manager.observe_execution_report(report);
1189 }
1190 ExecutionEvent::Account(_) => {}
1191 }
1192
1193 AsyncRunner::handle_exec_event(evt);
1194
1195 for coid in &close_ids {
1197 let is_closed = self.kernel.cache().borrow()
1198 .order(coid).is_some_and(|o| o.is_closed());
1199 if is_closed {
1200 self.exec_manager.clear_recon_tracking(coid, true);
1201 }
1202 }
1203 }
1204 Some(cmd) = exec_cmd_rx.recv() => {
1205 if is_shutting_down {
1206 log::debug!("Residual exec command: {cmd:?}");
1207 residual_events += 1;
1208 }
1209
1210 match &cmd {
1211 TradingCommand::SubmitOrder(submit) => {
1212 self.exec_manager.register_inflight(submit.client_order_id);
1213 }
1214 TradingCommand::SubmitOrderList(submit) => {
1215 for order_init in &submit.order_inits {
1216 self.exec_manager.register_inflight(order_init.client_order_id);
1217 }
1218 }
1219 TradingCommand::ModifyOrder(modify) => {
1220 self.exec_manager.register_inflight(modify.client_order_id);
1221 }
1222 TradingCommand::ModifyOrders(modify) => {
1223 for child in &modify.modifies {
1224 self.exec_manager.register_inflight(child.client_order_id);
1225 }
1226 }
1227 TradingCommand::CancelOrder(cancel) => {
1228 self.exec_manager.register_inflight(cancel.client_order_id);
1229 }
1230 _ => {}
1231 }
1232 AsyncRunner::handle_exec_command(cmd);
1233 }
1234 message = recv_external_msgbus_message(&mut external_msgbus_rx) => {
1235 match message {
1236 Some(message) => {
1237 if is_shutting_down {
1238 log::debug!("Residual external message bus message: {message}");
1239 residual_events += 1;
1240 }
1241 Self::republish_external_msgbus_message(&message);
1242 }
1243 None => {
1244 log::info!("External message bus ingress closed");
1245 external_msgbus_rx = None;
1246 self.close_external_ingress();
1247 }
1248 }
1249 }
1250 Some(evt) = data_evt_rx.recv() => {
1251 if is_shutting_down {
1252 log::debug!("Residual data event: {evt:?}");
1253 residual_events += 1;
1254 }
1255 AsyncRunner::handle_data_event(evt);
1256 }
1257 Some(cmd) = data_cmd_rx.recv() => {
1258 if is_shutting_down {
1259 log::debug!("Residual data command: {cmd:?}");
1260 residual_events += 1;
1261 }
1262 AsyncRunner::handle_data_command(cmd);
1263 }
1264 }
1265 }
1266
1267 if residual_events > 0 {
1268 log::debug!("Processed {residual_events} residual events during shutdown");
1269 }
1270
1271 drop(open_order_report_task.take());
1272 drop(position_report_task.take());
1273 drop(external_msgbus_rx.take());
1274 let _ = self.kernel.cache().borrow().check_residuals();
1275
1276 self.finalize_stop().await?;
1277
1278 Self::drain_channels(
1280 &mut time_evt_rx,
1281 &mut data_evt_rx,
1282 &mut data_cmd_rx,
1283 &mut exec_evt_rx,
1284 &mut exec_cmd_rx,
1285 );
1286
1287 log::info!("Event loop stopped");
1288
1289 Ok(())
1290 }
1291
1292 fn take_external_ingress_receiver(
1293 &mut self,
1294 ) -> anyhow::Result<Option<tokio::sync::mpsc::Receiver<BusMessage>>> {
1295 let Some(external_ingress) = self.external_msgbus.as_mut() else {
1296 return Ok(None);
1297 };
1298
1299 let receiver = external_ingress.take_receiver()?;
1300 log::info!("External message bus ingress started");
1301 Ok(Some(receiver))
1302 }
1303
1304 fn republish_external_msgbus_message(message: &BusMessage) {
1305 if let Err(e) = msgbus::republish_external_message(message) {
1306 log::error!("Failed to republish external message bus message: {e}");
1307 }
1308 }
1309
1310 fn close_external_ingress(&mut self) {
1311 if let Some(external_ingress) = self.external_msgbus.as_mut()
1312 && !external_ingress.is_closed()
1313 {
1314 external_ingress.close();
1315 }
1316 }
1317
1318 fn process_reconciliation_events(&mut self, events: &[OrderEventAny]) {
1319 if events.is_empty() {
1320 return;
1321 }
1322
1323 log::info!(
1324 "Processing {} reconciliation event{}",
1325 events.len(),
1326 if events.len() == 1 { "" } else { "s" }
1327 );
1328
1329 for event in events {
1330 self.exec_manager
1331 .record_local_activity(event.client_order_id());
1332 if let OrderEventAny::Filled(fill) = event {
1333 self.exec_manager.record_position_activity(
1334 fill.instrument_id,
1335 fill.account_id,
1336 fill.ts_event,
1337 );
1338 self.exec_manager.mark_fill_processed(fill.trade_id);
1339 }
1340 self.kernel.exec_engine.borrow_mut().process(event);
1341 }
1342 }
1343
1344 async fn connect_exec_phase(&mut self) -> anyhow::Result<EngineConnectionStatus> {
1349 self.kernel.connect_exec_clients().await;
1350 Ok(self.await_engines_connected().await)
1351 }
1352
1353 fn startup_abort_reason(&self) -> Option<&'static str> {
1354 if self.handle.should_stop() {
1355 Some("Stop signal received during startup")
1356 } else if self.kernel.is_shutdown_requested() {
1357 Some("Shutdown signal received during startup")
1358 } else {
1359 None
1360 }
1361 }
1362
1363 async fn abort_startup(&mut self, reason: &str) -> anyhow::Result<()> {
1364 log::info!("{reason}, aborting startup");
1365 self.handle.set_state(NodeState::ShuttingDown);
1366 self.finalize_stop().await
1367 }
1368
1369 #[cfg(feature = "plugin")]
1370 async fn abort_after_trader_start_failure(
1371 &mut self,
1372 start_err: anyhow::Error,
1373 ) -> anyhow::Result<()> {
1374 log::info!("Plug-in controller startup failed, aborting startup");
1375 self.handle.set_state(NodeState::ShuttingDown);
1376 self.kernel.stop_trader();
1377
1378 if let Err(finalize_err) = self.finalize_stop().await {
1379 anyhow::bail!(
1380 "failed to start plug-in controller: {start_err}; failed to finalize startup abort: {finalize_err}"
1381 );
1382 }
1383 Err(start_err)
1384 }
1385
1386 fn initiate_shutdown(&mut self) {
1387 #[cfg(feature = "plugin")]
1388 if let Err(e) = self.plugins.stop_controllers() {
1389 log::error!("Error stopping plug-in controllers: {e}");
1390 }
1391 self.kernel.stop_trader();
1392 let delay = self.kernel.delay_post_stop();
1393 log::info!("Awaiting residual events ({delay:?})...");
1394
1395 self.shutdown_deadline = Some(dst::time::Instant::now() + delay);
1396 self.handle.set_state(NodeState::ShuttingDown);
1397 }
1398
1399 async fn finalize_stop(&mut self) -> anyhow::Result<()> {
1400 self.close_external_ingress();
1401
1402 let disconnect_result = self.kernel.disconnect_clients().await;
1403 if let Err(ref e) = disconnect_result {
1404 log::error!("Error disconnecting clients: {e}");
1405 }
1406
1407 self.await_engines_disconnected().await;
1408 self.kernel.finalize_stop().await;
1409
1410 self.handle.set_state(NodeState::Stopped);
1411
1412 disconnect_result
1413 }
1414
1415 fn drain_channels(
1416 time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
1417 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1418 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1419 exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1420 exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
1421 ) {
1422 let mut drained = 0;
1423
1424 while let Ok(handler) = time_evt_rx.try_recv() {
1425 AsyncRunner::handle_time_event(handler);
1426 drained += 1;
1427 }
1428
1429 while let Ok(cmd) = data_cmd_rx.try_recv() {
1430 AsyncRunner::handle_data_command(cmd);
1431 drained += 1;
1432 }
1433
1434 while let Ok(evt) = data_evt_rx.try_recv() {
1435 AsyncRunner::handle_data_event(evt);
1436 drained += 1;
1437 }
1438
1439 while let Ok(cmd) = exec_cmd_rx.try_recv() {
1440 AsyncRunner::handle_exec_command(cmd);
1441 drained += 1;
1442 }
1443
1444 while let Ok(evt) = exec_evt_rx.try_recv() {
1445 AsyncRunner::handle_exec_event(evt);
1446 drained += 1;
1447 }
1448
1449 if drained > 0 {
1450 log::info!("Drained {drained} remaining events during shutdown");
1451 }
1452 }
1453
1454 #[must_use]
1456 pub fn environment(&self) -> Environment {
1457 self.kernel.environment()
1458 }
1459
1460 #[must_use]
1462 pub const fn kernel(&self) -> &NautilusKernel {
1463 &self.kernel
1464 }
1465
1466 #[must_use]
1468 pub const fn kernel_mut(&mut self) -> &mut NautilusKernel {
1469 &mut self.kernel
1470 }
1471
1472 #[must_use]
1474 pub fn trader_id(&self) -> TraderId {
1475 self.kernel.trader_id()
1476 }
1477
1478 #[must_use]
1480 pub const fn instance_id(&self) -> UUID4 {
1481 self.kernel.instance_id()
1482 }
1483
1484 #[must_use]
1486 pub fn state(&self) -> NodeState {
1487 self.handle.state()
1488 }
1489
1490 #[must_use]
1492 pub fn is_running(&self) -> bool {
1493 self.state().is_running()
1494 }
1495
1496 pub fn set_cache_database(
1506 &mut self,
1507 database: Box<dyn CacheDatabaseAdapter>,
1508 ) -> anyhow::Result<()> {
1509 if self.state() != NodeState::Idle {
1510 anyhow::bail!(
1511 "Cannot set cache database while node is running, set it before calling start()"
1512 );
1513 }
1514
1515 self.kernel.cache().borrow_mut().set_database(database);
1516 Ok(())
1517 }
1518
1519 #[must_use]
1521 pub fn exec_manager(&self) -> &ExecutionManager {
1522 &self.exec_manager
1523 }
1524
1525 #[must_use]
1527 pub fn exec_manager_mut(&mut self) -> &mut ExecutionManager {
1528 &mut self.exec_manager
1529 }
1530
1531 pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
1544 where
1545 T: DataActor + DataActorNative + Component + Actor + 'static,
1546 {
1547 if self.state() != NodeState::Idle {
1548 anyhow::bail!(
1549 "Cannot add actor while node is running, add actors before calling start()"
1550 );
1551 }
1552
1553 self.kernel.trader.borrow_mut().add_actor(actor)
1554 }
1555
1556 pub fn add_actor_from_factory<F, T>(&mut self, factory: F) -> anyhow::Result<()>
1568 where
1569 F: FnOnce() -> anyhow::Result<T>,
1570 T: DataActor + DataActorNative + Component + Actor + 'static,
1571 {
1572 if self.state() != NodeState::Idle {
1573 anyhow::bail!(
1574 "Cannot add actor while node is running, add actors before calling start()"
1575 );
1576 }
1577
1578 self.kernel
1579 .trader
1580 .borrow_mut()
1581 .add_actor_from_factory(factory)
1582 }
1583
1584 pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
1595 where
1596 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
1597 {
1598 if self.state() != NodeState::Idle {
1599 anyhow::bail!(
1600 "Cannot add strategy while node is running, add strategies before calling start()"
1601 );
1602 }
1603
1604 let strategy_id = self
1606 .kernel
1607 .trader
1608 .borrow()
1609 .prepare_strategy_for_registration(&mut strategy)?;
1610 let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
1611 if let Some(claims) = strategy.external_order_claims() {
1612 for instrument_id in &claims {
1613 self.exec_manager
1614 .claim_external_orders(*instrument_id, strategy_id)?;
1615 }
1616 log_info!(
1617 "Registered external order claims for {}: {:?}",
1618 strategy_id,
1619 claims,
1620 color = LogColor::Blue
1621 );
1622 }
1623
1624 self.kernel.trader.borrow_mut().add_strategy(strategy)?;
1625
1626 if let Some(oms_type) = oms_type {
1627 self.kernel
1628 .exec_engine
1629 .borrow_mut()
1630 .register_oms_type(strategy_id, oms_type);
1631 }
1632
1633 Ok(())
1634 }
1635
1636 pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
1647 where
1648 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
1649 {
1650 if self.state() != NodeState::Idle {
1651 anyhow::bail!(
1652 "Cannot add exec algorithm while node is running, add exec algorithms before calling start()"
1653 );
1654 }
1655
1656 self.kernel
1657 .trader
1658 .borrow_mut()
1659 .add_exec_algorithm(exec_algorithm)
1660 }
1661
1662 fn run_reconciliation_checks(
1666 &mut self,
1667 intervals: ReconciliationCheckIntervals,
1668 state: &mut ReconciliationCheckState<'_>,
1669 ) {
1670 let ts_now = self.exec_manager.generate_timestamp_ns();
1671
1672 if reconciliation_check_due(ts_now, *state.ts_last_inflight, intervals.inflight) {
1673 if self.state() == NodeState::ShuttingDown {
1674 return;
1675 }
1676 let result = self.exec_manager.check_inflight_orders();
1677 self.process_reconciliation_events(&result.events);
1678 for cmd in result.queries {
1679 AsyncRunner::handle_exec_command(cmd);
1680 }
1681 *state.ts_last_inflight = ts_now;
1682 }
1683
1684 let open_due = reconciliation_check_due(ts_now, *state.ts_last_open, intervals.open);
1685 let position_due =
1686 reconciliation_check_due(ts_now, *state.ts_last_position, intervals.position);
1687
1688 if (open_due || position_due) && self.state() == NodeState::ShuttingDown {
1689 return;
1690 }
1691
1692 if state.open_order_report_task.is_some() {
1693 if open_due {
1694 log::debug!("Open-order reconciliation already in progress");
1695 *state.ts_last_open = ts_now;
1696 }
1697
1698 if position_due {
1699 log::debug!(
1700 "Position reconciliation delayed: open-order reconciliation in progress"
1701 );
1702 }
1703
1704 return;
1705 }
1706
1707 if state.position_report_task.is_some() {
1708 if position_due {
1709 log::debug!("Position reconciliation already in progress");
1710 *state.ts_last_position = ts_now;
1711 }
1712
1713 if open_due {
1714 log::debug!(
1715 "Open-order reconciliation delayed: position reconciliation in progress"
1716 );
1717 }
1718
1719 return;
1720 }
1721
1722 if position_due && (!open_due || *state.ts_last_position < *state.ts_last_open) {
1723 *state.position_report_task = self.start_position_report_check();
1724 *state.ts_last_position = ts_now;
1725 } else if open_due {
1726 *state.open_order_report_task = self.start_open_order_report_check();
1727 *state.ts_last_open = ts_now;
1728 }
1729 }
1730
1731 fn start_open_order_report_check(&self) -> Option<OpenOrderReportTask> {
1732 if self.exec_clients.is_empty() {
1733 log::debug!("No execution clients to check orders consistency");
1734 return None;
1735 }
1736
1737 let check = self
1738 .exec_manager
1739 .prepare_open_order_report_check(UUID4::new());
1740 let command = check.command.clone();
1741 let clients = self.exec_clients.clone();
1742
1743 Some(OpenOrderReportTask {
1744 future: Box::pin(async move {
1745 let reports = request_open_order_reports(clients, command).await;
1746 OpenOrderReportResult { check, reports }
1747 }),
1748 })
1749 }
1750
1751 fn start_position_report_check(&self) -> Option<PositionReportTask> {
1752 if self.exec_clients.is_empty() {
1753 log::debug!("No execution clients to check positions consistency");
1754 return None;
1755 }
1756
1757 let check = self
1758 .exec_manager
1759 .prepare_position_report_check(UUID4::new());
1760 let command = check.command.clone();
1761 let clients = self.exec_clients.clone();
1762
1763 Some(PositionReportTask {
1764 future: Box::pin(async move {
1765 let result = request_position_reports(clients, command).await;
1766 PositionReportResult {
1767 check,
1768 reports: result.reports,
1769 failed_venues: result.failed_venues,
1770 }
1771 }),
1772 })
1773 }
1774}
1775
1776async fn recv_external_msgbus_message(
1777 rx: &mut Option<tokio::sync::mpsc::Receiver<BusMessage>>,
1778) -> Option<BusMessage> {
1779 match rx {
1780 Some(rx) => rx.recv().await,
1781 None => std::future::pending::<Option<BusMessage>>().await,
1782 }
1783}
1784
1785async fn request_open_order_reports(
1786 clients: Vec<LiveExecutionClient>,
1787 command: GenerateOrderStatusReports,
1788) -> Vec<OrderStatusReport> {
1789 let mut all_reports = Vec::new();
1790
1791 for client in clients {
1792 match client.generate_order_status_reports(&command).await {
1793 Ok(reports) => {
1794 all_reports.extend(reports);
1795 }
1796 Err(e) => {
1797 log::warn!(
1798 "Failed to generate order status reports from {}: {e}",
1799 client.client_id()
1800 );
1801 }
1802 }
1803 }
1804
1805 all_reports
1806}
1807
1808async fn request_position_reports(
1809 clients: Vec<LiveExecutionClient>,
1810 command: GeneratePositionStatusReports,
1811) -> PositionReportQueryResult {
1812 let mut all_reports = Vec::new();
1813 let mut failed_venues = IndexSet::new();
1814
1815 for client in clients {
1816 let venue = client.venue();
1817 match client.generate_position_status_reports(&command).await {
1818 Ok(reports) => {
1819 all_reports.extend(reports);
1820 }
1821 Err(e) => {
1822 failed_venues.insert(venue);
1823 log::warn!(
1824 "Failed to generate position status reports from {}: {e}",
1825 client.client_id()
1826 );
1827 }
1828 }
1829 }
1830
1831 PositionReportQueryResult {
1832 reports: all_reports,
1833 failed_venues,
1834 }
1835}
1836
1837fn reconciliation_check_due(ts_now: UnixNanos, ts_last: UnixNanos, interval_ns: u64) -> bool {
1838 interval_ns > 0
1839 && ts_now
1840 .duration_since(&ts_last)
1841 .is_some_and(|elapsed_ns| elapsed_ns >= interval_ns)
1842}
1843
1844#[derive(Clone, Copy)]
1845struct ReconciliationCheckIntervals {
1846 inflight: u64,
1847 open: u64,
1848 position: u64,
1849}
1850
1851struct ReconciliationCheckState<'a> {
1852 ts_last_inflight: &'a mut UnixNanos,
1853 ts_last_open: &'a mut UnixNanos,
1854 ts_last_position: &'a mut UnixNanos,
1855 open_order_report_task: &'a mut Option<OpenOrderReportTask>,
1856 position_report_task: &'a mut Option<PositionReportTask>,
1857}
1858
1859type OpenOrderReportFuture = Pin<Box<dyn Future<Output = OpenOrderReportResult>>>;
1860
1861struct OpenOrderReportTask {
1862 future: OpenOrderReportFuture,
1863}
1864
1865struct OpenOrderReportResult {
1866 check: OpenOrderReportCheck,
1867 reports: Vec<OrderStatusReport>,
1868}
1869
1870type PositionReportFuture = Pin<Box<dyn Future<Output = PositionReportResult>>>;
1871
1872struct PositionReportTask {
1873 future: PositionReportFuture,
1874}
1875
1876struct PositionReportResult {
1877 check: PositionReportCheck,
1878 reports: Vec<PositionStatusReport>,
1879 failed_venues: IndexSet<Venue>,
1880}
1881
1882struct PositionReportQueryResult {
1883 reports: Vec<PositionStatusReport>,
1884 failed_venues: IndexSet<Venue>,
1885}
1886
1887fn flush_pending_data(
1894 pending: &mut PendingEvents,
1895 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1896 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1897) {
1898 loop {
1899 let mut progressed = pending.drain_data();
1900
1901 while let Ok(evt) = data_evt_rx.try_recv() {
1902 AsyncRunner::handle_data_event(evt);
1903 progressed = true;
1904 }
1905
1906 while let Ok(cmd) = data_cmd_rx.try_recv() {
1907 AsyncRunner::handle_data_command(cmd);
1908 progressed = true;
1909 }
1910
1911 if !progressed {
1912 break;
1913 }
1914 }
1915}
1916
1917fn flush_all_pending(
1923 pending: &mut PendingEvents,
1924 time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
1925 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1926 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1927 exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1928 exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
1929) {
1930 while let Ok(handler) = time_evt_rx.try_recv() {
1932 AsyncRunner::handle_time_event(handler);
1933 }
1934
1935 while let Ok(evt) = data_evt_rx.try_recv() {
1936 pending.data_evts.push(evt);
1937 }
1938
1939 while let Ok(cmd) = data_cmd_rx.try_recv() {
1940 pending.data_cmds.push(cmd);
1941 }
1942
1943 while let Ok(evt) = exec_evt_rx.try_recv() {
1944 match evt {
1945 ExecutionEvent::Account(_) => {
1946 AsyncRunner::handle_exec_event(evt);
1947 }
1948 ExecutionEvent::Report(report) => {
1949 pending.exec_reports.push(report);
1950 }
1951 ExecutionEvent::Order(order_evt) => {
1952 pending.order_evts.push(order_evt);
1953 }
1954 ExecutionEvent::OrderSubmittedBatch(batch) => {
1955 for submitted in batch {
1956 pending.order_evts.push(OrderEventAny::Submitted(submitted));
1957 }
1958 }
1959 ExecutionEvent::OrderAcceptedBatch(batch) => {
1960 for accepted in batch {
1961 pending.order_evts.push(OrderEventAny::Accepted(accepted));
1962 }
1963 }
1964 ExecutionEvent::OrderCanceledBatch(batch) => {
1965 for canceled in batch {
1966 pending.order_evts.push(OrderEventAny::Canceled(canceled));
1967 }
1968 }
1969 }
1970 }
1971
1972 while let Ok(cmd) = exec_cmd_rx.try_recv() {
1973 pending.exec_cmds.push(cmd);
1974 }
1975
1976 pending.drain();
1977}
1978
1979async fn drive_with_event_buffering<F: std::future::Future>(
1984 future: F,
1985 pending: &mut PendingEvents,
1986 time_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
1987 data_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
1988 data_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
1989 exec_evt_rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1990 exec_cmd_rx: &mut tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
1991) -> F::Output {
1992 tokio::pin!(future);
1993
1994 loop {
1995 tokio::select! {
1996 biased;
1997
1998 result = &mut future => {
1999 break result;
2000 }
2001 Some(handler) = time_evt_rx.recv() => {
2002 AsyncRunner::handle_time_event(handler);
2003 }
2004 Some(evt) = exec_evt_rx.recv() => {
2005 match evt {
2009 ExecutionEvent::Account(_) => {
2010 AsyncRunner::handle_exec_event(evt);
2011 }
2012 ExecutionEvent::Report(report) => {
2013 pending.exec_reports.push(report);
2014 }
2015 ExecutionEvent::Order(order_evt) => {
2016 pending.order_evts.push(order_evt);
2017 }
2018 ExecutionEvent::OrderSubmittedBatch(batch) => {
2019 for submitted in batch {
2020 pending.order_evts.push(OrderEventAny::Submitted(submitted));
2021 }
2022 }
2023 ExecutionEvent::OrderAcceptedBatch(batch) => {
2024 for accepted in batch {
2025 pending.order_evts.push(OrderEventAny::Accepted(accepted));
2026 }
2027 }
2028 ExecutionEvent::OrderCanceledBatch(batch) => {
2029 for canceled in batch {
2030 pending.order_evts.push(OrderEventAny::Canceled(canceled));
2031 }
2032 }
2033 }
2034 }
2035 Some(cmd) = exec_cmd_rx.recv() => {
2036 pending.exec_cmds.push(cmd);
2037 }
2038 Some(evt) = data_evt_rx.recv() => {
2039 pending.data_evts.push(evt);
2040 }
2041 Some(cmd) = data_cmd_rx.recv() => {
2042 pending.data_cmds.push(cmd);
2043 }
2044 }
2045 }
2046}
2047
2048#[derive(Default)]
2049struct PendingEvents {
2050 data_cmds: Vec<DataCommand>,
2051 data_evts: Vec<DataEvent>,
2052 exec_cmds: Vec<TradingCommand>,
2053 exec_reports: Vec<ExecutionReport>,
2054 order_evts: Vec<OrderEventAny>,
2055}
2056
2057impl PendingEvents {
2058 fn is_empty(&self) -> bool {
2059 self.data_evts.is_empty()
2060 && self.data_cmds.is_empty()
2061 && self.exec_cmds.is_empty()
2062 && self.exec_reports.is_empty()
2063 && self.order_evts.is_empty()
2064 }
2065
2066 fn drain_data(&mut self) -> bool {
2070 let total = self.data_evts.len() + self.data_cmds.len();
2071
2072 if total > 0 {
2073 log::debug!(
2074 "Draining {total} data events/commands into cache \
2075 (data_evts={}, data_cmds={})",
2076 self.data_evts.len(),
2077 self.data_cmds.len(),
2078 );
2079 }
2080
2081 for evt in self.data_evts.drain(..) {
2082 AsyncRunner::handle_data_event(evt);
2083 }
2084
2085 for cmd in self.data_cmds.drain(..) {
2086 AsyncRunner::handle_data_command(cmd);
2087 }
2088
2089 total > 0
2090 }
2091
2092 fn drain(&mut self) {
2094 let total = self.data_evts.len()
2095 + self.data_cmds.len()
2096 + self.exec_cmds.len()
2097 + self.exec_reports.len()
2098 + self.order_evts.len();
2099
2100 if total > 0 {
2101 log::debug!(
2102 "Processing {total} events/commands queued during startup \
2103 (data_evts={}, data_cmds={}, exec_cmds={}, exec_reports={}, order_evts={})",
2104 self.data_evts.len(),
2105 self.data_cmds.len(),
2106 self.exec_cmds.len(),
2107 self.exec_reports.len(),
2108 self.order_evts.len()
2109 );
2110 }
2111
2112 for evt in self.data_evts.drain(..) {
2113 AsyncRunner::handle_data_event(evt);
2114 }
2115
2116 for cmd in self.data_cmds.drain(..) {
2117 AsyncRunner::handle_data_command(cmd);
2118 }
2119
2120 for report in self.exec_reports.drain(..) {
2121 AsyncRunner::handle_exec_event(ExecutionEvent::Report(report));
2122 }
2123
2124 for cmd in self.exec_cmds.drain(..) {
2125 AsyncRunner::handle_exec_command(cmd);
2126 }
2127
2128 for evt in self.order_evts.drain(..) {
2129 AsyncRunner::handle_exec_event(ExecutionEvent::Order(evt));
2130 }
2131 }
2132}
2133
2134struct ClientStatus {
2135 client: String,
2136 client_type: &'static str,
2137 connected: bool,
2138}
2139
2140fn render_client_statuses(rows: Vec<ClientStatus>) -> String {
2141 let mut builder = Builder::with_capacity(rows.len() + 1, 3);
2142 builder.push_record(["Client", "Type", "Connected"]);
2143
2144 for row in rows {
2145 builder.push_record([
2146 row.client,
2147 row.client_type.to_string(),
2148 row.connected.to_string(),
2149 ]);
2150 }
2151
2152 builder.build().with(Style::rounded()).to_string()
2153}
2154
2155#[cfg(test)]
2156mod tests {
2157 use std::{
2158 cell::{Cell, RefCell},
2159 fmt::Debug,
2160 rc::Rc,
2161 sync::{
2162 Arc, Mutex,
2163 atomic::{AtomicBool, Ordering},
2164 },
2165 };
2166
2167 use bytes::Bytes;
2168 #[cfg(feature = "python")]
2169 use nautilus_common::runner::{
2170 SyncDataCommandSender, SyncTradingCommandSender, replace_data_cmd_sender,
2171 replace_exec_cmd_sender,
2172 };
2173 use nautilus_common::{
2174 actor::DataActor,
2175 cache::Cache,
2176 clock::Clock,
2177 enums::SerializationEncoding,
2178 messages::execution::{SubmitOrder, TradingCommand},
2179 msgbus::{
2180 self, BusMessage, BusPayloadType, MessageBusBacking, MessageBusBackingFactory,
2181 MessageBusConfig, MessageBusExternalEgress, MessageBusExternalIngress,
2182 MessagingSwitchboard, TypedHandler, TypedIntoHandler,
2183 },
2184 };
2185 use nautilus_core::{UUID4, UnixNanos};
2186 use nautilus_execution::engine::{
2187 ExecutionEngine, SnapshotAnchorer, stubs::StubExecutionClient,
2188 };
2189 use nautilus_model::{
2190 data::QuoteTick,
2191 enums::{OmsType, OrderStatus, OrderType},
2192 identifiers::{
2193 AccountId, ClientId, InstrumentId, PositionId, StrategyId, TraderId, VenueOrderId,
2194 },
2195 instruments::{Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt},
2196 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
2197 types::{Price, Quantity},
2198 };
2199 use nautilus_system::{KernelEventStore, RegisteredComponents, event_store::EventStoreConfig};
2200 use nautilus_trading::{
2201 nautilus_strategy,
2202 strategy::{config::StrategyConfig, core::StrategyCore},
2203 };
2204 use rstest::*;
2205
2206 use super::*;
2207
2208 #[rstest]
2209 fn test_render_client_statuses() {
2210 let rows = vec![
2211 ClientStatus {
2212 client: "BINANCE".to_string(),
2213 client_type: "Data",
2214 connected: true,
2215 },
2216 ClientStatus {
2217 client: "SIM".to_string(),
2218 client_type: "Execution",
2219 connected: false,
2220 },
2221 ];
2222
2223 let output = render_client_statuses(rows);
2224 let expected = "â•─────────┬───────────┬───────────╮\n\
2225│ Client │ Type │ Connected │\n\
2226├─────────┼───────────┼───────────┤\n\
2227│ BINANCE │ Data │ true │\n\
2228│ SIM │ Execution │ false │\n\
2229╰─────────┴───────────┴───────────╯";
2230
2231 assert_eq!(output, expected);
2232 }
2233
2234 #[derive(Debug)]
2235 struct ReplayKernelEventStore {
2236 fail_restore: bool,
2237 }
2238
2239 impl KernelEventStore for ReplayKernelEventStore {
2240 fn restore_parent_cache(
2241 &mut self,
2242 _instance_id: UUID4,
2243 _cache: &mut Cache,
2244 ) -> anyhow::Result<()> {
2245 if self.fail_restore {
2246 anyhow::bail!("replay restore failed");
2247 }
2248
2249 Ok(())
2250 }
2251
2252 fn open(
2253 &mut self,
2254 _instance_id: UUID4,
2255 _components: &RegisteredComponents,
2256 _environment: Environment,
2257 ) -> anyhow::Result<()> {
2258 Ok(())
2259 }
2260
2261 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
2262 None
2263 }
2264
2265 fn seal(&mut self, _ts_init: UnixNanos) {}
2266
2267 fn run_id(&self) -> Option<&str> {
2268 Some("replay-child")
2269 }
2270
2271 fn parent_run_id(&self) -> Option<&str> {
2272 Some("seed-run")
2273 }
2274
2275 fn is_event_store_replay_configured(&self) -> bool {
2276 true
2277 }
2278
2279 fn is_halted(&self) -> bool {
2280 false
2281 }
2282 }
2283
2284 #[derive(Debug)]
2285 struct TestStrategy {
2286 core: StrategyCore,
2287 }
2288
2289 impl TestStrategy {
2290 fn new(config: StrategyConfig) -> Self {
2291 Self {
2292 core: StrategyCore::new(config),
2293 }
2294 }
2295 }
2296
2297 impl DataActor for TestStrategy {}
2298
2299 nautilus_strategy!(TestStrategy);
2300
2301 fn live_node_with_replay_store(fail_restore: bool) -> LiveNode {
2302 let builder = LiveNodeBuilder::new(TraderId::default(), Environment::Live)
2305 .unwrap()
2306 .with_exec_engine_config(crate::config::LiveExecEngineConfig {
2307 reconciliation: false,
2308 ..Default::default()
2309 })
2310 .with_load_state(true)
2311 .with_name("TestKernel")
2312 .with_event_store(move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
2313 Ok(Box::new(ReplayKernelEventStore { fail_restore }) as Box<dyn KernelEventStore>)
2314 });
2315
2316 builder.build().unwrap()
2317 }
2318
2319 #[rstest]
2320 fn test_add_strategy_registers_configured_hedging_oms_type() {
2321 let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
2322 .unwrap()
2323 .with_reconciliation(false)
2324 .with_delay_post_stop_secs(0)
2325 .with_timeout_connection(1)
2326 .build()
2327 .unwrap();
2328 let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
2329
2330 node.add_strategy(TestStrategy::new(StrategyConfig {
2331 strategy_id: Some(strategy_id),
2332 oms_type: Some(OmsType::Hedging),
2333 ..Default::default()
2334 }))
2335 .unwrap();
2336
2337 let instrument = crypto_perpetual_ethusdt();
2338 let instrument_id = instrument.id();
2339 let client_id = ClientId::from("STUB");
2340
2341 node.kernel
2342 .cache
2343 .borrow_mut()
2344 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2345 .unwrap();
2346 node.kernel
2347 .exec_engine
2348 .borrow_mut()
2349 .register_client(Box::new(StubExecutionClient::new(
2350 client_id,
2351 AccountId::from("TEST-ACCOUNT"),
2352 instrument_id.venue,
2353 OmsType::Netting,
2354 None,
2355 )))
2356 .unwrap();
2357
2358 let order = OrderTestBuilder::new(OrderType::Market)
2359 .trader_id(node.trader_id())
2360 .strategy_id(strategy_id)
2361 .instrument_id(instrument_id)
2362 .quantity(Quantity::from("1.000"))
2363 .build();
2364 let position_id = PositionId::new("CUSTOM-POSITION-001");
2365
2366 node.kernel
2367 .cache
2368 .borrow_mut()
2369 .add_order(order.clone(), Some(position_id), Some(client_id), true)
2370 .unwrap();
2371
2372 let submit_order = SubmitOrder::new(
2373 order.trader_id(),
2374 Some(client_id),
2375 strategy_id,
2376 instrument_id,
2377 order.client_order_id(),
2378 order.init_event().clone(),
2379 order.exec_algorithm_id(),
2380 Some(position_id),
2381 None,
2382 UUID4::new(),
2383 UnixNanos::default(),
2384 None,
2385 );
2386
2387 node.kernel
2388 .exec_engine
2389 .borrow()
2390 .execute(TradingCommand::SubmitOrder(submit_order));
2391
2392 let exec_engine = node.kernel.exec_engine.borrow();
2393 let cache = exec_engine.cache().borrow();
2394 let cached_order = cache
2395 .order(&order.client_order_id())
2396 .expect("Order should be cached");
2397
2398 assert_eq!(cached_order.status(), OrderStatus::Initialized);
2399 }
2400
2401 #[rstest]
2402 fn test_run_reconciliation_checks_does_not_publish_open_order_queries() {
2403 let config = LiveNodeConfig {
2404 exec_engine: crate::config::LiveExecEngineConfig {
2405 reconciliation: true,
2406 open_check_interval_secs: Some(1.0),
2407 position_check_interval_secs: Some(1.0),
2408 max_single_order_queries_per_cycle: 5,
2409 ..Default::default()
2410 },
2411 ..Default::default()
2412 };
2413 let mut node =
2414 LiveNode::build("ReconciliationFallbackNode".to_string(), Some(config)).unwrap();
2415 let client_id = ClientId::from("TEST-QUERY");
2416 let account_id = AccountId::from("TEST-QUERY-001");
2417
2418 let trading_commands = Rc::new(RefCell::new(Vec::new()));
2419 msgbus::register_trading_command_endpoint(
2420 MessagingSwitchboard::exec_engine_execute(),
2421 TypedIntoHandler::from({
2422 let trading_commands = trading_commands.clone();
2423 move |command: TradingCommand| {
2424 trading_commands.borrow_mut().push(command);
2425 }
2426 }),
2427 );
2428
2429 let venue_order_id = VenueOrderId::from("V-NODE-QUERY-001");
2430 let instrument = crypto_perpetual_ethusdt();
2431 let instrument_id = instrument.id();
2432 let client_order_id = ClientOrderId::from("O-NODE-QUERY-001");
2433
2434 node.kernel
2435 .cache
2436 .borrow_mut()
2437 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
2438 .unwrap();
2439 insert_accepted_limit_order_in_node(
2440 &node,
2441 account_id,
2442 client_id,
2443 instrument_id,
2444 client_order_id,
2445 venue_order_id,
2446 );
2447
2448 let mut ts_last_inflight = UnixNanos::default();
2449 let mut ts_last_open = UnixNanos::default();
2450 let mut ts_last_position = UnixNanos::default();
2451 let mut open_order_report_task = None;
2452 let mut position_report_task = None;
2453
2454 node.run_reconciliation_checks(
2455 ReconciliationCheckIntervals {
2456 inflight: 0,
2457 open: 1,
2458 position: 0,
2459 },
2460 &mut ReconciliationCheckState {
2461 ts_last_inflight: &mut ts_last_inflight,
2462 ts_last_open: &mut ts_last_open,
2463 ts_last_position: &mut ts_last_position,
2464 open_order_report_task: &mut open_order_report_task,
2465 position_report_task: &mut position_report_task,
2466 },
2467 );
2468
2469 let commands = trading_commands.borrow();
2470
2471 assert!(commands.is_empty());
2472 assert!(open_order_report_task.is_none());
2473 assert!(position_report_task.is_none());
2474
2475 ExecutionEngine::register_msgbus_handlers(&node.kernel.exec_engine);
2476 }
2477
2478 fn insert_accepted_limit_order_in_node(
2479 node: &LiveNode,
2480 account_id: AccountId,
2481 client_id: ClientId,
2482 instrument_id: InstrumentId,
2483 client_order_id: ClientOrderId,
2484 venue_order_id: VenueOrderId,
2485 ) {
2486 let order = OrderTestBuilder::new(OrderType::Limit)
2487 .client_order_id(client_order_id)
2488 .instrument_id(instrument_id)
2489 .quantity(Quantity::from("10.0"))
2490 .price(Price::from("100.0"))
2491 .build();
2492 let submitted = TestOrderEventStubs::submitted(&order, account_id);
2493 node.kernel
2494 .cache
2495 .borrow_mut()
2496 .add_order(order, None, Some(client_id), false)
2497 .unwrap();
2498 let order = node
2499 .kernel
2500 .cache
2501 .borrow_mut()
2502 .update_order(&submitted)
2503 .unwrap();
2504 let accepted = TestOrderEventStubs::accepted(&order, account_id, venue_order_id);
2505 node.kernel
2506 .cache
2507 .borrow_mut()
2508 .update_order(&accepted)
2509 .unwrap();
2510 }
2511
2512 #[rstest]
2513 #[case(0, NodeState::Idle)]
2514 #[case(1, NodeState::Starting)]
2515 #[case(2, NodeState::Running)]
2516 #[case(3, NodeState::ShuttingDown)]
2517 #[case(4, NodeState::Stopped)]
2518 fn test_node_state_from_u8_valid(#[case] value: u8, #[case] expected: NodeState) {
2519 assert_eq!(NodeState::from_u8(value), expected);
2520 }
2521
2522 #[rstest]
2523 #[case(5)]
2524 #[case(255)]
2525 #[should_panic(expected = "Invalid NodeState value")]
2526 fn test_node_state_from_u8_invalid_panics(#[case] value: u8) {
2527 let _ = NodeState::from_u8(value);
2528 }
2529
2530 #[rstest]
2531 fn test_node_state_roundtrip() {
2532 for state in [
2533 NodeState::Idle,
2534 NodeState::Starting,
2535 NodeState::Running,
2536 NodeState::ShuttingDown,
2537 NodeState::Stopped,
2538 ] {
2539 assert_eq!(NodeState::from_u8(state.as_u8()), state);
2540 }
2541 }
2542
2543 #[rstest]
2544 fn test_node_state_is_running_only_for_running() {
2545 assert!(!NodeState::Idle.is_running());
2546 assert!(!NodeState::Starting.is_running());
2547 assert!(NodeState::Running.is_running());
2548 assert!(!NodeState::ShuttingDown.is_running());
2549 assert!(!NodeState::Stopped.is_running());
2550 }
2551
2552 #[rstest]
2553 #[tokio::test]
2554 async fn test_await_engines_connected_returns_stop_requested() {
2555 let node = LiveNode::build("TestNode".to_string(), None).unwrap();
2556 let handle = node.handle();
2557
2558 handle.stop();
2559
2560 let status = node.await_engines_connected().await;
2561
2562 assert_eq!(status, EngineConnectionStatus::StopRequested);
2563 assert!(handle.should_stop());
2564 }
2565
2566 #[rstest]
2567 #[tokio::test]
2568 async fn test_await_engines_connected_returns_shutdown_requested() {
2569 let node = LiveNode::build("TestNode".to_string(), None).unwrap();
2570
2571 node.kernel().shutdown_flag().set(true);
2572
2573 let status = node.await_engines_connected().await;
2574
2575 assert_eq!(status, EngineConnectionStatus::ShutdownRequested);
2576 }
2577
2578 #[rstest]
2579 #[tokio::test]
2580 async fn test_start_stop_request_aborts_startup_without_running() {
2581 let config = LiveNodeConfig {
2582 exec_engine: crate::config::LiveExecEngineConfig {
2583 reconciliation: false,
2584 ..Default::default()
2585 },
2586 timeout_disconnection: Duration::from_millis(50),
2587 ..Default::default()
2588 };
2589 let mut node = LiveNode::build("TestNode".to_string(), Some(config)).unwrap();
2590 let handle = node.handle();
2591
2592 handle.stop();
2593 node.start().await.unwrap();
2594
2595 assert_eq!(handle.state(), NodeState::Stopped);
2596 assert!(handle.should_stop());
2597 assert!(!handle.is_running());
2598 }
2599
2600 #[rstest]
2601 #[tokio::test]
2602 async fn test_start_event_store_replay_skips_live_connections() {
2603 let mut node = live_node_with_replay_store(false);
2604 let handle = node.handle();
2605
2606 node.start().await.unwrap();
2607
2608 assert_eq!(handle.state(), NodeState::Running);
2609 assert!(handle.is_running());
2610 assert!(node.kernel.is_event_store_replay());
2611 assert!(node.runner.is_some());
2612 }
2613
2614 #[rstest]
2615 #[tokio::test]
2616 async fn test_start_event_store_replay_config_failure_aborts_startup() {
2617 let mut node = live_node_with_replay_store(true);
2618 let handle = node.handle();
2619
2620 node.start().await.unwrap();
2621
2622 assert_eq!(handle.state(), NodeState::Stopped);
2623 assert!(!handle.is_running());
2624 assert!(node.kernel.is_event_store_replay_configured());
2625 assert!(!node.kernel.is_event_store_replay());
2626 assert!(node.runner.is_some());
2627 }
2628
2629 #[rstest]
2630 #[tokio::test]
2631 async fn test_run_event_store_replay_consumes_runner_and_stops_before_connections() {
2632 let mut node = live_node_with_replay_store(false);
2633 let handle = node.handle();
2634
2635 node.run().await.unwrap();
2636
2637 assert_eq!(handle.state(), NodeState::Running);
2638 assert!(handle.is_running());
2639 assert!(node.kernel.is_event_store_replay());
2640 assert!(node.runner.is_none());
2641 }
2642
2643 #[rstest]
2644 #[tokio::test]
2645 async fn test_run_event_store_replay_config_failure_aborts_startup() {
2646 let mut node = live_node_with_replay_store(true);
2647 let handle = node.handle();
2648
2649 node.run().await.unwrap();
2650
2651 assert_eq!(handle.state(), NodeState::Stopped);
2652 assert!(!handle.is_running());
2653 assert!(node.kernel.is_event_store_replay_configured());
2654 assert!(!node.kernel.is_event_store_replay());
2655 assert!(node.runner.is_none());
2656 }
2657
2658 #[rstest]
2659 fn test_build_rejects_event_store_config_without_factory() {
2660 let config = LiveNodeConfig {
2661 event_store: Some(EventStoreConfig::default()),
2662 exec_engine: crate::config::LiveExecEngineConfig {
2663 reconciliation: false,
2664 ..Default::default()
2665 },
2666 ..Default::default()
2667 };
2668
2669 let err = LiveNodeBuilder::from_config(config)
2670 .expect("builder")
2671 .build()
2672 .expect_err("should reject event_store config without factory");
2673
2674 assert!(
2675 err.to_string().contains("with_event_store"),
2676 "error message should mention with_event_store, was: {err}"
2677 );
2678 }
2679
2680 #[rstest]
2681 fn test_direct_build_rejects_event_store_config() {
2682 let config = LiveNodeConfig {
2683 event_store: Some(EventStoreConfig::default()),
2684 exec_engine: crate::config::LiveExecEngineConfig {
2685 reconciliation: false,
2686 ..Default::default()
2687 },
2688 ..Default::default()
2689 };
2690
2691 let err = LiveNode::build("TestNode".to_string(), Some(config))
2692 .expect_err("LiveNode::build should reject event_store config");
2693
2694 assert!(
2695 err.to_string().contains("with_event_store"),
2696 "error message should mention with_event_store, was: {err}"
2697 );
2698 }
2699
2700 #[rstest]
2701 fn test_handle_initial_state() {
2702 let handle = LiveNodeHandle::new();
2703
2704 assert_eq!(handle.state(), NodeState::Idle);
2705 assert!(!handle.should_stop());
2706 assert!(!handle.is_running());
2707 }
2708
2709 #[rstest]
2710 fn test_handle_stop_sets_flag() {
2711 let handle = LiveNodeHandle::new();
2712
2713 handle.stop();
2714
2715 assert!(handle.should_stop());
2716 }
2717
2718 #[rstest]
2719 fn test_handle_set_state_running_clears_stop_flag() {
2720 let handle = LiveNodeHandle::new();
2721 handle.stop();
2722 assert!(handle.should_stop());
2723
2724 handle.set_state(NodeState::Running);
2725
2726 assert!(!handle.should_stop());
2727 assert!(handle.is_running());
2728 assert_eq!(handle.state(), NodeState::Running);
2729 }
2730
2731 #[rstest]
2732 fn test_handle_node_state_transitions() {
2733 let handle = LiveNodeHandle::new();
2734 assert_eq!(handle.state(), NodeState::Idle);
2735
2736 handle.set_state(NodeState::Starting);
2737 assert_eq!(handle.state(), NodeState::Starting);
2738 assert!(!handle.is_running());
2739
2740 handle.set_state(NodeState::Running);
2741 assert_eq!(handle.state(), NodeState::Running);
2742 assert!(handle.is_running());
2743
2744 handle.set_state(NodeState::ShuttingDown);
2745 assert_eq!(handle.state(), NodeState::ShuttingDown);
2746 assert!(!handle.is_running());
2747
2748 handle.set_state(NodeState::Stopped);
2749 assert_eq!(handle.state(), NodeState::Stopped);
2750 assert!(!handle.is_running());
2751 }
2752
2753 #[rstest]
2754 fn test_handle_clone_shares_state_bidirectionally() {
2755 let handle1 = LiveNodeHandle::new();
2756 let handle2 = handle1.clone();
2757
2758 handle1.stop();
2760 assert!(handle2.should_stop());
2761
2762 handle2.set_state(NodeState::Running);
2764 assert_eq!(handle1.state(), NodeState::Running);
2765 }
2766
2767 #[rstest]
2768 fn test_handle_stop_flag_independent_of_state() {
2769 let handle = LiveNodeHandle::new();
2770
2771 handle.set_state(NodeState::Starting);
2773 handle.stop();
2774 assert!(handle.should_stop());
2775 assert_eq!(handle.state(), NodeState::Starting);
2776
2777 handle.set_state(NodeState::ShuttingDown);
2779 assert!(handle.should_stop()); handle.set_state(NodeState::Running);
2782 assert!(!handle.should_stop()); }
2784
2785 #[rstest]
2786 fn test_builder_creation() {
2787 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox);
2788
2789 assert!(result.is_ok());
2790 }
2791
2792 #[rstest]
2793 fn test_builder_rejects_backtest() {
2794 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Backtest);
2795
2796 assert!(result.is_err());
2797 assert!(result.unwrap_err().to_string().contains("Backtest"));
2798 }
2799
2800 #[rstest]
2801 fn test_builder_accepts_live_environment() {
2802 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Live);
2803
2804 assert!(result.is_ok());
2805 }
2806
2807 #[rstest]
2808 fn test_builder_accepts_sandbox_environment() {
2809 let result = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox);
2810
2811 assert!(result.is_ok());
2812 }
2813
2814 #[rstest]
2815 fn test_builder_fluent_api_chaining() {
2816 let builder = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Live)
2817 .unwrap()
2818 .with_name("TestNode")
2819 .with_instance_id(UUID4::new())
2820 .with_load_state(false)
2821 .with_save_state(true)
2822 .with_timeout_connection(30)
2823 .with_timeout_reconciliation(60)
2824 .with_reconciliation(true)
2825 .with_reconciliation_lookback_mins(120)
2826 .with_timeout_portfolio(10)
2827 .with_timeout_disconnection_secs(5)
2828 .with_delay_post_stop_secs(3)
2829 .with_delay_shutdown_secs(10);
2830
2831 assert_eq!(builder.name(), "TestNode");
2832 }
2833
2834 #[rstest]
2835 fn test_builder_with_external_msgbus_egress_uses_configured_encoding() {
2836 let (external_egress, publications, closed) = CapturingExternalEgress::new();
2837 let msgbus_config = MessageBusConfig {
2838 encoding: SerializationEncoding::Json,
2839 ..Default::default()
2840 };
2841 let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
2842 .unwrap()
2843 .with_msgbus_config(msgbus_config)
2844 .with_external_msgbus_egress(Box::new(external_egress))
2845 .build()
2846 .expect("node builds with external message bus egress");
2847 let quote = QuoteTick::default();
2848
2849 msgbus::publish_quote("data.quotes.TEST".into(), "e);
2850
2851 let publications = publications.borrow();
2852 assert_eq!(publications.len(), 1);
2853 assert_eq!(publications[0].topic, "data.quotes.TEST");
2854 assert_eq!(
2855 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
2856 .expect("JSON payload must decode as QuoteTick"),
2857 quote
2858 );
2859 drop(publications);
2860
2861 msgbus::get_message_bus().borrow_mut().dispose();
2862 assert!(closed.get());
2863 drop(node);
2864 }
2865
2866 #[rstest]
2867 #[tokio::test(flavor = "current_thread")]
2868 async fn test_builder_with_external_msgbus_factory_installs_egress_and_ingress() {
2869 let quote = QuoteTick::default();
2870 let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
2871 let publications = Arc::new(Mutex::new(Vec::new()));
2872 let closed = Arc::new(AtomicBool::new(false));
2873 let factory = CapturingBackingFactory::new(publications.clone(), closed.clone(), Some(rx));
2874 let msgbus_config = MessageBusConfig {
2875 external_streams: Some(vec!["stream".to_string()]),
2876 ..Default::default()
2877 };
2878 let config = LiveNodeConfig {
2879 environment: Environment::Sandbox,
2880 msgbus: Some(msgbus_config),
2881 exec_engine: crate::config::LiveExecEngineConfig {
2882 reconciliation: false,
2883 ..Default::default()
2884 },
2885 delay_post_stop: Duration::ZERO,
2886 timeout_connection: Duration::from_millis(500),
2887 timeout_disconnection: Duration::from_millis(500),
2888 ..Default::default()
2889 };
2890 let mut node = LiveNodeBuilder::from_config(config)
2891 .unwrap()
2892 .with_external_msgbus_factory(Box::new(factory))
2893 .build()
2894 .expect("node builds with external message bus factory");
2895
2896 msgbus::publish_quote("data.quotes.TEST".into(), "e);
2897 {
2898 let publications = publications.lock().unwrap();
2899 assert_eq!(publications.len(), 1);
2900 assert_eq!(publications[0].topic, "data.quotes.TEST");
2901 assert_eq!(
2902 serde_json::from_slice::<QuoteTick>(&publications[0].payload)
2903 .expect("JSON payload must decode as QuoteTick"),
2904 quote
2905 );
2906 }
2907
2908 let received = Rc::new(RefCell::new(Vec::<QuoteTick>::new()));
2909 let handle = node.handle();
2910 let handler = TypedHandler::from({
2911 let received = received.clone();
2912 let handle = handle.clone();
2913 move |quote: &QuoteTick| {
2914 received.borrow_mut().push(*quote);
2915 handle.stop();
2916 }
2917 });
2918 msgbus::subscribe_quotes("data.quotes.*".into(), handler, None);
2919 msgbus::get_message_bus()
2920 .borrow_mut()
2921 .add_streaming_type(BusPayloadType::QuoteTick);
2922
2923 let payload =
2924 Bytes::from(serde_json::to_vec("e).expect("QuoteTick should serialize as JSON"));
2925 let message = BusMessage::with_str_topic(
2926 "data.quotes.TEST",
2927 BusPayloadType::QuoteTick,
2928 payload,
2929 SerializationEncoding::Json,
2930 );
2931
2932 tokio::time::timeout(Duration::from_secs(5), async {
2933 let run = node.run();
2934 tokio::pin!(run);
2935
2936 let drive = async {
2937 for _ in 0..100 {
2938 if handle.is_running() {
2939 break;
2940 }
2941 tokio::time::sleep(Duration::from_millis(10)).await;
2942 }
2943 assert!(handle.is_running(), "node should reach running state");
2944
2945 tx.send(message)
2946 .await
2947 .expect("external ingress receiver should be open");
2948
2949 for _ in 0..100 {
2950 if received.borrow().len() == 1 {
2951 break;
2952 }
2953 tokio::time::sleep(Duration::from_millis(10)).await;
2954 }
2955 assert_eq!(*received.borrow(), vec![quote]);
2956 };
2957
2958 tokio::select! {
2959 biased;
2960
2961 () = drive => {}
2962 result = &mut run => {
2963 panic!("node stopped before factory ingress was republished: {result:?}");
2964 }
2965 }
2966
2967 run.await.expect("node should stop cleanly");
2968 })
2969 .await
2970 .expect("live node should republish factory ingress and stop before timeout");
2971
2972 assert_eq!(handle.state(), NodeState::Stopped);
2973 assert!(closed.load(Ordering::Relaxed));
2974 msgbus::get_message_bus().borrow_mut().dispose();
2975 }
2976
2977 #[rstest]
2978 #[tokio::test(flavor = "current_thread")]
2979 async fn test_builder_with_external_msgbus_factory_without_streams_runs_without_ingress() {
2980 let quote = QuoteTick::default();
2981 let publications = Arc::new(Mutex::new(Vec::new()));
2982 let closed = Arc::new(AtomicBool::new(false));
2983 let factory = CapturingBackingFactory::new(publications.clone(), closed.clone(), None);
2984 let config = LiveNodeConfig {
2985 environment: Environment::Sandbox,
2986 msgbus: Some(MessageBusConfig::default()),
2987 exec_engine: crate::config::LiveExecEngineConfig {
2988 reconciliation: false,
2989 ..Default::default()
2990 },
2991 delay_post_stop: Duration::ZERO,
2992 timeout_connection: Duration::from_millis(500),
2993 timeout_disconnection: Duration::from_millis(500),
2994 ..Default::default()
2995 };
2996 let mut node = LiveNodeBuilder::from_config(config)
2997 .unwrap()
2998 .with_external_msgbus_factory(Box::new(factory))
2999 .build()
3000 .expect("node builds with egress-only message bus factory");
3001 let handle = node.handle();
3002
3003 msgbus::publish_quote("data.quotes.TEST".into(), "e);
3004 {
3005 let publications = publications.lock().unwrap();
3006 assert_eq!(publications.len(), 1);
3007 assert_eq!(publications[0].topic, "data.quotes.TEST");
3008 }
3009
3010 tokio::time::timeout(Duration::from_secs(5), async {
3011 let run = node.run();
3012 tokio::pin!(run);
3013
3014 let drive = async {
3015 for _ in 0..100 {
3016 if handle.is_running() {
3017 break;
3018 }
3019 tokio::time::sleep(Duration::from_millis(10)).await;
3020 }
3021 assert!(handle.is_running(), "node should reach running state");
3022 handle.stop();
3023 };
3024
3025 tokio::select! {
3026 biased;
3027
3028 () = drive => {}
3029 result = &mut run => {
3030 panic!("node stopped before egress-only factory run was observed: {result:?}");
3031 }
3032 }
3033
3034 run.await.expect("node should stop cleanly");
3035 })
3036 .await
3037 .expect("live node should run without external ingress before timeout");
3038
3039 assert_eq!(handle.state(), NodeState::Stopped);
3040 msgbus::get_message_bus().borrow_mut().dispose();
3041 assert!(closed.load(Ordering::Relaxed));
3042 }
3043
3044 #[rstest]
3045 fn test_builder_with_external_msgbus_factory_rejects_injected_surfaces() {
3046 let (external_egress, _publications, _closed) = CapturingExternalEgress::new();
3047 let egress_factory = CapturingBackingFactory::new(
3048 Arc::new(Mutex::new(Vec::new())),
3049 Arc::new(AtomicBool::new(false)),
3050 None,
3051 );
3052 let egress_error = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3053 .unwrap()
3054 .with_external_msgbus_factory(Box::new(egress_factory))
3055 .with_external_msgbus_egress(Box::new(external_egress))
3056 .build()
3057 .expect_err("builder should reject factory plus injected egress");
3058
3059 assert!(
3060 egress_error
3061 .to_string()
3062 .contains("cannot be combined with injected egress or ingress")
3063 );
3064
3065 let (_tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
3066 let ingress_factory = CapturingBackingFactory::new(
3067 Arc::new(Mutex::new(Vec::new())),
3068 Arc::new(AtomicBool::new(false)),
3069 None,
3070 );
3071 let ingress = CapturingExternalIngress::new(rx, Rc::new(Cell::new(false)));
3072 let ingress_error = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3073 .unwrap()
3074 .with_external_msgbus_factory(Box::new(ingress_factory))
3075 .with_external_ingress(Box::new(ingress))
3076 .build()
3077 .expect_err("builder should reject factory plus injected ingress");
3078
3079 assert!(
3080 ingress_error
3081 .to_string()
3082 .contains("cannot be combined with injected egress or ingress")
3083 );
3084 }
3085
3086 #[rstest]
3087 #[tokio::test(flavor = "current_thread")]
3088 async fn test_run_republishes_external_ingress_on_local_msgbus() {
3089 let quote = QuoteTick::default();
3090 let received = Rc::new(RefCell::new(Vec::<QuoteTick>::new()));
3091 let payload =
3092 Bytes::from(serde_json::to_vec("e).expect("QuoteTick should serialize as JSON"));
3093 let message = BusMessage::with_str_topic(
3094 "data.quotes.TEST",
3095 BusPayloadType::QuoteTick,
3096 payload,
3097 SerializationEncoding::Json,
3098 );
3099 let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
3100 let closed = Rc::new(Cell::new(false));
3101 let ingress = CapturingExternalIngress::new(rx, closed.clone());
3102 let config = LiveNodeConfig {
3103 environment: Environment::Sandbox,
3104 exec_engine: crate::config::LiveExecEngineConfig {
3105 reconciliation: false,
3106 ..Default::default()
3107 },
3108 delay_post_stop: Duration::ZERO,
3109 timeout_connection: Duration::from_millis(500),
3110 timeout_disconnection: Duration::from_millis(500),
3111 ..Default::default()
3112 };
3113 let mut node = LiveNodeBuilder::from_config(config)
3114 .unwrap()
3115 .with_external_ingress(Box::new(ingress))
3116 .build()
3117 .expect("node builds with external message bus ingress");
3118 let handle = node.handle();
3119 let handler = TypedHandler::from({
3120 let received = received.clone();
3121 move |quote: &QuoteTick| {
3122 received.borrow_mut().push(*quote);
3123 }
3124 });
3125 msgbus::subscribe_quotes("data.quotes.*".into(), handler, None);
3126 msgbus::get_message_bus()
3127 .borrow_mut()
3128 .add_streaming_type(BusPayloadType::QuoteTick);
3129
3130 tokio::time::timeout(Duration::from_secs(5), async {
3131 let run = node.run();
3132 tokio::pin!(run);
3133
3134 let drive = async {
3135 for _ in 0..100 {
3136 if handle.is_running() {
3137 break;
3138 }
3139 tokio::time::sleep(Duration::from_millis(10)).await;
3140 }
3141 assert!(handle.is_running(), "node should reach running state");
3142
3143 tx.send(message)
3144 .await
3145 .expect("external ingress receiver should be open");
3146
3147 for _ in 0..100 {
3148 if received.borrow().len() == 1 {
3149 break;
3150 }
3151 tokio::time::sleep(Duration::from_millis(10)).await;
3152 }
3153 assert_eq!(*received.borrow(), vec![quote]);
3154 handle.stop();
3155 };
3156
3157 tokio::select! {
3158 biased;
3159
3160 () = drive => {}
3161 result = &mut run => {
3162 panic!("node stopped before external message was republished: {result:?}");
3163 }
3164 }
3165
3166 run.await.expect("node should stop cleanly");
3167 })
3168 .await
3169 .expect("live node should republish ingress and stop before timeout");
3170
3171 assert_eq!(handle.state(), NodeState::Stopped);
3172 assert!(closed.get());
3173 msgbus::get_message_bus().borrow_mut().dispose();
3174 }
3175
3176 #[rstest]
3177 #[tokio::test(flavor = "current_thread")]
3178 async fn test_run_closes_external_ingress_when_receiver_closes() {
3179 let (tx, rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
3180 let closed = Rc::new(Cell::new(false));
3181 let ingress = CapturingExternalIngress::new(rx, closed.clone());
3182 let config = LiveNodeConfig {
3183 environment: Environment::Sandbox,
3184 exec_engine: crate::config::LiveExecEngineConfig {
3185 reconciliation: false,
3186 ..Default::default()
3187 },
3188 delay_post_stop: Duration::ZERO,
3189 timeout_connection: Duration::from_millis(500),
3190 timeout_disconnection: Duration::from_millis(500),
3191 ..Default::default()
3192 };
3193 let mut node = LiveNodeBuilder::from_config(config)
3194 .unwrap()
3195 .with_external_ingress(Box::new(ingress))
3196 .build()
3197 .expect("node builds with external message bus ingress");
3198 let handle = node.handle();
3199
3200 tokio::time::timeout(Duration::from_secs(5), async {
3201 let run = node.run();
3202 tokio::pin!(run);
3203
3204 let drive = async {
3205 for _ in 0..100 {
3206 if handle.is_running() {
3207 break;
3208 }
3209 tokio::time::sleep(Duration::from_millis(10)).await;
3210 }
3211 assert!(handle.is_running(), "node should reach running state");
3212
3213 drop(tx);
3214
3215 for _ in 0..100 {
3216 if closed.get() {
3217 break;
3218 }
3219 tokio::time::sleep(Duration::from_millis(10)).await;
3220 }
3221 assert!(closed.get(), "external ingress should close");
3222 assert!(
3223 handle.is_running(),
3224 "node should keep running after ingress closes"
3225 );
3226 handle.stop();
3227 };
3228
3229 tokio::select! {
3230 biased;
3231
3232 () = drive => {}
3233 result = &mut run => {
3234 panic!("node stopped before ingress close was observed: {result:?}");
3235 }
3236 }
3237
3238 run.await.expect("node should stop cleanly");
3239 })
3240 .await
3241 .expect("live node should close ingress and stop before timeout");
3242
3243 assert_eq!(handle.state(), NodeState::Stopped);
3244 }
3245
3246 #[rstest]
3247 #[tokio::test(flavor = "current_thread")]
3248 async fn test_run_aborts_startup_when_external_ingress_receiver_unavailable() {
3249 let closed = Rc::new(Cell::new(false));
3250 let ingress = FailingExternalIngress::new(closed.clone());
3251 let config = LiveNodeConfig {
3252 environment: Environment::Sandbox,
3253 exec_engine: crate::config::LiveExecEngineConfig {
3254 reconciliation: false,
3255 ..Default::default()
3256 },
3257 delay_post_stop: Duration::ZERO,
3258 timeout_connection: Duration::from_millis(500),
3259 timeout_disconnection: Duration::from_millis(500),
3260 ..Default::default()
3261 };
3262 let mut node = LiveNodeBuilder::from_config(config)
3263 .unwrap()
3264 .with_external_ingress(Box::new(ingress))
3265 .build()
3266 .expect("node builds with external message bus ingress");
3267 let handle = node.handle();
3268
3269 let err = node.run().await.expect_err("run should fail");
3270
3271 assert!(
3272 err.to_string()
3273 .contains("external ingress receiver unavailable")
3274 );
3275 assert_eq!(handle.state(), NodeState::Stopped);
3276 assert!(closed.get());
3277 }
3278
3279 #[cfg(feature = "python")]
3280 #[rstest]
3281 fn test_node_build_and_initial_state() {
3282 let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3283 .unwrap()
3284 .with_name("TestNode")
3285 .build()
3286 .unwrap();
3287
3288 assert_eq!(node.state(), NodeState::Idle);
3289 assert!(!node.is_running());
3290 assert_eq!(node.environment(), Environment::Sandbox);
3291 assert_eq!(node.trader_id(), TraderId::from("TRADER-001"));
3292 }
3293
3294 #[cfg(feature = "python")]
3295 #[rstest]
3296 fn test_node_build_replaces_stale_runner_senders() {
3297 replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
3298 replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
3299
3300 let first = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3301 .unwrap()
3302 .with_name("FirstNode")
3303 .build()
3304 .unwrap();
3305
3306 assert_eq!(first.state(), NodeState::Idle);
3307 drop(first);
3308
3309 let second = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3310 .unwrap()
3311 .with_name("SecondNode")
3312 .build()
3313 .unwrap();
3314
3315 assert_eq!(second.state(), NodeState::Idle);
3316 assert!(!second.is_running());
3317 }
3318
3319 #[cfg(feature = "python")]
3320 #[rstest]
3321 fn test_node_handle_reflects_node_state() {
3322 let node = LiveNode::builder(TraderId::from("TRADER-001"), Environment::Sandbox)
3323 .unwrap()
3324 .with_name("TestNode")
3325 .build()
3326 .unwrap();
3327
3328 let handle = node.handle();
3329
3330 assert_eq!(handle.state(), NodeState::Idle);
3331 assert!(!handle.is_running());
3332 }
3333
3334 #[rstest]
3335 fn test_pending_drain_data_returns_false_when_empty() {
3336 let mut pending = PendingEvents::default();
3337
3338 assert!(!pending.drain_data());
3339 }
3340
3341 #[rstest]
3342 fn test_pending_drain_data_returns_true_when_non_empty() {
3343 use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
3344
3345 let mut pending = PendingEvents::default();
3346 pending
3347 .data_evts
3348 .push(DataEvent::Instrument(InstrumentAny::CryptoPerpetual(
3349 crypto_perpetual_ethusdt(),
3350 )));
3351
3352 assert!(pending.drain_data());
3353 assert!(pending.data_evts.is_empty());
3354 }
3355
3356 fn stub_data_event() -> DataEvent {
3357 use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
3358
3359 DataEvent::Instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()))
3360 }
3361
3362 fn stub_data_command() -> DataCommand {
3363 use nautilus_common::messages::data::{SubscribeCommand, subscribe::SubscribeInstruments};
3364 use nautilus_core::{UUID4, UnixNanos};
3365 use nautilus_model::identifiers::Venue;
3366
3367 DataCommand::Subscribe(SubscribeCommand::Instruments(SubscribeInstruments::new(
3368 None,
3369 Venue::from("TEST"),
3370 UUID4::new(),
3371 UnixNanos::default(),
3372 None,
3373 None,
3374 )))
3375 }
3376
3377 #[rstest]
3378 fn test_flush_pending_data_drains_events_and_commands() {
3379 let (evt_tx, mut evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3380 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3381
3382 let mut pending = PendingEvents::default();
3383
3384 pending.data_evts.push(stub_data_event());
3386 pending.data_cmds.push(stub_data_command());
3387
3388 evt_tx.send(stub_data_event()).unwrap();
3390 cmd_tx.send(stub_data_command()).unwrap();
3391
3392 flush_pending_data(&mut pending, &mut evt_rx, &mut cmd_rx);
3393
3394 assert!(pending.data_evts.is_empty());
3395 assert!(pending.data_cmds.is_empty());
3396 assert!(evt_rx.try_recv().is_err());
3397 assert!(cmd_rx.try_recv().is_err());
3398 }
3399
3400 #[rstest]
3401 fn test_flush_pending_data_drains_mixed_sources() {
3402 let (evt_tx, mut evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3403 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3404
3405 let mut pending = PendingEvents::default();
3406
3407 pending.data_evts.push(stub_data_event());
3409 cmd_tx.send(stub_data_command()).unwrap();
3410
3411 evt_tx.send(stub_data_event()).unwrap();
3413 evt_tx.send(stub_data_event()).unwrap();
3414 cmd_tx.send(stub_data_command()).unwrap();
3415
3416 flush_pending_data(&mut pending, &mut evt_rx, &mut cmd_rx);
3417
3418 assert!(pending.data_evts.is_empty());
3419 assert!(pending.data_cmds.is_empty());
3420 assert!(evt_rx.try_recv().is_err());
3421 assert!(cmd_rx.try_recv().is_err());
3422 }
3423
3424 fn stub_time_event_handler() -> TimeEventHandler {
3425 use std::rc::Rc;
3426
3427 use nautilus_common::timer::{TimeEvent, TimeEventCallback, TimeEventHandler};
3428 use nautilus_core::{UUID4, UnixNanos};
3429 use ustr::Ustr;
3430
3431 TimeEventHandler::new(
3432 TimeEvent::new(
3433 Ustr::from("test-timer"),
3434 UUID4::new(),
3435 UnixNanos::default(),
3436 UnixNanos::default(),
3437 ),
3438 TimeEventCallback::RustLocal(Rc::new(|_| {})),
3439 )
3440 }
3441
3442 fn stub_trading_command() -> TradingCommand {
3443 use nautilus_common::messages::execution::query::QueryAccount;
3444 use nautilus_core::{UUID4, UnixNanos};
3445 use nautilus_model::identifiers::AccountId;
3446
3447 TradingCommand::QueryAccount(QueryAccount::new(
3448 TraderId::from("TESTER-001"),
3449 None,
3450 AccountId::from("TEST-001"),
3451 UUID4::new(),
3452 UnixNanos::default(),
3453 None,
3454 None, ))
3456 }
3457
3458 fn stub_exec_event() -> ExecutionEvent {
3459 use nautilus_model::{
3460 enums::{LiquiditySide, OrderSide},
3461 identifiers::{AccountId, InstrumentId, TradeId, VenueOrderId},
3462 reports::FillReport,
3463 types::{Money, Price, Quantity},
3464 };
3465
3466 ExecutionEvent::Report(ExecutionReport::Fill(Box::new(FillReport::new(
3467 AccountId::from("TEST-001"),
3468 InstrumentId::from("TEST.VENUE"),
3469 VenueOrderId::from("V-001"),
3470 TradeId::from("T-001"),
3471 OrderSide::Buy,
3472 Quantity::from("1.0"),
3473 Price::from("100.0"),
3474 Money::from("0.01 USD"),
3475 LiquiditySide::Maker,
3476 None,
3477 None,
3478 nautilus_core::UnixNanos::default(),
3479 nautilus_core::UnixNanos::default(),
3480 None,
3481 ))))
3482 }
3483
3484 #[rstest]
3485 fn test_flush_all_pending_drains_buffered_channels() {
3486 let (time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3487 let (data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3488 let (data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3489 let (exec_evt_tx, mut exec_evt_rx) =
3490 tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3491 let (exec_cmd_tx, mut exec_cmd_rx) =
3492 tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3493
3494 let mut pending = PendingEvents::default();
3495
3496 pending.data_evts.push(stub_data_event());
3498 pending.data_cmds.push(stub_data_command());
3499
3500 time_tx.send(stub_time_event_handler()).unwrap();
3502 data_evt_tx.send(stub_data_event()).unwrap();
3503 data_cmd_tx.send(stub_data_command()).unwrap();
3504 exec_evt_tx.send(stub_exec_event()).unwrap();
3505 exec_cmd_tx.send(stub_trading_command()).unwrap();
3506
3507 flush_all_pending(
3508 &mut pending,
3509 &mut time_rx,
3510 &mut data_evt_rx,
3511 &mut data_cmd_rx,
3512 &mut exec_evt_rx,
3513 &mut exec_cmd_rx,
3514 );
3515
3516 assert!(pending.data_evts.is_empty());
3517 assert!(pending.data_cmds.is_empty());
3518 assert!(pending.exec_reports.is_empty());
3519 assert!(pending.exec_cmds.is_empty());
3520 assert!(pending.order_evts.is_empty());
3521 assert!(time_rx.try_recv().is_err());
3522 assert!(data_evt_rx.try_recv().is_err());
3523 assert!(data_cmd_rx.try_recv().is_err());
3524 assert!(exec_evt_rx.try_recv().is_err());
3525 assert!(exec_cmd_rx.try_recv().is_err());
3526 }
3527
3528 fn stub_order_event() -> ExecutionEvent {
3529 use nautilus_model::events::order::spec::OrderSubmittedSpec;
3530
3531 ExecutionEvent::Order(OrderEventAny::Submitted(
3532 OrderSubmittedSpec::builder().build(),
3533 ))
3534 }
3535
3536 fn stub_account_event() -> ExecutionEvent {
3537 use nautilus_core::{UUID4, UnixNanos};
3538 use nautilus_model::{
3539 enums::AccountType, events::account::state::AccountState, identifiers::AccountId,
3540 };
3541
3542 ExecutionEvent::Account(AccountState::new(
3543 AccountId::from("TEST-001"),
3544 AccountType::Cash,
3545 vec![],
3546 vec![],
3547 true,
3548 UUID4::new(),
3549 UnixNanos::default(),
3550 UnixNanos::default(),
3551 None,
3552 ))
3553 }
3554
3555 #[rstest]
3556 fn test_flush_all_pending_routes_order_event_to_order_evts() {
3557 let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3558 let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3559 let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3560 let (exec_evt_tx, mut exec_evt_rx) =
3561 tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3562 let (_exec_cmd_tx, mut exec_cmd_rx) =
3563 tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3564
3565 let mut pending = PendingEvents::default();
3566
3567 exec_evt_tx.send(stub_order_event()).unwrap();
3568 exec_evt_tx.send(stub_exec_event()).unwrap();
3569
3570 flush_all_pending(
3571 &mut pending,
3572 &mut time_rx,
3573 &mut data_evt_rx,
3574 &mut data_cmd_rx,
3575 &mut exec_evt_rx,
3576 &mut exec_cmd_rx,
3577 );
3578
3579 assert!(pending.order_evts.is_empty());
3581 assert!(pending.exec_reports.is_empty());
3582 assert!(exec_evt_rx.try_recv().is_err());
3583 }
3584
3585 #[rstest]
3586 fn test_flush_all_pending_routes_account_event_immediately() {
3587 let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3588 let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3589 let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3590 let (exec_evt_tx, mut exec_evt_rx) =
3591 tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3592 let (_exec_cmd_tx, mut exec_cmd_rx) =
3593 tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3594
3595 let mut pending = PendingEvents::default();
3596
3597 exec_evt_tx.send(stub_account_event()).unwrap();
3598
3599 flush_all_pending(
3600 &mut pending,
3601 &mut time_rx,
3602 &mut data_evt_rx,
3603 &mut data_cmd_rx,
3604 &mut exec_evt_rx,
3605 &mut exec_cmd_rx,
3606 );
3607
3608 assert!(pending.exec_reports.is_empty());
3610 assert!(pending.order_evts.is_empty());
3611 assert!(pending.exec_cmds.is_empty());
3612 assert!(exec_evt_rx.try_recv().is_err());
3613 }
3614
3615 #[rstest]
3616 fn test_pending_is_empty_when_default() {
3617 let pending = PendingEvents::default();
3618
3619 assert!(pending.is_empty());
3620 }
3621
3622 #[rstest]
3623 fn test_pending_is_empty_false_with_data_evt() {
3624 let mut pending = PendingEvents::default();
3625 pending.data_evts.push(stub_data_event());
3626
3627 assert!(!pending.is_empty());
3628 }
3629
3630 #[rstest]
3631 fn test_pending_is_empty_false_with_data_cmd() {
3632 let mut pending = PendingEvents::default();
3633 pending.data_cmds.push(stub_data_command());
3634
3635 assert!(!pending.is_empty());
3636 }
3637
3638 #[rstest]
3639 fn test_pending_is_empty_false_with_exec_cmd() {
3640 let mut pending = PendingEvents::default();
3641 pending.exec_cmds.push(stub_trading_command());
3642
3643 assert!(!pending.is_empty());
3644 }
3645
3646 #[rstest]
3647 fn test_pending_is_empty_false_with_exec_report() {
3648 let mut pending = PendingEvents::default();
3649
3650 if let ExecutionEvent::Report(report) = stub_exec_event() {
3651 pending.exec_reports.push(report);
3652 }
3653
3654 assert!(!pending.is_empty());
3655 }
3656
3657 #[rstest]
3658 fn test_pending_is_empty_false_with_order_evt() {
3659 let mut pending = PendingEvents::default();
3660
3661 if let ExecutionEvent::Order(order_evt) = stub_order_event() {
3662 pending.order_evts.push(order_evt);
3663 }
3664
3665 assert!(!pending.is_empty());
3666 }
3667
3668 fn stub_submitted_batch_event() -> ExecutionEvent {
3669 use nautilus_model::{
3670 events::{OrderSubmittedBatch, order::spec::OrderSubmittedSpec},
3671 identifiers::ClientOrderId,
3672 };
3673
3674 let events = vec![
3675 OrderSubmittedSpec::builder()
3676 .client_order_id(ClientOrderId::from("O-001"))
3677 .build(),
3678 OrderSubmittedSpec::builder()
3679 .client_order_id(ClientOrderId::from("O-002"))
3680 .build(),
3681 ];
3682
3683 ExecutionEvent::OrderSubmittedBatch(OrderSubmittedBatch::new(events))
3684 }
3685
3686 fn stub_canceled_batch_event() -> ExecutionEvent {
3687 use nautilus_model::{
3688 events::{OrderCanceledBatch, order::spec::OrderCanceledSpec},
3689 identifiers::ClientOrderId,
3690 };
3691
3692 let events = vec![
3693 OrderCanceledSpec::builder()
3694 .client_order_id(ClientOrderId::from("O-001"))
3695 .build(),
3696 OrderCanceledSpec::builder()
3697 .client_order_id(ClientOrderId::from("O-002"))
3698 .build(),
3699 ];
3700
3701 ExecutionEvent::OrderCanceledBatch(OrderCanceledBatch::new(events))
3702 }
3703
3704 #[rstest]
3705 fn test_flush_all_pending_buffers_submitted_batch_as_individual_events() {
3706 let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3707 let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3708 let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3709 let (exec_evt_tx, mut exec_evt_rx) =
3710 tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3711 let (_exec_cmd_tx, mut exec_cmd_rx) =
3712 tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3713
3714 let mut pending = PendingEvents::default();
3715
3716 exec_evt_tx.send(stub_submitted_batch_event()).unwrap();
3717
3718 flush_all_pending(
3719 &mut pending,
3720 &mut time_rx,
3721 &mut data_evt_rx,
3722 &mut data_cmd_rx,
3723 &mut exec_evt_rx,
3724 &mut exec_cmd_rx,
3725 );
3726
3727 assert!(pending.order_evts.is_empty());
3729 assert!(exec_evt_rx.try_recv().is_err());
3730 }
3731
3732 #[rstest]
3733 fn test_flush_all_pending_buffers_canceled_batch_as_individual_events() {
3734 let (_time_tx, mut time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
3735 let (_data_evt_tx, mut data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
3736 let (_data_cmd_tx, mut data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
3737 let (exec_evt_tx, mut exec_evt_rx) =
3738 tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3739 let (_exec_cmd_tx, mut exec_cmd_rx) =
3740 tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
3741
3742 let mut pending = PendingEvents::default();
3743
3744 exec_evt_tx.send(stub_canceled_batch_event()).unwrap();
3745
3746 flush_all_pending(
3747 &mut pending,
3748 &mut time_rx,
3749 &mut data_evt_rx,
3750 &mut data_cmd_rx,
3751 &mut exec_evt_rx,
3752 &mut exec_cmd_rx,
3753 );
3754
3755 assert!(pending.order_evts.is_empty());
3757 assert!(exec_evt_rx.try_recv().is_err());
3758 }
3759
3760 #[rstest]
3761 fn test_flush_all_pending_expands_batch_into_order_evts_before_drain() {
3762 use nautilus_model::identifiers::ClientOrderId;
3763
3764 let (exec_evt_tx, mut exec_evt_rx) =
3765 tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
3766
3767 exec_evt_tx.send(stub_canceled_batch_event()).unwrap();
3768
3769 let mut pending = PendingEvents::default();
3770
3771 while let Ok(evt) = exec_evt_rx.try_recv() {
3773 match evt {
3774 ExecutionEvent::Account(_) => {
3775 AsyncRunner::handle_exec_event(evt);
3776 }
3777 ExecutionEvent::Report(report) => {
3778 pending.exec_reports.push(report);
3779 }
3780 ExecutionEvent::Order(order_evt) => {
3781 pending.order_evts.push(order_evt);
3782 }
3783 ExecutionEvent::OrderSubmittedBatch(batch) => {
3784 for submitted in batch {
3785 pending.order_evts.push(OrderEventAny::Submitted(submitted));
3786 }
3787 }
3788 ExecutionEvent::OrderAcceptedBatch(batch) => {
3789 for accepted in batch {
3790 pending.order_evts.push(OrderEventAny::Accepted(accepted));
3791 }
3792 }
3793 ExecutionEvent::OrderCanceledBatch(batch) => {
3794 for canceled in batch {
3795 pending.order_evts.push(OrderEventAny::Canceled(canceled));
3796 }
3797 }
3798 }
3799 }
3800
3801 assert_eq!(pending.order_evts.len(), 2);
3802 assert!(
3803 matches!(&pending.order_evts[0], OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("O-001"))
3804 );
3805 assert!(
3806 matches!(&pending.order_evts[1], OrderEventAny::Canceled(c) if c.client_order_id == ClientOrderId::from("O-002"))
3807 );
3808 }
3809
3810 #[derive(Debug)]
3811 struct CapturedEgressMessage {
3812 topic: String,
3813 payload: Bytes,
3814 }
3815
3816 type CapturedEgressMessages = Rc<RefCell<Vec<CapturedEgressMessage>>>;
3817 type SharedClosed = Rc<Cell<bool>>;
3818
3819 #[derive(Debug)]
3820 struct CapturingExternalIngress {
3821 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3822 closed: SharedClosed,
3823 }
3824
3825 impl CapturingExternalIngress {
3826 fn new(rx: tokio::sync::mpsc::Receiver<BusMessage>, closed: SharedClosed) -> Self {
3827 Self {
3828 rx: Some(rx),
3829 closed,
3830 }
3831 }
3832 }
3833
3834 impl MessageBusExternalIngress for CapturingExternalIngress {
3835 fn is_closed(&self) -> bool {
3836 self.closed.get()
3837 }
3838
3839 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
3840 self.rx
3841 .take()
3842 .ok_or_else(|| anyhow::anyhow!("external ingress receiver already taken"))
3843 }
3844
3845 fn close(&mut self) {
3846 self.closed.set(true);
3847 }
3848 }
3849
3850 #[derive(Debug)]
3851 struct FailingExternalIngress {
3852 closed: SharedClosed,
3853 }
3854
3855 impl FailingExternalIngress {
3856 fn new(closed: SharedClosed) -> Self {
3857 Self { closed }
3858 }
3859 }
3860
3861 impl MessageBusExternalIngress for FailingExternalIngress {
3862 fn is_closed(&self) -> bool {
3863 self.closed.get()
3864 }
3865
3866 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
3867 anyhow::bail!("external ingress receiver unavailable")
3868 }
3869
3870 fn close(&mut self) {
3871 self.closed.set(true);
3872 }
3873 }
3874
3875 struct CapturingExternalEgress {
3876 publications: CapturedEgressMessages,
3877 closed: SharedClosed,
3878 }
3879
3880 impl CapturingExternalEgress {
3881 fn new() -> (Self, CapturedEgressMessages, SharedClosed) {
3882 let publications = Rc::new(RefCell::new(Vec::new()));
3883 let closed = Rc::new(Cell::new(false));
3884 (
3885 Self {
3886 publications: publications.clone(),
3887 closed: closed.clone(),
3888 },
3889 publications,
3890 closed,
3891 )
3892 }
3893 }
3894
3895 impl MessageBusExternalEgress for CapturingExternalEgress {
3896 fn is_closed(&self) -> bool {
3897 self.closed.get()
3898 }
3899
3900 fn publish(&self, message: BusMessage) {
3901 self.publications.borrow_mut().push(CapturedEgressMessage {
3902 topic: message.topic.to_string(),
3903 payload: message.payload,
3904 });
3905 }
3906
3907 fn close(&mut self) {
3908 self.closed.set(true);
3909 }
3910 }
3911
3912 struct CapturingBackingFactory {
3913 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
3914 closed: Arc<AtomicBool>,
3915 rx: Mutex<Option<tokio::sync::mpsc::Receiver<BusMessage>>>,
3916 }
3917
3918 impl CapturingBackingFactory {
3919 fn new(
3920 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
3921 closed: Arc<AtomicBool>,
3922 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3923 ) -> Self {
3924 Self {
3925 publications,
3926 closed,
3927 rx: Mutex::new(rx),
3928 }
3929 }
3930 }
3931
3932 impl Debug for CapturingBackingFactory {
3933 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3934 f.debug_struct(stringify!(CapturingBackingFactory))
3935 .finish_non_exhaustive()
3936 }
3937 }
3938
3939 impl MessageBusBackingFactory for CapturingBackingFactory {
3940 fn create(
3941 &self,
3942 _trader_id: TraderId,
3943 _instance_id: UUID4,
3944 _config: MessageBusConfig,
3945 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
3946 let rx = self.rx.lock().unwrap().take();
3947 Ok(Box::new(CapturingBacking {
3948 publications: self.publications.clone(),
3949 closed: self.closed.clone(),
3950 rx,
3951 }))
3952 }
3953 }
3954
3955 struct CapturingBacking {
3956 publications: Arc<Mutex<Vec<CapturedEgressMessage>>>,
3957 closed: Arc<AtomicBool>,
3958 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
3959 }
3960
3961 impl MessageBusBacking for CapturingBacking {
3962 fn is_closed(&self) -> bool {
3963 self.closed.load(Ordering::Relaxed)
3964 }
3965
3966 fn publish(&self, message: BusMessage) {
3967 self.publications
3968 .lock()
3969 .unwrap()
3970 .push(CapturedEgressMessage {
3971 topic: message.topic.to_string(),
3972 payload: message.payload,
3973 });
3974 }
3975
3976 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
3977 self.rx
3978 .take()
3979 .ok_or_else(|| anyhow::anyhow!("external ingress receiver unavailable"))
3980 }
3981
3982 fn close(&mut self) {
3983 self.closed.store(true, Ordering::Relaxed);
3984 }
3985 }
3986}