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::{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/// Configuration for a Redis-backed message bus backing.
76///
77/// Redis 6.2 or higher is required for correct operation.
78#[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    /// The Redis host address. If `None`, `127.0.0.1` is used.
97    pub host: Option<String>,
98    /// The Redis port. If `None`, `6379` is used.
99    pub port: Option<u16>,
100    /// The Redis account username.
101    pub username: Option<String>,
102    /// The Redis account password.
103    pub password: Option<String>,
104    /// If Redis should use an SSL-enabled connection.
105    pub ssl: bool,
106    /// The timeout (in seconds) to wait for a new connection.
107    pub connection_timeout: u16,
108    /// The timeout (in seconds) to wait for a response.
109    pub response_timeout: u16,
110    /// The number of retry attempts with exponential backoff for connection attempts.
111    pub number_of_retries: usize,
112    /// The base value for exponential backoff calculation.
113    pub exponent_base: u64,
114    /// The maximum delay between retry attempts (in seconds).
115    pub max_delay: u64,
116    /// The multiplication factor for retry delay calculation.
117    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/// Factory for constructing Redis message bus backings.
220#[derive(Debug, Clone)]
221pub struct RedisMessageBusFactory {
222    config: RedisMessageBusConfig,
223}
224
225impl RedisMessageBusFactory {
226    /// Creates a new [`RedisMessageBusFactory`] from the given Redis configuration.
227    #[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    /// The trader ID for this message bus backing.
246    pub trader_id: TraderId,
247    /// The instance ID for this message bus backing.
248    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    /// Creates a new [`RedisMessageBusBacking`] instance for the given `trader_id`, `instance_id`, and `config`.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if the heartbeat interval is configured as zero seconds.
273    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        // Create publish task (start the runtime here for now)
292        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        // Conditionally create stream task and channel if external streams configured
300        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        // Create heartbeat task
324        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    /// Returns whether the message bus backing publishing channel is closed.
352    fn is_closed(&self) -> bool {
353        self.pub_tx.is_closed()
354    }
355
356    /// Queues a serialized bus message for external publication.
357    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    /// Closes the message bus backing.
368    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        // Keep close sync for now to avoid async trait method
383        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    /// Retrieves the Redis stream receiver for this message bus instance.
395    ///
396    /// # Errors
397    ///
398    /// Returns an error if the stream receiver has already been taken.
399    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    /// Streams messages arriving on the stream receiver channel.
408    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
425/// Publishes messages received on `rx` to Redis streams for the given `trader_id` and `instance_id`, using `config`.
426///
427/// # Errors
428///
429/// Returns an error if:
430/// - The backing configuration is missing in `config`.
431/// - Establishing the Redis connection fails.
432/// - Any Redis command fails during publishing.
433pub 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    // Auto-trimming
446    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    // Buffering
458    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    // A sleep used to trigger periodic flushing of the buffer.
462    // When `buffer_interval` is zero we skip using the timer and flush immediately
463    // after every message.
464    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                        // Ensure we exit the loop after flushing any remaining messages.
474                        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                        // Immediate flush mode
492                        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            // Only poll the timer when the interval is non-zero. This avoids
508            // unnecessarily waking the task when immediate flushing is enabled.
509            () = &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                // Schedule the next tick
523                flush_timer.as_mut().reset(tokio::time::Instant::now() + buffer_interval);
524            }
525        }
526    }
527
528    // Drain any remaining messages
529    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; // Nothing else to do
585        }
586
587        // Autotrim stream
588        let last_trim_ms = last_trim_index.entry(stream_key.clone()).or_insert(0); // Remove clone
589        let unix_duration_now = duration_since_unix_epoch();
590        let trim_buffer = Duration::from_secs(TRIM_BUFFER_SECS);
591
592        // Improve efficiency of this by batching
593        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
615/// Streams messages from Redis streams and sends them over the provided `tx` channel.
616///
617/// # Errors
618///
619/// Returns an error if:
620/// - Establishing the Redis connection fails before the terminate signal is received.
621/// - A Redis read operation returns a non-retryable error.
622pub 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    // Start streaming from current timestamp
645    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                    // Timeout occurred: no messages received
677                    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; // End streaming
691                                        }
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
808// Redis fields are unordered, and older streams may omit type or encoding headers
809fn 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                    // We expect an error if the channel is closed during shutdown
909                    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")] // Run Redis tests on Linux platforms only
1326#[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        // Start the message streaming task
1399        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; // Wait for the tx to close
1412
1413        // Shutdown and cleanup
1414        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        // Use a message ID in the future, as streaming begins
1439        // around the timestamp the task is spawned.
1440        let clock = get_atomic_clock_realtime();
1441        let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1442
1443        // Publish test message
1444        let _: () = con
1445            .xadd(
1446                stream_key,
1447                future_id,
1448                &[("topic", "topic1"), ("payload", "data1")],
1449            )
1450            .await
1451            .unwrap();
1452
1453        // Immediately close channel
1454        rx.close();
1455
1456        // Start the message streaming task
1457        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        // Shutdown and cleanup
1469        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        // Use a message ID in the future, as streaming begins
1491        // around the timestamp the task is spawned.
1492        let clock = get_atomic_clock_realtime();
1493        let future_id = (clock.get_time_ms() + 1_000_000).to_string();
1494
1495        // Publish test message
1496        let _: () = con
1497            .xadd(
1498                stream_key,
1499                future_id,
1500                &[("topic", "topic1"), ("payload", "data1")],
1501            )
1502            .await
1503            .unwrap();
1504
1505        // Start the message streaming task
1506        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        // Receive and verify the message
1518        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        // Shutdown and cleanup
1523        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        // Start the publish_messages task
1705        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        // Send a test message
1718        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 the message is published to Redis
1727        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        // Verify the message was published to Redis
1742        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        // Stop publishing task
1753        let msg = BusMessage::new_close();
1754        tx.send(msg).unwrap();
1755
1756        // Shutdown and cleanup
1757        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        // The integration service uses Redis's default stream node limits,
1798        // so these writes span multiple macro nodes.
1799        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(), &quote);
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        // Setup multiple stream keys
1982        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        // Start streaming task
1993        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        // Publish to stream 1 at higher ID
2005        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        // Publish to stream 2 at lower ID (tests independent cursor tracking)
2018        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        // Shutdown and cleanup
2031        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        // Setup multiple stream keys
2045        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        // Stream 1 advances with high ID
2072        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        // Stream 2 gets message at lower ID - would be skipped with global cursor
2084        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        // Stream 3 gets message at even lower ID
2096        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        // Shutdown and cleanup
2108        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        // Close the message bus backing (test should not hang)
2132        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        // Start the heartbeat task with a short interval
2142        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        // Stop the heartbeat task
2147        signal.store(true, Ordering::Relaxed);
2148        handle.await.unwrap();
2149
2150        // Ensure heartbeats were sent
2151        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}