Skip to main content

proxima_primitives/pipe/
clock.rs

1use core::time::Duration;
2
3use crate::pipe::capabilities::Clock;
4use proxima_core::time::Sleep;
5
6/// Production [`Clock`] backed by `proxima-time`'s link-bound driver.
7///
8/// `now_nanos` reads the monotonic clock; `delay` hands back proxima-time's
9/// concrete `Sleep` future, so a `Retry` built over this stays unboxed and
10/// no-alloc. Build the controller's `Deadline` from the same clock so the
11/// nanos origins agree.
12#[derive(Debug, Clone, Copy, Default)]
13pub struct TimeClock;
14
15impl Clock for TimeClock {
16    type Delay = Sleep;
17
18    fn now_nanos(&self) -> u64 {
19        u64::try_from(proxima_core::time::now().into_monotonic().as_nanos()).unwrap_or(u64::MAX)
20    }
21
22    fn delay(&self, dur: Duration) -> Sleep {
23        proxima_core::time::sleep(dur)
24    }
25}
26
27/// Canonical [`Clock`] test doubles — reachable from any crate via
28/// `proxima-primitives`'s `test-support` feature (same convention as
29/// `proxima-core`'s `time-driver-mock`: a lower crate ships the fake once,
30/// behind a feature flag, instead of every caller reinventing an
31/// `Arc<AtomicU64>`-backed clock).
32///
33/// Two doubles, not one, because they prove two different things:
34///
35/// - [`testing::MockClock`] proves a combinator's async pend/wake state
36///   machine — `delay` genuinely pends until [`testing::MockClock::advance`]
37///   crosses the
38///   deadline, waking the task through the real driver's `schedule_wake`.
39///   Collapsing this into `RecordingClock` would make every test that needs
40///   real pend/wake behavior (does the combinator park at the right time,
41///   does it wake exactly once) pass trivially without exercising it.
42/// - [`testing::RecordingClock`] proves what a combinator *computed* (a backoff
43///   schedule, a rate-limit refill) — `delay` never pends; it records the
44///   requested [`Duration`] and resolves on first poll, so a sequential test
45///   never has to drive a concurrent `advance()` to unblock it.
46#[cfg(any(test, feature = "test-support"))]
47pub mod testing {
48    use alloc::sync::Arc;
49    use alloc::vec::Vec;
50    use core::future::{Ready, ready};
51    use core::pin::Pin;
52    use core::task::{Context, Poll};
53    use core::time::Duration;
54    use std::sync::Mutex;
55
56    use portable_atomic::{AtomicU64, Ordering};
57    use proxima_core::time::drivers::mock::MockDriver;
58    use proxima_core::time::{Driver, Instant};
59
60    use crate::pipe::capabilities::Clock;
61
62    /// Deterministic `Clock` wrapping [`MockDriver`]. See the module doc for
63    /// when to reach for this over [`RecordingClock`].
64    #[derive(Clone, Default)]
65    pub struct MockClock {
66        driver: Arc<MockDriver>,
67    }
68
69    impl MockClock {
70        #[must_use]
71        pub fn new() -> Self {
72            Self::default()
73        }
74
75        /// Move virtual time forward, firing any pending [`MockClock::delay`]
76        /// whose deadline the new time now crosses.
77        pub fn advance(&self, delta: Duration) {
78            self.driver.advance(delta);
79        }
80    }
81
82    /// [`MockClock::delay`]'s future: pends until the driver's clock reaches
83    /// `deadline`, registering a real wake via `MockDriver::schedule_wake`
84    /// — never a busy-poll.
85    pub struct MockSleep {
86        driver: Arc<MockDriver>,
87        deadline: Instant,
88    }
89
90    impl core::future::Future for MockSleep {
91        type Output = ();
92
93        fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
94            if self.driver.now() >= self.deadline {
95                Poll::Ready(())
96            } else {
97                self.driver
98                    .schedule_wake(self.deadline, context.waker().clone());
99                Poll::Pending
100            }
101        }
102    }
103
104    impl Clock for MockClock {
105        type Delay = MockSleep;
106
107        fn now_nanos(&self) -> u64 {
108            u64::try_from(self.driver.now().into_monotonic().as_nanos()).unwrap_or(u64::MAX)
109        }
110
111        fn delay(&self, duration: Duration) -> MockSleep {
112            let deadline = self.driver.now() + duration;
113            MockSleep {
114                driver: self.driver.clone(),
115                deadline,
116            }
117        }
118    }
119
120    /// Deterministic, never-pending `Clock`. See the module doc for when to
121    /// reach for this over [`MockClock`].
122    #[derive(Clone, Default)]
123    pub struct RecordingClock {
124        now_nanos: Arc<AtomicU64>,
125        delays: Arc<Mutex<Vec<Duration>>>,
126    }
127
128    impl RecordingClock {
129        /// `now_nanos` starts at zero; advance it with [`RecordingClock::advance`].
130        #[must_use]
131        pub fn new() -> Self {
132            Self::default()
133        }
134
135        /// Start `now_nanos` at an arbitrary value instead of zero — needed
136        /// when the code under test cares about the clock's absolute
137        /// magnitude (e.g. distinguishing an elapsed-since-origin reading
138        /// from an absolute-epoch reading).
139        #[must_use]
140        pub fn at(now_nanos: u64) -> Self {
141            Self {
142                now_nanos: Arc::new(AtomicU64::new(now_nanos)),
143                delays: Arc::new(Mutex::new(Vec::new())),
144            }
145        }
146
147        /// Move `now_nanos` forward. `delay` never needs this to resolve —
148        /// only code that reads `now_nanos` (deadline/refill arithmetic)
149        /// observes it.
150        pub fn advance(&self, duration: Duration) {
151            let elapsed_nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
152            self.now_nanos.fetch_add(elapsed_nanos, Ordering::Relaxed);
153        }
154
155        /// Every duration a caller has asked `delay` to wait, in call order —
156        /// the backoff/refill schedule a test asserts against.
157        #[must_use]
158        pub fn delays(&self) -> Vec<Duration> {
159            self.delays
160                .lock()
161                .unwrap_or_else(|poisoned| poisoned.into_inner())
162                .clone()
163        }
164    }
165
166    impl Clock for RecordingClock {
167        type Delay = Ready<()>;
168
169        fn now_nanos(&self) -> u64 {
170            self.now_nanos.load(Ordering::Relaxed)
171        }
172
173        fn delay(&self, duration: Duration) -> Ready<()> {
174            self.delays
175                .lock()
176                .unwrap_or_else(|poisoned| poisoned.into_inner())
177                .push(duration);
178            ready(())
179        }
180    }
181}