Skip to main content

nautilus_common/
timer.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Real-time and test timers for use with `Clock` implementations.
17
18use 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/// Creates a valid nanoseconds interval that is guaranteed to be positive.
35///
36/// Coerces zero to one to ensure a valid `NonZeroU64`.
37#[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)]
52/// Represents a time event occurring at the event timestamp.
53///
54/// A `TimeEvent` carries metadata such as the event's name, a unique event ID,
55/// and timestamps indicating when the event was scheduled to occur and when it was initialized.
56pub struct TimeEvent {
57    /// The event name, identifying the nature or purpose of the event.
58    pub name: Ustr,
59    /// The unique identifier for the event.
60    pub event_id: UUID4,
61    /// UNIX timestamp (nanoseconds) when the event occurred.
62    pub ts_event: UnixNanos,
63    /// UNIX timestamp (nanoseconds) when the instance was created.
64    pub ts_init: UnixNanos,
65}
66
67impl TimeEvent {
68    /// Creates a new [`TimeEvent`] instance.
69    #[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/// Wrapper for [`TimeEvent`] that implements ordering by timestamp for heap scheduling.
95///
96/// This newtype allows time events to be ordered in a priority queue (max heap) by their
97/// timestamp while keeping [`TimeEvent`] itself clean with standard field-based equality.
98/// Events are ordered in reverse (earlier timestamps have higher priority).
99#[repr(transparent)] // Guarantees zero-cost abstraction with identical memory layout
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct ScheduledTimeEvent(pub TimeEvent);
102
103impl ScheduledTimeEvent {
104    /// Creates a new scheduled time event.
105    #[must_use]
106    pub const fn new(event: TimeEvent) -> Self {
107        Self(event)
108    }
109
110    /// Extracts the inner time event.
111    #[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        // Reverse order for max heap: earlier timestamps have higher priority
126        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")]
137/// Python callback for time events.
138pub struct PythonTimeEventCallback {
139    callback: Py<PyAny>,
140}
141
142#[cfg(feature = "python")]
143impl PythonTimeEventCallback {
144    /// Creates a new [`PythonTimeEventCallback`] instance.
145    #[must_use]
146    pub const fn new(callback: Py<PyAny>) -> Self {
147        Self { callback }
148    }
149
150    /// Invokes the Python callback for the given `TimeEvent`.
151    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
168/// Callback type for time events.
169///
170/// # Variants
171///
172/// - `Python`: For Python callbacks (requires `python` feature).
173/// - `Rust`: Thread-safe callbacks using `Arc`. Use when the closure is `Send + Sync`.
174/// - `RustLocal`: Single-threaded callbacks using `Rc`. Use when capturing `Rc<RefCell<...>>`.
175///
176/// # Choosing Between `Rust` and `RustLocal`
177///
178/// Use `Rust` (thread-safe) when:
179/// - The callback doesn't capture `Rc<RefCell<...>>` or other non-`Send` types.
180/// - The closure is `Send + Sync` (most simple closures qualify).
181///
182/// Use `RustLocal` when:
183/// - The callback captures `Rc<RefCell<...>>` for shared mutable state.
184/// - Thread safety constraints prevent using `Arc`.
185///
186/// `RustLocal` works with `TestClock` and with `LiveClock` when its event channel
187/// is drained on the callback's originating thread.
188///
189/// # Automatic Conversion
190///
191/// - Closures that are `Fn + Send + Sync + 'static` automatically convert to `Rust`.
192/// - `Rc<dyn Fn(TimeEvent)>` converts to `RustLocal`.
193/// - `Arc<dyn Fn(TimeEvent) + Send + Sync>` converts to `Rust`.
194pub enum TimeEventCallback {
195    /// Python callable for use from Python via PyO3.
196    #[cfg(feature = "python")]
197    Python(Arc<PythonTimeEventCallback>),
198    /// Thread-safe Rust callback using `Arc` (`Send + Sync`).
199    Rust(Arc<dyn Fn(TimeEvent) + Send + Sync>),
200    /// Local Rust callback using `Rc` (not `Send`/`Sync`).
201    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    /// Returns `true` if this is a local (non-thread-safe) Rust callback.
228    ///
229    /// Local callbacks use `Rc` internally and require creation, cloning, dropping,
230    /// and invocation to stay on the originating thread.
231    #[must_use]
232    pub const fn is_local(&self) -> bool {
233        matches!(self, Self::RustLocal(_))
234    }
235
236    /// Invokes the callback for the given `TimeEvent`.
237    ///
238    /// For Python callbacks, exceptions are logged as errors rather than panicking.
239    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    /// Creates a Python callback that receives a PyO3 `TimeEvent`.
280    #[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)]
288/// Represents a time event and its associated handler.
289///
290/// `TimeEventHandler` associates a `TimeEvent` with a callback function that is triggered
291/// when the event's timestamp is reached.
292pub struct TimeEventHandler {
293    /// The time event.
294    pub event: TimeEvent,
295    /// The callable handler for the event.
296    pub callback: TimeEventCallback,
297}
298
299impl TimeEventHandler {
300    /// Creates a new [`TimeEventHandler`] instance.
301    #[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    /// Executes the handler by invoking its callback for the associated event.
321    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/// A test timer for user with a `TestClock`.
354///
355/// `TestTimer` simulates time progression in a controlled environment,
356/// allowing for precise control over event generation in test scenarios.
357///
358/// # Threading
359///
360/// The timer mutates its internal state and should only be used from its owning thread.
361#[derive(Clone, Debug)]
362pub struct TestTimer {
363    /// The name of the timer.
364    pub name: Ustr,
365    /// The interval between timer events in nanoseconds.
366    pub interval_ns: NonZeroU64,
367    /// The start time of the timer in UNIX nanoseconds.
368    pub start_time_ns: UnixNanos,
369    /// The optional stop time of the timer in UNIX nanoseconds.
370    pub stop_time_ns: Option<UnixNanos>,
371    /// If the timer should fire immediately at start time.
372    pub fire_immediately: bool,
373    next_time_ns: UnixNanos,
374    is_expired: bool,
375}
376
377impl TestTimer {
378    /// Creates a new [`TestTimer`] instance.
379    ///
380    /// # Panics
381    ///
382    /// Panics if `name` is not a valid string.
383    #[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    /// Returns the next time in UNIX nanoseconds when the timer will fire.
411    #[must_use]
412    pub const fn next_time_ns(&self) -> UnixNanos {
413        self.next_time_ns
414    }
415
416    /// Returns whether the timer is expired.
417    #[must_use]
418    pub const fn is_expired(&self) -> bool {
419        self.is_expired
420    }
421
422    /// Advance the test timer forward to the given time, generating a sequence
423    /// of events. A [`TimeEvent`] is appended for each time a next event is
424    /// <= the given `to_time_ns`.
425    ///
426    /// This allows testing of multiple time intervals within a single step.
427    pub fn advance(&mut self, to_time_ns: UnixNanos) -> impl Iterator<Item = TimeEvent> + '_ {
428        // Calculate how many events should fire up to and including to_time_ns
429        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    /// Cancels the timer (the timer will not generate an event).
439    ///
440    /// Used to stop the timer before its scheduled stop time.
441    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            // Check if current event would exceed stop time before creating the event
464            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, // fire_immediately = true
628        );
629
630        // With fire_immediately=true, next_time_ns should be start_time_ns
631        assert_eq!(timer.next_time_ns(), UnixNanos::from(10));
632
633        // Advance to start time should produce an event
634        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        // Next event should be at start_time + interval
639        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, // fire_immediately = false
650        );
651
652        // With fire_immediately=false, next_time_ns should be start_time_ns + interval
653        assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
654
655        // Advance to start time should produce no events
656        assert_eq!(timer.advance(UnixNanos::from(10)).count(), 0);
657
658        // Advance to first interval should produce an event
659        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, // No stop time for this test
1087                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}