Skip to main content

scheduler/
executor.rs

1use crate::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer};
2use async_task::Runnable;
3use std::{
4    any::Any,
5    collections::HashMap,
6    future::Future,
7    marker::PhantomData,
8    mem::ManuallyDrop,
9    panic::Location,
10    pin::Pin,
11    rc::Rc,
12    sync::{
13        Arc, Mutex, Weak,
14        atomic::{AtomicBool, Ordering},
15    },
16    task::{Context, Poll, Waker},
17    thread::{self, ThreadId},
18    time::Duration,
19};
20
21/// The type-erased closure that [`Scheduler::spawn_dedicated`] runs on a new
22/// session.
23///
24/// It receives the session's [`LocalExecutor`] and returns a future whose
25/// output is boxed as `Box<dyn Any + Send + Sync>`, which keeps the trait
26/// object-safe. Call [`LocalExecutor::spawn_dedicated`] or
27/// [`BackgroundExecutor::spawn_dedicated`] instead of building one by hand.
28pub type DedicatedFn = Box<
29    dyn FnOnce(LocalExecutor) -> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
30        + Send
31        + 'static,
32>;
33
34/// Tracks local tasks that a platform must cancel on their owning thread
35/// before retiring a thread-bound event loop.
36#[doc(hidden)]
37pub struct LocalTaskScope {
38    retiring: AtomicBool,
39    state: Mutex<LocalTaskState>,
40}
41
42struct LocalTaskState {
43    next_task_id: u64,
44    tasks: HashMap<u64, Option<Waker>>,
45    sealed: bool,
46}
47
48impl LocalTaskScope {
49    /// Creates an empty owner-thread task scope.
50    pub fn new() -> Self {
51        Self {
52            retiring: AtomicBool::new(false),
53            state: Mutex::new(LocalTaskState {
54                next_task_id: 0,
55                tasks: HashMap::new(),
56                sealed: false,
57            }),
58        }
59    }
60
61    /// Whether retirement has started.
62    pub fn is_retiring(&self) -> bool {
63        self.retiring.load(Ordering::Acquire)
64    }
65
66    /// Returns the number of local futures that have not yet been destroyed.
67    pub fn live_task_count(&self) -> usize {
68        self.state
69            .lock()
70            .expect("local task scope poisoned")
71            .tasks
72            .len()
73    }
74
75    /// Latches retirement and wakes registered tasks without holding the
76    /// registry lock. The platform must drop their runnables on the owner
77    /// thread before treating this scope as quiescent.
78    pub fn begin_retirement(&self) {
79        if self.retiring.swap(true, Ordering::AcqRel) {
80            return;
81        }
82        let wakers = {
83            let mut state = self.state.lock().expect("local task scope poisoned");
84            state
85                .tasks
86                .values_mut()
87                .filter_map(Option::take)
88                .collect::<Vec<_>>()
89        };
90        for waker in wakers {
91            waker.wake();
92        }
93    }
94
95    /// Seals task admission after retirement when every registered future is destroyed.
96    ///
97    /// A platform calls this only after its owner-thread queues are empty. The task registry
98    /// lock makes the empty check atomic with later spawn registration.
99    #[doc(hidden)]
100    pub fn seal_if_quiescent(&self) -> bool {
101        let mut state = self.state.lock().expect("local task scope poisoned");
102        if !self.is_retiring() || !state.tasks.is_empty() {
103            return false;
104        }
105        state.sealed = true;
106        true
107    }
108
109    fn register(&self) -> u64 {
110        let mut state = self.state.lock().expect("local task scope poisoned");
111        assert!(
112            !state.sealed,
113            "cannot spawn a local task after its owner scope has been sealed"
114        );
115        let id = state.next_task_id;
116        state.next_task_id = state
117            .next_task_id
118            .checked_add(1)
119            .expect("local task scope task ID overflow");
120        state.tasks.insert(id, None);
121        id
122    }
123
124    fn attach_waker(&self, id: u64, waker: Waker) {
125        let mut waker = Some(waker);
126        let replaced = {
127            let mut state = self.state.lock().expect("local task scope poisoned");
128            state
129                .tasks
130                .get_mut(&id)
131                .and_then(|entry| entry.replace(waker.take().unwrap()))
132        };
133        drop(replaced);
134        drop(waker);
135    }
136
137    fn unregister(&self, id: u64) {
138        let removed = self
139            .state
140            .lock()
141            .expect("local task scope poisoned")
142            .tasks
143            .remove(&id);
144        drop(removed);
145    }
146}
147
148impl Default for LocalTaskScope {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154struct LocalTaskRegistration {
155    scope: Weak<LocalTaskScope>,
156    id: u64,
157}
158
159impl Drop for LocalTaskRegistration {
160    fn drop(&mut self) {
161        if let Some(scope) = self.scope.upgrade() {
162            scope.unregister(self.id);
163        }
164    }
165}
166
167struct RegisteredLocalFuture<F> {
168    future: F,
169    // Rust drops fields in declaration order: the user future is destroyed
170    // before the registration is removed from its scope.
171    _registration: LocalTaskRegistration,
172}
173
174impl<F: Future> Future for RegisteredLocalFuture<F> {
175    type Output = F::Output;
176
177    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
178        // The future remains pinned in its original field.
179        unsafe { self.map_unchecked_mut(|this| &mut this.future) }.poll(cx)
180    }
181}
182
183/// An executor for one session, for futures that are not `Send`.
184///
185/// Tasks spawned on a `LocalExecutor` run in order on whichever thread drains
186/// the dispatch destination it was built with. That is the main thread for the
187/// default session, or a dedicated thread for a session created with
188/// [`LocalExecutor::spawn_dedicated`]. Futures spawned here are polled and
189/// dropped on the thread that spawned them.
190#[derive(Clone)]
191pub struct LocalExecutor {
192    session_id: SessionId,
193    scheduler: Arc<dyn Scheduler>,
194    // Spawned tasks' schedule callbacks each hold an `Arc` clone of this
195    // closure, so the destination it captures stays alive as long as work
196    // could still land on it.
197    dispatch: Arc<dyn Fn(Runnable<RunnableMeta>) + Send + Sync>,
198    retirement: Option<Arc<LocalTaskScope>>,
199    dispatch_cancellation: Option<Arc<dyn Fn(Runnable<RunnableMeta>) + Send + Sync>>,
200    not_send: PhantomData<Rc<()>>,
201}
202
203impl LocalExecutor {
204    /// Creates a local executor for `session_id` that sends each spawned task's
205    /// runnables to `dispatch`.
206    ///
207    /// For the default session, `dispatch` usually calls
208    /// [`Scheduler::schedule_local`]. The dedicated-session path passes a
209    /// closure that sends to the dedicated thread's channel. `scheduler` gives
210    /// the executor access to clocks, timers and the other scheduler services.
211    pub fn new(
212        session_id: SessionId,
213        scheduler: Arc<dyn Scheduler>,
214        dispatch: impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static,
215    ) -> Self {
216        Self {
217            session_id,
218            scheduler,
219            dispatch: Arc::new(dispatch),
220            retirement: None,
221            dispatch_cancellation: None,
222            not_send: PhantomData,
223        }
224    }
225
226    /// Creates a local executor whose tasks are tracked by a platform-owned
227    /// retirement scope. Cancellation dispatch must enqueue runnables for
228    /// owner-thread destruction and must not invoke user code inline.
229    #[doc(hidden)]
230    pub fn new_with_scope(
231        session_id: SessionId,
232        scheduler: Arc<dyn Scheduler>,
233        scope: Arc<LocalTaskScope>,
234        dispatch: impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static,
235        dispatch_cancellation: impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static,
236    ) -> Self {
237        Self {
238            session_id,
239            scheduler,
240            dispatch: Arc::new(dispatch),
241            retirement: Some(scope),
242            dispatch_cancellation: Some(Arc::new(dispatch_cancellation)),
243            not_send: PhantomData,
244        }
245    }
246
247    /// Returns this executor's platform retirement scope, if present.
248    #[doc(hidden)]
249    pub fn retirement_scope(&self) -> Option<Arc<LocalTaskScope>> {
250        self.retirement.clone()
251    }
252
253    /// Returns the session this executor runs tasks for.
254    pub fn session_id(&self) -> SessionId {
255        self.session_id
256    }
257
258    /// Returns the scheduler that this executor's timers and clock come from.
259    pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
260        &self.scheduler
261    }
262
263    /// Whether this executor runs on a TestScheduler.
264    pub fn is_test(&self) -> bool {
265        self.scheduler.as_test().is_some()
266    }
267
268    /// Spawns a future on this session and returns a [`Task`] for its output.
269    ///
270    /// The future doesn't need to be `Send`. It is polled on the thread that
271    /// spawned it, so it runs on the session's thread when called from there.
272    /// Dropping the returned task cancels the future; call [`Task::detach`] to
273    /// let it run to completion instead.
274    #[track_caller]
275    pub fn spawn<F>(&self, future: F) -> Task<F::Output>
276    where
277        F: Future + 'static,
278        F::Output: 'static,
279    {
280        let dispatch = self.dispatch.clone();
281        self.spawn_with_dispatch(future, move |runnable| dispatch(runnable))
282    }
283
284    /// Like [`Self::spawn`], but sends the task's runnables to `dispatch`
285    /// instead of this executor's own destination.
286    ///
287    /// `dispatch` must deliver runnables back to this executor's thread. The
288    /// future is polled and dropped on the thread that spawned it.
289    #[track_caller]
290    pub fn spawn_with_dispatch<F>(
291        &self,
292        future: F,
293        dispatch: impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static,
294    ) -> Task<F::Output>
295    where
296        F: Future + 'static,
297        F::Output: 'static,
298    {
299        let location = Location::caller();
300        let metadata = RunnableMeta {
301            location,
302            spawned: crate::SpawnTime(Instant::now()),
303        };
304        if let Some(scope) = self.retirement.clone() {
305            let id = scope.register();
306            let registration = LocalTaskRegistration {
307                scope: Arc::downgrade(&scope),
308                id,
309            };
310            let future = RegisteredLocalFuture {
311                future,
312                _registration: registration,
313            };
314            let cancellation = self
315                .dispatch_cancellation
316                .as_ref()
317                .expect("scoped executor must have a cancellation dispatcher")
318                .clone();
319            let normal_dispatch: Arc<dyn Fn(Runnable<RunnableMeta>) + Send + Sync> =
320                Arc::new(dispatch);
321            let scope_for_schedule = scope.clone();
322            let schedule: Arc<dyn Fn(Runnable<RunnableMeta>) + Send + Sync> =
323                Arc::new(move |runnable| {
324                    if scope_for_schedule.is_retiring() {
325                        cancellation(runnable);
326                    } else {
327                        normal_dispatch(runnable);
328                    }
329                });
330            let (runnable, task) = spawn_local_with_source_location(
331                future,
332                move |runnable| schedule(runnable),
333                metadata,
334            );
335            // Registration precedes initial scheduling. If retirement raced
336            // with construction, that initial schedule goes to owner cancellation.
337            scope.attach_waker(id, runnable.waker());
338            runnable.schedule();
339            Task(TaskState::Spawned(task))
340        } else {
341            let (runnable, task) = spawn_local_with_source_location(future, dispatch, metadata);
342            runnable.schedule();
343            Task(TaskState::Spawned(task))
344        }
345    }
346
347    /// Blocks the current thread until `future` completes, and returns its output.
348    ///
349    /// Use this where async code can't be used, such as a test or the entry
350    /// point of a session. Not available on wasm, where the main thread can't
351    /// block.
352    ///
353    /// # Panics
354    ///
355    /// Panics if the scheduler stops before `future` completes, for example
356    /// because `future` waits on work that can never run.
357    #[cfg(not(target_family = "wasm"))]
358    pub fn block_on<Fut: Future>(&self, future: Fut) -> Fut::Output {
359        use std::cell::Cell;
360
361        let output = Cell::new(None);
362        let future = async {
363            output.set(Some(future.await));
364        };
365        let mut future = std::pin::pin!(future);
366
367        self.scheduler
368            .block(Some(self.session_id), future.as_mut(), None);
369
370        output.take().expect("block_on future did not complete")
371    }
372
373    /// Blocks the current thread until `future` completes or `timeout` elapses.
374    ///
375    /// Returns `Ok` with the output if the future completed in time. On timeout
376    /// it returns `Err` with the unfinished future. Await that future later to
377    /// keep waiting, or drop it to cancel the work. The future may already have
378    /// run part of its work.
379    #[cfg(not(target_family = "wasm"))]
380    pub fn block_with_timeout<Fut: Future>(
381        &self,
382        timeout: Duration,
383        future: Fut,
384    ) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
385        use std::cell::Cell;
386
387        let output = Cell::new(None);
388        let mut future = Box::pin(future);
389
390        {
391            let future_ref = &mut future;
392            let wrapper = async {
393                output.set(Some(future_ref.await));
394            };
395            let mut wrapper = std::pin::pin!(wrapper);
396
397            self.scheduler
398                .block(Some(self.session_id), wrapper.as_mut(), Some(timeout));
399        }
400
401        match output.take() {
402            Some(value) => Ok(value),
403            None => Err(future),
404        }
405    }
406
407    /// Returns a [`Timer`] that completes after `duration`.
408    #[track_caller]
409    pub fn timer(&self, duration: Duration) -> Timer {
410        self.scheduler.timer(duration)
411    }
412
413    /// Returns the current time from this scheduler's clock.
414    ///
415    /// Under the test scheduler this is the simulated clock, which moves only
416    /// when the test advances it.
417    pub fn now(&self) -> Instant {
418        self.scheduler.clock().now()
419    }
420
421    /// Spawns a closure on a new session that has its own [`LocalExecutor`].
422    ///
423    /// The closure runs on a new OS thread under the platform scheduler, or
424    /// on the test scheduler's loop under `TestScheduler`. Its future can use
425    /// the executor it receives to spawn more work on that thread.
426    ///
427    /// The returned task is the closure's work. Dropping it cancels the work,
428    /// awaiting it yields the closure's return value, and [`Task::detach`]
429    /// lets the work run on after the caller is done with it.
430    #[track_caller]
431    pub fn spawn_dedicated<F, Fut>(&self, f: F) -> Task<Fut::Output>
432    where
433        F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
434        Fut: Future + 'static,
435        Fut::Output: Send + Sync + 'static,
436    {
437        self.scheduler
438            .clone()
439            .spawn_dedicated(box_dedicated(f))
440            .downcast::<Fut::Output>()
441    }
442}
443
444/// Boxes the user-supplied dedicated closure into the type-erased shape
445/// expected by [`Scheduler::spawn_dedicated`]. The user's `Fut::Output` is
446/// boxed as `Box<dyn Any + Send + Sync>` on the dedicated side and downcast
447/// back to `Fut::Output` by [`Task::downcast`] in the wrapper.
448fn box_dedicated<F, Fut>(f: F) -> DedicatedFn
449where
450    F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
451    Fut: Future + 'static,
452    Fut::Output: Send + Sync + 'static,
453{
454    Box::new(move |executor| {
455        Box::pin(async move { Box::new(f(executor).await) as Box<dyn Any + Send + Sync> })
456    })
457}
458
459/// An executor that runs `Send` futures on the background thread pool.
460///
461/// Applications get one from `background_executor` on their context. Cloning
462/// is cheap, and every clone shares the same scheduler.
463#[derive(Clone)]
464pub struct BackgroundExecutor {
465    scheduler: Arc<dyn Scheduler>,
466}
467
468impl BackgroundExecutor {
469    /// Creates a background executor that spawns onto `scheduler`.
470    pub fn new(scheduler: Arc<dyn Scheduler>) -> Self {
471        Self { scheduler }
472    }
473
474    /// Spawns a future on the background thread pool with the default priority.
475    ///
476    /// The future may run on any background thread, so it must be [`Send`]. Dropping
477    /// the returned [`Task`] cancels the future. Call [`Task::detach`] to let
478    /// it finish on its own. Use [`LocalExecutor::spawn`] for futures that
479    /// aren't `Send` or that need to stay on the main thread.
480    #[track_caller]
481    pub fn spawn<F>(&self, future: F) -> Task<F::Output>
482    where
483        F: Future + Send + 'static,
484        F::Output: Send + 'static,
485    {
486        self.spawn_with_priority(Priority::default(), future)
487    }
488
489    /// Spawns a future on the background thread pool at `priority`.
490    ///
491    /// Like [`Self::spawn`], but the scheduler uses `priority` to order this
492    /// future against other background work waiting to run.
493    #[track_caller]
494    pub fn spawn_with_priority<F>(&self, priority: Priority, future: F) -> Task<F::Output>
495    where
496        F: Future + Send + 'static,
497        F::Output: Send + 'static,
498    {
499        let schedule = self.schedule_with_priority(priority);
500        let location = Location::caller();
501        let (runnable, task) = async_task::Builder::new()
502            .metadata(RunnableMeta {
503                location,
504                spawned: crate::SpawnTime(Instant::now()),
505            })
506            .spawn(move |_| future, schedule);
507        runnable.schedule();
508        Task(TaskState::Spawned(task))
509    }
510
511    /// Spawns a future on the platform's realtime thread, for latency-sensitive
512    /// work such as audio processing.
513    ///
514    /// The future must be [`Send`]. Use [`Self::spawn`] for everything else.
515    #[track_caller]
516    pub fn spawn_realtime<F>(&self, future: F) -> Task<F::Output>
517    where
518        F: Future + Send + 'static,
519        F::Output: Send + 'static,
520    {
521        let location = Location::caller();
522        let (tx, rx) = flume::bounded::<async_task::Runnable<RunnableMeta>>(1);
523
524        self.scheduler.spawn_realtime(Box::new(move || {
525            while let Ok(runnable) = rx.recv() {
526                runnable.run();
527            }
528        }));
529
530        let (runnable, task) = async_task::Builder::new()
531            .metadata(RunnableMeta {
532                location,
533                spawned: crate::SpawnTime(Instant::now()),
534            })
535            .spawn(
536                move |_| future,
537                move |runnable| {
538                    let _ = tx.send(runnable);
539                },
540            );
541        runnable.schedule();
542        Task(TaskState::Spawned(task))
543    }
544
545    /// Returns a [`Timer`] that completes after `duration`.
546    #[track_caller]
547    pub fn timer(&self, duration: Duration) -> Timer {
548        self.scheduler.timer(duration)
549    }
550
551    /// Returns the current time from this scheduler's clock.
552    ///
553    /// Under the test scheduler this is the simulated clock, which moves only
554    /// when the test advances it.
555    pub fn now(&self) -> Instant {
556        self.scheduler.clock().now()
557    }
558
559    /// Returns the scheduler that this executor spawns onto.
560    pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
561        &self.scheduler
562    }
563
564    /// Whether this executor runs on a TestScheduler.
565    pub fn is_test(&self) -> bool {
566        self.scheduler.as_test().is_some()
567    }
568
569    /// Spawns a closure on a new session that has its own [`LocalExecutor`].
570    ///
571    /// The closure runs on a new OS thread under the platform scheduler, or on
572    /// the test scheduler's loop under `TestScheduler`. Its future can use
573    /// the executor it receives to spawn more work on that thread.
574    ///
575    /// The returned task is the closure's work. Dropping it cancels the work,
576    /// awaiting it yields the closure's return value, and [`Task::detach`]
577    /// lets the work run on after the caller is done with it.
578    #[track_caller]
579    pub fn spawn_dedicated<F, Fut>(&self, f: F) -> Task<Fut::Output>
580    where
581        F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
582        Fut: Future + 'static,
583        Fut::Output: Send + Sync + 'static,
584    {
585        self.scheduler
586            .clone()
587            .spawn_dedicated(box_dedicated(f))
588            .downcast::<Fut::Output>()
589    }
590
591    fn schedule_with_priority(
592        &self,
593        priority: Priority,
594    ) -> impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static {
595        let scheduler = Arc::downgrade(&self.scheduler);
596        move |runnable| {
597            if let Some(scheduler) = scheduler.upgrade() {
598                scheduler.schedule_background_with_priority(runnable, priority);
599            }
600        }
601    }
602}
603
604/// A handle to one long-lived session. Every future spawned on it runs on that
605/// session's single thread, never on the background pool.
606///
607/// Use this when tasks rely on thread-local state that must stay the same
608/// across spawns, or when per-thread setup is expensive enough to pay once.
609/// Creating the handle starts a thread (on the web, a worker, which needs the
610/// `wasm-threads` feature and is costly). Each later spawn only sends a message
611/// to that thread.
612///
613/// Dropping the handle cancels the session. Futures that haven't run yet are
614/// cancelled: awaiting one of their [`Task`]s panics, and a
615/// [`FallibleTask`] resolves to `None`.
616pub struct DedicatedExecutor {
617    sender: flume::Sender<Runnable<RunnableMeta>>,
618    _session: Task<()>,
619}
620
621impl DedicatedExecutor {
622    /// Starts a dedicated session on `executor` and returns a handle that
623    /// spawns futures onto it.
624    ///
625    /// Under `TestScheduler` the session runs on the deterministic test loop,
626    /// and no real thread is created.
627    #[track_caller]
628    pub fn new(executor: &BackgroundExecutor) -> Self {
629        let (sender, receiver) = flume::unbounded::<Runnable<RunnableMeta>>();
630        let session = executor.spawn_dedicated(move |_executor| async move {
631            while let Ok(runnable) = receiver.recv_async().await {
632                runnable.run();
633            }
634        });
635        Self {
636            sender,
637            _session: session,
638        }
639    }
640
641    /// Spawns a future onto the dedicated session's thread and returns a
642    /// [`Task`] for its output.
643    ///
644    /// The future must be [`Send`], because it moves to the session's thread.
645    /// Dropping the task cancels the future, awaiting the task yields its
646    /// output, and [`Task::detach`] lets it run to completion on its own.
647    #[track_caller]
648    pub fn spawn<F>(&self, future: F) -> Task<F::Output>
649    where
650        F: Future + Send + 'static,
651        F::Output: Send + 'static,
652    {
653        let sender = self.sender.clone();
654        let (runnable, task) = async_task::Builder::new()
655            .metadata(RunnableMeta::new_with_callers_location())
656            .spawn(
657                move |_| future,
658                move |runnable| {
659                    let _ = sender.send(runnable);
660                },
661            );
662        runnable.schedule();
663        Task(TaskState::Spawned(task))
664    }
665}
666
667/// A handle to a future that runs in the background, and to its output.
668///
669/// Executors return a `Task` from their `spawn` methods. `gpui::Task` is this
670/// type. The handle controls the future's lifetime in three ways:
671///
672/// - **Dropping** the task cancels the future. It stops at its current
673///   `.await` point and is never polled again. Work it already finished stays
674///   finished. Keep the handle alive while you need the result.
675/// - **[`Task::detach`]** gives up the handle and lets the future run to
676///   completion. Its output is discarded, so use this for side effects.
677/// - **Awaiting** the task waits for the output. A `Task` implements
678///   [`Future`], and awaiting it consumes it. If the future panics, the panic
679///   resumes in the awaiting code.
680///
681/// Use [`Task::ready`] for a value you already have. Use [`Task::fallible`]
682/// when a cancelled task should resolve to `None` instead of panicking.
683///
684/// # Panics
685///
686/// Awaiting a task panics if the task closed without producing an output. This
687/// happens when the scheduler drops the runnable before it runs, as during
688/// shutdown. Use [`Task::fallible`] to get `None` in that case.
689///
690/// For the bigger picture, see `gpui::_documentation::concepts::executors`.
691#[must_use]
692pub struct Task<T>(TaskState<T>);
693
694enum TaskState<T> {
695    /// A task that is ready to return a value
696    Ready(Option<T>),
697
698    /// A task that is currently running.
699    Spawned(async_task::Task<T, RunnableMeta>),
700
701    /// A typed view of a [`Task<Box<dyn Any + Send + Sync>>`] obtained via
702    /// [`Task::downcast`]. The inner task drives the actual work; the
703    /// downcast layer just unwraps the `Box<dyn Any + Send + Sync>` on poll.
704    Downcast {
705        inner: Box<Task<Box<dyn Any + Send + Sync>>>,
706        marker: PhantomData<fn() -> T>,
707    },
708
709    /// A task whose real handle is delivered later by another thread (see
710    /// [`Task::rendezvous`]). Once delivered, polling replaces this state
711    /// with the delivered task's state.
712    Rendezvous(RendezvousReceiver<T>),
713}
714
715/// State shared between the two halves of a [`Task::rendezvous`] pair.
716enum RendezvousState<T> {
717    /// No task delivered yet; holds the consumer's waker if it polled.
718    Pending(Option<Waker>),
719    /// The producer delivered before the consumer consumed the task.
720    Delivered(Task<T>),
721    /// The consumer was dropped before delivery; delivery cancels the task.
722    Cancelled,
723    /// The consumer was detached before delivery; delivery detaches the task.
724    Detached,
725    /// The consumer took the delivered task.
726    Taken,
727}
728
729pub(crate) struct RendezvousReceiver<T> {
730    shared: Arc<parking_lot::Mutex<RendezvousState<T>>>,
731}
732
733impl<T> RendezvousReceiver<T> {
734    fn poll_take(&self, cx: &mut Context) -> Poll<Task<T>> {
735        let mut state = self.shared.lock();
736        match std::mem::replace(&mut *state, RendezvousState::Taken) {
737            RendezvousState::Delivered(task) => Poll::Ready(task),
738            RendezvousState::Pending(_) => {
739                *state = RendezvousState::Pending(Some(cx.waker().clone()));
740                Poll::Pending
741            }
742            RendezvousState::Cancelled | RendezvousState::Detached | RendezvousState::Taken => {
743                unreachable!("a rendezvous task was polled after its receiver was consumed")
744            }
745        }
746    }
747
748    fn is_ready(&self) -> bool {
749        match &*self.shared.lock() {
750            RendezvousState::Delivered(task) => task.is_ready(),
751            _ => false,
752        }
753    }
754
755    fn detach(self) {
756        let mut state = self.shared.lock();
757        match std::mem::replace(&mut *state, RendezvousState::Detached) {
758            RendezvousState::Delivered(task) => {
759                *state = RendezvousState::Taken;
760                drop(state);
761                task.detach();
762            }
763            // The producer applies the detached disposition on delivery.
764            RendezvousState::Pending(_) => {}
765            RendezvousState::Cancelled | RendezvousState::Detached | RendezvousState::Taken => {
766                unreachable!("a rendezvous task was detached after its receiver was consumed")
767            }
768        }
769    }
770}
771
772impl<T> Drop for RendezvousReceiver<T> {
773    fn drop(&mut self) {
774        let mut state = self.shared.lock();
775        match &*state {
776            RendezvousState::Pending(_) => *state = RendezvousState::Cancelled,
777            RendezvousState::Delivered(_) => {
778                let delivered = std::mem::replace(&mut *state, RendezvousState::Cancelled);
779                drop(state);
780                drop(delivered);
781            }
782            // `Taken`, `Detached`, and `Cancelled` record dispositions that
783            // this drop must not overwrite: the receiver is also dropped as a
784            // normal side effect of taking or detaching.
785            _ => {}
786        }
787    }
788}
789
790pub(crate) struct RendezvousSender<T> {
791    shared: Arc<parking_lot::Mutex<RendezvousState<T>>>,
792}
793
794impl<T> RendezvousSender<T> {
795    /// Hands the real task to the rendezvous, applying the consumer's
796    /// disposition if it was dropped or detached before delivery.
797    pub(crate) fn deliver(self, task: Task<T>) {
798        let mut state = self.shared.lock();
799        match std::mem::replace(&mut *state, RendezvousState::Delivered(task)) {
800            RendezvousState::Pending(waker) => {
801                drop(state);
802                if let Some(waker) = waker {
803                    waker.wake();
804                }
805            }
806            RendezvousState::Cancelled => {
807                let delivered = std::mem::replace(&mut *state, RendezvousState::Cancelled);
808                drop(state);
809                drop(delivered);
810            }
811            RendezvousState::Detached => {
812                let RendezvousState::Delivered(task) =
813                    std::mem::replace(&mut *state, RendezvousState::Detached)
814                else {
815                    unreachable!("the delivered task was just stored");
816                };
817                drop(state);
818                task.detach();
819            }
820            RendezvousState::Delivered(_) | RendezvousState::Taken => {
821                unreachable!("a rendezvous task was delivered twice")
822            }
823        }
824    }
825}
826
827impl<T> Task<T> {
828    /// Returns a task that is already complete and resolves to `val`.
829    ///
830    /// Use it where a function returns a `Task` but the value is known now,
831    /// such as a cache hit. No work is scheduled. Dropping or detaching the
832    /// task has no effect.
833    ///
834    /// # Examples
835    ///
836    /// ```
837    /// # use scheduler::Task;
838    /// let task = Task::ready(42);
839    /// assert!(task.is_ready());
840    /// ```
841    pub fn ready(val: T) -> Self {
842        Task(TaskState::Ready(Some(val)))
843    }
844
845    /// Creates a task whose real handle arrives later through the returned
846    /// sender, typically from another thread. Until delivery the task is
847    /// pending; afterwards it behaves exactly like the delivered task.
848    /// Dropping or detaching the task before delivery is honored on delivery.
849    pub(crate) fn rendezvous() -> (Self, RendezvousSender<T>) {
850        let shared = Arc::new(parking_lot::Mutex::new(RendezvousState::Pending(None)));
851        (
852            Task(TaskState::Rendezvous(RendezvousReceiver {
853                shared: shared.clone(),
854            })),
855            RendezvousSender { shared },
856        )
857    }
858
859    /// Wraps an `async_task::Task` that carries [`RunnableMeta`] as its metadata.
860    ///
861    /// The executors use this internally. Call it only when you build the
862    /// `async_task` runnable yourself.
863    pub fn from_async_task(task: async_task::Task<T, RunnableMeta>) -> Self {
864        Task(TaskState::Spawned(task))
865    }
866
867    /// Returns `true` if the output is ready to take without waiting.
868    ///
869    /// A [`Task::ready`] task is always ready. A spawned task becomes ready
870    /// when its future finishes, even if the output hasn't been awaited yet.
871    /// This is a snapshot: a task on another thread may finish right after the
872    /// call returns `false`.
873    ///
874    /// # Examples
875    ///
876    /// ```
877    /// # use scheduler::Task;
878    /// assert!(Task::ready("done").is_ready());
879    /// ```
880    pub fn is_ready(&self) -> bool {
881        match &self.0 {
882            TaskState::Ready(_) => true,
883            TaskState::Spawned(task) => task.is_finished(),
884            TaskState::Downcast { inner, .. } => inner.is_ready(),
885            TaskState::Rendezvous(receiver) => receiver.is_ready(),
886        }
887    }
888
889    /// Lets the future run to completion in the background and discards its
890    /// output.
891    ///
892    /// Use this for work whose result you don't need, such as saving a file or
893    /// sending a message. Dropping the task would cancel it instead. Detaching
894    /// a [`Task::ready`] task does nothing.
895    pub fn detach(self) {
896        match self {
897            Task(TaskState::Ready(_)) => {}
898            Task(TaskState::Spawned(task)) => task.detach(),
899            Task(TaskState::Downcast { inner, .. }) => inner.detach(),
900            Task(TaskState::Rendezvous(receiver)) => receiver.detach(),
901        }
902    }
903
904    /// Converts this task into a [`FallibleTask`], which resolves to `None`
905    /// instead of panicking when the task closes without an output.
906    ///
907    /// A panic inside the future still propagates to the awaiter.
908    pub fn fallible(self) -> FallibleTask<T> {
909        FallibleTask(match self.0 {
910            TaskState::Ready(val) => FallibleTaskState::Ready(val),
911            TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()),
912            TaskState::Downcast { inner, .. } => FallibleTaskState::Downcast {
913                inner: Box::new(inner.fallible()),
914                marker: PhantomData,
915            },
916            TaskState::Rendezvous(receiver) => FallibleTaskState::Rendezvous(receiver),
917        })
918    }
919}
920
921impl Task<Box<dyn Any + Send + Sync>> {
922    /// Converts a task that yields a boxed `Any` into a task that yields `T`.
923    ///
924    /// [`LocalExecutor::spawn_dedicated`] and [`BackgroundExecutor::spawn_dedicated`]
925    /// use this to recover the closure's output type from the type-erased
926    /// [`Scheduler::spawn_dedicated`].
927    ///
928    /// # Panics
929    ///
930    /// Polling the returned task panics if the boxed value is not a `T`. The
931    /// check runs on poll, not in this call. A mismatch is a bug in whatever
932    /// produced the inner task, because the caller chose `T`.
933    pub fn downcast<T: Send + Sync + 'static>(self) -> Task<T> {
934        Task(TaskState::Downcast {
935            inner: Box::new(self),
936            marker: PhantomData,
937        })
938    }
939}
940
941impl<T> std::fmt::Debug for Task<T> {
942    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
943        match &self.0 {
944            TaskState::Ready(_) => f.debug_tuple("Task::Ready").finish(),
945            TaskState::Spawned(task) => f.debug_tuple("Task::Spawned").field(task).finish(),
946            TaskState::Downcast { inner, .. } => {
947                f.debug_tuple("Task::Downcast").field(inner).finish()
948            }
949            TaskState::Rendezvous(_) => f.debug_tuple("Task::Rendezvous").finish(),
950        }
951    }
952}
953
954/// A [`Task`] that resolves to `Option<T>`, and to `None` instead of panicking
955/// when the task is cancelled.
956///
957/// Create one with [`Task::fallible`]. Like `Task`, dropping it cancels the
958/// future and detaching lets it finish. Await it to get `Some(output)`, or
959/// `None` if the task closed without an output. A panic in the future still
960/// propagates to the awaiter.
961#[must_use]
962pub struct FallibleTask<T>(FallibleTaskState<T>);
963
964enum FallibleTaskState<T> {
965    /// A task that is ready to return a value
966    Ready(Option<T>),
967
968    /// A task that is currently running (wraps async_task::FallibleTask).
969    Spawned(async_task::FallibleTask<T, RunnableMeta>),
970
971    /// Mirror of [`TaskState::Downcast`] for fallible tasks.
972    Downcast {
973        inner: Box<FallibleTask<Box<dyn Any + Send + Sync>>>,
974        marker: PhantomData<fn() -> T>,
975    },
976
977    /// Mirror of [`TaskState::Rendezvous`] for fallible tasks.
978    Rendezvous(RendezvousReceiver<T>),
979}
980
981impl<T> FallibleTask<T> {
982    /// Returns a fallible task that is already complete and resolves to
983    /// `Some(val)`.
984    pub fn ready(val: T) -> Self {
985        FallibleTask(FallibleTaskState::Ready(Some(val)))
986    }
987
988    /// Lets the future run to completion in the background and discards its
989    /// output. See [`Task::detach`].
990    pub fn detach(self) {
991        match self.0 {
992            FallibleTaskState::Ready(_) => {}
993            FallibleTaskState::Spawned(task) => task.detach(),
994            FallibleTaskState::Downcast { inner, .. } => inner.detach(),
995            FallibleTaskState::Rendezvous(receiver) => receiver.detach(),
996        }
997    }
998}
999
1000impl<T: 'static> Future for FallibleTask<T> {
1001    type Output = Option<T>;
1002
1003    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
1004        let this = unsafe { self.get_unchecked_mut() };
1005        loop {
1006            match &mut this.0 {
1007                FallibleTaskState::Ready(val) => return Poll::Ready(val.take()),
1008                FallibleTaskState::Spawned(task) => return Pin::new(task).poll(cx),
1009                FallibleTaskState::Downcast { inner, .. } => {
1010                    return match Pin::new(inner.as_mut()).poll(cx) {
1011                        Poll::Ready(Some(boxed_any)) => Poll::Ready(Some(
1012                            *boxed_any
1013                                .downcast::<T>()
1014                                .expect("FallibleTask::poll: downcast type mismatch"),
1015                        )),
1016                        Poll::Ready(None) => Poll::Ready(None),
1017                        Poll::Pending => Poll::Pending,
1018                    };
1019                }
1020                FallibleTaskState::Rendezvous(receiver) => match receiver.poll_take(cx) {
1021                    Poll::Ready(task) => {
1022                        this.0 = task.fallible().0;
1023                        continue;
1024                    }
1025                    Poll::Pending => return Poll::Pending,
1026                },
1027            }
1028        }
1029    }
1030}
1031
1032impl<T> std::fmt::Debug for FallibleTask<T> {
1033    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1034        match &self.0 {
1035            FallibleTaskState::Ready(_) => f.debug_tuple("FallibleTask::Ready").finish(),
1036            FallibleTaskState::Spawned(task) => {
1037                f.debug_tuple("FallibleTask::Spawned").field(task).finish()
1038            }
1039            FallibleTaskState::Downcast { inner, .. } => f
1040                .debug_tuple("FallibleTask::Downcast")
1041                .field(inner)
1042                .finish(),
1043            FallibleTaskState::Rendezvous(_) => f.debug_tuple("FallibleTask::Rendezvous").finish(),
1044        }
1045    }
1046}
1047
1048impl<T: 'static> Future for Task<T> {
1049    type Output = T;
1050
1051    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
1052        let this = unsafe { self.get_unchecked_mut() };
1053        loop {
1054            match &mut this.0 {
1055                TaskState::Ready(val) => return Poll::Ready(val.take().unwrap()),
1056                TaskState::Spawned(task) => return Pin::new(task).poll(cx),
1057                TaskState::Downcast { inner, .. } => {
1058                    return match Pin::new(inner.as_mut()).poll(cx) {
1059                        Poll::Ready(boxed_any) => Poll::Ready(
1060                            *boxed_any
1061                                .downcast::<T>()
1062                                .expect("Task::poll: downcast type mismatch"),
1063                        ),
1064                        Poll::Pending => Poll::Pending,
1065                    };
1066                }
1067                TaskState::Rendezvous(receiver) => match receiver.poll_take(cx) {
1068                    Poll::Ready(task) => {
1069                        this.0 = task.0;
1070                        continue;
1071                    }
1072                    Poll::Pending => return Poll::Pending,
1073                },
1074            }
1075        }
1076    }
1077}
1078
1079/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
1080#[track_caller]
1081fn spawn_local_with_source_location<Fut, S>(
1082    future: Fut,
1083    schedule: S,
1084    metadata: RunnableMeta,
1085) -> (
1086    async_task::Runnable<RunnableMeta>,
1087    async_task::Task<Fut::Output, RunnableMeta>,
1088)
1089where
1090    Fut: Future + 'static,
1091    Fut::Output: 'static,
1092    S: async_task::Schedule<RunnableMeta> + Send + Sync + 'static,
1093{
1094    #[inline]
1095    fn thread_id() -> ThreadId {
1096        std::thread_local! {
1097            static ID: ThreadId = thread::current().id();
1098        }
1099        ID.try_with(|id| *id)
1100            .unwrap_or_else(|_| thread::current().id())
1101    }
1102
1103    struct Checked<F> {
1104        id: ThreadId,
1105        inner: ManuallyDrop<F>,
1106        location: &'static Location<'static>,
1107    }
1108
1109    impl<F> Drop for Checked<F> {
1110        fn drop(&mut self) {
1111            assert_eq!(
1112                self.id,
1113                thread_id(),
1114                "local task dropped by a thread that didn't spawn it. Task spawned at {}",
1115                self.location
1116            );
1117            // SAFETY: `inner` is wrapped in `ManuallyDrop`, so this is the only
1118            // place it is dropped. The thread check above ensures local futures
1119            // are dropped on the thread that created them.
1120            unsafe { ManuallyDrop::drop(&mut self.inner) };
1121        }
1122    }
1123
1124    impl<F: Future> Future for Checked<F> {
1125        type Output = F::Output;
1126
1127        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1128            // SAFETY: We don't move any fields out of `self`; this mutable
1129            // reference is only used to check metadata and to project the pin to
1130            // `inner` below.
1131            let this = unsafe { self.get_unchecked_mut() };
1132            assert!(
1133                this.id == thread_id(),
1134                "local task polled by a thread that didn't spawn it. Task spawned at {}",
1135                this.location
1136            );
1137            // SAFETY: `inner` is structurally pinned by `Checked`; after
1138            // `Checked` is pinned, `inner` is never moved. The thread check
1139            // above ensures the local future is only polled by its spawning
1140            // thread.
1141            unsafe { Pin::new_unchecked(&mut *this.inner).poll(cx) }
1142        }
1143    }
1144
1145    let location = metadata.location;
1146
1147    let future = move |_| Checked {
1148        id: thread_id(),
1149        inner: ManuallyDrop::new(future),
1150        location,
1151    };
1152
1153    let builder = async_task::Builder::new().metadata(metadata);
1154    // SAFETY: `Checked` enforces the invariants required by `spawn_unchecked`:
1155    // the non-`Send` future is only polled and dropped on the thread that
1156    // spawned it.
1157    unsafe { builder.spawn_unchecked(future, schedule) }
1158}
1159
1160#[cfg(test)]
1161mod local_task_scope_tests {
1162    use super::*;
1163    use crate::TestScheduler;
1164    use std::{
1165        collections::VecDeque,
1166        sync::{Arc, Mutex},
1167        task::{Poll, Waker},
1168        thread::ThreadId,
1169    };
1170
1171    type Queue = Arc<Mutex<VecDeque<Runnable<RunnableMeta>>>>;
1172
1173    fn queue() -> Queue {
1174        Arc::new(Mutex::new(VecDeque::new()))
1175    }
1176
1177    fn pop(queue: &Queue) -> Runnable<RunnableMeta> {
1178        queue.lock().unwrap().pop_front().expect("queued runnable")
1179    }
1180
1181    fn executor(scope: Arc<LocalTaskScope>, normal: Queue, cancellation: Queue) -> LocalExecutor {
1182        let scheduler: Arc<dyn Scheduler> =
1183            Arc::new(TestScheduler::new(crate::TestSchedulerConfig::default()));
1184        LocalExecutor::new_with_scope(
1185            SessionId::new(0),
1186            scheduler,
1187            scope,
1188            move |runnable| normal.lock().unwrap().push_back(runnable),
1189            move |runnable| cancellation.lock().unwrap().push_back(runnable),
1190        )
1191    }
1192
1193    struct DropOnThread {
1194        dropped_on: Arc<Mutex<Option<ThreadId>>>,
1195    }
1196
1197    impl Drop for DropOnThread {
1198        fn drop(&mut self) {
1199            *self.dropped_on.lock().unwrap() = Some(std::thread::current().id());
1200        }
1201    }
1202
1203    #[test]
1204    fn sealed_scope_late_waker_cannot_dispatch_or_drop_local_work_off_owner() {
1205        let owner = std::thread::current().id();
1206        let scope = Arc::new(LocalTaskScope::new());
1207        let normal = queue();
1208        let cancellation = queue();
1209        let executor = executor(scope.clone(), normal.clone(), cancellation.clone());
1210        let saved_waker = Arc::new(Mutex::new(None::<Waker>));
1211        let dropped_on = Arc::new(Mutex::new(None));
1212
1213        let task = {
1214            let saved_waker = saved_waker.clone();
1215            let dropped_on = dropped_on.clone();
1216            executor.spawn(async move {
1217                let _probe = DropOnThread { dropped_on };
1218                futures::future::poll_fn(move |cx| {
1219                    *saved_waker.lock().unwrap() = Some(cx.waker().clone());
1220                    Poll::<()>::Pending
1221                })
1222                .await;
1223            })
1224        };
1225        task.detach();
1226        pop(&normal).run();
1227        assert_eq!(scope.live_task_count(), 1);
1228
1229        let late_waker = saved_waker.lock().unwrap().clone().unwrap();
1230        scope.begin_retirement();
1231        drop(pop(&cancellation));
1232
1233        assert_eq!(scope.live_task_count(), 0);
1234        assert_eq!(*dropped_on.lock().unwrap(), Some(owner));
1235        assert!(scope.seal_if_quiescent());
1236        std::thread::spawn(move || late_waker.wake())
1237            .join()
1238            .expect("late wake thread");
1239        assert!(cancellation.lock().unwrap().is_empty());
1240    }
1241
1242    #[test]
1243    fn completion_unregisters_task_and_spawn_after_retirement_is_canceled() {
1244        let scope = Arc::new(LocalTaskScope::new());
1245        let normal = queue();
1246        let cancellation = queue();
1247        let executor = executor(scope.clone(), normal.clone(), cancellation.clone());
1248
1249        executor.spawn(async { 42 }).detach();
1250        pop(&normal).run();
1251        assert_eq!(scope.live_task_count(), 0);
1252
1253        scope.begin_retirement();
1254        executor.spawn(async { 7 }).detach();
1255        assert!(normal.lock().unwrap().is_empty());
1256        assert_eq!(scope.live_task_count(), 1);
1257        drop(pop(&cancellation));
1258        assert_eq!(scope.live_task_count(), 0);
1259    }
1260
1261    #[test]
1262    fn custom_idle_dispatch_is_bypassed_during_retirement() {
1263        let scope = Arc::new(LocalTaskScope::new());
1264        let normal = queue();
1265        let cancellation = queue();
1266        let executor = executor(scope.clone(), normal, cancellation.clone());
1267        let idle = queue();
1268        let idle_dispatches = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1269        let pending_waker = Arc::new(Mutex::new(None::<Waker>));
1270
1271        let task = {
1272            let idle_dispatches = idle_dispatches.clone();
1273            let idle = idle.clone();
1274            let pending_waker = pending_waker.clone();
1275            executor.spawn_with_dispatch(
1276                async move {
1277                    futures::future::poll_fn(move |cx| {
1278                        *pending_waker.lock().unwrap() = Some(cx.waker().clone());
1279                        Poll::<()>::Pending
1280                    })
1281                    .await;
1282                },
1283                move |runnable| {
1284                    idle_dispatches.fetch_add(1, Ordering::SeqCst);
1285                    idle.lock().unwrap().push_back(runnable);
1286                },
1287            )
1288        };
1289        task.detach();
1290        pop(&idle).run();
1291        assert_eq!(idle_dispatches.load(Ordering::SeqCst), 1);
1292
1293        scope.begin_retirement();
1294        drop(pop(&cancellation));
1295        assert_eq!(idle_dispatches.load(Ordering::SeqCst), 1);
1296        assert_eq!(scope.live_task_count(), 0);
1297    }
1298}
1299
1300#[cfg(test)]
1301mod sealed_scope_tests {
1302    use super::*;
1303    use crate::TestScheduler;
1304    use std::{
1305        collections::VecDeque,
1306        sync::{Arc, Mutex},
1307        thread::ThreadId,
1308    };
1309
1310    #[test]
1311    fn sealed_scope_rejects_new_spawns_and_drops_input_on_owner() {
1312        let owner = std::thread::current().id();
1313        let scope = Arc::new(LocalTaskScope::new());
1314        scope.begin_retirement();
1315        assert!(scope.seal_if_quiescent());
1316
1317        let normal = Arc::new(Mutex::new(VecDeque::new()));
1318        let cancellation = Arc::new(Mutex::new(VecDeque::new()));
1319        let scheduler: Arc<dyn Scheduler> =
1320            Arc::new(TestScheduler::new(crate::TestSchedulerConfig::default()));
1321        let executor = LocalExecutor::new_with_scope(
1322            SessionId::new(1),
1323            scheduler,
1324            scope,
1325            {
1326                let normal = normal.clone();
1327                move |runnable| normal.lock().unwrap().push_back(runnable)
1328            },
1329            {
1330                let cancellation = cancellation.clone();
1331                move |runnable| cancellation.lock().unwrap().push_back(runnable)
1332            },
1333        );
1334        let dropped_on = Arc::new(Mutex::new(None::<ThreadId>));
1335        let probe = {
1336            let dropped_on = dropped_on.clone();
1337            struct Probe(Arc<Mutex<Option<ThreadId>>>);
1338            impl Drop for Probe {
1339                fn drop(&mut self) {
1340                    *self.0.lock().unwrap() = Some(std::thread::current().id());
1341                }
1342            }
1343            Probe(dropped_on)
1344        };
1345        let future = async move {
1346            let _probe = probe;
1347        };
1348        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1349            executor.spawn(future).detach();
1350        }));
1351
1352        assert!(result.is_err());
1353        assert_eq!(*dropped_on.lock().unwrap(), Some(owner));
1354        assert!(normal.lock().unwrap().is_empty());
1355        assert!(cancellation.lock().unwrap().is_empty());
1356    }
1357}