Skip to main content

osproxy_core/
time.rs

1//! The clock seam, the foundation of deterministic time.
2//!
3//! Production code must never read wall-clock time directly: a hidden
4//! `Instant::now()` makes behavior depend on the machine and turns tests flaky.
5//! Instead, every component that needs time takes a [`Clock`]. Production wires
6//! [`SystemClock`]; tests wire [`ManualClock`] and advance it explicitly, so a
7//! timeout, a TTL, or an affinity expiry is reproducible to the nanosecond.
8//!
9//! This is enforced mechanically: `clippy.toml` bans `SystemTime::now`,
10//! `Instant::now`, and friends everywhere except [`SystemClock`], which is the
11//! single sanctioned place that touches the real clock (`docs/09`, `docs/12`).
12
13use std::sync::Mutex;
14use std::time::Duration;
15
16/// A monotonic instant, in nanoseconds since an unspecified epoch.
17///
18/// Opaque and only meaningful relative to another [`Instant`] from the same
19/// [`Clock`]. Comparable and subtractable; never convertible to wall-clock time
20/// (the proxy reasons about elapsed durations, not calendar time).
21#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
22pub struct Instant(u64);
23
24impl Instant {
25    /// Returns the duration elapsed from `earlier` to `self`, saturating at zero
26    /// if `earlier` is later (clocks are monotonic, so this should not happen,
27    /// but saturation keeps the type panic-free, NFR-R1).
28    #[must_use]
29    pub fn saturating_duration_since(self, earlier: Instant) -> Duration {
30        Duration::from_nanos(self.0.saturating_sub(earlier.0))
31    }
32
33    /// Returns the instant `delta` after `self`, saturating at the maximum.
34    #[must_use]
35    pub fn saturating_add(self, delta: Duration) -> Instant {
36        let nanos = u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX);
37        Instant(self.0.saturating_add(nanos))
38    }
39}
40
41/// A source of time. Inject this anywhere time is needed.
42pub trait Clock: Send + Sync {
43    /// The current monotonic instant (for elapsed-time logic).
44    fn now(&self) -> Instant;
45
46    /// The current **wall-clock** time in nanoseconds since the Unix epoch.
47    ///
48    /// Distinct from [`Clock::now`] (which is monotonic and meaningless as an
49    /// absolute time): this is for stamping externally-meaningful timestamps such
50    /// as OTLP span start/end. Like `now`, it goes through the seam so it stays
51    /// deterministic under a [`ManualClock`].
52    fn unix_nanos(&self) -> u64;
53}
54
55/// The production clock, backed by the operating system's monotonic timer.
56///
57/// This is the **only** type permitted to read the real clock.
58#[derive(Clone, Copy, Default, Debug)]
59pub struct SystemClock;
60
61impl Clock for SystemClock {
62    fn now(&self) -> Instant {
63        // Anchor to a process-lifetime epoch so values are stable u64 nanos.
64        static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
65        // SystemClock is the single sanctioned site permitted to read the OS
66        // clock; everything else takes a Clock so time stays deterministic.
67        #[allow(
68            clippy::disallowed_methods,
69            reason = "the one sanctioned site reading the OS monotonic clock (docs/12)"
70        )]
71        let (raw, epoch) = (
72            std::time::Instant::now(),
73            *EPOCH.get_or_init(std::time::Instant::now),
74        );
75        Instant(u64::try_from(raw.saturating_duration_since(epoch).as_nanos()).unwrap_or(u64::MAX))
76    }
77
78    fn unix_nanos(&self) -> u64 {
79        // The sanctioned site for reading the OS wall clock (see `now`).
80        #[allow(
81            clippy::disallowed_methods,
82            reason = "the one sanctioned site reading the OS wall clock (docs/12)"
83        )]
84        let since_epoch = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
85        since_epoch.map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
86    }
87}
88
89/// A test clock advanced explicitly. Starts at zero; never moves on its own.
90#[derive(Debug, Default)]
91pub struct ManualClock {
92    nanos: Mutex<u64>,
93}
94
95impl ManualClock {
96    /// Creates a clock reading zero.
97    #[must_use]
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Advances the clock by `delta`. Saturates at the maximum.
103    pub fn advance(&self, delta: Duration) {
104        let add = u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX);
105        if let Ok(mut nanos) = self.nanos.lock() {
106            *nanos = nanos.saturating_add(add);
107        }
108    }
109}
110
111impl Clock for ManualClock {
112    fn now(&self) -> Instant {
113        Instant(self.nanos.lock().map_or(0, |n| *n))
114    }
115
116    fn unix_nanos(&self) -> u64 {
117        // The test clock has no separate wall clock: it reports its advanced
118        // nanos as the absolute time, so OTLP timestamps stay deterministic.
119        self.nanos.lock().map_or(0, |n| *n)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn manual_clock_is_frozen_until_advanced() {
129        let clock = ManualClock::new();
130        let t0 = clock.now();
131        assert_eq!(clock.now(), t0, "clock must not advance on its own");
132        clock.advance(Duration::from_millis(250));
133        let t1 = clock.now();
134        assert_eq!(t1.saturating_duration_since(t0), Duration::from_millis(250));
135    }
136
137    #[test]
138    fn instant_arithmetic_saturates_and_does_not_panic() {
139        let clock = ManualClock::new();
140        let t0 = clock.now();
141        let later = t0.saturating_add(Duration::from_secs(5));
142        assert_eq!(later.saturating_duration_since(t0), Duration::from_secs(5));
143        // Reverse subtraction saturates to zero rather than panicking.
144        assert_eq!(t0.saturating_duration_since(later), Duration::ZERO);
145    }
146
147    #[test]
148    fn system_clock_is_monotonic() {
149        let clock = SystemClock;
150        let a = clock.now();
151        let b = clock.now();
152        assert!(b >= a, "monotonic clock must not go backwards");
153    }
154}