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