epics_libcom_rs/runtime/background/timer_sleep.rs
1//! Timer-backed `sleep` / `sleep_until` futures — the RTEMS backend for
2//! [`crate::runtime::task::sleep`] / [`crate::runtime::task::sleep_until`]
3//! (decision A2, increment W3b item 4).
4//!
5//! # Model
6//!
7//! A hosted build sleeps via `tokio::time::sleep`, whose waker is driven by the
8//! tokio timer wheel. RTEMS has no such wheel, so a [`Sleep`] future arms a
9//! one-shot entry on the [`DelayedTimer`](super::delayed_timer::DelayedTimer):
10//! on its first poll it schedules a wakeup for its deadline; when that wakeup
11//! fires it wakes the future's stored waker, and the next poll — now past the
12//! deadline — returns `Ready`. This is the same deadline-ordered timer thread
13//! that backs C `callbackRequestDelayed` (`callback.c:410-419`); a `Sleep` is
14//! just that facility with the "callback" being "wake this future".
15//!
16//! # Why the wakeup runs on the timer thread, not the callback pool
17//!
18//! The wakeup is armed via [`TimerHandle::schedule_wake`], so it runs **inline
19//! on the timer thread** rather than being dispatched to the callback pool.
20//! Waking is a non-blocking `waker.wake()` (an `unpark` for a `park_on` driver,
21//! a task re-enqueue for [`super::future_exec`], or a tokio task-schedule) and
22//! needs no worker, so it does not take one. Routing the wake off the band keeps
23//! the band's sole job "run futures" and makes the wake uniform for every
24//! sleeper — bare `spawn`ed tails and periodic-scan `interval` alike.
25//!
26//! This is also what closed the sleep-wake self-deadlock (`bug_pattern
27//! rtems-exec-sleep-wake-band-deadlock`): back when `future_exec` parked a pool
28//! worker for a spawned future's whole life, a wake dispatched to the same
29//! single-worker band sat behind the very worker it had to wake. That executor
30//! is cooperative now and releases its worker at every suspension, so the wake
31//! would no longer starve — but a wake that costs a worker is still the wrong
32//! shape, and `Inline` remains the rule.
33//!
34//! # Lazy arming and drop-cancel
35//!
36//! The deadline is fixed when the [`Sleep`] is constructed (`now + dur` for
37//! [`sleep`], the given instant for [`sleep_until`]), matching tokio, but the
38//! timer entry is armed lazily on the **first poll** — a `Sleep` that is
39//! created and dropped without ever being awaited schedules nothing.
40//!
41//! The [`DelayedTimer`](crate::runtime::background::delayed_timer::DelayedTimer) has no cancel handle (C `callbackRequestDelayed` starts
42//! a fire-and-forget `epicsTimer`), so a dropped-before-deadline `Sleep` cannot
43//! un-schedule its entry. Instead [`Sleep`]'s `Drop` clears the stored waker, so
44//! when the orphaned timer callback eventually fires it finds no waker and wakes
45//! nobody — a clean cancel with no stale wakeup. The one wasted timer tick is
46//! the cost of the runtime-free design and matches how C leaves an already-armed
47//! `epicsTimer` to expire harmlessly.
48
49use std::future::Future;
50use std::pin::Pin;
51use std::sync::{Arc, Mutex};
52use std::task::{Context, Poll, Waker};
53use std::time::{Duration, Instant};
54
55use super::delayed_timer::TimerHandle;
56
57/// Shared between a [`Sleep`] and its armed timer callback.
58struct SleepState {
59 /// Set by the timer callback once the deadline has fired.
60 fired: bool,
61 /// Waker of the task awaiting the [`Sleep`]. Cleared on drop so an orphaned
62 /// timer callback wakes nobody.
63 waker: Option<Waker>,
64}
65
66/// A future that completes at a fixed deadline, driven by the delayed-callback
67/// timer — the RTEMS-side mirror of `tokio::time::Sleep`.
68pub struct Sleep {
69 deadline: Instant,
70 timer: TimerHandle,
71 state: Arc<Mutex<SleepState>>,
72 /// Whether the timer entry has been armed (arming is lazy, on first poll).
73 armed: bool,
74}
75
76/// A future completing `dur` from now — mirrors `tokio::time::sleep`. The
77/// deadline is fixed at construction; the timer entry arms on first poll.
78pub fn sleep(timer: &TimerHandle, dur: Duration) -> Sleep {
79 sleep_until(timer, Instant::now() + dur)
80}
81
82/// A future completing at `deadline` — mirrors `tokio::time::sleep_until`. A
83/// deadline already in the past makes the future ready on its first poll
84/// without arming a timer entry.
85pub fn sleep_until(timer: &TimerHandle, deadline: Instant) -> Sleep {
86 Sleep {
87 deadline,
88 timer: timer.clone(),
89 state: Arc::new(Mutex::new(SleepState {
90 fired: false,
91 waker: None,
92 })),
93 armed: false,
94 }
95}
96
97impl Future for Sleep {
98 type Output = ();
99
100 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
101 // `Sleep` holds no self-referential state, so it is `Unpin` and we can
102 // take a plain `&mut` to it.
103 let this = self.get_mut();
104
105 {
106 let mut st = this.state.lock().unwrap();
107 if st.fired {
108 return Poll::Ready(());
109 }
110 // Deadline already reached (past-deadline construction, or the
111 // clock crossed it before the timer callback landed): complete now.
112 if Instant::now() >= this.deadline {
113 st.fired = true;
114 return Poll::Ready(());
115 }
116 st.waker = Some(cx.waker().clone());
117 }
118
119 if !this.armed {
120 let delay = this.deadline.saturating_duration_since(Instant::now());
121 let cb_state = Arc::clone(&this.state);
122 // Inline wakeup on the timer thread — see the module docs: a sleep
123 // wake is a non-blocking `waker.wake()`, and dispatching it to the
124 // callback pool would deadlock a `spawn`ed future that awaits it.
125 this.timer.schedule_wake(
126 delay,
127 Box::new(move || {
128 let mut st = cb_state.lock().unwrap();
129 st.fired = true;
130 if let Some(w) = st.waker.take() {
131 w.wake();
132 }
133 }),
134 );
135 this.armed = true;
136 }
137 Poll::Pending
138 }
139}
140
141impl Drop for Sleep {
142 fn drop(&mut self) {
143 // Clear the waker so an already-armed (uncancellable) timer callback
144 // wakes nobody when it fires. Leaving `fired` untouched is fine — the
145 // future is gone.
146 self.state.lock().unwrap().waker = None;
147 }
148}
149
150/// A periodic ticker over the delayed-callback timer — the RTEMS backend for
151/// [`crate::runtime::task::interval`]. Mirrors `tokio::time::Interval` with its
152/// default `MissedTickBehavior::Burst`: the first tick is immediate and tick
153/// deadlines are anchored at construction (`start + period`, `start + 2·period`,
154/// …), so an overdue tick fires immediately and successive overdue ticks burst
155/// back-to-back until the schedule is caught up.
156pub struct TimerInterval {
157 timer: TimerHandle,
158 period: Duration,
159 /// Next tick deadline, anchored at construction so catch-up is Burst.
160 next: Instant,
161 /// The first tick completes immediately (tokio parity).
162 first: bool,
163}
164
165/// Build a periodic ticker firing every `period`, backed by `timer` — the
166/// runtime-free mirror of `tokio::time::interval`.
167pub fn interval(timer: &TimerHandle, period: Duration) -> TimerInterval {
168 TimerInterval {
169 timer: timer.clone(),
170 period,
171 next: Instant::now() + period,
172 first: true,
173 }
174}
175
176impl TimerInterval {
177 /// Complete at the next tick. The first tick is immediate; thereafter each
178 /// tick waits until its (construction-anchored) deadline, with Burst
179 /// catch-up when the caller has fallen behind.
180 pub async fn tick(&mut self) {
181 if self.first {
182 self.first = false;
183 return;
184 }
185 sleep_until(&self.timer, self.next).await;
186 // Advance by a whole period from the previous deadline (not from now),
187 // so overdue deadlines stay in the past and the next tick bursts.
188 self.next += self.period;
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::runtime::background::callback_executor::CallbackPool;
196 use crate::runtime::background::delayed_timer::DelayedTimer;
197 use crate::runtime::task::park_on_interruptible as drive;
198 use std::sync::atomic::{AtomicUsize, Ordering};
199 use std::sync::mpsc;
200 use std::task::Wake;
201
202 const T: Duration = Duration::from_secs(5);
203
204 /// A waker that counts how often it is woken — lets a test prove a dropped
205 /// sleep's orphaned timer callback wakes nobody.
206 struct CountWaker(Arc<AtomicUsize>);
207 impl Wake for CountWaker {
208 fn wake(self: Arc<Self>) {
209 self.0.fetch_add(1, Ordering::SeqCst);
210 }
211 fn wake_by_ref(self: &Arc<Self>) {
212 self.0.fetch_add(1, Ordering::SeqCst);
213 }
214 }
215
216 #[test]
217 fn sleep_completes_no_earlier_than_delay() {
218 let pool = CallbackPool::new();
219 let timer = DelayedTimer::new(pool.handle());
220 let delay = Duration::from_millis(60);
221 let start = Instant::now();
222 // Real path: park-driver polls, parks, the timer callback wakes it
223 // cross-thread, the next poll returns Ready.
224 drive(sleep(&timer.handle(), delay), || false).unwrap();
225 let elapsed = start.elapsed();
226 assert!(
227 elapsed >= delay,
228 "sleep returned after {elapsed:?}, earlier than the {delay:?} delay"
229 );
230 }
231
232 #[test]
233 fn sleep_until_past_deadline_is_immediately_ready() {
234 let pool = CallbackPool::new();
235 let timer = DelayedTimer::new(pool.handle());
236 let past = Instant::now() - Duration::from_secs(1);
237
238 let count = Arc::new(AtomicUsize::new(0));
239 let waker = Waker::from(Arc::new(CountWaker(Arc::clone(&count))));
240 let mut cx = Context::from_waker(&waker);
241
242 let mut s = Box::pin(sleep_until(&timer.handle(), past));
243 assert!(s.as_mut().poll(&mut cx).is_ready());
244 // Nothing was armed, so nothing ever wakes the waker.
245 assert_eq!(count.load(Ordering::SeqCst), 0);
246 }
247
248 #[test]
249 fn drop_before_deadline_wakes_nobody() {
250 let pool = CallbackPool::new();
251 let timer = DelayedTimer::new(pool.handle());
252
253 let count = Arc::new(AtomicUsize::new(0));
254 let waker = Waker::from(Arc::new(CountWaker(Arc::clone(&count))));
255 let mut cx = Context::from_waker(&waker);
256
257 let mut s = Box::pin(sleep(&timer.handle(), Duration::from_millis(60)));
258 // First poll arms the timer entry and registers the CountWaker.
259 assert!(s.as_mut().poll(&mut cx).is_pending());
260 drop(s); // clears the registered waker
261
262 // Wait well past the deadline: the orphaned timer callback fires but
263 // must find no waker and wake nobody.
264 std::thread::sleep(Duration::from_millis(140));
265 assert_eq!(
266 count.load(Ordering::SeqCst),
267 0,
268 "a dropped sleep must not wake a stale waker"
269 );
270 }
271
272 #[test]
273 fn concurrent_sleepers_complete_in_deadline_order() {
274 // The future layer must not serialize sleepers: a later deadline must
275 // not hold back an earlier one.
276 let pool = CallbackPool::new();
277 let timer = DelayedTimer::new(pool.handle());
278 let (tx, rx) = mpsc::channel();
279
280 let th_long = timer.handle();
281 let tx_long = tx.clone();
282 let long = std::thread::spawn(move || {
283 drive(sleep(&th_long, Duration::from_millis(150)), || false).unwrap();
284 tx_long.send("long").unwrap();
285 });
286 let th_short = timer.handle();
287 let short = std::thread::spawn(move || {
288 drive(sleep(&th_short, Duration::from_millis(30)), || false).unwrap();
289 tx.send("short").unwrap();
290 });
291
292 assert_eq!(rx.recv_timeout(T).unwrap(), "short");
293 assert_eq!(rx.recv_timeout(T).unwrap(), "long");
294 long.join().unwrap();
295 short.join().unwrap();
296 }
297
298 #[test]
299 fn interval_first_tick_immediate_then_periodic() {
300 let pool = CallbackPool::new();
301 let timer = DelayedTimer::new(pool.handle());
302 let period = Duration::from_millis(40);
303 let th = timer.handle();
304 let start = Instant::now();
305 drive(
306 async move {
307 let mut iv = interval(&th, period);
308 iv.tick().await; // first tick: immediate
309 let after_first = start.elapsed();
310 assert!(
311 after_first < period,
312 "first tick should be immediate, was {after_first:?}"
313 );
314 iv.tick().await; // ~1 period in
315 iv.tick().await; // ~2 periods in
316 },
317 || false,
318 )
319 .unwrap();
320 assert!(
321 start.elapsed() >= 2 * period,
322 "two periodic ticks should take at least two periods, took {:?}",
323 start.elapsed()
324 );
325 }
326
327 #[test]
328 fn interval_bursts_to_catch_up_after_a_stall() {
329 // MissedTickBehavior::Burst: after stalling past several deadlines, the
330 // overdue ticks fire back-to-back rather than re-spacing from now.
331 let pool = CallbackPool::new();
332 let timer = DelayedTimer::new(pool.handle());
333 let period = Duration::from_millis(30);
334 let th = timer.handle();
335 drive(
336 async move {
337 let mut iv = interval(&th, period);
338 iv.tick().await; // immediate; deadlines land at 30/60/90/120ms
339 // Stall well past four deadlines.
340 sleep(&th, Duration::from_millis(140)).await;
341 let t = Instant::now();
342 iv.tick().await; // deadline 30ms already passed -> immediate
343 iv.tick().await; // deadline 60ms passed -> immediate
344 iv.tick().await; // deadline 90ms passed -> immediate
345 assert!(
346 t.elapsed() < period,
347 "overdue ticks must burst, three took {:?}",
348 t.elapsed()
349 );
350 },
351 || false,
352 )
353 .unwrap();
354 }
355}