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