Skip to main content

clock_kit/
system.rs

1//! `SystemClock` — real wall+monotonic reads (`std` or `libc` path).
2//!
3//! Wall: `SystemTime` under `std`, `clock_gettime(CLOCK_REALTIME)` under
4//! core+libc. Monotonic: an `Instant`-anchored counter under `std`,
5//! `clock_gettime(CLOCK_MONOTONIC)` under libc. Both readings pass through
6//! the process-wide monotonic floor (REQ-CLK-001) and clamp (never panic)
7//! on misconfigured machines.
8
9use crate::clock::Clock;
10use crate::monotonic::mono_floor;
11use crate::quality::{ClockSource, SyncQuality};
12use crate::timestamp::{Timestamp, WallTime};
13
14#[cfg(all(feature = "std", feature = "ptp"))]
15use core::sync::atomic::{AtomicU8, Ordering};
16#[cfg(all(not(feature = "std"), feature = "libc"))]
17use libc::{CLOCK_MONOTONIC, CLOCK_REALTIME};
18#[cfg(feature = "std")]
19use std::sync::OnceLock;
20#[cfg(feature = "std")]
21use std::time::Instant;
22
23/// Real system clock (REQ-CLK-001/002). Zero-sized; `Clock` is implemented
24/// on a shared reference so one instance can be handed across threads.
25///
26/// With the `ptp` feature, `quality()` lazily runs
27/// [`detect_sync_quality`](crate::ptp::detect_sync_quality) once and caches
28/// it process-wide (G2: advisory, read at discovery time).
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
30pub struct SystemClock;
31
32impl SystemClock {
33    /// Create the clock.
34    #[must_use]
35    pub const fn new() -> Self {
36        Self
37    }
38}
39
40impl Clock for SystemClock {
41    fn now(&self) -> Timestamp {
42        Timestamp {
43            wall: WallTime::from_unix_nanos(wall_nanos()),
44            mono: mono_floor(monotonic_nanos()),
45        }
46    }
47
48    fn quality(&self) -> SyncQuality {
49        #[cfg(feature = "ptp")]
50        {
51            cached_quality()
52        }
53        #[cfg(not(feature = "ptp"))]
54        {
55            // Nothing detected → fail-closed to distrust (G2).
56            SyncQuality::Fallback
57        }
58    }
59
60    fn source(&self) -> ClockSource {
61        #[cfg(feature = "std")]
62        {
63            ClockSource::Instant
64        }
65        #[cfg(not(feature = "std"))]
66        {
67            ClockSource::LibcMono
68        }
69    }
70}
71
72/// Wall nanos since the Unix epoch (0 on pre-epoch misconfiguration).
73pub(crate) fn wall_nanos() -> u64 {
74    #[cfg(feature = "std")]
75    {
76        crate::timestamp::WallTime::from_system_now().unix_nanos()
77    }
78    #[cfg(all(not(feature = "std"), feature = "libc"))]
79    {
80        crate::low::clock_gettime_ns(CLOCK_REALTIME).unwrap_or(0)
81    }
82}
83
84/// Monotonic nanos: `Instant` elapsed since a process-wide anchor under
85/// `std`; `CLOCK_MONOTONIC` under libc.
86pub(crate) fn monotonic_nanos() -> u64 {
87    #[cfg(feature = "std")]
88    {
89        static ANCHOR: OnceLock<Instant> = OnceLock::new();
90        let anchor = ANCHOR.get_or_init(Instant::now);
91        u64::try_from(anchor.elapsed().as_nanos()).unwrap_or(u64::MAX)
92    }
93    #[cfg(all(not(feature = "std"), feature = "libc"))]
94    {
95        crate::low::clock_gettime_ns(CLOCK_MONOTONIC).unwrap_or(0)
96    }
97}
98
99#[cfg(all(not(feature = "std"), feature = "libc"))]
100use libc::{CLOCK_MONOTONIC, CLOCK_REALTIME};
101
102#[cfg(feature = "ptp")]
103pub(crate) fn cached_quality() -> SyncQuality {
104    // 0 = undetected, 1..=3 = SyncQuality discriminants.
105    static CACHE: AtomicU8 = AtomicU8::new(0);
106    let seen = CACHE.load(Ordering::Acquire);
107    if seen != 0 {
108        return num_to_quality(seen);
109    }
110    let detected = crate::ptp::detect_sync_quality();
111    CACHE.store(discriminant(detected), Ordering::Release);
112    detected
113}
114
115#[cfg(feature = "ptp")]
116fn discriminant(q: SyncQuality) -> u8 {
117    match q {
118        SyncQuality::Ptp => 1,
119        SyncQuality::Ntp => 2,
120        SyncQuality::Fallback => 3,
121    }
122}
123
124#[cfg(feature = "ptp")]
125fn num_to_quality(n: u8) -> SyncQuality {
126    match n {
127        1 => SyncQuality::Ptp,
128        2 => SyncQuality::Ntp,
129        _ => SyncQuality::Fallback,
130    }
131}
132
133// Touches the OS clock (via `now()`) — out of miri scope (spec §Gate plan).
134#[cfg(all(test, feature = "std", not(miri)))]
135mod tests {
136    use crate::clock::Clock;
137    use crate::quality::{ClockSource, SyncQuality};
138
139    /// The advisory surface is total: `quality()` returns a classification
140    /// (and caches it — second call hits the same value), `source()` reports
141    /// the active reference path. REQ-CLK-004/003 observability.
142    /// Reads the OS clock — out of miri scope (spec §Gate plan).
143    #[cfg(not(miri))]
144    #[test]
145    fn advisory_surface_is_total_and_cached() {
146        let clock = crate::SystemClock::new();
147        let q1 = clock.quality();
148        let q2 = clock.quality();
149        assert_eq!(q1, q2, "cached quality must be stable");
150        assert!(matches!(
151            q1,
152            SyncQuality::Ptp | SyncQuality::Ntp | SyncQuality::Fallback
153        ));
154
155        #[cfg(feature = "ptp")]
156        assert_eq!(q1, crate::ptp::detect_sync_quality());
157        #[cfg(not(feature = "ptp"))]
158        assert_eq!(q1, SyncQuality::Fallback);
159
160        #[cfg(feature = "std")]
161        assert_eq!(clock.source(), ClockSource::Instant);
162        assert!(matches!(
163            clock.source(),
164            ClockSource::Instant | ClockSource::LibcMono
165        ));
166
167        // now() remains monotone alongside the advisory calls.
168        let a = clock.now();
169        let b = clock.now();
170        assert!(b.mono >= a.mono);
171        assert!(b.wall.unix_nanos() >= a.wall.unix_nanos() || a.mono == 0);
172    }
173}