use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::AtomicBool;
use arty_executor::TaskSet;
use events_once::{BoxedSender, Event};
use observed::Sink;
use observed::context::Transfer;
use performables::arc::Arc;
use crate::task::Builtins;
use crate::task::execution::discard_panic;
use crate::task::execution::remote::RemoteTaskFuture;
use crate::task::execution::result::TaskResult;
use crate::task::execution::storage::TaskFactory;
use crate::task::join::JoinHandle;
pub(crate) type BoxedRemoteFutureFactory<FgArg> = Box<dyn FnOnce(FgArg, &TaskSet) + Send + 'static>;
pub(crate) fn prepare_remote<C, FF, F, R>(
future_factory: FF,
parent_task_enrichment: Transfer,
sink: Sink,
shutdown_signal: Arc<AtomicBool>,
) -> (BoxedRemoteFutureFactory<C>, JoinHandle<R>)
where
C: 'static,
FF: FnOnce(C) -> F + Send + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
let (result_tx, result_rx) = Event::<TaskResult<R>>::boxed();
let future_factory = TaskFactory::new(future_factory, parent_task_enrichment, sink);
let future_factory: BoxedRemoteFutureFactory<C> = Box::new(move |cx, tasks| {
let (future_factory, parent_task_enrichment, sink) = future_factory.into_parts();
let inner = async move { future_factory(cx).await };
drop(tasks.add(RemoteTaskFuture::new_with_shutdown(
inner,
result_tx,
parent_task_enrichment,
sink,
Some(shutdown_signal),
)));
});
(future_factory, JoinHandle::new(result_rx))
}
pub(crate) fn prepare_remote_on_worker<FF, F, R>(
future_factory: FF,
builtins: Builtins,
parent_task_enrichment: Transfer,
sink: Sink,
shutdown_signal: Arc<AtomicBool>,
tasks: &TaskSet,
) -> JoinHandle<R>
where
FF: FnOnce(Builtins) -> F + Send + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
let (result_tx, result_rx) = Event::<TaskResult<R>>::boxed();
let inner = async move { future_factory(builtins).await };
drop(tasks.add(RemoteTaskFuture::new_with_shutdown(
inner,
result_tx,
parent_task_enrichment,
sink,
Some(shutdown_signal),
)));
JoinHandle::new(result_rx)
}
pub(crate) fn prepare_blocking<F, R>(body: F) -> (impl FnOnce() + Send + 'static, JoinHandle<R>)
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let (result_tx, result_rx) = Event::<TaskResult<R>>::boxed();
let task = BlockingTask {
body: Some(body),
result_tx: Some(result_tx),
};
(move || task.run(), JoinHandle::new(result_rx))
}
struct BlockingTask<F, R: Send + 'static> {
body: Option<F>,
result_tx: Option<BoxedSender<TaskResult<R>>>,
}
impl<F, R> BlockingTask<F, R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
fn run(mut self) {
let body = self.body.take().expect("blocking task body is consumed exactly once");
let result_tx = self.result_tx.take().expect("blocking task sender is consumed exactly once");
let body_result = catch_unwind(AssertUnwindSafe(body));
match body_result {
Ok(result) => {
if let Err(panic) = catch_unwind(AssertUnwindSafe(|| result_tx.send(TaskResult::Completed(result)))) {
discard_panic(panic);
}
}
Err(panic) => {
if let Err(disposal) = catch_unwind(AssertUnwindSafe(|| result_tx.send(TaskResult::Panicked(panic)))) {
discard_panic(disposal);
}
}
}
}
}
impl<F, R: Send + 'static> Drop for BlockingTask<F, R> {
fn drop(&mut self) {
if let Some(body) = self.body.take()
&& let Err(panic) = catch_unwind(AssertUnwindSafe(|| drop(body)))
{
discard_panic(panic);
}
if let Some(sender) = self.result_tx.take()
&& let Err(panic) = catch_unwind(AssertUnwindSafe(|| drop(sender)))
{
discard_panic(panic);
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] mod tests {
use std::panic::panic_any;
use std::pin::pin;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::task::{Context, Poll, Wake, Waker};
use arty_executor::testing::new_guarded_executor;
use arty_executor::{CycleOutcome, Executor};
use futures::executor::block_on;
use performables::arc::Arc as PArc;
use super::*;
#[test]
fn remote_factory_remains_deferred_until_poll() {
let invoked = Arc::new(AtomicBool::new(false));
let factory_invoked = Arc::clone(&invoked);
let sink = Sink::noop();
let shutdown_signal = PArc::new(AtomicBool::new(false));
let (factory, handle) = prepare_remote(
move |()| {
factory_invoked.store(true, Ordering::Relaxed);
async {
let value = Rc::new(42);
std::future::ready(()).await;
*value
}
},
sink.transfer_context(),
sink,
shutdown_signal,
);
let executor = new_guarded_executor(Waker::noop().clone());
factory((), &executor.tasks());
let before_poll = invoked.load(Ordering::Relaxed);
run_to_completion(&executor);
let result = block_on(handle).unwrap();
assert_eq!((before_poll, invoked.load(Ordering::Relaxed), result), (false, true, 42),);
}
#[test]
fn remote_factory_is_not_invoked_if_shutdown_starts_after_registration() {
let invoked = Arc::new(AtomicBool::new(false));
let shutdown_signal = PArc::new(AtomicBool::new(false));
let sink = Sink::noop();
let (factory, handle) = prepare_remote(
{
let invoked = Arc::clone(&invoked);
move |()| {
invoked.store(true, Ordering::Relaxed);
std::future::ready(())
}
},
sink.transfer_context(),
sink,
PArc::clone(&shutdown_signal),
);
let executor = new_guarded_executor(Waker::noop().clone());
factory((), &executor.tasks());
shutdown_signal.store(true, Ordering::Release);
run_to_completion(&executor);
assert!(!invoked.load(Ordering::Relaxed));
assert!(block_on(handle).unwrap_err().is_shutdown());
}
fn run_to_completion(executor: &Executor) {
while executor.execute_cycle() == CycleOutcome::Continue {}
}
struct PanicWake;
impl Wake for PanicWake {
fn wake(self: Arc<Self>) {
panic!("join receiver notification");
}
}
#[test]
fn blocking_task_delivers_its_result() {
let (task, handle) = prepare_blocking(|| 42);
task();
assert_eq!(block_on(handle).unwrap(), 42);
}
#[test]
fn blocking_panic_is_delivered_to_joiner() {
let (task, handle) = prepare_blocking(|| panic!("blocking task"));
task();
assert!(block_on(handle).unwrap_err().is_panic());
}
#[test]
fn remote_factory_panic_is_delivered_to_joiner() {
#[derive(Debug, PartialEq)]
struct Payload(u32);
let sink = Sink::noop();
let shutdown_signal = PArc::new(AtomicBool::new(false));
let (factory, handle) = prepare_remote(
|()| -> std::future::Ready<()> { panic_any(Payload(42)) },
sink.transfer_context(),
sink,
shutdown_signal,
);
let executor = new_guarded_executor(Waker::noop().clone());
factory((), &executor.tasks());
run_to_completion(&executor);
assert!(block_on(handle).unwrap_err().is_panic());
}
#[test]
fn discarded_preparation_reports_shutdown() {
let (task, handle) = prepare_blocking(|| 42);
drop(task);
let mut handle = pin!(handle);
let mut context = Context::from_waker(Waker::noop());
let Poll::Ready(Err(error)) = handle.as_mut().poll(&mut context) else {
panic!("discarded work must report shutdown");
};
assert!(error.is_shutdown());
}
#[test]
fn discarded_blocking_task_contains_body_destructor_panic() {
struct PanicOnDrop;
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("discarded blocking capture");
}
}
let capture = PanicOnDrop;
let (task, handle) = prepare_blocking(move || drop(capture));
catch_unwind(AssertUnwindSafe(|| drop(task))).unwrap();
assert!(block_on(handle).unwrap_err().is_shutdown());
}
#[test]
fn discarded_blocking_task_contains_join_waker_panic() {
let (task, handle) = prepare_blocking(|| 42);
let mut handle = pin!(handle);
let waker = Waker::from(Arc::new(PanicWake));
assert!(handle.as_mut().poll(&mut Context::from_waker(&waker)).is_pending());
catch_unwind(AssertUnwindSafe(|| drop(task))).unwrap();
let Poll::Ready(Err(error)) = handle.as_mut().poll(&mut Context::from_waker(Waker::noop())) else {
panic!("discarded blocking work must report shutdown");
};
assert!(error.is_shutdown());
}
}