Skip to main content

nautilus_common/live/
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//! Live timer scheduling and callback dispatch.
17//!
18//! # Scheduling
19//!
20//! Runtime deadlines use the remaining duration to the nominal schedule and are computed before
21//! the timer task is spawned. A deadline already in the past starts immediately. Event timestamps
22//! retain the nominal schedule, and stop times are inclusive. An event whose following schedule is
23//! not representable in [`UnixNanos`] is the timer's final event.
24//!
25//! # Task lifecycle
26//!
27//! Each [`LiveTimer::start`] creates fresh public schedule and task-state atomics. Because aborting
28//! a runtime task does not join it, task retirement linearizes restart, cancellation, and drop
29//! against event reservation. Retirement either prevents the old task from dispatching, or observes
30//! what that task reserved with its event: the following schedule, or terminal exhaustion when no
31//! successor is representable. Fresh atomics keep that task from overwriting its replacement's
32//! schedule.
33//!
34//! # Callback dispatch
35//!
36//! Thread-safe callbacks cross the worker boundary as [`TimeEventMessage`] values. `RustLocal`
37//! callbacks remain in an owner-thread registry and cross the boundary only through tokens and
38//! leases. Senderless Python callbacks run inline and publish their following schedule, when one
39//! exists, after the callback returns.
40
41use std::{
42    num::NonZeroU64,
43    sync::{
44        Arc,
45        atomic::{self, AtomicU8, AtomicU64},
46    },
47};
48
49use nautilus_core::{
50    DurationNanos, UUID4, UnixNanos,
51    correctness::{FAILED, check_valid_string_utf8},
52    datetime::floor_to_nearest_microsecond,
53    time::get_atomic_clock_realtime,
54};
55use ustr::Ustr;
56
57use super::dst::{
58    self,
59    task::JoinHandle,
60    time::{Duration, Instant},
61};
62#[cfg(not(all(feature = "simulation", madsim)))]
63use super::runtime::get_runtime;
64use crate::{
65    runner::{
66        TimeEventCallbackLease, TimeEventCallbackToken, TimeEventMessage, TimeEventMessageFactory,
67        TimeEventSender, register_time_event_callback,
68    },
69    timer::{TimeEvent, TimeEventCallback, Timer},
70};
71
72const TASK_ACTIVE: u8 = 0;
73const TASK_FIRING: u8 = 1;
74const TASK_RETIRED: u8 = 2;
75const TASK_EXHAUSTED: u8 = 3;
76
77/// A live timer for use with a `LiveClock`.
78///
79/// `LiveTimer` triggers events at specified intervals in a real-time environment,
80/// using Tokio's async runtime to handle scheduling and execution.
81///
82/// # Threading
83///
84/// The timer runs on the runtime thread that created it and dispatches events across threads as needed.
85#[derive(Debug)]
86pub struct LiveTimer {
87    /// The name of the timer.
88    pub name: Ustr,
89    /// The interval between timer events in nanoseconds.
90    pub interval_ns: NonZeroU64,
91    /// The start time of the timer in UNIX nanoseconds.
92    pub start_time_ns: UnixNanos,
93    /// The optional stop time of the timer in UNIX nanoseconds.
94    pub stop_time_ns: Option<UnixNanos>,
95    /// If the timer should fire immediately at start time.
96    pub fire_immediately: bool,
97    next_time_ns: Arc<AtomicU64>,
98    callback: OwnerCallback,
99    task_handle: Option<JoinHandle<()>>,
100    task_state: Option<Arc<TimerTaskState>>,
101    exhausted: bool,
102    canceled: bool,
103    sender: Option<Arc<dyn TimeEventSender>>,
104}
105
106impl LiveTimer {
107    /// Creates a new [`LiveTimer`] instance.
108    ///
109    /// # Panics
110    ///
111    /// Panics if:
112    /// - `name` is not a valid string.
113    /// - `fire_immediately` is false and `start_time_ns + interval_ns` overflows `UnixNanos`.
114    #[must_use]
115    pub fn new(
116        name: Ustr,
117        interval_ns: NonZeroU64,
118        start_time_ns: UnixNanos,
119        stop_time_ns: Option<UnixNanos>,
120        callback: TimeEventCallback,
121        fire_immediately: bool,
122        sender: Option<Arc<dyn TimeEventSender>>,
123    ) -> Self {
124        check_valid_string_utf8(name, stringify!(name)).expect(FAILED);
125
126        let next_time_ns = if fire_immediately {
127            start_time_ns.as_u64()
128        } else {
129            (start_time_ns + DurationNanos::new(interval_ns.get())).as_u64()
130        };
131
132        log::trace!("Creating timer '{name}'");
133
134        let owner_callback = if sender.is_some() {
135            if callback.is_local() {
136                OwnerCallback::Registered {
137                    token: register_time_event_callback(callback.clone()),
138                    callback,
139                }
140            } else {
141                OwnerCallback::Direct(TimeEventMessageFactory::new(&callback))
142            }
143        } else {
144            OwnerCallback::Senderless(callback)
145        };
146
147        Self {
148            name,
149            interval_ns,
150            start_time_ns,
151            stop_time_ns,
152            fire_immediately,
153            next_time_ns: Arc::new(AtomicU64::new(next_time_ns)),
154            callback: owner_callback,
155            task_handle: None,
156            task_state: None,
157            exhausted: false,
158            canceled: false,
159            sender,
160        }
161    }
162
163    /// Returns the next time in UNIX nanoseconds when the timer will fire.
164    ///
165    /// Provides the scheduled time for the next event based on the current state of the timer.
166    /// An exhausted timer retains the timestamp of its final event.
167    #[must_use]
168    pub fn next_time_ns(&self) -> UnixNanos {
169        UnixNanos::from(self.next_time_ns.load(atomic::Ordering::SeqCst))
170    }
171
172    /// Returns whether the timer is expired.
173    ///
174    /// An expired timer will not trigger any further events.
175    /// A timer that has not been started is not expired.
176    #[must_use]
177    pub fn is_expired(&self) -> bool {
178        self.canceled
179            || self
180                .task_handle
181                .as_ref()
182                .is_some_and(JoinHandle::is_finished)
183    }
184
185    /// Starts the timer.
186    ///
187    /// Time events will begin triggering at the specified intervals.
188    /// The generated events are handled by the provided callback function.
189    ///
190    /// Starting a timer whose task is still active aborts that task first
191    /// (restart semantics); a previously fired event that is already queued
192    /// still dispatches.
193    ///
194    /// An event whose following schedule would overflow [`UnixNanos`] is the timer's final event.
195    /// The timer then remains expired, and further calls return without starting a task.
196    ///
197    /// # Panics
198    ///
199    /// Panics if using a Rust callback (`Rust` or `RustLocal`) without a `TimeEventSender`.
200    #[allow(unused_variables)]
201    pub fn start(&mut self) {
202        if let OwnerCallback::Senderless(callback) = &self.callback {
203            match callback {
204                #[cfg(feature = "python")]
205                TimeEventCallback::Python(_) => {}
206                TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_) => {
207                    panic!("timer event sender was unset for Rust callback system");
208                }
209            }
210        }
211
212        let event_name = self.name;
213        let stop_time_ns = self.stop_time_ns;
214        let interval_ns = DurationNanos::new(self.interval_ns.get());
215
216        let retired_task = self.retire_task();
217
218        if self.exhausted {
219            return;
220        }
221
222        let mut observed_next = retired_task.map_or_else(
223            || self.next_time_ns.load(atomic::Ordering::SeqCst),
224            |retirement| retirement.next_time_ns,
225        );
226
227        // Close the old token before registering its replacement;
228        // any lease acquired before retirement remains valid.
229        if let Some(handle) = self.task_handle.take() {
230            self.close_registered_callback();
231            handle.abort();
232        }
233
234        let worker_dispatch = match &mut self.callback {
235            OwnerCallback::Registered { token, callback } => {
236                // A cancel or a final stop-time fire closed the token; a
237                // restart needs a fresh registration or acquire() would
238                // return None on every fire.
239                if token.is_closed() {
240                    *token = register_time_event_callback(callback.clone());
241                }
242                WorkerDispatch::Registered(token.clone())
243            }
244            OwnerCallback::Direct(factory) => WorkerDispatch::Direct(factory.clone()),
245            OwnerCallback::Senderless(callback) => match callback {
246                #[cfg(feature = "python")]
247                TimeEventCallback::Python(callback) => {
248                    WorkerDispatch::SenderlessPython(callback.clone())
249                }
250                TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_) => {
251                    unreachable!("senderless Rust callback rejected at start")
252                }
253            },
254        };
255
256        // Get current time
257        let clock = get_atomic_clock_realtime();
258        let now_ns = clock.get_time_ns();
259
260        // Check if the timer's alert time is in the past and adjust if needed
261        let now_raw = now_ns.as_u64();
262
263        if should_adjust_past_due_time(observed_next, now_ns, stop_time_ns) {
264            if observed_next < now_raw {
265                let original = UnixNanos::from(observed_next);
266                log::warn!(
267                    "Timer '{event_name}' alert time {} was in the past, adjusted to current time for immediate fire",
268                    original.to_rfc3339(),
269                );
270            }
271
272            observed_next = now_raw;
273        }
274
275        // Floor the next time to the nearest microsecond which is within the timers accuracy
276        let mut next_time_ns = normalize_start_time_ns(observed_next, now_ns, stop_time_ns);
277        let next_time_atomic = Arc::new(AtomicU64::new(next_time_ns.as_u64()));
278        let task_state = Arc::new(TimerTaskState::new(next_time_ns.as_u64()));
279        self.next_time_ns = next_time_atomic.clone();
280        self.task_state = Some(task_state.clone());
281
282        let sender = self.sender.clone();
283        let now_ns = clock.get_time_ns();
284        let start = Instant::now() + timer_start_delay(next_time_ns, now_ns);
285
286        let task = async move {
287            let clock = get_atomic_clock_realtime();
288
289            let mut timer = dst::time::interval_at(start, Duration::from(interval_ns));
290
291            loop {
292                // Never fire an event scheduled past the stop time. The event's
293                // `ts_event` is the scheduled `next_time_ns`, so the bound is
294                // enforced on the scheduled time (matching `VirtualTimer`), not on
295                // the wall-clock read used only for `ts_init`.
296                if !should_fire_scheduled_time(next_time_ns, stop_time_ns) {
297                    if let (Some(sender), WorkerDispatch::Registered(token)) =
298                        (sender.as_ref(), &worker_dispatch)
299                        && let Some(lease) = token.acquire()
300                    {
301                        token.close();
302                        let now_ns = clock.get_time_ns();
303                        let event = TimeEvent::new(event_name, UUID4::new(), next_time_ns, now_ns);
304                        sender.send(TimeEventMessage::cleanup(event, lease));
305                    }
306                    break; // Timer expired before this event
307                }
308
309                // `timer.tick` is cancellation safe, if the cancel branch completes
310                // first then no tick has been consumed (no event was ready).
311                timer.tick().await;
312                let now_ns = clock.get_time_ns();
313
314                let event = TimeEvent::new(event_name, UUID4::new(), next_time_ns, now_ns);
315
316                // An event at the inclusive stop boundary or without a representable successor
317                // is terminal.
318                let following_next_time_ns = next_time_ns.checked_add(interval_ns);
319                let expires_after_fire = expires_after_scheduled_time(next_time_ns, stop_time_ns)
320                    || following_next_time_ns.is_none();
321
322                // Reserve this fire with its following schedule or terminal exhaustion. A restart
323                // observes that outcome or retires the task before it can dispatch. Registered
324                // callbacks acquire their lease first so token closure cannot suppress a reserved
325                // event.
326                let registered_lease = if let WorkerDispatch::Registered(token) = &worker_dispatch {
327                    match task_state.reserve_registered_fire(
328                        token,
329                        following_next_time_ns.map(|time| time.as_u64()),
330                    ) {
331                        Some(lease) => Some(lease),
332                        None => break,
333                    }
334                } else {
335                    if !task_state.reserve_fire(following_next_time_ns.map(|time| time.as_u64())) {
336                        break;
337                    }
338                    None
339                };
340
341                if sender.is_some()
342                    && let Some(following_next_time_ns) = following_next_time_ns
343                {
344                    next_time_atomic
345                        .store(following_next_time_ns.as_u64(), atomic::Ordering::SeqCst);
346                }
347
348                match (&sender, &worker_dispatch) {
349                    (Some(sender), WorkerDispatch::Direct(factory)) => {
350                        sender.send(factory.message(event));
351                    }
352                    (Some(sender), WorkerDispatch::Registered(token)) => {
353                        let lease =
354                            registered_lease.expect("registered callback lease was not acquired");
355
356                        if expires_after_fire {
357                            token.close();
358                        }
359                        sender.send(TimeEventMessage::registered(event, lease));
360                    }
361                    #[cfg(feature = "python")]
362                    (None, WorkerDispatch::SenderlessPython(callback)) => callback.call(event),
363                    _ => unreachable!("timer callback dispatch did not match its sender"),
364                }
365
366                if sender.is_none()
367                    && let Some(following_next_time_ns) = following_next_time_ns
368                {
369                    next_time_atomic
370                        .store(following_next_time_ns.as_u64(), atomic::Ordering::SeqCst);
371                }
372
373                if let Some(following_next_time_ns) = following_next_time_ns {
374                    next_time_ns = following_next_time_ns;
375                }
376
377                if expires_after_fire {
378                    break; // Stop boundary reached, or no representable successor
379                }
380            }
381        };
382
383        #[cfg(all(feature = "simulation", madsim))]
384        let handle = dst::task::spawn(task);
385        #[cfg(not(all(feature = "simulation", madsim)))]
386        let handle = get_runtime().spawn(task);
387
388        self.task_handle = Some(handle);
389        self.canceled = false;
390    }
391
392    /// Cancels the timer.
393    ///
394    /// The timer will not generate a final event.
395    pub fn cancel(&mut self) {
396        log::trace!("Cancel timer '{}'", self.name);
397
398        self.close_registered_callback();
399
400        self.retire_task();
401
402        if let Some(handle) = self.task_handle.take() {
403            handle.abort();
404        }
405        self.canceled = true;
406    }
407
408    fn close_registered_callback(&self) {
409        if let OwnerCallback::Registered { token, .. } = &self.callback {
410            token.close();
411        }
412    }
413
414    fn retire_task(&mut self) -> Option<TimerTaskRetirement> {
415        let task_state = self.task_state.take()?;
416        let retirement = task_state.retire();
417        self.exhausted |= retirement.exhausted;
418        self.next_time_ns
419            .store(retirement.next_time_ns, atomic::Ordering::SeqCst);
420        Some(retirement)
421    }
422}
423
424impl Timer for LiveTimer {
425    fn is_expired(&self) -> bool {
426        Self::is_expired(self)
427    }
428
429    fn cancel(&mut self) {
430        Self::cancel(self);
431    }
432}
433
434impl Drop for LiveTimer {
435    fn drop(&mut self) {
436        self.close_registered_callback();
437        self.retire_task();
438
439        if let Some(handle) = self.task_handle.take() {
440            handle.abort();
441        }
442    }
443}
444
445fn should_fire_scheduled_time(next_time_ns: UnixNanos, stop_time_ns: Option<UnixNanos>) -> bool {
446    stop_time_ns.is_none_or(|stop_time_ns| next_time_ns <= stop_time_ns)
447}
448
449fn expires_after_scheduled_time(next_time_ns: UnixNanos, stop_time_ns: Option<UnixNanos>) -> bool {
450    stop_time_ns == Some(next_time_ns)
451}
452
453fn is_stop_boundary(next_time_ns: u64, stop_time_ns: Option<UnixNanos>) -> bool {
454    stop_time_ns == Some(UnixNanos::from(next_time_ns))
455}
456
457fn should_adjust_past_due_time(
458    observed_next: u64,
459    now_ns: UnixNanos,
460    stop_time_ns: Option<UnixNanos>,
461) -> bool {
462    observed_next <= now_ns.as_u64() && !is_stop_boundary(observed_next, stop_time_ns)
463}
464
465fn normalize_start_time_ns(
466    observed_next: u64,
467    now_ns: UnixNanos,
468    stop_time_ns: Option<UnixNanos>,
469) -> UnixNanos {
470    if is_stop_boundary(observed_next, stop_time_ns) {
471        return UnixNanos::from(observed_next);
472    }
473
474    let now_raw = now_ns.as_u64();
475    let start_time_ns = if observed_next <= now_raw {
476        now_raw
477    } else {
478        observed_next
479    };
480
481    UnixNanos::from(floor_to_nearest_microsecond(start_time_ns))
482}
483
484fn timer_start_delay(next_time_ns: UnixNanos, now_ns: UnixNanos) -> Duration {
485    Duration::from(next_time_ns.saturating_duration_since(now_ns))
486}
487
488#[derive(Debug)]
489struct TimerTaskState {
490    status: AtomicU8,
491    next_time_ns: AtomicU64,
492}
493
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
495struct TimerTaskRetirement {
496    next_time_ns: u64,
497    exhausted: bool,
498}
499
500impl TimerTaskState {
501    fn new(next_time_ns: u64) -> Self {
502        Self {
503            status: AtomicU8::new(TASK_ACTIVE),
504            next_time_ns: AtomicU64::new(next_time_ns),
505        }
506    }
507
508    fn reserve_registered_fire(
509        &self,
510        token: &TimeEventCallbackToken,
511        following_next_time_ns: Option<u64>,
512    ) -> Option<TimeEventCallbackLease> {
513        let lease = token.acquire()?;
514        self.reserve_fire(following_next_time_ns).then_some(lease)
515    }
516
517    fn reserve_fire(&self, following_next_time_ns: Option<u64>) -> bool {
518        if self
519            .status
520            .compare_exchange(
521                TASK_ACTIVE,
522                TASK_FIRING,
523                atomic::Ordering::SeqCst,
524                atomic::Ordering::SeqCst,
525            )
526            .is_err()
527        {
528            return false;
529        }
530
531        if let Some(following_next_time_ns) = following_next_time_ns {
532            self.next_time_ns
533                .store(following_next_time_ns, atomic::Ordering::SeqCst);
534            self.status.store(TASK_ACTIVE, atomic::Ordering::SeqCst);
535        } else {
536            self.status.store(TASK_EXHAUSTED, atomic::Ordering::SeqCst);
537        }
538        true
539    }
540
541    fn retire(&self) -> TimerTaskRetirement {
542        let exhausted = loop {
543            match self.status.compare_exchange(
544                TASK_ACTIVE,
545                TASK_RETIRED,
546                atomic::Ordering::SeqCst,
547                atomic::Ordering::SeqCst,
548            ) {
549                Ok(_) | Err(TASK_RETIRED) => break false,
550                Err(TASK_EXHAUSTED) => break true,
551                // The firing section contains only atomic schedule publication
552                Err(TASK_FIRING) => std::hint::spin_loop(),
553                Err(status) => unreachable!("invalid timer task state {status}"),
554            }
555        };
556
557        TimerTaskRetirement {
558            next_time_ns: self.next_time_ns.load(atomic::Ordering::SeqCst),
559            exhausted,
560        }
561    }
562}
563
564#[derive(Debug)]
565enum OwnerCallback {
566    Direct(TimeEventMessageFactory),
567    /// The callback is retained owner-side so a restarted timer (cancel or
568    /// natural expiry closed the token) can register a fresh token.
569    Registered {
570        token: TimeEventCallbackToken,
571        callback: TimeEventCallback,
572    },
573    Senderless(TimeEventCallback),
574}
575
576#[derive(Clone, Debug)]
577enum WorkerDispatch {
578    Direct(TimeEventMessageFactory),
579    Registered(TimeEventCallbackToken),
580    #[cfg(feature = "python")]
581    SenderlessPython(Arc<crate::timer::PythonTimeEventCallback>),
582}
583
584#[cfg(test)]
585mod tests {
586    #[cfg(not(all(feature = "simulation", madsim)))]
587    use std::rc::Rc;
588    #[cfg(all(feature = "python", not(all(feature = "simulation", madsim))))]
589    use std::sync::{OnceLock, atomic::AtomicU64};
590    use std::{
591        num::NonZeroU64,
592        sync::{
593            Arc,
594            atomic::{AtomicUsize, Ordering},
595        },
596    };
597    #[cfg(any(feature = "python", not(all(feature = "simulation", madsim))))]
598    use std::{sync::mpsc, time::Duration as StdDuration};
599
600    use nautilus_core::{UnixNanos, time::get_atomic_clock_realtime};
601    #[cfg(not(all(feature = "simulation", madsim)))]
602    use parking_lot::Mutex;
603    #[cfg(feature = "python")]
604    use pyo3::{
605        Python,
606        types::{PyAnyMethods, PyList, PyListMethods},
607    };
608    use rstest::*;
609    use ustr::Ustr;
610
611    use super::LiveTimer;
612    #[cfg(not(all(feature = "simulation", madsim)))]
613    use crate::runner::register_time_event_callback;
614    #[cfg(not(all(feature = "simulation", madsim)))]
615    use crate::testing::wait_until;
616    use crate::{
617        runner::{TimeEventMessage, TimeEventSender},
618        timer::TimeEventCallback,
619    };
620
621    #[cfg(any(feature = "python", not(all(feature = "simulation", madsim))))]
622    #[derive(Debug)]
623    struct ChannelSender {
624        tx: mpsc::Sender<TimeEventMessage>,
625    }
626
627    #[cfg(any(feature = "python", not(all(feature = "simulation", madsim))))]
628    impl TimeEventSender for ChannelSender {
629        fn send(&self, message: TimeEventMessage) {
630            self.tx.send(message).expect("message should send");
631        }
632    }
633
634    #[cfg(not(all(feature = "simulation", madsim)))]
635    #[derive(Debug)]
636    struct PausingChannelSender {
637        tx: mpsc::Sender<TimeEventMessage>,
638        release_rx: Mutex<mpsc::Receiver<()>>,
639    }
640
641    #[cfg(not(all(feature = "simulation", madsim)))]
642    impl TimeEventSender for PausingChannelSender {
643        fn send(&self, message: TimeEventMessage) {
644            self.tx.send(message).expect("message should send");
645            self.release_rx
646                .lock()
647                .recv()
648                .expect("timer send should release");
649        }
650    }
651
652    #[cfg(all(feature = "simulation", madsim))]
653    #[derive(Debug)]
654    struct CountingSender {
655        count: Arc<AtomicUsize>,
656    }
657
658    #[cfg(all(feature = "simulation", madsim))]
659    impl TimeEventSender for CountingSender {
660        fn send(&self, _message: TimeEventMessage) {
661            self.count.fetch_add(1, Ordering::Relaxed);
662        }
663    }
664
665    #[rstest]
666    #[case::unbounded(100, None, true, false)]
667    #[case::past_stop(110, Some(100), false, false)]
668    #[case::before_stop(90, Some(100), true, false)]
669    #[case::at_stop(100, Some(100), true, true)]
670    fn test_live_timer_stop_bound(
671        #[case] next_time_ns: u64,
672        #[case] stop_time_ns: Option<u64>,
673        #[case] should_fire: bool,
674        #[case] expires: bool,
675    ) {
676        let next_time_ns = UnixNanos::from(next_time_ns);
677        let stop_time_ns = stop_time_ns.map(UnixNanos::from);
678
679        assert_eq!(
680            super::should_fire_scheduled_time(next_time_ns, stop_time_ns),
681            should_fire
682        );
683        assert_eq!(
684            super::expires_after_scheduled_time(next_time_ns, stop_time_ns),
685            expires
686        );
687    }
688
689    #[rstest]
690    #[case::stop_boundary(100, 110, 100, false)]
691    #[case::before_stop(90, 110, 120, true)]
692    fn test_live_timer_past_due_adjustment(
693        #[case] observed_next: u64,
694        #[case] now: u64,
695        #[case] stop_time_ns: u64,
696        #[case] expected: bool,
697    ) {
698        assert_eq!(
699            super::should_adjust_past_due_time(
700                observed_next,
701                UnixNanos::from(now),
702                Some(UnixNanos::from(stop_time_ns)),
703            ),
704            expected
705        );
706    }
707
708    #[rstest]
709    #[case::past_due(1_234_567, 2_345_678, None, 2_345_000)]
710    #[case::future(3_456_789, 2_345_678, None, 3_456_000)]
711    #[case::stop_boundary(1_234_567, 2_345_678, Some(1_234_567), 1_234_567)]
712    fn test_live_timer_start_time_normalization(
713        #[case] observed_next: u64,
714        #[case] now: u64,
715        #[case] stop_time_ns: Option<u64>,
716        #[case] expected: u64,
717    ) {
718        assert_eq!(
719            super::normalize_start_time_ns(
720                observed_next,
721                UnixNanos::from(now),
722                stop_time_ns.map(UnixNanos::from),
723            ),
724            UnixNanos::from(expected)
725        );
726    }
727
728    #[rstest]
729    #[case::full(12_000_000, 10_000_000, 2_000_000)]
730    #[case::sub_millisecond(10_500_000, 10_000_000, 500_000)]
731    fn test_live_timer_start_delay(
732        #[case] next_time_ns: u64,
733        #[case] now: u64,
734        #[case] expected_ns: u64,
735    ) {
736        assert_eq!(
737            super::timer_start_delay(UnixNanos::from(next_time_ns), UnixNanos::from(now)),
738            tokio::time::Duration::from_nanos(expected_ns)
739        );
740    }
741
742    #[rstest]
743    #[case::following_schedule(Some(200))]
744    #[case::exhaustion(None)]
745    fn test_timer_task_retirement_prevents_a_late_fire(
746        #[case] following_next_time_ns: Option<u64>,
747    ) {
748        let state = super::TimerTaskState::new(100);
749
750        let retirement = state.retire();
751        let reserved = state.reserve_fire(following_next_time_ns);
752
753        assert_eq!(retirement.next_time_ns, 100);
754        assert!(!retirement.exhausted);
755        assert!(!reserved);
756        assert_eq!(state.next_time_ns.load(Ordering::SeqCst), 100);
757    }
758
759    #[rstest]
760    fn test_timer_task_retirement_preserves_a_reserved_fire() {
761        let state = super::TimerTaskState::new(100);
762
763        let reserved = state.reserve_fire(Some(200));
764        let retirement = state.retire();
765
766        assert!(reserved);
767        assert_eq!(retirement.next_time_ns, 200);
768        assert!(!retirement.exhausted);
769        assert_eq!(state.next_time_ns.load(Ordering::SeqCst), 200);
770    }
771
772    #[rstest]
773    fn test_timer_task_retirement_preserves_exhaustion() {
774        let state = super::TimerTaskState::new(100);
775
776        let reserved = state.reserve_fire(None);
777        let retirement = state.retire();
778
779        assert!(reserved);
780        assert_eq!(retirement.next_time_ns, 100);
781        assert!(retirement.exhausted);
782        assert_eq!(state.next_time_ns.load(Ordering::SeqCst), 100);
783    }
784
785    #[cfg(not(all(feature = "simulation", madsim)))]
786    #[rstest]
787    fn test_closed_registered_callback_does_not_reserve_fire() {
788        let state = super::TimerTaskState::new(100);
789        let callback = TimeEventCallback::RustLocal(Rc::new(|_| {}));
790        let token = register_time_event_callback(callback);
791        token.close();
792
793        let lease = state.reserve_registered_fire(&token, Some(200));
794        let retirement = state.retire();
795
796        assert!(lease.is_none());
797        assert_eq!(retirement.next_time_ns, 100);
798        assert!(!retirement.exhausted);
799        assert_eq!(state.next_time_ns.load(Ordering::SeqCst), 100);
800    }
801
802    #[rstest]
803    #[case::immediate(true, 100)]
804    #[case::after_interval(false, 1_100)]
805    fn test_live_timer_fire_immediately(
806        #[case] fire_immediately: bool,
807        #[case] expected_next_time_ns: u64,
808    ) {
809        let timer = LiveTimer::new(
810            Ustr::from("TEST_TIMER"),
811            NonZeroU64::new(1000).unwrap(),
812            UnixNanos::from(100),
813            None,
814            TimeEventCallback::from(|_| {}),
815            fire_immediately,
816            None,
817        );
818
819        assert_eq!(timer.fire_immediately, fire_immediately);
820        assert_eq!(timer.next_time_ns(), UnixNanos::from(expected_next_time_ns));
821    }
822
823    #[rstest]
824    #[should_panic(expected = "timer event sender was unset for Rust callback system")]
825    fn test_live_timer_start_panics_on_senderless_rust_callback() {
826        let now = get_atomic_clock_realtime().get_time_ns();
827        let mut timer = LiveTimer::new(
828            Ustr::from("SENDERLESS_RUST"),
829            NonZeroU64::new(1_000_000).unwrap(),
830            now,
831            None,
832            TimeEventCallback::from(|_| {}),
833            false,
834            None, // time_event_sender
835        );
836
837        timer.start();
838    }
839
840    #[cfg(not(all(feature = "simulation", madsim)))]
841    #[rstest]
842    fn test_live_timer_uses_global_runtime() {
843        let (tx, rx) = mpsc::channel();
844        let sender = Arc::new(ChannelSender { tx });
845        let now = get_atomic_clock_realtime().get_time_ns();
846        let mut timer = LiveTimer::new(
847            Ustr::from("LIVE_TIMER"),
848            NonZeroU64::new(1_000_000).unwrap(),
849            now,
850            Some(now),
851            TimeEventCallback::from(|_| {}),
852            true,
853            Some(sender),
854        );
855
856        timer.start();
857        let message = rx
858            .recv_timeout(StdDuration::from_secs(1))
859            .expect("timer message should arrive on the global runtime");
860        wait_until(|| timer.is_expired(), StdDuration::from_secs(1));
861
862        assert_eq!(message.event().ts_event, now);
863        assert!(timer.is_expired());
864    }
865
866    #[cfg(not(all(feature = "simulation", madsim)))]
867    #[rstest]
868    fn test_live_timer_expires_after_terminal_event() {
869        let (tx, rx) = mpsc::channel();
870        let sender = Arc::new(ChannelSender { tx });
871        let now = get_atomic_clock_realtime().get_time_ns();
872        let mut timer = LiveTimer::new(
873            Ustr::from("TERMINAL_TIMER"),
874            NonZeroU64::new(u64::MAX).unwrap(),
875            now,
876            None,
877            TimeEventCallback::from(|_| {}),
878            true,
879            Some(sender),
880        );
881
882        timer.start();
883        let scheduled_time = timer.next_time_ns();
884        let message = rx
885            .recv_timeout(StdDuration::from_secs(1))
886            .expect("terminal timer event should arrive");
887        wait_until(|| timer.is_expired(), StdDuration::from_secs(1));
888
889        assert_eq!(message.event().ts_event, scheduled_time);
890        assert_eq!(timer.next_time_ns(), scheduled_time);
891        assert!(timer.is_expired());
892
893        timer.start();
894        assert!(rx.recv_timeout(StdDuration::from_millis(10)).is_err());
895        assert_eq!(timer.next_time_ns(), scheduled_time);
896        assert!(timer.is_expired());
897    }
898
899    #[cfg(not(all(feature = "simulation", madsim)))]
900    #[rstest]
901    fn test_live_timer_registered_callback_expires_after_terminal_event() {
902        let (tx, rx) = mpsc::channel();
903        let sender = Arc::new(ChannelSender { tx });
904        let count = Rc::new(std::cell::Cell::new(0));
905        let callback_count = count.clone();
906        let callback: Rc<dyn Fn(crate::timer::TimeEvent)> =
907            Rc::new(move |_| callback_count.set(callback_count.get() + 1));
908        let callback_weak = Rc::downgrade(&callback);
909        let now = get_atomic_clock_realtime().get_time_ns();
910        let mut timer = LiveTimer::new(
911            Ustr::from("TERMINAL_LOCAL_TIMER"),
912            NonZeroU64::new(u64::MAX).unwrap(),
913            now,
914            None,
915            TimeEventCallback::RustLocal(callback),
916            true,
917            Some(sender),
918        );
919
920        timer.start();
921        let scheduled_time = timer.next_time_ns();
922        let message = rx
923            .recv_timeout(StdDuration::from_secs(1))
924            .expect("registered terminal timer event should arrive");
925        wait_until(|| timer.is_expired(), StdDuration::from_secs(1));
926
927        assert_eq!(message.event().ts_event, scheduled_time);
928        assert!(message.dispatch());
929        assert_eq!(count.get(), 1);
930        assert_eq!(timer.next_time_ns(), scheduled_time);
931        assert!(timer.is_expired());
932
933        timer.start();
934        let token_closed = match &timer.callback {
935            super::OwnerCallback::Registered { token, .. } => token.is_closed(),
936            _ => false,
937        };
938        assert!(rx.recv_timeout(StdDuration::from_millis(10)).is_err());
939        assert!(token_closed);
940        assert_eq!(timer.next_time_ns(), scheduled_time);
941        assert!(timer.is_expired());
942        assert!(callback_weak.upgrade().is_some());
943
944        drop(timer);
945        assert!(callback_weak.upgrade().is_none());
946    }
947
948    #[cfg(not(all(feature = "simulation", madsim)))]
949    #[rstest]
950    fn test_live_timer_dispatches_rust_local_callback_on_owner_thread() {
951        let (tx, rx) = mpsc::channel();
952        let sender = Arc::new(ChannelSender { tx });
953        let count = Rc::new(std::cell::Cell::new(0));
954        let callback_count = count.clone();
955        let callback: Rc<dyn Fn(crate::timer::TimeEvent)> =
956            Rc::new(move |_| callback_count.set(callback_count.get() + 1));
957        let now = get_atomic_clock_realtime().get_time_ns();
958        let mut timer = LiveTimer::new(
959            Ustr::from("LOCAL_TIMER"),
960            NonZeroU64::new(1_000_000).unwrap(),
961            now,
962            Some(now),
963            TimeEventCallback::RustLocal(callback),
964            true,
965            Some(sender),
966        );
967
968        timer.start();
969        let message = rx
970            .recv_timeout(StdDuration::from_secs(1))
971            .expect("registered timer message should arrive");
972
973        assert!(message.dispatch());
974        assert_eq!(count.get(), 1);
975    }
976
977    #[cfg(not(all(feature = "simulation", madsim)))]
978    #[rstest]
979    fn test_live_timer_cancel_preserves_queued_rust_local_callback_lease() {
980        let (tx, rx) = mpsc::channel();
981        let (release_tx, release_rx) = mpsc::channel();
982        let sender = Arc::new(PausingChannelSender {
983            tx,
984            release_rx: Mutex::new(release_rx),
985        });
986        let count = Rc::new(std::cell::Cell::new(0));
987        let callback_count = count.clone();
988        let callback: Rc<dyn Fn(crate::timer::TimeEvent)> =
989            Rc::new(move |_| callback_count.set(callback_count.get() + 1));
990        let callback_weak = Rc::downgrade(&callback);
991        let now = get_atomic_clock_realtime().get_time_ns();
992        let mut timer = LiveTimer::new(
993            Ustr::from("CANCEL_QUEUED"),
994            NonZeroU64::new(1_000_000).unwrap(),
995            now,
996            None,
997            TimeEventCallback::RustLocal(callback),
998            true,
999            Some(sender),
1000        );
1001
1002        timer.start();
1003        let message = rx
1004            .recv_timeout(StdDuration::from_secs(1))
1005            .expect("registered timer message should arrive");
1006        timer.cancel();
1007        release_tx.send(()).expect("timer send should release");
1008
1009        assert!(callback_weak.upgrade().is_some());
1010        assert!(message.dispatch());
1011        assert_eq!(count.get(), 1);
1012
1013        // The timer retains an owner-side clone for restart; only after it
1014        // drops must no registry copy remain.
1015        drop(timer);
1016        assert!(callback_weak.upgrade().is_none());
1017    }
1018
1019    #[cfg(not(all(feature = "simulation", madsim)))]
1020    #[rstest]
1021    fn test_live_timer_cancel_preserves_queued_direct_callback() {
1022        let (tx, rx) = mpsc::channel();
1023        let sender = Arc::new(ChannelSender { tx });
1024        let count = Arc::new(AtomicUsize::new(0));
1025        let callback_count = count.clone();
1026        let now = get_atomic_clock_realtime().get_time_ns();
1027        let mut timer = LiveTimer::new(
1028            Ustr::from("CANCEL_QUEUED_DIRECT"),
1029            NonZeroU64::new(1_000_000).unwrap(),
1030            now,
1031            None,
1032            TimeEventCallback::from(move |_| {
1033                callback_count.fetch_add(1, Ordering::Relaxed);
1034            }),
1035            true,
1036            Some(sender),
1037        );
1038
1039        timer.start();
1040        let message = rx
1041            .recv_timeout(StdDuration::from_secs(1))
1042            .expect("direct timer message should arrive");
1043        timer.cancel();
1044
1045        assert!(message.dispatch());
1046        assert_eq!(count.load(Ordering::Relaxed), 1);
1047    }
1048
1049    #[cfg(not(all(feature = "simulation", madsim)))]
1050    #[rstest]
1051    fn test_live_timer_restart_after_cancel_re_registers_rust_local_callback() {
1052        let (tx, rx) = mpsc::channel();
1053        let sender = Arc::new(ChannelSender { tx });
1054        let count = Rc::new(std::cell::Cell::new(0));
1055        let callback_count = count.clone();
1056        let callback: Rc<dyn Fn(crate::timer::TimeEvent)> =
1057            Rc::new(move |_| callback_count.set(callback_count.get() + 1));
1058        let now = get_atomic_clock_realtime().get_time_ns();
1059        let mut timer = LiveTimer::new(
1060            Ustr::from("RESTART_TIMER"),
1061            NonZeroU64::new(1_000_000).unwrap(),
1062            now,
1063            None,
1064            TimeEventCallback::RustLocal(callback),
1065            true,
1066            Some(sender),
1067        );
1068
1069        timer.start();
1070        let first = rx
1071            .recv_timeout(StdDuration::from_secs(1))
1072            .expect("first registered timer message should arrive");
1073        timer.cancel();
1074        assert!(first.dispatch());
1075
1076        timer.start();
1077        let second = rx
1078            .recv_timeout(StdDuration::from_secs(1))
1079            .expect("restarted timer should re-register and fire");
1080        timer.cancel();
1081
1082        assert!(second.dispatch());
1083        assert_eq!(count.get(), 2);
1084    }
1085
1086    #[cfg(not(all(feature = "simulation", madsim)))]
1087    #[rstest]
1088    fn test_live_timer_start_while_active_restarts_and_keeps_dispatching() {
1089        let (tx, rx) = mpsc::channel();
1090        let sender = Arc::new(ChannelSender { tx });
1091        let count = Rc::new(std::cell::Cell::new(0));
1092        let callback_count = count.clone();
1093        let callback: Rc<dyn Fn(crate::timer::TimeEvent)> =
1094            Rc::new(move |_| callback_count.set(callback_count.get() + 1));
1095        let now = get_atomic_clock_realtime().get_time_ns();
1096        let mut timer = LiveTimer::new(
1097            Ustr::from("DOUBLE_START_TIMER"),
1098            NonZeroU64::new(1_000_000).unwrap(),
1099            now,
1100            None,
1101            TimeEventCallback::RustLocal(callback),
1102            true,
1103            Some(sender),
1104        );
1105
1106        timer.start();
1107        let first = rx
1108            .recv_timeout(StdDuration::from_secs(1))
1109            .expect("first task message should arrive");
1110
1111        // Second start with the first task still active: the old task is
1112        // aborted and the token stays live for the new one.
1113        timer.start();
1114        let second = rx
1115            .recv_timeout(StdDuration::from_secs(1))
1116            .expect("restarted task should keep dispatching");
1117        timer.cancel();
1118
1119        assert!(first.dispatch());
1120        assert!(second.dispatch());
1121        assert_eq!(count.get(), 2);
1122    }
1123
1124    #[cfg(not(all(feature = "simulation", madsim)))]
1125    #[rstest]
1126    fn test_live_timer_stop_before_first_fire_sends_cleanup_message() {
1127        let (tx, rx) = mpsc::channel();
1128        let sender = Arc::new(ChannelSender { tx });
1129        let count = Rc::new(std::cell::Cell::new(0));
1130        let callback_count = count.clone();
1131        let callback: Rc<dyn Fn(crate::timer::TimeEvent)> =
1132            Rc::new(move |_| callback_count.set(callback_count.get() + 1));
1133        let callback_weak = Rc::downgrade(&callback);
1134        let now = get_atomic_clock_realtime().get_time_ns();
1135        let mut timer = LiveTimer::new(
1136            Ustr::from("CLEANUP_TIMER"),
1137            NonZeroU64::new(1_000_000).unwrap(),
1138            now,
1139            Some(now),
1140            TimeEventCallback::RustLocal(callback),
1141            false,
1142            Some(sender),
1143        );
1144
1145        timer.start();
1146        let cleanup = rx
1147            .recv_timeout(StdDuration::from_secs(1))
1148            .expect("cleanup message should arrive");
1149
1150        assert!(callback_weak.upgrade().is_some());
1151        assert!(!cleanup.dispatch());
1152        assert_eq!(count.get(), 0);
1153
1154        // The timer retains an owner-side clone for restart; only after it
1155        // drops must no registry copy remain.
1156        drop(timer);
1157        assert!(callback_weak.upgrade().is_none());
1158    }
1159
1160    #[cfg(all(feature = "simulation", madsim))]
1161    #[madsim::test]
1162    async fn test_live_timer_uses_dst_runtime() {
1163        let count = Arc::new(AtomicUsize::new(0));
1164        let sender = Arc::new(CountingSender {
1165            count: count.clone(),
1166        });
1167        let now = get_atomic_clock_realtime().get_time_ns();
1168        let mut timer = LiveTimer::new(
1169            Ustr::from("DST_TIMER"),
1170            NonZeroU64::new(1_000_000).unwrap(),
1171            now,
1172            Some(now),
1173            TimeEventCallback::from(|_| {}),
1174            true,
1175            Some(sender),
1176        );
1177
1178        timer.start();
1179        crate::live::dst::time::sleep(crate::live::dst::time::Duration::from_millis(2)).await;
1180        crate::live::dst::task::yield_now().await;
1181
1182        assert_eq!(count.load(Ordering::Relaxed), 1);
1183        assert!(timer.is_expired());
1184    }
1185
1186    #[cfg(feature = "python")]
1187    #[rstest]
1188    fn test_live_timer_with_sender_defers_python_callback_to_handler() {
1189        Python::initialize();
1190
1191        Python::attach(|py| {
1192            let py_list = PyList::empty(py);
1193            let py_append = py_list
1194                .getattr("append")
1195                .expect("append should exist")
1196                .unbind();
1197            let callback = TimeEventCallback::from(py_append);
1198            let (tx, rx) = mpsc::channel();
1199            let sender = Arc::new(ChannelSender { tx });
1200            let now = get_atomic_clock_realtime().get_time_ns();
1201
1202            let mut timer = LiveTimer::new(
1203                Ustr::from("PY_TIMER"),
1204                NonZeroU64::new(1_000_000).unwrap(),
1205                now,
1206                None,
1207                callback,
1208                true,
1209                Some(sender),
1210            );
1211
1212            timer.start();
1213            let message = rx
1214                .recv_timeout(StdDuration::from_secs(1))
1215                .expect("timer message should arrive without acquiring the GIL on the worker");
1216            timer.cancel();
1217
1218            assert_eq!(py_list.len(), 0);
1219            assert!(message.dispatch());
1220            assert_eq!(py_list.len(), 1);
1221        });
1222    }
1223
1224    #[cfg(all(feature = "python", not(all(feature = "simulation", madsim))))]
1225    #[rstest]
1226    fn test_senderless_callback_observes_current_schedule() {
1227        Python::initialize();
1228
1229        Python::attach(|py| {
1230            let schedule = Arc::new(OnceLock::<Arc<AtomicU64>>::new());
1231            let callback_schedule = schedule.clone();
1232            let (tx, rx) = mpsc::channel();
1233            let callback = pyo3::types::PyCFunction::new_closure(
1234                py,
1235                None,
1236                None,
1237                move |_args: &pyo3::Bound<'_, pyo3::types::PyTuple>,
1238                      _kwargs: Option<&pyo3::Bound<'_, pyo3::types::PyDict>>|
1239                      -> pyo3::PyResult<()> {
1240                    let next_time_ns = callback_schedule
1241                        .get()
1242                        .expect("timer schedule should be available")
1243                        .load(Ordering::SeqCst);
1244                    tx.send(next_time_ns)
1245                        .expect("observed schedule should send");
1246                    Ok(())
1247                },
1248            )
1249            .expect("callback should create")
1250            .into_any()
1251            .unbind();
1252            let now = get_atomic_clock_realtime().get_time_ns();
1253            let interval_ns = 10_000_000;
1254            let mut timer = LiveTimer::new(
1255                Ustr::from("SENDERLESS_SCHEDULE"),
1256                NonZeroU64::new(interval_ns).unwrap(),
1257                now,
1258                Some(now),
1259                TimeEventCallback::from(callback),
1260                true,
1261                None,
1262            );
1263
1264            timer.start();
1265            let expected_time_ns = timer.next_time_ns().as_u64();
1266            schedule
1267                .set(timer.next_time_ns.clone())
1268                .expect("timer schedule should set once");
1269            let observed_time_ns = py
1270                .detach(move || rx.recv_timeout(StdDuration::from_secs(1)))
1271                .expect("senderless callback should observe the schedule");
1272            wait_until(
1273                || timer.next_time_ns().as_u64() == expected_time_ns + interval_ns,
1274                StdDuration::from_secs(1),
1275            );
1276            timer.cancel();
1277
1278            assert_eq!(observed_time_ns, expected_time_ns);
1279        });
1280    }
1281}