1use 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#[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#[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 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#[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 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#[derive(Debug)]
272pub struct TimeEventMessage {
273 event: TimeEvent,
274 dispatch: TimeEventDispatch,
275}
276
277impl TimeEventMessage {
278 #[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 #[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 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
392pub trait DataCommandSender {
394 fn execute(&self, command: DataCommand);
399}
400
401#[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
419pub 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
453pub fn data_cmd_queue_is_empty() -> bool {
455 DATA_CMD_QUEUE.with(|q| q.borrow().is_empty())
456}
457
458pub 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#[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
485pub 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
501pub fn replace_data_cmd_sender(sender: Arc<dyn DataCommandSender>) {
503 DATA_CMD_SENDER.with(|s| {
504 *s.borrow_mut() = Some(sender);
505 });
506}
507
508pub trait TimeEventSender: Debug + Send + Sync {
513 fn send(&self, message: TimeEventMessage);
515}
516
517#[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#[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
541pub 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
556pub 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#[derive(Debug)]
565pub struct TradingCommandMessage {
566 endpoint: MStr<Endpoint>,
567 command: TradingCommand,
568}
569
570impl TradingCommandMessage {
571 #[must_use]
573 pub const fn new(endpoint: MStr<Endpoint>, command: TradingCommand) -> Self {
574 Self { endpoint, command }
575 }
576
577 #[must_use]
579 pub const fn command(&self) -> &TradingCommand {
580 &self.command
581 }
582
583 #[must_use]
585 pub const fn endpoint(&self) -> MStr<Endpoint> {
586 self.endpoint
587 }
588
589 #[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#[must_use]
655pub fn trading_cmd_is_dispatching() -> bool {
656 TRADING_CMD_DISPATCHES.with(|dispatches| !dispatches.borrow().is_empty())
657}
658
659pub 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
679pub trait TradingCommandSender {
681 fn execute(&self, message: TradingCommandMessage);
688}
689
690#[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
704pub 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 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
785pub fn trading_cmd_queue_is_empty() -> bool {
787 TRADING_CMD_QUEUE.with(|q| q.borrow().is_empty())
788}
789
790#[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#[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
814pub 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
833pub 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 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 #[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 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 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}