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}