1use std::{
23 cmp::Ordering,
24 fmt::{Debug, Display},
25 num::NonZeroU64,
26 rc::Rc,
27 sync::Arc,
28};
29
30use nautilus_core::{
31 DurationNanos, UUID4, UnixNanos,
32 correctness::{FAILED, check_valid_string_utf8},
33};
34#[cfg(feature = "python")]
35use pyo3::{Py, PyAny, Python};
36use ustr::Ustr;
37
38#[must_use]
40pub fn create_valid_interval(interval_ns: DurationNanos) -> NonZeroU64 {
41 NonZeroU64::new(interval_ns.as_u64()).unwrap_or(NonZeroU64::MIN)
42}
43
44pub trait TimerSchedule: Debug + Send + Sync {
50 fn start_time_ns(&self) -> UnixNanos;
52
53 fn next_after(&self, time_ns: UnixNanos, index: &mut u64) -> Option<UnixNanos>;
58}
59
60#[derive(Debug, Clone)]
62pub enum TimerInterval {
63 Fixed(NonZeroU64),
65 Schedule(Arc<dyn TimerSchedule>),
67}
68
69impl TimerInterval {
70 pub(crate) fn next_time_after(
72 &self,
73 start_time_ns: UnixNanos,
74 time_ns: UnixNanos,
75 index: &mut u64,
76 ) -> Option<UnixNanos> {
77 match self {
78 Self::Fixed(interval_ns) => {
79 *index =
80 time_ns.saturating_duration_since(start_time_ns).as_u64() / interval_ns.get();
81 *index = index.checked_add(1)?;
82 start_time_ns
83 .checked_add(DurationNanos::new(interval_ns.get().checked_mul(*index)?))
84 }
85 Self::Schedule(schedule) => schedule
86 .next_after(time_ns, index)
87 .filter(|next| *next > time_ns),
88 }
89 }
90}
91
92impl From<NonZeroU64> for TimerInterval {
93 fn from(value: NonZeroU64) -> Self {
94 Self::Fixed(value)
95 }
96}
97
98#[repr(C)]
99#[derive(Clone, Debug, PartialEq, Eq)]
100#[cfg_attr(
101 feature = "python",
102 pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
103)]
104#[cfg_attr(
105 feature = "python",
106 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
107)]
108pub struct TimeEvent {
113 pub name: Ustr,
115 pub event_id: UUID4,
117 pub ts_event: UnixNanos,
119 pub ts_init: UnixNanos,
121}
122
123impl TimeEvent {
124 #[must_use]
126 pub const fn new(name: Ustr, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self {
127 Self {
128 name,
129 event_id,
130 ts_event,
131 ts_init,
132 }
133 }
134}
135
136impl Display for TimeEvent {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 write!(
139 f,
140 "{}(name={}, event_id={}, ts_event={}, ts_init={})",
141 stringify!(TimeEvent),
142 self.name,
143 self.event_id,
144 self.ts_event,
145 self.ts_init
146 )
147 }
148}
149
150#[repr(transparent)] #[derive(Clone, Debug, PartialEq, Eq)]
157pub struct ScheduledTimeEvent(
158 pub TimeEvent,
160);
161
162impl ScheduledTimeEvent {
163 #[must_use]
165 pub const fn new(event: TimeEvent) -> Self {
166 Self(event)
167 }
168
169 #[must_use]
171 pub fn into_inner(self) -> TimeEvent {
172 self.0
173 }
174}
175
176impl PartialOrd for ScheduledTimeEvent {
177 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
178 Some(self.cmp(other))
179 }
180}
181
182impl Ord for ScheduledTimeEvent {
183 fn cmp(&self, other: &Self) -> Ordering {
184 cmp_time_events(&other.0, &self.0)
186 }
187}
188
189#[cfg(feature = "python")]
190pub struct PythonTimeEventCallback {
192 callback: Py<PyAny>,
193}
194
195#[cfg(feature = "python")]
196impl PythonTimeEventCallback {
197 #[must_use]
199 pub const fn new(callback: Py<PyAny>) -> Self {
200 Self { callback }
201 }
202
203 pub fn call(&self, event: TimeEvent) {
207 Python::attach(|py| {
208 if let Err(e) = self.callback.call1(py, (event,)) {
209 let exception = crate::python::logging::format_exception(&e);
210
211 log::error!("Python time event callback raised exception:\n{exception}");
212 }
213 });
214 }
215}
216
217#[cfg(feature = "python")]
218impl Debug for PythonTimeEventCallback {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.debug_struct(stringify!(PythonTimeEventCallback))
221 .finish_non_exhaustive()
222 }
223}
224
225#[derive(Clone)]
226pub enum TimeEventCallback {
253 #[cfg(feature = "python")]
255 Python(Arc<PythonTimeEventCallback>),
256 Rust(Arc<dyn Fn(TimeEvent) + Send + Sync>),
258 RustLocal(Rc<dyn Fn(TimeEvent)>),
260}
261
262impl Debug for TimeEventCallback {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 #[cfg(feature = "python")]
266 Self::Python(_) => f.write_str("Python callback"),
267 Self::Rust(_) => f.write_str("Rust callback (thread-safe)"),
268 Self::RustLocal(_) => f.write_str("Rust callback (local)"),
269 }
270 }
271}
272
273impl TimeEventCallback {
274 #[must_use]
279 pub const fn is_local(&self) -> bool {
280 matches!(self, Self::RustLocal(_))
281 }
282
283 pub fn call(&self, event: TimeEvent) {
291 match self {
292 #[cfg(feature = "python")]
293 Self::Python(callback) => callback.call(event),
294 Self::Rust(callback) => callback(event),
295 Self::RustLocal(callback) => callback(event),
296 }
297 }
298}
299
300impl<F> From<F> for TimeEventCallback
301where
302 F: Fn(TimeEvent) + Send + Sync + 'static,
303{
304 fn from(value: F) -> Self {
305 Self::Rust(Arc::new(value))
306 }
307}
308
309impl From<Arc<dyn Fn(TimeEvent) + Send + Sync>> for TimeEventCallback {
310 fn from(value: Arc<dyn Fn(TimeEvent) + Send + Sync>) -> Self {
311 Self::Rust(value)
312 }
313}
314
315impl From<Rc<dyn Fn(TimeEvent)>> for TimeEventCallback {
316 fn from(value: Rc<dyn Fn(TimeEvent)>) -> Self {
317 Self::RustLocal(value)
318 }
319}
320
321#[cfg(feature = "python")]
322impl From<Py<PyAny>> for TimeEventCallback {
323 fn from(value: Py<PyAny>) -> Self {
324 Self::from_python_time_event(value)
325 }
326}
327
328#[cfg(feature = "python")]
329impl TimeEventCallback {
330 #[must_use]
332 pub fn from_python_time_event(callback: Py<PyAny>) -> Self {
333 Self::Python(Arc::new(PythonTimeEventCallback::new(callback)))
334 }
335}
336
337#[repr(C)]
338#[derive(Clone, Debug)]
339pub struct TimeEventHandler {
343 pub event: TimeEvent,
345 pub callback: TimeEventCallback,
347}
348
349impl TimeEventHandler {
350 #[must_use]
352 pub const fn new(event: TimeEvent, callback: TimeEventCallback) -> Self {
353 Self { event, callback }
354 }
355
356 pub fn run(self) {
362 let Self { event, callback } = self;
363 crate::msgbus::dispatch_tap_time_event(&event);
364 callback.call(event);
365 }
366}
367
368impl PartialOrd for TimeEventHandler {
369 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
370 Some(self.cmp(other))
371 }
372}
373
374impl PartialEq for TimeEventHandler {
375 fn eq(&self, other: &Self) -> bool {
376 self.cmp(other).is_eq()
377 }
378}
379
380impl Eq for TimeEventHandler {}
381
382impl Ord for TimeEventHandler {
383 fn cmp(&self, other: &Self) -> Ordering {
384 cmp_time_events(&self.event, &other.event)
385 }
386}
387
388fn cmp_time_events(left: &TimeEvent, right: &TimeEvent) -> Ordering {
389 left.ts_event
390 .cmp(&right.ts_event)
391 .then_with(|| left.name.cmp(&right.name))
392 .then_with(|| left.ts_init.cmp(&right.ts_init))
393 .then_with(|| left.event_id.as_str().cmp(right.event_id.as_str()))
394}
395
396pub(crate) trait Timer {
397 fn is_expired(&self) -> bool;
398 fn cancel(&mut self);
399}
400
401#[derive(Clone, Debug)]
406pub struct VirtualTimer {
407 pub name: Ustr,
409 pub interval: TimerInterval,
411 pub start_time_ns: UnixNanos,
413 pub stop_time_ns: Option<UnixNanos>,
415 pub fire_immediately: bool,
417 next_time_ns: UnixNanos,
418 next_index: u64,
419 is_expired: bool,
420}
421
422impl VirtualTimer {
423 #[must_use]
431 pub fn new(
432 name: Ustr,
433 interval: impl Into<TimerInterval>,
434 start_time_ns: UnixNanos,
435 stop_time_ns: Option<UnixNanos>,
436 fire_immediately: bool,
437 ) -> Self {
438 check_valid_string_utf8(name, stringify!(name)).expect(FAILED);
439
440 let interval = interval.into();
441 let mut next_index = 0;
442
443 let next_time_ns = if fire_immediately {
444 start_time_ns
445 } else {
446 interval
447 .next_time_after(start_time_ns, start_time_ns, &mut next_index)
448 .expect("first timer event should be within the UnixNanos range")
449 };
450
451 Self {
452 name,
453 interval,
454 start_time_ns,
455 stop_time_ns,
456 fire_immediately,
457 next_time_ns,
458 next_index,
459 is_expired: false,
460 }
461 }
462
463 #[must_use]
465 pub const fn next_time_ns(&self) -> UnixNanos {
466 self.next_time_ns
467 }
468
469 #[must_use]
471 pub const fn is_expired(&self) -> bool {
472 self.is_expired
473 }
474
475 pub fn advance(&mut self, to_time_ns: UnixNanos) -> impl Iterator<Item = TimeEvent> + '_ {
480 std::iter::from_fn(move || {
481 if self.next_time_ns > to_time_ns {
482 return None;
483 }
484
485 self.next().map(|(event, _)| event)
486 })
487 }
488
489 pub const fn cancel(&mut self) {
491 self.is_expired = true;
492 }
493}
494
495impl Timer for VirtualTimer {
496 fn is_expired(&self) -> bool {
497 Self::is_expired(self)
498 }
499
500 fn cancel(&mut self) {
501 Self::cancel(self);
502 }
503}
504
505impl Iterator for VirtualTimer {
506 type Item = (TimeEvent, UnixNanos);
507
508 fn next(&mut self) -> Option<Self::Item> {
509 if self.is_expired {
510 return None;
511 }
512
513 if let Some(stop_time_ns) = self.stop_time_ns
515 && self.next_time_ns > stop_time_ns
516 {
517 self.is_expired = true;
518 return None;
519 }
520
521 let event_time_ns = self.next_time_ns;
522
523 let item = (
524 TimeEvent {
525 name: self.name,
526 event_id: UUID4::new(),
527 ts_event: event_time_ns,
528 ts_init: event_time_ns,
529 },
530 event_time_ns,
531 );
532
533 if let Some(following_time_ns) =
534 self.interval
535 .next_time_after(self.start_time_ns, event_time_ns, &mut self.next_index)
536 {
537 self.next_time_ns = following_time_ns;
538 } else {
539 self.is_expired = true;
540 }
541
542 if self.stop_time_ns == Some(event_time_ns) {
543 self.is_expired = true;
544 }
545
546 Some(item)
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use std::{cell::RefCell, collections::BinaryHeap, num::NonZeroU64, rc::Rc, sync::Arc};
553
554 use jiff::{Span, Timestamp};
555 use nautilus_core::{DurationNanos, UUID4, UnixNanos, datetime::get_timezone};
556 #[cfg(feature = "python")]
557 use pyo3::{
558 Bound, PyResult, Python,
559 ffi::c_str,
560 types::{
561 PyAnyMethods, PyCFunction, PyDict, PyList, PyListMethods, PyModule, PyTuple,
562 PyTupleMethods, PyTypeMethods,
563 },
564 };
565 use rstest::*;
566 use ustr::Ustr;
567
568 use super::{
569 ScheduledTimeEvent, TimeEvent, TimeEventCallback, TimeEventHandler, TimerInterval,
570 VirtualTimer, create_valid_interval,
571 };
572 #[cfg(feature = "python")]
573 use crate::logging::{
574 arm_shutdown_on_error, disarm_shutdown_on_error, init_logging,
575 take_shutdown_on_error_trigger,
576 };
577 use crate::{
578 calendar::CalendarSchedule,
579 msgbus::{BusTap, Endpoint, MStr, MessagingSwitchboard, Topic, clear_bus_tap, set_bus_tap},
580 };
581
582 #[rstest]
583 #[case(0, 1)]
584 #[case(1, 1)]
585 #[case(25, 25)]
586 fn test_create_valid_interval(#[case] interval_ns: u64, #[case] expected: u64) {
587 assert_eq!(
588 create_valid_interval(DurationNanos::new(interval_ns)).get(),
589 expected
590 );
591 }
592
593 fn utc_nanos(time: &str) -> UnixNanos {
594 UnixNanos::from(time.parse::<Timestamp>().unwrap())
595 }
596
597 #[rstest]
598 #[case::next_interval(100, 100, 0, Some(110), 1)]
599 #[case::skips_elapsed_intervals(100, 135, 0, Some(140), 4)]
600 #[case::scheduled_time_equal_to_time_is_skipped(100, 140, 3, Some(150), 5)]
601 #[case::unrepresentable(u64::MAX - 5, u64::MAX - 5, 0, None, 1)]
602 fn test_timer_interval_fixed_next_time_after(
603 #[case] start_time_ns: u64,
604 #[case] time_ns: u64,
605 #[case] index: u64,
606 #[case] expected: Option<u64>,
607 #[case] expected_index: u64,
608 ) {
609 let interval = TimerInterval::Fixed(NonZeroU64::new(10).unwrap());
610 let mut index = index;
611
612 let next_time_ns = interval.next_time_after(
613 UnixNanos::from(start_time_ns),
614 UnixNanos::from(time_ns),
615 &mut index,
616 );
617
618 assert_eq!(next_time_ns, expected.map(UnixNanos::from));
619 assert_eq!(index, expected_index);
620 }
621
622 #[rstest]
623 fn test_virtual_timer_calendar_advance_keeps_local_time() {
624 let mut timer = VirtualTimer::new(
625 Ustr::from("TEST_TIMER"),
626 TimerInterval::Schedule(Arc::new(
627 CalendarSchedule::new(
628 &utc_nanos("2026-10-23T07:00:00Z")
629 .to_datetime_utc()
630 .to_zoned(get_timezone("Europe/London").unwrap()),
631 Span::new().days(1),
632 )
633 .unwrap(),
634 )),
635 utc_nanos("2026-10-23T07:00:00Z"),
636 None,
637 false,
638 );
639
640 let events: Vec<UnixNanos> = timer
641 .advance(utc_nanos("2026-10-26T08:00:00Z"))
642 .map(|event| event.ts_event)
643 .collect();
644
645 assert_eq!(
646 events,
647 vec![
648 utc_nanos("2026-10-24T07:00:00Z"),
649 utc_nanos("2026-10-25T08:00:00Z"),
650 utc_nanos("2026-10-26T08:00:00Z"),
651 ]
652 );
653 assert_eq!(timer.next_time_ns(), utc_nanos("2026-10-27T08:00:00Z"));
654 }
655
656 #[rstest]
657 fn test_virtual_timer_advance_within_next_time_ns() {
658 let mut timer = VirtualTimer::new(
659 Ustr::from("TEST_TIMER"),
660 NonZeroU64::new(5).unwrap(),
661 UnixNanos::default(),
662 None,
663 false,
664 );
665 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(1)).collect();
666 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(2)).collect();
667 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(3)).collect();
668 assert_eq!(timer.advance(UnixNanos::from(4)).count(), 0);
669 assert_eq!(timer.next_time_ns, 5);
670 assert!(!timer.is_expired);
671 }
672
673 #[rstest]
674 fn test_virtual_timer_advance_up_to_next_time_ns() {
675 let mut timer = VirtualTimer::new(
676 Ustr::from("TEST_TIMER"),
677 NonZeroU64::new(1).unwrap(),
678 UnixNanos::default(),
679 None,
680 false,
681 );
682 assert_eq!(timer.advance(UnixNanos::from(1)).count(), 1);
683 assert!(!timer.is_expired);
684 }
685
686 #[rstest]
687 fn test_virtual_timer_advance_up_to_next_time_ns_with_stop_time() {
688 let mut timer = VirtualTimer::new(
689 Ustr::from("TEST_TIMER"),
690 NonZeroU64::new(1).unwrap(),
691 UnixNanos::default(),
692 Some(UnixNanos::from(2)),
693 false,
694 );
695 assert_eq!(timer.advance(UnixNanos::from(2)).count(), 2);
696 assert!(timer.is_expired);
697 }
698
699 #[rstest]
700 fn test_virtual_timer_advance_beyond_next_time_ns() {
701 let mut timer = VirtualTimer::new(
702 Ustr::from("TEST_TIMER"),
703 NonZeroU64::new(1).unwrap(),
704 UnixNanos::default(),
705 Some(UnixNanos::from(5)),
706 false,
707 );
708 assert_eq!(timer.advance(UnixNanos::from(5)).count(), 5);
709 assert!(timer.is_expired);
710 }
711
712 #[rstest]
713 fn test_virtual_timer_advance_beyond_stop_time() {
714 let mut timer = VirtualTimer::new(
715 Ustr::from("TEST_TIMER"),
716 NonZeroU64::new(1).unwrap(),
717 UnixNanos::default(),
718 Some(UnixNanos::from(5)),
719 false,
720 );
721 assert_eq!(timer.advance(UnixNanos::from(10)).count(), 5);
722 assert!(timer.is_expired);
723 }
724
725 #[rstest]
726 fn test_virtual_timer_advance_exact_boundary() {
727 let mut timer = VirtualTimer::new(
728 Ustr::from("TEST_TIMER"),
729 NonZeroU64::new(5).unwrap(),
730 UnixNanos::from(0),
731 None,
732 false,
733 );
734 assert_eq!(
735 timer.advance(UnixNanos::from(5)).count(),
736 1,
737 "Expected one event at the 5 ns boundary"
738 );
739 assert_eq!(
740 timer.advance(UnixNanos::from(10)).count(),
741 1,
742 "Expected one event at the 10 ns boundary"
743 );
744 }
745
746 #[rstest]
747 fn test_virtual_timer_fire_immediately_true() {
748 let mut timer = VirtualTimer::new(
749 Ustr::from("TEST_TIMER"),
750 NonZeroU64::new(5).unwrap(),
751 UnixNanos::from(10),
752 None,
753 true, );
755
756 assert_eq!(timer.next_time_ns(), UnixNanos::from(10));
758
759 let events: Vec<TimeEvent> = timer.advance(UnixNanos::from(10)).collect();
761 assert_eq!(events.len(), 1);
762 assert_eq!(events[0].ts_event, UnixNanos::from(10));
763
764 assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
766 }
767
768 #[rstest]
769 fn test_virtual_timer_fire_immediately_false() {
770 let mut timer = VirtualTimer::new(
771 Ustr::from("TEST_TIMER"),
772 NonZeroU64::new(5).unwrap(),
773 UnixNanos::from(10),
774 None,
775 false, );
777
778 assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
780
781 assert_eq!(timer.advance(UnixNanos::from(10)).count(), 0);
783
784 let events: Vec<TimeEvent> = timer.advance(UnixNanos::from(15)).collect();
786 assert_eq!(events.len(), 1);
787 assert_eq!(events[0].ts_event, UnixNanos::from(15));
788 }
789
790 #[rstest]
791 fn test_time_event_handler_ordering_uses_tie_breakers() {
792 let callback = TimeEventCallback::from(|_: TimeEvent| {});
793
794 let later_name = TimeEventHandler::new(
795 TimeEvent::new(
796 Ustr::from("TIME_BAR_ESM4-2-MINUTE-ASK-INTERNAL"),
797 UUID4::from("00000000-0000-4000-8000-000000000003"),
798 100.into(),
799 100.into(),
800 ),
801 callback.clone(),
802 );
803 let earlier_name = TimeEventHandler::new(
804 TimeEvent::new(
805 Ustr::from("SPREAD_QUOTE_ESM4"),
806 UUID4::from("00000000-0000-4000-8000-000000000002"),
807 100.into(),
808 100.into(),
809 ),
810 callback.clone(),
811 );
812 let later_init = TimeEventHandler::new(
813 TimeEvent::new(
814 Ustr::from("SPREAD_QUOTE_ESM4"),
815 UUID4::from("00000000-0000-4000-8000-000000000004"),
816 100.into(),
817 101.into(),
818 ),
819 callback.clone(),
820 );
821 let later_id = TimeEventHandler::new(
822 TimeEvent::new(
823 Ustr::from("SPREAD_QUOTE_ESM4"),
824 UUID4::from("00000000-0000-4000-8000-000000000005"),
825 100.into(),
826 100.into(),
827 ),
828 callback,
829 );
830
831 assert!(earlier_name < later_name);
832 assert!(earlier_name < later_init);
833 assert!(earlier_name < later_id);
834 assert_ne!(earlier_name, later_id);
835 }
836
837 #[rstest]
838 fn test_scheduled_time_event_ordering_laws() {
839 let base = ScheduledTimeEvent::new(TimeEvent::new(
840 Ustr::from("ALPHA"),
841 UUID4::from("00000000-0000-4000-8000-000000000001"),
842 100.into(),
843 10.into(),
844 ));
845 let variants = [
846 base.clone(),
847 ScheduledTimeEvent::new(TimeEvent::new(
848 Ustr::from("BETA"),
849 base.0.event_id,
850 base.0.ts_event,
851 base.0.ts_init,
852 )),
853 ScheduledTimeEvent::new(TimeEvent::new(
854 base.0.name,
855 UUID4::from("00000000-0000-4000-8000-000000000002"),
856 base.0.ts_event,
857 base.0.ts_init,
858 )),
859 ScheduledTimeEvent::new(TimeEvent::new(
860 base.0.name,
861 base.0.event_id,
862 101.into(),
863 base.0.ts_init,
864 )),
865 ScheduledTimeEvent::new(TimeEvent::new(
866 base.0.name,
867 base.0.event_id,
868 base.0.ts_event,
869 11.into(),
870 )),
871 ];
872
873 for a in &variants {
874 for b in &variants {
875 assert_eq!(a == b, a.cmp(b).is_eq());
876 assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
877 assert_eq!(a.cmp(b), b.cmp(a).reverse());
878 }
879 }
880 }
881
882 #[rstest]
883 fn test_scheduled_time_event_heap_ordering() {
884 let expected = [
885 TimeEvent::new(
886 Ustr::from("ALPHA"),
887 UUID4::from("00000000-0000-4000-8000-000000000001"),
888 100.into(),
889 10.into(),
890 ),
891 TimeEvent::new(
892 Ustr::from("ALPHA"),
893 UUID4::from("00000000-0000-4000-8000-000000000002"),
894 100.into(),
895 10.into(),
896 ),
897 TimeEvent::new(
898 Ustr::from("ALPHA"),
899 UUID4::from("00000000-0000-4000-8000-000000000003"),
900 100.into(),
901 11.into(),
902 ),
903 TimeEvent::new(
904 Ustr::from("BETA"),
905 UUID4::from("00000000-0000-4000-8000-000000000004"),
906 100.into(),
907 10.into(),
908 ),
909 TimeEvent::new(
910 Ustr::from("ALPHA"),
911 UUID4::from("00000000-0000-4000-8000-000000000005"),
912 101.into(),
913 10.into(),
914 ),
915 ];
916 let insertion_order = [4, 1, 3, 0, 2];
917 let mut heap = BinaryHeap::new();
918
919 for index in insertion_order {
920 heap.push(ScheduledTimeEvent::new(expected[index].clone()));
921 }
922
923 let popped = std::iter::from_fn(|| heap.pop().map(ScheduledTimeEvent::into_inner))
924 .collect::<Vec<_>>();
925 assert_eq!(popped, expected);
926 }
927
928 #[cfg(feature = "python")]
929 #[rstest]
930 #[case(false)]
931 #[case(true)]
932 fn test_python_callback_exception_requests_shutdown(#[case] shutdown: bool) {
933 Python::initialize();
934 let _guard = init_logging(
935 "TRADER-001".into(),
936 UUID4::new(),
937 Default::default(),
938 Default::default(),
939 )
940 .unwrap();
941 Python::attach(|py| {
942 let module = PyModule::from_code(
943 py,
944 c_str!(
945 r#"
946seen = []
947def callback(event):
948 seen.append(event.name)
949 raise RuntimeError("timer callback failure")
950"#
951 ),
952 c_str!("timer_callback.py"),
953 c_str!("timer_callback"),
954 )
955 .unwrap();
956 let callback = TimeEventCallback::from_python_time_event(
957 module.getattr("callback").unwrap().unbind(),
958 );
959
960 let event = TimeEvent::new(
961 Ustr::from("ALERT"),
962 UUID4::new(),
963 UnixNanos::from(10),
964 UnixNanos::from(11),
965 );
966 arm_shutdown_on_error(shutdown);
967 callback.call(event.clone());
968 let trigger = take_shutdown_on_error_trigger();
969 disarm_shutdown_on_error();
970 callback.call(event);
971 let seen = module
972 .getattr("seen")
973 .unwrap()
974 .extract::<Vec<String>>()
975 .unwrap();
976
977 assert_eq!(seen, ["ALERT", "ALERT"]);
978
979 if shutdown {
980 let trigger = trigger.expect("timer exception must request shutdown");
981 assert!(
982 trigger
983 .message
984 .contains("Python time event callback raised exception:")
985 );
986 assert!(trigger.message.contains("in callback"));
987 assert!(
988 trigger
989 .message
990 .contains("RuntimeError: timer callback failure")
991 );
992 } else {
993 assert_eq!(trigger, None);
994 }
995 });
996 }
997
998 #[cfg(feature = "python")]
999 #[rstest]
1000 fn test_python_callback_passes_time_event() {
1001 Python::initialize();
1002
1003 Python::attach(|py| {
1004 let seen = PyList::empty(py);
1005 let seen_obj = seen.clone().unbind().into_any();
1006
1007 let callback = new_sync_py_callback(
1008 py,
1009 move |args: &Bound<'_, PyTuple>,
1010 _kwargs: Option<&Bound<'_, PyDict>>|
1011 -> PyResult<()> {
1012 let arg = args.get_item(0)?;
1013 let type_name = arg.get_type().name()?.to_string();
1014 Python::attach(|py| seen_obj.call_method1(py, "append", (type_name,)))?;
1015 Ok(())
1016 },
1017 )
1018 .expect("callback should create")
1019 .into_any()
1020 .unbind();
1021
1022 let event = TimeEvent::new(
1023 Ustr::from("PY_CALLBACK_MODE"),
1024 UUID4::from("00000000-0000-4000-8000-000000000007"),
1025 UnixNanos::from(100),
1026 UnixNanos::from(99),
1027 );
1028
1029 TimeEventCallback::from_python_time_event(callback).call(event);
1030
1031 assert_eq!(seen.len(), 1);
1032 assert_eq!(
1033 seen.get_item(0).unwrap().extract::<String>().unwrap(),
1034 "TimeEvent"
1035 );
1036 });
1037 }
1038
1039 #[cfg(feature = "python")]
1040 fn new_sync_py_callback<F>(py: Python<'_>, closure: F) -> PyResult<Bound<'_, PyCFunction>>
1041 where
1042 F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> PyResult<()>
1043 + Send
1044 + Sync
1045 + 'static,
1046 {
1047 PyCFunction::new_closure(py, None, None, closure)
1048 }
1049
1050 #[derive(Default)]
1051 struct RecordingTimeEventTap {
1052 time_events: RefCell<Vec<(String, TimeEvent)>>,
1053 }
1054
1055 impl RecordingTimeEventTap {
1056 fn time_events(&self) -> Vec<(String, TimeEvent)> {
1057 self.time_events.borrow().clone()
1058 }
1059 }
1060
1061 impl BusTap for RecordingTimeEventTap {
1062 fn on_publish(&self, topic: MStr<Topic>, message: &dyn std::any::Any) {
1063 if let Some(event) = message.downcast_ref::<TimeEvent>() {
1064 self.time_events
1065 .borrow_mut()
1066 .push((topic.to_string(), event.clone()));
1067 }
1068 }
1069
1070 fn on_send(&self, _endpoint: MStr<Endpoint>, _message: &dyn std::any::Any) {}
1071 }
1072
1073 #[rstest]
1074 fn test_time_event_handler_run_dispatches_tap_before_callback() {
1075 let event = TimeEvent::new(
1076 Ustr::from("strategy.heartbeat"),
1077 UUID4::from("00000000-0000-4000-8000-000000000006"),
1078 UnixNanos::from(100),
1079 UnixNanos::from(99),
1080 );
1081 let tap = Rc::new(RecordingTimeEventTap::default());
1082 let callback_seen: Rc<RefCell<Vec<TimeEvent>>> = Rc::new(RefCell::new(Vec::new()));
1083 let expected_topic = MessagingSwitchboard::time_event_topic().to_string();
1084 let callback_expected = event.clone();
1085 let callback_expected_topic = expected_topic.clone();
1086 let callback_tap = Rc::clone(&tap);
1087 let callback_seen_ref = Rc::clone(&callback_seen);
1088
1089 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |callback_event| {
1090 assert_eq!(
1091 callback_tap.time_events(),
1092 vec![(callback_expected_topic.clone(), callback_expected.clone())],
1093 );
1094 callback_seen_ref.borrow_mut().push(callback_event);
1095 });
1096
1097 set_bus_tap(tap.clone());
1098 TimeEventHandler::new(event.clone(), TimeEventCallback::from(callback)).run();
1099 clear_bus_tap();
1100
1101 assert_eq!(tap.time_events(), vec![(expected_topic, event.clone())]);
1102 assert_eq!(*callback_seen.borrow(), vec![event]);
1103 }
1104
1105 use proptest::{prelude::*, test_runner::TestCaseResult};
1106
1107 #[derive(Clone, Debug)]
1108 enum TimerOperation {
1109 AdvanceTime(u64),
1110 Cancel,
1111 }
1112
1113 fn timer_operation_strategy() -> impl Strategy<Value = TimerOperation> {
1114 prop_oneof![
1115 8 => (0u64..=1000).prop_map(TimerOperation::AdvanceTime),
1116 2 => Just(TimerOperation::Cancel),
1117 ]
1118 }
1119
1120 fn timer_config_strategy() -> impl Strategy<Value = (u64, u64, Option<u64>, bool)> {
1121 (
1122 1u64..=1000,
1123 timer_start_time_strategy(),
1124 prop::option::of(0u64..=20_000),
1125 prop::bool::ANY,
1126 )
1127 .prop_map(
1128 |(interval_ns, start_time_ns, stop_after_ns, fire_immediately)| {
1129 (
1130 interval_ns,
1131 start_time_ns,
1132 stop_after_ns.map(|offset| start_time_ns + offset),
1133 fire_immediately,
1134 )
1135 },
1136 )
1137 }
1138
1139 fn timer_start_time_strategy() -> impl Strategy<Value = u64> {
1140 prop_oneof![
1141 6 => 0u64..=u64::MAX - TIMER_TIME_HEADROOM,
1142 2 => 0u64..=1_000_000,
1143 1 => Just(1_700_000_000_000_000_000),
1144 1 => Just(u64::MAX - TIMER_TIME_HEADROOM),
1145 ]
1146 }
1147
1148 fn timer_test_strategy()
1149 -> impl Strategy<Value = (Vec<TimerOperation>, (u64, u64, Option<u64>, bool))> {
1150 (
1151 prop::collection::vec(timer_operation_strategy(), 5..=75),
1152 timer_config_strategy(),
1153 )
1154 }
1155
1156 fn test_timer_with_operations(
1157 operations: Vec<TimerOperation>,
1158 (interval_ns, start_time_ns, stop_time_ns, fire_immediately): (u64, u64, Option<u64>, bool),
1159 ) -> TestCaseResult {
1160 let mut timer = VirtualTimer::new(
1161 Ustr::from("PROP_TEST_TIMER"),
1162 NonZeroU64::new(interval_ns).unwrap(),
1163 UnixNanos::from(start_time_ns),
1164 stop_time_ns.map(UnixNanos::from),
1165 fire_immediately,
1166 );
1167
1168 let mut current_time = start_time_ns;
1169
1170 let mut expected_next = if fire_immediately {
1171 start_time_ns
1172 } else {
1173 start_time_ns + interval_ns
1174 };
1175
1176 let mut expected_expired = false;
1177
1178 for operation in operations {
1179 match operation {
1180 TimerOperation::AdvanceTime(delta) => {
1181 let to_time = current_time + delta;
1182 let actual: Vec<(Ustr, u64, u64)> = timer
1183 .advance(UnixNanos::from(to_time))
1184 .map(|event| time_event_state(&event))
1185 .collect();
1186 let expected = expected_event_states(
1187 expected_event_times(
1188 to_time,
1189 interval_ns,
1190 stop_time_ns,
1191 &mut expected_next,
1192 &mut expected_expired,
1193 ),
1194 Ustr::from("PROP_TEST_TIMER"),
1195 );
1196 current_time = to_time;
1197
1198 prop_assert_eq!(actual, expected);
1199 }
1200 TimerOperation::Cancel => {
1201 timer.cancel();
1202 expected_expired = true;
1203 }
1204 }
1205
1206 prop_assert_eq!(timer.is_expired(), expected_expired);
1207 prop_assert_eq!(timer.next_time_ns().as_u64(), expected_next);
1208 }
1209
1210 if !expected_expired && let Some(stop_time_ns) = stop_time_ns {
1211 let to_time = stop_time_ns.saturating_add(interval_ns);
1212 let actual: Vec<(Ustr, u64, u64)> = timer
1213 .advance(UnixNanos::from(to_time))
1214 .map(|event| time_event_state(&event))
1215 .collect();
1216 let expected = expected_event_states(
1217 expected_event_times(
1218 to_time,
1219 interval_ns,
1220 Some(stop_time_ns),
1221 &mut expected_next,
1222 &mut expected_expired,
1223 ),
1224 Ustr::from("PROP_TEST_TIMER"),
1225 );
1226 prop_assert_eq!(actual, expected);
1227 prop_assert!(expected_expired);
1228 prop_assert!(timer.is_expired());
1229 prop_assert_eq!(timer.next_time_ns().as_u64(), expected_next);
1230 }
1231
1232 Ok(())
1233 }
1234
1235 fn expected_event_times(
1236 to_time: u64,
1237 interval_ns: u64,
1238 stop_time_ns: Option<u64>,
1239 next_time: &mut u64,
1240 is_expired: &mut bool,
1241 ) -> Vec<u64> {
1242 let mut events = Vec::new();
1243
1244 while !*is_expired && *next_time <= to_time {
1245 if let Some(stop_time_ns) = stop_time_ns
1246 && *next_time > stop_time_ns
1247 {
1248 *is_expired = true;
1249 break;
1250 }
1251
1252 let event_time = *next_time;
1253 events.push(event_time);
1254
1255 let Some(following_time) = event_time.checked_add(interval_ns) else {
1256 *is_expired = true;
1257 break;
1258 };
1259
1260 *next_time = following_time;
1261
1262 if Some(event_time) == stop_time_ns {
1263 *is_expired = true;
1264 break;
1265 }
1266 }
1267
1268 events
1269 }
1270
1271 proptest! {
1272 #[rstest]
1273 fn prop_timer_advance_operations((operations, config) in timer_test_strategy()) {
1274 test_timer_with_operations(operations, config)?;
1275 }
1276
1277 #[rstest]
1278 fn prop_timer_advance_batching_is_consistent(
1279 interval_ns in 1u64..=1000,
1280 start_time_ns in timer_start_time_strategy(),
1281 fire_immediately in prop::bool::ANY,
1282 advance_count in 1u64..=20,
1283 ) {
1284 let mut timer = VirtualTimer::new(
1285 Ustr::from("CONSISTENCY_TEST"),
1286 NonZeroU64::new(interval_ns).unwrap(),
1287 UnixNanos::from(start_time_ns),
1288 None, fire_immediately,
1290 );
1291
1292 let first_event_time = if fire_immediately { start_time_ns } else { start_time_ns + interval_ns };
1293 let final_event_time = first_event_time + interval_ns * (advance_count - 1);
1294 let expected = expected_event_states(
1295 (0..advance_count)
1296 .map(|index| first_event_time + interval_ns * index)
1297 .collect(),
1298 Ustr::from("CONSISTENCY_TEST"),
1299 );
1300
1301 let mut batched_timer = timer.clone();
1302 let batched: Vec<(Ustr, u64, u64)> = batched_timer
1303 .advance(UnixNanos::from(final_event_time))
1304 .map(|event| time_event_state(&event))
1305 .collect();
1306
1307 let mut stepped = Vec::new();
1308
1309 for event_time in
1310 (0..advance_count).map(|index| first_event_time + interval_ns * index)
1311 {
1312 stepped.extend(
1313 timer
1314 .advance(UnixNanos::from(event_time))
1315 .map(|event| time_event_state(&event)),
1316 );
1317 }
1318
1319 prop_assert_eq!(&batched, &expected);
1320 prop_assert_eq!(&stepped, &expected);
1321 prop_assert_eq!(timer.next_time_ns(), batched_timer.next_time_ns());
1322 prop_assert_eq!(timer.is_expired(), batched_timer.is_expired());
1323 }
1324
1325 #[rstest]
1326 fn prop_timer_terminal_time_does_not_require_following_time(
1327 (interval_ns, event_headroom) in terminal_time_strategy(),
1328 fire_immediately in prop::bool::ANY,
1329 bounded in prop::bool::ANY,
1330 ) {
1331 let event_time_ns = u64::MAX - event_headroom;
1332 let start_time_ns = if fire_immediately {
1333 event_time_ns
1334 } else {
1335 event_time_ns - interval_ns
1336 };
1337 let mut timer = VirtualTimer::new(
1338 Ustr::from("TERMINAL_STOP_TEST"),
1339 NonZeroU64::new(interval_ns).unwrap(),
1340 UnixNanos::from(start_time_ns),
1341 bounded.then_some(UnixNanos::max()),
1342 fire_immediately,
1343 );
1344
1345 let events: Vec<(Ustr, u64, u64)> = timer
1346 .advance(UnixNanos::max())
1347 .map(|event| time_event_state(&event))
1348 .collect();
1349
1350 prop_assert_eq!(
1351 events,
1352 vec![(Ustr::from("TERMINAL_STOP_TEST"), event_time_ns, event_time_ns)]
1353 );
1354 prop_assert!(timer.is_expired());
1355 prop_assert_eq!(timer.next_time_ns(), UnixNanos::from(event_time_ns));
1356 }
1357 }
1358
1359 const TIMER_TIME_HEADROOM: u64 = 100_000;
1360
1361 fn time_event_state(event: &TimeEvent) -> (Ustr, u64, u64) {
1362 (event.name, event.ts_event.as_u64(), event.ts_init.as_u64())
1363 }
1364
1365 fn expected_event_states(times: Vec<u64>, name: Ustr) -> Vec<(Ustr, u64, u64)> {
1366 times.into_iter().map(|time| (name, time, time)).collect()
1367 }
1368
1369 fn terminal_time_strategy() -> impl Strategy<Value = (u64, u64)> {
1370 (1u64..=1000).prop_flat_map(|interval_ns| (Just(interval_ns), 0u64..interval_ns))
1371 }
1372}