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}