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