Skip to main content

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