Skip to main content

nautilus_infrastructure/redis/
msgbus.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Redis-backed message bus backing for the system.
17//!
18//! # Architecture
19//!
20//! Runs background tasks on `get_runtime()` for publishing, stream reading,
21//! and heartbeats. Messages are sent via an unbounded `tokio::sync::mpsc`
22//! channel to the publish task, which buffers and writes them to Redis
23//! streams. Each background task owns its own Redis connection created on
24//! the Nautilus runtime.
25//!
26//! Handles are stored as `Option<JoinHandle>` for idempotent shutdown via
27//! `close_async()`. The synchronous `close()` uses `block_in_place` to
28//! bridge into the async shutdown path and must be called from outside any
29//! `current_thread` Tokio runtime.
30
31use 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/// Configuration for a Redis-backed message bus backing.
79///
80/// Redis 6.2 or higher is required for correct operation.
81#[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    /// The Redis host address. If `None`, `127.0.0.1` is used.
100    pub host: Option<String>,
101    /// The Redis port. If `None`, `6379` is used.
102    pub port: Option<u16>,
103    /// The Redis account username.
104    pub username: Option<String>,
105    /// The Redis account password.
106    pub password: Option<String>,
107    /// If Redis should use an SSL-enabled connection.
108    pub ssl: bool,
109    /// The timeout (in seconds) to wait for a new connection.
110    pub connection_timeout: u16,
111    /// The timeout (in seconds) to wait for a response.
112    pub response_timeout: u16,
113    /// The number of retry attempts with exponential backoff for connection attempts.
114    pub number_of_retries: usize,
115    /// The base value for exponential backoff calculation.
116    pub exponent_base: u64,
117    /// The maximum delay between retry attempts (in seconds).
118    pub max_delay: u64,
119    /// The multiplication factor for retry delay calculation.
120    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/// Factory for constructing Redis message bus backings.
223#[derive(Debug, Clone)]
224pub struct RedisMessageBusFactory {
225    config: RedisMessageBusConfig,
226}
227
228impl RedisMessageBusFactory {
229    /// Creates a new [`RedisMessageBusFactory`] from the given Redis configuration.
230    #[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    /// The trader ID for this message bus backing.
249    pub trader_id: TraderId,
250    /// The instance ID for this message bus backing.
251    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    /// Creates a new [`RedisMessageBusBacking`] instance for the given `trader_id`, `instance_id`, and `config`.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if the heartbeat interval is configured as zero seconds.
276    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        // Create publish task (start the runtime here for now)
295        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        // Conditionally create stream task and channel if external streams configured
303        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        // Create heartbeat task
327        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    /// Returns whether the message bus backing publishing channel is closed.
355    fn is_closed(&self) -> bool {
356        self.pub_tx.is_closed()
357    }
358
359    /// Queues a serialized bus message for external publication.
360    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    /// Closes the message bus backing.
371    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        // Keep close sync for now to avoid async trait method
386        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    /// Retrieves the Redis stream receiver for this message bus instance.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error if the stream receiver has already been taken.
402    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    /// Streams messages arriving on the stream receiver channel.
411    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
428/// Publishes messages received on `rx` to Redis streams for the given `trader_id` and `instance_id`, using `config`.
429///
430/// # Errors
431///
432/// Returns an error if:
433/// - The backing configuration is missing in `config`.
434/// - Establishing the Redis connection fails.
435/// - Any Redis command fails during publishing.
436pub 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    // Auto-trimming
449    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    // Buffering
461    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    // A sleep used to trigger periodic flushing of the buffer.
465    // When `buffer_interval` is zero we skip using the timer and flush immediately
466    // after every message.
467    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                        // Ensure we exit the loop after flushing any remaining messages.
477                        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                        // Immediate flush mode
495                        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            // Only poll the timer when the interval is non-zero. This avoids
511            // unnecessarily waking the task when immediate flushing is enabled.
512            () = &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                // Schedule the next tick
526                flush_timer.as_mut().reset(tokio::time::Instant::now() + buffer_interval);
527            }
528        }
529    }
530
531    // Drain any remaining messages
532    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; // Nothing else to do
583        }
584
585        // Autotrim stream
586        let last_trim_ms = last_trim_index.entry(stream_key.clone()).or_insert(0); // Remove clone
587        let unix_duration_now = duration_since_unix_epoch();
588        let trim_buffer = Duration::from_secs(TRIM_BUFFER_SECS);
589
590        // Improve efficiency of this by batching
591        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
613/// Streams messages from Redis streams and sends them over the provided `tx` channel.
614///
615/// # Errors
616///
617/// Returns an error if:
618/// - Establishing the Redis connection fails before the terminate signal is received.
619/// - A Redis read operation returns a non-retryable error.
620pub 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    // Start streaming from current timestamp
643    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                    // Timeout occurred: no messages received
675                    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; // End streaming
689                                        }
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
806// Redis fields are unordered, and older streams may omit type or encoding headers
807fn 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                    // We expect an error if the channel is closed during shutdown
930                    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")] // Run Redis tests on Linux platforms only
1482#[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        // Start the message streaming task
1555        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; // Wait for the tx to close
1568
1569        // Shutdown and cleanup
1570        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        // Use a message ID in the future, as streaming begins
1595        // around the timestamp the task is spawned.
1596        let clock = get_atomic_clock_realtime();
1597        let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1598
1599        // Publish test message
1600        let _: () = con
1601            .xadd(
1602                stream_key,
1603                future_id,
1604                &[("topic", "topic1"), ("payload", "data1")],
1605            )
1606            .await
1607            .unwrap();
1608
1609        // Immediately close channel
1610        rx.close();
1611
1612        // Start the message streaming task
1613        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        // Shutdown and cleanup
1625        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        // Use a message ID in the future, as streaming begins
1647        // around the timestamp the task is spawned.
1648        let clock = get_atomic_clock_realtime();
1649        let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1650
1651        // Publish test message
1652        let _: () = con
1653            .xadd(
1654                stream_key,
1655                future_id,
1656                &[("topic", "topic1"), ("payload", "data1")],
1657            )
1658            .await
1659            .unwrap();
1660
1661        // Start the message streaming task
1662        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        // Receive and verify the message
1674        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        // Shutdown and cleanup
1679        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        // Start the publish_messages task
1866        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        // Send a test message
1879        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 the message is published to Redis
1888        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        // Verify the message was published to Redis
1903        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        // Stop publishing task
1914        let msg = BusMessage::new_close();
1915        tx.send(msg).unwrap();
1916
1917        // Shutdown and cleanup
1918        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        // The integration service uses Redis's default stream node limits,
1959        // so these writes span multiple macro nodes.
1960        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(), &quote);
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        // Setup multiple stream keys
2143        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        // Start streaming task
2154        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        // Publish to stream 1 at higher ID
2166        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        // Publish to stream 2 at lower ID (tests independent cursor tracking)
2179        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        // Shutdown and cleanup
2192        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        // Setup multiple stream keys
2206        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        // Stream 1 advances with high ID
2233        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        // Stream 2 gets message at lower ID - would be skipped with global cursor
2245        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        // Stream 3 gets message at even lower ID
2257        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        // Shutdown and cleanup
2269        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        // Close the message bus backing (test should not hang)
2293        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        // Start the heartbeat task with a short interval
2303        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        // Stop the heartbeat task
2308        signal.store(true, Ordering::Relaxed);
2309        handle.await.unwrap();
2310
2311        // Ensure heartbeats were sent
2312        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}