Skip to main content

minip2p_platform/
clock.rs

1use crate::Deadline;
2
3/// A single time sample handed to caller-driven components.
4///
5/// Hosts take one sample per drive iteration and pass the same value to every
6/// agent, transport, and runtime they poll, so all of them observe a consistent
7/// "now".
8///
9/// # Monotonic time
10///
11/// [`monotonic_ms`](Self::monotonic_ms) counts milliseconds from an epoch
12/// chosen by the [`Clock`] that produced it. The epoch is arbitrary and carries
13/// no meaning across clocks: only differences between samples from the *same*
14/// clock are meaningful. Samples from one clock never decrease, and never
15/// exceed [`MAX_MONOTONIC_MS`](Self::MAX_MONOTONIC_MS).
16///
17/// # Wall-clock time
18///
19/// [`unix_seconds`](Self::unix_seconds) is `None` when the platform offers no
20/// usable wall-clock reading: it has no wall-clock source at all, such as an
21/// embedded board without an RTC or NTP sync, or its source currently reads
22/// before the Unix epoch. Components that need real time (signed beacon
23/// freshness, certificate validity) must handle its absence explicitly rather
24/// than substituting monotonic time, which is not comparable across peers.
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub struct Now {
27    /// Milliseconds since this clock's arbitrary epoch. Never decreases, and
28    /// never exceeds [`MAX_MONOTONIC_MS`](Self::MAX_MONOTONIC_MS).
29    pub monotonic_ms: u64,
30    /// Seconds since the Unix epoch, or `None` if the platform has no usable
31    /// wall-clock reading.
32    pub unix_seconds: Option<u64>,
33}
34
35impl Now {
36    /// The largest monotonic value a clock may report.
37    ///
38    /// `u64::MAX` is reserved as the end of the timeline: it is the value
39    /// [`Deadline::NEVER`] occupies, so an instant there could not be expressed
40    /// as a deadline that is due. Clocks saturate here instead, which at
41    /// millisecond resolution costs one millisecond after ~584 million years of
42    /// uptime.
43    pub const MAX_MONOTONIC_MS: u64 = u64::MAX - 1;
44
45    /// Creates a sample with monotonic time only and no wall clock.
46    pub const fn from_millis(monotonic_ms: u64) -> Self {
47        Self {
48            monotonic_ms,
49            unix_seconds: None,
50        }
51    }
52
53    /// Creates a sample carrying both monotonic and wall-clock time.
54    pub const fn new(monotonic_ms: u64, unix_seconds: u64) -> Self {
55        Self {
56            monotonic_ms,
57            unix_seconds: Some(unix_seconds),
58        }
59    }
60
61    /// Returns this sample with the given wall-clock time attached.
62    pub const fn with_unix_seconds(self, unix_seconds: u64) -> Self {
63        Self {
64            unix_seconds: Some(unix_seconds),
65            ..self
66        }
67    }
68
69    /// Returns the milliseconds elapsed since `earlier`, saturating at zero.
70    ///
71    /// Both samples must come from the same clock; comparing across clocks is
72    /// meaningless because their epochs are unrelated.
73    pub const fn saturating_millis_since(self, earlier: Self) -> u64 {
74        self.monotonic_ms.saturating_sub(earlier.monotonic_ms)
75    }
76
77    /// Returns a deadline `millis` in the future, saturating at
78    /// [`Deadline::NEVER`].
79    pub const fn deadline_after(self, millis: u64) -> Deadline {
80        Deadline::from_millis(self.monotonic_ms.saturating_add(millis))
81    }
82
83    /// Returns the deadline that expires exactly at this sample, so it is due
84    /// as of `self`.
85    ///
86    /// Samples respecting [`MAX_MONOTONIC_MS`](Self::MAX_MONOTONIC_MS) always
87    /// convert; the reserved `u64::MAX` is the one value that cannot, and
88    /// yields [`Deadline::NEVER`].
89    pub const fn as_deadline(self) -> Deadline {
90        Deadline::from_millis(self.monotonic_ms)
91    }
92}
93
94/// A source of monotonic (and optionally wall-clock) time.
95///
96/// Implementations live in adapters — never in protocol or orchestrator crates,
97/// which receive [`Now`] from their caller instead.
98///
99/// `now()` takes `&mut self` so implementations can cache or correct state,
100/// such as latching a monotonic floor over a clock that can step backwards.
101///
102/// # Contract
103///
104/// - `monotonic_ms` never decreases across successive calls, and saturates at
105///   [`Now::MAX_MONOTONIC_MS`] rather than reaching the reserved `u64::MAX`.
106/// - `unix_seconds` is `None` whenever no usable wall-clock reading is
107///   available — either the platform has no wall clock, or its clock reads
108///   before the Unix epoch. An implementation must not fabricate one from
109///   monotonic time.
110pub trait Clock {
111    /// Samples the current time.
112    fn now(&mut self) -> Now;
113}
114
115impl<C: Clock + ?Sized> Clock for &mut C {
116    fn now(&mut self) -> Now {
117        (**self).now()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use alloc::boxed::Box;
125
126    #[test]
127    fn from_millis_has_no_wall_clock() {
128        let now = Now::from_millis(42);
129        assert_eq!(now.monotonic_ms, 42);
130        assert_eq!(now.unix_seconds, None);
131    }
132
133    #[test]
134    fn with_unix_seconds_preserves_monotonic() {
135        let now = Now::from_millis(42).with_unix_seconds(1_700_000_000);
136        assert_eq!(now.monotonic_ms, 42);
137        assert_eq!(now.unix_seconds, Some(1_700_000_000));
138        assert_eq!(now, Now::new(42, 1_700_000_000));
139    }
140
141    #[test]
142    fn elapsed_saturates_instead_of_wrapping() {
143        let earlier = Now::from_millis(100);
144        let later = Now::from_millis(250);
145        assert_eq!(later.saturating_millis_since(earlier), 150);
146        assert_eq!(earlier.saturating_millis_since(later), 0);
147    }
148
149    #[test]
150    fn deadline_after_saturates_at_never() {
151        let now = Now::from_millis(10);
152        assert_eq!(now.deadline_after(5), Deadline::from_millis(15));
153        assert_eq!(now.deadline_after(u64::MAX), Deadline::NEVER);
154        assert_eq!(now.as_deadline(), Deadline::from_millis(10));
155    }
156
157    #[test]
158    fn every_permitted_sample_converts_to_a_deadline_that_is_due() {
159        for millis in [0, 1, 1_000_000, u64::MAX / 2, Now::MAX_MONOTONIC_MS] {
160            let now = Now::from_millis(millis);
161            assert!(
162                now.as_deadline().is_expired_at(now),
163                "as_deadline was not due at {now:?}"
164            );
165        }
166        // `u64::MAX` is reserved for `Deadline::NEVER`, which is why clocks
167        // stop one millisecond short of it.
168        assert_eq!(Now::MAX_MONOTONIC_MS, u64::MAX - 1);
169        assert_eq!(Now::from_millis(u64::MAX).as_deadline(), Deadline::NEVER);
170    }
171
172    struct Fake(u64);
173
174    impl Clock for Fake {
175        fn now(&mut self) -> Now {
176            self.0 += 1;
177            Now::from_millis(self.0)
178        }
179    }
180
181    /// Generic over `C: Clock`, so passing `&mut Fake` exercises the blanket
182    /// impl rather than auto-deref.
183    fn sample<C: Clock>(mut clock: C) -> Now {
184        clock.now()
185    }
186
187    #[test]
188    fn mutable_reference_forwards_to_inner_clock() {
189        let mut fake = Fake(0);
190        assert_eq!(sample(&mut fake).monotonic_ms, 1);
191        assert_eq!(sample(&mut fake).monotonic_ms, 2);
192        assert_eq!(fake.0, 2);
193    }
194
195    #[test]
196    fn trait_is_object_safe() {
197        let mut clock: Box<dyn Clock> = Box::new(Fake(7));
198        assert_eq!(clock.now().monotonic_ms, 8);
199    }
200}