1use std::{
19 cmp::Ordering,
20 fmt::{Debug, Display},
21 num::NonZeroU64,
22 rc::Rc,
23 sync::Arc,
24};
25
26#[cfg(feature = "python")]
27use nautilus_core::python::IntoPyObjectNautilusExt;
28use nautilus_core::{
29 UUID4, UnixNanos,
30 correctness::{FAILED, check_valid_string_utf8},
31};
32#[cfg(feature = "python")]
33use pyo3::{Py, PyAny, PyResult, Python, types::PyCapsule};
34use ustr::Ustr;
35
36#[must_use]
40pub fn create_valid_interval(interval_ns: u64) -> NonZeroU64 {
41 NonZeroU64::new(interval_ns).unwrap_or(NonZeroU64::MIN)
42}
43
44#[repr(C)]
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[cfg_attr(
47 feature = "python",
48 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common", from_py_object)
49)]
50#[cfg_attr(
51 feature = "python",
52 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
53)]
54pub struct TimeEvent {
59 pub name: Ustr,
61 pub event_id: UUID4,
63 pub ts_event: UnixNanos,
65 pub ts_init: UnixNanos,
67}
68
69impl TimeEvent {
70 #[must_use]
72 pub const fn new(name: Ustr, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self {
73 Self {
74 name,
75 event_id,
76 ts_event,
77 ts_init,
78 }
79 }
80}
81
82impl Display for TimeEvent {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(
85 f,
86 "{}(name={}, event_id={}, ts_event={}, ts_init={})",
87 stringify!(TimeEvent),
88 self.name,
89 self.event_id,
90 self.ts_event,
91 self.ts_init
92 )
93 }
94}
95
96#[repr(transparent)] #[derive(Clone, Debug, PartialEq, Eq)]
103pub struct ScheduledTimeEvent(pub TimeEvent);
104
105impl ScheduledTimeEvent {
106 #[must_use]
108 pub const fn new(event: TimeEvent) -> Self {
109 Self(event)
110 }
111
112 #[must_use]
114 pub fn into_inner(self) -> TimeEvent {
115 self.0
116 }
117}
118
119impl PartialOrd for ScheduledTimeEvent {
120 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
121 Some(self.cmp(other))
122 }
123}
124
125impl Ord for ScheduledTimeEvent {
126 fn cmp(&self, other: &Self) -> Ordering {
127 other
129 .0
130 .ts_event
131 .cmp(&self.0.ts_event)
132 .then_with(|| other.0.name.cmp(&self.0.name))
133 .then_with(|| other.0.ts_init.cmp(&self.0.ts_init))
134 .then_with(|| other.0.event_id.as_str().cmp(self.0.event_id.as_str()))
135 }
136}
137
138#[cfg(feature = "python")]
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum PythonTimeEventCallbackArg {
142 TimeEvent,
144 LegacyCapsule,
146}
147
148#[cfg(feature = "python")]
149pub struct PythonTimeEventCallback {
151 callback: Py<PyAny>,
152 arg: PythonTimeEventCallbackArg,
153}
154
155#[cfg(feature = "python")]
156impl PythonTimeEventCallback {
157 #[must_use]
159 pub const fn new(callback: Py<PyAny>, arg: PythonTimeEventCallbackArg) -> Self {
160 Self { callback, arg }
161 }
162
163 #[must_use]
165 pub const fn callback(&self) -> &Py<PyAny> {
166 &self.callback
167 }
168
169 pub fn call(&self, event: TimeEvent) {
171 Python::attach(|py| {
172 let result = match self.arg {
173 PythonTimeEventCallbackArg::TimeEvent => self.callback.call1(py, (event,)),
174 PythonTimeEventCallbackArg::LegacyCapsule => {
175 call_legacy_python_time_event_callback(py, event, &self.callback)
176 }
177 };
178
179 if let Err(e) = result {
180 log::error!("Python time event callback raised exception: {e}");
181 }
182 });
183 }
184}
185
186#[cfg(feature = "python")]
187impl Debug for PythonTimeEventCallback {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct(stringify!(PythonTimeEventCallback))
190 .field("arg", &self.arg)
191 .finish_non_exhaustive()
192 }
193}
194
195pub enum TimeEventCallback {
222 #[cfg(feature = "python")]
224 Python(Arc<PythonTimeEventCallback>),
225 Rust(Arc<dyn Fn(TimeEvent) + Send + Sync>),
227 RustLocal(Rc<dyn Fn(TimeEvent)>),
229}
230
231impl Clone for TimeEventCallback {
232 fn clone(&self) -> Self {
233 match self {
234 #[cfg(feature = "python")]
235 Self::Python(callback) => Self::Python(callback.clone()),
236 Self::Rust(cb) => Self::Rust(cb.clone()),
237 Self::RustLocal(cb) => Self::RustLocal(cb.clone()),
238 }
239 }
240}
241
242impl Debug for TimeEventCallback {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 #[cfg(feature = "python")]
246 Self::Python(_) => f.write_str("Python callback"),
247 Self::Rust(_) => f.write_str("Rust callback (thread-safe)"),
248 Self::RustLocal(_) => f.write_str("Rust callback (local)"),
249 }
250 }
251}
252
253impl TimeEventCallback {
254 #[must_use]
256 pub const fn is_rust(&self) -> bool {
257 matches!(self, Self::Rust(_))
258 }
259
260 #[must_use]
265 pub const fn is_local(&self) -> bool {
266 matches!(self, Self::RustLocal(_))
267 }
268
269 pub fn call(&self, event: TimeEvent) {
273 match self {
274 #[cfg(feature = "python")]
275 Self::Python(callback) => callback.call(event),
276 Self::Rust(callback) => callback(event),
277 Self::RustLocal(callback) => callback(event),
278 }
279 }
280}
281
282impl<F> From<F> for TimeEventCallback
283where
284 F: Fn(TimeEvent) + Send + Sync + 'static,
285{
286 fn from(value: F) -> Self {
287 Self::Rust(Arc::new(value))
288 }
289}
290
291impl From<Arc<dyn Fn(TimeEvent) + Send + Sync>> for TimeEventCallback {
292 fn from(value: Arc<dyn Fn(TimeEvent) + Send + Sync>) -> Self {
293 Self::Rust(value)
294 }
295}
296
297impl From<Rc<dyn Fn(TimeEvent)>> for TimeEventCallback {
298 fn from(value: Rc<dyn Fn(TimeEvent)>) -> Self {
299 Self::RustLocal(value)
300 }
301}
302
303#[cfg(feature = "python")]
304impl From<Py<PyAny>> for TimeEventCallback {
305 fn from(value: Py<PyAny>) -> Self {
306 Self::from_python_time_event(value)
307 }
308}
309
310#[cfg(feature = "python")]
311impl TimeEventCallback {
312 #[must_use]
314 pub fn from_python_time_event(callback: Py<PyAny>) -> Self {
315 Self::Python(Arc::new(PythonTimeEventCallback::new(
316 callback,
317 PythonTimeEventCallbackArg::TimeEvent,
318 )))
319 }
320
321 #[must_use]
323 pub fn from_python_legacy_capsule(callback: Py<PyAny>) -> Self {
324 Self::Python(Arc::new(PythonTimeEventCallback::new(
325 callback,
326 PythonTimeEventCallbackArg::LegacyCapsule,
327 )))
328 }
329}
330
331#[cfg(feature = "python")]
332fn call_legacy_python_time_event_callback(
333 py: Python<'_>,
334 event: TimeEvent,
335 callback: &Py<PyAny>,
336) -> PyResult<Py<PyAny>> {
337 #[allow(
338 deprecated,
339 reason = "unnamed capsules are required for legacy Cython time-event callbacks"
340 )]
341 let capsule: Py<PyAny> = PyCapsule::new_with_destructor(py, event, None, |_, _| {})
342 .expect("Error creating `PyCapsule`")
343 .into_py_any_unwrap(py);
344
345 callback.call1(py, (capsule,))
346}
347
348#[repr(C)]
349#[derive(Clone, Debug)]
350pub struct TimeEventHandler {
355 pub event: TimeEvent,
357 pub callback: TimeEventCallback,
359}
360
361impl TimeEventHandler {
362 #[must_use]
364 pub const fn new(event: TimeEvent, callback: TimeEventCallback) -> Self {
365 Self { event, callback }
366 }
367
368 fn cmp_event(&self, other: &Self) -> Ordering {
369 self.event
370 .ts_event
371 .cmp(&other.event.ts_event)
372 .then_with(|| self.event.name.cmp(&other.event.name))
373 .then_with(|| self.event.ts_init.cmp(&other.event.ts_init))
374 .then_with(|| {
375 self.event
376 .event_id
377 .as_str()
378 .cmp(other.event.event_id.as_str())
379 })
380 }
381
382 pub fn run(self) {
384 let Self { event, callback } = self;
385 crate::msgbus::dispatch_tap_time_event(&event);
386 callback.call(event);
387 }
388}
389
390impl PartialOrd for TimeEventHandler {
391 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
392 Some(self.cmp(other))
393 }
394}
395
396impl PartialEq for TimeEventHandler {
397 fn eq(&self, other: &Self) -> bool {
398 self.cmp_event(other).is_eq()
399 }
400}
401
402impl Eq for TimeEventHandler {}
403
404impl Ord for TimeEventHandler {
405 fn cmp(&self, other: &Self) -> Ordering {
406 self.cmp_event(other)
407 }
408}
409
410pub(crate) trait Timer {
411 fn is_expired(&self) -> bool;
412 fn cancel(&mut self);
413}
414
415#[derive(Clone, Debug)]
424pub struct TestTimer {
425 pub name: Ustr,
427 pub interval_ns: NonZeroU64,
429 pub start_time_ns: UnixNanos,
431 pub stop_time_ns: Option<UnixNanos>,
433 pub fire_immediately: bool,
435 next_time_ns: UnixNanos,
436 is_expired: bool,
437}
438
439impl TestTimer {
440 #[must_use]
446 pub fn new(
447 name: Ustr,
448 interval_ns: NonZeroU64,
449 start_time_ns: UnixNanos,
450 stop_time_ns: Option<UnixNanos>,
451 fire_immediately: bool,
452 ) -> Self {
453 check_valid_string_utf8(name, stringify!(name)).expect(FAILED);
454
455 let next_time_ns = if fire_immediately {
456 start_time_ns
457 } else {
458 start_time_ns + interval_ns.get()
459 };
460
461 Self {
462 name,
463 interval_ns,
464 start_time_ns,
465 stop_time_ns,
466 fire_immediately,
467 next_time_ns,
468 is_expired: false,
469 }
470 }
471
472 #[must_use]
474 pub const fn next_time_ns(&self) -> UnixNanos {
475 self.next_time_ns
476 }
477
478 #[must_use]
480 pub const fn is_expired(&self) -> bool {
481 self.is_expired
482 }
483
484 #[must_use]
485 pub const fn pop_event(&self, event_id: UUID4, ts_init: UnixNanos) -> TimeEvent {
486 TimeEvent {
487 name: self.name,
488 event_id,
489 ts_event: self.next_time_ns,
490 ts_init,
491 }
492 }
493
494 pub fn advance(&mut self, to_time_ns: UnixNanos) -> impl Iterator<Item = TimeEvent> + '_ {
500 let advances = if self.next_time_ns <= to_time_ns {
502 ((to_time_ns.as_u64() - self.next_time_ns.as_u64()) / self.interval_ns.get())
503 .saturating_add(1)
504 } else {
505 0
506 };
507 self.take(advances as usize).map(|(event, _)| event)
508 }
509
510 pub const fn cancel(&mut self) {
514 self.is_expired = true;
515 }
516}
517
518impl Timer for TestTimer {
519 fn is_expired(&self) -> bool {
520 self.is_expired
521 }
522
523 fn cancel(&mut self) {
524 self.is_expired = true;
525 }
526}
527
528impl Iterator for TestTimer {
529 type Item = (TimeEvent, UnixNanos);
530
531 fn next(&mut self) -> Option<Self::Item> {
532 if self.is_expired {
533 None
534 } else {
535 if let Some(stop_time_ns) = self.stop_time_ns
537 && self.next_time_ns > stop_time_ns
538 {
539 self.is_expired = true;
540 return None;
541 }
542
543 let event_time_ns = self.next_time_ns;
544
545 let item = (
546 TimeEvent {
547 name: self.name,
548 event_id: UUID4::new(),
549 ts_event: event_time_ns,
550 ts_init: event_time_ns,
551 },
552 event_time_ns,
553 );
554
555 if let Some(following_time_ns) = event_time_ns.checked_add(self.interval_ns.get()) {
556 self.next_time_ns = following_time_ns;
557 } else {
558 self.is_expired = true;
559 }
560
561 if self.stop_time_ns == Some(event_time_ns) {
562 self.is_expired = true;
563 }
564
565 Some(item)
566 }
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use std::{cell::RefCell, collections::BinaryHeap, num::NonZeroU64, rc::Rc};
573
574 use nautilus_core::{UUID4, UnixNanos};
575 #[cfg(feature = "python")]
576 use pyo3::{
577 Bound, PyResult, Python,
578 types::{
579 PyAnyMethods, PyCFunction, PyDict, PyList, PyListMethods, PyTuple, PyTupleMethods,
580 PyTypeMethods,
581 },
582 };
583 use rstest::*;
584 use ustr::Ustr;
585
586 use super::{
587 ScheduledTimeEvent, TestTimer, TimeEvent, TimeEventCallback, TimeEventHandler,
588 create_valid_interval,
589 };
590 use crate::msgbus::{
591 BusTap, Endpoint, MStr, MessagingSwitchboard, Topic, clear_bus_tap, set_bus_tap,
592 };
593
594 #[rstest]
595 #[case(0, 1)]
596 #[case(1, 1)]
597 #[case(25, 25)]
598 fn test_create_valid_interval(#[case] interval_ns: u64, #[case] expected: u64) {
599 assert_eq!(create_valid_interval(interval_ns).get(), expected);
600 }
601
602 #[rstest]
603 fn test_test_timer_pop_event() {
604 let mut timer = TestTimer::new(
605 Ustr::from("TEST_TIMER"),
606 NonZeroU64::new(1).unwrap(),
607 UnixNanos::from(1),
608 None,
609 false,
610 );
611
612 assert!(timer.next().is_some());
613 assert!(timer.next().is_some());
614 timer.is_expired = true;
615 assert!(timer.next().is_none());
616 }
617
618 #[rstest]
619 fn test_test_timer_advance_within_next_time_ns() {
620 let mut timer = TestTimer::new(
621 Ustr::from("TEST_TIMER"),
622 NonZeroU64::new(5).unwrap(),
623 UnixNanos::default(),
624 None,
625 false,
626 );
627 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(1)).collect();
628 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(2)).collect();
629 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(3)).collect();
630 assert_eq!(timer.advance(UnixNanos::from(4)).count(), 0);
631 assert_eq!(timer.next_time_ns, 5);
632 assert!(!timer.is_expired);
633 }
634
635 #[rstest]
636 fn test_test_timer_advance_up_to_next_time_ns() {
637 let mut timer = TestTimer::new(
638 Ustr::from("TEST_TIMER"),
639 NonZeroU64::new(1).unwrap(),
640 UnixNanos::default(),
641 None,
642 false,
643 );
644 assert_eq!(timer.advance(UnixNanos::from(1)).count(), 1);
645 assert!(!timer.is_expired);
646 }
647
648 #[rstest]
649 fn test_test_timer_advance_up_to_next_time_ns_with_stop_time() {
650 let mut timer = TestTimer::new(
651 Ustr::from("TEST_TIMER"),
652 NonZeroU64::new(1).unwrap(),
653 UnixNanos::default(),
654 Some(UnixNanos::from(2)),
655 false,
656 );
657 assert_eq!(timer.advance(UnixNanos::from(2)).count(), 2);
658 assert!(timer.is_expired);
659 }
660
661 #[rstest]
662 fn test_test_timer_advance_beyond_next_time_ns() {
663 let mut timer = TestTimer::new(
664 Ustr::from("TEST_TIMER"),
665 NonZeroU64::new(1).unwrap(),
666 UnixNanos::default(),
667 Some(UnixNanos::from(5)),
668 false,
669 );
670 assert_eq!(timer.advance(UnixNanos::from(5)).count(), 5);
671 assert!(timer.is_expired);
672 }
673
674 #[rstest]
675 fn test_test_timer_advance_beyond_stop_time() {
676 let mut timer = TestTimer::new(
677 Ustr::from("TEST_TIMER"),
678 NonZeroU64::new(1).unwrap(),
679 UnixNanos::default(),
680 Some(UnixNanos::from(5)),
681 false,
682 );
683 assert_eq!(timer.advance(UnixNanos::from(10)).count(), 5);
684 assert!(timer.is_expired);
685 }
686
687 #[rstest]
688 fn test_test_timer_advance_exact_boundary() {
689 let mut timer = TestTimer::new(
690 Ustr::from("TEST_TIMER"),
691 NonZeroU64::new(5).unwrap(),
692 UnixNanos::from(0),
693 None,
694 false,
695 );
696 assert_eq!(
697 timer.advance(UnixNanos::from(5)).count(),
698 1,
699 "Expected one event at the 5 ns boundary"
700 );
701 assert_eq!(
702 timer.advance(UnixNanos::from(10)).count(),
703 1,
704 "Expected one event at the 10 ns boundary"
705 );
706 }
707
708 #[rstest]
709 fn test_test_timer_fire_immediately_true() {
710 let mut timer = TestTimer::new(
711 Ustr::from("TEST_TIMER"),
712 NonZeroU64::new(5).unwrap(),
713 UnixNanos::from(10),
714 None,
715 true, );
717
718 assert_eq!(timer.next_time_ns(), UnixNanos::from(10));
720
721 let events: Vec<TimeEvent> = timer.advance(UnixNanos::from(10)).collect();
723 assert_eq!(events.len(), 1);
724 assert_eq!(events[0].ts_event, UnixNanos::from(10));
725
726 assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
728 }
729
730 #[rstest]
731 fn test_test_timer_fire_immediately_false() {
732 let mut timer = TestTimer::new(
733 Ustr::from("TEST_TIMER"),
734 NonZeroU64::new(5).unwrap(),
735 UnixNanos::from(10),
736 None,
737 false, );
739
740 assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
742
743 assert_eq!(timer.advance(UnixNanos::from(10)).count(), 0);
745
746 let events: Vec<TimeEvent> = timer.advance(UnixNanos::from(15)).collect();
748 assert_eq!(events.len(), 1);
749 assert_eq!(events[0].ts_event, UnixNanos::from(15));
750 }
751
752 #[rstest]
753 fn test_time_event_handler_ordering_uses_tie_breakers() {
754 let callback = TimeEventCallback::from(|_: TimeEvent| {});
755
756 let later_name = TimeEventHandler::new(
757 TimeEvent::new(
758 Ustr::from("TIME_BAR_ESM4-2-MINUTE-ASK-INTERNAL"),
759 UUID4::from("00000000-0000-4000-8000-000000000003"),
760 100.into(),
761 100.into(),
762 ),
763 callback.clone(),
764 );
765 let earlier_name = TimeEventHandler::new(
766 TimeEvent::new(
767 Ustr::from("SPREAD_QUOTE_ESM4"),
768 UUID4::from("00000000-0000-4000-8000-000000000002"),
769 100.into(),
770 100.into(),
771 ),
772 callback.clone(),
773 );
774 let later_init = TimeEventHandler::new(
775 TimeEvent::new(
776 Ustr::from("SPREAD_QUOTE_ESM4"),
777 UUID4::from("00000000-0000-4000-8000-000000000004"),
778 100.into(),
779 101.into(),
780 ),
781 callback.clone(),
782 );
783 let later_id = TimeEventHandler::new(
784 TimeEvent::new(
785 Ustr::from("SPREAD_QUOTE_ESM4"),
786 UUID4::from("00000000-0000-4000-8000-000000000005"),
787 100.into(),
788 100.into(),
789 ),
790 callback,
791 );
792
793 assert!(earlier_name < later_name);
794 assert!(earlier_name < later_init);
795 assert!(earlier_name < later_id);
796 assert_ne!(earlier_name, later_id);
797 }
798
799 #[rstest]
800 fn test_scheduled_time_event_ordering_laws() {
801 let base = ScheduledTimeEvent::new(TimeEvent::new(
802 Ustr::from("ALPHA"),
803 UUID4::from("00000000-0000-4000-8000-000000000001"),
804 100.into(),
805 10.into(),
806 ));
807 let variants = [
808 base.clone(),
809 ScheduledTimeEvent::new(TimeEvent::new(
810 Ustr::from("BETA"),
811 base.0.event_id,
812 base.0.ts_event,
813 base.0.ts_init,
814 )),
815 ScheduledTimeEvent::new(TimeEvent::new(
816 base.0.name,
817 UUID4::from("00000000-0000-4000-8000-000000000002"),
818 base.0.ts_event,
819 base.0.ts_init,
820 )),
821 ScheduledTimeEvent::new(TimeEvent::new(
822 base.0.name,
823 base.0.event_id,
824 101.into(),
825 base.0.ts_init,
826 )),
827 ScheduledTimeEvent::new(TimeEvent::new(
828 base.0.name,
829 base.0.event_id,
830 base.0.ts_event,
831 11.into(),
832 )),
833 ];
834
835 for a in &variants {
836 for b in &variants {
837 assert_eq!(a == b, a.cmp(b).is_eq());
838 assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
839 assert_eq!(a.cmp(b), b.cmp(a).reverse());
840 }
841 }
842 }
843
844 #[rstest]
845 fn test_scheduled_time_event_heap_ordering() {
846 let expected = [
847 TimeEvent::new(
848 Ustr::from("ALPHA"),
849 UUID4::from("00000000-0000-4000-8000-000000000001"),
850 100.into(),
851 10.into(),
852 ),
853 TimeEvent::new(
854 Ustr::from("ALPHA"),
855 UUID4::from("00000000-0000-4000-8000-000000000002"),
856 100.into(),
857 10.into(),
858 ),
859 TimeEvent::new(
860 Ustr::from("ALPHA"),
861 UUID4::from("00000000-0000-4000-8000-000000000003"),
862 100.into(),
863 11.into(),
864 ),
865 TimeEvent::new(
866 Ustr::from("BETA"),
867 UUID4::from("00000000-0000-4000-8000-000000000004"),
868 100.into(),
869 10.into(),
870 ),
871 TimeEvent::new(
872 Ustr::from("ALPHA"),
873 UUID4::from("00000000-0000-4000-8000-000000000005"),
874 101.into(),
875 10.into(),
876 ),
877 ];
878 let insertion_order = [4, 1, 3, 0, 2];
879 let mut heap = BinaryHeap::new();
880
881 for index in insertion_order {
882 heap.push(ScheduledTimeEvent::new(expected[index].clone()));
883 }
884
885 let popped = std::iter::from_fn(|| heap.pop().map(ScheduledTimeEvent::into_inner))
886 .collect::<Vec<_>>();
887 assert_eq!(popped, expected);
888 }
889
890 #[cfg(feature = "python")]
891 #[rstest]
892 fn test_python_callback_modes_pass_expected_argument_types() {
893 Python::initialize();
894
895 Python::attach(|py| {
896 let seen = PyList::empty(py);
897 let seen_obj = seen.clone().unbind().into_any();
898 let callback = new_sync_py_callback(
899 py,
900 move |args: &Bound<'_, PyTuple>,
901 _kwargs: Option<&Bound<'_, PyDict>>|
902 -> PyResult<()> {
903 let arg = args.get_item(0)?;
904 let type_name = arg.get_type().name()?.to_string();
905 Python::attach(|py| seen_obj.call_method1(py, "append", (type_name,)))?;
906 Ok(())
907 },
908 )
909 .expect("callback should create")
910 .into_any()
911 .unbind();
912
913 let event = TimeEvent::new(
914 Ustr::from("PY_CALLBACK_MODE"),
915 UUID4::from("00000000-0000-4000-8000-000000000007"),
916 UnixNanos::from(100),
917 UnixNanos::from(99),
918 );
919
920 TimeEventCallback::from_python_time_event(callback.clone_ref(py)).call(event.clone());
921 TimeEventCallback::from_python_legacy_capsule(callback).call(event);
922
923 assert_eq!(
924 seen.get_item(0).unwrap().extract::<String>().unwrap(),
925 "TimeEvent"
926 );
927 assert_eq!(
928 seen.get_item(1).unwrap().extract::<String>().unwrap(),
929 "PyCapsule"
930 );
931 });
932 }
933
934 #[cfg(feature = "python")]
935 fn new_sync_py_callback<F>(py: Python<'_>, closure: F) -> PyResult<Bound<'_, PyCFunction>>
936 where
937 F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> PyResult<()>
938 + Send
939 + Sync
940 + 'static,
941 {
942 PyCFunction::new_closure(py, None, None, closure)
943 }
944
945 #[derive(Default)]
946 struct RecordingTimeEventTap {
947 time_events: RefCell<Vec<(String, TimeEvent)>>,
948 }
949
950 impl RecordingTimeEventTap {
951 fn time_events(&self) -> Vec<(String, TimeEvent)> {
952 self.time_events.borrow().clone()
953 }
954 }
955
956 impl BusTap for RecordingTimeEventTap {
957 fn on_publish(&self, topic: MStr<Topic>, message: &dyn std::any::Any) {
958 if let Some(event) = message.downcast_ref::<TimeEvent>() {
959 self.time_events
960 .borrow_mut()
961 .push((topic.to_string(), event.clone()));
962 }
963 }
964
965 fn on_send(&self, _endpoint: MStr<Endpoint>, _message: &dyn std::any::Any) {}
966 }
967
968 #[rstest]
969 fn test_time_event_handler_run_dispatches_tap_before_callback() {
970 let event = TimeEvent::new(
971 Ustr::from("strategy.heartbeat"),
972 UUID4::from("00000000-0000-4000-8000-000000000006"),
973 UnixNanos::from(100),
974 UnixNanos::from(99),
975 );
976 let tap = Rc::new(RecordingTimeEventTap::default());
977 let callback_seen: Rc<RefCell<Vec<TimeEvent>>> = Rc::new(RefCell::new(Vec::new()));
978 let expected_topic = MessagingSwitchboard::time_event_topic().to_string();
979 let callback_expected = event.clone();
980 let callback_expected_topic = expected_topic.clone();
981 let callback_tap = Rc::clone(&tap);
982 let callback_seen_ref = Rc::clone(&callback_seen);
983 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |callback_event| {
984 assert_eq!(
985 callback_tap.time_events(),
986 vec![(callback_expected_topic.clone(), callback_expected.clone())],
987 );
988 callback_seen_ref.borrow_mut().push(callback_event);
989 });
990
991 set_bus_tap(tap.clone());
992 TimeEventHandler::new(event.clone(), TimeEventCallback::from(callback)).run();
993 clear_bus_tap();
994
995 assert_eq!(tap.time_events(), vec![(expected_topic, event.clone())]);
996 assert_eq!(*callback_seen.borrow(), vec![event]);
997 }
998
999 use proptest::{prelude::*, test_runner::TestCaseResult};
1000
1001 #[derive(Clone, Debug)]
1002 enum TimerOperation {
1003 AdvanceTime(u64),
1004 Cancel,
1005 }
1006
1007 fn timer_operation_strategy() -> impl Strategy<Value = TimerOperation> {
1008 prop_oneof![
1009 8 => (0u64..=1000).prop_map(TimerOperation::AdvanceTime),
1010 2 => Just(TimerOperation::Cancel),
1011 ]
1012 }
1013
1014 fn timer_config_strategy() -> impl Strategy<Value = (u64, u64, Option<u64>, bool)> {
1015 (
1016 1u64..=1000,
1017 timer_start_time_strategy(),
1018 prop::option::of(0u64..=20_000),
1019 prop::bool::ANY,
1020 )
1021 .prop_map(
1022 |(interval_ns, start_time_ns, stop_after_ns, fire_immediately)| {
1023 (
1024 interval_ns,
1025 start_time_ns,
1026 stop_after_ns.map(|offset| start_time_ns + offset),
1027 fire_immediately,
1028 )
1029 },
1030 )
1031 }
1032
1033 fn timer_start_time_strategy() -> impl Strategy<Value = u64> {
1034 prop_oneof![
1035 6 => 0u64..=u64::MAX - TIMER_TIME_HEADROOM,
1036 2 => 0u64..=1_000_000,
1037 1 => Just(1_700_000_000_000_000_000),
1038 1 => Just(u64::MAX - TIMER_TIME_HEADROOM),
1039 ]
1040 }
1041
1042 fn timer_test_strategy()
1043 -> impl Strategy<Value = (Vec<TimerOperation>, (u64, u64, Option<u64>, bool))> {
1044 (
1045 prop::collection::vec(timer_operation_strategy(), 5..=75),
1046 timer_config_strategy(),
1047 )
1048 }
1049
1050 fn test_timer_with_operations(
1051 operations: Vec<TimerOperation>,
1052 (interval_ns, start_time_ns, stop_time_ns, fire_immediately): (u64, u64, Option<u64>, bool),
1053 ) -> TestCaseResult {
1054 let mut timer = TestTimer::new(
1055 Ustr::from("PROP_TEST_TIMER"),
1056 NonZeroU64::new(interval_ns).unwrap(),
1057 UnixNanos::from(start_time_ns),
1058 stop_time_ns.map(UnixNanos::from),
1059 fire_immediately,
1060 );
1061
1062 let mut current_time = start_time_ns;
1063 let mut expected_next = if fire_immediately {
1064 start_time_ns
1065 } else {
1066 start_time_ns + interval_ns
1067 };
1068 let mut expected_expired = false;
1069
1070 for operation in operations {
1071 match operation {
1072 TimerOperation::AdvanceTime(delta) => {
1073 let to_time = current_time + delta;
1074 let actual: Vec<(Ustr, u64, u64)> = timer
1075 .advance(UnixNanos::from(to_time))
1076 .map(|event| time_event_state(&event))
1077 .collect();
1078 let expected = expected_event_states(
1079 expected_event_times(
1080 to_time,
1081 interval_ns,
1082 stop_time_ns,
1083 &mut expected_next,
1084 &mut expected_expired,
1085 ),
1086 Ustr::from("PROP_TEST_TIMER"),
1087 );
1088 current_time = to_time;
1089
1090 prop_assert_eq!(actual, expected);
1091 }
1092 TimerOperation::Cancel => {
1093 timer.cancel();
1094 expected_expired = true;
1095 }
1096 }
1097
1098 prop_assert_eq!(timer.is_expired(), expected_expired);
1099 prop_assert_eq!(timer.next_time_ns().as_u64(), expected_next);
1100 }
1101
1102 if !expected_expired && let Some(stop_time_ns) = stop_time_ns {
1103 let to_time = stop_time_ns.saturating_add(interval_ns);
1104 let actual: Vec<(Ustr, u64, u64)> = timer
1105 .advance(UnixNanos::from(to_time))
1106 .map(|event| time_event_state(&event))
1107 .collect();
1108 let expected = expected_event_states(
1109 expected_event_times(
1110 to_time,
1111 interval_ns,
1112 Some(stop_time_ns),
1113 &mut expected_next,
1114 &mut expected_expired,
1115 ),
1116 Ustr::from("PROP_TEST_TIMER"),
1117 );
1118 prop_assert_eq!(actual, expected);
1119 prop_assert!(expected_expired);
1120 prop_assert!(timer.is_expired());
1121 prop_assert_eq!(timer.next_time_ns().as_u64(), expected_next);
1122 }
1123
1124 Ok(())
1125 }
1126
1127 fn expected_event_times(
1128 to_time: u64,
1129 interval_ns: u64,
1130 stop_time_ns: Option<u64>,
1131 next_time: &mut u64,
1132 is_expired: &mut bool,
1133 ) -> Vec<u64> {
1134 let mut events = Vec::new();
1135
1136 while !*is_expired && *next_time <= to_time {
1137 if let Some(stop_time_ns) = stop_time_ns
1138 && *next_time > stop_time_ns
1139 {
1140 *is_expired = true;
1141 break;
1142 }
1143
1144 let event_time = *next_time;
1145 events.push(event_time);
1146 let Some(following_time) = event_time.checked_add(interval_ns) else {
1147 *is_expired = true;
1148 break;
1149 };
1150 *next_time = following_time;
1151
1152 if Some(event_time) == stop_time_ns {
1153 *is_expired = true;
1154 break;
1155 }
1156 }
1157
1158 events
1159 }
1160
1161 proptest! {
1162 #[rstest]
1163 fn prop_timer_advance_operations((operations, config) in timer_test_strategy()) {
1164 test_timer_with_operations(operations, config)?;
1165 }
1166
1167 #[rstest]
1168 fn prop_timer_advance_batching_is_consistent(
1169 interval_ns in 1u64..=1000,
1170 start_time_ns in timer_start_time_strategy(),
1171 fire_immediately in prop::bool::ANY,
1172 advance_count in 1u64..=20,
1173 ) {
1174 let mut timer = TestTimer::new(
1175 Ustr::from("CONSISTENCY_TEST"),
1176 NonZeroU64::new(interval_ns).unwrap(),
1177 UnixNanos::from(start_time_ns),
1178 None, fire_immediately,
1180 );
1181
1182 let first_event_time = if fire_immediately { start_time_ns } else { start_time_ns + interval_ns };
1183 let final_event_time = first_event_time + interval_ns * (advance_count - 1);
1184 let expected = expected_event_states(
1185 (0..advance_count)
1186 .map(|index| first_event_time + interval_ns * index)
1187 .collect(),
1188 Ustr::from("CONSISTENCY_TEST"),
1189 );
1190
1191 let mut batched_timer = timer.clone();
1192 let batched: Vec<(Ustr, u64, u64)> = batched_timer
1193 .advance(UnixNanos::from(final_event_time))
1194 .map(|event| time_event_state(&event))
1195 .collect();
1196
1197 let mut stepped = Vec::new();
1198
1199 for event_time in
1200 (0..advance_count).map(|index| first_event_time + interval_ns * index)
1201 {
1202 stepped.extend(
1203 timer
1204 .advance(UnixNanos::from(event_time))
1205 .map(|event| time_event_state(&event)),
1206 );
1207 }
1208
1209 prop_assert_eq!(&batched, &expected);
1210 prop_assert_eq!(&stepped, &expected);
1211 prop_assert_eq!(timer.next_time_ns(), batched_timer.next_time_ns());
1212 prop_assert_eq!(timer.is_expired(), batched_timer.is_expired());
1213 }
1214
1215 #[rstest]
1216 fn prop_timer_terminal_time_does_not_require_following_time(
1217 (interval_ns, event_headroom) in terminal_time_strategy(),
1218 fire_immediately in prop::bool::ANY,
1219 bounded in prop::bool::ANY,
1220 ) {
1221 let event_time_ns = u64::MAX - event_headroom;
1222 let start_time_ns = if fire_immediately {
1223 event_time_ns
1224 } else {
1225 event_time_ns - interval_ns
1226 };
1227 let mut timer = TestTimer::new(
1228 Ustr::from("TERMINAL_STOP_TEST"),
1229 NonZeroU64::new(interval_ns).unwrap(),
1230 UnixNanos::from(start_time_ns),
1231 bounded.then_some(UnixNanos::max()),
1232 fire_immediately,
1233 );
1234
1235 let events: Vec<(Ustr, u64, u64)> = timer
1236 .advance(UnixNanos::max())
1237 .map(|event| time_event_state(&event))
1238 .collect();
1239
1240 prop_assert_eq!(
1241 events,
1242 vec![(Ustr::from("TERMINAL_STOP_TEST"), event_time_ns, event_time_ns)]
1243 );
1244 prop_assert!(timer.is_expired());
1245 prop_assert_eq!(timer.next_time_ns(), UnixNanos::from(event_time_ns));
1246 }
1247 }
1248
1249 const TIMER_TIME_HEADROOM: u64 = 100_000;
1250
1251 fn time_event_state(event: &TimeEvent) -> (Ustr, u64, u64) {
1252 (event.name, event.ts_event.as_u64(), event.ts_init.as_u64())
1253 }
1254
1255 fn expected_event_states(times: Vec<u64>, name: Ustr) -> Vec<(Ustr, u64, u64)> {
1256 times.into_iter().map(|time| (name, time, time)).collect()
1257 }
1258
1259 fn terminal_time_strategy() -> impl Strategy<Value = (u64, u64)> {
1260 (1u64..=1000).prop_flat_map(|interval_ns| (Just(interval_ns), 0u64..interval_ns))
1261 }
1262}