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::Reactor::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 (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` — which is what the band does with it once shut down. That is
309        // the only way an entry goes un-run: it holds no ring slot, so a full
310        // ring cannot turn a wake into the silent end of a long-lived task.
311        let mut entry = Entry {
312            task: Some(Arc::clone(self)),
313        };
314        self.callbacks
315            .schedule_task(self.priority, Box::new(move || entry.run()));
316    }
317
318    /// Poll the task once on the calling worker, then either publish its
319    /// outcome or release the worker.
320    fn run(self: Arc<Self>) {
321        // SCHEDULED → RUNNING. Anything else means the task was finalized out
322        // from under this ring entry; there is nothing left to poll.
323        if self
324            .state
325            .compare_exchange(SCHEDULED, RUNNING, Ordering::AcqRel, Ordering::Acquire)
326            .is_err()
327        {
328            return;
329        }
330
331        let Some(mut fut) = self.future.lock().unwrap().take() else {
332            // Already consumed by a terminal path.
333            self.state.store(DONE, Ordering::Release);
334            return;
335        };
336
337        // Cancel is checked here, before the poll — the same point the previous
338        // park-driver checked it, so "the task is dropped at its next
339        // suspension point" is unchanged. Dropping `fut` here runs its
340        // destructors, exactly as a cancelled tokio task's does.
341        if self.shared.control.abort.load(Ordering::Acquire) {
342            drop(fut);
343            self.state.store(DONE, Ordering::Release);
344            self.shared.finalize(Err(JoinError::cancelled()));
345            return;
346        }
347
348        // The waker IS the task: waking re-enqueues it (module docs).
349        let waker = Waker::from(Arc::clone(&self));
350        // callback.c:210-235 drain-loop invariant: a panicking future must not
351        // tear down the band worker.
352        let polled = catch_unwind(AssertUnwindSafe(|| {
353            let mut cx = Context::from_waker(&waker);
354            fut.as_mut().poll(&mut cx)
355        }));
356
357        match polled {
358            Ok(Poll::Ready(value)) => {
359                drop(fut);
360                self.state.store(DONE, Ordering::Release);
361                self.shared.finalize(Ok(value));
362            }
363            Err(_panic) => {
364                drop(fut);
365                self.state.store(DONE, Ordering::Release);
366                self.shared.finalize(Err(JoinError::panicked()));
367            }
368            Ok(Poll::Pending) => {
369                // Put the future back BEFORE announcing we are schedulable
370                // again, or the worker that picks us up next could find an
371                // empty slot.
372                *self.future.lock().unwrap() = Some(fut);
373                if self
374                    .state
375                    .compare_exchange(RUNNING, IDLE, Ordering::AcqRel, Ordering::Acquire)
376                    .is_err()
377                {
378                    // RUNNING_NOTIFIED: a wake (or an abort) landed mid-poll.
379                    // It was deliberately not enqueued then — that is this
380                    // path's job, and re-enqueueing at the tail is what keeps a
381                    // self-waking task from monopolising the worker.
382                    self.state.store(SCHEDULED, Ordering::Release);
383                    self.enqueue();
384                }
385            }
386        }
387    }
388}
389
390impl<T: Send + 'static> Schedulable for Task<T> {
391    fn schedule(self: Arc<Self>) {
392        loop {
393            match self.state.load(Ordering::Acquire) {
394                IDLE => {
395                    if self
396                        .state
397                        .compare_exchange(IDLE, SCHEDULED, Ordering::AcqRel, Ordering::Acquire)
398                        .is_ok()
399                    {
400                        self.enqueue();
401                        return;
402                    }
403                }
404                RUNNING => {
405                    // Defer to the poll that is in flight — it re-enqueues.
406                    if self
407                        .state
408                        .compare_exchange(
409                            RUNNING,
410                            RUNNING_NOTIFIED,
411                            Ordering::AcqRel,
412                            Ordering::Acquire,
413                        )
414                        .is_ok()
415                    {
416                        return;
417                    }
418                }
419                // SCHEDULED: already on a ring. RUNNING_NOTIFIED: already
420                // deferred. DONE: nothing to run. All redundant.
421                _ => return,
422            }
423        }
424    }
425}
426
427impl<T: Send + 'static> Wake for Task<T> {
428    fn wake(self: Arc<Self>) {
429        Schedulable::schedule(self);
430    }
431
432    fn wake_by_ref(self: &Arc<Self>) {
433        Schedulable::schedule(Arc::clone(self));
434    }
435}
436
437impl<T> Drop for Task<T> {
438    /// The task became unreachable — no ring entry, no live waker — so nothing
439    /// will ever poll it again. Resolve the joiner rather than strand it.
440    /// A no-op on the normal paths, where `finalize` has already run.
441    fn drop(&mut self) {
442        self.shared.finalize(Err(JoinError::cancelled()));
443    }
444}
445
446/// One run-queue entry for a task, with the "ran or was dropped" bookkeeping.
447///
448/// A [`Callback`] is a `FnOnce` that the pool drops instead of calling when the
449/// band shuts down (`callback.c:237-284` semantics). Then this
450/// task's only scheduled run is gone and its state is stuck at `SCHEDULED`, so
451/// no later wake would re-enqueue it. `Drop` closes that: an entry that is
452/// dropped un-run finalizes its task as cancelled.
453struct Entry<T> {
454    task: Option<Arc<Task<T>>>,
455}
456
457impl<T: Send + 'static> Entry<T> {
458    fn run(&mut self) {
459        if let Some(task) = self.task.take() {
460            task.run();
461        }
462    }
463}
464
465impl<T> Drop for Entry<T> {
466    fn drop(&mut self) {
467        if let Some(task) = self.task.take() {
468            task.state.store(DONE, Ordering::Release);
469            // Drop the future here rather than leaving it to the task's own
470            // `Drop`, which may not run promptly if a waker still holds a
471            // reference. Its destructors are part of the cancel.
472            let _ = task.future.lock().unwrap().take();
473            task.shared.finalize(Err(JoinError::cancelled()));
474        }
475    }
476}
477
478/// A handle over a spawned task — the RTEMS-side mirror of
479/// `tokio::task::JoinHandle`. `await` it for `Result<T, JoinError>`.
480pub struct JoinFuture<T> {
481    shared: Arc<Shared<T>>,
482}
483
484impl<T> JoinFuture<T> {
485    /// Request cancellation — mirrors `tokio::task::JoinHandle::abort`.
486    /// Best-effort: the task is dropped at its next suspension point (or before
487    /// its first poll if not yet started). A task already inside a synchronous
488    /// stretch runs to its next `await` before the cancel is observed.
489    pub fn abort(&self) {
490        request_abort(&self.shared.control);
491    }
492
493    /// `true` once the task has produced its result — mirrors
494    /// `tokio::task::JoinHandle::is_finished`.
495    pub fn is_finished(&self) -> bool {
496        self.shared.control.finished.load(Ordering::Acquire)
497    }
498
499    /// A non-generic abort handle for this task — mirrors
500    /// `tokio::task::JoinHandle::abort_handle`.
501    pub fn abort_handle(&self) -> AbortHandle {
502        AbortHandle {
503            control: Arc::clone(&self.shared.control),
504        }
505    }
506}
507
508impl<T> Future for JoinFuture<T> {
509    type Output = Result<T, JoinError>;
510
511    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
512        let mut slot = self.shared.slot.lock().unwrap();
513        match slot.result.take() {
514            Some(result) => Poll::Ready(result),
515            None => {
516                // Re-register the latest waker (the joiner may have moved).
517                slot.join_waker = Some(cx.waker().clone());
518                Poll::Pending
519            }
520        }
521    }
522}
523
524/// A cancellation handle detached from a [`JoinFuture`] — the mirror of
525/// `tokio::task::AbortHandle`. Cloneable and non-generic, so it can be stored in
526/// heterogeneous collections (as ca/pva store `AbortHandle`s).
527#[derive(Clone)]
528pub struct AbortHandle {
529    control: Arc<Control>,
530}
531
532// `tokio::task::AbortHandle` is `Debug`, and call sites rely on it: pva's
533// `server_native::tcp::AbortOnDrop` is a `#[derive(Debug)]` newtype over
534// whichever handle the seam selected. `Control` holds a `Mutex<Option<Weak<dyn
535// Schedulable>>>` that cannot be derived, so the mirror is written out — the
536// two flags are the whole observable state of the handle.
537impl std::fmt::Debug for AbortHandle {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        f.debug_struct("AbortHandle")
540            .field("aborted", &self.control.abort.load(Ordering::Relaxed))
541            .field("finished", &self.control.finished.load(Ordering::Acquire))
542            .finish()
543    }
544}
545
546impl AbortHandle {
547    /// Request cancellation — mirrors `tokio::task::AbortHandle::abort`.
548    pub fn abort(&self) {
549        request_abort(&self.control);
550    }
551
552    /// `true` once the task has finished — mirrors
553    /// `tokio::task::AbortHandle::is_finished`.
554    pub fn is_finished(&self) -> bool {
555        self.control.finished.load(Ordering::Acquire)
556    }
557}
558
559/// Spawn `fut` onto the callback pool behind `callbacks`, polled on
560/// `priority`-band workers. Returns immediately with a [`JoinFuture`].
561///
562/// The task occupies a worker only for the duration of each poll; between polls
563/// it holds nothing (module docs). If the band's ring is full (C
564/// `S_db_bufFull`) the task cannot be enqueued and the returned handle resolves
565/// as cancelled with an error logged — mirroring the fact that a tokio spawn
566/// never fails while still giving the caller a handle that resolves.
567pub fn spawn_future<F>(
568    callbacks: &CallbackHandle,
569    priority: CallbackPriority,
570    fut: F,
571) -> JoinFuture<F::Output>
572where
573    F: Future + Send + 'static,
574    F::Output: Send + 'static,
575{
576    spawn_task(callbacks, priority, fut).0
577}
578
579/// [`spawn_future`], also handing back the task itself so a test can inspect
580/// its scheduling. Dropping the extra `Arc` is harmless: the ring entry holds
581/// its own reference (and if the enqueue was rejected, dropping the last
582/// reference is exactly what resolves the handle).
583fn spawn_task<F>(
584    callbacks: &CallbackHandle,
585    priority: CallbackPriority,
586    fut: F,
587) -> (JoinFuture<F::Output>, Arc<Task<F::Output>>)
588where
589    F: Future + Send + 'static,
590    F::Output: Send + 'static,
591{
592    let shared = Shared::new();
593    let task = Arc::new(Task {
594        // Claimed immediately: the spawn itself is the first schedule.
595        state: AtomicU8::new(SCHEDULED),
596        future: Mutex::new(Some(Box::pin(fut))),
597        shared: Arc::clone(&shared),
598        callbacks: callbacks.clone(),
599        priority,
600        #[cfg(test)]
601        enqueues: std::sync::atomic::AtomicUsize::new(0),
602    });
603    *shared.control.task.lock().unwrap() = Some(Arc::downgrade(&task) as Weak<dyn Schedulable>);
604    task.enqueue();
605    (JoinFuture { shared }, task)
606}
607
608/// Run a blocking closure `f` on a callback-pool worker — the RTEMS backend for
609/// [`crate::runtime::task::spawn_blocking`]. Returns a [`JoinFuture`] resolving
610/// to `f`'s return value.
611///
612/// A blocking closure has no suspension point, so it cannot be aborted
613/// mid-run (as `tokio::task::spawn_blocking` also cannot); `abort` before it
614/// starts still cancels it. A panic is isolated exactly as for
615/// [`spawn_future`]. It holds its worker for its whole run — that is what
616/// "blocking" means, and unlike a spawned future there is nothing to yield at.
617pub fn spawn_blocking_on<F, R>(
618    callbacks: &CallbackHandle,
619    priority: CallbackPriority,
620    f: F,
621) -> JoinFuture<R>
622where
623    F: FnOnce() -> R + Send + 'static,
624    R: Send + 'static,
625{
626    let shared = Shared::new();
627    let task_shared = Arc::clone(&shared);
628    // Same "the pool may drop a callback instead of calling it" hazard as a
629    // spawned future's ring entry: resolve the handle either way.
630    let mut guard = FinalizeOnDrop {
631        shared: Some(Arc::clone(&shared)),
632    };
633    let callback: Callback = Box::new(move || {
634        guard.defuse();
635        // Honor an abort that landed before we started running.
636        if task_shared.control.abort.load(Ordering::Acquire) {
637            task_shared.finalize(Err(JoinError::cancelled()));
638            return;
639        }
640        let outcome = catch_unwind(AssertUnwindSafe(f));
641        let result = match outcome {
642            Ok(value) => Ok(value),
643            Err(_panic) => Err(JoinError::panicked()),
644        };
645        task_shared.finalize(result);
646    });
647
648    if callbacks.request(priority, callback).is_err() {
649        tracing::error!(
650            target: "epics_base_rs::runtime::future_exec",
651            "spawn_blocking_on: callback ring full; closure dropped, handle resolves cancelled"
652        );
653    }
654    JoinFuture { shared }
655}
656
657/// Resolves a handle as cancelled unless [`defuse`](Self::defuse)d — the
658/// blocking-closure counterpart of [`Entry`]'s `Drop`.
659struct FinalizeOnDrop<R> {
660    shared: Option<Arc<Shared<R>>>,
661}
662
663impl<R> FinalizeOnDrop<R> {
664    fn defuse(&mut self) {
665        self.shared = None;
666    }
667}
668
669impl<R> Drop for FinalizeOnDrop<R> {
670    fn drop(&mut self) {
671        if let Some(shared) = self.shared.take() {
672            shared.finalize(Err(JoinError::cancelled()));
673        }
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680    use crate::runtime::background::callback_executor::{
681        CallbackPool, DEFAULT_QUEUE_SIZE, DEFAULT_THREADS_PER_PRIORITY,
682    };
683    use crate::runtime::background::delayed_timer::DelayedTimer;
684    use crate::runtime::background::timer_sleep::sleep;
685    use crate::runtime::task::park_on_interruptible as drive;
686    use std::sync::mpsc;
687    use std::time::Duration;
688
689    const T: Duration = Duration::from_secs(5);
690
691    /// Block the test thread on a `JoinFuture`, returning its `Result`.
692    fn join<T>(jf: JoinFuture<T>) -> Result<T, JoinError> {
693        drive(jf, || false).expect("uncancelled join returned None")
694    }
695
696    // `JoinFuture` is single-await; several tests need both a method call and a
697    // join. Re-expose the shared handle by cloning the Arc so the test can do
698    // both without moving the handle into `join`.
699    fn jf_reborrow<T>(jf: &JoinFuture<T>) -> JoinFuture<T> {
700        JoinFuture {
701            shared: Arc::clone(&jf.shared),
702        }
703    }
704
705    /// Suspends, waking itself each poll, until the flag is set — the
706    /// unbounded [`YieldN`]. For tests whose subject is *that* a task is
707    /// suspended rather than how often: a fixed count makes the test thread
708    /// race the worker, because the worker may exhaust every yield before the
709    /// thread reaches its next statement.
710    struct YieldUntil(Arc<AtomicBool>);
711
712    impl Future for YieldUntil {
713        type Output = ();
714
715        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
716            if self.0.load(Ordering::Acquire) {
717                return Poll::Ready(());
718            }
719            cx.waker().wake_by_ref();
720            Poll::Pending
721        }
722    }
723
724    /// Returns `Pending` `n` times, waking itself each time, then `Ready`.
725    /// Exercises the wake-during-poll (`RUNNING_NOTIFIED`) edge on every step.
726    struct YieldN(usize);
727
728    impl Future for YieldN {
729        type Output = ();
730
731        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
732            if self.0 == 0 {
733                return Poll::Ready(());
734            }
735            self.0 -= 1;
736            cx.waker().wake_by_ref();
737            Poll::Pending
738        }
739    }
740
741    // -- basic execution ----------------------------------------------------
742
743    #[test]
744    fn future_runs_to_completion() {
745        let pool = CallbackPool::new();
746        let jf = spawn_future(&pool.handle(), DEFAULT_SPAWN_PRIORITY, async { 42u32 });
747        assert_eq!(join(jf).unwrap(), 42);
748    }
749
750    #[test]
751    fn future_awaiting_cross_thread_primitive_completes() {
752        // The A2 precondition: a spawned tail that awaits a runtime-agnostic
753        // tokio::sync primitive, woken from ANOTHER thread, must complete with
754        // no tokio runtime present.
755        let pool = CallbackPool::new();
756        let (tx, rx) = tokio::sync::oneshot::channel::<u32>();
757        let jf = spawn_future(&pool.handle(), DEFAULT_SPAWN_PRIORITY, async move {
758            rx.await.unwrap()
759        });
760        // Send from the test thread after the task has already yielded.
761        std::thread::sleep(Duration::from_millis(20));
762        tx.send(7).unwrap();
763        assert_eq!(join(jf).unwrap(), 7);
764    }
765
766    #[test]
767    fn panic_in_task_does_not_kill_the_worker() {
768        // callback.c:210-235 drain-loop invariant: one bad callback must not
769        // stop the band. The panicked task reports a non-cancelled JoinError,
770        // and a subsequent task on the SAME pool still runs.
771        let pool = CallbackPool::new();
772
773        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async {
774            panic!("boom");
775        });
776        let err = join(jf).unwrap_err();
777        assert!(!err.is_cancelled(), "a panic is not a cancellation");
778
779        // Same pool, same band — the worker survived and drains the next task.
780        let jf2 = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 99u32 });
781        assert_eq!(join(jf2).unwrap(), 99);
782    }
783
784    #[test]
785    fn panic_after_a_yield_is_isolated_too() {
786        // Boundary partner to the above: the panic happens on a LATER poll, so
787        // it unwinds out of a re-enqueued run rather than the first one.
788        let pool = CallbackPool::new();
789        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async {
790            YieldN(3).await;
791            panic!("late boom");
792        });
793        assert!(!join::<()>(jf).unwrap_err().is_cancelled());
794
795        let jf2 = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 7u32 });
796        assert_eq!(join(jf2).unwrap(), 7);
797    }
798
799    // -- invariant 4: a synchronous completion never round-trips the queue ---
800
801    #[test]
802    fn synchronously_ready_future_is_enqueued_exactly_once() {
803        // Boundary: zero suspensions. The spawn itself is one enqueue; a task
804        // that is Ready on its first poll must add no more.
805        let pool = CallbackPool::new();
806        let (jf, task) = spawn_task(&pool.handle(), CallbackPriority::Medium, async { 1u8 });
807        assert_eq!(join(jf).unwrap(), 1);
808        assert_eq!(
809            task.enqueues.load(Ordering::Relaxed),
810            1,
811            "a future that completes on its first poll must not round-trip the ring"
812        );
813    }
814
815    #[test]
816    fn each_suspension_costs_exactly_one_re_enqueue() {
817        // The other side of the same boundary: N suspensions → N re-enqueues,
818        // i.e. the queue is used once per wake and never speculatively.
819        let pool = CallbackPool::new();
820        let (jf, task) = spawn_task(&pool.handle(), CallbackPriority::Medium, YieldN(3));
821        join(jf).unwrap();
822        assert_eq!(
823            task.enqueues.load(Ordering::Relaxed),
824            4,
825            "expected 1 spawn + 3 wakes"
826        );
827    }
828
829    // -- invariant 3: a released worker is reused; a woken task does not starve
830
831    #[test]
832    fn a_yielding_task_releases_the_worker_to_a_queued_task() {
833        // Single worker, two tasks. `slow` stays suspended until this thread
834        // releases it; `quick` is queued behind it. Under the old
835        // park-a-worker design `quick` could not run until `slow` finished, so
836        // it would never arrive at all.
837        //
838        // The gate is what makes the order an assertion rather than a race.
839        // Holding `slow` for a fixed number of yields instead leaves the
840        // executor free to burn all of them before this thread has enqueued
841        // `quick` — then "slow" arrives first through no fault of the
842        // executor, which is the flake this shape removes. Gated, `slow`
843        // *cannot* finish before "quick" is received, so "quick" arriving is
844        // itself the proof that a suspended task released the band's only
845        // worker.
846        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
847        let pool = CallbackPool::new();
848        let (tx, rx) = mpsc::channel::<&'static str>();
849        let gate = Arc::new(AtomicBool::new(false));
850
851        let tx_slow = tx.clone();
852        let gate_slow = Arc::clone(&gate);
853        let slow = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
854            YieldUntil(gate_slow).await;
855            tx_slow.send("slow").unwrap();
856        });
857        let quick = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
858            tx.send("quick").unwrap();
859        });
860
861        assert_eq!(
862            rx.recv_timeout(T).unwrap(),
863            "quick",
864            "a suspended task must not hold the band's only worker"
865        );
866        gate.store(true, Ordering::Release);
867        assert_eq!(
868            rx.recv_timeout(T).unwrap(),
869            "slow",
870            "a repeatedly re-enqueued task must still make progress"
871        );
872        join(quick).unwrap();
873        join(slow).unwrap();
874    }
875
876    #[test]
877    fn many_self_waking_tasks_all_finish_on_one_worker() {
878        // No-starvation under contention: 8 tasks, each waking itself 25 times,
879        // multiplexed over a single worker. Tail re-enqueue makes this fair
880        // enough that every one of them terminates.
881        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
882        let pool = CallbackPool::new();
883        let handles: Vec<_> = (0..8)
884            .map(|i| {
885                spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
886                    YieldN(25).await;
887                    i
888                })
889            })
890            .collect();
891        for (i, jf) in handles.into_iter().enumerate() {
892            assert_eq!(join(jf).unwrap(), i);
893        }
894    }
895
896    // -- invariant 1: the sleep-wake inline path ----------------------------
897
898    #[test]
899    fn spawned_future_awaiting_sleep_does_not_self_deadlock() {
900        // Regression for the sleep-wake self-deadlock (bug_pattern
901        // rtems-exec-sleep-wake-band-deadlock): a future SPAWNED ON THE POOL
902        // that awaits `timer_sleep::sleep` must complete. This is the exact
903        // `spawn(async { sleep().await })` shape that ODLY/SDLY async-record
904        // reprocessing uses on the RTEMS backend.
905        //
906        // Why the existing `timer_sleep` unit tests did NOT catch it: they
907        // `drive()` the Sleep on the TEST thread, leaving the pool's single
908        // Medium worker free to run the wake callback. Here the future runs on
909        // the pool worker (via `spawn_future`) — so with the old behavior, where
910        // the sleep-wake was `sink.request(Medium, ...)`, the wake queued behind
911        // the very worker parked on this future (one worker per band) and never
912        // ran. We must OBSERVE completion via a channel, NOT `join()`/`drive()`
913        // on the test thread, or we would re-introduce the free-worker escape
914        // hatch and stop reproducing the deadlock.
915        let pool = CallbackPool::new();
916        let timer = DelayedTimer::new(pool.handle());
917        let th = timer.handle();
918
919        let (done_tx, done_rx) = mpsc::channel::<()>();
920        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
921            sleep(&th, Duration::from_millis(50)).await;
922            done_tx.send(()).unwrap();
923        });
924
925        // With the fix (inline wake on the timer thread) the sleep fires and the
926        // future finishes well inside T. With the pre-fix band-dispatch wake this
927        // recv times out because the wake starves behind the parked worker.
928        done_rx.recv_timeout(T).expect(
929            "spawned future awaiting sleep deadlocked (wake starved behind its own worker)",
930        );
931        assert!(join(jf).is_ok());
932    }
933
934    #[test]
935    fn a_sleep_wake_needs_no_pool_worker() {
936        // The inline-wake guarantee, stated directly: pin the band's ONLY
937        // worker inside an unrelated blocking callback, and a sleeping task's
938        // wake must still land. If the wake needed a worker it would sit behind
939        // the pinned one and `fired` would stay false.
940        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
941        let pool = CallbackPool::new();
942        let timer = DelayedTimer::new(pool.handle());
943
944        let (pinned_tx, pinned_rx) = mpsc::channel::<()>();
945        let (release_tx, release_rx) = mpsc::channel::<()>();
946        pool.request(
947            CallbackPriority::Medium,
948            Box::new(move || {
949                pinned_tx.send(()).unwrap();
950                release_rx.recv().unwrap();
951            }),
952        )
953        .unwrap();
954        pinned_rx.recv_timeout(T).unwrap(); // the sole Medium worker is busy
955
956        let (woke_tx, woke_rx) = mpsc::channel::<()>();
957        // Drive the Sleep on the TEST thread — no pool worker involved at all,
958        // so what this observes is purely "did the wake arrive".
959        let th = timer.handle();
960        let sleeper = std::thread::spawn(move || {
961            drive(sleep(&th, Duration::from_millis(30)), || false).unwrap();
962            woke_tx.send(()).unwrap();
963        });
964        woke_rx
965            .recv_timeout(T)
966            .expect("sleep wake did not arrive while the band's worker was pinned");
967
968        release_tx.send(()).unwrap();
969        sleeper.join().unwrap();
970    }
971
972    #[test]
973    fn three_sleeping_tails_share_one_worker() {
974        // The old failure mode, gone. DEFAULT_THREADS_PER_PRIORITY is 1, so
975        // under the park-a-worker-per-future design these three tails would
976        // have needed three workers: #1 would hold the only one for its whole
977        // sleep, and #2 and #3 could not even START until it finished.
978        //
979        // The assertion is structural, not a stopwatch: every task announces
980        // Started before it sleeps and Done after. All three Starteds must
981        // arrive before the first Done — which is only possible if each task
982        // handed the worker back at its suspension point.
983        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
984        let pool = CallbackPool::with_config(DEFAULT_QUEUE_SIZE, DEFAULT_THREADS_PER_PRIORITY);
985        let timer = DelayedTimer::new(pool.handle());
986
987        #[derive(Debug, PartialEq, Eq)]
988        enum Ev {
989            Started,
990            Done(u8),
991        }
992
993        let (tx, rx) = mpsc::channel::<Ev>();
994        let handles: Vec<_> = (0..3u8)
995            .map(|i| {
996                let th = timer.handle();
997                let tx = tx.clone();
998                spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
999                    tx.send(Ev::Started).unwrap();
1000                    sleep(&th, Duration::from_millis(120)).await;
1001                    tx.send(Ev::Done(i)).unwrap();
1002                    i
1003                })
1004            })
1005            .collect();
1006        drop(tx);
1007
1008        for n in 0..3 {
1009            assert_eq!(
1010                rx.recv_timeout(T).unwrap(),
1011                Ev::Started,
1012                "task {n} had not started before the first task finished — the \
1013                 worker was held across a suspension"
1014            );
1015        }
1016        let mut done: Vec<u8> = (0..3)
1017            .map(|_| match rx.recv_timeout(T).unwrap() {
1018                Ev::Done(i) => i,
1019                Ev::Started => panic!("only three tasks exist"),
1020            })
1021            .collect();
1022        done.sort_unstable();
1023        assert_eq!(done, vec![0, 1, 2], "all three tails must complete");
1024
1025        for (i, jf) in handles.into_iter().enumerate() {
1026            assert_eq!(join(jf).unwrap() as usize, i);
1027        }
1028    }
1029
1030    // -- invariant 2: abort ------------------------------------------------
1031
1032    #[test]
1033    fn abort_while_idle_cancels_cleanly() {
1034        // Boundary: the task is IDLE — parked between polls on a primitive that
1035        // will never fire, holding no worker. Nothing but the abort can wake
1036        // it, so this is what proves `request_abort` schedules.
1037        let pool = CallbackPool::new();
1038        let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
1039        let (ran_tx, ran_rx) = mpsc::channel();
1040        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1041            ran_tx.send(()).unwrap();
1042            let _ = rx.await;
1043        });
1044        // The task has run at least once and yielded.
1045        ran_rx.recv_timeout(T).unwrap();
1046        std::thread::sleep(Duration::from_millis(20));
1047
1048        jf.abort();
1049        let err = join(jf_reborrow(&jf)).unwrap_err();
1050        assert!(err.is_cancelled(), "aborted task must report cancelled");
1051        assert!(jf.is_finished());
1052    }
1053
1054    #[test]
1055    fn abort_before_the_first_poll_cancels_without_running() {
1056        // Boundary: the task is SCHEDULED but not yet RUNNING. Pin the band's
1057        // only worker so the task cannot start, abort it, then release. It must
1058        // resolve cancelled and its body must never have executed.
1059        assert_eq!(DEFAULT_THREADS_PER_PRIORITY, 1);
1060        let pool = CallbackPool::new();
1061        let (pinned_tx, pinned_rx) = mpsc::channel::<()>();
1062        let (release_tx, release_rx) = mpsc::channel::<()>();
1063        pool.request(
1064            CallbackPriority::Medium,
1065            Box::new(move || {
1066                pinned_tx.send(()).unwrap();
1067                release_rx.recv().unwrap();
1068            }),
1069        )
1070        .unwrap();
1071        pinned_rx.recv_timeout(T).unwrap();
1072
1073        let ran = Arc::new(AtomicBool::new(false));
1074        let flag = Arc::clone(&ran);
1075        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1076            flag.store(true, Ordering::SeqCst);
1077        });
1078        jf.abort();
1079        release_tx.send(()).unwrap();
1080
1081        assert!(join(jf_reborrow(&jf)).unwrap_err().is_cancelled());
1082        assert!(
1083            !ran.load(Ordering::SeqCst),
1084            "an abort observed before the first poll must not run the future"
1085        );
1086    }
1087
1088    #[test]
1089    fn abort_during_a_poll_is_observed_at_the_next_one() {
1090        // Boundary: the abort lands while the task is RUNNING, i.e. inside a
1091        // synchronous stretch. tokio's contract (and the previous park-driver's)
1092        // is that it is observed at the NEXT suspension point, not mid-poll —
1093        // so the current poll finishes and the following one cancels.
1094        let pool = CallbackPool::new();
1095        let (in_poll_tx, in_poll_rx) = mpsc::channel::<()>();
1096        let (go_tx, go_rx) = mpsc::channel::<()>();
1097        let polls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1098        let counter = Arc::clone(&polls);
1099
1100        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1101            counter.fetch_add(1, Ordering::SeqCst);
1102            // Synchronous stretch: tell the test we are inside the poll and
1103            // block here until it has aborted us.
1104            in_poll_tx.send(()).unwrap();
1105            go_rx.recv().unwrap();
1106            YieldN(1).await; // the suspension point the cancel is observed at
1107            counter.fetch_add(100, Ordering::SeqCst);
1108        });
1109
1110        in_poll_rx.recv_timeout(T).unwrap();
1111        jf.abort(); // lands while the task is RUNNING
1112        go_tx.send(()).unwrap();
1113
1114        assert!(join(jf_reborrow(&jf)).unwrap_err().is_cancelled());
1115        assert_eq!(
1116            polls.load(Ordering::SeqCst),
1117            1,
1118            "the code after the suspension point must not have run"
1119        );
1120    }
1121
1122    #[test]
1123    fn abort_handle_cancels_the_task() {
1124        let pool = CallbackPool::new();
1125        let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
1126        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1127            let _ = rx.await;
1128        });
1129        let ah = jf.abort_handle();
1130        std::thread::sleep(Duration::from_millis(20));
1131        assert!(!ah.is_finished());
1132        ah.abort();
1133        assert!(join(jf).unwrap_err().is_cancelled());
1134        assert!(ah.is_finished());
1135    }
1136
1137    #[test]
1138    fn abort_after_completion_does_not_rewrite_the_outcome() {
1139        // Boundary: DONE. `finalize` is idempotent, so a late abort must not
1140        // turn a delivered value into a cancellation.
1141        let pool = CallbackPool::new();
1142        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 5u32 });
1143        while !jf.is_finished() {
1144            std::thread::sleep(Duration::from_millis(2));
1145        }
1146        jf.abort();
1147        assert_eq!(join(jf).unwrap(), 5);
1148    }
1149
1150    // -- every handle resolves ---------------------------------------------
1151
1152    /// Pin the band's single worker and fill its single ring slot, so the
1153    /// next `callbackRequest` is refused. Returns the release for the worker.
1154    fn saturate(pool: &CallbackPool) -> mpsc::Sender<()> {
1155        let (pinned_tx, pinned_rx) = mpsc::channel::<()>();
1156        let (release_tx, release_rx) = mpsc::channel::<()>();
1157        pool.request(
1158            CallbackPriority::Medium,
1159            Box::new(move || {
1160                pinned_tx.send(()).unwrap();
1161                release_rx.recv().unwrap();
1162            }),
1163        )
1164        .unwrap();
1165        pinned_rx.recv_timeout(T).unwrap();
1166        pool.request(CallbackPriority::Medium, Box::new(|| {}))
1167            .unwrap();
1168        assert!(
1169            pool.request(CallbackPriority::Medium, Box::new(|| {}))
1170                .is_err()
1171        );
1172        release_tx
1173    }
1174
1175    #[test]
1176    fn a_spawn_onto_a_full_ring_still_runs() {
1177        // Boundary: ring at capacity when the task is first queued.
1178        let pool = CallbackPool::with_config(1, 1);
1179        let release = saturate(&pool);
1180        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async { 1u32 });
1181        release.send(()).unwrap();
1182        assert_eq!(join(jf).unwrap(), 1);
1183    }
1184
1185    #[test]
1186    fn a_wake_onto_a_full_ring_is_not_lost() {
1187        // Boundary: ring at capacity when a suspended task is woken. A
1188        // long-lived consumer loop dies silently if this wake is refused.
1189        let pool = CallbackPool::with_config(1, 1);
1190        let (tx, rx) = tokio::sync::oneshot::channel::<u32>();
1191        let (polled_tx, polled_rx) = mpsc::channel::<()>();
1192        let jf = spawn_future(&pool.handle(), CallbackPriority::Medium, async move {
1193            polled_tx.send(()).unwrap();
1194            rx.await.unwrap()
1195        });
1196        polled_rx.recv_timeout(T).unwrap();
1197
1198        let release = saturate(&pool);
1199        tx.send(7).unwrap();
1200        release.send(()).unwrap();
1201        assert_eq!(join(jf).unwrap(), 7);
1202    }
1203
1204    #[test]
1205    fn spawn_blocking_returns_value_and_isolates_panic() {
1206        let pool = CallbackPool::new();
1207        let jf = spawn_blocking_on(&pool.handle(), CallbackPriority::Medium, || 123u32);
1208        assert_eq!(join(jf).unwrap(), 123);
1209
1210        let jf = spawn_blocking_on(&pool.handle(), CallbackPriority::Medium, || {
1211            panic!("blocking boom")
1212        });
1213        assert!(!join::<()>(jf).unwrap_err().is_cancelled());
1214
1215        // Worker survived the panic.
1216        let jf = spawn_blocking_on(&pool.handle(), CallbackPriority::Medium, || 5u32);
1217        assert_eq!(join(jf).unwrap(), 5);
1218    }
1219}