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