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