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