use std::cell::{Cell, OnceCell, UnsafeCell};
use std::fmt::Debug;
use std::marker::PhantomPinned;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::pin::Pin;
use std::task;
use observed::Sink;
use pin_project::{pin_project, pinned_drop};
use plurality::MultiPool;
use crate::{
ExecutorShared, ShutdownCause, TaskCreatedReporter, TaskHandle, TaskHandleSlot, TaskOptions, TaskPanicPayload, TaskPtr, WakeSignal,
discard_task_panic_payload,
};
#[derive(Debug, Default)]
pub(crate) struct TaskLinks {
pub(crate) previous: Cell<Option<TaskPtr>>,
pub(crate) next: Cell<Option<TaskPtr>>,
pub(crate) linked: Cell<bool>,
}
#[derive(Debug)]
struct Task<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
payload: UnsafeCell<Option<Payload<F, R, T>>>,
wake_signal: OnceCell<WakeSignal>,
handle_slot: TaskHandleSlot,
options: TaskOptions,
sink: Sink,
poll_count: Cell<u64>,
links: TaskLinks,
_requires_pin: PhantomPinned,
}
#[expect(
clippy::non_send_fields_in_send_ty,
reason = "remote owners retain only the allocation; the executor retires non-Send payloads on their owner thread"
)]
unsafe impl<F, R, T> Send for Task<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
}
unsafe impl<F, R, T> Sync for Task<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
}
impl<F, R, T> Task<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
fn new_with_target(future: F, result_target: T, options: TaskOptions, sink: Sink) -> Self {
Self {
payload: UnsafeCell::new(Some(Payload {
future: FutureStorage::new(future),
result_target: Some(result_target),
})),
wake_signal: OnceCell::new(),
handle_slot: TaskHandleSlot::new(),
options,
sink,
poll_count: Cell::new(0),
links: TaskLinks::default(),
_requires_pin: PhantomPinned,
}
}
}
pub(crate) fn allocate_task<F, R, T>(pool: &MultiPool, future: F, options: TaskOptions, sink: Sink, completion: T) -> TaskHandle
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
TaskHandle::new(pool.alloc_arc_pin(Task::new_with_target(future, completion, options, sink)))
}
#[cfg(test)]
#[derive(Debug)]
struct TestCompletion;
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
impl<R: 'static> TaskCompletion<R> for TestCompletion {
fn completed(self, _result: R) {}
fn panicked(self, panic: TaskPanicPayload, _cause: Option<ShutdownCause>) {
discard_task_panic_payload(panic);
}
fn cancelled(self, _cause: ShutdownCause) {}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) fn allocate_test_task_in(pool: &MultiPool) -> TaskHandle {
allocate_task(
pool,
std::future::pending::<()>(),
TaskOptions::default(),
Sink::noop(),
TestCompletion,
)
}
impl<F, R, T> TypeErasedTask for Task<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
fn poll(self: Pin<&Self>, shared: &ExecutorShared) -> task::Poll<()> {
self.poll_count.set(self.poll_count.get().saturating_add(1));
self.options.with_context(|| self.poll_in_context(shared))
}
fn created_reporter(&self) -> Option<TaskCreatedReporter> {
self.options.created_reporter(self.sink.clone())
}
fn links(&self) -> &TaskLinks {
&self.links
}
fn handle_slot(&self) -> &TaskHandleSlot {
&self.handle_slot
}
fn clear_queued_notification(&self) {
self.wake_signal
.get()
.expect("task must be initialized before clearing a wake notification")
.clear_queued_notification();
}
fn promote_remote_notification(&self) -> bool {
self.wake_signal
.get()
.expect("remote notifications retain initialized tasks")
.promote_remote_notification()
}
#[cfg_attr(test, mutants::skip)] fn abort(self: Pin<&Self>, cause: ShutdownCause) {
if let Some(wake_signal) = self.wake_signal.get() {
wake_signal.retire();
}
let maybe_payload = unsafe { self.payload.get().as_mut() }.expect("UnsafeCell pointer cannot be null");
self.options
.with_context(|| self.finish_terminal(maybe_payload, TerminalOutcome::Cancelled(cause)));
}
fn initialize(self: Pin<&Self>, wake_signal: WakeSignal) {
self.wake_signal.set(wake_signal).expect("task must be initialized exactly once");
}
}
impl<F, R, T> Task<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
fn pinned_wake_signal(self: Pin<&Self>) -> Pin<&WakeSignal> {
let signal = self.get_ref().wake_signal.get().expect("task must be initialized before poll()");
unsafe { Pin::new_unchecked(signal) }
}
fn poll_in_context(self: Pin<&Self>, shared: &ExecutorShared) -> task::Poll<()> {
let maybe_payload = unsafe { self.payload.get().as_mut().expect("UnsafeCell pointer cannot be null") };
let Some(payload) = maybe_payload else {
unreachable!("attempted to poll a task whose future was already dropped");
};
let future_as_mut_pinned = unsafe { Pin::new_unchecked(&mut payload.future) };
let wake_signal = self.pinned_wake_signal();
let poll_result = {
let waker = unsafe { wake_signal.waker_ref() };
let mut cx = task::Context::from_waker(&waker);
match catch_unwind(AssertUnwindSafe(|| future_as_mut_pinned.poll(&mut cx))) {
Ok(x) => x,
Err(panic) => return self.finish_panicked(maybe_payload, wake_signal, panic, shared),
}
};
match poll_result {
task::Poll::Ready(result) => {
wake_signal.retire();
self.finish_terminal(maybe_payload, TerminalOutcome::Succeeded(result));
task::Poll::Ready(())
}
task::Poll::Pending => task::Poll::Pending,
}
}
fn finish_panicked(
self: Pin<&Self>,
maybe_payload: &mut Option<Payload<F, R, T>>,
wake_signal: Pin<&WakeSignal>,
panic: TaskPanicPayload,
shared: &ExecutorShared,
) -> task::Poll<()> {
let payload = maybe_payload.as_mut().expect("a polled task retains its payload");
let result_target = payload.result_target.take().expect("a polled task retains its result target");
wake_signal.retire();
if let Err(disposal) = unsafe { Pin::new_unchecked(&mut payload.future) }.destroy_pinned() {
discard_task_panic_payload(disposal);
}
*maybe_payload = None;
let shutdown = shared.shutdown();
if matches!(shared.panic_policy(), crate::PanicPolicy::Shutdown) {
_ = shutdown.publish(ShutdownCause::panic());
}
let cause = shutdown.is_triggered().then(|| shutdown.cause());
result_target.panicked(panic, cause);
self.options.report_panicked(&self.sink, self.poll_count.get());
task::Poll::Ready(())
}
fn finish_terminal(self: Pin<&Self>, maybe_payload: &mut Option<Payload<F, R, T>>, outcome: TerminalOutcome<R>) {
let Some(payload) = maybe_payload.as_mut() else {
debug_assert!(matches!(outcome, TerminalOutcome::Cancelled(_)));
return;
};
let result_target = payload
.result_target
.take()
.expect("the result target is present until the task enters a terminal state");
let future_panic = if matches!(&outcome, TerminalOutcome::Succeeded(_)) {
None
} else {
unsafe { Pin::new_unchecked(&mut payload.future) }.destroy_pinned().err()
};
*maybe_payload = None;
match outcome {
TerminalOutcome::Succeeded(result) => {
result_target.completed(result);
self.options.report_succeeded(&self.sink, self.poll_count.get());
}
TerminalOutcome::Cancelled(cause) => {
if future_panic.is_some() {
self.options.report_panicked(&self.sink, self.poll_count.get());
}
result_target.cancelled(cause);
if let Some(panic) = future_panic {
discard_task_panic_payload(panic);
}
}
}
}
}
#[cfg_attr(
not(test),
expect(clippy::needless_pass_by_value, reason = "semantically correct to consume the panic")
)]
#[cfg_attr(test, mutants::skip)] #[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) fn on_unhandled_task_panic(panic: TaskPanicPayload) -> ! {
#[cfg(test)]
std::panic::resume_unwind(panic);
#[cfg(not(test))]
if let Some(s) = panic.downcast_ref::<&str>() {
eprintln!("unhandled panic in async task - terminating process: {s}");
} else {
eprintln!("unhandled panic in async task - terminating process");
}
#[cfg(not(test))]
std::process::abort();
}
struct Payload<F, R, T>
where
F: Future<Output = R> + 'static,
R: 'static,
T: TaskCompletion<R>,
{
future: FutureStorage<F>,
result_target: Option<T>,
}
enum TerminalOutcome<R> {
Succeeded(R),
Cancelled(ShutdownCause),
}
pub trait TaskCompletion<R>: Debug + 'static {
fn completed(self, result: R);
fn panicked(self, panic: TaskPanicPayload, cause: Option<ShutdownCause>);
fn cancelled(self, cause: ShutdownCause);
}
#[pin_project(PinnedDrop)]
struct FutureStorage<F> {
#[pin]
inner: Option<F>,
}
impl<F> FutureStorage<F> {
fn new(inner: F) -> Self {
Self { inner: Some(inner) }
}
fn destroy_pinned(self: Pin<&mut Self>) -> Result<(), TaskPanicPayload> {
let mut inner = self.project().inner;
if inner.is_none() {
return Ok(());
}
catch_unwind(AssertUnwindSafe(|| inner.set(None)))
}
}
impl<F: Future> Future for FutureStorage<F> {
type Output = F::Output;
fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
let outcome = self
.as_mut()
.project()
.inner
.as_pin_mut()
.expect("task future must not be polled after completion")
.poll(cx);
if outcome.is_ready()
&& let Err(panic) = self.destroy_pinned()
{
std::panic::resume_unwind(panic);
}
outcome
}
}
#[pinned_drop]
impl<F> PinnedDrop for FutureStorage<F> {
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(test, mutants::skip)] fn drop(self: Pin<&mut Self>) {
if let Err(panic) = self.destroy_pinned() {
on_unhandled_task_panic(panic);
}
}
}
pub(crate) trait TypeErasedTask: Send + Sync {
fn poll(self: Pin<&Self>, shared: &ExecutorShared) -> task::Poll<()>;
fn created_reporter(&self) -> Option<TaskCreatedReporter>;
fn links(&self) -> &TaskLinks;
fn handle_slot(&self) -> &TaskHandleSlot;
fn clear_queued_notification(&self);
fn promote_remote_notification(&self) -> bool;
fn abort(self: Pin<&Self>, cause: ShutdownCause);
fn initialize(self: Pin<&Self>, wake_signal: WakeSignal);
}
#[cfg(test)]
mod tests {
use std::pin::pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Waker};
use super::*;
fn initialized_task<R: 'static>(pool: &MultiPool, future: impl Future<Output = R> + 'static) -> TaskHandle {
let handle = allocate_task(pool, future, TaskOptions::default(), Sink::noop(), TestCompletion);
let (shared, _remote_wakes) = crate::WakeShared::new(Waker::noop().clone(), std::thread::current().id());
let signal = WakeSignal::new(shared, handle.as_ptr());
unsafe { handle.as_task() }.initialize(signal);
handle
}
#[test]
fn abort_is_idempotent_after_the_payload_is_cleared() {
let pool = MultiPool::new();
let handle = initialized_task(&pool, std::future::pending::<()>());
let task = unsafe { handle.as_task() };
task.abort(ShutdownCause::default());
task.abort(ShutdownCause::default());
drop(handle);
assert!(pool.is_empty());
}
#[test]
fn polling_after_completion_panics() {
let pool = MultiPool::new();
let handle = initialized_task(&pool, std::future::ready(()));
let shared = ExecutorShared::new(
Waker::noop().clone(),
Sink::noop(),
std::thread::current().id(),
crate::PanicPolicy::Ignore,
);
let task = unsafe { handle.as_task() };
assert!(task.poll(&shared).is_ready());
catch_unwind(AssertUnwindSafe(|| task.poll(&shared))).unwrap_err();
}
struct DropCountingFuture(Arc<AtomicUsize>);
impl Future for DropCountingFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> task::Poll<Self::Output> {
task::Poll::Pending
}
}
impl Drop for DropCountingFuture {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn future_storage_destroys_the_future_exactly_once() {
let drops = Arc::new(AtomicUsize::new(0));
{
let mut storage = pin!(FutureStorage::new(DropCountingFuture(Arc::clone(&drops))));
storage.as_mut().destroy_pinned().unwrap();
storage.as_mut().destroy_pinned().unwrap();
}
assert_eq!(drops.load(Ordering::Relaxed), 1);
}
#[test]
fn future_storage_retires_a_panicking_destructor_exactly_once() {
struct PanickingDropFuture(Arc<AtomicUsize>);
impl Future for PanickingDropFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> task::Poll<Self::Output> {
task::Poll::Ready(())
}
}
impl Drop for PanickingDropFuture {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
panic!("future destructor panic");
}
}
for completes in [false, true] {
let drops = Arc::new(AtomicUsize::new(0));
{
let mut storage = pin!(FutureStorage::new(PanickingDropFuture(Arc::clone(&drops))));
if completes {
catch_unwind(AssertUnwindSafe(|| storage.as_mut().poll(&mut Context::from_waker(Waker::noop())))).unwrap_err();
} else {
storage.as_mut().destroy_pinned().unwrap_err();
}
storage.as_mut().destroy_pinned().unwrap();
}
assert_eq!(drops.load(Ordering::Relaxed), 1);
}
}
}