1use 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#[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#[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 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#[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 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#[derive(Debug)]
271pub struct TimeEventMessage {
272 event: TimeEvent,
273 dispatch: TimeEventDispatch,
274}
275
276impl TimeEventMessage {
277 #[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 #[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 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
391pub trait DataCommandSender {
393 fn execute(&self, command: DataCommand);
398}
399
400#[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
413pub 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
424pub fn data_cmd_queue_is_empty() -> bool {
426 DATA_CMD_QUEUE.with(|q| q.borrow().is_empty())
427}
428
429#[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
445pub 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
461pub fn replace_data_cmd_sender(sender: Arc<dyn DataCommandSender>) {
463 DATA_CMD_SENDER.with(|s| {
464 *s.borrow_mut() = Some(sender);
465 });
466}
467
468pub trait TimeEventSender: Debug + Send + Sync {
473 fn send(&self, message: TimeEventMessage);
475}
476
477#[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#[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
501pub 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
516pub 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#[derive(Debug)]
525pub struct TradingCommandMessage {
526 endpoint: MStr<Endpoint>,
527 command: TradingCommand,
528}
529
530impl TradingCommandMessage {
531 #[must_use]
533 pub const fn new(endpoint: MStr<Endpoint>, command: TradingCommand) -> Self {
534 Self { endpoint, command }
535 }
536
537 #[must_use]
539 pub const fn command(&self) -> &TradingCommand {
540 &self.command
541 }
542
543 #[must_use]
545 pub const fn endpoint(&self) -> MStr<Endpoint> {
546 self.endpoint
547 }
548
549 #[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#[must_use]
591pub fn trading_cmd_is_dispatching() -> bool {
592 TRADING_CMD_DISPATCHES.with(|dispatches| !dispatches.borrow().is_empty())
593}
594
595pub 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
610pub trait TradingCommandSender {
612 fn execute(&self, message: TradingCommandMessage);
619}
620
621#[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
634pub 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
651pub fn trading_cmd_queue_is_empty() -> bool {
653 TRADING_CMD_QUEUE.with(|q| q.borrow().is_empty())
654}
655
656#[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#[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
680pub 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
699pub 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 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 #[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 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 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}