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