Skip to main content

harn_clock/
lib.rs

1//! Unified `Clock` trait for the Harn runtime.
2//!
3//! Production code that needs the current time, monotonic measurement, or a
4//! cancellable sleep takes an `Arc<dyn Clock>` and reads through it. Real
5//! deployments wire [`RealClock`]; tests substitute [`PausedClock`] to drive
6//! virtual time deterministically; recording layers wrap any inner clock with
7//! [`RecordedClock`] to capture every observation for replay.
8//!
9//! See `docs/src/dev/testing.md` for usage patterns.
10
11use std::fmt;
12use std::sync::Arc;
13use std::time::Duration;
14
15use async_trait::async_trait;
16use parking_lot::Mutex;
17use serde::{Deserialize, Serialize};
18use time::OffsetDateTime;
19use tokio::sync::Notify;
20
21/// Unified clock abstraction.
22///
23/// All time observations and sleeps in production-facing Harn code should
24/// route through a `Clock`. Cron, the trigger dispatcher, the stdlib
25/// `now_ms` / `sleep_ms` builtins, and the `OrchestratorHarness` all accept
26/// `Arc<dyn Clock>` so test harnesses can swap in [`PausedClock`].
27#[async_trait]
28pub trait Clock: Send + Sync + fmt::Debug {
29    /// Current wall-clock UTC time.
30    fn now_utc(&self) -> OffsetDateTime;
31
32    /// Monotonic milliseconds since an implementation-defined origin.
33    ///
34    /// Required to be non-decreasing across calls on the same `Clock`
35    /// instance. Used by the stdlib `monotonic_ms` / `elapsed` builtins and
36    /// any code measuring durations across operations.
37    fn monotonic_ms(&self) -> i64;
38
39    /// Sleep for `duration`. No-op when `duration` is zero.
40    async fn sleep(&self, duration: Duration);
41
42    /// Sleep until the wall-clock UTC `deadline`. No-op if `deadline` is in
43    /// the past.
44    async fn sleep_until_utc(&self, deadline: OffsetDateTime);
45}
46
47/// Convert an `OffsetDateTime` to Unix epoch milliseconds. The division
48/// happens in `i128` so timestamps past April 2262 — where nanoseconds
49/// exceed `i64::MAX` — don't silently corrupt when cast.
50pub fn offset_datetime_to_ms(ts: OffsetDateTime) -> i64 {
51    (ts.unix_timestamp_nanos() / 1_000_000) as i64
52}
53
54/// Convenience: current wall-clock millis since UNIX_EPOCH.
55pub fn now_wall_ms(clock: &dyn Clock) -> i64 {
56    offset_datetime_to_ms(clock.now_utc())
57}
58
59// ── RFC3339 rendering ──────────────────────────────────────────────────────────
60
61/// Rendered fallback for the unreachable formatting failure in
62/// [`format_rfc3339`]. Chosen over an empty string so consumers that parse
63/// these timestamps back always receive syntactically valid RFC3339.
64const EPOCH_RFC3339: &str = "1970-01-01T00:00:00Z";
65
66/// Render `ts` as an RFC3339 timestamp.
67///
68/// Returns `String` rather than `Result` deliberately. `time`'s RFC3339
69/// formatter only fails for years outside `0000..=9999` or for UTC offsets
70/// with sub-minute precision — neither is reachable for a wall-clock UTC
71/// `OffsetDateTime` produced by a [`Clock`]. Making every call site thread an
72/// error it can never observe bought nothing, so the unreachable branch
73/// resolves to [`EPOCH_RFC3339`] instead of panicking on `unwrap`.
74pub fn format_rfc3339(ts: OffsetDateTime) -> String {
75    ts.format(&time::format_description::well_known::Rfc3339)
76        .unwrap_or_else(|_| EPOCH_RFC3339.to_string())
77}
78
79/// Current wall-clock UTC time as RFC3339, read through `clock`.
80///
81/// Prefer this over [`system_now_rfc3339`] wherever a `Clock` is already in
82/// scope: it makes the timestamp deterministic under [`PausedClock`].
83pub fn now_rfc3339(clock: &dyn Clock) -> String {
84    format_rfc3339(clock.now_utc())
85}
86
87/// Current wall-clock UTC time as RFC3339, read straight from the system
88/// clock.
89///
90/// The escape hatch for call sites that have no [`Clock`] in scope and where
91/// threading one through would mean invasive plumbing. Anything that *can*
92/// reach a `Clock` should call [`now_rfc3339`] instead so tests can pause it.
93pub fn system_now_rfc3339() -> String {
94    format_rfc3339(OffsetDateTime::now_utc())
95}
96
97// ── Real clock ─────────────────────────────────────────────────────────────────
98
99/// Production clock. Reads `OffsetDateTime::now_utc()` and
100/// `tokio::time::sleep`. Honors `tokio::time::pause()` for sleep-driven
101/// scheduling but `now_utc` always returns true wall time.
102pub struct RealClock {
103    monotonic_origin: tokio::time::Instant,
104}
105
106impl Default for RealClock {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl fmt::Debug for RealClock {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.debug_struct("RealClock").finish()
115    }
116}
117
118impl RealClock {
119    pub fn new() -> Self {
120        Self {
121            monotonic_origin: tokio::time::Instant::now(),
122        }
123    }
124
125    /// Convenience: wrap in an `Arc` for handing to consumers.
126    pub fn arc() -> Arc<dyn Clock> {
127        Arc::new(Self::new())
128    }
129}
130
131#[async_trait]
132impl Clock for RealClock {
133    fn now_utc(&self) -> OffsetDateTime {
134        OffsetDateTime::now_utc()
135    }
136
137    fn monotonic_ms(&self) -> i64 {
138        let elapsed = tokio::time::Instant::now().saturating_duration_since(self.monotonic_origin);
139        elapsed.as_millis() as i64
140    }
141
142    async fn sleep(&self, duration: Duration) {
143        if duration.is_zero() {
144            return;
145        }
146        tokio::time::sleep(duration).await;
147    }
148
149    async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
150        let now = self.now_utc();
151        if deadline <= now {
152            return;
153        }
154        let delta = deadline - now;
155        let Ok(duration) = Duration::try_from(delta) else {
156            return;
157        };
158        tokio::time::sleep(duration).await;
159    }
160}
161
162// ── Paused clock ───────────────────────────────────────────────────────────────
163
164/// Fully-virtual clock for tests.
165///
166/// Stores its own wall-clock cursor and a monotonic counter. Sleeps suspend on
167/// an internal `Notify` and wake when [`PausedClock::advance`] or
168/// [`PausedClock::set`] crosses the deadline. No tokio-runtime cooperation is
169/// required: `PausedClock` works inside `current_thread`, `multi_thread`, and
170/// `start_paused` runtimes equally well.
171///
172/// Pairs with `tokio::time::pause()` for tests that mix this clock with code
173/// that uses `tokio::time::sleep` directly (e.g. `tokio::time::timeout`).
174pub struct PausedClock {
175    state: Mutex<PausedState>,
176    notify: Notify,
177}
178
179struct PausedState {
180    wall: OffsetDateTime,
181    monotonic: Duration,
182}
183
184impl fmt::Debug for PausedClock {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        let state = self.state.lock();
187        f.debug_struct("PausedClock")
188            .field("wall", &state.wall)
189            .field("monotonic_ms", &state.monotonic.as_millis())
190            .finish()
191    }
192}
193
194impl PausedClock {
195    /// Build a paused clock pinned at `origin`.
196    pub fn new(origin: OffsetDateTime) -> Arc<Self> {
197        Arc::new(Self {
198            state: Mutex::new(PausedState {
199                wall: origin,
200                monotonic: Duration::ZERO,
201            }),
202            notify: Notify::new(),
203        })
204    }
205
206    /// Advance wall + monotonic by `duration` and wake any sleepers whose
207    /// deadlines are now in the past.
208    pub fn advance(&self, duration: Duration) {
209        let delta = match time::Duration::try_from(duration) {
210            Ok(value) => value,
211            Err(_) => return,
212        };
213        {
214            let mut state = self.state.lock();
215            state.wall += delta;
216            state.monotonic = state.monotonic.saturating_add(duration);
217        }
218        self.notify.notify_waiters();
219    }
220
221    /// Advance using a `time::Duration`. Negative durations are clamped to zero.
222    pub fn advance_time(&self, duration: time::Duration) {
223        let Ok(positive) = Duration::try_from(duration) else {
224            return;
225        };
226        self.advance(positive);
227    }
228
229    /// Step `ticks` times by `tick`, notifying sleepers between every step so
230    /// tasks observing intermediate wake-ups see each notification.
231    pub fn advance_ticks(&self, ticks: u32, tick: Duration) {
232        for _ in 0..ticks {
233            self.advance(tick);
234        }
235    }
236
237    /// Pin the wall clock to `wall`. Monotonic counter advances by the
238    /// (signed) delta, never moving backwards.
239    pub fn set(&self, wall: OffsetDateTime) {
240        {
241            let mut state = self.state.lock();
242            let delta = wall - state.wall;
243            state.wall = wall;
244            if delta.is_positive() {
245                if let Ok(positive) = Duration::try_from(delta) {
246                    state.monotonic = state.monotonic.saturating_add(positive);
247                }
248            }
249        }
250        self.notify.notify_waiters();
251    }
252}
253
254#[async_trait]
255impl Clock for PausedClock {
256    fn now_utc(&self) -> OffsetDateTime {
257        self.state.lock().wall
258    }
259
260    fn monotonic_ms(&self) -> i64 {
261        self.state.lock().monotonic.as_millis() as i64
262    }
263
264    async fn sleep(&self, duration: Duration) {
265        if duration.is_zero() {
266            return;
267        }
268        let deadline = self.state.lock().wall
269            + time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO);
270        self.sleep_until_utc(deadline).await;
271    }
272
273    async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
274        loop {
275            let notified = self.notify.notified();
276            if self.state.lock().wall >= deadline {
277                return;
278            }
279            notified.await;
280        }
281    }
282}
283
284// ── Recorded clock ─────────────────────────────────────────────────────────────
285
286/// Single observation captured by [`RecordedClock`].
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(tag = "kind", rename_all = "snake_case")]
289pub enum ClockEvent {
290    NowUtc { wall_ns: i128 },
291    MonotonicMs { value: i64 },
292    Sleep { duration_ms: u64 },
293    SleepUntil { wall_ns: i128 },
294}
295
296/// In-memory append-only log of clock observations.
297#[derive(Debug, Default)]
298pub struct ClockEventLog {
299    events: Mutex<Vec<ClockEvent>>,
300}
301
302impl ClockEventLog {
303    pub fn new() -> Self {
304        Self::default()
305    }
306
307    /// Append an event. Called from `RecordedClock`; tests can also seed
308    /// expected events when building a replay oracle.
309    pub fn push(&self, event: ClockEvent) {
310        self.events.lock().push(event);
311    }
312
313    /// Snapshot of events recorded so far.
314    pub fn snapshot(&self) -> Vec<ClockEvent> {
315        self.events.lock().clone()
316    }
317
318    /// Number of events recorded.
319    pub fn len(&self) -> usize {
320        self.events.lock().len()
321    }
322
323    pub fn is_empty(&self) -> bool {
324        self.len() == 0
325    }
326}
327
328/// Wraps an inner [`Clock`] and records every observation to a
329/// [`ClockEventLog`].
330///
331/// The recording is the substrate the testbench replay/recording feature
332/// (#1441) builds on. It is deliberately scoped to the clock surface; other
333/// I/O substrates record separately.
334pub struct RecordedClock {
335    inner: Arc<dyn Clock>,
336    log: Arc<ClockEventLog>,
337}
338
339impl fmt::Debug for RecordedClock {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        f.debug_struct("RecordedClock")
342            .field("inner", &self.inner)
343            .field("events", &self.log.len())
344            .finish()
345    }
346}
347
348impl RecordedClock {
349    pub fn new(inner: Arc<dyn Clock>, log: Arc<ClockEventLog>) -> Self {
350        Self { inner, log }
351    }
352
353    pub fn log(&self) -> Arc<ClockEventLog> {
354        self.log.clone()
355    }
356}
357
358#[async_trait]
359impl Clock for RecordedClock {
360    fn now_utc(&self) -> OffsetDateTime {
361        let value = self.inner.now_utc();
362        self.log.push(ClockEvent::NowUtc {
363            wall_ns: value.unix_timestamp_nanos(),
364        });
365        value
366    }
367
368    fn monotonic_ms(&self) -> i64 {
369        let value = self.inner.monotonic_ms();
370        self.log.push(ClockEvent::MonotonicMs { value });
371        value
372    }
373
374    async fn sleep(&self, duration: Duration) {
375        self.log.push(ClockEvent::Sleep {
376            duration_ms: duration.as_millis() as u64,
377        });
378        self.inner.sleep(duration).await;
379    }
380
381    async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
382        self.log.push(ClockEvent::SleepUntil {
383            wall_ns: deadline.unix_timestamp_nanos(),
384        });
385        self.inner.sleep_until_utc(deadline).await;
386    }
387}
388
389// ── Tests ──────────────────────────────────────────────────────────────────────
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    fn epoch() -> OffsetDateTime {
396        OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap()
397    }
398
399    #[tokio::test]
400    async fn real_clock_returns_increasing_monotonic() {
401        let clock = RealClock::new();
402        let a = clock.monotonic_ms();
403        tokio::task::yield_now().await;
404        let b = clock.monotonic_ms();
405        assert!(b >= a, "monotonic must not move backwards");
406    }
407
408    #[tokio::test]
409    async fn paused_clock_pins_wall_and_monotonic_until_advanced() {
410        let clock = PausedClock::new(epoch());
411        assert_eq!(clock.now_utc(), epoch());
412        assert_eq!(clock.monotonic_ms(), 0);
413        clock.advance(Duration::from_millis(250));
414        assert_eq!(clock.monotonic_ms(), 250);
415        assert_eq!(clock.now_utc(), epoch() + time::Duration::milliseconds(250));
416    }
417
418    #[tokio::test]
419    async fn paused_clock_sleep_resumes_after_advance() {
420        let clock = PausedClock::new(epoch());
421        let clock_for_sleep = clock.clone();
422        let task = tokio::spawn(async move {
423            clock_for_sleep.sleep(Duration::from_secs(5)).await;
424        });
425        tokio::task::yield_now().await;
426        assert!(!task.is_finished(), "sleep should still be pending");
427        clock.advance(Duration::from_secs(10));
428        task.await.expect("sleep task panicked");
429    }
430
431    #[tokio::test]
432    async fn paused_clock_sleep_until_returns_immediately_for_past_deadline() {
433        let clock = PausedClock::new(epoch());
434        clock.advance(Duration::from_mins(1));
435        clock.sleep_until_utc(epoch()).await;
436    }
437
438    #[tokio::test]
439    async fn recorded_clock_appends_one_event_per_call() {
440        let log = Arc::new(ClockEventLog::new());
441        let clock = RecordedClock::new(PausedClock::new(epoch()), log.clone());
442        let _ = clock.now_utc();
443        let _ = clock.monotonic_ms();
444        clock.sleep(Duration::ZERO).await;
445        clock.sleep_until_utc(epoch()).await;
446        let events = log.snapshot();
447        assert_eq!(events.len(), 4);
448        assert!(matches!(events[0], ClockEvent::NowUtc { .. }));
449        assert!(matches!(events[1], ClockEvent::MonotonicMs { .. }));
450        assert!(matches!(events[2], ClockEvent::Sleep { duration_ms: 0 }));
451        assert!(matches!(events[3], ClockEvent::SleepUntil { .. }));
452    }
453
454    #[tokio::test]
455    async fn now_wall_ms_helper_matches_clock_observation() {
456        let clock = PausedClock::new(epoch());
457        let observed = now_wall_ms(clock.as_ref());
458        let expected = epoch().unix_timestamp_nanos() / 1_000_000;
459        assert_eq!(observed, expected as i64);
460    }
461
462    #[test]
463    fn format_rfc3339_renders_utc_with_z_suffix() {
464        assert_eq!(format_rfc3339(epoch()), "2023-11-14T22:13:20Z");
465    }
466
467    #[tokio::test]
468    async fn now_rfc3339_is_deterministic_under_a_paused_clock() {
469        // The whole point of routing the ~20 hand-rolled `now_rfc3339` copies
470        // through here: a paused clock now pins the rendered timestamp.
471        let clock = PausedClock::new(epoch());
472        assert_eq!(now_rfc3339(clock.as_ref()), "2023-11-14T22:13:20Z");
473        clock.advance(Duration::from_secs(1));
474        assert_eq!(now_rfc3339(clock.as_ref()), "2023-11-14T22:13:21Z");
475    }
476
477    #[test]
478    fn system_now_rfc3339_is_parseable_back() {
479        let rendered = system_now_rfc3339();
480        OffsetDateTime::parse(&rendered, &time::format_description::well_known::Rfc3339)
481            .expect("system_now_rfc3339 must emit valid RFC3339");
482    }
483}