Skip to main content

pylon_kernel/
clock.rs

1//! Clock abstraction for testable time-dependent logic.
2//!
3//! Code that needs the current time should accept a `&dyn Clock` instead of
4//! calling `SystemTime::now()` directly. Production callers pass
5//! `SystemClock`; tests pass `MockClock` and advance it explicitly.
6//!
7//! Both `now_unix_secs()` (wall clock) and `now_monotonic()` (monotonic) are
8//! exposed because they have different uses:
9//! - wall clock for timestamps stored in the database (session expiry,
10//!   `createdAt`)
11//! - monotonic for measuring elapsed time (rate-limiter windows, tick
12//!   scheduling) — wall clock can jump backward via NTP
13
14use std::sync::Mutex;
15use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
16
17pub trait Clock: Send + Sync {
18    /// Current wall-clock time as Unix epoch seconds.
19    fn now_unix_secs(&self) -> u64;
20
21    /// Current wall-clock time as Unix epoch milliseconds.
22    fn now_unix_millis(&self) -> u64 {
23        self.now_unix_secs() * 1000
24    }
25
26    /// A monotonically increasing instant for measuring durations.
27    /// Implementations may return synthetic instants that share a base.
28    fn now_monotonic(&self) -> Instant;
29}
30
31/// Clock backed by `std::time` — the production default.
32#[derive(Debug, Default, Clone, Copy)]
33pub struct SystemClock;
34
35impl Clock for SystemClock {
36    fn now_unix_secs(&self) -> u64 {
37        SystemTime::now()
38            .duration_since(UNIX_EPOCH)
39            .map(|d| d.as_secs())
40            .unwrap_or(0)
41    }
42
43    fn now_unix_millis(&self) -> u64 {
44        SystemTime::now()
45            .duration_since(UNIX_EPOCH)
46            .map(|d| d.as_millis() as u64)
47            .unwrap_or(0)
48    }
49
50    fn now_monotonic(&self) -> Instant {
51        Instant::now()
52    }
53}
54
55/// Test clock with manual advancement.
56///
57/// Both wall and monotonic time advance together. Start at any Unix time and
58/// step forward with `advance(duration)`.
59pub struct MockClock {
60    inner: Mutex<MockState>,
61}
62
63struct MockState {
64    unix_millis: u64,
65    base_instant: Instant,
66    elapsed: Duration,
67}
68
69impl MockClock {
70    pub fn new(start_unix_secs: u64) -> Self {
71        Self {
72            inner: Mutex::new(MockState {
73                unix_millis: start_unix_secs * 1000,
74                base_instant: Instant::now(),
75                elapsed: Duration::ZERO,
76            }),
77        }
78    }
79
80    pub fn advance(&self, by: Duration) {
81        let mut s = self.inner.lock().expect("MockClock poisoned");
82        s.unix_millis += by.as_millis() as u64;
83        s.elapsed += by;
84    }
85}
86
87impl Clock for MockClock {
88    fn now_unix_secs(&self) -> u64 {
89        self.inner.lock().expect("MockClock poisoned").unix_millis / 1000
90    }
91
92    fn now_unix_millis(&self) -> u64 {
93        self.inner.lock().expect("MockClock poisoned").unix_millis
94    }
95
96    fn now_monotonic(&self) -> Instant {
97        let s = self.inner.lock().expect("MockClock poisoned");
98        s.base_instant + s.elapsed
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn system_clock_returns_nonzero() {
108        let c = SystemClock;
109        assert!(c.now_unix_secs() > 1_700_000_000);
110    }
111
112    #[test]
113    fn mock_clock_starts_at_given_time() {
114        let c = MockClock::new(1_000_000);
115        assert_eq!(c.now_unix_secs(), 1_000_000);
116        assert_eq!(c.now_unix_millis(), 1_000_000_000);
117    }
118
119    #[test]
120    fn mock_clock_advances_wall_and_monotonic_together() {
121        let c = MockClock::new(0);
122        let m0 = c.now_monotonic();
123        c.advance(Duration::from_secs(60));
124        assert_eq!(c.now_unix_secs(), 60);
125        let m1 = c.now_monotonic();
126        assert_eq!(m1.duration_since(m0), Duration::from_secs(60));
127    }
128
129    #[test]
130    fn dyn_clock_is_object_safe() {
131        fn use_it(c: &dyn Clock) -> u64 {
132            c.now_unix_secs()
133        }
134        assert_eq!(use_it(&MockClock::new(42)), 42);
135    }
136}