nixfleet_proto/
clock.rs

1//! Clock abstraction.
2//!
3//! Production code threads `ClockHandle` through constructors so tests can
4//! drive time deterministically. The pure reducer (RFC-0006 §3) takes
5//! `now: DateTime<Utc>` as a parameter and never reads it from a global —
6//! consumers obtain `now` from a `Clock` at the impure boundary and pass it
7//! down.
8//!
9//! Two methods on the trait:
10//! - `now()` — wallclock `DateTime<Utc>`, agent-reported into events
11//!   (RFC-0005 §4.2). Subject to NTP step-back.
12//! - `monotonic_instant()` — `std::time::Instant`, for measuring elapsed
13//!   durations (sustained-failure threshold, RFC-0005 §6). Never moves
14//!   backwards under NTP step; `FakeClock` preserves this property too.
15
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18
19use chrono::{DateTime, Utc};
20
21pub type ClockHandle = Arc<dyn Clock>;
22
23pub trait Clock: Send + Sync {
24    fn now(&self) -> DateTime<Utc>;
25    fn monotonic_instant(&self) -> Instant;
26}
27
28pub struct SystemClock;
29
30impl SystemClock {
31    pub fn new() -> Self {
32        Self
33    }
34}
35
36impl Default for SystemClock {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Clock for SystemClock {
43    fn now(&self) -> DateTime<Utc> {
44        Utc::now()
45    }
46
47    fn monotonic_instant(&self) -> Instant {
48        Instant::now()
49    }
50}
51
52pub struct FakeClock {
53    state: Mutex<FakeState>,
54}
55
56struct FakeState {
57    wall: DateTime<Utc>,
58    monotonic: Instant,
59}
60
61impl FakeClock {
62    pub fn new(initial_wall: DateTime<Utc>) -> Self {
63        Self {
64            state: Mutex::new(FakeState {
65                wall: initial_wall,
66                monotonic: Instant::now(),
67            }),
68        }
69    }
70
71    /// Advance both wallclock and monotonic by the same duration.
72    pub fn advance(&self, by: Duration) {
73        let mut s = self.state.lock().expect("FakeClock state poisoned");
74        s.wall += chrono::Duration::from_std(by).expect("FakeClock advance overflow");
75        s.monotonic += by;
76    }
77
78    /// Set the wallclock absolutely. Does not affect monotonic — same shape
79    /// as a real NTP step: wall jumps, monotonic doesn't.
80    pub fn set(&self, wall: DateTime<Utc>) {
81        let mut s = self.state.lock().expect("FakeClock state poisoned");
82        s.wall = wall;
83    }
84}
85
86impl Clock for FakeClock {
87    fn now(&self) -> DateTime<Utc> {
88        self.state.lock().expect("FakeClock state poisoned").wall
89    }
90
91    fn monotonic_instant(&self) -> Instant {
92        self.state
93            .lock()
94            .expect("FakeClock state poisoned")
95            .monotonic
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn t0() -> DateTime<Utc> {
104        "2026-05-16T00:00:00Z".parse().unwrap()
105    }
106
107    #[test]
108    fn advance_moves_wall_and_monotonic_together() {
109        let c = FakeClock::new(t0());
110        let m0 = c.monotonic_instant();
111        c.advance(Duration::from_secs(60));
112        assert_eq!(c.now() - t0(), chrono::Duration::seconds(60));
113        assert_eq!(c.monotonic_instant() - m0, Duration::from_secs(60));
114    }
115
116    #[test]
117    fn set_moves_wall_only() {
118        let c = FakeClock::new(t0());
119        let m0 = c.monotonic_instant();
120        let later = t0() + chrono::Duration::hours(1);
121        c.set(later);
122        assert_eq!(c.now(), later);
123        assert_eq!(c.monotonic_instant(), m0);
124    }
125
126    #[test]
127    fn monotonic_never_regresses_across_advances() {
128        let c = FakeClock::new(t0());
129        let m0 = c.monotonic_instant();
130        c.advance(Duration::from_secs(30));
131        let m1 = c.monotonic_instant();
132        c.advance(Duration::from_secs(30));
133        let m2 = c.monotonic_instant();
134        assert!(m1 > m0);
135        assert!(m2 > m1);
136    }
137
138    #[test]
139    fn system_clock_now_tracks_real_now() {
140        let c = SystemClock::new();
141        let drift = (Utc::now() - c.now()).num_milliseconds().abs();
142        assert!(drift < 1000, "SystemClock drift {drift}ms");
143    }
144
145    #[test]
146    fn fake_clock_dispatches_via_trait_object() {
147        let fake = Arc::new(FakeClock::new(t0()));
148        let handle: ClockHandle = fake.clone();
149        assert_eq!(handle.now(), t0());
150        fake.advance(Duration::from_secs(10));
151        assert_eq!(handle.now() - t0(), chrono::Duration::seconds(10));
152    }
153}