1use std::{
32 collections::{HashMap, VecDeque},
33 fmt::Debug,
34 sync::{
35 Arc,
36 atomic::{AtomicBool, Ordering},
37 },
38 time::Duration,
39};
40
41use bytes::Bytes;
42use futures::stream::Stream;
43use nautilus_common::{
44 enums::SerializationEncoding,
45 live::get_runtime,
46 logging::{log_task_error, log_task_started, log_task_stopped},
47 msgbus::{
48 BusMessage, BusPayloadType, MessageBusBacking, MessageBusBackingFactory, MessageBusConfig,
49 switchboard::CLOSE_TOPIC,
50 },
51};
52use nautilus_core::{
53 UUID4,
54 time::{duration_since_unix_epoch, get_atomic_clock_realtime},
55};
56use nautilus_cryptography::providers::install_cryptographic_provider;
57use nautilus_model::identifiers::TraderId;
58use redis::{AsyncCommands, RetryMethod, aio::ConnectionManager, streams};
59use serde::{Deserialize, Serialize};
60use streams::StreamReadOptions;
61use ustr::Ustr;
62
63use super::{REDIS_MINID, REDIS_XTRIM, await_handle};
64use crate::redis::{RedisConnectionConfig, create_redis_connection, get_stream_key};
65
66const MSGBUS_PUBLISH: &str = "msgbus-publish";
67const MSGBUS_STREAM: &str = "msgbus-stream";
68const MSGBUS_HEARTBEAT: &str = "msgbus-heartbeat";
69const HEARTBEAT_TOPIC: &str = "health:heartbeat";
70const TRIM_BUFFER_SECS: u64 = 60;
71
72type RedisStreamBulk = Vec<HashMap<String, Vec<HashMap<String, redis::Value>>>>;
73
74#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(default, deny_unknown_fields)]
79#[cfg_attr(
80 feature = "python",
81 pyo3::pyclass(
82 module = "nautilus_trader.core.nautilus_pyo3.infrastructure",
83 from_py_object
84 )
85)]
86#[cfg_attr(
87 feature = "python",
88 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.infrastructure")
89)]
90pub struct RedisMessageBusConfig {
91 pub host: Option<String>,
93 pub port: Option<u16>,
95 pub username: Option<String>,
97 pub password: Option<String>,
99 pub ssl: bool,
101 pub connection_timeout: u16,
103 pub response_timeout: u16,
105 pub number_of_retries: usize,
107 pub exponent_base: u64,
109 pub max_delay: u64,
111 pub factor: u64,
113}
114
115impl Debug for RedisMessageBusConfig {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 let redacted = self.password.as_ref().map(|_| "***");
118 f.debug_struct(stringify!(RedisMessageBusConfig))
119 .field("host", &self.host)
120 .field("port", &self.port)
121 .field("username", &self.username)
122 .field("password", &redacted)
123 .field("ssl", &self.ssl)
124 .field("connection_timeout", &self.connection_timeout)
125 .field("response_timeout", &self.response_timeout)
126 .field("number_of_retries", &self.number_of_retries)
127 .field("exponent_base", &self.exponent_base)
128 .field("max_delay", &self.max_delay)
129 .field("factor", &self.factor)
130 .finish()
131 }
132}
133
134impl Default for RedisMessageBusConfig {
135 fn default() -> Self {
136 Self {
137 host: None,
138 port: None,
139 username: None,
140 password: None,
141 ssl: false,
142 connection_timeout: 20,
143 response_timeout: 20,
144 number_of_retries: 100,
145 exponent_base: 2,
146 max_delay: 1000,
147 factor: 2,
148 }
149 }
150}
151
152impl RedisConnectionConfig for RedisMessageBusConfig {
153 fn host(&self) -> Option<&str> {
154 self.host.as_deref()
155 }
156
157 fn port(&self) -> Option<u16> {
158 self.port
159 }
160
161 fn username(&self) -> Option<&str> {
162 self.username.as_deref()
163 }
164
165 fn password(&self) -> Option<&str> {
166 self.password.as_deref()
167 }
168
169 fn ssl(&self) -> bool {
170 self.ssl
171 }
172
173 fn connection_timeout(&self) -> u16 {
174 self.connection_timeout
175 }
176
177 fn response_timeout(&self) -> u16 {
178 self.response_timeout
179 }
180
181 fn number_of_retries(&self) -> usize {
182 self.number_of_retries
183 }
184
185 fn exponent_base(&self) -> u64 {
186 self.exponent_base
187 }
188
189 fn max_delay(&self) -> u64 {
190 self.max_delay
191 }
192
193 fn factor(&self) -> u64 {
194 self.factor
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct RedisMessageBusFactory {
201 config: RedisMessageBusConfig,
202}
203
204impl RedisMessageBusFactory {
205 #[must_use]
207 pub const fn new(config: RedisMessageBusConfig) -> Self {
208 Self { config }
209 }
210}
211
212impl MessageBusBackingFactory for RedisMessageBusFactory {
213 fn create(
214 &self,
215 trader_id: TraderId,
216 instance_id: UUID4,
217 config: MessageBusConfig,
218 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
219 Ok(Box::new(RedisMessageBusBacking::new(
220 trader_id,
221 instance_id,
222 config,
223 self.config.clone(),
224 )?))
225 }
226}
227
228#[cfg_attr(
229 feature = "python",
230 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.infrastructure")
231)]
232pub struct RedisMessageBusBacking {
233 pub trader_id: TraderId,
235 pub instance_id: UUID4,
237 pub_tx: tokio::sync::mpsc::UnboundedSender<BusMessage>,
238 pub_handle: Option<tokio::task::JoinHandle<()>>,
239 stream_rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
240 stream_handle: Option<tokio::task::JoinHandle<()>>,
241 stream_signal: Arc<AtomicBool>,
242 heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
243 heartbeat_signal: Arc<AtomicBool>,
244}
245
246impl Debug for RedisMessageBusBacking {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct(stringify!(RedisMessageBusBacking))
249 .field("trader_id", &self.trader_id)
250 .field("instance_id", &self.instance_id)
251 .finish_non_exhaustive()
252 }
253}
254
255impl RedisMessageBusBacking {
256 pub fn new(
262 trader_id: TraderId,
263 instance_id: UUID4,
264 config: MessageBusConfig,
265 backing: RedisMessageBusConfig,
266 ) -> anyhow::Result<Self> {
267 install_cryptographic_provider();
268
269 if config.heartbeat_interval_secs == Some(0) {
270 anyhow::bail!("heartbeat_interval_secs must be greater than 0");
271 }
272
273 let external_streams = config.external_streams.clone().unwrap_or_default();
274 let heartbeat_interval_secs = config.heartbeat_interval_secs;
275 let publish = backing.clone();
276
277 let (pub_tx, pub_rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
278
279 let pub_handle = Some(get_runtime().spawn(async move {
281 if let Err(e) = publish_messages(pub_rx, trader_id, instance_id, config, publish).await
282 {
283 log_task_error(MSGBUS_PUBLISH, &e);
284 }
285 }));
286
287 let stream_signal = Arc::new(AtomicBool::new(false));
289 let (stream_rx, stream_handle) = if external_streams.is_empty() {
290 (None, None)
291 } else {
292 let stream_signal_clone = stream_signal.clone();
293 let (stream_tx, stream_rx) = tokio::sync::mpsc::channel::<BusMessage>(100_000);
294 (
295 Some(stream_rx),
296 Some(get_runtime().spawn(async move {
297 if let Err(e) = stream_messages(
298 stream_tx,
299 backing.clone(),
300 external_streams,
301 stream_signal_clone,
302 )
303 .await
304 {
305 log_task_error(MSGBUS_STREAM, &e);
306 }
307 })),
308 )
309 };
310
311 let heartbeat_signal = Arc::new(AtomicBool::new(false));
313 let heartbeat_handle = if let Some(heartbeat_interval_secs) = heartbeat_interval_secs {
314 let signal = heartbeat_signal.clone();
315 let pub_tx_clone = pub_tx.clone();
316
317 Some(get_runtime().spawn(async move {
318 run_heartbeat(heartbeat_interval_secs, signal, pub_tx_clone).await;
319 }))
320 } else {
321 None
322 };
323
324 Ok(Self {
325 trader_id,
326 instance_id,
327 pub_tx,
328 pub_handle,
329 stream_rx,
330 stream_handle,
331 stream_signal,
332 heartbeat_handle,
333 heartbeat_signal,
334 })
335 }
336}
337
338impl MessageBusBacking for RedisMessageBusBacking {
339 fn is_closed(&self) -> bool {
341 self.pub_tx.is_closed()
342 }
343
344 fn publish(&self, message: BusMessage) {
346 if let Err(e) = self.pub_tx.send(message) {
347 log::error!("Failed to send message: {e}");
348 }
349 }
350
351 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
352 self.get_stream_receiver()
353 }
354
355 fn close(&mut self) {
357 log::debug!("Closing");
358
359 self.stream_signal.store(true, Ordering::Relaxed);
360 self.heartbeat_signal.store(true, Ordering::Relaxed);
361
362 if !self.pub_tx.is_closed() {
363 let msg = BusMessage::new_close();
364
365 if let Err(e) = self.pub_tx.send(msg) {
366 log::warn!("Failed to send close message: {e:?}");
367 }
368 }
369
370 tokio::task::block_in_place(|| {
372 get_runtime().block_on(async {
373 self.close_async().await;
374 });
375 });
376
377 log::debug!("Closed");
378 }
379}
380
381impl RedisMessageBusBacking {
382 pub fn get_stream_receiver(
388 &mut self,
389 ) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
390 self.stream_rx
391 .take()
392 .ok_or_else(|| anyhow::anyhow!("Stream receiver already taken"))
393 }
394
395 pub fn stream(
397 mut stream_rx: tokio::sync::mpsc::Receiver<BusMessage>,
398 ) -> impl Stream<Item = BusMessage> + 'static {
399 async_stream::stream! {
400 while let Some(msg) = stream_rx.recv().await {
401 yield msg;
402 }
403 }
404 }
405
406 pub async fn close_async(&mut self) {
407 await_handle(self.pub_handle.take(), MSGBUS_PUBLISH).await;
408 await_handle(self.stream_handle.take(), MSGBUS_STREAM).await;
409 await_handle(self.heartbeat_handle.take(), MSGBUS_HEARTBEAT).await;
410 }
411}
412
413pub async fn publish_messages(
422 mut rx: tokio::sync::mpsc::UnboundedReceiver<BusMessage>,
423 trader_id: TraderId,
424 instance_id: UUID4,
425 config: MessageBusConfig,
426 backing: RedisMessageBusConfig,
427) -> anyhow::Result<()> {
428 log_task_started(MSGBUS_PUBLISH);
429
430 let mut con = create_redis_connection(MSGBUS_PUBLISH, &backing).await?;
431 let stream_key = get_stream_key(trader_id, instance_id, &config);
432
433 let autotrim_duration = config
435 .autotrim_mins
436 .filter(|&mins| mins > 0)
437 .map(|mins| Duration::from_secs(u64::from(mins) * 60));
438 let mut last_trim_index: HashMap<String, usize> = HashMap::new();
439
440 let mut buffer: VecDeque<BusMessage> = VecDeque::new();
442 let buffer_interval = Duration::from_millis(u64::from(config.buffer_interval_ms.unwrap_or(0)));
443
444 let flush_timer = tokio::time::sleep(buffer_interval);
448 tokio::pin!(flush_timer);
449
450 loop {
451 tokio::select! {
452 maybe_msg = rx.recv() => {
453 if let Some(msg) = maybe_msg {
454 if msg.topic == CLOSE_TOPIC {
455 log::debug!("Received close message");
456 if !buffer.is_empty() {
458 drain_buffer(
459 &mut con,
460 &stream_key,
461 config.stream_per_topic,
462 autotrim_duration,
463 &mut last_trim_index,
464 &mut buffer,
465 ).await?;
466 }
467 break;
468 }
469
470 buffer.push_back(msg);
471
472 if buffer_interval.is_zero() {
473 drain_buffer(
475 &mut con,
476 &stream_key,
477 config.stream_per_topic,
478 autotrim_duration,
479 &mut last_trim_index,
480 &mut buffer,
481 ).await?;
482 }
483 } else {
484 log::debug!("Channel hung up");
485 break;
486 }
487 }
488 () = &mut flush_timer, if !buffer_interval.is_zero() => {
491 if !buffer.is_empty() {
492 drain_buffer(
493 &mut con,
494 &stream_key,
495 config.stream_per_topic,
496 autotrim_duration,
497 &mut last_trim_index,
498 &mut buffer,
499 ).await?;
500 }
501
502 flush_timer.as_mut().reset(tokio::time::Instant::now() + buffer_interval);
504 }
505 }
506 }
507
508 if !buffer.is_empty() {
510 drain_buffer(
511 &mut con,
512 &stream_key,
513 config.stream_per_topic,
514 autotrim_duration,
515 &mut last_trim_index,
516 &mut buffer,
517 )
518 .await?;
519 }
520
521 log_task_stopped(MSGBUS_PUBLISH);
522 Ok(())
523}
524
525async fn drain_buffer(
526 conn: &mut redis::aio::ConnectionManager,
527 stream_key: &str,
528 stream_per_topic: bool,
529 autotrim_duration: Option<Duration>,
530 last_trim_index: &mut HashMap<String, usize>,
531 buffer: &mut VecDeque<BusMessage>,
532) -> anyhow::Result<()> {
533 let mut pipe = redis::pipe();
534 pipe.atomic();
535
536 for msg in buffer.drain(..) {
537 let encoding = msg.encoding.to_string();
538 let items: Vec<(&str, &[u8])> = vec![
539 ("topic", msg.topic.as_ref()),
540 ("type", msg.payload_type.as_str().as_bytes()),
541 ("payload", msg.payload.as_ref()),
542 ("encoding", encoding.as_bytes()),
543 ];
544 let stream_key = if stream_per_topic {
545 format!("{stream_key}:{}", msg.topic)
546 } else {
547 stream_key.to_string()
548 };
549 pipe.xadd(&stream_key, "*", &items);
550
551 if autotrim_duration.is_none() {
552 continue; }
554
555 let last_trim_ms = last_trim_index.entry(stream_key.clone()).or_insert(0); let unix_duration_now = duration_since_unix_epoch();
558 let trim_buffer = Duration::from_secs(TRIM_BUFFER_SECS);
559
560 if *last_trim_ms < unix_duration_now.saturating_sub(trim_buffer).as_millis() as usize {
562 let min_timestamp_ms = unix_duration_now
563 .saturating_sub(autotrim_duration.unwrap())
564 .as_millis() as usize;
565 let result: Result<(), redis::RedisError> = redis::cmd(REDIS_XTRIM)
566 .arg(stream_key.clone())
567 .arg(REDIS_MINID)
568 .arg(min_timestamp_ms)
569 .query_async(conn)
570 .await;
571
572 if let Err(e) = result {
573 log::error!("Error trimming stream '{stream_key}': {e}");
574 } else {
575 last_trim_index.insert(stream_key.clone(), unix_duration_now.as_millis() as usize);
576 }
577 }
578 }
579
580 pipe.query_async(conn).await.map_err(anyhow::Error::from)
581}
582
583pub async fn stream_messages(
591 tx: tokio::sync::mpsc::Sender<BusMessage>,
592 config: RedisMessageBusConfig,
593 stream_keys: Vec<String>,
594 stream_signal: Arc<AtomicBool>,
595) -> anyhow::Result<()> {
596 log_task_started(MSGBUS_STREAM);
597
598 let Some(mut con) = connect_stream_connection(&config, &stream_signal).await? else {
599 log_task_stopped(MSGBUS_STREAM);
600 return Ok(());
601 };
602
603 let mut read_error_count = 0;
604
605 let stream_keys = &stream_keys
606 .iter()
607 .map(String::as_str)
608 .collect::<Vec<&str>>();
609
610 log::debug!("Listening to streams: [{}]", stream_keys.join(", "));
611
612 let clock = get_atomic_clock_realtime();
614 let timestamp_ms = clock.get_time_ms();
615 let initial_id = timestamp_ms.to_string();
616
617 let mut last_ids: HashMap<String, String> = stream_keys
618 .iter()
619 .map(|&key| (key.to_string(), initial_id.clone()))
620 .collect();
621
622 let opts = StreamReadOptions::default().block(100);
623
624 'outer: loop {
625 if stream_signal.load(Ordering::Relaxed) {
626 log::debug!("Received streaming terminate signal");
627 break;
628 }
629
630 let ids: Vec<String> = stream_keys
631 .iter()
632 .map(|&key| last_ids[key].clone())
633 .collect();
634 let id_refs: Vec<&str> = ids.iter().map(String::as_str).collect();
635
636 let result: Result<RedisStreamBulk, _> =
637 con.xread_options(&[&stream_keys], &[&id_refs], &opts).await;
638
639 match result {
640 Ok(stream_bulk) => {
641 read_error_count = 0;
642
643 if stream_bulk.is_empty() {
644 continue;
646 }
647
648 for entry in &stream_bulk {
649 for (stream_key, stream_msgs) in entry {
650 for stream_msg in stream_msgs {
651 for (id, array) in stream_msg {
652 last_ids.insert(stream_key.clone(), id.clone());
653
654 match decode_bus_message(array) {
655 Ok(msg) => {
656 if let Err(e) = tx.send(msg).await {
657 log::debug!("Channel closed: {e:?}");
658 break 'outer; }
660 }
661 Err(e) => {
662 log::error!("{e:?}");
663 }
664 }
665 }
666 }
667 }
668 }
669 }
670 Err(e) => {
671 if !is_retryable_stream_error(&e) {
672 anyhow::bail!("Error reading from stream: {e:?}");
673 }
674
675 log::error!("Error reading from stream: {e:?}");
676
677 let Some(reconnected) =
678 reconnect_stream_connection(&config, &stream_signal, &mut read_error_count)
679 .await?
680 else {
681 break;
682 };
683 con = reconnected;
684 }
685 }
686 }
687
688 log_task_stopped(MSGBUS_STREAM);
689 Ok(())
690}
691
692async fn connect_stream_connection(
693 config: &RedisMessageBusConfig,
694 stream_signal: &Arc<AtomicBool>,
695) -> anyhow::Result<Option<ConnectionManager>> {
696 let connect = create_redis_connection(MSGBUS_STREAM, config);
697 let terminate = wait_for_stream_signal(stream_signal);
698
699 tokio::pin!(connect);
700 tokio::pin!(terminate);
701
702 tokio::select! {
703 result = &mut connect => result.map(Some),
704 () = &mut terminate => Ok(None),
705 }
706}
707
708async fn reconnect_stream_connection(
709 config: &RedisMessageBusConfig,
710 stream_signal: &Arc<AtomicBool>,
711 read_error_count: &mut usize,
712) -> anyhow::Result<Option<ConnectionManager>> {
713 loop {
714 let retry_delay = stream_retry_delay(config, *read_error_count);
715 *read_error_count = (*read_error_count).saturating_add(1);
716
717 if !wait_for_retry_delay(retry_delay, stream_signal).await {
718 return Ok(None);
719 }
720
721 match connect_stream_connection(config, stream_signal).await {
722 Ok(Some(con)) => return Ok(Some(con)),
723 Ok(None) => return Ok(None),
724 Err(e) => {
725 log::error!("Error reconnecting to stream: {e:?}");
726 }
727 }
728 }
729}
730
731fn stream_retry_delay(config: &RedisMessageBusConfig, attempt: usize) -> Duration {
732 let exponent = u32::try_from(attempt.min(32)).unwrap_or(32);
733 let delay_ms = config
734 .factor
735 .saturating_mul(config.exponent_base.saturating_pow(exponent));
736 let max_delay = Duration::from_secs(config.max_delay);
737
738 Duration::from_millis(delay_ms)
739 .min(max_delay)
740 .max(Duration::from_millis(1))
741}
742
743fn is_retryable_stream_error(error: &redis::RedisError) -> bool {
744 matches!(
745 error.retry_method(),
746 RetryMethod::Reconnect
747 | RetryMethod::ReconnectFromInitialConnections
748 | RetryMethod::RetryImmediately
749 | RetryMethod::WaitAndRetry
750 )
751}
752
753async fn wait_for_retry_delay(retry_delay: Duration, stream_signal: &Arc<AtomicBool>) -> bool {
754 let retry_timer = tokio::time::sleep(retry_delay);
755 let terminate = wait_for_stream_signal(stream_signal);
756
757 tokio::pin!(retry_timer);
758 tokio::pin!(terminate);
759
760 tokio::select! {
761 () = &mut retry_timer => true,
762 () = &mut terminate => false,
763 }
764}
765
766async fn wait_for_stream_signal(stream_signal: &Arc<AtomicBool>) {
767 let check_timer = tokio::time::interval(Duration::from_millis(100));
768
769 tokio::pin!(check_timer);
770
771 while !stream_signal.load(Ordering::Relaxed) {
772 check_timer.tick().await;
773 }
774}
775
776fn decode_bus_message(stream_msg: &redis::Value) -> anyhow::Result<BusMessage> {
778 let redis::Value::Array(fields) = stream_msg else {
779 anyhow::bail!("Invalid stream message format: {stream_msg:?}");
780 };
781
782 if fields.len() < 4 || fields.len() % 2 != 0 {
783 anyhow::bail!("Invalid stream message format: {stream_msg:?}");
784 }
785
786 let mut topic: Option<String> = None;
787 let mut payload_type = BusPayloadType::Custom(Ustr::default());
788 let mut encoding = SerializationEncoding::default();
789 let mut payload: Option<Bytes> = None;
790
791 for pair in fields.chunks_exact(2) {
792 let redis::Value::BulkString(key) = &pair[0] else {
793 anyhow::bail!("Invalid stream field key: {stream_msg:?}");
794 };
795
796 match key.as_slice() {
797 b"topic" => {
798 let redis::Value::BulkString(bytes) = &pair[1] else {
799 anyhow::bail!("Invalid topic format: {stream_msg:?}");
800 };
801 topic = Some(
802 String::from_utf8(bytes.clone())
803 .map_err(|e| anyhow::anyhow!("Error parsing topic: {e}"))?,
804 );
805 }
806 b"type" => {
807 let redis::Value::BulkString(bytes) = &pair[1] else {
808 anyhow::bail!("Invalid type format: {stream_msg:?}");
809 };
810 let type_name = std::str::from_utf8(bytes)
811 .map_err(|e| anyhow::anyhow!("Error parsing type: {e}"))?;
812 payload_type = BusPayloadType::from_name(type_name);
813 }
814 b"encoding" => {
815 let redis::Value::BulkString(bytes) = &pair[1] else {
816 anyhow::bail!("Invalid encoding format: {stream_msg:?}");
817 };
818 let value = std::str::from_utf8(bytes)
819 .map_err(|e| anyhow::anyhow!("Error parsing encoding: {e}"))?;
820 encoding = value
821 .parse()
822 .map_err(|e| anyhow::anyhow!("Error parsing encoding: {e}"))?;
823 }
824 b"payload" => {
825 let redis::Value::BulkString(bytes) = &pair[1] else {
826 anyhow::bail!("Invalid payload format: {stream_msg:?}");
827 };
828 payload = Some(Bytes::copy_from_slice(bytes));
829 }
830 _ => {}
831 }
832 }
833
834 let Some(topic) = topic else {
835 anyhow::bail!("Stream message missing topic: {stream_msg:?}");
836 };
837 let Some(payload) = payload else {
838 anyhow::bail!("Stream message missing payload: {stream_msg:?}");
839 };
840
841 Ok(BusMessage::with_str_topic(
842 topic,
843 payload_type,
844 payload,
845 encoding,
846 ))
847}
848
849async fn run_heartbeat(
850 heartbeat_interval_secs: u16,
851 signal: Arc<AtomicBool>,
852 pub_tx: tokio::sync::mpsc::UnboundedSender<BusMessage>,
853) {
854 log_task_started("heartbeat");
855 log::debug!("Heartbeat at {heartbeat_interval_secs} second intervals");
856
857 let heartbeat_interval = Duration::from_secs(u64::from(heartbeat_interval_secs));
858 let heartbeat_timer = tokio::time::interval(heartbeat_interval);
859
860 let check_interval = Duration::from_millis(100);
861 let check_timer = tokio::time::interval(check_interval);
862
863 tokio::pin!(heartbeat_timer);
864 tokio::pin!(check_timer);
865
866 loop {
867 if signal.load(Ordering::Relaxed) {
868 log::debug!("Received heartbeat terminate signal");
869 break;
870 }
871
872 tokio::select! {
873 _ = heartbeat_timer.tick() => {
874 let heartbeat = create_heartbeat_msg();
875 if let Err(e) = pub_tx.send(heartbeat) {
876 log::debug!("Error sending heartbeat: {e}");
878 }
879 },
880 _ = check_timer.tick() => {}
881 }
882 }
883
884 log_task_stopped("heartbeat");
885}
886
887fn create_heartbeat_msg() -> BusMessage {
888 let payload = Bytes::from(chrono::Utc::now().to_rfc3339().into_bytes());
889 BusMessage::with_str_topic(
890 HEARTBEAT_TOPIC,
891 BusPayloadType::Custom(Ustr::default()),
892 payload,
893 SerializationEncoding::default(),
894 )
895}
896
897#[cfg(test)]
898mod tests {
899 use nautilus_common::{msgbus::external_io_from_backing, testing::wait_until_async};
900 use redis::Value;
901 use rstest::*;
902 use serde_json::json;
903
904 use super::*;
905
906 #[rstest]
907 fn test_default_redis_message_bus_config() {
908 let config = RedisMessageBusConfig::default();
909
910 assert_eq!(config.host, None);
911 assert_eq!(config.port, None);
912 assert_eq!(config.username, None);
913 assert_eq!(config.password, None);
914 assert!(!config.ssl);
915 assert_eq!(config.connection_timeout, 20);
916 assert_eq!(config.response_timeout, 20);
917 assert_eq!(config.number_of_retries, 100);
918 assert_eq!(config.exponent_base, 2);
919 assert_eq!(config.max_delay, 1000);
920 assert_eq!(config.factor, 2);
921 }
922
923 #[rstest]
924 fn test_deserialize_redis_message_bus_config() {
925 let config_json = json!({
926 "host": "localhost",
927 "port": 6379,
928 "username": "user",
929 "password": "pass",
930 "ssl": true,
931 "connection_timeout": 30,
932 "response_timeout": 10,
933 "number_of_retries": 3,
934 "exponent_base": 2,
935 "max_delay": 10,
936 "factor": 2
937 });
938
939 let config: RedisMessageBusConfig = serde_json::from_value(config_json).unwrap();
940
941 assert_eq!(config.host, Some("localhost".to_string()));
942 assert_eq!(config.port, Some(6379));
943 assert_eq!(config.username, Some("user".to_string()));
944 assert_eq!(config.password, Some("pass".to_string()));
945 assert!(config.ssl);
946 assert_eq!(config.connection_timeout, 30);
947 assert_eq!(config.response_timeout, 10);
948 assert_eq!(config.number_of_retries, 3);
949 assert_eq!(config.exponent_base, 2);
950 assert_eq!(config.max_delay, 10);
951 assert_eq!(config.factor, 2);
952 }
953
954 #[rstest]
955 fn test_deserialize_redis_message_bus_config_rejects_type_selector() {
956 let config_json = json!({
957 "type": "redis",
958 });
959
960 let error = serde_json::from_value::<RedisMessageBusConfig>(config_json).unwrap_err();
961
962 assert!(error.to_string().contains("unknown field `type`"));
963 }
964
965 #[rstest]
966 fn test_decode_bus_message_valid() {
967 let stream_msg = Value::Array(vec![
968 Value::BulkString(b"topic".to_vec()),
969 Value::BulkString(b"topic1".to_vec()),
970 Value::BulkString(b"type".to_vec()),
971 Value::BulkString(b"QuoteTick".to_vec()),
972 Value::BulkString(b"payload".to_vec()),
973 Value::BulkString(b"data1".to_vec()),
974 Value::BulkString(b"encoding".to_vec()),
975 Value::BulkString(b"msgpack".to_vec()),
976 ]);
977
978 let result = decode_bus_message(&stream_msg);
979 assert!(result.is_ok());
980 let msg = result.unwrap();
981 assert_eq!(msg.topic, "topic1");
982 assert_eq!(msg.payload_type, BusPayloadType::QuoteTick);
983 assert_eq!(msg.encoding, SerializationEncoding::MsgPack);
984 assert_eq!(msg.payload, Bytes::from("data1"));
985 }
986
987 #[rstest]
988 fn test_decode_bus_message_defaults_legacy_headers() {
989 let stream_msg = Value::Array(vec![
990 Value::BulkString(b"topic".to_vec()),
991 Value::BulkString(b"topic1".to_vec()),
992 Value::BulkString(b"payload".to_vec()),
993 Value::BulkString(b"data1".to_vec()),
994 ]);
995
996 let result = decode_bus_message(&stream_msg);
997 assert!(result.is_ok());
998 let msg = result.unwrap();
999 assert_eq!(msg.topic, "topic1");
1000 assert_eq!(msg.payload_type, BusPayloadType::Custom(Ustr::default()));
1001 assert_eq!(msg.encoding, SerializationEncoding::Json);
1002 assert_eq!(msg.payload, Bytes::from("data1"));
1003 }
1004
1005 #[rstest]
1006 fn test_decode_bus_message_unknown_type_is_custom() {
1007 let stream_msg = Value::Array(vec![
1008 Value::BulkString(b"topic".to_vec()),
1009 Value::BulkString(b"topic1".to_vec()),
1010 Value::BulkString(b"type".to_vec()),
1011 Value::BulkString(b"UnknownPayload".to_vec()),
1012 Value::BulkString(b"payload".to_vec()),
1013 Value::BulkString(b"data1".to_vec()),
1014 ]);
1015
1016 let result = decode_bus_message(&stream_msg);
1017 assert!(result.is_ok());
1018 let msg = result.unwrap();
1019 assert_eq!(
1020 msg.payload_type,
1021 BusPayloadType::Custom(Ustr::from("UnknownPayload"))
1022 );
1023 assert_eq!(msg.encoding, SerializationEncoding::Json);
1024 }
1025
1026 #[rstest]
1027 fn test_decode_bus_message_accepts_unordered_metadata_fields() {
1028 let stream_msg = Value::Array(vec![
1029 Value::BulkString(b"payload".to_vec()),
1030 Value::BulkString(b"data1".to_vec()),
1031 Value::BulkString(b"encoding".to_vec()),
1032 Value::BulkString(b"msgpack".to_vec()),
1033 Value::BulkString(b"type".to_vec()),
1034 Value::BulkString(b"TradeTick".to_vec()),
1035 Value::BulkString(b"topic".to_vec()),
1036 Value::BulkString(b"topic1".to_vec()),
1037 ]);
1038
1039 let msg = decode_bus_message(&stream_msg).unwrap();
1040
1041 assert_eq!(msg.topic, "topic1");
1042 assert_eq!(msg.payload_type, BusPayloadType::TradeTick);
1043 assert_eq!(msg.encoding, SerializationEncoding::MsgPack);
1044 assert_eq!(msg.payload, Bytes::from("data1"));
1045 }
1046
1047 #[rstest]
1048 fn test_decode_bus_message_rejects_invalid_encoding_header() {
1049 let stream_msg = Value::Array(vec![
1050 Value::BulkString(b"topic".to_vec()),
1051 Value::BulkString(b"topic1".to_vec()),
1052 Value::BulkString(b"encoding".to_vec()),
1053 Value::BulkString(b"invalid".to_vec()),
1054 Value::BulkString(b"payload".to_vec()),
1055 Value::BulkString(b"data1".to_vec()),
1056 ]);
1057
1058 let error = decode_bus_message(&stream_msg).unwrap_err();
1059
1060 assert!(
1061 error.to_string().contains("Error parsing encoding"),
1062 "{error:?}"
1063 );
1064 }
1065
1066 #[rstest]
1067 fn test_decode_bus_message_missing_fields() {
1068 let stream_msg = Value::Array(vec![
1069 Value::BulkString(b"0".to_vec()),
1070 Value::BulkString(b"topic1".to_vec()),
1071 ]);
1072
1073 let result = decode_bus_message(&stream_msg);
1074 assert!(result.is_err());
1075 assert_eq!(
1076 format!("{}", result.unwrap_err()),
1077 "Invalid stream message format: array([bulk-string('\"0\"'), bulk-string('\"topic1\"')])"
1078 );
1079 }
1080
1081 #[rstest]
1082 fn test_decode_bus_message_invalid_topic_format() {
1083 let stream_msg = Value::Array(vec![
1084 Value::BulkString(b"topic".to_vec()),
1085 Value::Int(42),
1086 Value::BulkString(b"payload".to_vec()),
1087 Value::BulkString(b"data1".to_vec()),
1088 ]);
1089
1090 let result = decode_bus_message(&stream_msg);
1091 assert!(result.is_err());
1092 assert_eq!(
1093 format!("{}", result.unwrap_err()),
1094 "Invalid topic format: array([bulk-string('\"topic\"'), int(42), bulk-string('\"payload\"'), bulk-string('\"data1\"')])"
1095 );
1096 }
1097
1098 #[rstest]
1099 fn test_decode_bus_message_invalid_type_format() {
1100 let stream_msg = Value::Array(vec![
1101 Value::BulkString(b"topic".to_vec()),
1102 Value::BulkString(b"topic1".to_vec()),
1103 Value::BulkString(b"type".to_vec()),
1104 Value::Int(42),
1105 Value::BulkString(b"payload".to_vec()),
1106 Value::BulkString(b"data1".to_vec()),
1107 ]);
1108
1109 let result = decode_bus_message(&stream_msg);
1110 assert!(result.is_err());
1111 assert_eq!(
1112 format!("{}", result.unwrap_err()),
1113 "Invalid type format: array([bulk-string('\"topic\"'), bulk-string('\"topic1\"'), bulk-string('\"type\"'), int(42), bulk-string('\"payload\"'), bulk-string('\"data1\"')])"
1114 );
1115 }
1116
1117 #[rstest]
1118 fn test_decode_bus_message_invalid_encoding_format() {
1119 let stream_msg = Value::Array(vec![
1120 Value::BulkString(b"topic".to_vec()),
1121 Value::BulkString(b"topic1".to_vec()),
1122 Value::BulkString(b"encoding".to_vec()),
1123 Value::Int(42),
1124 Value::BulkString(b"payload".to_vec()),
1125 Value::BulkString(b"data1".to_vec()),
1126 ]);
1127
1128 let result = decode_bus_message(&stream_msg);
1129 assert!(result.is_err());
1130 assert_eq!(
1131 format!("{}", result.unwrap_err()),
1132 "Invalid encoding format: array([bulk-string('\"topic\"'), bulk-string('\"topic1\"'), bulk-string('\"encoding\"'), int(42), bulk-string('\"payload\"'), bulk-string('\"data1\"')])"
1133 );
1134 }
1135
1136 #[rstest]
1137 fn test_decode_bus_message_invalid_payload_format() {
1138 let stream_msg = Value::Array(vec![
1139 Value::BulkString(b"topic".to_vec()),
1140 Value::BulkString(b"topic1".to_vec()),
1141 Value::BulkString(b"payload".to_vec()),
1142 Value::Int(42),
1143 ]);
1144
1145 let result = decode_bus_message(&stream_msg);
1146 assert!(result.is_err());
1147 assert_eq!(
1148 format!("{}", result.unwrap_err()),
1149 "Invalid payload format: array([bulk-string('\"topic\"'), bulk-string('\"topic1\"'), bulk-string('\"payload\"'), int(42)])"
1150 );
1151 }
1152
1153 #[rstest]
1154 fn test_decode_bus_message_invalid_stream_msg_format() {
1155 let stream_msg = Value::BulkString(b"not an array".to_vec());
1156
1157 let result = decode_bus_message(&stream_msg);
1158 assert!(result.is_err());
1159 assert_eq!(
1160 format!("{}", result.unwrap_err()),
1161 "Invalid stream message format: bulk-string('\"not an array\"')"
1162 );
1163 }
1164
1165 #[rstest]
1166 fn test_new_rejects_zero_heartbeat_interval() {
1167 let trader_id = TraderId::from("tester-001");
1168 let instance_id = UUID4::new();
1169 let config = MessageBusConfig {
1170 heartbeat_interval_secs: Some(0),
1171 ..Default::default()
1172 };
1173
1174 let result = RedisMessageBusBacking::new(
1175 trader_id,
1176 instance_id,
1177 config,
1178 RedisMessageBusConfig::default(),
1179 );
1180
1181 assert!(result.is_err());
1182 assert_eq!(
1183 result.unwrap_err().to_string(),
1184 "heartbeat_interval_secs must be greater than 0"
1185 );
1186 }
1187
1188 #[rstest]
1189 fn test_stream_retry_delay_uses_config_bounds() {
1190 let config = RedisMessageBusConfig {
1191 factor: 10,
1192 exponent_base: 2,
1193 max_delay: 1,
1194 ..Default::default()
1195 };
1196
1197 assert_eq!(stream_retry_delay(&config, 0), Duration::from_millis(10));
1198 assert_eq!(stream_retry_delay(&config, 1), Duration::from_millis(20));
1199 assert_eq!(stream_retry_delay(&config, 10), Duration::from_secs(1));
1200 }
1201
1202 #[rstest]
1203 fn test_stream_error_retry_classification() {
1204 let dropped =
1205 redis::RedisError::from(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
1206 let client: redis::RedisError = (redis::ErrorKind::Client, "client error").into();
1207
1208 assert!(is_retryable_stream_error(&dropped));
1209 assert!(!is_retryable_stream_error(&client));
1210 }
1211
1212 #[tokio::test]
1213 async fn test_wait_for_retry_delay_returns_false_when_signaled() {
1214 let stream_signal = Arc::new(AtomicBool::new(true));
1215 let signal = stream_signal.clone();
1216 let fut = async move { wait_for_retry_delay(Duration::from_secs(30), &signal).await };
1217
1218 let handle = tokio::spawn(fut);
1219
1220 wait_until_async(|| async { handle.is_finished() }, Duration::from_secs(1)).await;
1221
1222 assert!(!handle.await.unwrap());
1223 }
1224
1225 #[rstest]
1226 fn test_external_io_from_backing_takes_stream_receiver() {
1227 let (stream_tx, stream_rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
1228 let backing = backing_with_stream_receiver(stream_rx);
1229 let message = BusMessage::with_str_topic(
1230 "events/data",
1231 BusPayloadType::QuoteTick,
1232 Bytes::from_static(b"payload"),
1233 SerializationEncoding::Json,
1234 );
1235
1236 let (_egress, mut ingress) = external_io_from_backing(Box::new(backing));
1237 stream_tx.try_send(message.clone()).unwrap();
1238 let mut receiver = ingress.take_receiver().unwrap();
1239 let received = receiver.try_recv().unwrap();
1240
1241 assert_eq!(received.topic, message.topic);
1242 assert_eq!(received.payload, message.payload);
1243 assert!(ingress.take_receiver().is_err());
1244 }
1245
1246 fn backing_with_stream_receiver(
1247 stream_rx: tokio::sync::mpsc::Receiver<BusMessage>,
1248 ) -> RedisMessageBusBacking {
1249 let (pub_tx, _pub_rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
1250 RedisMessageBusBacking {
1251 trader_id: TraderId::from("tester-001"),
1252 instance_id: UUID4::new(),
1253 pub_tx,
1254 pub_handle: None,
1255 stream_rx: Some(stream_rx),
1256 stream_handle: None,
1257 stream_signal: Arc::new(AtomicBool::new(false)),
1258 heartbeat_handle: None,
1259 heartbeat_signal: Arc::new(AtomicBool::new(false)),
1260 }
1261 }
1262}
1263
1264#[cfg(target_os = "linux")] #[cfg(test)]
1266mod serial_tests {
1267 use std::{sync::mpsc, thread};
1268
1269 use nautilus_common::{
1270 enums::Environment,
1271 msgbus::{self, TypedHandler},
1272 testing::wait_until_async,
1273 };
1274 use nautilus_live::{
1275 builder::LiveNodeBuilder,
1276 config::{LiveExecEngineConfig, LiveNodeConfig},
1277 };
1278 use nautilus_model::data::{QuoteTick, TradeTick};
1279 use redis::aio::ConnectionManager;
1280 use rstest::*;
1281
1282 use super::*;
1283
1284 #[fixture]
1285 async fn redis_connection() -> ConnectionManager {
1286 let config = RedisMessageBusConfig::default();
1287 create_redis_connection(MSGBUS_STREAM, &config)
1288 .await
1289 .unwrap()
1290 }
1291
1292 #[rstest]
1293 #[tokio::test(flavor = "multi_thread")]
1294 async fn test_stream_messages_terminate_signal(#[future] redis_connection: ConnectionManager) {
1295 let _con = redis_connection.await;
1296 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1297
1298 let trader_id = TraderId::from("tester-001");
1299 let instance_id = UUID4::new();
1300 let config = MessageBusConfig {
1301 use_instance_id: true,
1302 ..Default::default()
1303 };
1304
1305 let stream_key = get_stream_key(trader_id, instance_id, &config);
1306 let external_streams = vec![stream_key.clone()];
1307 let stream_signal = Arc::new(AtomicBool::new(false));
1308 let stream_signal_clone = stream_signal.clone();
1309
1310 let handle = tokio::spawn(async move {
1312 stream_messages(
1313 tx,
1314 RedisMessageBusConfig::default(),
1315 external_streams,
1316 stream_signal_clone,
1317 )
1318 .await
1319 .unwrap();
1320 });
1321
1322 stream_signal.store(true, Ordering::Relaxed);
1323 let _ = rx.recv().await; rx.close();
1327 handle.await.unwrap();
1328 }
1329
1330 #[rstest]
1331 #[tokio::test(flavor = "multi_thread")]
1332 async fn test_stream_messages_when_receiver_closed(
1333 #[future] redis_connection: ConnectionManager,
1334 ) {
1335 let mut con = redis_connection.await;
1336 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1337
1338 let trader_id = TraderId::from("tester-001");
1339 let instance_id = UUID4::new();
1340 let config = MessageBusConfig {
1341 use_instance_id: true,
1342 ..Default::default()
1343 };
1344
1345 let stream_key = get_stream_key(trader_id, instance_id, &config);
1346 let external_streams = vec![stream_key.clone()];
1347 let stream_signal = Arc::new(AtomicBool::new(false));
1348 let stream_signal_clone = stream_signal.clone();
1349
1350 let clock = get_atomic_clock_realtime();
1353 let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1354
1355 let _: () = con
1357 .xadd(
1358 stream_key,
1359 future_id,
1360 &[("topic", "topic1"), ("payload", "data1")],
1361 )
1362 .await
1363 .unwrap();
1364
1365 rx.close();
1367
1368 let handle = tokio::spawn(async move {
1370 stream_messages(
1371 tx,
1372 RedisMessageBusConfig::default(),
1373 external_streams,
1374 stream_signal_clone,
1375 )
1376 .await
1377 .unwrap();
1378 });
1379
1380 handle.await.unwrap();
1382 }
1383
1384 #[rstest]
1385 #[tokio::test(flavor = "multi_thread")]
1386 async fn test_stream_messages(#[future] redis_connection: ConnectionManager) {
1387 let mut con = redis_connection.await;
1388 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1389
1390 let trader_id = TraderId::from("tester-001");
1391 let instance_id = UUID4::new();
1392 let config = MessageBusConfig {
1393 use_instance_id: true,
1394 ..Default::default()
1395 };
1396
1397 let stream_key = get_stream_key(trader_id, instance_id, &config);
1398 let external_streams = vec![stream_key.clone()];
1399 let stream_signal = Arc::new(AtomicBool::new(false));
1400 let stream_signal_clone = stream_signal.clone();
1401
1402 let clock = get_atomic_clock_realtime();
1405 let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1406
1407 let _: () = con
1409 .xadd(
1410 stream_key,
1411 future_id,
1412 &[("topic", "topic1"), ("payload", "data1")],
1413 )
1414 .await
1415 .unwrap();
1416
1417 let handle = tokio::spawn(async move {
1419 stream_messages(
1420 tx,
1421 RedisMessageBusConfig::default(),
1422 external_streams,
1423 stream_signal_clone,
1424 )
1425 .await
1426 .unwrap();
1427 });
1428
1429 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1431 assert_eq!(msg.topic, "topic1");
1432 assert_eq!(msg.payload, Bytes::from("data1"));
1433
1434 rx.close();
1436 stream_signal.store(true, Ordering::Relaxed);
1437 handle.await.unwrap();
1438 }
1439
1440 #[rstest]
1441 #[tokio::test(flavor = "multi_thread")]
1442 async fn test_stream_messages_skips_malformed_entry(
1443 #[future] redis_connection: ConnectionManager,
1444 ) {
1445 let mut con = redis_connection.await;
1446 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1447
1448 let suffix = UUID4::new();
1449 let stream_key = format!("test:stream:malformed:{suffix}");
1450 let external_streams = vec![stream_key.clone()];
1451 let stream_signal = Arc::new(AtomicBool::new(false));
1452 let stream_signal_clone = stream_signal.clone();
1453
1454 let clock = get_atomic_clock_realtime();
1455 let base_id = clock.get_time_ms() + 1_000_000;
1456
1457 let _: () = con
1458 .xadd(
1459 &stream_key,
1460 format!("{}", base_id + 1),
1461 &[("topic", "missing-payload")],
1462 )
1463 .await
1464 .unwrap();
1465 let _: () = con
1466 .xadd(
1467 &stream_key,
1468 format!("{}", base_id + 2),
1469 &[("topic", "valid"), ("payload", "data")],
1470 )
1471 .await
1472 .unwrap();
1473
1474 let handle = tokio::spawn(async move {
1475 stream_messages(
1476 tx,
1477 RedisMessageBusConfig::default(),
1478 external_streams,
1479 stream_signal_clone,
1480 )
1481 .await
1482 .unwrap();
1483 });
1484
1485 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1486
1487 rx.close();
1488 stream_signal.store(true, Ordering::Relaxed);
1489 handle.await.unwrap();
1490
1491 assert_eq!(msg.topic, "valid");
1492 assert_eq!(msg.payload, Bytes::from("data"));
1493 }
1494
1495 #[rstest]
1496 #[tokio::test(flavor = "multi_thread")]
1497 async fn test_stream_messages_returns_unrecoverable_read_error(
1498 #[future] redis_connection: ConnectionManager,
1499 ) {
1500 let mut con = redis_connection.await;
1501 let (tx, _rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1502
1503 let suffix = UUID4::new();
1504 let stream_key = format!("test:stream:wrong-type:{suffix}");
1505 let external_streams = vec![stream_key.clone()];
1506 let stream_signal = Arc::new(AtomicBool::new(false));
1507
1508 let _: () = con.set(&stream_key, "not-a-stream").await.unwrap();
1509
1510 let result = stream_messages(
1511 tx,
1512 RedisMessageBusConfig::default(),
1513 external_streams,
1514 stream_signal,
1515 )
1516 .await;
1517
1518 let _: () = con.del(&stream_key).await.unwrap();
1519
1520 assert!(result.is_err());
1521 assert!(
1522 result
1523 .unwrap_err()
1524 .to_string()
1525 .contains("Error reading from stream")
1526 );
1527 }
1528
1529 #[rstest]
1530 #[tokio::test(flavor = "multi_thread")]
1531 async fn test_stream_connection_returns_none_when_signaled() {
1532 let config = RedisMessageBusConfig {
1533 port: Some(1),
1534 connection_timeout: 20,
1535 ..Default::default()
1536 };
1537 let stream_signal = Arc::new(AtomicBool::new(true));
1538 let signal = stream_signal.clone();
1539 let handle = tokio::spawn(async move { connect_stream_connection(&config, &signal).await });
1540
1541 wait_until_async(|| async { handle.is_finished() }, Duration::from_secs(1)).await;
1542
1543 assert!(handle.await.unwrap().unwrap().is_none());
1544 }
1545
1546 #[rstest]
1547 #[tokio::test(flavor = "multi_thread")]
1548 async fn test_publish_messages(#[future] redis_connection: ConnectionManager) {
1549 let mut con = redis_connection.await;
1550 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
1551
1552 let trader_id = TraderId::from("tester-001");
1553 let instance_id = UUID4::new();
1554 let config = MessageBusConfig {
1555 use_instance_id: true,
1556 stream_per_topic: false,
1557 ..Default::default()
1558 };
1559 let stream_key = get_stream_key(trader_id, instance_id, &config);
1560
1561 let handle = tokio::spawn(async move {
1563 publish_messages(
1564 rx,
1565 trader_id,
1566 instance_id,
1567 config,
1568 RedisMessageBusConfig::default(),
1569 )
1570 .await
1571 .unwrap();
1572 });
1573
1574 let msg = BusMessage::with_str_topic(
1576 "test_topic",
1577 BusPayloadType::QuoteTick,
1578 Bytes::from("test_payload"),
1579 SerializationEncoding::Json,
1580 );
1581 tx.send(msg).unwrap();
1582
1583 wait_until_async(
1585 || {
1586 let mut con = con.clone();
1587 let stream_key = stream_key.clone();
1588 async move {
1589 let messages: RedisStreamBulk =
1590 con.xread(&[&stream_key], &["0"]).await.unwrap();
1591 !messages.is_empty()
1592 }
1593 },
1594 Duration::from_secs(3),
1595 )
1596 .await;
1597
1598 let messages: RedisStreamBulk = con.xread(&[&stream_key], &["0"]).await.unwrap();
1600 assert_eq!(messages.len(), 1);
1601 let stream_msgs = messages[0].get(&stream_key).unwrap();
1602 let stream_msg_array = &stream_msgs[0].values().next().unwrap();
1603 let decoded_message = decode_bus_message(stream_msg_array).unwrap();
1604 assert_eq!(decoded_message.topic, "test_topic");
1605 assert_eq!(decoded_message.payload_type, BusPayloadType::QuoteTick);
1606 assert_eq!(decoded_message.encoding, SerializationEncoding::Json);
1607 assert_eq!(decoded_message.payload, Bytes::from("test_payload"));
1608
1609 let msg = BusMessage::new_close();
1611 tx.send(msg).unwrap();
1612
1613 handle.await.unwrap();
1615 }
1616
1617 #[rstest]
1618 #[tokio::test(flavor = "multi_thread")]
1619 async fn test_two_live_nodes_publish_and_ingest_external_redis_stream(
1620 #[future] redis_connection: ConnectionManager,
1621 ) {
1622 let _con = redis_connection.await;
1623 let redis_config = RedisMessageBusConfig::default();
1624 let trader_a = TraderId::from("NODEA-001");
1625 let instance_a = UUID4::new();
1626 let node_a_msgbus = MessageBusConfig {
1627 use_instance_id: true,
1628 stream_per_topic: false,
1629 ..Default::default()
1630 };
1631 let stream_key = get_stream_key(trader_a, instance_a, &node_a_msgbus);
1632 let node_b_msgbus = MessageBusConfig {
1633 external_streams: Some(vec![stream_key]),
1634 stream_per_topic: false,
1635 ..Default::default()
1636 };
1637 let quote = QuoteTick::default();
1638 let trade = TradeTick::default();
1639 let (ready_tx, ready_rx) = mpsc::channel::<()>();
1640 let (quote_tx, quote_rx) = mpsc::channel::<QuoteTick>();
1641 let (trade_tx, trade_rx) = mpsc::channel::<TradeTick>();
1642
1643 let node_b = thread::spawn({
1644 let redis_config = redis_config.clone();
1645 move || -> anyhow::Result<()> {
1646 let runtime = tokio::runtime::Builder::new_multi_thread()
1647 .worker_threads(2)
1648 .enable_all()
1649 .build()?;
1650
1651 runtime.block_on(async move {
1652 let config = LiveNodeConfig {
1653 environment: Environment::Sandbox,
1654 trader_id: TraderId::from("NODEB-001"),
1655 msgbus: Some(node_b_msgbus),
1656 exec_engine: LiveExecEngineConfig {
1657 reconciliation: false,
1658 ..Default::default()
1659 },
1660 delay_post_stop: Duration::ZERO,
1661 timeout_connection: Duration::from_millis(500),
1662 timeout_disconnection: Duration::from_millis(500),
1663 ..Default::default()
1664 };
1665 let mut node = LiveNodeBuilder::from_config(config)?
1666 .with_external_msgbus_factory(Box::new(RedisMessageBusFactory::new(
1667 redis_config,
1668 )))
1669 .build()?;
1670 let handle = node.handle();
1671 let quote_handler = TypedHandler::from({
1672 let quote_tx = quote_tx.clone();
1673 let handle = handle.clone();
1674 move |quote: &QuoteTick| {
1675 let _ = quote_tx.send(*quote);
1676 handle.stop();
1677 }
1678 });
1679 let trade_handler = TypedHandler::from(move |trade: &TradeTick| {
1680 let _ = trade_tx.send(*trade);
1681 });
1682
1683 msgbus::subscribe_quotes("data.quotes.*".into(), quote_handler, None);
1684 msgbus::subscribe_trades("data.trades.*".into(), trade_handler, None);
1685 msgbus::get_message_bus()
1686 .borrow_mut()
1687 .add_streaming_type(BusPayloadType::QuoteTick);
1688 let result = tokio::time::timeout(Duration::from_secs(10), async {
1689 let run = node.run();
1690 tokio::pin!(run);
1691
1692 let announce_ready = async {
1693 for _ in 0..100 {
1694 if handle.is_running() {
1695 ready_tx.send(())?;
1696 return Ok(());
1697 }
1698 tokio::time::sleep(Duration::from_millis(10)).await;
1699 }
1700
1701 anyhow::bail!("node B did not reach running state")
1702 };
1703
1704 tokio::select! {
1705 result = &mut run => result,
1706 ready = announce_ready => {
1707 ready?;
1708 run.await
1709 }
1710 }
1711 })
1712 .await;
1713 msgbus::get_message_bus().borrow_mut().dispose();
1714
1715 match result {
1716 Ok(Ok(())) => Ok(()),
1717 Ok(Err(e)) => Err(e),
1718 Err(e) => anyhow::bail!("node B timed out: {e}"),
1719 }
1720 })
1721 }
1722 });
1723
1724 ready_rx
1725 .recv_timeout(Duration::from_secs(5))
1726 .expect("node B should start Redis ingress");
1727
1728 let node_a = thread::spawn(move || -> anyhow::Result<()> {
1729 let runtime = tokio::runtime::Builder::new_multi_thread()
1730 .worker_threads(2)
1731 .enable_all()
1732 .build()?;
1733
1734 runtime.block_on(async move {
1735 let config = LiveNodeConfig {
1736 environment: Environment::Sandbox,
1737 trader_id: trader_a,
1738 instance_id: Some(instance_a),
1739 msgbus: Some(node_a_msgbus),
1740 exec_engine: LiveExecEngineConfig {
1741 reconciliation: false,
1742 ..Default::default()
1743 },
1744 delay_post_stop: Duration::ZERO,
1745 timeout_connection: Duration::from_millis(500),
1746 timeout_disconnection: Duration::from_millis(500),
1747 ..Default::default()
1748 };
1749 let _node = LiveNodeBuilder::from_config(config)?
1750 .with_external_msgbus_factory(Box::new(RedisMessageBusFactory::new(
1751 redis_config,
1752 )))
1753 .build()?;
1754
1755 msgbus::publish_trade("data.trades.TEST".into(), &trade);
1756 msgbus::publish_quote("data.quotes.TEST".into(), "e);
1757 msgbus::get_message_bus().borrow_mut().dispose();
1758
1759 Ok(())
1760 })
1761 });
1762
1763 node_a
1764 .join()
1765 .expect("node A thread should not panic")
1766 .expect("node A should publish externally");
1767 let received_quote = quote_rx
1768 .recv_timeout(Duration::from_secs(10))
1769 .expect("node B should republish the registered quote type");
1770 node_b
1771 .join()
1772 .expect("node B thread should not panic")
1773 .expect("node B should ingest and stop cleanly");
1774
1775 assert_eq!(received_quote, quote);
1776 assert!(
1777 trade_rx.try_recv().is_err(),
1778 "unregistered trade type should not republish internally"
1779 );
1780 }
1781
1782 #[rstest]
1783 #[tokio::test(flavor = "multi_thread")]
1784 async fn test_stream_messages_multiple_streams(#[future] redis_connection: ConnectionManager) {
1785 let mut con = redis_connection.await;
1786 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1787
1788 let suffix = UUID4::new();
1790 let stream_key1 = format!("test:stream:{suffix}:1");
1791 let stream_key2 = format!("test:stream:{suffix}:2");
1792 let external_streams = vec![stream_key1.clone(), stream_key2.clone()];
1793 let stream_signal = Arc::new(AtomicBool::new(false));
1794 let stream_signal_clone = stream_signal.clone();
1795
1796 let clock = get_atomic_clock_realtime();
1797 let base_id = clock.get_time_ms() + 1_000_000;
1798
1799 let handle = tokio::spawn(async move {
1801 stream_messages(
1802 tx,
1803 RedisMessageBusConfig::default(),
1804 external_streams,
1805 stream_signal_clone,
1806 )
1807 .await
1808 .unwrap();
1809 });
1810
1811 let _: () = con
1813 .xadd(
1814 &stream_key1,
1815 format!("{}", base_id + 100),
1816 &[("topic", "stream1-first"), ("payload", "data")],
1817 )
1818 .await
1819 .unwrap();
1820
1821 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1822 assert_eq!(msg.topic, "stream1-first");
1823
1824 let _: () = con
1826 .xadd(
1827 &stream_key2,
1828 format!("{}", base_id + 50),
1829 &[("topic", "stream2-second"), ("payload", "data")],
1830 )
1831 .await
1832 .unwrap();
1833
1834 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1835 assert_eq!(msg.topic, "stream2-second");
1836
1837 rx.close();
1839 stream_signal.store(true, Ordering::Relaxed);
1840 handle.await.unwrap();
1841 }
1842
1843 #[rstest]
1844 #[tokio::test(flavor = "multi_thread")]
1845 async fn test_stream_messages_interleaved_at_different_rates(
1846 #[future] redis_connection: ConnectionManager,
1847 ) {
1848 let mut con = redis_connection.await;
1849 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1850
1851 let suffix = UUID4::new();
1853 let stream_key1 = format!("test:stream:interleaved:{suffix}:1");
1854 let stream_key2 = format!("test:stream:interleaved:{suffix}:2");
1855 let stream_key3 = format!("test:stream:interleaved:{suffix}:3");
1856 let external_streams = vec![
1857 stream_key1.clone(),
1858 stream_key2.clone(),
1859 stream_key3.clone(),
1860 ];
1861 let stream_signal = Arc::new(AtomicBool::new(false));
1862 let stream_signal_clone = stream_signal.clone();
1863
1864 let clock = get_atomic_clock_realtime();
1865 let base_id = clock.get_time_ms() + 1_000_000;
1866
1867 let handle = tokio::spawn(async move {
1868 stream_messages(
1869 tx,
1870 RedisMessageBusConfig::default(),
1871 external_streams,
1872 stream_signal_clone,
1873 )
1874 .await
1875 .unwrap();
1876 });
1877
1878 let _: () = con
1880 .xadd(
1881 &stream_key1,
1882 format!("{}", base_id + 100),
1883 &[("topic", "s1m1"), ("payload", "data")],
1884 )
1885 .await
1886 .unwrap();
1887 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1888 assert_eq!(msg.topic, "s1m1");
1889
1890 let _: () = con
1892 .xadd(
1893 &stream_key2,
1894 format!("{}", base_id + 50),
1895 &[("topic", "s2m1"), ("payload", "data")],
1896 )
1897 .await
1898 .unwrap();
1899 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1900 assert_eq!(msg.topic, "s2m1");
1901
1902 let _: () = con
1904 .xadd(
1905 &stream_key3,
1906 format!("{}", base_id + 25),
1907 &[("topic", "s3m1"), ("payload", "data")],
1908 )
1909 .await
1910 .unwrap();
1911 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1912 assert_eq!(msg.topic, "s3m1");
1913
1914 rx.close();
1916 stream_signal.store(true, Ordering::Relaxed);
1917 handle.await.unwrap();
1918 }
1919
1920 #[rstest]
1921 #[tokio::test(flavor = "multi_thread")]
1922 async fn test_close() {
1923 let trader_id = TraderId::from("tester-001");
1924 let instance_id = UUID4::new();
1925 let config = MessageBusConfig {
1926 use_instance_id: true,
1927 ..Default::default()
1928 };
1929
1930 let mut db = RedisMessageBusBacking::new(
1931 trader_id,
1932 instance_id,
1933 config,
1934 RedisMessageBusConfig::default(),
1935 )
1936 .unwrap();
1937
1938 MessageBusBacking::close(&mut db);
1940 }
1941
1942 #[rstest]
1943 #[tokio::test(flavor = "multi_thread")]
1944 async fn test_heartbeat_task() {
1945 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
1946 let signal = Arc::new(AtomicBool::new(false));
1947
1948 let handle = tokio::spawn(run_heartbeat(1, signal.clone(), tx));
1950
1951 let heartbeat = receive_unbounded_bus_message(&mut rx, Duration::from_secs(2)).await;
1952
1953 signal.store(true, Ordering::Relaxed);
1955 handle.await.unwrap();
1956
1957 assert_eq!(heartbeat.topic, HEARTBEAT_TOPIC);
1959 }
1960
1961 async fn receive_bus_message(
1962 rx: &mut tokio::sync::mpsc::Receiver<BusMessage>,
1963 timeout: Duration,
1964 ) -> BusMessage {
1965 let mut received = None;
1966
1967 wait_until_async(
1968 || {
1969 if received.is_none() {
1970 received = rx.try_recv().ok();
1971 }
1972
1973 let has_received = received.is_some();
1974 async move { has_received }
1975 },
1976 timeout,
1977 )
1978 .await;
1979
1980 received.unwrap()
1981 }
1982
1983 async fn receive_unbounded_bus_message(
1984 rx: &mut tokio::sync::mpsc::UnboundedReceiver<BusMessage>,
1985 timeout: Duration,
1986 ) -> BusMessage {
1987 let mut received = None;
1988
1989 wait_until_async(
1990 || {
1991 if received.is_none() {
1992 received = rx.try_recv().ok();
1993 }
1994
1995 let has_received = received.is_some();
1996 async move { has_received }
1997 },
1998 timeout,
1999 )
2000 .await;
2001
2002 received.unwrap()
2003 }
2004}