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 jiff::{Timestamp, tz::Offset};
44use nautilus_common::{
45 enums::SerializationEncoding,
46 live::get_runtime,
47 logging::{log_task_error, log_task_started, log_task_stopped},
48 msgbus::{
49 BusMessage, BusPayloadType, MessageBusBacking, MessageBusBackingFactory, MessageBusConfig,
50 switchboard::CLOSE_TOPIC,
51 },
52};
53use nautilus_core::{
54 UUID4,
55 time::{duration_since_unix_epoch, get_atomic_clock_realtime},
56};
57use nautilus_cryptography::providers::install_cryptographic_provider;
58use nautilus_model::identifiers::TraderId;
59use redis::{AsyncCommands, RetryMethod, aio::ConnectionManager, streams};
60use serde::{Deserialize, Serialize};
61use streams::StreamReadOptions;
62use ustr::Ustr;
63
64use super::{
65 REDIS_MINID, REDIS_XTRIM, await_handle,
66 stream_fields::{PAYLOAD_KIND_FIELD, PAYLOAD_KIND_TYPED, bus_message_fields},
67};
68use crate::redis::{RedisConnectionConfig, create_redis_connection, get_stream_key};
69
70const MSGBUS_PUBLISH: &str = "msgbus-publish";
71const MSGBUS_STREAM: &str = "msgbus-stream";
72const MSGBUS_HEARTBEAT: &str = "msgbus-heartbeat";
73const HEARTBEAT_TOPIC: &str = "health:heartbeat";
74const TRIM_BUFFER_SECS: u64 = 60;
75
76type RedisStreamBulk = Vec<HashMap<String, Vec<HashMap<String, redis::Value>>>>;
77
78#[cfg_attr(
82 feature = "python",
83 expect(
84 clippy::unsafe_derive_deserialize,
85 reason = "config deserializes plain fields; unsafe methods come from generated PyO3 integration"
86 )
87)]
88#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(default, deny_unknown_fields)]
90#[cfg_attr(
91 feature = "python",
92 pyo3::pyclass(module = "nautilus_trader.infrastructure", from_py_object)
93)]
94#[cfg_attr(
95 feature = "python",
96 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.infrastructure")
97)]
98pub struct RedisMessageBusConfig {
99 pub host: Option<String>,
101 pub port: Option<u16>,
103 pub username: Option<String>,
105 pub password: Option<String>,
107 pub ssl: bool,
109 pub connection_timeout: u16,
111 pub response_timeout: u16,
113 pub number_of_retries: usize,
115 pub exponent_base: u64,
117 pub max_delay: u64,
119 pub factor: u64,
121}
122
123impl Debug for RedisMessageBusConfig {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 let redacted = self.password.as_ref().map(|_| "***");
126 f.debug_struct(stringify!(RedisMessageBusConfig))
127 .field("host", &self.host)
128 .field("port", &self.port)
129 .field("username", &self.username)
130 .field("password", &redacted)
131 .field("ssl", &self.ssl)
132 .field("connection_timeout", &self.connection_timeout)
133 .field("response_timeout", &self.response_timeout)
134 .field("number_of_retries", &self.number_of_retries)
135 .field("exponent_base", &self.exponent_base)
136 .field("max_delay", &self.max_delay)
137 .field("factor", &self.factor)
138 .finish()
139 }
140}
141
142impl Default for RedisMessageBusConfig {
143 fn default() -> Self {
144 Self {
145 host: None,
146 port: None,
147 username: None,
148 password: None,
149 ssl: false,
150 connection_timeout: 20,
151 response_timeout: 20,
152 number_of_retries: 100,
153 exponent_base: 2,
154 max_delay: 1000,
155 factor: 2,
156 }
157 }
158}
159
160impl RedisConnectionConfig for RedisMessageBusConfig {
161 fn host(&self) -> Option<&str> {
162 self.host.as_deref()
163 }
164
165 fn port(&self) -> Option<u16> {
166 self.port
167 }
168
169 fn username(&self) -> Option<&str> {
170 self.username.as_deref()
171 }
172
173 fn password(&self) -> Option<&str> {
174 self.password.as_deref()
175 }
176
177 fn ssl(&self) -> bool {
178 self.ssl
179 }
180
181 fn connection_timeout(&self) -> u16 {
182 self.connection_timeout
183 }
184
185 fn response_timeout(&self) -> u16 {
186 self.response_timeout
187 }
188
189 fn number_of_retries(&self) -> usize {
190 self.number_of_retries
191 }
192
193 fn exponent_base(&self) -> u64 {
194 self.exponent_base
195 }
196
197 fn max_delay(&self) -> u64 {
198 self.max_delay
199 }
200
201 fn factor(&self) -> u64 {
202 self.factor
203 }
204}
205
206impl MessageBusBackingFactory for RedisMessageBusConfig {
207 fn create(
208 &self,
209 trader_id: TraderId,
210 instance_id: UUID4,
211 config: MessageBusConfig,
212 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
213 Ok(Box::new(RedisMessageBusBacking::new(
214 trader_id,
215 instance_id,
216 config,
217 self.clone(),
218 )?))
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct RedisMessageBusFactory {
225 config: RedisMessageBusConfig,
226}
227
228impl RedisMessageBusFactory {
229 #[must_use]
231 pub const fn new(config: RedisMessageBusConfig) -> Self {
232 Self { config }
233 }
234}
235
236impl MessageBusBackingFactory for RedisMessageBusFactory {
237 fn create(
238 &self,
239 trader_id: TraderId,
240 instance_id: UUID4,
241 config: MessageBusConfig,
242 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
243 self.config.create(trader_id, instance_id, config)
244 }
245}
246
247pub struct RedisMessageBusBacking {
248 pub trader_id: TraderId,
250 pub instance_id: UUID4,
252 pub_tx: tokio::sync::mpsc::UnboundedSender<BusMessage>,
253 pub_handle: Option<tokio::task::JoinHandle<()>>,
254 stream_rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
255 stream_handle: Option<tokio::task::JoinHandle<()>>,
256 stream_signal: Arc<AtomicBool>,
257 heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
258 heartbeat_signal: Arc<AtomicBool>,
259}
260
261impl Debug for RedisMessageBusBacking {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 f.debug_struct(stringify!(RedisMessageBusBacking))
264 .field("trader_id", &self.trader_id)
265 .field("instance_id", &self.instance_id)
266 .finish_non_exhaustive()
267 }
268}
269
270impl RedisMessageBusBacking {
271 pub fn new(
277 trader_id: TraderId,
278 instance_id: UUID4,
279 config: MessageBusConfig,
280 backing: RedisMessageBusConfig,
281 ) -> anyhow::Result<Self> {
282 install_cryptographic_provider();
283
284 if config.heartbeat_interval_secs == Some(0) {
285 anyhow::bail!("heartbeat_interval_secs must be greater than 0");
286 }
287
288 let external_streams = config.external_streams.clone().unwrap_or_default();
289 let heartbeat_interval_secs = config.heartbeat_interval_secs;
290 let publish = backing.clone();
291
292 let (pub_tx, pub_rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
293
294 let pub_handle = Some(get_runtime().spawn(async move {
296 if let Err(e) = publish_messages(pub_rx, trader_id, instance_id, config, publish).await
297 {
298 log_task_error(MSGBUS_PUBLISH, &e);
299 }
300 }));
301
302 let stream_signal = Arc::new(AtomicBool::new(false));
304 let (stream_rx, stream_handle) = if external_streams.is_empty() {
305 (None, None)
306 } else {
307 let stream_signal_clone = stream_signal.clone();
308 let (stream_tx, stream_rx) = tokio::sync::mpsc::channel::<BusMessage>(100_000);
309 (
310 Some(stream_rx),
311 Some(get_runtime().spawn(async move {
312 if let Err(e) = stream_messages(
313 stream_tx,
314 backing.clone(),
315 external_streams,
316 stream_signal_clone,
317 )
318 .await
319 {
320 log_task_error(MSGBUS_STREAM, &e);
321 }
322 })),
323 )
324 };
325
326 let heartbeat_signal = Arc::new(AtomicBool::new(false));
328 let heartbeat_handle = if let Some(heartbeat_interval_secs) = heartbeat_interval_secs {
329 let signal = heartbeat_signal.clone();
330 let pub_tx_clone = pub_tx.clone();
331
332 Some(get_runtime().spawn(async move {
333 run_heartbeat(heartbeat_interval_secs, signal, pub_tx_clone).await;
334 }))
335 } else {
336 None
337 };
338
339 Ok(Self {
340 trader_id,
341 instance_id,
342 pub_tx,
343 pub_handle,
344 stream_rx,
345 stream_handle,
346 stream_signal,
347 heartbeat_handle,
348 heartbeat_signal,
349 })
350 }
351}
352
353impl MessageBusBacking for RedisMessageBusBacking {
354 fn is_closed(&self) -> bool {
356 self.pub_tx.is_closed()
357 }
358
359 fn publish(&self, message: BusMessage) {
361 if let Err(e) = self.pub_tx.send(message) {
362 log::error!("Failed to send message: {e}");
363 }
364 }
365
366 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
367 self.get_stream_receiver()
368 }
369
370 fn close(&mut self) {
372 log::debug!("Closing");
373
374 self.stream_signal.store(true, Ordering::Relaxed);
375 self.heartbeat_signal.store(true, Ordering::Relaxed);
376
377 if !self.pub_tx.is_closed() {
378 let msg = BusMessage::new_close();
379
380 if let Err(e) = self.pub_tx.send(msg) {
381 log::warn!("Failed to send close message: {e:?}");
382 }
383 }
384
385 tokio::task::block_in_place(|| {
387 get_runtime().block_on(async {
388 self.close_async().await;
389 });
390 });
391
392 log::debug!("Closed");
393 }
394}
395
396impl RedisMessageBusBacking {
397 pub fn get_stream_receiver(
403 &mut self,
404 ) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
405 self.stream_rx
406 .take()
407 .ok_or_else(|| anyhow::anyhow!("Stream receiver already taken"))
408 }
409
410 pub fn stream(
412 mut stream_rx: tokio::sync::mpsc::Receiver<BusMessage>,
413 ) -> impl Stream<Item = BusMessage> + 'static {
414 async_stream::stream! {
415 while let Some(msg) = stream_rx.recv().await {
416 yield msg;
417 }
418 }
419 }
420
421 pub async fn close_async(&mut self) {
422 await_handle(self.pub_handle.take(), MSGBUS_PUBLISH).await;
423 await_handle(self.stream_handle.take(), MSGBUS_STREAM).await;
424 await_handle(self.heartbeat_handle.take(), MSGBUS_HEARTBEAT).await;
425 }
426}
427
428pub async fn publish_messages(
437 mut rx: tokio::sync::mpsc::UnboundedReceiver<BusMessage>,
438 trader_id: TraderId,
439 instance_id: UUID4,
440 config: MessageBusConfig,
441 backing: RedisMessageBusConfig,
442) -> anyhow::Result<()> {
443 log_task_started(MSGBUS_PUBLISH);
444
445 let mut con = create_redis_connection(MSGBUS_PUBLISH, &backing).await?;
446 let stream_key = get_stream_key(trader_id, instance_id, &config);
447
448 let autotrim_duration = config
450 .autotrim_mins
451 .filter(|&mins| mins > 0)
452 .map(|mins| Duration::from_secs(u64::from(mins) * 60));
453 let autotrim_maxlen = config
454 .autotrim_maxlen
455 .filter(|&maxlen| maxlen > 0)
456 .map(usize::try_from)
457 .transpose()?;
458 let mut last_trim_index: HashMap<String, usize> = HashMap::new();
459
460 let mut buffer: VecDeque<BusMessage> = VecDeque::new();
462 let buffer_interval = Duration::from_millis(u64::from(config.buffer_interval_ms.unwrap_or(0)));
463
464 let flush_timer = tokio::time::sleep(buffer_interval);
468 tokio::pin!(flush_timer);
469
470 loop {
471 tokio::select! {
472 maybe_msg = rx.recv() => {
473 if let Some(msg) = maybe_msg {
474 if msg.topic == CLOSE_TOPIC {
475 log::debug!("Received close message");
476 if !buffer.is_empty() {
478 drain_buffer(
479 &mut con,
480 &stream_key,
481 config.stream_per_topic,
482 autotrim_duration,
483 autotrim_maxlen,
484 &mut last_trim_index,
485 &mut buffer,
486 ).await?;
487 }
488 break;
489 }
490
491 buffer.push_back(msg);
492
493 if buffer_interval.is_zero() {
494 drain_buffer(
496 &mut con,
497 &stream_key,
498 config.stream_per_topic,
499 autotrim_duration,
500 autotrim_maxlen,
501 &mut last_trim_index,
502 &mut buffer,
503 ).await?;
504 }
505 } else {
506 log::debug!("Channel hung up");
507 break;
508 }
509 }
510 () = &mut flush_timer, if !buffer_interval.is_zero() => {
513 if !buffer.is_empty() {
514 drain_buffer(
515 &mut con,
516 &stream_key,
517 config.stream_per_topic,
518 autotrim_duration,
519 autotrim_maxlen,
520 &mut last_trim_index,
521 &mut buffer,
522 ).await?;
523 }
524
525 flush_timer.as_mut().reset(tokio::time::Instant::now() + buffer_interval);
527 }
528 }
529 }
530
531 if !buffer.is_empty() {
533 drain_buffer(
534 &mut con,
535 &stream_key,
536 config.stream_per_topic,
537 autotrim_duration,
538 autotrim_maxlen,
539 &mut last_trim_index,
540 &mut buffer,
541 )
542 .await?;
543 }
544
545 log_task_stopped(MSGBUS_PUBLISH);
546 Ok(())
547}
548
549async fn drain_buffer(
550 conn: &mut redis::aio::ConnectionManager,
551 stream_key: &str,
552 stream_per_topic: bool,
553 autotrim_duration: Option<Duration>,
554 autotrim_maxlen: Option<usize>,
555 last_trim_index: &mut HashMap<String, usize>,
556 buffer: &mut VecDeque<BusMessage>,
557) -> anyhow::Result<()> {
558 let mut pipe = redis::pipe();
559 pipe.atomic();
560
561 for msg in buffer.drain(..) {
562 let encoding = msg.encoding.to_string();
563 let items = bus_message_fields(&msg, &encoding);
564 let stream_key = if stream_per_topic {
565 format!("{stream_key}:{}", msg.topic)
566 } else {
567 stream_key.to_string()
568 };
569
570 if let Some(maxlen) = autotrim_maxlen {
571 pipe.xadd_maxlen(
572 &stream_key,
573 streams::StreamMaxlen::Approx(maxlen),
574 "*",
575 items.as_slice(),
576 );
577 } else {
578 pipe.xadd(&stream_key, "*", items.as_slice());
579 }
580
581 if autotrim_duration.is_none() {
582 continue; }
584
585 let last_trim_ms = last_trim_index.entry(stream_key.clone()).or_insert(0); let unix_duration_now = duration_since_unix_epoch();
588 let trim_buffer = Duration::from_secs(TRIM_BUFFER_SECS);
589
590 if *last_trim_ms < unix_duration_now.saturating_sub(trim_buffer).as_millis() as usize {
592 let min_timestamp_ms = unix_duration_now
593 .saturating_sub(autotrim_duration.unwrap())
594 .as_millis() as usize;
595 let result: Result<(), redis::RedisError> = redis::cmd(REDIS_XTRIM)
596 .arg(stream_key.clone())
597 .arg(REDIS_MINID)
598 .arg(min_timestamp_ms)
599 .query_async(conn)
600 .await;
601
602 if let Err(e) = result {
603 log::error!("Error trimming stream '{stream_key}': {e}");
604 } else {
605 last_trim_index.insert(stream_key.clone(), unix_duration_now.as_millis() as usize);
606 }
607 }
608 }
609
610 pipe.query_async(conn).await.map_err(anyhow::Error::from)
611}
612
613pub async fn stream_messages(
621 tx: tokio::sync::mpsc::Sender<BusMessage>,
622 config: RedisMessageBusConfig,
623 stream_keys: Vec<String>,
624 stream_signal: Arc<AtomicBool>,
625) -> anyhow::Result<()> {
626 log_task_started(MSGBUS_STREAM);
627
628 let Some(mut con) = connect_stream_connection(&config, &stream_signal).await? else {
629 log_task_stopped(MSGBUS_STREAM);
630 return Ok(());
631 };
632
633 let mut read_error_count = 0;
634
635 let stream_keys = &stream_keys
636 .iter()
637 .map(String::as_str)
638 .collect::<Vec<&str>>();
639
640 log::debug!("Listening to streams: [{}]", stream_keys.join(", "));
641
642 let clock = get_atomic_clock_realtime();
644 let timestamp_ms = clock.get_time_ms();
645 let initial_id = timestamp_ms.to_string();
646
647 let mut last_ids: HashMap<String, String> = stream_keys
648 .iter()
649 .map(|&key| (key.to_string(), initial_id.clone()))
650 .collect();
651
652 let opts = StreamReadOptions::default().block(100);
653
654 'outer: loop {
655 if stream_signal.load(Ordering::Relaxed) {
656 log::debug!("Received streaming terminate signal");
657 break;
658 }
659
660 let ids: Vec<String> = stream_keys
661 .iter()
662 .map(|&key| last_ids[key].clone())
663 .collect();
664 let id_refs: Vec<&str> = ids.iter().map(String::as_str).collect();
665
666 let result: Result<RedisStreamBulk, _> =
667 con.xread_options(&[&stream_keys], &[&id_refs], &opts).await;
668
669 match result {
670 Ok(stream_bulk) => {
671 read_error_count = 0;
672
673 if stream_bulk.is_empty() {
674 continue;
676 }
677
678 for entry in &stream_bulk {
679 for (stream_key, stream_msgs) in entry {
680 for stream_msg in stream_msgs {
681 for (id, array) in stream_msg {
682 last_ids.insert(stream_key.clone(), id.clone());
683
684 match decode_bus_message(array) {
685 Ok(msg) => {
686 if let Err(e) = tx.send(msg).await {
687 log::debug!("Channel closed: {e:?}");
688 break 'outer; }
690 }
691 Err(e) => {
692 log::error!("{e:?}");
693 }
694 }
695 }
696 }
697 }
698 }
699 }
700 Err(e) => {
701 if !is_retryable_stream_error(&e) {
702 anyhow::bail!("Error reading from stream: {e:?}");
703 }
704
705 log::error!("Error reading from stream: {e:?}");
706
707 let Some(reconnected) =
708 reconnect_stream_connection(&config, &stream_signal, &mut read_error_count)
709 .await?
710 else {
711 break;
712 };
713 con = reconnected;
714 }
715 }
716 }
717
718 log_task_stopped(MSGBUS_STREAM);
719 Ok(())
720}
721
722async fn connect_stream_connection(
723 config: &RedisMessageBusConfig,
724 stream_signal: &Arc<AtomicBool>,
725) -> anyhow::Result<Option<ConnectionManager>> {
726 let connect = create_redis_connection(MSGBUS_STREAM, config);
727 let terminate = wait_for_stream_signal(stream_signal);
728
729 tokio::pin!(connect);
730 tokio::pin!(terminate);
731
732 tokio::select! {
733 result = &mut connect => result.map(Some),
734 () = &mut terminate => Ok(None),
735 }
736}
737
738async fn reconnect_stream_connection(
739 config: &RedisMessageBusConfig,
740 stream_signal: &Arc<AtomicBool>,
741 read_error_count: &mut usize,
742) -> anyhow::Result<Option<ConnectionManager>> {
743 loop {
744 let retry_delay = stream_retry_delay(config, *read_error_count);
745 *read_error_count = (*read_error_count).saturating_add(1);
746
747 if !wait_for_retry_delay(retry_delay, stream_signal).await {
748 return Ok(None);
749 }
750
751 match connect_stream_connection(config, stream_signal).await {
752 Ok(Some(con)) => return Ok(Some(con)),
753 Ok(None) => return Ok(None),
754 Err(e) => {
755 log::error!("Error reconnecting to stream: {e:?}");
756 }
757 }
758 }
759}
760
761fn stream_retry_delay(config: &RedisMessageBusConfig, attempt: usize) -> Duration {
762 let exponent = u32::try_from(attempt.min(32)).unwrap_or(32);
763 let delay_ms = config
764 .factor
765 .saturating_mul(config.exponent_base.saturating_pow(exponent));
766 let max_delay = Duration::from_secs(config.max_delay);
767
768 Duration::from_millis(delay_ms)
769 .min(max_delay)
770 .max(Duration::from_millis(1))
771}
772
773fn is_retryable_stream_error(error: &redis::RedisError) -> bool {
774 matches!(
775 error.retry_method(),
776 RetryMethod::Reconnect
777 | RetryMethod::ReconnectFromInitialConnections
778 | RetryMethod::RetryImmediately
779 | RetryMethod::WaitAndRetry
780 )
781}
782
783async fn wait_for_retry_delay(retry_delay: Duration, stream_signal: &Arc<AtomicBool>) -> bool {
784 let retry_timer = tokio::time::sleep(retry_delay);
785 let terminate = wait_for_stream_signal(stream_signal);
786
787 tokio::pin!(retry_timer);
788 tokio::pin!(terminate);
789
790 tokio::select! {
791 () = &mut retry_timer => true,
792 () = &mut terminate => false,
793 }
794}
795
796async fn wait_for_stream_signal(stream_signal: &Arc<AtomicBool>) {
797 let check_timer = tokio::time::interval(Duration::from_millis(100));
798
799 tokio::pin!(check_timer);
800
801 while !stream_signal.load(Ordering::Relaxed) {
802 check_timer.tick().await;
803 }
804}
805
806fn decode_bus_message(stream_msg: &redis::Value) -> anyhow::Result<BusMessage> {
808 let redis::Value::Array(fields) = stream_msg else {
809 anyhow::bail!("Invalid stream message format: {stream_msg:?}");
810 };
811
812 if fields.len() < 4 || fields.len() % 2 != 0 {
813 anyhow::bail!("Invalid stream message format: {stream_msg:?}");
814 }
815
816 let mut topic: Option<String> = None;
817 let mut type_name: Option<String> = None;
818 let mut typed_payload = false;
819 let mut encoding = SerializationEncoding::default();
820 let mut payload: Option<Bytes> = None;
821
822 for pair in fields.as_chunks::<2>().0 {
823 let redis::Value::BulkString(key) = &pair[0] else {
824 anyhow::bail!("Invalid stream field key: {stream_msg:?}");
825 };
826
827 match key.as_slice() {
828 b"topic" => {
829 let redis::Value::BulkString(bytes) = &pair[1] else {
830 anyhow::bail!("Invalid topic format: {stream_msg:?}");
831 };
832 topic = Some(
833 String::from_utf8(bytes.clone())
834 .map_err(|e| anyhow::anyhow!("Error parsing topic: {e}"))?,
835 );
836 }
837 b"type" => {
838 let redis::Value::BulkString(bytes) = &pair[1] else {
839 anyhow::bail!("Invalid type format: {stream_msg:?}");
840 };
841 type_name = Some(
842 String::from_utf8(bytes.clone())
843 .map_err(|e| anyhow::anyhow!("Error parsing type: {e}"))?,
844 );
845 }
846 key if key == PAYLOAD_KIND_FIELD.as_bytes() => {
847 let redis::Value::BulkString(bytes) = &pair[1] else {
848 anyhow::bail!("Invalid payload kind format: {stream_msg:?}");
849 };
850 let value = std::str::from_utf8(bytes)
851 .map_err(|e| anyhow::anyhow!("Error parsing payload kind: {e}"))?;
852 anyhow::ensure!(
853 value == PAYLOAD_KIND_TYPED,
854 "Unknown payload kind '{value}'"
855 );
856 typed_payload = true;
857 }
858 b"encoding" => {
859 let redis::Value::BulkString(bytes) = &pair[1] else {
860 anyhow::bail!("Invalid encoding format: {stream_msg:?}");
861 };
862 let value = std::str::from_utf8(bytes)
863 .map_err(|e| anyhow::anyhow!("Error parsing encoding: {e}"))?;
864 encoding = value
865 .parse()
866 .map_err(|e| anyhow::anyhow!("Error parsing encoding: {e}"))?;
867 }
868 b"payload" => {
869 let redis::Value::BulkString(bytes) = &pair[1] else {
870 anyhow::bail!("Invalid payload format: {stream_msg:?}");
871 };
872 payload = Some(Bytes::copy_from_slice(bytes));
873 }
874 _ => {}
875 }
876 }
877
878 let Some(topic) = topic else {
879 anyhow::bail!("Stream message missing topic: {stream_msg:?}");
880 };
881 let Some(payload) = payload else {
882 anyhow::bail!("Stream message missing payload: {stream_msg:?}");
883 };
884 let payload_type = match type_name {
885 Some(type_name) if typed_payload => BusPayloadType::from_typed_name(&type_name)
886 .ok_or_else(|| anyhow::anyhow!("Unknown typed payload '{type_name}'"))?,
887 Some(type_name) => BusPayloadType::from_name(&type_name),
888 None if typed_payload => {
889 anyhow::bail!("Typed stream message missing type: {stream_msg:?}")
890 }
891 None => BusPayloadType::Custom(Ustr::default()),
892 };
893
894 Ok(BusMessage::with_str_topic(
895 topic,
896 payload_type,
897 payload,
898 encoding,
899 ))
900}
901
902async fn run_heartbeat(
903 heartbeat_interval_secs: u16,
904 signal: Arc<AtomicBool>,
905 pub_tx: tokio::sync::mpsc::UnboundedSender<BusMessage>,
906) {
907 log_task_started("heartbeat");
908 log::debug!("Heartbeat at {heartbeat_interval_secs} second intervals");
909
910 let heartbeat_interval = Duration::from_secs(u64::from(heartbeat_interval_secs));
911 let heartbeat_timer = tokio::time::interval(heartbeat_interval);
912
913 let check_interval = Duration::from_millis(100);
914 let check_timer = tokio::time::interval(check_interval);
915
916 tokio::pin!(heartbeat_timer);
917 tokio::pin!(check_timer);
918
919 loop {
920 if signal.load(Ordering::Relaxed) {
921 log::debug!("Received heartbeat terminate signal");
922 break;
923 }
924
925 tokio::select! {
926 _ = heartbeat_timer.tick() => {
927 let heartbeat = create_heartbeat_msg();
928 if let Err(e) = pub_tx.send(heartbeat) {
929 log::debug!("Error sending heartbeat: {e}");
931 }
932 },
933 _ = check_timer.tick() => {}
934 }
935 }
936
937 log_task_stopped("heartbeat");
938}
939
940fn create_heartbeat_msg() -> BusMessage {
941 let payload = Bytes::from(
942 Timestamp::now()
943 .display_with_offset(Offset::UTC)
944 .to_string()
945 .into_bytes(),
946 );
947 BusMessage::with_str_topic(
948 HEARTBEAT_TOPIC,
949 BusPayloadType::Custom(Ustr::default()),
950 payload,
951 SerializationEncoding::default(),
952 )
953}
954
955#[cfg(test)]
956mod tests {
957 use nautilus_common::{msgbus::external_io_from_backing, testing::wait_until_async};
958 use redis::Value;
959 use rstest::*;
960 use serde_json::json;
961
962 use super::*;
963
964 #[rstest]
965 fn test_default_redis_message_bus_config() {
966 let config = RedisMessageBusConfig::default();
967
968 assert_eq!(config.host, None);
969 assert_eq!(config.port, None);
970 assert_eq!(config.username, None);
971 assert_eq!(config.password, None);
972 assert!(!config.ssl);
973 assert_eq!(config.connection_timeout, 20);
974 assert_eq!(config.response_timeout, 20);
975 assert_eq!(config.number_of_retries, 100);
976 assert_eq!(config.exponent_base, 2);
977 assert_eq!(config.max_delay, 1000);
978 assert_eq!(config.factor, 2);
979 }
980
981 #[rstest]
982 fn test_deserialize_redis_message_bus_config() {
983 let config_json = json!({
984 "host": "localhost",
985 "port": 6379,
986 "username": "user",
987 "password": "pass",
988 "ssl": true,
989 "connection_timeout": 30,
990 "response_timeout": 10,
991 "number_of_retries": 3,
992 "exponent_base": 2,
993 "max_delay": 10,
994 "factor": 2
995 });
996
997 let config: RedisMessageBusConfig = serde_json::from_value(config_json).unwrap();
998
999 assert_eq!(config.host, Some("localhost".to_string()));
1000 assert_eq!(config.port, Some(6379));
1001 assert_eq!(config.username, Some("user".to_string()));
1002 assert_eq!(config.password, Some("pass".to_string()));
1003 assert!(config.ssl);
1004 assert_eq!(config.connection_timeout, 30);
1005 assert_eq!(config.response_timeout, 10);
1006 assert_eq!(config.number_of_retries, 3);
1007 assert_eq!(config.exponent_base, 2);
1008 assert_eq!(config.max_delay, 10);
1009 assert_eq!(config.factor, 2);
1010 }
1011
1012 #[rstest]
1013 fn test_deserialize_redis_message_bus_config_rejects_type_selector() {
1014 let config_json = json!({
1015 "type": "redis",
1016 });
1017
1018 let error = serde_json::from_value::<RedisMessageBusConfig>(config_json).unwrap_err();
1019
1020 assert!(error.to_string().contains("unknown field `type`"));
1021 }
1022
1023 #[rstest]
1024 fn test_bus_message_fields_distinguish_custom_and_typed_names() {
1025 let custom = BusMessage::with_str_topic(
1026 "external.test",
1027 BusPayloadType::Custom(Ustr::from("TradingCommand")),
1028 Bytes::from_static(b"custom"),
1029 SerializationEncoding::Json,
1030 );
1031 let typed = BusMessage::with_str_topic(
1032 "external.test",
1033 BusPayloadType::TradingCommand,
1034 Bytes::from_static(b"typed"),
1035 SerializationEncoding::Json,
1036 );
1037
1038 let custom_fields = bus_message_fields(&custom, "json");
1039 let typed_fields = bus_message_fields(&typed, "json");
1040 let custom_fields = custom_fields.as_slice();
1041 let typed_fields = typed_fields.as_slice();
1042
1043 assert_eq!(custom_fields.len(), 4);
1044 assert_eq!(typed_fields.len(), 5);
1045 assert_eq!(
1046 custom_fields
1047 .iter()
1048 .find(|(key, _)| *key == "type")
1049 .expect("custom fields must include type")
1050 .1,
1051 b"TradingCommand"
1052 );
1053 assert_eq!(
1054 typed_fields
1055 .iter()
1056 .find(|(key, _)| *key == "type")
1057 .expect("typed fields must include type")
1058 .1,
1059 b"TradingCommand"
1060 );
1061 assert!(
1062 custom_fields
1063 .iter()
1064 .all(|(key, _)| *key != PAYLOAD_KIND_FIELD)
1065 );
1066 assert_eq!(
1067 typed_fields
1068 .iter()
1069 .find(|(key, _)| *key == PAYLOAD_KIND_FIELD)
1070 .expect("typed fields must include payload kind")
1071 .1,
1072 PAYLOAD_KIND_TYPED.as_bytes()
1073 );
1074 }
1075
1076 #[rstest]
1077 fn test_decode_bus_message_valid() {
1078 let stream_msg = Value::Array(vec![
1079 Value::BulkString(b"topic".to_vec()),
1080 Value::BulkString(b"topic1".to_vec()),
1081 Value::BulkString(b"type".to_vec()),
1082 Value::BulkString(b"QuoteTick".to_vec()),
1083 Value::BulkString(b"payload".to_vec()),
1084 Value::BulkString(b"data1".to_vec()),
1085 Value::BulkString(b"encoding".to_vec()),
1086 Value::BulkString(b"msgpack".to_vec()),
1087 ]);
1088
1089 let result = decode_bus_message(&stream_msg);
1090 assert!(result.is_ok());
1091 let msg = result.unwrap();
1092 assert_eq!(msg.topic, "topic1");
1093 assert_eq!(msg.payload_type, BusPayloadType::QuoteTick);
1094 assert_eq!(msg.encoding, SerializationEncoding::MsgPack);
1095 assert_eq!(msg.payload, Bytes::from("data1"));
1096 }
1097
1098 #[rstest]
1099 fn test_decode_bus_message_defaults_legacy_headers() {
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"payload".to_vec()),
1104 Value::BulkString(b"data1".to_vec()),
1105 ]);
1106
1107 let result = decode_bus_message(&stream_msg);
1108 assert!(result.is_ok());
1109 let msg = result.unwrap();
1110 assert_eq!(msg.topic, "topic1");
1111 assert_eq!(msg.payload_type, BusPayloadType::Custom(Ustr::default()));
1112 assert_eq!(msg.encoding, SerializationEncoding::Json);
1113 assert_eq!(msg.payload, Bytes::from("data1"));
1114 }
1115
1116 #[rstest]
1117 fn test_decode_bus_message_reads_chrono_heartbeat() {
1118 let heartbeat =
1119 include_str!("../../test_data/redis_msgbus_heartbeat_chrono.txt").trim_end();
1120 let stream_msg = Value::Array(vec![
1121 Value::BulkString(b"topic".to_vec()),
1122 Value::BulkString(HEARTBEAT_TOPIC.as_bytes().to_vec()),
1123 Value::BulkString(b"payload".to_vec()),
1124 Value::BulkString(heartbeat.as_bytes().to_vec()),
1125 ]);
1126
1127 let msg = decode_bus_message(&stream_msg).unwrap();
1128 let timestamp = std::str::from_utf8(&msg.payload)
1129 .unwrap()
1130 .parse::<Timestamp>()
1131 .unwrap();
1132
1133 assert_eq!(msg.topic, HEARTBEAT_TOPIC);
1134 assert_eq!(msg.payload_type, BusPayloadType::Custom(Ustr::default()));
1135 assert_eq!(msg.encoding, SerializationEncoding::Json);
1136 assert_eq!(msg.payload.as_ref(), heartbeat.as_bytes());
1137 assert_eq!(timestamp.as_nanosecond(), 1_785_805_323_456_789_000);
1138 }
1139
1140 #[rstest]
1141 fn test_decode_bus_message_typed_name_without_kind_is_custom() {
1142 let stream_msg = Value::Array(vec![
1143 Value::BulkString(b"topic".to_vec()),
1144 Value::BulkString(b"topic1".to_vec()),
1145 Value::BulkString(b"type".to_vec()),
1146 Value::BulkString(b"TradingCommand".to_vec()),
1147 Value::BulkString(b"payload".to_vec()),
1148 Value::BulkString(b"data1".to_vec()),
1149 ]);
1150
1151 let result = decode_bus_message(&stream_msg);
1152 assert!(result.is_ok());
1153 let msg = result.unwrap();
1154 assert_eq!(
1155 msg.payload_type,
1156 BusPayloadType::Custom(Ustr::from("TradingCommand"))
1157 );
1158 assert_eq!(msg.encoding, SerializationEncoding::Json);
1159 }
1160
1161 #[rstest]
1162 fn test_decode_bus_message_typed_kind_resolves_fixed_type() {
1163 let stream_msg = Value::Array(vec![
1164 Value::BulkString(b"topic".to_vec()),
1165 Value::BulkString(b"external.test".to_vec()),
1166 Value::BulkString(b"type".to_vec()),
1167 Value::BulkString(b"TradingCommand".to_vec()),
1168 Value::BulkString(PAYLOAD_KIND_FIELD.as_bytes().to_vec()),
1169 Value::BulkString(PAYLOAD_KIND_TYPED.as_bytes().to_vec()),
1170 Value::BulkString(b"payload".to_vec()),
1171 Value::BulkString(b"data1".to_vec()),
1172 ]);
1173
1174 let msg = decode_bus_message(&stream_msg).expect("typed message must decode");
1175
1176 assert_eq!(msg.topic, "external.test");
1177 assert_eq!(msg.payload_type, BusPayloadType::TradingCommand);
1178 assert_eq!(msg.encoding, SerializationEncoding::Json);
1179 assert_eq!(msg.payload, Bytes::from_static(b"data1"));
1180 }
1181
1182 #[rstest]
1183 fn test_decode_bus_message_rejects_invalid_typed_metadata() {
1184 let unknown_kind = stream_message_with_typed_metadata(
1185 Some("TradingCommand"),
1186 Value::BulkString(b"unknown".to_vec()),
1187 );
1188 let missing_type = stream_message_with_typed_metadata(
1189 None,
1190 Value::BulkString(PAYLOAD_KIND_TYPED.as_bytes().to_vec()),
1191 );
1192 let unknown_type = stream_message_with_typed_metadata(
1193 Some("UnknownPayload"),
1194 Value::BulkString(PAYLOAD_KIND_TYPED.as_bytes().to_vec()),
1195 );
1196 let invalid_kind =
1197 stream_message_with_typed_metadata(Some("TradingCommand"), Value::Int(42));
1198 let cases = [
1199 (&unknown_kind, "Unknown payload kind 'unknown'".to_string()),
1200 (
1201 &missing_type,
1202 format!("Typed stream message missing type: {missing_type:?}"),
1203 ),
1204 (
1205 &unknown_type,
1206 "Unknown typed payload 'UnknownPayload'".to_string(),
1207 ),
1208 (
1209 &invalid_kind,
1210 format!("Invalid payload kind format: {invalid_kind:?}"),
1211 ),
1212 ];
1213
1214 for (stream_msg, expected) in cases {
1215 let error =
1216 decode_bus_message(stream_msg).expect_err("invalid typed metadata must not decode");
1217
1218 assert_eq!(error.to_string(), expected);
1219 }
1220 }
1221
1222 fn stream_message_with_typed_metadata(type_name: Option<&str>, payload_kind: Value) -> Value {
1223 let mut fields = vec![
1224 Value::BulkString(b"topic".to_vec()),
1225 Value::BulkString(b"external.test".to_vec()),
1226 ];
1227
1228 if let Some(type_name) = type_name {
1229 fields.extend([
1230 Value::BulkString(b"type".to_vec()),
1231 Value::BulkString(type_name.as_bytes().to_vec()),
1232 ]);
1233 }
1234 fields.extend([
1235 Value::BulkString(PAYLOAD_KIND_FIELD.as_bytes().to_vec()),
1236 payload_kind,
1237 Value::BulkString(b"payload".to_vec()),
1238 Value::BulkString(b"data1".to_vec()),
1239 ]);
1240 Value::Array(fields)
1241 }
1242
1243 #[rstest]
1244 fn test_decode_bus_message_accepts_unordered_metadata_fields() {
1245 let stream_msg = Value::Array(vec![
1246 Value::BulkString(b"payload".to_vec()),
1247 Value::BulkString(b"data1".to_vec()),
1248 Value::BulkString(b"encoding".to_vec()),
1249 Value::BulkString(b"msgpack".to_vec()),
1250 Value::BulkString(b"type".to_vec()),
1251 Value::BulkString(b"TradeTick".to_vec()),
1252 Value::BulkString(b"topic".to_vec()),
1253 Value::BulkString(b"topic1".to_vec()),
1254 ]);
1255
1256 let msg = decode_bus_message(&stream_msg).unwrap();
1257
1258 assert_eq!(msg.topic, "topic1");
1259 assert_eq!(msg.payload_type, BusPayloadType::TradeTick);
1260 assert_eq!(msg.encoding, SerializationEncoding::MsgPack);
1261 assert_eq!(msg.payload, Bytes::from("data1"));
1262 }
1263
1264 #[rstest]
1265 fn test_decode_bus_message_rejects_invalid_encoding_header() {
1266 let stream_msg = Value::Array(vec![
1267 Value::BulkString(b"topic".to_vec()),
1268 Value::BulkString(b"topic1".to_vec()),
1269 Value::BulkString(b"encoding".to_vec()),
1270 Value::BulkString(b"invalid".to_vec()),
1271 Value::BulkString(b"payload".to_vec()),
1272 Value::BulkString(b"data1".to_vec()),
1273 ]);
1274
1275 let error = decode_bus_message(&stream_msg).unwrap_err();
1276
1277 assert!(
1278 error.to_string().contains("Error parsing encoding"),
1279 "{error:?}"
1280 );
1281 }
1282
1283 #[rstest]
1284 fn test_decode_bus_message_missing_fields() {
1285 let stream_msg = Value::Array(vec![
1286 Value::BulkString(b"0".to_vec()),
1287 Value::BulkString(b"topic1".to_vec()),
1288 ]);
1289
1290 let result = decode_bus_message(&stream_msg);
1291 assert!(result.is_err());
1292 assert_eq!(
1293 format!("{}", result.unwrap_err()),
1294 "Invalid stream message format: array([bulk-string('\"0\"'), bulk-string('\"topic1\"')])"
1295 );
1296 }
1297
1298 #[rstest]
1299 fn test_decode_bus_message_invalid_topic_format() {
1300 let stream_msg = Value::Array(vec![
1301 Value::BulkString(b"topic".to_vec()),
1302 Value::Int(42),
1303 Value::BulkString(b"payload".to_vec()),
1304 Value::BulkString(b"data1".to_vec()),
1305 ]);
1306
1307 let result = decode_bus_message(&stream_msg);
1308 assert!(result.is_err());
1309 assert_eq!(
1310 format!("{}", result.unwrap_err()),
1311 "Invalid topic format: array([bulk-string('\"topic\"'), int(42), bulk-string('\"payload\"'), bulk-string('\"data1\"')])"
1312 );
1313 }
1314
1315 #[rstest]
1316 fn test_decode_bus_message_invalid_type_format() {
1317 let stream_msg = Value::Array(vec![
1318 Value::BulkString(b"topic".to_vec()),
1319 Value::BulkString(b"topic1".to_vec()),
1320 Value::BulkString(b"type".to_vec()),
1321 Value::Int(42),
1322 Value::BulkString(b"payload".to_vec()),
1323 Value::BulkString(b"data1".to_vec()),
1324 ]);
1325
1326 let result = decode_bus_message(&stream_msg);
1327 assert!(result.is_err());
1328 assert_eq!(
1329 format!("{}", result.unwrap_err()),
1330 "Invalid type format: array([bulk-string('\"topic\"'), bulk-string('\"topic1\"'), bulk-string('\"type\"'), int(42), bulk-string('\"payload\"'), bulk-string('\"data1\"')])"
1331 );
1332 }
1333
1334 #[rstest]
1335 fn test_decode_bus_message_invalid_encoding_format() {
1336 let stream_msg = Value::Array(vec![
1337 Value::BulkString(b"topic".to_vec()),
1338 Value::BulkString(b"topic1".to_vec()),
1339 Value::BulkString(b"encoding".to_vec()),
1340 Value::Int(42),
1341 Value::BulkString(b"payload".to_vec()),
1342 Value::BulkString(b"data1".to_vec()),
1343 ]);
1344
1345 let result = decode_bus_message(&stream_msg);
1346 assert!(result.is_err());
1347 assert_eq!(
1348 format!("{}", result.unwrap_err()),
1349 "Invalid encoding format: array([bulk-string('\"topic\"'), bulk-string('\"topic1\"'), bulk-string('\"encoding\"'), int(42), bulk-string('\"payload\"'), bulk-string('\"data1\"')])"
1350 );
1351 }
1352
1353 #[rstest]
1354 fn test_decode_bus_message_invalid_payload_format() {
1355 let stream_msg = Value::Array(vec![
1356 Value::BulkString(b"topic".to_vec()),
1357 Value::BulkString(b"topic1".to_vec()),
1358 Value::BulkString(b"payload".to_vec()),
1359 Value::Int(42),
1360 ]);
1361
1362 let result = decode_bus_message(&stream_msg);
1363 assert!(result.is_err());
1364 assert_eq!(
1365 format!("{}", result.unwrap_err()),
1366 "Invalid payload format: array([bulk-string('\"topic\"'), bulk-string('\"topic1\"'), bulk-string('\"payload\"'), int(42)])"
1367 );
1368 }
1369
1370 #[rstest]
1371 fn test_decode_bus_message_invalid_stream_msg_format() {
1372 let stream_msg = Value::BulkString(b"not an array".to_vec());
1373
1374 let result = decode_bus_message(&stream_msg);
1375 assert!(result.is_err());
1376 assert_eq!(
1377 format!("{}", result.unwrap_err()),
1378 "Invalid stream message format: bulk-string('\"not an array\"')"
1379 );
1380 }
1381
1382 #[rstest]
1383 fn test_new_rejects_zero_heartbeat_interval() {
1384 let trader_id = TraderId::from("tester-001");
1385 let instance_id = UUID4::new();
1386 let config = MessageBusConfig {
1387 heartbeat_interval_secs: Some(0),
1388 ..Default::default()
1389 };
1390
1391 let result = RedisMessageBusBacking::new(
1392 trader_id,
1393 instance_id,
1394 config,
1395 RedisMessageBusConfig::default(),
1396 );
1397
1398 assert!(result.is_err());
1399 assert_eq!(
1400 result.unwrap_err().to_string(),
1401 "heartbeat_interval_secs must be greater than 0"
1402 );
1403 }
1404
1405 #[rstest]
1406 fn test_stream_retry_delay_uses_config_bounds() {
1407 let config = RedisMessageBusConfig {
1408 factor: 10,
1409 exponent_base: 2,
1410 max_delay: 1,
1411 ..Default::default()
1412 };
1413
1414 assert_eq!(stream_retry_delay(&config, 0), Duration::from_millis(10));
1415 assert_eq!(stream_retry_delay(&config, 1), Duration::from_millis(20));
1416 assert_eq!(stream_retry_delay(&config, 10), Duration::from_secs(1));
1417 }
1418
1419 #[rstest]
1420 fn test_stream_error_retry_classification() {
1421 let dropped =
1422 redis::RedisError::from(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
1423 let client: redis::RedisError = (redis::ErrorKind::Client, "client error").into();
1424
1425 assert!(is_retryable_stream_error(&dropped));
1426 assert!(!is_retryable_stream_error(&client));
1427 }
1428
1429 #[tokio::test]
1430 async fn test_wait_for_retry_delay_returns_false_when_signaled() {
1431 let stream_signal = Arc::new(AtomicBool::new(true));
1432 let signal = stream_signal.clone();
1433 let fut = async move { wait_for_retry_delay(Duration::from_secs(30), &signal).await };
1434
1435 let handle = tokio::spawn(fut);
1436
1437 wait_until_async(|| async { handle.is_finished() }, Duration::from_secs(1)).await;
1438
1439 assert!(!handle.await.unwrap());
1440 }
1441
1442 #[rstest]
1443 fn test_external_io_from_backing_takes_stream_receiver() {
1444 let (stream_tx, stream_rx) = tokio::sync::mpsc::channel::<BusMessage>(1);
1445 let backing = backing_with_stream_receiver(stream_rx);
1446 let message = BusMessage::with_str_topic(
1447 "events/data",
1448 BusPayloadType::QuoteTick,
1449 Bytes::from_static(b"payload"),
1450 SerializationEncoding::Json,
1451 );
1452
1453 let (_egress, mut ingress) = external_io_from_backing(Box::new(backing));
1454 stream_tx.try_send(message.clone()).unwrap();
1455 let mut receiver = ingress.take_receiver().unwrap();
1456 let received = receiver.try_recv().unwrap();
1457
1458 assert_eq!(received.topic, message.topic);
1459 assert_eq!(received.payload, message.payload);
1460 assert!(ingress.take_receiver().is_err());
1461 }
1462
1463 fn backing_with_stream_receiver(
1464 stream_rx: tokio::sync::mpsc::Receiver<BusMessage>,
1465 ) -> RedisMessageBusBacking {
1466 let (pub_tx, _pub_rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
1467 RedisMessageBusBacking {
1468 trader_id: TraderId::from("tester-001"),
1469 instance_id: UUID4::new(),
1470 pub_tx,
1471 pub_handle: None,
1472 stream_rx: Some(stream_rx),
1473 stream_handle: None,
1474 stream_signal: Arc::new(AtomicBool::new(false)),
1475 heartbeat_handle: None,
1476 heartbeat_signal: Arc::new(AtomicBool::new(false)),
1477 }
1478 }
1479}
1480
1481#[cfg(target_os = "linux")] #[cfg(test)]
1483mod serial_tests {
1484 use std::{sync::mpsc, thread};
1485
1486 #[cfg(feature = "python")]
1487 use nautilus_common::python::msgbus::get_global_msgbus_factory_registry;
1488 use nautilus_common::{
1489 enums::Environment,
1490 msgbus::{self, TypedHandler},
1491 testing::wait_until_async,
1492 };
1493 use nautilus_live::{
1494 builder::LiveNodeBuilder,
1495 config::{LiveExecutionEngineConfig, LiveNodeConfig},
1496 };
1497 use nautilus_model::data::{QuoteTick, TradeTick};
1498 #[cfg(feature = "python")]
1499 use pyo3::{Py, Python, types::PyModule};
1500 use redis::aio::ConnectionManager;
1501 use rstest::*;
1502
1503 use super::*;
1504
1505 #[fixture]
1506 async fn redis_connection() -> ConnectionManager {
1507 let config = RedisMessageBusConfig {
1508 connection_timeout: 1,
1509 number_of_retries: 0,
1510 ..Default::default()
1511 };
1512 create_redis_connection(MSGBUS_STREAM, &config)
1513 .await
1514 .expect("A running Redis service is required for this test")
1515 }
1516
1517 fn redis_msgbus_factory(config: RedisMessageBusConfig) -> Box<dyn MessageBusBackingFactory> {
1518 #[cfg(feature = "python")]
1519 {
1520 Python::initialize();
1521 Python::attach(|py| {
1522 let module = PyModule::new(py, "infrastructure").unwrap();
1523 crate::python::infrastructure(py, &module).unwrap();
1524 let factory = Py::new(py, config).unwrap().into_any();
1525
1526 get_global_msgbus_factory_registry()
1527 .extract(py, factory)
1528 .unwrap()
1529 })
1530 }
1531
1532 #[cfg(not(feature = "python"))]
1533 Box::new(config)
1534 }
1535
1536 #[rstest]
1537 #[tokio::test(flavor = "multi_thread")]
1538 async fn test_stream_messages_terminate_signal(#[future] redis_connection: ConnectionManager) {
1539 let _con = redis_connection.await;
1540 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1541
1542 let trader_id = TraderId::from("tester-001");
1543 let instance_id = UUID4::new();
1544 let config = MessageBusConfig {
1545 use_instance_id: true,
1546 ..Default::default()
1547 };
1548
1549 let stream_key = get_stream_key(trader_id, instance_id, &config);
1550 let external_streams = vec![stream_key.clone()];
1551 let stream_signal = Arc::new(AtomicBool::new(false));
1552 let stream_signal_clone = stream_signal.clone();
1553
1554 let handle = tokio::spawn(async move {
1556 stream_messages(
1557 tx,
1558 RedisMessageBusConfig::default(),
1559 external_streams,
1560 stream_signal_clone,
1561 )
1562 .await
1563 .unwrap();
1564 });
1565
1566 stream_signal.store(true, Ordering::Relaxed);
1567 let _ = rx.recv().await; rx.close();
1571 handle.await.unwrap();
1572 }
1573
1574 #[rstest]
1575 #[tokio::test(flavor = "multi_thread")]
1576 async fn test_stream_messages_when_receiver_closed(
1577 #[future] redis_connection: ConnectionManager,
1578 ) {
1579 let mut con = redis_connection.await;
1580 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1581
1582 let trader_id = TraderId::from("tester-001");
1583 let instance_id = UUID4::new();
1584 let config = MessageBusConfig {
1585 use_instance_id: true,
1586 ..Default::default()
1587 };
1588
1589 let stream_key = get_stream_key(trader_id, instance_id, &config);
1590 let external_streams = vec![stream_key.clone()];
1591 let stream_signal = Arc::new(AtomicBool::new(false));
1592 let stream_signal_clone = stream_signal.clone();
1593
1594 let clock = get_atomic_clock_realtime();
1597 let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1598
1599 let _: () = con
1601 .xadd(
1602 stream_key,
1603 future_id,
1604 &[("topic", "topic1"), ("payload", "data1")],
1605 )
1606 .await
1607 .unwrap();
1608
1609 rx.close();
1611
1612 let handle = tokio::spawn(async move {
1614 stream_messages(
1615 tx,
1616 RedisMessageBusConfig::default(),
1617 external_streams,
1618 stream_signal_clone,
1619 )
1620 .await
1621 .unwrap();
1622 });
1623
1624 handle.await.unwrap();
1626 }
1627
1628 #[rstest]
1629 #[tokio::test(flavor = "multi_thread")]
1630 async fn test_stream_messages(#[future] redis_connection: ConnectionManager) {
1631 let mut con = redis_connection.await;
1632 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1633
1634 let trader_id = TraderId::from("tester-001");
1635 let instance_id = UUID4::new();
1636 let config = MessageBusConfig {
1637 use_instance_id: true,
1638 ..Default::default()
1639 };
1640
1641 let stream_key = get_stream_key(trader_id, instance_id, &config);
1642 let external_streams = vec![stream_key.clone()];
1643 let stream_signal = Arc::new(AtomicBool::new(false));
1644 let stream_signal_clone = stream_signal.clone();
1645
1646 let clock = get_atomic_clock_realtime();
1649 let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1650
1651 let _: () = con
1653 .xadd(
1654 stream_key,
1655 future_id,
1656 &[("topic", "topic1"), ("payload", "data1")],
1657 )
1658 .await
1659 .unwrap();
1660
1661 let handle = tokio::spawn(async move {
1663 stream_messages(
1664 tx,
1665 RedisMessageBusConfig::default(),
1666 external_streams,
1667 stream_signal_clone,
1668 )
1669 .await
1670 .unwrap();
1671 });
1672
1673 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1675 assert_eq!(msg.topic, "topic1");
1676 assert_eq!(msg.payload, Bytes::from("data1"));
1677
1678 rx.close();
1680 stream_signal.store(true, Ordering::Relaxed);
1681 handle.await.unwrap();
1682 }
1683
1684 #[rstest]
1685 #[tokio::test(flavor = "multi_thread")]
1686 async fn test_stream_messages_skips_preexisting_entries(
1687 #[future] redis_connection: ConnectionManager,
1688 ) {
1689 let mut con = redis_connection.await;
1690 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1691
1692 let suffix = UUID4::new();
1693 let stream_key = format!("test:stream:no-backlog:{suffix}");
1694 let external_streams = vec![stream_key.clone()];
1695 let stream_signal = Arc::new(AtomicBool::new(false));
1696 let stream_signal_clone = stream_signal.clone();
1697
1698 let _: () = con
1699 .xadd(
1700 &stream_key,
1701 "1-0",
1702 &[("topic", "preexisting"), ("payload", "old")],
1703 )
1704 .await
1705 .unwrap();
1706
1707 let handle = tokio::spawn(async move {
1708 stream_messages(
1709 tx,
1710 RedisMessageBusConfig::default(),
1711 external_streams,
1712 stream_signal_clone,
1713 )
1714 .await
1715 .unwrap();
1716 });
1717
1718 let future_id = (get_atomic_clock_realtime().get_time_ms() + 1_000_000).to_string();
1719 let _: () = con
1720 .xadd(
1721 &stream_key,
1722 future_id,
1723 &[("topic", "live"), ("payload", "new")],
1724 )
1725 .await
1726 .unwrap();
1727
1728 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1729
1730 rx.close();
1731 stream_signal.store(true, Ordering::Relaxed);
1732 handle.await.unwrap();
1733 let _: usize = con.del(stream_key).await.unwrap();
1734
1735 assert_eq!(msg.topic, "live");
1736 assert_eq!(msg.payload, Bytes::from("new"));
1737 }
1738
1739 #[rstest]
1740 #[tokio::test(flavor = "multi_thread")]
1741 async fn test_stream_messages_skips_malformed_entry(
1742 #[future] redis_connection: ConnectionManager,
1743 ) {
1744 let mut con = redis_connection.await;
1745 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1746
1747 let suffix = UUID4::new();
1748 let stream_key = format!("test:stream:malformed:{suffix}");
1749 let external_streams = vec![stream_key.clone()];
1750 let stream_signal = Arc::new(AtomicBool::new(false));
1751 let stream_signal_clone = stream_signal.clone();
1752
1753 let clock = get_atomic_clock_realtime();
1754 let base_id = clock.get_time_ms() + 1_000_000;
1755
1756 let _: () = con
1757 .xadd(
1758 &stream_key,
1759 format!("{}", base_id + 1),
1760 &[("topic", "missing-payload")],
1761 )
1762 .await
1763 .unwrap();
1764 let _: () = con
1765 .xadd(
1766 &stream_key,
1767 format!("{}", base_id + 2),
1768 &[("topic", "valid"), ("payload", "data")],
1769 )
1770 .await
1771 .unwrap();
1772
1773 let handle = tokio::spawn(async move {
1774 stream_messages(
1775 tx,
1776 RedisMessageBusConfig::default(),
1777 external_streams,
1778 stream_signal_clone,
1779 )
1780 .await
1781 .unwrap();
1782 });
1783
1784 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
1785
1786 rx.close();
1787 stream_signal.store(true, Ordering::Relaxed);
1788 handle.await.unwrap();
1789
1790 assert_eq!(msg.topic, "valid");
1791 assert_eq!(msg.payload, Bytes::from("data"));
1792 }
1793
1794 #[rstest]
1795 #[tokio::test(flavor = "multi_thread")]
1796 async fn test_stream_messages_returns_unrecoverable_read_error(
1797 #[future] redis_connection: ConnectionManager,
1798 ) {
1799 let mut con = redis_connection.await;
1800 let (tx, _rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
1801
1802 let suffix = UUID4::new();
1803 let stream_key = format!("test:stream:wrong-type:{suffix}");
1804 let external_streams = vec![stream_key.clone()];
1805 let stream_signal = Arc::new(AtomicBool::new(false));
1806
1807 let _: () = con.set(&stream_key, "not-a-stream").await.unwrap();
1808
1809 let result = stream_messages(
1810 tx,
1811 RedisMessageBusConfig::default(),
1812 external_streams,
1813 stream_signal,
1814 )
1815 .await;
1816
1817 let _: () = con.del(&stream_key).await.unwrap();
1818
1819 assert!(result.is_err());
1820 assert!(
1821 result
1822 .unwrap_err()
1823 .to_string()
1824 .contains("Error reading from stream")
1825 );
1826 }
1827
1828 #[rstest]
1829 #[tokio::test(flavor = "multi_thread")]
1830 async fn test_stream_connection_returns_none_when_signaled() {
1831 let config = RedisMessageBusConfig {
1832 port: Some(1),
1833 connection_timeout: 20,
1834 ..Default::default()
1835 };
1836 let stream_signal = Arc::new(AtomicBool::new(true));
1837 let signal = stream_signal.clone();
1838 let handle = tokio::spawn(async move { connect_stream_connection(&config, &signal).await });
1839
1840 wait_until_async(|| async { handle.is_finished() }, Duration::from_secs(1)).await;
1841
1842 assert!(handle.await.unwrap().unwrap().is_none());
1843 }
1844
1845 #[rstest]
1846 #[case::market_data(BusPayloadType::QuoteTick)]
1847 #[case::typed(BusPayloadType::TradingCommand)]
1848 #[tokio::test(flavor = "multi_thread")]
1849 async fn test_publish_messages(
1850 #[future] redis_connection: ConnectionManager,
1851 #[case] payload_type: BusPayloadType,
1852 ) {
1853 let mut con = redis_connection.await;
1854 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
1855
1856 let trader_id = TraderId::from("tester-001");
1857 let instance_id = UUID4::new();
1858 let config = MessageBusConfig {
1859 use_instance_id: true,
1860 stream_per_topic: false,
1861 ..Default::default()
1862 };
1863 let stream_key = get_stream_key(trader_id, instance_id, &config);
1864
1865 let handle = tokio::spawn(async move {
1867 publish_messages(
1868 rx,
1869 trader_id,
1870 instance_id,
1871 config,
1872 RedisMessageBusConfig::default(),
1873 )
1874 .await
1875 .unwrap();
1876 });
1877
1878 let msg = BusMessage::with_str_topic(
1880 "test_topic",
1881 payload_type,
1882 Bytes::from("test_payload"),
1883 SerializationEncoding::Json,
1884 );
1885 tx.send(msg).unwrap();
1886
1887 wait_until_async(
1889 || {
1890 let mut con = con.clone();
1891 let stream_key = stream_key.clone();
1892 async move {
1893 let messages: RedisStreamBulk =
1894 con.xread(&[&stream_key], &["0"]).await.unwrap();
1895 !messages.is_empty()
1896 }
1897 },
1898 Duration::from_secs(3),
1899 )
1900 .await;
1901
1902 let messages: RedisStreamBulk = con.xread(&[&stream_key], &["0"]).await.unwrap();
1904 assert_eq!(messages.len(), 1);
1905 let stream_msgs = messages[0].get(&stream_key).unwrap();
1906 let stream_msg_array = &stream_msgs[0].values().next().unwrap();
1907 let decoded_message = decode_bus_message(stream_msg_array).unwrap();
1908 assert_eq!(decoded_message.topic, "test_topic");
1909 assert_eq!(decoded_message.payload_type, payload_type);
1910 assert_eq!(decoded_message.encoding, SerializationEncoding::Json);
1911 assert_eq!(decoded_message.payload, Bytes::from("test_payload"));
1912
1913 let msg = BusMessage::new_close();
1915 tx.send(msg).unwrap();
1916
1917 handle.await.unwrap();
1919 }
1920
1921 #[rstest]
1922 #[tokio::test(flavor = "multi_thread")]
1923 async fn test_publish_messages_applies_autotrim_maxlen(
1924 #[future] redis_connection: ConnectionManager,
1925 ) {
1926 let mut con = redis_connection.await;
1927 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
1928 let trader_id = TraderId::from("tester-001");
1929 let instance_id = UUID4::new();
1930 let config = MessageBusConfig {
1931 buffer_interval_ms: Some(10),
1932 autotrim_mins: Some(60),
1933 autotrim_maxlen: Some(10),
1934 use_instance_id: true,
1935 stream_per_topic: false,
1936 ..Default::default()
1937 };
1938 let stream_key = get_stream_key(trader_id, instance_id, &config);
1939
1940 let handle = tokio::spawn(async move {
1941 publish_messages(
1942 rx,
1943 trader_id,
1944 instance_id,
1945 config,
1946 RedisMessageBusConfig::default(),
1947 )
1948 .await
1949 .unwrap();
1950 });
1951 let msg = BusMessage::with_str_topic(
1952 "test_topic",
1953 BusPayloadType::QuoteTick,
1954 Bytes::from_static(b"test_payload"),
1955 SerializationEncoding::Json,
1956 );
1957
1958 let messages_sent = 250;
1961
1962 for _ in 0..messages_sent {
1963 tx.send(msg.clone()).unwrap();
1964 }
1965 tx.send(BusMessage::new_close()).unwrap();
1966 handle.await.unwrap();
1967
1968 let stream_len: usize = con.xlen(&stream_key).await.unwrap();
1969 let _: usize = con.del(stream_key).await.unwrap();
1970
1971 assert!(stream_len >= 10);
1972 assert!(stream_len < messages_sent);
1973 }
1974
1975 #[rstest]
1976 #[tokio::test(flavor = "multi_thread")]
1977 async fn test_two_live_nodes_publish_and_ingest_external_redis_stream(
1978 #[future] redis_connection: ConnectionManager,
1979 ) {
1980 let _con = redis_connection.await;
1981 let redis_config = RedisMessageBusConfig::default();
1982 let trader_a = TraderId::from("NODEA-001");
1983 let instance_a = UUID4::new();
1984 let node_a_msgbus = MessageBusConfig {
1985 use_instance_id: true,
1986 stream_per_topic: false,
1987 ..Default::default()
1988 };
1989 let stream_key = get_stream_key(trader_a, instance_a, &node_a_msgbus);
1990 let node_b_msgbus = MessageBusConfig {
1991 external_streams: Some(vec![stream_key]),
1992 stream_per_topic: false,
1993 ..Default::default()
1994 };
1995 let quote = QuoteTick::default();
1996 let trade = TradeTick::default();
1997 let (ready_tx, ready_rx) = mpsc::channel::<()>();
1998 let (quote_tx, quote_rx) = mpsc::channel::<QuoteTick>();
1999 let (trade_tx, trade_rx) = mpsc::channel::<TradeTick>();
2000
2001 let node_b = thread::spawn({
2002 let redis_config = redis_config.clone();
2003 move || -> anyhow::Result<()> {
2004 let runtime = tokio::runtime::Builder::new_multi_thread()
2005 .worker_threads(2)
2006 .enable_all()
2007 .build()?;
2008
2009 runtime.block_on(async move {
2010 let config = LiveNodeConfig {
2011 environment: Environment::Sandbox,
2012 trader_id: TraderId::from("NODEB-001"),
2013 msgbus: Some(node_b_msgbus),
2014 exec_engine: LiveExecutionEngineConfig {
2015 reconciliation: false,
2016 ..Default::default()
2017 },
2018 delay_post_stop: Duration::ZERO,
2019 timeout_connection: Duration::from_millis(500),
2020 timeout_disconnection: Duration::from_millis(500),
2021 ..Default::default()
2022 };
2023 let mut node = LiveNodeBuilder::from_config(config)?
2024 .with_external_msgbus_factory(redis_msgbus_factory(redis_config))
2025 .build()?;
2026 let handle = node.handle();
2027 let quote_handler = TypedHandler::from({
2028 let quote_tx = quote_tx.clone();
2029 let handle = handle.clone();
2030 move |quote: &QuoteTick| {
2031 let _ = quote_tx.send(*quote);
2032 handle.stop();
2033 }
2034 });
2035 let trade_handler = TypedHandler::from(move |trade: &TradeTick| {
2036 let _ = trade_tx.send(*trade);
2037 });
2038
2039 msgbus::subscribe_quotes("data.quotes.*".into(), quote_handler, None);
2040 msgbus::subscribe_trades("data.trades.*".into(), trade_handler, None);
2041 msgbus::get_message_bus()
2042 .borrow_mut()
2043 .add_streaming_type(BusPayloadType::QuoteTick);
2044 let result = tokio::time::timeout(Duration::from_secs(10), async {
2045 let run = node.run();
2046 tokio::pin!(run);
2047
2048 let announce_ready = async {
2049 for _ in 0..100 {
2050 if handle.is_running() {
2051 ready_tx.send(())?;
2052 return Ok(());
2053 }
2054 tokio::time::sleep(Duration::from_millis(10)).await;
2055 }
2056
2057 anyhow::bail!("node B did not reach running state")
2058 };
2059
2060 tokio::select! {
2061 result = &mut run => result,
2062 ready = announce_ready => {
2063 ready?;
2064 run.await
2065 }
2066 }
2067 })
2068 .await;
2069 msgbus::get_message_bus().borrow_mut().dispose();
2070
2071 match result {
2072 Ok(Ok(())) => Ok(()),
2073 Ok(Err(e)) => Err(e),
2074 Err(e) => anyhow::bail!("node B timed out: {e}"),
2075 }
2076 })
2077 }
2078 });
2079
2080 ready_rx
2081 .recv_timeout(Duration::from_secs(5))
2082 .expect("node B should start Redis ingress");
2083
2084 let node_a = thread::spawn(move || -> anyhow::Result<()> {
2085 let runtime = tokio::runtime::Builder::new_multi_thread()
2086 .worker_threads(2)
2087 .enable_all()
2088 .build()?;
2089
2090 runtime.block_on(async move {
2091 let config = LiveNodeConfig {
2092 environment: Environment::Sandbox,
2093 trader_id: trader_a,
2094 instance_id: Some(instance_a),
2095 msgbus: Some(node_a_msgbus),
2096 exec_engine: LiveExecutionEngineConfig {
2097 reconciliation: false,
2098 ..Default::default()
2099 },
2100 delay_post_stop: Duration::ZERO,
2101 timeout_connection: Duration::from_millis(500),
2102 timeout_disconnection: Duration::from_millis(500),
2103 ..Default::default()
2104 };
2105 let _node = LiveNodeBuilder::from_config(config)?
2106 .with_external_msgbus_factory(Box::new(redis_config))
2107 .build()?;
2108
2109 msgbus::publish_trade("data.trades.TEST".into(), &trade);
2110 msgbus::publish_quote("data.quotes.TEST".into(), "e);
2111 msgbus::get_message_bus().borrow_mut().dispose();
2112
2113 Ok(())
2114 })
2115 });
2116
2117 node_a
2118 .join()
2119 .expect("node A thread should not panic")
2120 .expect("node A should publish externally");
2121 let received_quote = quote_rx
2122 .recv_timeout(Duration::from_secs(10))
2123 .expect("node B should republish the registered quote type");
2124 node_b
2125 .join()
2126 .expect("node B thread should not panic")
2127 .expect("node B should ingest and stop cleanly");
2128
2129 assert_eq!(received_quote, quote);
2130 assert!(
2131 trade_rx.try_recv().is_err(),
2132 "unregistered trade type should not republish internally"
2133 );
2134 }
2135
2136 #[rstest]
2137 #[tokio::test(flavor = "multi_thread")]
2138 async fn test_stream_messages_multiple_streams(#[future] redis_connection: ConnectionManager) {
2139 let mut con = redis_connection.await;
2140 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
2141
2142 let suffix = UUID4::new();
2144 let stream_key1 = format!("test:stream:{suffix}:1");
2145 let stream_key2 = format!("test:stream:{suffix}:2");
2146 let external_streams = vec![stream_key1.clone(), stream_key2.clone()];
2147 let stream_signal = Arc::new(AtomicBool::new(false));
2148 let stream_signal_clone = stream_signal.clone();
2149
2150 let clock = get_atomic_clock_realtime();
2151 let base_id = clock.get_time_ms() + 1_000_000;
2152
2153 let handle = tokio::spawn(async move {
2155 stream_messages(
2156 tx,
2157 RedisMessageBusConfig::default(),
2158 external_streams,
2159 stream_signal_clone,
2160 )
2161 .await
2162 .unwrap();
2163 });
2164
2165 let _: () = con
2167 .xadd(
2168 &stream_key1,
2169 format!("{}", base_id + 100),
2170 &[("topic", "stream1-first"), ("payload", "data")],
2171 )
2172 .await
2173 .unwrap();
2174
2175 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
2176 assert_eq!(msg.topic, "stream1-first");
2177
2178 let _: () = con
2180 .xadd(
2181 &stream_key2,
2182 format!("{}", base_id + 50),
2183 &[("topic", "stream2-second"), ("payload", "data")],
2184 )
2185 .await
2186 .unwrap();
2187
2188 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
2189 assert_eq!(msg.topic, "stream2-second");
2190
2191 rx.close();
2193 stream_signal.store(true, Ordering::Relaxed);
2194 handle.await.unwrap();
2195 }
2196
2197 #[rstest]
2198 #[tokio::test(flavor = "multi_thread")]
2199 async fn test_stream_messages_interleaved_at_different_rates(
2200 #[future] redis_connection: ConnectionManager,
2201 ) {
2202 let mut con = redis_connection.await;
2203 let (tx, mut rx) = tokio::sync::mpsc::channel::<BusMessage>(100);
2204
2205 let suffix = UUID4::new();
2207 let stream_key1 = format!("test:stream:interleaved:{suffix}:1");
2208 let stream_key2 = format!("test:stream:interleaved:{suffix}:2");
2209 let stream_key3 = format!("test:stream:interleaved:{suffix}:3");
2210 let external_streams = vec![
2211 stream_key1.clone(),
2212 stream_key2.clone(),
2213 stream_key3.clone(),
2214 ];
2215 let stream_signal = Arc::new(AtomicBool::new(false));
2216 let stream_signal_clone = stream_signal.clone();
2217
2218 let clock = get_atomic_clock_realtime();
2219 let base_id = clock.get_time_ms() + 1_000_000;
2220
2221 let handle = tokio::spawn(async move {
2222 stream_messages(
2223 tx,
2224 RedisMessageBusConfig::default(),
2225 external_streams,
2226 stream_signal_clone,
2227 )
2228 .await
2229 .unwrap();
2230 });
2231
2232 let _: () = con
2234 .xadd(
2235 &stream_key1,
2236 format!("{}", base_id + 100),
2237 &[("topic", "s1m1"), ("payload", "data")],
2238 )
2239 .await
2240 .unwrap();
2241 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
2242 assert_eq!(msg.topic, "s1m1");
2243
2244 let _: () = con
2246 .xadd(
2247 &stream_key2,
2248 format!("{}", base_id + 50),
2249 &[("topic", "s2m1"), ("payload", "data")],
2250 )
2251 .await
2252 .unwrap();
2253 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
2254 assert_eq!(msg.topic, "s2m1");
2255
2256 let _: () = con
2258 .xadd(
2259 &stream_key3,
2260 format!("{}", base_id + 25),
2261 &[("topic", "s3m1"), ("payload", "data")],
2262 )
2263 .await
2264 .unwrap();
2265 let msg = receive_bus_message(&mut rx, Duration::from_secs(2)).await;
2266 assert_eq!(msg.topic, "s3m1");
2267
2268 rx.close();
2270 stream_signal.store(true, Ordering::Relaxed);
2271 handle.await.unwrap();
2272 }
2273
2274 #[rstest]
2275 #[tokio::test(flavor = "multi_thread")]
2276 async fn test_close() {
2277 let trader_id = TraderId::from("tester-001");
2278 let instance_id = UUID4::new();
2279 let config = MessageBusConfig {
2280 use_instance_id: true,
2281 ..Default::default()
2282 };
2283
2284 let mut db = RedisMessageBusBacking::new(
2285 trader_id,
2286 instance_id,
2287 config,
2288 RedisMessageBusConfig::default(),
2289 )
2290 .unwrap();
2291
2292 MessageBusBacking::close(&mut db);
2294 }
2295
2296 #[rstest]
2297 #[tokio::test(flavor = "multi_thread")]
2298 async fn test_heartbeat_task() {
2299 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<BusMessage>();
2300 let signal = Arc::new(AtomicBool::new(false));
2301
2302 let handle = tokio::spawn(run_heartbeat(1, signal.clone(), tx));
2304
2305 let heartbeat = receive_unbounded_bus_message(&mut rx, Duration::from_secs(2)).await;
2306
2307 signal.store(true, Ordering::Relaxed);
2309 handle.await.unwrap();
2310
2311 assert_eq!(heartbeat.topic, HEARTBEAT_TOPIC);
2313 }
2314
2315 async fn receive_bus_message(
2316 rx: &mut tokio::sync::mpsc::Receiver<BusMessage>,
2317 timeout: Duration,
2318 ) -> BusMessage {
2319 let mut received = None;
2320
2321 wait_until_async(
2322 || {
2323 if received.is_none() {
2324 received = rx.try_recv().ok();
2325 }
2326
2327 let has_received = received.is_some();
2328 async move { has_received }
2329 },
2330 timeout,
2331 )
2332 .await;
2333
2334 received.unwrap()
2335 }
2336
2337 async fn receive_unbounded_bus_message(
2338 rx: &mut tokio::sync::mpsc::UnboundedReceiver<BusMessage>,
2339 timeout: Duration,
2340 ) -> BusMessage {
2341 let mut received = None;
2342
2343 wait_until_async(
2344 || {
2345 if received.is_none() {
2346 received = rx.try_recv().ok();
2347 }
2348
2349 let has_received = received.is_some();
2350 async move { has_received }
2351 },
2352 timeout,
2353 )
2354 .await;
2355
2356 received.unwrap()
2357 }
2358}