Skip to main content

nautilus_common/
runner.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//! Global runtime machinery and thread-local storage.
17//!
18//! This module provides global access to shared runtime resources including clocks,
19//! message queues, and time event channels. It manages thread-local storage for
20//! system-wide components that need to be accessible across threads.
21
22use std::{
23    cell::RefCell,
24    fmt::Debug,
25    num::NonZeroU64,
26    sync::{
27        Arc, Weak,
28        atomic::{AtomicU64, AtomicUsize, Ordering},
29    },
30    thread::{self, ThreadId},
31};
32
33use ahash::AHashMap;
34
35use crate::{
36    messages::{data::DataCommand, execution::TradingCommand},
37    msgbus::{self, Endpoint, MStr, MessagingSwitchboard},
38    timer::{TimeEvent, TimeEventCallback, TimeEventHandler},
39};
40
41const CALLBACK_CLOSED: usize = 1 << (usize::BITS - 1);
42const CALLBACK_LEASES: usize = CALLBACK_CLOSED - 1;
43static NEXT_TIME_EVENT_CALLBACK_ID: AtomicU64 = AtomicU64::new(1);
44
45/// A monitored message channel feeding the runner event loop.
46///
47/// Each variant identifies an engine-facing channel tracked by the queue monitor.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[cfg_attr(
50    feature = "python",
51    pyo3::pyclass(
52        frozen,
53        eq,
54        eq_int,
55        module = "nautilus_trader.common",
56        from_py_object,
57        rename_all = "SCREAMING_SNAKE_CASE",
58    )
59)]
60#[cfg_attr(
61    feature = "python",
62    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.common")
63)]
64pub enum SystemChannel {
65    TimeEvents,
66    ExecEvents,
67    ExecCommands,
68    DataEvents,
69    DataCommands,
70}
71
72#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
73struct TimeEventCallbackId(NonZeroU64);
74
75#[derive(Debug)]
76struct TimeEventCallbackTokenInner {
77    id: TimeEventCallbackId,
78    owner: ThreadId,
79    state: AtomicUsize,
80}
81
82struct TimeEventCallbackEntry {
83    callback: TimeEventCallback,
84    token: Weak<TimeEventCallbackTokenInner>,
85}
86
87/// A send-safe handle to a thread-local time event callback.
88#[derive(Clone, Debug)]
89pub(crate) struct TimeEventCallbackToken(Arc<TimeEventCallbackTokenInner>);
90
91impl TimeEventCallbackToken {
92    fn register(callback: TimeEventCallback) -> Self {
93        debug_assert!(callback.is_local());
94        purge_closed_time_event_callbacks();
95
96        let raw_id = NEXT_TIME_EVENT_CALLBACK_ID.fetch_add(1, Ordering::Relaxed);
97        let id = TimeEventCallbackId(
98            NonZeroU64::new(raw_id).expect("time event callback IDs exhausted"),
99        );
100        let token = Self(Arc::new(TimeEventCallbackTokenInner {
101            id,
102            owner: thread::current().id(),
103            state: AtomicUsize::new(0),
104        }));
105        TIME_EVENT_CALLBACKS.with(|callbacks| {
106            let previous = callbacks.borrow_mut().insert(
107                id,
108                TimeEventCallbackEntry {
109                    callback,
110                    token: Arc::downgrade(&token.0),
111                },
112            );
113            debug_assert!(previous.is_none());
114        });
115        token
116    }
117
118    pub(crate) fn acquire(&self) -> Option<TimeEventCallbackLease> {
119        let mut state = self.0.state.load(Ordering::Acquire);
120        loop {
121            if state & CALLBACK_CLOSED != 0 {
122                return None;
123            }
124            let leases = state & CALLBACK_LEASES;
125            assert!(
126                leases < CALLBACK_LEASES,
127                "time event callback lease count overflow"
128            );
129
130            match self.0.state.compare_exchange_weak(
131                state,
132                state + 1,
133                Ordering::AcqRel,
134                Ordering::Acquire,
135            ) {
136                Ok(_) => return Some(TimeEventCallbackLease(self.0.clone())),
137                Err(actual) => state = actual,
138            }
139        }
140    }
141
142    #[cfg(any(feature = "live", test))]
143    pub(crate) fn is_closed(&self) -> bool {
144        self.0.state.load(Ordering::Acquire) & CALLBACK_CLOSED != 0
145    }
146
147    pub(crate) fn close(&self) {
148        let previous = self.0.state.fetch_or(CALLBACK_CLOSED, Ordering::AcqRel);
149        if previous & CALLBACK_LEASES == 0 {
150            self.remove_on_owner_thread();
151        }
152    }
153
154    fn remove_on_owner_thread(&self) {
155        if self.0.owner == thread::current().id() {
156            // Reachable from `Drop` (`LiveTimer::drop` -> `close`), which can
157            // run during thread-local teardown when `TIME_EVENT_CALLBACKS` is
158            // already destroyed. `try_with` returns `AccessError` rather than
159            // panicking (a panic in a TLS destructor aborts the process); the
160            // map is being torn down, so the removal is moot. The removed
161            // entry is returned out of the closure and dropped after the
162            // `RefMut` is released. Never log here: the logging TLS may also
163            // be in teardown.
164            let _ = TIME_EVENT_CALLBACKS
165                .try_with(|callbacks| callbacks.borrow_mut().remove(&self.0.id));
166        }
167    }
168
169    #[cfg(test)]
170    fn is_registered(&self) -> bool {
171        TIME_EVENT_CALLBACKS.with(|callbacks| callbacks.borrow().contains_key(&self.0.id))
172    }
173}
174
175/// A per-message hold on a registered callback entry.
176///
177/// The final lease of a closed token removes the TLS entry when it drops on
178/// the owner thread. A final lease dropped on another thread (failed send,
179/// receiver shutdown on a foreign thread) cannot touch the owner's TLS map;
180/// the closed entry is then reclaimed lazily by the next owner-thread
181/// registration or [`purge_closed_time_event_callbacks`] call (`LiveClock`
182/// invokes the latter from `clear_expired_timers`). That is bounded
183/// retention of the callback, never a leak across registrations and never
184/// a cross-thread `Rc` access.
185#[derive(Debug)]
186pub(crate) struct TimeEventCallbackLease(Arc<TimeEventCallbackTokenInner>);
187
188impl Drop for TimeEventCallbackLease {
189    fn drop(&mut self) {
190        let previous = self.0.state.fetch_sub(1, Ordering::AcqRel);
191        debug_assert!(previous & CALLBACK_LEASES > 0);
192        if previous == CALLBACK_CLOSED | 1 && self.0.owner == thread::current().id() {
193            // As in `remove_on_owner_thread`, this final lease can drop during
194            // thread-local teardown with `TIME_EVENT_CALLBACKS` already gone;
195            // `try_with` keeps the destructor from aborting the process.
196            let _ = TIME_EVENT_CALLBACKS
197                .try_with(|callbacks| callbacks.borrow_mut().remove(&self.0.id));
198        }
199    }
200}
201
202#[derive(Clone)]
203enum SendTimeEventCallback {
204    #[cfg(feature = "python")]
205    Python(Arc<crate::timer::PythonTimeEventCallback>),
206    Rust(Arc<dyn Fn(TimeEvent) + Send + Sync>),
207}
208
209impl Debug for SendTimeEventCallback {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        match self {
212            #[cfg(feature = "python")]
213            Self::Python(_) => f.write_str("Python callback"),
214            Self::Rust(_) => f.write_str("Rust callback (thread-safe)"),
215        }
216    }
217}
218
219impl SendTimeEventCallback {
220    fn into_callback(self) -> TimeEventCallback {
221        match self {
222            #[cfg(feature = "python")]
223            Self::Python(callback) => TimeEventCallback::Python(callback),
224            Self::Rust(callback) => TimeEventCallback::Rust(callback),
225        }
226    }
227}
228
229#[derive(Clone, Debug)]
230#[cfg(feature = "live")]
231pub(crate) struct TimeEventMessageFactory(SendTimeEventCallback);
232
233#[cfg(feature = "live")]
234impl TimeEventMessageFactory {
235    pub(crate) fn new(callback: &TimeEventCallback) -> Self {
236        match callback {
237            #[cfg(feature = "python")]
238            TimeEventCallback::Python(callback) => {
239                Self(SendTimeEventCallback::Python(callback.clone()))
240            }
241            TimeEventCallback::Rust(callback) => {
242                Self(SendTimeEventCallback::Rust(callback.clone()))
243            }
244            TimeEventCallback::RustLocal(_) => {
245                unreachable!("RustLocal callbacks require registered dispatch")
246            }
247        }
248    }
249
250    pub(crate) fn message(&self, event: TimeEvent) -> TimeEventMessage {
251        TimeEventMessage {
252            event,
253            dispatch: TimeEventDispatch::Direct(self.0.clone()),
254        }
255    }
256}
257
258#[derive(Debug)]
259enum TimeEventDispatch {
260    Direct(SendTimeEventCallback),
261    Registered(TimeEventCallbackLease),
262    #[cfg(any(feature = "live", test))]
263    Cleanup(TimeEventCallbackLease),
264}
265
266/// A send-safe live time event channel payload.
267///
268/// The dispatch representation is private so local callbacks can never be
269/// embedded in a cross-thread message.
270#[derive(Debug)]
271pub struct TimeEventMessage {
272    event: TimeEvent,
273    dispatch: TimeEventDispatch,
274}
275
276impl TimeEventMessage {
277    /// Creates a message from a time event and callback.
278    ///
279    /// # Panics
280    ///
281    /// Panics if the process-wide callback ID or lease count is exhausted.
282    #[must_use]
283    pub fn new(event: TimeEvent, callback: TimeEventCallback) -> Self {
284        match callback {
285            #[cfg(feature = "python")]
286            TimeEventCallback::Python(callback) => Self {
287                event,
288                dispatch: TimeEventDispatch::Direct(SendTimeEventCallback::Python(callback)),
289            },
290            TimeEventCallback::Rust(callback) => Self {
291                event,
292                dispatch: TimeEventDispatch::Direct(SendTimeEventCallback::Rust(callback)),
293            },
294            callback @ TimeEventCallback::RustLocal(_) => {
295                let token = TimeEventCallbackToken::register(callback);
296                let lease = token
297                    .acquire()
298                    .expect("new time event callback token should be open");
299                token.close();
300                Self::registered(event, lease)
301            }
302        }
303    }
304
305    /// Returns the time event carried by this message.
306    #[must_use]
307    pub const fn event(&self) -> &TimeEvent {
308        &self.event
309    }
310
311    pub(crate) const fn registered(event: TimeEvent, lease: TimeEventCallbackLease) -> Self {
312        Self {
313            event,
314            dispatch: TimeEventDispatch::Registered(lease),
315        }
316    }
317
318    #[cfg(any(feature = "live", test))]
319    pub(crate) const fn cleanup(event: TimeEvent, lease: TimeEventCallbackLease) -> Self {
320        Self {
321            event,
322            dispatch: TimeEventDispatch::Cleanup(lease),
323        }
324    }
325
326    /// Resolves and runs this message on the receiving thread.
327    ///
328    /// Messages for a `RustLocal` callback must be dispatched on the thread
329    /// where the callback was registered. Dispatching them elsewhere drops
330    /// the event and returns `false`.
331    ///
332    /// Returns `true` when a callback was dispatched. Cleanup messages and
333    /// wrong-thread registered messages return `false`.
334    pub fn dispatch(self) -> bool {
335        let Self { event, dispatch } = self;
336        match dispatch {
337            TimeEventDispatch::Direct(callback) => {
338                TimeEventHandler::new(event, callback.into_callback()).run();
339                true
340            }
341            TimeEventDispatch::Registered(lease) => {
342                if lease.0.owner != thread::current().id() {
343                    log::error!(
344                        "Dropping time event '{}' drained outside its callback owner thread",
345                        event.name
346                    );
347                    return false;
348                }
349                let callback = TIME_EVENT_CALLBACKS.with(|callbacks| {
350                    callbacks
351                        .borrow()
352                        .get(&lease.0.id)
353                        .map(|entry| entry.callback.clone())
354                });
355
356                if let Some(callback) = callback {
357                    TimeEventHandler::new(event, callback).run();
358                    true
359                } else {
360                    log::error!("Dropping time event with an unregistered callback token");
361                    false
362                }
363            }
364            #[cfg(any(feature = "live", test))]
365            TimeEventDispatch::Cleanup(lease) => {
366                if lease.0.owner != thread::current().id() {
367                    log::error!("Dropping timer cleanup message outside its callback owner thread");
368                }
369                false
370            }
371        }
372    }
373}
374
375#[cfg(any(feature = "live", test))]
376pub(crate) fn register_time_event_callback(callback: TimeEventCallback) -> TimeEventCallbackToken {
377    TimeEventCallbackToken::register(callback)
378}
379
380pub(crate) fn purge_closed_time_event_callbacks() {
381    TIME_EVENT_CALLBACKS.with(|callbacks| {
382        callbacks.borrow_mut().retain(|_, entry| {
383            entry
384                .token
385                .upgrade()
386                .is_some_and(|token| token.state.load(Ordering::Acquire) != CALLBACK_CLOSED)
387        });
388    });
389}
390
391/// Trait for data command sending that can be implemented for both sync and async runners.
392pub trait DataCommandSender {
393    /// Executes a data command.
394    ///
395    /// - **Sync runners** send the command to a queue for synchronous execution.
396    /// - **Async runners** send the command to a channel for asynchronous execution.
397    fn execute(&self, command: DataCommand);
398}
399
400/// Synchronous [`DataCommandSender`] for backtest environments.
401///
402/// Buffers commands in a thread-local queue for deferred execution,
403/// avoiding `RefCell` re-entrancy when sent from event handler callbacks.
404#[derive(Debug)]
405pub struct SyncDataCommandSender;
406
407impl DataCommandSender for SyncDataCommandSender {
408    fn execute(&self, command: DataCommand) {
409        DATA_CMD_QUEUE.with(|q| q.borrow_mut().push(command));
410    }
411}
412
413/// Drain all buffered data commands, dispatching each to the data engine.
414pub fn drain_data_cmd_queue() {
415    DATA_CMD_QUEUE.with(|q| {
416        let commands: Vec<DataCommand> = q.borrow_mut().drain(..).collect();
417        let endpoint = MessagingSwitchboard::data_engine_execute();
418        for cmd in commands {
419            msgbus::send_data_command(endpoint, cmd);
420        }
421    });
422}
423
424/// Returns `true` if the data command queue is empty.
425pub fn data_cmd_queue_is_empty() -> bool {
426    DATA_CMD_QUEUE.with(|q| q.borrow().is_empty())
427}
428
429/// Gets the global data command sender.
430///
431/// # Panics
432///
433/// Panics if the sender is uninitialized.
434#[must_use]
435pub fn get_data_cmd_sender() -> Arc<dyn DataCommandSender> {
436    DATA_CMD_SENDER.with(|sender| {
437        sender
438            .borrow()
439            .as_ref()
440            .expect("Data command sender should be initialized by runner")
441            .clone()
442    })
443}
444
445/// Sets the global data command sender.
446///
447/// This should be called by the runner when it initializes.
448/// Can only be called once per thread.
449///
450/// # Panics
451///
452/// Panics if a sender has already been set.
453pub fn set_data_cmd_sender(sender: Arc<dyn DataCommandSender>) {
454    DATA_CMD_SENDER.with(|s| {
455        let mut slot = s.borrow_mut();
456        assert!(slot.is_none(), "Data command sender can only be set once");
457        *slot = Some(sender);
458    });
459}
460
461/// Replaces the global data command sender for the current thread.
462pub fn replace_data_cmd_sender(sender: Arc<dyn DataCommandSender>) {
463    DATA_CMD_SENDER.with(|s| {
464        *s.borrow_mut() = Some(sender);
465    });
466}
467
468/// Trait for time event sending that can be implemented for both sync and async runners.
469///
470/// Implementations may transfer messages across threads, but messages for
471/// `RustLocal` callbacks must be dispatched on the callback's owner thread.
472pub trait TimeEventSender: Debug + Send + Sync {
473    /// Sends a live time event message.
474    fn send(&self, message: TimeEventMessage);
475}
476
477/// Gets the global time event sender.
478///
479/// # Panics
480///
481/// Panics if the sender is uninitialized.
482#[must_use]
483pub fn get_time_event_sender() -> Arc<dyn TimeEventSender> {
484    TIME_EVENT_SENDER.with(|sender| {
485        sender
486            .borrow()
487            .as_ref()
488            .expect("Time event sender should be initialized by runner")
489            .clone()
490    })
491}
492
493/// Attempts to get the global time event sender without panicking.
494///
495/// Returns `None` if the sender is not initialized (e.g., in test environments).
496#[must_use]
497pub fn try_get_time_event_sender() -> Option<Arc<dyn TimeEventSender>> {
498    TIME_EVENT_SENDER.with(|sender| sender.borrow().as_ref().cloned())
499}
500
501/// Sets the global time event sender.
502///
503/// Can only be called once per thread.
504///
505/// # Panics
506///
507/// Panics if a sender has already been set.
508pub fn set_time_event_sender(sender: Arc<dyn TimeEventSender>) {
509    TIME_EVENT_SENDER.with(|s| {
510        let mut slot = s.borrow_mut();
511        assert!(slot.is_none(), "Time event sender can only be set once");
512        *slot = Some(sender);
513    });
514}
515
516/// Replaces the global time event sender for the current thread.
517pub fn replace_time_event_sender(sender: Arc<dyn TimeEventSender>) {
518    TIME_EVENT_SENDER.with(|s| {
519        *s.borrow_mut() = Some(sender);
520    });
521}
522
523/// A deferred trading command and its direct endpoint.
524#[derive(Debug)]
525pub struct TradingCommandMessage {
526    endpoint: MStr<Endpoint>,
527    command: TradingCommand,
528}
529
530impl TradingCommandMessage {
531    /// Creates a deferred trading command message.
532    #[must_use]
533    pub const fn new(endpoint: MStr<Endpoint>, command: TradingCommand) -> Self {
534        Self { endpoint, command }
535    }
536
537    /// Returns the trading command carried by this message.
538    #[must_use]
539    pub const fn command(&self) -> &TradingCommand {
540        &self.command
541    }
542
543    /// Returns the direct endpoint carried by this message.
544    #[must_use]
545    pub const fn endpoint(&self) -> MStr<Endpoint> {
546        self.endpoint
547    }
548
549    /// Dispatches the command and returns commands deferred by the endpoint handler.
550    #[must_use]
551    pub fn dispatch(self) -> Vec<Self> {
552        let guard = TradingCommandDispatchGuard::new();
553        msgbus::send_trading_command(self.endpoint, self.command);
554        guard.finish()
555    }
556}
557
558struct TradingCommandDispatchGuard {
559    active: bool,
560}
561
562impl TradingCommandDispatchGuard {
563    fn new() -> Self {
564        TRADING_CMD_DISPATCHES.with(|dispatches| dispatches.borrow_mut().push(Vec::new()));
565        Self { active: true }
566    }
567
568    fn finish(mut self) -> Vec<TradingCommandMessage> {
569        self.active = false;
570        TRADING_CMD_DISPATCHES.with(|dispatches| {
571            dispatches
572                .borrow_mut()
573                .pop()
574                .expect("trading command dispatch should be active")
575        })
576    }
577}
578
579impl Drop for TradingCommandDispatchGuard {
580    fn drop(&mut self) {
581        if self.active {
582            TRADING_CMD_DISPATCHES.with(|dispatches| {
583                dispatches.borrow_mut().pop();
584            });
585        }
586    }
587}
588
589/// Returns `true` while a deferred trading command is being dispatched.
590#[must_use]
591pub fn trading_cmd_is_dispatching() -> bool {
592    TRADING_CMD_DISPATCHES.with(|dispatches| !dispatches.borrow().is_empty())
593}
594
595/// Captures a trading command for dispatch after the current endpoint handler returns.
596///
597/// # Panics
598///
599/// Panics if no deferred trading command is being dispatched.
600pub fn capture_trading_cmd(message: TradingCommandMessage) {
601    TRADING_CMD_DISPATCHES.with(|dispatches| {
602        dispatches
603            .borrow_mut()
604            .last_mut()
605            .expect("trading command dispatch should be active")
606            .push(message);
607    });
608}
609
610/// Trait for trading command sending that can be implemented for both sync and async runners.
611pub trait TradingCommandSender {
612    /// Defers a trading command message.
613    ///
614    /// - **Sync runners** enqueue the message for synchronous execution.
615    /// - **Async runners** send the message to a channel for asynchronous execution.
616    ///
617    /// Runners dispatch each message to the direct endpoint it carries.
618    fn execute(&self, message: TradingCommandMessage);
619}
620
621/// Synchronous [`TradingCommandSender`] for backtest environments.
622///
623/// Buffers commands in a thread-local queue for deferred execution,
624/// avoiding `RefCell` re-entrancy when sent from event handler callbacks.
625#[derive(Debug)]
626pub struct SyncTradingCommandSender;
627
628impl TradingCommandSender for SyncTradingCommandSender {
629    fn execute(&self, message: TradingCommandMessage) {
630        TRADING_CMD_QUEUE.with(|q| q.borrow_mut().push(message));
631    }
632}
633
634/// Drains all buffered trading commands to their direct endpoints.
635pub fn drain_trading_cmd_queue() {
636    TRADING_CMD_QUEUE.with(|q| {
637        let messages: Vec<TradingCommandMessage> = q.borrow_mut().drain(..).collect();
638        for message in messages {
639            dispatch_trading_cmd(message);
640        }
641    });
642}
643
644fn dispatch_trading_cmd(message: TradingCommandMessage) {
645    let mut messages = vec![message];
646    while let Some(message) = messages.pop() {
647        messages.extend(message.dispatch().into_iter().rev());
648    }
649}
650
651/// Returns `true` if the trading command queue is empty.
652pub fn trading_cmd_queue_is_empty() -> bool {
653    TRADING_CMD_QUEUE.with(|q| q.borrow().is_empty())
654}
655
656/// Gets the global trading command sender.
657///
658/// # Panics
659///
660/// Panics if the sender is uninitialized.
661#[must_use]
662pub fn get_trading_cmd_sender() -> Arc<dyn TradingCommandSender> {
663    EXEC_CMD_SENDER.with(|sender| {
664        sender
665            .borrow()
666            .as_ref()
667            .expect("Trading command sender should be initialized by runner")
668            .clone()
669    })
670}
671
672/// Attempts to get the global trading command sender without panicking.
673///
674/// Returns `None` if the sender is not initialized (e.g., in test environments).
675#[must_use]
676pub fn try_get_trading_cmd_sender() -> Option<Arc<dyn TradingCommandSender>> {
677    EXEC_CMD_SENDER.with(|sender| sender.borrow().as_ref().cloned())
678}
679
680/// Sets the global trading command sender.
681///
682/// This should be called by the runner when it initializes.
683/// Can only be called once per thread.
684///
685/// # Panics
686///
687/// Panics if a sender has already been set.
688pub fn set_exec_cmd_sender(sender: Arc<dyn TradingCommandSender>) {
689    EXEC_CMD_SENDER.with(|s| {
690        let mut slot = s.borrow_mut();
691        assert!(
692            slot.is_none(),
693            "Trading command sender can only be set once"
694        );
695        *slot = Some(sender);
696    });
697}
698
699/// Replaces the global trading command sender for the current thread.
700pub fn replace_exec_cmd_sender(sender: Arc<dyn TradingCommandSender>) {
701    EXEC_CMD_SENDER.with(|s| {
702        *s.borrow_mut() = Some(sender);
703    });
704}
705
706thread_local! {
707    static TIME_EVENT_CALLBACKS: RefCell<AHashMap<TimeEventCallbackId, TimeEventCallbackEntry>> = RefCell::new(AHashMap::new());
708    static TIME_EVENT_SENDER: RefCell<Option<Arc<dyn TimeEventSender>>> = const { RefCell::new(None) };
709    static DATA_CMD_SENDER: RefCell<Option<Arc<dyn DataCommandSender>>> = const { RefCell::new(None) };
710    static EXEC_CMD_SENDER: RefCell<Option<Arc<dyn TradingCommandSender>>> = const { RefCell::new(None) };
711    static DATA_CMD_QUEUE: RefCell<Vec<DataCommand>> = const { RefCell::new(Vec::new()) };
712    static TRADING_CMD_QUEUE: RefCell<Vec<TradingCommandMessage>> = const { RefCell::new(Vec::new()) };
713    static TRADING_CMD_DISPATCHES: RefCell<Vec<Vec<TradingCommandMessage>>> = const { RefCell::new(Vec::new()) };
714}
715
716#[cfg(test)]
717mod tests {
718    use std::{
719        cell::{Cell, RefCell},
720        rc::Rc,
721        sync::Arc,
722    };
723
724    use nautilus_core::{UUID4, UnixNanos};
725    use rstest::rstest;
726    use ustr::Ustr;
727
728    use super::*;
729
730    #[derive(Debug)]
731    struct NoopTimeEventSender;
732
733    impl TimeEventSender for NoopTimeEventSender {
734        fn send(&self, _message: TimeEventMessage) {}
735    }
736
737    fn event(name: &str) -> TimeEvent {
738        TimeEvent::new(
739            Ustr::from(name),
740            UUID4::new(),
741            UnixNanos::from(1),
742            UnixNanos::from(2),
743        )
744    }
745
746    fn local_callback(count: Rc<Cell<usize>>) -> TimeEventCallback {
747        TimeEventCallback::RustLocal(Rc::new(move |_| count.set(count.get() + 1)))
748    }
749
750    #[rstest]
751    fn test_time_event_message_is_send_and_sync() {
752        fn assert_send_sync<T: Send + Sync>() {}
753
754        assert_send_sync::<TimeEventMessage>();
755    }
756
757    #[rstest]
758    fn test_registered_time_event_dispatches_on_owner_thread() {
759        let count = Rc::new(Cell::new(0));
760        let token = register_time_event_callback(local_callback(count.clone()));
761        let lease = token.acquire().unwrap();
762        let message = TimeEventMessage::registered(event("same-thread"), lease);
763
764        assert!(message.dispatch());
765        assert_eq!(count.get(), 1);
766        assert!(token.is_registered());
767
768        token.close();
769        assert!(!token.is_registered());
770    }
771
772    #[rstest]
773    fn test_registered_time_event_dropped_on_wrong_thread() {
774        let count = Rc::new(Cell::new(0));
775        let token = register_time_event_callback(local_callback(count.clone()));
776        let lease = token.acquire().unwrap();
777        let message = TimeEventMessage::registered(event("wrong-thread"), lease);
778
779        let dispatched = std::thread::spawn(move || message.dispatch())
780            .join()
781            .unwrap();
782
783        assert!(!dispatched);
784        assert_eq!(count.get(), 0);
785        assert!(token.is_registered());
786
787        token.close();
788        assert!(!token.is_registered());
789    }
790
791    #[rstest]
792    fn test_closing_registered_callback_without_leases_removes_it_immediately() {
793        let token = register_time_event_callback(local_callback(Rc::new(Cell::new(0))));
794        assert!(!token.is_closed());
795
796        token.close();
797
798        assert!(token.is_closed());
799        assert!(!token.is_registered());
800    }
801
802    #[rstest]
803    fn test_closing_registered_callback_preserves_queued_leases_until_last_dispatch() {
804        let count = Rc::new(Cell::new(0));
805        let token = register_time_event_callback(local_callback(count.clone()));
806        let first = TimeEventMessage::registered(event("first"), token.acquire().unwrap());
807        let second = TimeEventMessage::registered(event("second"), token.acquire().unwrap());
808
809        token.close();
810        assert!(token.is_registered());
811
812        assert!(first.dispatch());
813        assert_eq!(count.get(), 1);
814        assert!(token.is_registered());
815
816        assert!(second.dispatch());
817        assert_eq!(count.get(), 2);
818        assert!(!token.is_registered());
819    }
820
821    #[rstest]
822    fn test_replaced_registered_callbacks_have_distinct_lifecycles() {
823        let old_count = Rc::new(Cell::new(0));
824        let old = register_time_event_callback(local_callback(old_count.clone()));
825        let old_message = TimeEventMessage::registered(event("same-name"), old.acquire().unwrap());
826        old.close();
827
828        let new_count = Rc::new(Cell::new(0));
829        let new = register_time_event_callback(local_callback(new_count.clone()));
830
831        assert_ne!(old.0.id, new.0.id);
832        assert!(old_message.dispatch());
833        assert_eq!(old_count.get(), 1);
834        assert_eq!(new_count.get(), 0);
835        assert!(new.is_registered());
836
837        new.close();
838        assert!(!new.is_registered());
839    }
840
841    #[rstest]
842    fn test_one_shot_callback_can_rearm_same_name_without_old_lease_removing_new_callback() {
843        let replacement = Rc::new(RefCell::new(None));
844        let replacement_slot = replacement.clone();
845        let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
846            let token = register_time_event_callback(TimeEventCallback::RustLocal(Rc::new(|_| {})));
847            replacement_slot.replace(Some(token));
848        }));
849        let old = register_time_event_callback(callback);
850        let message = TimeEventMessage::registered(event("rearm"), old.acquire().unwrap());
851        old.close();
852
853        assert!(message.dispatch());
854        assert!(!old.is_registered());
855
856        let new = replacement.borrow_mut().take().unwrap();
857        assert_ne!(old.0.id, new.0.id);
858        assert!(new.is_registered());
859        new.close();
860        assert!(!new.is_registered());
861    }
862
863    #[rstest]
864    fn test_wrong_thread_final_lease_is_lazily_purged_on_owner_thread() {
865        let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(|_| {});
866        let callback_weak = Rc::downgrade(&callback);
867        let token = register_time_event_callback(TimeEventCallback::RustLocal(callback));
868        let message = TimeEventMessage::registered(event("lazy-purge"), token.acquire().unwrap());
869        token.close();
870        drop(token);
871
872        let dispatched = std::thread::spawn(move || message.dispatch())
873            .join()
874            .unwrap();
875
876        assert!(!dispatched);
877        assert!(callback_weak.upgrade().is_some());
878
879        let next = register_time_event_callback(local_callback(Rc::new(Cell::new(0))));
880        assert!(callback_weak.upgrade().is_none());
881        next.close();
882    }
883
884    #[rstest]
885    #[cfg(any(feature = "live", test))]
886    fn test_cleanup_message_removes_callback_without_dispatching() {
887        let count = Rc::new(Cell::new(0));
888        let token = register_time_event_callback(local_callback(count.clone()));
889        let cleanup = TimeEventMessage::cleanup(event("cleanup"), token.acquire().unwrap());
890        token.close();
891
892        assert!(!cleanup.dispatch());
893        assert_eq!(count.get(), 0);
894        assert!(!token.is_registered());
895    }
896
897    #[rstest]
898    fn test_purge_retains_closed_entry_while_final_lease_is_queued() {
899        let count = Rc::new(Cell::new(0));
900        let token = register_time_event_callback(local_callback(count.clone()));
901        let message = TimeEventMessage::registered(event("purge-queued"), token.acquire().unwrap());
902        token.close();
903
904        purge_closed_time_event_callbacks();
905        assert!(token.is_registered());
906
907        assert!(message.dispatch());
908        assert_eq!(count.get(), 1);
909        assert!(!token.is_registered());
910    }
911
912    #[rstest]
913    fn test_off_owner_final_lease_drop_is_reclaimed_by_owner_purge() {
914        let count = Rc::new(Cell::new(0));
915        let token = register_time_event_callback(local_callback(count.clone()));
916        let lease = token.acquire().unwrap();
917        token.close();
918
919        std::thread::spawn(move || drop(lease)).join().unwrap();
920
921        assert!(token.is_registered());
922
923        purge_closed_time_event_callbacks();
924        assert!(!token.is_registered());
925        assert_eq!(count.get(), 0);
926    }
927
928    #[rstest]
929    #[case::token_close(false)]
930    #[case::final_lease(true)]
931    fn test_callback_removal_releases_registry_borrow_before_entry_drop(
932        #[case] via_final_lease: bool,
933    ) {
934        let inner = register_time_event_callback(local_callback(Rc::new(Cell::new(0))));
935        let inner_lease = inner.acquire().unwrap();
936        inner.close();
937
938        // Dropping the outer callback drops this final lease and re-enters the registry
939        let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
940            assert_eq!(inner_lease.0.owner, std::thread::current().id());
941        }));
942        let outer = register_time_event_callback(callback);
943        let outer_lease = via_final_lease.then(|| outer.acquire().unwrap());
944        outer.close();
945        drop(outer_lease);
946
947        assert!(!outer.is_registered());
948        assert!(!inner.is_registered());
949    }
950
951    // The two following tests reproduce the destructor-during-TLS-teardown
952    // abort: a callback holder (a lease, or a token closed from a `Drop` as
953    // `LiveTimer::drop` does) is placed in a thread-local initialized BEFORE
954    // `TIME_EVENT_CALLBACKS`. Rust does not guarantee a destruction order
955    // between independent TLS keys, but the affected implementation (native
956    // Linux TLS) destroys keys LIFO by initialization order, so the registry
957    // is torn down first and the holder's own destructor reaches the removal
958    // path with the registry TLS already gone. On the unfixed `.with` code
959    // that access panics inside a TLS destructor and aborts the whole process
960    // (the thread never joins); the `try_with` guard makes it a no-op. This
961    // mirrors the live path where `MESSAGE_BUS` outlives the callback
962    // registry and drops the last clock owner during teardown.
963
964    #[rstest]
965    fn test_final_lease_drop_survives_registry_tls_teardown() {
966        std::thread::spawn(|| {
967            thread_local! {
968                static HELD_LEASE: RefCell<Option<TimeEventCallbackLease>> =
969                    const { RefCell::new(None) };
970            }
971
972            // Initialize the holder before the registry so it is destroyed last.
973            HELD_LEASE.with(|_| {});
974
975            let token = register_time_event_callback(local_callback(Rc::new(Cell::new(0))));
976            let lease = token.acquire().unwrap();
977            token.close();
978            HELD_LEASE.with(|slot| *slot.borrow_mut() = Some(lease));
979        })
980        .join()
981        .expect("final-lease drop after registry teardown must not abort");
982    }
983
984    #[rstest]
985    fn test_owner_close_survives_registry_tls_teardown() {
986        struct CloseOnDrop(TimeEventCallbackToken);
987
988        impl Drop for CloseOnDrop {
989            fn drop(&mut self) {
990                self.0.close();
991            }
992        }
993
994        std::thread::spawn(|| {
995            thread_local! {
996                static HELD_TOKEN: RefCell<Option<CloseOnDrop>> = const { RefCell::new(None) };
997            }
998
999            // Initialize the holder before the registry so it is destroyed last.
1000            HELD_TOKEN.with(|_| {});
1001
1002            let token = register_time_event_callback(local_callback(Rc::new(Cell::new(0))));
1003            HELD_TOKEN.with(|slot| *slot.borrow_mut() = Some(CloseOnDrop(token)));
1004        })
1005        .join()
1006        .expect("owner close after registry teardown must not abort");
1007    }
1008
1009    #[rstest]
1010    fn test_replace_data_cmd_sender_overwrites_previous() {
1011        std::thread::spawn(|| {
1012            replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
1013            replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
1014            let _sender = get_data_cmd_sender();
1015        })
1016        .join()
1017        .unwrap();
1018    }
1019
1020    #[rstest]
1021    fn test_replace_exec_cmd_sender_overwrites_previous() {
1022        std::thread::spawn(|| {
1023            replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1024            replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1025            let _sender = get_trading_cmd_sender();
1026        })
1027        .join()
1028        .unwrap();
1029    }
1030
1031    #[rstest]
1032    fn test_replace_time_event_sender_overwrites_previous() {
1033        std::thread::spawn(|| {
1034            replace_time_event_sender(Arc::new(NoopTimeEventSender));
1035            replace_time_event_sender(Arc::new(NoopTimeEventSender));
1036            let _sender = get_time_event_sender();
1037        })
1038        .join()
1039        .unwrap();
1040    }
1041
1042    #[rstest]
1043    fn test_set_data_cmd_sender_panics_on_double_set() {
1044        let result = std::thread::spawn(|| {
1045            set_data_cmd_sender(Arc::new(SyncDataCommandSender));
1046            set_data_cmd_sender(Arc::new(SyncDataCommandSender));
1047        })
1048        .join();
1049        assert!(result.is_err());
1050    }
1051
1052    #[rstest]
1053    fn test_set_exec_cmd_sender_panics_on_double_set() {
1054        let result = std::thread::spawn(|| {
1055            set_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1056            set_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1057        })
1058        .join();
1059        assert!(result.is_err());
1060    }
1061
1062    #[rstest]
1063    fn test_set_time_event_sender_panics_on_double_set() {
1064        let result = std::thread::spawn(|| {
1065            set_time_event_sender(Arc::new(NoopTimeEventSender));
1066            set_time_event_sender(Arc::new(NoopTimeEventSender));
1067        })
1068        .join();
1069        assert!(result.is_err());
1070    }
1071
1072    #[rstest]
1073    fn test_try_get_time_event_sender_returns_none_when_unset() {
1074        let result = std::thread::spawn(try_get_time_event_sender)
1075            .join()
1076            .unwrap();
1077        assert!(result.is_none());
1078    }
1079
1080    #[rstest]
1081    fn test_try_get_trading_cmd_sender_returns_none_when_unset() {
1082        let is_none = std::thread::spawn(|| try_get_trading_cmd_sender().is_none())
1083            .join()
1084            .unwrap();
1085        assert!(is_none);
1086    }
1087}