Skip to main content

nautilus_common/clock/
mod.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 virtual `Clock` implementations.
17//!
18//! Defines the [`Clock`] contract, the user-facing [`ClockApi`] facade, and the deterministic
19//! [`VirtualClock`] used for controlled time advancement. Shared validation and callback registration
20//! support virtual and live clock implementations.
21
22mod api;
23#[path = "virtual.rs"]
24mod virtual_clock;
25
26#[cfg(test)]
27mod tests;
28
29use std::{any::Any, collections::BTreeMap, fmt::Debug, sync::Arc, time::Duration};
30
31use ahash::AHashMap;
32pub use api::ClockApi; // Re-export
33use jiff::Timestamp;
34use nautilus_core::{
35    DurationNanos, UnixNanos,
36    correctness::{check_positive_u64, check_valid_string_utf8},
37    datetime::try_datetime_to_unix_nanos,
38};
39use ustr::Ustr;
40pub use virtual_clock::VirtualClock; // Re-export
41
42use crate::timer::{
43    TimeEvent, TimeEventCallback, TimeEventHandler, Timer, TimerInterval, TimerSchedule,
44    create_valid_interval,
45};
46
47/// Provides time access, timer scheduling, and callback registration.
48///
49/// An active timer is one that has not expired.
50pub trait Clock: Debug + Any {
51    /// Returns the current UTC timestamp.
52    fn utc_now(&self) -> Timestamp {
53        self.timestamp_ns().to_datetime_utc()
54    }
55
56    /// Returns the current UNIX timestamp in nanoseconds (ns).
57    fn timestamp_ns(&self) -> UnixNanos;
58
59    /// Returns the current UNIX timestamp in microseconds (μs).
60    fn timestamp_us(&self) -> u64;
61
62    /// Returns the current UNIX timestamp in milliseconds (ms).
63    fn timestamp_ms(&self) -> u64;
64
65    /// Returns the current UNIX timestamp in seconds.
66    fn timestamp(&self) -> f64;
67
68    /// Returns the names of active timers in the clock.
69    fn timer_names(&self) -> Vec<&str>;
70
71    /// Returns the count of active timers in the clock.
72    fn timer_count(&self) -> usize;
73
74    /// Returns whether an active timer named `name` exists.
75    fn timer_exists(&self, name: &Ustr) -> bool;
76
77    /// Registers the callback used when a timer has no named callback.
78    fn register_default_handler(&mut self, callback: TimeEventCallback);
79
80    /// Cancels the registered default event handler, if any.
81    ///
82    /// Releases the held callback so any Python object owned by it can be dropped.
83    /// `Trader::release_component` calls this at component retirement to break the cycle
84    /// between a Python component and its clock: the clock holds the callback as a
85    /// `Py<PyAny>` that Python's cycle collector cannot reach through.
86    fn cancel_default_handler(&mut self);
87
88    /// Cancels all registered named event callbacks, preserving the default handler.
89    ///
90    /// Releases callbacks registered via [`Clock::set_time_alert_ns`] or
91    /// [`Clock::set_timer_ns`] with an explicit `callback` argument.
92    /// `Trader::release_component` calls this at component retirement, breaking the same
93    /// cycle as [`Clock::cancel_default_handler`].
94    fn cancel_callbacks(&mut self);
95
96    /// Sets a timer to alert at the specified time.
97    ///
98    /// See [`Clock::set_time_alert_ns`] for flag semantics.
99    ///
100    /// # Callback
101    ///
102    /// - `Some(callback)` registers and uses `callback` for the named alert.
103    /// - `None` uses a callback registered under `name`, falling back to the default callback.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if:
108    /// - `name` is invalid.
109    /// - `alert_time` is before the UNIX epoch or outside the [`UnixNanos`] range.
110    /// - The alert is in the past and `allow_past` is `Some(false)`.
111    /// - No explicit, named, or default callback is available.
112    fn set_time_alert(
113        &mut self,
114        name: &str,
115        alert_time: Timestamp,
116        callback: Option<TimeEventCallback>,
117        allow_past: Option<bool>,
118    ) -> anyhow::Result<()> {
119        self.set_time_alert_ns(
120            name,
121            try_datetime_to_unix_nanos(alert_time)?,
122            callback,
123            allow_past,
124        )
125    }
126
127    /// Sets a timer to alert at the specified time.
128    ///
129    /// Any active timer registered under the same `name` is canceled with a warning before the
130    /// new alert is scheduled. `allow_past` defaults to `true`.
131    ///
132    /// # Flags
133    ///
134    /// | `allow_past` | Behavior                                                               |
135    /// | ------------ | ---------------------------------------------------------------------- |
136    /// | `true`       | A past alert is moved to the current time and fires immediately.       |
137    /// | `false`      | An alert earlier than the current time returns an error.               |
138    ///
139    /// # Callback
140    ///
141    /// - `Some(callback)` registers and uses `callback` for the named alert.
142    /// - `None` uses a callback registered under `name`, falling back to the default callback.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if:
147    /// - `name` is invalid.
148    /// - `alert_time_ns` is earlier than now and `allow_past` is `Some(false)`.
149    /// - No explicit, named, or default callback is available.
150    fn set_time_alert_ns(
151        &mut self,
152        name: &str,
153        alert_time_ns: UnixNanos,
154        callback: Option<TimeEventCallback>,
155        allow_past: Option<bool>,
156    ) -> anyhow::Result<()>;
157
158    /// Sets a timer to fire time events at every interval between the start and stop times.
159    ///
160    /// Any active timer registered under the same `name` is canceled with a warning before the
161    /// new timer is scheduled.
162    ///
163    /// See [`Clock::set_timer_ns`] for flag semantics.
164    ///
165    /// # Callback
166    ///
167    /// - `Some(callback)` registers and uses `callback` for the named timer.
168    /// - `None` uses a callback registered under `name`, falling back to the default callback.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if:
173    /// - `name` is invalid.
174    /// - `interval` is zero or exceeds `u64::MAX` nanoseconds.
175    /// - `start_time` or `stop_time` is before the UNIX epoch or out of range for `UnixNanos`.
176    /// - The first event timestamp is out of range for `UnixNanos`.
177    /// - The first event is in the past when past times are disallowed.
178    /// - The stop time is not after the start time.
179    /// - The stop time is not after the current time when past times are disallowed.
180    /// - No explicit, named, or default callback is available.
181    #[expect(clippy::too_many_arguments)]
182    fn set_timer(
183        &mut self,
184        name: &str,
185        interval: Duration,
186        start_time: Option<Timestamp>,
187        stop_time: Option<Timestamp>,
188        callback: Option<TimeEventCallback>,
189        allow_past: Option<bool>,
190        fire_immediately: Option<bool>,
191    ) -> anyhow::Result<()> {
192        self.set_timer_ns(
193            name,
194            duration_to_nanos(interval)?,
195            start_time.map(try_datetime_to_unix_nanos).transpose()?,
196            stop_time.map(try_datetime_to_unix_nanos).transpose()?,
197            callback,
198            allow_past,
199            fire_immediately,
200        )
201    }
202
203    /// Sets a timer to fire time events at every interval between the start and stop times.
204    ///
205    /// Any active timer registered under the same `name` is canceled with a warning before the
206    /// new timer is scheduled. `allow_past` defaults to `true`, and `fire_immediately` defaults to
207    /// `false`.
208    ///
209    /// # Start Time
210    ///
211    /// - `None` or `Some(0)`: Uses the current time as start time.
212    /// - `Some(non_zero)`: Uses the specified timestamp as start time.
213    ///
214    /// # Flags
215    ///
216    /// | `allow_past` | `fire_immediately` | First event behavior                                |
217    /// | ------------ | ------------------ | --------------------------------------------------- |
218    /// | `true`       | `true`             | Fires at the start time, including a past start.    |
219    /// | `true`       | `false`            | Fires one interval after the start, including past. |
220    /// | `false`      | `true`             | A past start time returns an error.                 |
221    /// | `false`      | `false`            | A past first event returns an error.                |
222    ///
223    /// # Callback
224    ///
225    /// - `Some(callback)` registers and uses `callback` for the named timer.
226    /// - `None` uses a callback registered under `name`, falling back to the default callback.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if:
231    /// - `name` is invalid.
232    /// - `interval_ns` is zero.
233    /// - `start_time_ns + interval_ns` is out of range for `UnixNanos` when not firing immediately.
234    /// - The first event is in the past when past times are disallowed.
235    /// - The stop time is not after the start time.
236    /// - The stop time is not after the current time when past times are disallowed.
237    /// - No explicit, named, or default callback is available.
238    #[expect(clippy::too_many_arguments)]
239    fn set_timer_ns(
240        &mut self,
241        name: &str,
242        interval_ns: DurationNanos,
243        start_time_ns: Option<UnixNanos>,
244        stop_time_ns: Option<UnixNanos>,
245        callback: Option<TimeEventCallback>,
246        allow_past: Option<bool>,
247        fire_immediately: Option<bool>,
248    ) -> anyhow::Result<()>;
249
250    /// Registers a recurring source of absolute UTC deadlines.
251    ///
252    /// The schedule supplies its nominal start and deadlines. The clock owns event delivery,
253    /// cancellation, replacement, and the inclusive stop time. See [`Clock::set_timer_ns`] for
254    /// flag and callback semantics. An explicit epoch start supplied by the schedule stays at
255    /// the UNIX epoch.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if:
260    /// - This clock does not support deadline schedules.
261    /// - `name` is invalid.
262    /// - No valid first deadline exists when not firing immediately.
263    /// - The first event is in the past when past times are disallowed.
264    /// - The stop time is not after the start time.
265    /// - The stop time is not after the current time when past times are disallowed.
266    /// - No explicit, named, or default callback is available.
267    fn set_timer_schedule(
268        &mut self,
269        name: &str,
270        schedule: Arc<dyn TimerSchedule>,
271        stop_time_ns: Option<UnixNanos>,
272        callback: Option<TimeEventCallback>,
273        allow_past: Option<bool>,
274        fire_immediately: Option<bool>,
275    ) -> anyhow::Result<()> {
276        let _ = (
277            schedule,
278            stop_time_ns,
279            callback,
280            allow_past,
281            fire_immediately,
282        );
283        anyhow::bail!("Timer '{name}' needs a clock that supports deadline schedules")
284    }
285
286    /// Returns the next trigger timestamp for the active timer named `name`.
287    ///
288    /// Returns `None` if no active timer with that name exists.
289    fn next_time_ns(&self, name: &str) -> Option<UnixNanos>;
290
291    /// Cancels the timer named `name`, if it exists.
292    fn cancel_timer(&mut self, name: &str);
293
294    /// Cancels all timers.
295    fn cancel_timers(&mut self);
296
297    /// Resets scheduling state while preserving the default callback.
298    ///
299    /// The reset clears all timers and named callbacks. Static clocks also reset their stored time.
300    fn reset(&mut self);
301}
302
303impl dyn Clock {
304    /// Returns a reference to this clock as `Any` for downcasting.
305    pub fn as_any(&self) -> &dyn std::any::Any {
306        self
307    }
308
309    /// Returns a mutable reference to this clock as `Any` for downcasting.
310    pub fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
311        self
312    }
313}
314
315fn duration_to_nanos(duration: Duration) -> anyhow::Result<DurationNanos> {
316    DurationNanos::try_from(duration)
317        .map_err(|_| anyhow::anyhow!("Interval exceeds u64 nanoseconds"))
318}
319
320/// Registry for timer event callbacks.
321///
322/// Provides shared callback registration and retrieval logic used by both
323/// `VirtualClock` and `LiveClock`.
324#[derive(Debug, Default)]
325pub struct CallbackRegistry {
326    default_callback: Option<TimeEventCallback>,
327    callbacks: AHashMap<Ustr, TimeEventCallback>,
328}
329
330impl CallbackRegistry {
331    /// Creates an empty callback registry.
332    #[must_use]
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// Registers the callback used when no callback exists for a timer name.
338    pub fn register_default_handler(&mut self, callback: TimeEventCallback) {
339        self.default_callback = Some(callback);
340    }
341
342    /// Removes the default callback, preserving all named callbacks.
343    pub fn cancel_default_handler(&mut self) {
344        self.default_callback = None;
345    }
346
347    /// Registers a callback for `name`, replacing any existing callback for that name.
348    pub fn register_callback(&mut self, name: Ustr, callback: TimeEventCallback) {
349        self.callbacks.insert(name, callback);
350    }
351
352    /// Returns whether a named or default callback is available for `name`.
353    #[must_use]
354    pub fn has_any_callback(&self, name: &Ustr) -> bool {
355        self.callbacks.contains_key(name) || self.default_callback.is_some()
356    }
357
358    /// Returns the callback for `name`, falling back to the default callback.
359    #[must_use]
360    pub fn get_callback(&self, name: &Ustr) -> Option<TimeEventCallback> {
361        self.callbacks
362            .get(name)
363            .cloned()
364            .or_else(|| self.default_callback.clone())
365    }
366
367    /// Creates a handler for `event` using its named callback or the default callback.
368    ///
369    /// # Panics
370    ///
371    /// Panics if neither a named nor default callback exists for the event.
372    #[must_use]
373    pub fn get_handler(&self, event: TimeEvent) -> TimeEventHandler {
374        let callback = self
375            .get_callback(&event.name)
376            .unwrap_or_else(|| panic!("Event '{}' should have associated handler", event.name));
377
378        TimeEventHandler::new(event, callback)
379    }
380
381    /// Clears all named callbacks, preserving the default callback.
382    pub fn clear(&mut self) {
383        self.callbacks.clear();
384    }
385}
386
387/// Validates and normalizes parameters for a time alert.
388///
389/// `allow_past` defaults to `true`. When enabled, a past alert timestamp is replaced with
390/// `ts_now`. Returns the interned name and normalized alert timestamp.
391///
392/// # Errors
393///
394/// Returns an error if `name` is invalid or the alert is in the past when past alerts are
395/// disallowed.
396pub fn validate_and_prepare_time_alert(
397    name: &str,
398    mut alert_time_ns: UnixNanos,
399    allow_past: Option<bool>,
400    ts_now: UnixNanos,
401) -> anyhow::Result<(Ustr, UnixNanos)> {
402    check_valid_string_utf8(name, stringify!(name))?;
403
404    let name = Ustr::from(name);
405    let allow_past = allow_past.unwrap_or(true);
406
407    if alert_time_ns < ts_now {
408        if allow_past {
409            log::warn!(
410                "Timer '{name}' alert time {} was in the past, adjusted to current time for immediate firing",
411                alert_time_ns.to_rfc3339(),
412            );
413            alert_time_ns = ts_now;
414        } else {
415            anyhow::bail!(
416                "Timer '{name}' alert time {} was in the past (current time is {ts_now})",
417                alert_time_ns.to_rfc3339(),
418            );
419        }
420    }
421
422    Ok((name, alert_time_ns))
423}
424
425/// Validates and normalizes parameters for a fixed interval timer.
426///
427/// A missing or zero `start_time_ns` resolves to `ts_now`. `allow_past` defaults to `true`, and
428/// `fire_immediately` defaults to `false`. Returns the interned name, normalized start and stop
429/// times, and resolved flag values.
430///
431/// # Errors
432///
433/// Returns an error if:
434/// - `name` is invalid.
435/// - `interval_ns` is zero.
436/// - `start_time_ns + interval_ns` is out of range for [`UnixNanos`] when not firing immediately.
437/// - The first event is in the past when past times are disallowed.
438/// - The stop time is not after the normalized start time.
439/// - The stop time is not after `ts_now` when past times are disallowed.
440pub fn validate_and_prepare_timer(
441    name: &str,
442    interval_ns: DurationNanos,
443    start_time_ns: Option<UnixNanos>,
444    stop_time_ns: Option<UnixNanos>,
445    allow_past: Option<bool>,
446    fire_immediately: Option<bool>,
447    ts_now: UnixNanos,
448) -> anyhow::Result<(Ustr, UnixNanos, Option<UnixNanos>, bool, bool)> {
449    check_positive_u64(interval_ns.as_u64(), stringify!(interval_ns))?;
450    validate_and_prepare_schedule(
451        name,
452        &TimerInterval::Fixed(create_valid_interval(interval_ns)),
453        start_time_ns.filter(|start| *start != 0),
454        stop_time_ns,
455        allow_past,
456        fire_immediately,
457        ts_now,
458    )
459}
460
461// Validates native timer registration before replacing an existing timer
462pub(crate) fn validate_and_prepare_schedule(
463    name: &str,
464    interval: &TimerInterval,
465    start_time_ns: Option<UnixNanos>,
466    stop_time_ns: Option<UnixNanos>,
467    allow_past: Option<bool>,
468    fire_immediately: Option<bool>,
469    ts_now: UnixNanos,
470) -> anyhow::Result<(Ustr, UnixNanos, Option<UnixNanos>, bool, bool)> {
471    check_valid_string_utf8(name, stringify!(name))?;
472
473    let name = Ustr::from(name);
474    let allow_past = allow_past.unwrap_or(true);
475    let fire_immediately = fire_immediately.unwrap_or(false);
476
477    let start_time_ns = start_time_ns.unwrap_or(ts_now);
478    let next_event_time = if fire_immediately {
479        start_time_ns
480    } else {
481        interval
482            .next_time_after(start_time_ns, start_time_ns, &mut 0)
483            .ok_or_else(|| {
484                anyhow::anyhow!("Timer '{name}' first event time exceeds UnixNanos range")
485            })?
486    };
487
488    if !allow_past && next_event_time < ts_now {
489        anyhow::bail!(
490            "Timer '{name}' next event time {} would be in the past (current time is {ts_now})",
491            next_event_time.to_rfc3339(),
492        );
493    }
494
495    if let Some(stop_time) = stop_time_ns {
496        if stop_time <= start_time_ns {
497            anyhow::bail!(
498                "Timer '{name}' stop time {} must be after start time {}",
499                stop_time.to_rfc3339(),
500                start_time_ns.to_rfc3339(),
501            );
502        }
503
504        if !allow_past && stop_time <= ts_now {
505            anyhow::bail!(
506                "Timer '{name}' stop time {} is in the past (current time is {ts_now})",
507                stop_time.to_rfc3339(),
508            );
509        }
510    }
511
512    Ok((
513        name,
514        start_time_ns,
515        stop_time_ns,
516        allow_past,
517        fire_immediately,
518    ))
519}
520
521// Cancels and removes the active timer registered under `name`, if any.
522//
523// Shared by `VirtualClock` and `LiveClock` to enforce one active timer per name.
524pub(crate) fn replace_existing_timer<T: Timer>(timers: &mut BTreeMap<Ustr, T>, name: &Ustr) {
525    let Some(mut timer) = timers.remove(name) else {
526        return;
527    };
528
529    if timer.is_expired() {
530        return;
531    }
532
533    timer.cancel();
534    log::warn!("Timer '{name}' replaced");
535}