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//! A [`Sleep`] **owns** the queue entry it arms: [`TimerHandle::schedule_wake`]
42//! hands back a [`WakeKey`], and [`Sleep`]'s `Drop` both clears the stored waker
43//! and cancels that key. Clearing the waker is what makes the cancel clean — a
44//! wake that races the drop finds no waker and wakes nobody — and cancelling the
45//! key is what makes it *free*: the entry holds a clone of the shared
46//! `Arc<Mutex<SleepState>>`, so leaving it queued keeps that cell and the OS
47//! mutex inside it alive for the entire remaining delay.
48//!
49//! That retention is not theoretical and not small. A `select!` arm holding a
50//! long-period `interval` tick re-arms a fresh `Sleep` on every loop iteration
51//! and drops it when another arm wins, so an uncancellable entry accumulates at
52//! the loop's iteration rate for the whole period. Measured on VxWorks 7 against
53//! the PVA search engine's 180 s `BEACON_CLEAN_INTERVAL` tick: ~124 live entries
54//! at ~184 B each, released in one batch every 180 s
55//! (`doc/vxworks-dial-attempt-residue-on-target-measurement.md`).
56//!
57//! Cancellation is a property of the *wake* path only.
58//! [`TimerHandle::schedule`] — C `callbackRequestDelayed`
59//! (`callback.c:410-419`) — stays fire-and-forget, because there the caller
60//! keeps no handle and the queue is the only owner.
61
62use std::future::Future;
63use std::pin::Pin;
64use std::sync::{Arc, Mutex};
65use std::task::{Context, Poll, Waker};
66use std::time::{Duration, Instant};
67
68use super::delayed_timer::{TimerHandle, WakeKey};
69
70/// Shared between a [`Sleep`] and its armed timer callback.
71struct SleepState {
72 /// Set by the timer callback once the deadline has fired.
73 fired: bool,
74 /// Waker of the task awaiting the [`Sleep`]. Cleared on drop so an orphaned
75 /// timer callback wakes nobody.
76 waker: Option<Waker>,
77}
78
79/// A future that completes at a fixed deadline, driven by the delayed-callback
80/// timer — the RTEMS-side mirror of `tokio::time::Sleep`.
81pub struct Sleep {
82 deadline: Instant,
83 timer: TimerHandle,
84 state: Arc<Mutex<SleepState>>,
85 /// Whether the first poll has run. Arming is lazy and attempted exactly
86 /// once; a timer already shut down when that poll ran queues nothing, and
87 /// this is what stops every later poll from retrying.
88 armed: bool,
89 /// The queue entry this `Sleep` owns — `Some` exactly while one is queued,
90 /// and the thing `Drop` gives back. Separate from `armed` so neither field
91 /// has to mean two things: "we tried" and "we hold one" are different
92 /// facts, and it is the second that governs the memory.
93 entry: Option<WakeKey>,
94}
95
96/// A future completing `dur` from now — mirrors `tokio::time::sleep`. The
97/// deadline is fixed at construction; the timer entry arms on first poll.
98pub fn sleep(timer: &TimerHandle, dur: Duration) -> Sleep {
99 sleep_until(timer, Instant::now() + dur)
100}
101
102/// A future completing at `deadline` — mirrors `tokio::time::sleep_until`. A
103/// deadline already in the past makes the future ready on its first poll
104/// without arming a timer entry.
105pub fn sleep_until(timer: &TimerHandle, deadline: Instant) -> Sleep {
106 Sleep {
107 deadline,
108 timer: timer.clone(),
109 state: Arc::new(Mutex::new(SleepState {
110 fired: false,
111 waker: None,
112 })),
113 armed: false,
114 entry: None,
115 }
116}
117
118impl Future for Sleep {
119 type Output = ();
120
121 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
122 // `Sleep` holds no self-referential state, so it is `Unpin` and we can
123 // take a plain `&mut` to it.
124 let this = self.get_mut();
125
126 {
127 let mut st = this.state.lock().unwrap();
128 if st.fired {
129 return Poll::Ready(());
130 }
131 // Deadline already reached (past-deadline construction, or the
132 // clock crossed it before the timer callback landed): complete now.
133 if Instant::now() >= this.deadline {
134 st.fired = true;
135 return Poll::Ready(());
136 }
137 st.waker = Some(cx.waker().clone());
138 }
139
140 if !this.armed {
141 let delay = this.deadline.saturating_duration_since(Instant::now());
142 let cb_state = Arc::clone(&this.state);
143 // Inline wakeup on the timer thread — see the module docs: a sleep
144 // wake is a non-blocking `waker.wake()`, and dispatching it to the
145 // callback pool would deadlock a `spawn`ed future that awaits it.
146 this.entry = this.timer.schedule_wake(
147 delay,
148 Box::new(move || {
149 let mut st = cb_state.lock().unwrap();
150 st.fired = true;
151 if let Some(w) = st.waker.take() {
152 w.wake();
153 }
154 }),
155 );
156 this.armed = true;
157 }
158 Poll::Pending
159 }
160}
161
162impl Drop for Sleep {
163 fn drop(&mut self) {
164 // Give the queue entry back first. It holds a clone of `state`, so
165 // until it goes the shared cell — and the OS mutex std lazily creates
166 // inside it — stays alive for the whole remaining delay. Cancelling an
167 // entry that already fired is a no-op, so no ordering is owed here.
168 if let Some(key) = self.entry.take() {
169 self.timer.cancel_wake(key);
170 }
171 // Clear the waker so a wake that raced the cancel finds nobody. Leaving
172 // `fired` untouched is fine — the future is gone.
173 self.state.lock().unwrap().waker = None;
174 }
175}
176
177/// A periodic ticker over the delayed-callback timer — the RTEMS backend for
178/// [`crate::runtime::task::interval`]. Mirrors `tokio::time::Interval` with its
179/// default `MissedTickBehavior::Burst`: the first tick is immediate and tick
180/// deadlines are anchored at construction (`start + period`, `start + 2·period`,
181/// …), so an overdue tick fires immediately and successive overdue ticks burst
182/// back-to-back until the schedule is caught up.
183pub struct TimerInterval {
184 timer: TimerHandle,
185 period: Duration,
186 /// Next tick deadline, anchored at construction so catch-up is Burst.
187 next: Instant,
188 /// The first tick completes immediately (tokio parity).
189 first: bool,
190}
191
192/// Build a periodic ticker firing every `period`, backed by `timer` — the
193/// runtime-free mirror of `tokio::time::interval`.
194pub fn interval(timer: &TimerHandle, period: Duration) -> TimerInterval {
195 TimerInterval {
196 timer: timer.clone(),
197 period,
198 next: Instant::now() + period,
199 first: true,
200 }
201}
202
203impl TimerInterval {
204 /// Complete at the next tick. The first tick is immediate; thereafter each
205 /// tick waits until its (construction-anchored) deadline, with Burst
206 /// catch-up when the caller has fallen behind.
207 pub async fn tick(&mut self) {
208 if self.first {
209 self.first = false;
210 return;
211 }
212 sleep_until(&self.timer, self.next).await;
213 // Advance by a whole period from the previous deadline (not from now),
214 // so overdue deadlines stay in the past and the next tick bursts.
215 self.next += self.period;
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::runtime::background::callback_executor::CallbackPool;
223 use crate::runtime::background::delayed_timer::DelayedTimer;
224 use crate::runtime::task::park_on_interruptible as drive;
225 use std::sync::atomic::{AtomicUsize, Ordering};
226 use std::sync::mpsc;
227 use std::task::Wake;
228
229 const T: Duration = Duration::from_secs(5);
230
231 /// A waker that counts how often it is woken — lets a test prove a dropped
232 /// sleep's orphaned timer callback wakes nobody.
233 struct CountWaker(Arc<AtomicUsize>);
234 impl Wake for CountWaker {
235 fn wake(self: Arc<Self>) {
236 self.0.fetch_add(1, Ordering::SeqCst);
237 }
238 fn wake_by_ref(self: &Arc<Self>) {
239 self.0.fetch_add(1, Ordering::SeqCst);
240 }
241 }
242
243 #[test]
244 fn sleep_completes_no_earlier_than_delay() {
245 let pool = CallbackPool::new();
246 let timer = DelayedTimer::new(pool.handle());
247 let delay = Duration::from_millis(60);
248 let start = Instant::now();
249 // Real path: park-driver polls, parks, the timer callback wakes it
250 // cross-thread, the next poll returns Ready.
251 drive(sleep(&timer.handle(), delay), || false).unwrap();
252 let elapsed = start.elapsed();
253 assert!(
254 elapsed >= delay,
255 "sleep returned after {elapsed:?}, earlier than the {delay:?} delay"
256 );
257 }
258
259 #[test]
260 fn sleep_until_past_deadline_is_immediately_ready() {
261 let pool = CallbackPool::new();
262 let timer = DelayedTimer::new(pool.handle());
263 let past = Instant::now() - Duration::from_secs(1);
264
265 let count = Arc::new(AtomicUsize::new(0));
266 let waker = Waker::from(Arc::new(CountWaker(Arc::clone(&count))));
267 let mut cx = Context::from_waker(&waker);
268
269 let mut s = Box::pin(sleep_until(&timer.handle(), past));
270 assert!(s.as_mut().poll(&mut cx).is_ready());
271 // Nothing was armed, so nothing ever wakes the waker.
272 assert_eq!(count.load(Ordering::SeqCst), 0);
273 }
274
275 #[test]
276 fn drop_before_deadline_wakes_nobody() {
277 let pool = CallbackPool::new();
278 let timer = DelayedTimer::new(pool.handle());
279
280 let count = Arc::new(AtomicUsize::new(0));
281 let waker = Waker::from(Arc::new(CountWaker(Arc::clone(&count))));
282 let mut cx = Context::from_waker(&waker);
283
284 let mut s = Box::pin(sleep(&timer.handle(), Duration::from_millis(60)));
285 // First poll arms the timer entry and registers the CountWaker.
286 assert!(s.as_mut().poll(&mut cx).is_pending());
287 drop(s); // clears the registered waker
288
289 // Wait well past the deadline: the orphaned timer callback fires but
290 // must find no waker and wake nobody.
291 std::thread::sleep(Duration::from_millis(140));
292 assert_eq!(
293 count.load(Ordering::SeqCst),
294 0,
295 "a dropped sleep must not wake a stale waker"
296 );
297 }
298
299 /// The E10 regression: a dropped `Sleep` must give its queue entry back,
300 /// not leave it to expire. Before the entry was owned, the ~184 B a `Sleep`
301 /// allocates (shared cell, boxed wake, and the OS mutex std lazily creates
302 /// inside the cell) stayed live for the whole remaining delay — so a
303 /// `select!` arm re-arming a long-period tick each iteration accumulated
304 /// one of those per iteration until the period elapsed.
305 #[test]
306 fn dropping_a_sleep_releases_its_timer_entry() {
307 let pool = CallbackPool::new();
308 let timer = DelayedTimer::new(pool.handle());
309 let h = timer.handle();
310
311 let waker = Waker::from(Arc::new(CountWaker(Arc::new(AtomicUsize::new(0)))));
312 let mut cx = Context::from_waker(&waker);
313
314 // An hour out, so only the drop can retire it.
315 let mut s = Box::pin(sleep(&h, Duration::from_secs(3600)));
316 assert!(s.as_mut().poll(&mut cx).is_pending());
317 assert_eq!(h.scheduled_count(), 1, "the first poll must arm an entry");
318
319 drop(s);
320 assert_eq!(
321 h.scheduled_count(),
322 0,
323 "a dropped sleep left its entry queued; it holds the shared cell for an hour"
324 );
325 }
326
327 /// A `Sleep` created and never polled arms nothing, so it has nothing to
328 /// give back — the lazy-arming half of the same invariant.
329 #[test]
330 fn dropping_an_unpolled_sleep_queues_nothing() {
331 let pool = CallbackPool::new();
332 let timer = DelayedTimer::new(pool.handle());
333 let h = timer.handle();
334
335 drop(sleep(&h, Duration::from_secs(3600)));
336 assert_eq!(h.scheduled_count(), 0);
337 }
338
339 /// The interval case the leak was actually measured through: each `tick()`
340 /// that loses a `select!` race drops mid-await, and every one of those must
341 /// leave the queue as it found it.
342 #[test]
343 fn abandoned_interval_ticks_leave_no_entries() {
344 let pool = CallbackPool::new();
345 let timer = DelayedTimer::new(pool.handle());
346 let h = timer.handle();
347
348 let waker = Waker::from(Arc::new(CountWaker(Arc::new(AtomicUsize::new(0)))));
349 let mut cx = Context::from_waker(&waker);
350
351 let mut iv = interval(&h, Duration::from_secs(180));
352 // The first tick is immediate and arms nothing; the rest are 180 s out.
353 let mut first = Box::pin(iv.tick());
354 assert!(first.as_mut().poll(&mut cx).is_ready());
355 drop(first);
356
357 for _ in 0..32 {
358 let mut t = Box::pin(iv.tick());
359 assert!(t.as_mut().poll(&mut cx).is_pending());
360 drop(t); // the `select!` arm lost
361 }
362 assert_eq!(
363 h.scheduled_count(),
364 0,
365 "abandoned interval ticks accumulate one queue entry each per period"
366 );
367 }
368
369 #[test]
370 fn concurrent_sleepers_complete_in_deadline_order() {
371 // The future layer must not serialize sleepers: a later deadline must
372 // not hold back an earlier one.
373 let pool = CallbackPool::new();
374 let timer = DelayedTimer::new(pool.handle());
375 let (tx, rx) = mpsc::channel();
376
377 let th_long = timer.handle();
378 let tx_long = tx.clone();
379 let long = std::thread::spawn(move || {
380 drive(sleep(&th_long, Duration::from_millis(150)), || false).unwrap();
381 tx_long.send("long").unwrap();
382 });
383 let th_short = timer.handle();
384 let short = std::thread::spawn(move || {
385 drive(sleep(&th_short, Duration::from_millis(30)), || false).unwrap();
386 tx.send("short").unwrap();
387 });
388
389 assert_eq!(rx.recv_timeout(T).unwrap(), "short");
390 assert_eq!(rx.recv_timeout(T).unwrap(), "long");
391 long.join().unwrap();
392 short.join().unwrap();
393 }
394
395 #[test]
396 fn interval_first_tick_immediate_then_periodic() {
397 let pool = CallbackPool::new();
398 let timer = DelayedTimer::new(pool.handle());
399 let period = Duration::from_millis(40);
400 let th = timer.handle();
401 let start = Instant::now();
402 drive(
403 async move {
404 let mut iv = interval(&th, period);
405 iv.tick().await; // first tick: immediate
406 let after_first = start.elapsed();
407 assert!(
408 after_first < period,
409 "first tick should be immediate, was {after_first:?}"
410 );
411 iv.tick().await; // ~1 period in
412 iv.tick().await; // ~2 periods in
413 },
414 || false,
415 )
416 .unwrap();
417 assert!(
418 start.elapsed() >= 2 * period,
419 "two periodic ticks should take at least two periods, took {:?}",
420 start.elapsed()
421 );
422 }
423
424 #[test]
425 fn interval_bursts_to_catch_up_after_a_stall() {
426 // MissedTickBehavior::Burst: after stalling past several deadlines, the
427 // overdue ticks fire back-to-back rather than re-spacing from now.
428 let pool = CallbackPool::new();
429 let timer = DelayedTimer::new(pool.handle());
430 let period = Duration::from_millis(30);
431 let th = timer.handle();
432 drive(
433 async move {
434 let mut iv = interval(&th, period);
435 iv.tick().await; // immediate; deadlines land at 30/60/90/120ms
436 // Stall well past four deadlines.
437 sleep(&th, Duration::from_millis(140)).await;
438 let t = Instant::now();
439 iv.tick().await; // deadline 30ms already passed -> immediate
440 iv.tick().await; // deadline 60ms passed -> immediate
441 iv.tick().await; // deadline 90ms passed -> immediate
442 assert!(
443 t.elapsed() < period,
444 "overdue ticks must burst, three took {:?}",
445 t.elapsed()
446 );
447 },
448 || false,
449 )
450 .unwrap();
451 }
452}