Skip to main content

epics_libcom_rs/runtime/background/
future_exec.rs

1//! Runtime-free future executor over the callback pool — the RTEMS backend for
2//! [`crate::runtime::task::spawn`] / [`crate::runtime::task::spawn_blocking`]
3//! (decision A2, increment W3b).
4//!
5//! # Model — cooperative, not worker-per-future
6//!
7//! A hosted build runs a spawned async *tail* as a tokio task. RTEMS has no
8//! tokio runtime, so this module runs each spawned future as a **task object
9//! multiplexed over the [`CallbackPool`](super::callback_executor::CallbackPool)**:
10//!
11//! 1. `spawn` builds a `Task` and pushes it onto its band's ring.
12//! 2. A band worker pops it and polls it **once**.
13//! 3. `Ready` → the outcome is published and the task is done.
14//! 4. `Pending` → the worker **releases the task and moves on**. The waker
15//!    handed to the poll is the task itself: waking it pushes the task back
16//!    onto the ring (at the tail), where some worker picks it up and polls it
17//!    again.
18//!
19//! So a band's workers are held only for the duration of a single poll, and a
20//! bounded worker set multiplexes an unbounded number of mostly-idle tails. All
21//! primitives the future awaits must still be runtime-agnostic (`tokio::sync`
22//! locks/channels/notifies, [`super::timer_sleep`]) — nothing here drives a
23//! reactor or a timer wheel — which is precisely the A2 precondition.
24//!
25//! This replaces the original design, where a worker ran
26//! `park_on_interruptible` for
27//! the *whole life* of the future and stayed parked between polls. That model
28//! had a structural defect: N concurrent long-lived tails exhausted the band's
29//! N workers, after which every further task on that band starved until one
30//! finished. Memory note `rtems-exec-worker-per-future-fragility`; it mattered
31//! more once PVA started sharing this backend.
32//!
33//! ## Task state machine
34//!
35//! One [`AtomicU8`] is the single owner of "may this task be polled, and by
36//! whom":
37//!
38//! | State | Meaning | Who leaves it |
39//! |---|---|---|
40//! | `IDLE` | not queued, not running — waiting for a wake | a waker, or an abort |
41//! | `SCHEDULED` | sitting on a band ring | the worker that pops it |
42//! | `RUNNING` | being polled right now | the polling worker |
43//! | `RUNNING_NOTIFIED` | being polled, and a wake landed mid-poll | the polling worker (re-enqueues) |
44//! | `DONE` | terminal | nobody |
45//!
46//! `RUNNING_NOTIFIED` is what makes a wake that races the poll safe: it is
47//! never dropped, it is deferred to the end of the current poll and turned into
48//! a re-enqueue. A wake arriving in any other state either enqueues (`IDLE`) or
49//! is redundant (`SCHEDULED` — already queued; `DONE` — nothing to run).
50//!
51//! ## Abort
52//!
53//! [`JoinFuture::abort`] / [`AbortHandle::abort`] latch a flag and then
54//! **schedule** the task, so an idle task is polled once more and observes the
55//! flag at the top of that poll — before the future is polled again. That is
56//! the same "cancel is observed at the next suspension point" contract the
57//! previous park-driver had (there, the abort unparked the parked worker); only
58//! the wake-up mechanism changed.
59//!
60//! ## Handle surface
61//!
62//! Unchanged. [`JoinFuture<T>`] mirrors the subset of `tokio::task::JoinHandle`
63//! the CA/PVA call sites actually use (see the W3b call-site map): `impl
64//! Future<Output = Result<T, JoinError>>`, [`abort`](JoinFuture::abort),
65//! [`is_finished`](JoinFuture::is_finished), and
66//! [`abort_handle`](JoinFuture::abort_handle) returning a non-generic
67//! [`AbortHandle`] with [`abort`](AbortHandle::abort) /
68//! [`is_finished`](AbortHandle::is_finished). [`JoinError`] mirrors only
69//! [`is_cancelled`](JoinError::is_cancelled) — the one `JoinError` method any
70//! call site consumes.
71//!
72//! ## Every handle resolves
73//!
74//! `Shared::finalize` is the single owner of "this task has an outcome", and
75//! it is idempotent. Three paths reach it, covering every way a task can stop
76//! existing: the poll produced `Ready`/cancel/panic; the queued entry was
77//! dropped without ever running (ring full, or the band shut down under it);
78//! or the task became unreachable — no queue entry and no live waker — and its
79//! `Drop` ran. A [`JoinFuture`] therefore never strands.
80//!
81//! ## Panic isolation
82//!
83//! C `callbackTask` (`callback.c:210-235`, cited in
84//! [`super::callback_executor`]) is a bare drain loop: it calls each callback
85//! and loops, with no exception machinery (C has none). A Rust callback *can*
86//! unwind, and an unwind out of the worker closure would tear down the band's
87//! worker thread — breaking that drain-loop invariant. So each poll is run
88//! under [`catch_unwind`]: a panicking task is reported as a panicked
89//! [`JoinError`] and the worker keeps draining, preserving the C loop's
90//! "one callback never stops the worker" property.
91
92use std::future::Future;
93use std::panic::{AssertUnwindSafe, catch_unwind};
94use std::pin::Pin;
95use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
96use std::sync::{Arc, Mutex, Weak};
97use std::task::{Context, Poll, Wake, Waker};
98
99use super::callback_executor::{Callback, CallbackHandle, CallbackPriority};
100
101/// Default band for a general spawned tail. C routes general deferred work
102/// through `callbackRequest` at `priorityMedium` (`callback.h:42`) — the middle
103/// of the three bands (`callback.h:41-43`) — so a spawned async tail lands
104/// there unless a caller picks another band.
105pub const DEFAULT_SPAWN_PRIORITY: CallbackPriority = CallbackPriority::Medium;
106
107// --- Task states (see the module docs' state table) -------------------------
108
109/// Not queued and not running; only a wake or an abort moves it on.
110const IDLE: u8 = 0;
111/// Sitting on a band ring, waiting for a worker to pop it.
112const SCHEDULED: u8 = 1;
113/// Being polled right now by a band worker.
114const RUNNING: u8 = 2;
115/// Being polled, and a wake landed during the poll — re-enqueue when it ends.
116const RUNNING_NOTIFIED: u8 = 3;
117/// Terminal: the outcome has been (or is being) published.
118const DONE: u8 = 4;
119
120/// Why awaiting a [`JoinFuture`] yielded an error instead of the task output —
121/// the seam-owned mirror of `tokio::task::JoinError`.
122///
123/// Only [`is_cancelled`](Self::is_cancelled) is exposed: it is the one
124/// `JoinError` method any seam call site consumes (the W3b map shows
125/// `is_cancelled()` in ca/pva shutdown paths; no site calls `is_panic()` /
126/// `into_panic()`).
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct JoinError {
129    kind: JoinErrorKind,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133enum JoinErrorKind {
134    /// The task was [`abort`](JoinFuture::abort)ed before it completed.
135    Cancelled,
136    /// The task's future (or blocking closure) panicked; the worker survived.
137    Panicked,
138}
139
140impl JoinError {
141    fn cancelled() -> Self {
142        JoinError {
143            kind: JoinErrorKind::Cancelled,
144        }
145    }
146
147    fn panicked() -> Self {
148        JoinError {
149            kind: JoinErrorKind::Panicked,
150        }
151    }
152
153    /// `true` when the task was aborted before completing — mirrors
154    /// `tokio::task::JoinError::is_cancelled`. A panicked task returns `false`
155    /// here (as tokio does).
156    pub fn is_cancelled(&self) -> bool {
157        matches!(self.kind, JoinErrorKind::Cancelled)
158    }
159}
160
161impl std::fmt::Display for JoinError {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self.kind {
164            JoinErrorKind::Cancelled => f.write_str("task was cancelled"),
165            JoinErrorKind::Panicked => f.write_str("task panicked"),
166        }
167    }
168}
169
170impl std::error::Error for JoinError {}
171
172/// The non-generic half of a task, so [`AbortHandle`] can be non-generic —
173/// exactly like `tokio::task::AbortHandle`, which ca/pva store in
174/// `Vec<(_, AbortHandle)>` fields that name no task type.
175trait Schedulable: Send + Sync {
176    /// Push the task onto its band ring if it is not already queued or running.
177    fn schedule(self: Arc<Self>);
178}
179
180/// Non-generic control block shared by a [`JoinFuture`] and every
181/// [`AbortHandle`] cloned from it.
182struct Control {
183    /// Set by [`AbortHandle::abort`] / [`JoinFuture::abort`]; read at the top of
184    /// every poll.
185    abort: AtomicBool,
186    /// Set once the task has produced its result (completed, cancelled, or
187    /// panicked). Backs `is_finished`.
188    finished: AtomicBool,
189    /// The task, for waking it so it can observe an abort.
190    ///
191    /// **`Weak` on purpose.** A strong reference here would close the cycle
192    /// task → shared → control → task and leak every task that ends without
193    /// running. `Weak` costs nothing: the task is alive exactly while it can
194    /// still run (a queue entry or a registered waker holds it), and if it is
195    /// *not* alive its `Drop` has already resolved the handle as cancelled — so
196    /// an abort that finds a dead `Weak` still leaves the joiner with
197    /// `is_cancelled()`.
198    task: Mutex<Option<Weak<dyn Schedulable>>>,
199}
200
201impl Control {
202    fn new() -> Arc<Self> {
203        Arc::new(Control {
204            abort: AtomicBool::new(false),
205            finished: AtomicBool::new(false),
206            task: Mutex::new(None),
207        })
208    }
209}
210
211/// Request cancellation on a control block: latch the abort flag, then schedule
212/// the task so it is polled once more and observes the flag at the top of that
213/// poll. A no-op once the task has finished (a `DONE` task ignores schedules)
214/// or once it is gone (its `Drop` already finalized it as cancelled).
215fn request_abort(control: &Control) {
216    control.abort.store(true, Ordering::Release);
217    // Clone out from under the lock: `schedule` reaches into the callback pool,
218    // and no pool operation may run while a control lock is held.
219    let task = control.task.lock().unwrap().clone();
220    if let Some(task) = task.and_then(|w| w.upgrade()) {
221        task.schedule();
222    }
223}
224
225/// Generic result slot plus the joiner's waker.
226struct Slot<T> {
227    /// The task's outcome, taken by the first [`JoinFuture::poll`] that sees it.
228    result: Option<Result<T, JoinError>>,
229    /// Whether an outcome has ever been published. Distinct from
230    /// `result.is_some()`, which goes back to `None` once the joiner takes it —
231    /// this is what makes [`Shared::finalize`] idempotent.
232    finalized: bool,
233    /// Waker of the task awaiting this [`JoinFuture`], if it polled before the
234    /// task finished.
235    join_waker: Option<Waker>,
236}
237
238struct Shared<T> {
239    control: Arc<Control>,
240    slot: Mutex<Slot<T>>,
241}
242
243impl<T> Shared<T> {
244    fn new() -> Arc<Self> {
245        Arc::new(Shared {
246            control: Control::new(),
247            slot: Mutex::new(Slot {
248                result: None,
249                finalized: false,
250                join_waker: None,
251            }),
252        })
253    }
254
255    /// Publish the task's outcome and wake any joiner — **the single owner of
256    /// the "task has an outcome" transition**, and idempotent: the first caller
257    /// wins and every later one is a no-op.
258    ///
259    /// Sets the result under the slot lock, then the `finished` flag (still
260    /// under the lock, so a concurrent `is_finished()` never observes
261    /// `finished` before the result is visible to a `poll`), then wakes outside
262    /// it — waking may run arbitrary code, including a re-entrant spawn.
263    fn finalize(&self, result: Result<T, JoinError>) {
264        let waker = {
265            let mut slot = self.slot.lock().unwrap();
266            if slot.finalized {
267                return;
268            }
269            slot.finalized = true;
270            slot.result = Some(result);
271            let waker = slot.join_waker.take();
272            self.control.finished.store(true, Ordering::Release);
273            waker
274        };
275        if let Some(w) = waker {
276            w.wake();
277        }
278    }
279}
280
281/// A spawned future plus everything needed to re-schedule it: the executor's
282/// unit of work, and the [`Waker`] handed to its own polls.
283struct Task<T> {
284    /// The state machine (see the module docs).
285    state: AtomicU8,
286    /// The future, taken out for the duration of a poll and put back on
287    /// `Pending`. `None` means "consumed" — completed, cancelled, or panicked.
288    future: Mutex<Option<Pin<Box<dyn Future<Output = T> + Send>>>>,
289    shared: Arc<Shared<T>>,
290    /// Where to push ourselves on wake.
291    callbacks: CallbackHandle,
292    priority: CallbackPriority,
293    /// Test-only: how many times this task has been pushed onto a band ring.
294    /// Backs the "a synchronously-completing future never round-trips the
295    /// queue" boundary test. Not compiled into a production build.
296    #[cfg(test)]
297    enqueues: std::sync::atomic::AtomicUsize,
298}
299
300impl<T: Send + 'static> Task<T> {
301    /// Push this task onto its band ring. The caller must have just claimed the
302    /// `SCHEDULED` state, so exactly one ring entry exists per task at a time.
303    fn enqueue(self: &Arc<Self>) {
304        #[cfg(test)]
305        self.enqueues.fetch_add(1, Ordering::Relaxed);
306
307        // `Entry` finalizes the task if it is dropped without running — see its
308        // `Drop`. Both `request` failure modes drop the callback, so the ring
309        // rejecting us and the band shutting down under us are both covered.
310        let mut entry = Entry {
311            task: Some(Arc::clone(self)),
312        };
313        let cb: Callback = Box::new(move || entry.run());
314        if self.callbacks.request(self.priority, cb).is_err() {
315            self.state.store(DONE, Ordering::Release);
316            tracing::error!(
317                target: "epics_base_rs::runtime::future_exec",
318                "spawn_future: callback ring full; task dropped, handle resolves cancelled"
319            );
320        }
321    }
322
323    /// Poll the task once on the calling worker, then either publish its
324    /// outcome or release the worker.
325    fn run(self: Arc<Self>) {
326        // SCHEDULED → RUNNING. Anything else means the task was finalized out
327        // from under this ring entry; there is nothing left to poll.
328        if self
329            .state
330            .compare_exchange(SCHEDULED, RUNNING, Ordering::AcqRel, Ordering::Acquire)
331            .is_err()
332        {
333            return;
334        }
335
336        let Some(mut fut) = self.future.lock().unwrap().take() else {
337            // Already consumed by a terminal path.
338            self.state.store(DONE, Ordering::Release);
339            return;
340        };
341
342        // Cancel is checked here, before the poll — the same point the previous
343        // park-driver checked it, so "the task is dropped at its next
344        // suspension point" is unchanged. Dropping `fut` here runs its
345        // destructors, exactly as a cancelled tokio task's does.
346        if self.shared.control.abort.load(Ordering::Acquire) {
347            drop(fut);
348            self.state.store(DONE, Ordering::Release);
349            self.shared.finalize(Err(JoinError::cancelled()));
350            return;
351        }
352
353        // The waker IS the task: waking re-enqueues it (module docs).
354        let waker = Waker::from(Arc::clone(&self));
355        // callback.c:210-235 drain-loop invariant: a panicking future must not
356        // tear down the band worker.
357        let polled = catch_unwind(AssertUnwindSafe(|| {
358            let mut cx = Context::from_waker(&waker);
359            fut.as_mut().poll(&mut cx)
360        }));
361
362        match polled {
363            Ok(Poll::Ready(value)) => {
364                drop(fut);
365                self.state.store(DONE, Ordering::Release);
366                self.shared.finalize(Ok(value));
367            }
368            Err(_panic) => {
369                drop(fut);
370                self.state.store(DONE, Ordering::Release);
371                self.shared.finalize(Err(JoinError::panicked()));
372            }
373            Ok(Poll::Pending) => {
374                // Put the future back BEFORE announcing we are schedulable
375                // again, or the worker that picks us up next could find an
376                // empty slot.
377                *self.future.lock().unwrap() = Some(fut);
378                if self
379                    .state
380                    .compare_exchange(RUNNING, IDLE, Ordering::AcqRel, Ordering::Acquire)
381                    .is_err()
382                {
383                    // RUNNING_NOTIFIED: a wake (or an abort) landed mid-poll.
384                    // It was deliberately not enqueued then — that is this
385                    // path's job, and re-enqueueing at the tail is what keeps a
386                    // self-waking task from monopolising the worker.
387                    self.state.store(SCHEDULED, Ordering::Release);
388                    self.enqueue();
389                }
390            }
391        }
392    }
393}
394
395impl<T: Send + 'static> Schedulable for Task<T> {
396    fn schedule(self: Arc<Self>) {
397        loop {
398            match self.state.load(Ordering::Acquire) {
399                IDLE => {
400                    if self
401                        .state
402                        .compare_exchange(IDLE, SCHEDULED, Ordering::AcqRel, Ordering::Acquire)
403                        .is_ok()
404                    {
405                        self.enqueue();
406                        return;
407                    }
408                }
409                RUNNING => {
410                    // Defer to the poll that is in flight — it re-enqueues.
411                    if self
412                        .state
413                        .compare_exchange(
414                            RUNNING,
415                            RUNNING_NOTIFIED,
416                            Ordering::AcqRel,
417                            Ordering::Acquire,
418                        )
419                        .is_ok()
420                    {
421                        return;
422                    }
423                }
424                // SCHEDULED: already on a ring. RUNNING_NOTIFIED: already
425                // deferred. DONE: nothing to run. All redundant.
426                _ => return,
427            }
428        }
429    }
430}
431
432impl<T: Send + 'static> Wake for Task<T> {
433    fn wake(self: Arc<Self>) {
434        Schedulable::schedule(self);
435    }
436
437    fn wake_by_ref(self: &Arc<Self>) {
438        Schedulable::schedule(Arc::clone(self));
439    }
440}
441
442impl<T> Drop for Task<T> {
443    /// The task became unreachable — no ring entry, no live waker — so nothing
444    /// will ever poll it again. Resolve the joiner rather than strand it.
445    /// A no-op on the normal paths, where `finalize` has already run.
446    fn drop(&mut self) {
447        self.shared.finalize(Err(JoinError::cancelled()));
448    }
449}
450
451/// One ring entry for a task, with the "ran or was dropped" bookkeeping.
452///
453/// A [`Callback`] is a `FnOnce` that the pool may drop instead of calling — on
454/// a full ring, or when the band shuts down (`callback.c:237-284` semantics,
455/// see [`super::callback_executor::CallbackHandle::request`]). Either way this
456/// task's only scheduled run is gone and its state is stuck at `SCHEDULED`, so
457/// no later wake would re-enqueue it. `Drop` closes that: an entry that is
458/// dropped un-run finalizes its task as cancelled.
459struct Entry<T> {
460    task: Option<Arc<Task<T>>>,
461}
462
463impl<T: Send + 'static> Entry<T> {
464    fn run(&mut self) {
465        if let Some(task) = self.task.take() {
466            task.run();
467        }
468    }
469}
470
471impl<T> Drop for Entry<T> {
472    fn drop(&mut self) {
473        if let Some(task) = self.task.take() {
474            task.state.store(DONE, Ordering::Release);
475            // Drop the future here rather than leaving it to the task's own
476            // `Drop`, which may not run promptly if a waker still holds a
477            // reference. Its destructors are part of the cancel.
478            let _ = task.future.lock().unwrap().take();
479            task.shared.finalize(Err(JoinError::cancelled()));
480        }
481    }
482}
483
484/// A handle over a spawned task — the RTEMS-side mirror of
485/// `tokio::task::JoinHandle`. `await` it for `Result<T, JoinError>`.
486pub struct JoinFuture<T> {
487    shared: Arc<Shared<T>>,
488}
489
490impl<T> JoinFuture<T> {
491    /// Request cancellation — mirrors `tokio::task::JoinHandle::abort`.
492    /// Best-effort: the task is dropped at its next suspension point (or before
493    /// its first poll if not yet started). A task already inside a synchronous
494    /// stretch runs to its next `await` before the cancel is observed.
495    pub fn abort(&self) {
496        request_abort(&self.shared.control);
497    }
498
499    /// `true` once the task has produced its result — mirrors
500    /// `tokio::task::JoinHandle::is_finished`.
501    pub fn is_finished(&self) -> bool {
502        self.shared.control.finished.load(Ordering::Acquire)
503    }
504
505    /// A non-generic abort handle for this task — mirrors
506    /// `tokio::task::JoinHandle::abort_handle`.
507    pub fn abort_handle(&self) -> AbortHandle {
508        AbortHandle {
509            control: Arc::clone(&self.shared.control),
510        }
511    }
512}
513
514impl<T> Future for JoinFuture<T> {
515    type Output = Result<T, JoinError>;
516
517    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
518        let mut slot = self.shared.slot.lock().unwrap();
519        match slot.result.take() {
520            Some(result) => Poll::Ready(result),
521            None => {
522                // Re-register the latest waker (the joiner may have moved).
523                slot.join_waker = Some(cx.waker().clone());
524                Poll::Pending
525            }
526        }
527    }
528}
529
530/// A cancellation handle detached from a [`JoinFuture`] — the mirror of
531/// `tokio::task::AbortHandle`. Cloneable and non-generic, so it can be stored in
532/// heterogeneous collections (as ca/pva store `AbortHandle`s).
533#[derive(Clone)]
534pub struct AbortHandle {
535    control: Arc<Control>,
536}
537
538// `tokio::task::AbortHandle` is `Debug`, and call sites rely on it: pva's
539// `server_native::tcp::AbortOnDrop` is a `#[derive(Debug)]` newtype over
540// whichever handle the seam selected. `Control` holds a `Mutex<Option<Weak<dyn
541// Schedulable>>>` that cannot be derived, so the mirror is written out — the
542// two flags are the whole observable state of the handle.
543impl std::fmt::Debug for AbortHandle {
544    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545        f.debug_struct("AbortHandle")
546            .field("aborted", &self.control.abort.load(Ordering::Relaxed))
547            .field("finished", &self.control.finished.load(Ordering::Acquire))
548            .finish()
549    }
550}
551
552impl AbortHandle {
553    /// Request cancellation — mirrors `tokio::task::AbortHandle::abort`.
554    pub fn abort(&self) {
555        request_abort(&self.control);
556    }
557
558    /// `true` once the task has finished — mirrors
559    /// `tokio::task::AbortHandle::is_finished`.
560    pub fn is_finished(&self) -> bool {
561        self.control.finished.load(Ordering::Acquire)
562    }
563}
564
565/// Spawn `fut` onto the callback pool behind `callbacks`, polled on
566/// `priority`-band workers. Returns immediately with a [`JoinFuture`].
567///
568/// The task occupies a worker only for the duration of each poll; between polls
569/// it holds nothing (module docs). If the band's ring is full (C
570/// `S_db_bufFull`) the task cannot be enqueued and the returned handle resolves
571/// as cancelled with an error logged — mirroring the fact that a tokio spawn
572/// never fails while still giving the caller a handle that resolves.
573pub fn spawn_future<F>(
574    callbacks: &CallbackHandle,
575    priority: CallbackPriority,
576    fut: F,
577) -> JoinFuture<F::Output>
578where
579    F: Future + Send + 'static,
580    F::Output: Send + 'static,
581{
582    spawn_task(callbacks, priority, fut).0
583}
584
585/// [`spawn_future`], also handing back the task itself so a test can inspect
586/// its scheduling. Dropping the extra `Arc` is harmless: the ring entry holds
587/// its own reference (and if the enqueue was rejected, dropping the last
588/// reference is exactly what resolves the handle).
589fn spawn_task<F>(
590    callbacks: &CallbackHandle,
591    priority: CallbackPriority,
592    fut: F,
593) -> (JoinFuture<F::Output>, Arc<Task<F::Output>>)
594where
595    F: Future + Send + 'static,
596    F::Output: Send + 'static,
597{
598    let shared = Shared::new();
599    let task = Arc::new(Task {
600        // Claimed immediately: the spawn itself is the first schedule.
601        state: AtomicU8::new(SCHEDULED),
602        future: Mutex::new(Some(Box::pin(fut))),
603        shared: Arc::clone(&shared),
604        callbacks: callbacks.clone(),
605        priority,
606        #[cfg(test)]
607        enqueues: std::sync::atomic::AtomicUsize::new(0),
608    });
609    *shared.control.task.lock().unwrap() = Some(Arc::downgrade(&task) as Weak<dyn Schedulable>);
610    task.enqueue();
611    (JoinFuture { shared }, task)
612}
613
614/// Run a blocking closure `f` on a callback-pool worker — the RTEMS backend for
615/// [`crate::runtime::task::spawn_blocking`]. Returns a [`JoinFuture`] resolving
616/// to `f`'s return value.
617///
618/// A blocking closure has no suspension point, so it cannot be aborted
619/// mid-run (as `tokio::task::spawn_blocking` also cannot); `abort` before it
620/// starts still cancels it. A panic is isolated exactly as for
621/// [`spawn_future`]. It holds its worker for its whole run — that is what
622/// "blocking" means, and unlike a spawned future there is nothing to yield at.
623pub fn spawn_blocking_on<F, R>(
624    callbacks: &CallbackHandle,
625    priority: CallbackPriority,
626    f: F,
627) -> JoinFuture<R>
628where
629    F: FnOnce() -> R + Send + 'static,
630    R: Send + 'static,
631{
632    let shared = Shared::new();
633    let task_shared = Arc::clone(&shared);
634    // Same "the pool may drop a callback instead of calling it" hazard as a
635    // spawned future's ring entry: resolve the handle either way.
636    let mut guard = FinalizeOnDrop {
637        shared: Some(Arc::clone(&shared)),
638    };
639    let callback: Callback = Box::new(move || {
640        guard.defuse();
641        // Honor an abort that landed before we started running.
642        if task_shared.control.abort.load(Ordering::Acquire) {
643            task_shared.finalize(Err(JoinError::cancelled()));
644            return;
645        }
646        let outcome = catch_unwind(AssertUnwindSafe(f));
647        let result = match outcome {
648            Ok(value) => Ok(value),
649            Err(_panic) => Err(JoinError::panicked()),
650        };
651        task_shared.finalize(result);
652    });
653
654    if callbacks.request(priority, callback).is_err() {
655        tracing::error!(
656            target: "epics_base_rs::runtime::future_exec",
657            "spawn_blocking_on: callback ring full; closure dropped, handle resolves cancelled"
658        );
659    }
660    JoinFuture { shared }
661}
662
663/// Resolves a handle as cancelled unless [`defuse`](Self::defuse)d — the
664/// blocking-closure counterpart of [`Entry`]'s `Drop`.
665struct FinalizeOnDrop<R> {
666    shared: Option<Arc<Shared<R>>>,
667}
668
669impl<R> FinalizeOnDrop<R> {
670    fn defuse(&mut self) {
671        self.shared = None;
672    }
673}
674
675impl<R> Drop for FinalizeOnDrop<R> {
676    fn drop(&mut self) {
677        if let Some(shared) = self.shared.take() {
678            shared.finalize(Err(JoinError::cancelled()));
679        }
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686    use crate::runtime::background::callback_executor::{
687        CallbackPool, DEFAULT_QUEUE_SIZE, DEFAULT_THREADS_PER_PRIORITY,
688    };
689    use crate::runtime::background::delayed_timer::DelayedTimer;
690    use crate::runtime::background::timer_sleep::sleep;
691    use crate::runtime::task::park_on_interruptible as drive;
692    use std::sync::mpsc;
693    use std::time::Duration;
694
695    const T: Duration = Duration::from_secs(5);
696
697    /// Block the test thread on a `JoinFuture`, returning its `Result`.
698    fn join<T>(jf: JoinFuture<T>) -> Result<T, JoinError> {
699        drive(jf, || false).expect("uncancelled join returned None")
700    }
701
702    // `JoinFuture` is single-await; several tests need both a method call and a
703    // join. Re-expose the shared handle by cloning the Arc so the test can do
704    // both without moving the handle into `join`.
705    fn jf_reborrow<T>(jf: &JoinFuture<T>) -> JoinFuture<T> {
706        JoinFuture {
707            shared: Arc::clone(&jf.shared),
708        }
709    }
710
711    /// Returns `Pending` `n` times, waking itself each time, then `Ready`.
712    /// Exercises the wake-during-poll (`RUNNING_NOTIFIED`) edge on every step.
713    struct YieldN(usize);
714
715    impl Future for YieldN {
716        type Output = ();
717
718        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
719            if self.0 == 0 {
720                return Poll::Ready(());
721            }
722            self.0 -= 1;
723            cx.waker().wake_by_ref();
724            Poll::Pending
725        }
726    }
727
728    // -- basic execution ----------------------------------------------------
729
730    #[test]
731    fn future_runs_to_completion() {
732        let pool = CallbackPool::new();
733        let jf = spawn_future(&pool.handle(), DEFAULT_SPAWN_PRIORITY, async { 42u32 });
734        assert_eq!(join(jf).unwrap(), 42);
735    }
736
737    #[test]
738    fn future_awaiting_cross_thread_primitive_completes() {
739        // The A2 precondition: a spawned tail that awaits a runtime-agnostic
740        // tokio::sync primitive, woken from ANOTHER thread, must complete with
741        // no tokio runtime present.
742        let pool = CallbackPool::new();
743        let (tx, rx) = tokio::sync::oneshot::channel::<u32>();
744        let jf = spawn_future(&pool.handle(), DEFAULT_SPAWN_PRIORITY, async move {
745            rx.await.unwrap()
746        });
747        // Send from the test thread after the task has already yielded.
748        std::thread::sleep(Duration::from_millis(20));
749        tx.send(7).unwrap();
750        assert_eq!(join(jf).unwrap(), 7);
751    }
752
753    #[test]
754    fn panic_in_task_does_not_kill_the_worker() {
755        // callback.c:210-235 drain-loop invariant: one bad callback must not
756        // stop the band. The panicked task reports a non-cancelled JoinError,
757        // and a subsequent task on the SAME pool still runs.
758        let pool = CallbackPool::new();
759
760        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async {
761            panic!("boom");
762        });
763        let err = join(jf).unwrap_err();
764        assert!(!err.is_cancelled(), "a panic is not a cancellation");
765
766        // Same pool, same band — the worker survived and drains the next task.
767        let jf2 = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 99u32 });
768        assert_eq!(join(jf2).unwrap(), 99);
769    }
770
771    #[test]
772    fn panic_after_a_yield_is_isolated_too() {
773        // Boundary partner to the above: the panic happens on a LATER poll, so
774        // it unwinds out of a re-enqueued run rather than the first one.
775        let pool = CallbackPool::new();
776        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async {
777            YieldN(3).await;
778            panic!("late boom");
779        });
780        assert!(!join::<()>(jf).unwrap_err().is_cancelled());
781
782        let jf2 = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 7u32 });
783        assert_eq!(join(jf2).unwrap(), 7);
784    }
785
786    // -- invariant 4: a synchronous completion never round-trips the queue ---
787
788    #[test]
789    fn synchronously_ready_future_is_enqueued_exactly_once() {
790        // Boundary: zero suspensions. The spawn itself is one enqueue; a task
791        // that is Ready on its first poll must add no more.
792        let pool = CallbackPool::new();
793        let (jf, task) = spawn_task(&pool.handle(), CallbackPriority::Medium, async { 1u8 });
794        assert_eq!(join(jf).unwrap(), 1);
795        assert_eq!(
796            task.enqueues.load(Ordering::Relaxed),
797            1,
798            "a future that completes on its first poll must not round-trip the ring"
799        );
800    }
801
802    #[test]
803    fn each_suspension_costs_exactly_one_re_enqueue() {
804        // The other side of the same boundary: N suspensions → N re-enqueues,
805        // i.e. the queue is used once per wake and never speculatively.
806        let pool = CallbackPool::new();
807        let (jf, task) = spawn_task(&pool.handle(), CallbackPriority::Medium, YieldN(3));
808        join(jf).unwrap();
809        assert_eq!(
810            task.enqueues.load(Ordering::Relaxed),
811            4,
812            "expected 1 spawn + 3 wakes"
813        );
814    }
815
816    // -- invariant 3: a released worker is reused; a woken task does not starve
817
818    #[test]
819    fn a_yielding_task_releases_the_worker_to_a_queued_task() {
820        // Single worker, two tasks. `slow` yields many times; `quick` is queued
821        // behind it. Under the old park-a-worker design `quick` could not run
822        // until `slow` finished, so the order would be slow-then-quick. With
823        // the worker released at every suspension, `quick` — already on the
824        // ring when `slow` first re-enqueues at the TAIL — runs first.
825        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
826        let pool = CallbackPool::new();
827        let (tx, rx) = mpsc::channel::<&'static str>();
828
829        let tx_slow = tx.clone();
830        let slow = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
831            YieldN(20).await;
832            tx_slow.send("slow").unwrap();
833        });
834        let quick = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
835            tx.send("quick").unwrap();
836        });
837
838        assert_eq!(
839            rx.recv_timeout(T).unwrap(),
840            "quick",
841            "a suspended task must not hold the band's only worker"
842        );
843        assert_eq!(
844            rx.recv_timeout(T).unwrap(),
845            "slow",
846            "a repeatedly re-enqueued task must still make progress"
847        );
848        join(quick).unwrap();
849        join(slow).unwrap();
850    }
851
852    #[test]
853    fn many_self_waking_tasks_all_finish_on_one_worker() {
854        // No-starvation under contention: 8 tasks, each waking itself 25 times,
855        // multiplexed over a single worker. Tail re-enqueue makes this fair
856        // enough that every one of them terminates.
857        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
858        let pool = CallbackPool::new();
859        let handles: Vec<_> = (0..8)
860            .map(|i| {
861                spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
862                    YieldN(25).await;
863                    i
864                })
865            })
866            .collect();
867        for (i, jf) in handles.into_iter().enumerate() {
868            assert_eq!(join(jf).unwrap(), i);
869        }
870    }
871
872    // -- invariant 1: the sleep-wake inline path ----------------------------
873
874    #[test]
875    fn spawned_future_awaiting_sleep_does_not_self_deadlock() {
876        // Regression for the sleep-wake self-deadlock (bug_pattern
877        // rtems-exec-sleep-wake-band-deadlock): a future SPAWNED ON THE POOL
878        // that awaits `timer_sleep::sleep` must complete. This is the exact
879        // `spawn(async { sleep().await })` shape that ODLY/SDLY async-record
880        // reprocessing uses on the RTEMS backend.
881        //
882        // Why the existing `timer_sleep` unit tests did NOT catch it: they
883        // `drive()` the Sleep on the TEST thread, leaving the pool's single
884        // Medium worker free to run the wake callback. Here the future runs on
885        // the pool worker (via `spawn_future`) — so with the old behavior, where
886        // the sleep-wake was `sink.request(Medium, ...)`, the wake queued behind
887        // the very worker parked on this future (one worker per band) and never
888        // ran. We must OBSERVE completion via a channel, NOT `join()`/`drive()`
889        // on the test thread, or we would re-introduce the free-worker escape
890        // hatch and stop reproducing the deadlock.
891        let pool = CallbackPool::new();
892        let timer = DelayedTimer::new(pool.handle());
893        let th = timer.handle();
894
895        let (done_tx, done_rx) = mpsc::channel::<()>();
896        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
897            sleep(&th, Duration::from_millis(50)).await;
898            done_tx.send(()).unwrap();
899        });
900
901        // With the fix (inline wake on the timer thread) the sleep fires and the
902        // future finishes well inside T. With the pre-fix band-dispatch wake this
903        // recv times out because the wake starves behind the parked worker.
904        done_rx.recv_timeout(T).expect(
905            "spawned future awaiting sleep deadlocked (wake starved behind its own worker)",
906        );
907        assert!(join(jf).is_ok());
908    }
909
910    #[test]
911    fn a_sleep_wake_needs_no_pool_worker() {
912        // The inline-wake guarantee, stated directly: pin the band's ONLY
913        // worker inside an unrelated blocking callback, and a sleeping task's
914        // wake must still land. If the wake needed a worker it would sit behind
915        // the pinned one and `fired` would stay false.
916        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
917        let pool = CallbackPool::new();
918        let timer = DelayedTimer::new(pool.handle());
919
920        let (pinned_tx, pinned_rx) = mpsc::channel::<()>();
921        let (release_tx, release_rx) = mpsc::channel::<()>();
922        pool.request(
923            CallbackPriority::Medium,
924            Box::new(move || {
925                pinned_tx.send(()).unwrap();
926                release_rx.recv().unwrap();
927            }),
928        )
929        .unwrap();
930        pinned_rx.recv_timeout(T).unwrap(); // the sole Medium worker is busy
931
932        let (woke_tx, woke_rx) = mpsc::channel::<()>();
933        // Drive the Sleep on the TEST thread — no pool worker involved at all,
934        // so what this observes is purely "did the wake arrive".
935        let th = timer.handle();
936        let sleeper = std::thread::spawn(move || {
937            drive(sleep(&th, Duration::from_millis(30)), || false).unwrap();
938            woke_tx.send(()).unwrap();
939        });
940        woke_rx
941            .recv_timeout(T)
942            .expect("sleep wake did not arrive while the band's worker was pinned");
943
944        release_tx.send(()).unwrap();
945        sleeper.join().unwrap();
946    }
947
948    #[test]
949    fn three_sleeping_tails_share_one_worker() {
950        // The old failure mode, gone. DEFAULT_THREADS_PER_PRIORITY is 1, so
951        // under the park-a-worker-per-future design these three tails would
952        // have needed three workers: #1 would hold the only one for its whole
953        // sleep, and #2 and #3 could not even START until it finished.
954        //
955        // The assertion is structural, not a stopwatch: every task announces
956        // Started before it sleeps and Done after. All three Starteds must
957        // arrive before the first Done — which is only possible if each task
958        // handed the worker back at its suspension point.
959        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
960        let pool = CallbackPool::with_config(DEFAULT_QUEUE_SIZE, DEFAULT_THREADS_PER_PRIORITY);
961        let timer = DelayedTimer::new(pool.handle());
962
963        #[derive(Debug, PartialEq, Eq)]
964        enum Ev {
965            Started,
966            Done(u8),
967        }
968
969        let (tx, rx) = mpsc::channel::<Ev>();
970        let handles: Vec<_> = (0..3u8)
971            .map(|i| {
972                let th = timer.handle();
973                let tx = tx.clone();
974                spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
975                    tx.send(Ev::Started).unwrap();
976                    sleep(&th, Duration::from_millis(120)).await;
977                    tx.send(Ev::Done(i)).unwrap();
978                    i
979                })
980            })
981            .collect();
982        drop(tx);
983
984        for n in 0..3 {
985            assert_eq!(
986                rx.recv_timeout(T).unwrap(),
987                Ev::Started,
988                "task {n} had not started before the first task finished — the \
989                 worker was held across a suspension"
990            );
991        }
992        let mut done: Vec<u8> = (0..3)
993            .map(|_| match rx.recv_timeout(T).unwrap() {
994                Ev::Done(i) => i,
995                Ev::Started => panic!("only three tasks exist"),
996            })
997            .collect();
998        done.sort_unstable();
999        assert_eq!(done, vec![0, 1, 2], "all three tails must complete");
1000
1001        for (i, jf) in handles.into_iter().enumerate() {
1002            assert_eq!(join(jf).unwrap() as usize, i);
1003        }
1004    }
1005
1006    // -- invariant 2: abort ------------------------------------------------
1007
1008    #[test]
1009    fn abort_while_idle_cancels_cleanly() {
1010        // Boundary: the task is IDLE — parked between polls on a primitive that
1011        // will never fire, holding no worker. Nothing but the abort can wake
1012        // it, so this is what proves `request_abort` schedules.
1013        let pool = CallbackPool::new();
1014        let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
1015        let (ran_tx, ran_rx) = mpsc::channel();
1016        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1017            ran_tx.send(()).unwrap();
1018            let _ = rx.await;
1019        });
1020        // The task has run at least once and yielded.
1021        ran_rx.recv_timeout(T).unwrap();
1022        std::thread::sleep(Duration::from_millis(20));
1023
1024        jf.abort();
1025        let err = join(jf_reborrow(&jf)).unwrap_err();
1026        assert!(err.is_cancelled(), "aborted task must report cancelled");
1027        assert!(jf.is_finished());
1028    }
1029
1030    #[test]
1031    fn abort_before_the_first_poll_cancels_without_running() {
1032        // Boundary: the task is SCHEDULED but not yet RUNNING. Pin the band's
1033        // only worker so the task cannot start, abort it, then release. It must
1034        // resolve cancelled and its body must never have executed.
1035        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
1036        let pool = CallbackPool::new();
1037        let (pinned_tx, pinned_rx) = mpsc::channel::<()>();
1038        let (release_tx, release_rx) = mpsc::channel::<()>();
1039        pool.request(
1040            CallbackPriority::Medium,
1041            Box::new(move || {
1042                pinned_tx.send(()).unwrap();
1043                release_rx.recv().unwrap();
1044            }),
1045        )
1046        .unwrap();
1047        pinned_rx.recv_timeout(T).unwrap();
1048
1049        let ran = Arc::new(AtomicBool::new(false));
1050        let flag = Arc::clone(&ran);
1051        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1052            flag.store(true, Ordering::SeqCst);
1053        });
1054        jf.abort();
1055        release_tx.send(()).unwrap();
1056
1057        assert!(join(jf_reborrow(&jf)).unwrap_err().is_cancelled());
1058        assert!(
1059            !ran.load(Ordering::SeqCst),
1060            "an abort observed before the first poll must not run the future"
1061        );
1062    }
1063
1064    #[test]
1065    fn abort_during_a_poll_is_observed_at_the_next_one() {
1066        // Boundary: the abort lands while the task is RUNNING, i.e. inside a
1067        // synchronous stretch. tokio's contract (and the previous park-driver's)
1068        // is that it is observed at the NEXT suspension point, not mid-poll —
1069        // so the current poll finishes and the following one cancels.
1070        let pool = CallbackPool::new();
1071        let (in_poll_tx, in_poll_rx) = mpsc::channel::<()>();
1072        let (go_tx, go_rx) = mpsc::channel::<()>();
1073        let polls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1074        let counter = Arc::clone(&polls);
1075
1076        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1077            counter.fetch_add(1, Ordering::SeqCst);
1078            // Synchronous stretch: tell the test we are inside the poll and
1079            // block here until it has aborted us.
1080            in_poll_tx.send(()).unwrap();
1081            go_rx.recv().unwrap();
1082            YieldN(1).await; // the suspension point the cancel is observed at
1083            counter.fetch_add(100, Ordering::SeqCst);
1084        });
1085
1086        in_poll_rx.recv_timeout(T).unwrap();
1087        jf.abort(); // lands while the task is RUNNING
1088        go_tx.send(()).unwrap();
1089
1090        assert!(join(jf_reborrow(&jf)).unwrap_err().is_cancelled());
1091        assert_eq!(
1092            polls.load(Ordering::SeqCst),
1093            1,
1094            "the code after the suspension point must not have run"
1095        );
1096    }
1097
1098    #[test]
1099    fn abort_handle_cancels_the_task() {
1100        let pool = CallbackPool::new();
1101        let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
1102        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1103            let _ = rx.await;
1104        });
1105        let ah = jf.abort_handle();
1106        std::thread::sleep(Duration::from_millis(20));
1107        assert!(!ah.is_finished());
1108        ah.abort();
1109        assert!(join(jf).unwrap_err().is_cancelled());
1110        assert!(ah.is_finished());
1111    }
1112
1113    #[test]
1114    fn abort_after_completion_does_not_rewrite_the_outcome() {
1115        // Boundary: DONE. `finalize` is idempotent, so a late abort must not
1116        // turn a delivered value into a cancellation.
1117        let pool = CallbackPool::new();
1118        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 5u32 });
1119        while !jf.is_finished() {
1120            std::thread::sleep(Duration::from_millis(2));
1121        }
1122        jf.abort();
1123        assert_eq!(join(jf).unwrap(), 5);
1124    }
1125
1126    // -- every handle resolves ---------------------------------------------
1127
1128    #[test]
1129    fn full_ring_resolves_the_handle_as_cancelled() {
1130        // Boundary: the spawn never reaches a worker at all. C `S_db_bufFull`
1131        // (callback.c:373) — the task is dropped, so the handle must resolve
1132        // rather than strand its joiner.
1133        let pool = CallbackPool::with_config(1, 1);
1134        let (pinned_tx, pinned_rx) = mpsc::channel::<()>();
1135        let (release_tx, release_rx) = mpsc::channel::<()>();
1136        pool.request(
1137            CallbackPriority::Medium,
1138            Box::new(move || {
1139                pinned_tx.send(()).unwrap();
1140                release_rx.recv().unwrap();
1141            }),
1142        )
1143        .unwrap();
1144        pinned_rx.recv_timeout(T).unwrap();
1145
1146        // Fill the single ring slot, then latch overflow.
1147        pool.request(CallbackPriority::Medium, Box::new(|| {}))
1148            .unwrap();
1149        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 1u32 });
1150        assert!(
1151            join(jf).unwrap_err().is_cancelled(),
1152            "a rejected spawn must resolve its handle, not strand it"
1153        );
1154
1155        release_tx.send(()).unwrap();
1156    }
1157
1158    #[test]
1159    fn spawn_blocking_returns_value_and_isolates_panic() {
1160        let pool = CallbackPool::new();
1161        let jf = spawn_blocking_on(&pool.handle(), CallbackPriority::Medium, || 123u32);
1162        assert_eq!(join(jf).unwrap(), 123);
1163
1164        let jf = spawn_blocking_on(&pool.handle(), CallbackPriority::Medium, || {
1165            panic!("blocking boom")
1166        });
1167        assert!(!join::<()>(jf).unwrap_err().is_cancelled());
1168
1169        // Worker survived the panic.
1170        let jf = spawn_blocking_on(&pool.handle(), CallbackPriority::Medium, || 5u32);
1171        assert_eq!(join(jf).unwrap(), 5);
1172    }
1173}