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}