use std::cell::OnceCell;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::ControlFlow;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use arty_executor::{CycleOutcome, Executor, TaskSet};
use performables::arc::Arc;
use performables::sync::channel;
use tick::Clock;
use tick::runtime::{ClockDriver, InactiveClock};
use crate::runtime::blocking_worker::BlockingWorker;
use crate::runtime::worker::protocol::AsyncWorkerCommand;
use crate::runtime::worker::signal::WorkerSignal;
use crate::task::{Builtins, Scheduler};
const SUSPEND_SLEEP_DURATION: Duration = Duration::from_millis(1);
const COMMANDS_PER_CYCLE: usize = 256;
#[derive(Debug)]
pub(in crate::runtime) struct AsyncWorker<TS = Builtins>
where
TS: Clone + 'static,
{
command_rx: Option<channel::Receiver<AsyncWorkerCommand<TS>>>,
shutdown_signal: Arc<AtomicBool>,
thread_state: Option<Rc<OnceCell<TS>>>,
executor: Option<Executor>,
tasks: TaskSet,
blocking_worker: Arc<BlockingWorker>,
clock_driver: ClockDriver,
signal: Arc<WorkerSignal>,
_single_threaded: PhantomData<*const ()>,
}
impl<TS> AsyncWorker<TS>
where
TS: Clone + 'static,
{
pub(in crate::runtime) unsafe fn new<TSFF, TSF>(
command_rx: channel::Receiver<AsyncWorkerCommand<TS>>,
thread_state_constructor: TSFF,
blocking_worker: Arc<BlockingWorker>,
clock: InactiveClock,
signal: Arc<WorkerSignal>,
shutdown_signal: Arc<AtomicBool>,
thread_state_constructed_tx: channel::Sender<()>,
) -> Self
where
TSFF: FnOnce(TaskSet, Clock) -> TSF + 'static,
TSF: Future<Output = TS>,
{
let (clock, clock_driver) = clock.activate();
let executor = unsafe { Executor::builder().owner_waker(WorkerSignal::waker(&signal)).build() };
let tasks = executor.tasks();
let thread_state = Rc::new(OnceCell::new());
tasks.add({
let tasks = tasks.clone();
let thread_state = Rc::clone(&thread_state);
async move {
let ts = thread_state_constructor(tasks, clock).await;
thread_state
.set(ts)
.map_err(|__ts| ())
.expect("thread state initialized multiple times");
_ = thread_state_constructed_tx.send(());
}
});
Self {
command_rx: Some(command_rx),
shutdown_signal,
thread_state: Some(thread_state),
executor: Some(executor),
tasks,
clock_driver,
blocking_worker,
signal,
_single_threaded: PhantomData,
}
}
pub(in crate::runtime) fn run(mut self) {
self.execute_phase();
Scheduler::clear_worker_thread();
}
#[cfg_attr(test, mutants::skip)] fn execute_phase(&mut self) {
while matches!(self.execute_step(), ControlFlow::Continue(())) {}
}
fn execute_step(&mut self) -> ControlFlow<()> {
let batch_exhausted = self.process_commands();
let cycle_outcome = self
.executor
.as_ref()
.expect("executor is not dropped until execute phase is finished")
.execute_cycle();
if matches!(cycle_outcome, CycleOutcome::Shutdown) {
self.executor = None;
}
_ = self.clock_driver.advance_timers(Instant::now());
match cycle_outcome {
CycleOutcome::Suspend if !batch_exhausted => self.signal.wait(SUSPEND_SLEEP_DURATION),
CycleOutcome::Continue | CycleOutcome::Suspend => {}
CycleOutcome::Shutdown => return ControlFlow::Break(()),
}
ControlFlow::Continue(())
}
#[cfg_attr(test, mutants::skip)] fn process_commands(&mut self) -> bool {
let Some(thread_state) = self.thread_state.as_ref() else {
debug_assert!(self.command_rx.is_none());
return false;
};
let Some(thread_state) = thread_state.get() else {
return false;
};
let command_rx = self
.command_rx
.as_ref()
.expect("thread state and command channel are dropped together");
for _ in 0..COMMANDS_PER_CYCLE {
match command_rx.try_recv() {
Ok(mut command) => {
match &mut command {
AsyncWorkerCommand::EnqueueTask { future_factory } => {
if self.shutdown_signal.load(Ordering::Acquire) {
self.begin_shutdown();
return false;
}
future_factory.take().expect("queued task factory is consumed exactly once")(thread_state.clone(), &self.tasks);
}
AsyncWorkerCommand::Shutdown => {
self.begin_shutdown();
return false;
}
}
}
Err(error) => {
assert!(error.is_empty(), "async worker command channel disconnected without shutdown");
return false;
}
}
}
true
}
#[cfg_attr(test, mutants::skip)] fn begin_shutdown(&mut self) {
Scheduler::clear_current();
self.thread_state = None;
self.command_rx = None;
self.blocking_worker.shutdown();
self.executor
.as_mut()
.expect("executor is not dropped until execute phase is finished")
.begin_shutdown();
}
}
impl<TS> Drop for AsyncWorker<TS>
where
TS: Clone + 'static,
{
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(test, mutants::skip)] fn drop(&mut self) {
if self.executor.is_some() {
std::process::abort();
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] mod tests {
#[cfg(not(miri))]
use std::pin::Pin;
use std::sync::atomic::AtomicUsize;
#[cfg(not(miri))]
use std::task::{Wake, Waker};
use std::thread;
#[cfg(not(miri))]
use events_once::BoxedSender;
use events_once::{Event, IntoValueError};
use observed::Sink;
use testing_aids::async_test;
use tick::ClockControl;
use super::*;
use crate::runtime::blocking_worker::BlockingPool;
use crate::runtime::blocking_worker::blocking_worker_tests::is_blocking_worker_shutting_down;
#[derive(Clone, Debug)]
struct TestTaskContext;
impl TestTaskContext {
#[cfg_attr(test, mutants::skip)]
fn new(_: TaskSet) -> Self {
Self
}
}
fn replenishing_command(sender: channel::Sender<AsyncWorkerCommand<()>>, processed: Arc<AtomicUsize>) -> AsyncWorkerCommand<()> {
AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new(move |(), _| {
processed.fetch_add(1, Ordering::Relaxed);
sender.send(replenishing_command(sender.clone(), Arc::clone(&processed))).unwrap();
})),
}
}
#[cfg(not(miri))]
struct GeneratedWorkerScenario {
command_tx: channel::Sender<AsyncWorkerCommand<()>>,
initialize: Option<BoxedSender<()>>,
ready_task: Option<BoxedSender<()>>,
control: ClockControl,
task_done: Arc<AtomicBool>,
timer_done: Arc<AtomicBool>,
factory_invocations: Arc<AtomicUsize>,
forbidden_factory_invocations: Arc<AtomicUsize>,
shutdown_signal: Arc<AtomicBool>,
signal: Arc<WorkerSignal>,
worker: AsyncWorker<()>,
timer_ready: bool,
timer_advanced_by_step: bool,
queued_shutdown: bool,
queued_factories: usize,
sent_factories: usize,
progress_failure: Option<&'static str>,
}
#[cfg(not(miri))]
impl GeneratedWorkerScenario {
const MAX_OPERATIONS: usize = 12;
const MAX_CLEANUP_STEPS: usize = Self::MAX_OPERATIONS + 8;
fn new() -> Self {
let (command_tx, command_rx) = channel::unbounded();
let (ready_tx, _ready_rx) = channel::unbounded();
let (initialize, initialized) = Event::boxed();
let (ready_task, task_ready) = Event::boxed();
let control = ClockControl::new();
let task_done = Arc::new(AtomicBool::new(false));
let timer_done = Arc::new(AtomicBool::new(false));
let factory_invocations = Arc::new(AtomicUsize::new(0));
let forbidden_factory_invocations = Arc::new(AtomicUsize::new(0));
let shutdown_signal = Arc::new(AtomicBool::new(false));
let signal = Arc::new(WorkerSignal::default());
let task_observer = Arc::clone(&task_done);
let timer_observer = Arc::clone(&timer_done);
let worker = unsafe {
AsyncWorker::new(
command_rx,
async move |tasks, clock| {
drop(tasks.add(async move {
task_ready.await.unwrap();
task_observer.store(true, Ordering::Relaxed);
}));
drop(tasks.add(async move {
clock.delay(Duration::from_secs(1)).await;
timer_observer.store(true, Ordering::Relaxed);
}));
initialized.await.unwrap();
},
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
control.clone().into(),
Arc::clone(&signal),
Arc::clone(&shutdown_signal),
ready_tx,
)
};
let mut scenario = Self {
command_tx,
initialize: Some(initialize),
ready_task: Some(ready_task),
control,
task_done,
timer_done,
factory_invocations,
forbidden_factory_invocations,
shutdown_signal,
signal,
worker,
timer_ready: false,
timer_advanced_by_step: false,
queued_shutdown: false,
queued_factories: 0,
sent_factories: 0,
progress_failure: None,
};
scenario.setup_step("worker stopped while registering controlled work");
WorkerSignal::waker(&scenario.signal).wake_by_ref();
scenario.setup_step("worker stopped while polling controlled work");
scenario
}
fn setup_step(&mut self, failure: &'static str) {
if self.worker.execute_step().is_break() {
self.progress_failure = Some(failure);
}
}
fn apply(&mut self, operation: u8) {
match operation % 7 {
0 => self.queue_factory_batch(operation),
1 => {
if let Some(initialize) = self.initialize.take() {
initialize.send(());
}
}
2 => {
if let Some(ready_task) = self.ready_task.take() {
ready_task.send(());
}
}
3 => self.ready_timer(),
4 => self.step(),
5 => self.shutdown_signal.store(true, Ordering::Release),
_ => self.queue_shutdown(),
}
}
fn queue_factory_batch(&mut self, operation: u8) {
let batch_size = match (operation / 7) % 4 {
0 => 1,
1 => COMMANDS_PER_CYCLE - 1,
2 => COMMANDS_PER_CYCLE,
_ => COMMANDS_PER_CYCLE + 1,
};
for _ in 0..batch_size {
let invocations = Arc::clone(&self.factory_invocations);
let forbidden = Arc::clone(&self.forbidden_factory_invocations);
let shutdown_signal = Arc::clone(&self.shutdown_signal);
let forbidden_after_queued_shutdown = self.queued_shutdown;
if self
.command_tx
.send(AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new(move |(), _| {
invocations.fetch_add(1, Ordering::Relaxed);
if forbidden_after_queued_shutdown || shutdown_signal.load(Ordering::Acquire) {
forbidden.fetch_add(1, Ordering::Relaxed);
}
})),
})
.is_err()
{
break;
}
self.queued_factories += 1;
self.sent_factories += 1;
}
}
fn ready_timer(&mut self) {
if !self.timer_ready {
self.control.advance(Duration::from_secs(1));
self.timer_ready = true;
}
}
fn step(&mut self) {
if self.worker.executor.is_none() {
return;
}
WorkerSignal::waker(&self.signal).wake_by_ref();
let initialized = self.worker.thread_state.as_ref().is_some_and(|state| state.get().is_some());
let full_batch = initialized
&& self.queued_factories >= COMMANDS_PER_CYCLE
&& !self.shutdown_signal.load(Ordering::Acquire)
&& !self.queued_shutdown;
let task_should_progress =
full_batch && self.ready_task.is_none() && !self.task_done.load(Ordering::Relaxed) && self.progress_failure.is_none();
let timer_should_progress =
full_batch && self.timer_advanced_by_step && !self.timer_done.load(Ordering::Relaxed) && self.progress_failure.is_none();
let running = self.worker.execute_step().is_continue();
if running && task_should_progress && !self.task_done.load(Ordering::Relaxed) {
self.progress_failure = Some("ready task must progress between full command batches");
}
if running && timer_should_progress && !self.timer_done.load(Ordering::Relaxed) {
self.progress_failure = Some("ready timer must progress between full command batches");
}
if full_batch {
self.queued_factories -= COMMANDS_PER_CYCLE;
} else if initialized && !self.shutdown_signal.load(Ordering::Acquire) && !self.queued_shutdown {
self.queued_factories = 0;
}
self.timer_advanced_by_step |= self.timer_ready;
}
fn queue_shutdown(&mut self) {
if !self.queued_shutdown && self.command_tx.send(AsyncWorkerCommand::Shutdown).is_ok() {
self.queued_shutdown = true;
}
}
fn finish(mut self) {
if let Some(initialize) = self.initialize.take() {
initialize.send(());
}
if let Some(ready_task) = self.ready_task.take() {
ready_task.send(());
}
self.ready_timer();
self.queue_shutdown();
let mut stopped = self.worker.executor.is_none();
for _ in 0..Self::MAX_CLEANUP_STEPS {
if stopped {
break;
}
WorkerSignal::waker(&self.signal).wake_by_ref();
stopped = self.worker.execute_step().is_break();
}
if !stopped {
std::mem::forget(self.worker);
panic!("bounded cleanup must complete worker shutdown");
}
assert!(self.progress_failure.is_none(), "{}", self.progress_failure.unwrap_or_default());
assert_eq!(
self.forbidden_factory_invocations.load(Ordering::Relaxed),
0,
"factories queued after shutdown or pending at shutdown publication must not be invoked"
);
assert!(self.factory_invocations.load(Ordering::Relaxed) <= self.sent_factories);
}
}
#[cfg(not(miri))]
#[test]
fn generated_command_sequences_preserve_progress_and_close_admission() {
bolero::check!().for_each(|input: &[u8]| {
let mut scenario = GeneratedWorkerScenario::new();
for operation in input.iter().copied().take(GeneratedWorkerScenario::MAX_OPERATIONS) {
scenario.apply(operation);
}
scenario.finish();
});
}
#[test]
fn a_never_empty_command_queue_allows_registered_tasks_and_timers_to_progress() {
let (command_tx, command_rx) = channel::unbounded();
let (ready_tx, ready_rx) = channel::unbounded();
let (proceed, progress) = Event::boxed();
let control = ClockControl::new();
let task_done = Arc::new(AtomicBool::new(false));
let timer_done = Arc::new(AtomicBool::new(false));
let processed = Arc::new(AtomicUsize::new(0));
let task_observer = Arc::clone(&task_done);
let timer_observer = Arc::clone(&timer_done);
let shutdown = command_tx.clone();
let worker = unsafe {
AsyncWorker::new(
command_rx,
async move |tasks, clock| {
drop(tasks.add(async move {
progress.await.unwrap();
task_observer.store(true, Ordering::Relaxed);
}));
drop(tasks.add(async move {
clock.delay(Duration::from_secs(1)).await;
timer_observer.store(true, Ordering::Relaxed);
shutdown.send(AsyncWorkerCommand::Shutdown).unwrap();
}));
},
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
control.clone().into(),
Arc::new(WorkerSignal::default()),
Arc::new(AtomicBool::new(false)),
ready_tx,
)
};
let executor = worker.executor.as_ref().unwrap();
let _ = executor.execute_cycle();
ready_rx.recv().unwrap();
let _ = executor.execute_cycle();
assert!(!task_done.load(Ordering::Relaxed));
assert!(!timer_done.load(Ordering::Relaxed));
command_tx
.send(replenishing_command(command_tx.clone(), Arc::clone(&processed)))
.unwrap();
proceed.send(());
control.advance(Duration::from_secs(1));
worker.run();
assert!(task_done.load(Ordering::Relaxed));
assert!(timer_done.load(Ordering::Relaxed));
assert!((COMMANDS_PER_CYCLE..=2 * COMMANDS_PER_CYCLE + 1).contains(&processed.load(Ordering::Relaxed)));
}
#[test]
fn a_full_command_batch_skips_waiting_for_the_worker_signal() {
let (command_tx, command_rx) = channel::unbounded();
let (ready_tx, ready_rx) = channel::unbounded();
let signal = Arc::new(WorkerSignal::default());
let mut worker = unsafe {
AsyncWorker::new(
command_rx,
async |_, _| (),
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
InactiveClock::default(),
Arc::clone(&signal),
Arc::new(AtomicBool::new(false)),
ready_tx,
)
};
assert!(worker.execute_step().is_continue());
ready_rx.recv().unwrap();
WorkerSignal::waker(&signal).wake_by_ref();
assert!(signal.is_notified());
assert!(worker.execute_step().is_continue());
assert!(!signal.is_notified());
for _ in 0..COMMANDS_PER_CYCLE {
command_tx
.send(AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new(|(), _| {})),
})
.unwrap();
}
WorkerSignal::waker(&signal).wake_by_ref();
assert!(signal.is_notified());
assert!(worker.execute_step().is_continue());
assert!(signal.is_notified());
command_tx.send(AsyncWorkerCommand::Shutdown).unwrap();
worker.run();
}
#[cfg(not(miri))]
#[test]
fn final_timer_panic_after_completed_shutdown_does_not_trigger_the_lifecycle_guard() {
struct PanicWake;
impl Wake for PanicWake {
fn wake(self: std::sync::Arc<Self>) {
panic!("final timer callback");
}
}
let (command_tx, command_rx) = channel::unbounded();
let (ready_tx, ready_rx) = channel::unbounded();
let (clock_tx, clock_rx) = channel::unbounded();
let worker = unsafe {
AsyncWorker::new(
command_rx,
async move |_, clock| clock_tx.send(clock).unwrap(),
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
InactiveClock::default(),
Arc::new(WorkerSignal::default()),
Arc::new(AtomicBool::new(false)),
ready_tx,
)
};
let _ = worker.executor.as_ref().unwrap().execute_cycle();
ready_rx.recv().unwrap();
let _ = worker.executor.as_ref().unwrap().execute_cycle();
let clock = clock_rx.recv().unwrap();
let mut timer = clock.delay(Duration::from_millis(1));
let waker = Waker::from(Arc::into_std_arc(Arc::new(PanicWake)));
assert!(Pin::new(&mut timer).poll(&mut std::task::Context::from_waker(&waker)).is_pending());
std::mem::forget(timer);
thread::sleep(Duration::from_millis(2));
command_tx.send(AsyncWorkerCommand::Shutdown).unwrap();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| worker.run()));
assert_eq!(*outcome.unwrap_err().downcast::<&str>().unwrap(), "final timer callback");
}
#[test]
fn smoke_test() {
async_test(async || {
let (command_tx, command_rx) = channel::unbounded();
let async_worker_thread = thread::spawn(move || {
let blocking_worker = BlockingWorker::new(BlockingPool::new(None), Sink::noop());
let signal = Arc::new(WorkerSignal::default());
let worker = unsafe {
AsyncWorker::new(
command_rx,
async move |tasks, _| TestTaskContext::new(tasks),
Arc::clone(&blocking_worker),
InactiveClock::default(),
signal,
Arc::new(AtomicBool::new(false)),
channel::unbounded().0,
)
};
worker.run();
assert!(is_blocking_worker_shutting_down(&blocking_worker));
});
let (outer_completed_tx, outer_completed_rx) = Event::boxed();
let (inner_completed_tx, inner_completed_rx) = Event::boxed();
command_tx
.send(AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new({
move |_: TestTaskContext, tasks: &TaskSet| {
drop(tasks.add(async move {
inner_completed_tx.send(());
}));
drop(tasks.add(async move {
inner_completed_rx.await.unwrap();
outer_completed_tx.send(());
}));
}
})),
})
.unwrap();
outer_completed_rx.await.unwrap();
command_tx.send(AsyncWorkerCommand::Shutdown).unwrap();
async_worker_thread.join().unwrap();
});
}
#[test]
fn thread_state_constructor_creates_new_tasks() {
async_test(async || {
let (initial_completed_tx, initial_completed_rx) = Event::boxed();
let (spawned_completed_tx, spawned_completed_rx) = Event::boxed();
let (constructed_tx, constructed_rx) = channel::unbounded();
let (command_tx, command_rx) = channel::unbounded();
let worker_thread = thread::spawn(move || {
let signal = Arc::new(WorkerSignal::default());
let worker = unsafe {
AsyncWorker::new(
command_rx,
async move |tasks, _| {
drop(tasks.add(async move { initial_completed_tx.send(()) }));
TestTaskContext::new(tasks)
},
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
InactiveClock::default(),
signal,
Arc::new(AtomicBool::new(false)),
constructed_tx,
)
};
worker.run();
});
command_tx
.send(AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new({
move |_: TestTaskContext, tasks: &TaskSet| {
drop(tasks.add(async move {
spawned_completed_tx.send(());
}));
}
})),
})
.unwrap();
constructed_rx.recv().unwrap();
spawned_completed_rx.await.unwrap();
initial_completed_rx.into_value().unwrap();
command_tx.send(AsyncWorkerCommand::Shutdown).unwrap();
worker_thread.join().unwrap();
});
}
#[test]
fn task_after_shutdown_is_ignored() {
let (command_tx, command_rx) = channel::unbounded();
let async_worker_thread = thread::spawn(move || {
let signal = Arc::new(WorkerSignal::default());
let worker = unsafe {
AsyncWorker::new(
command_rx,
async move |tasks, _| TestTaskContext::new(tasks),
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
InactiveClock::default(),
signal,
Arc::new(AtomicBool::new(false)),
channel::unbounded().0,
)
};
worker.run();
});
command_tx.send(AsyncWorkerCommand::Shutdown).unwrap();
let (completed_tx, completed_rx) = Event::boxed();
let _ = command_tx.send(AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new({
move |_: TestTaskContext, tasks: &TaskSet| {
drop(tasks.add(async move {
completed_tx.send(14);
}));
}
})),
});
async_worker_thread.join().unwrap();
let completed_result = completed_rx.into_value();
assert!(matches!(completed_result, Err(IntoValueError::Disconnected)));
}
#[test]
fn shutdown_signal_prevents_queued_factories_from_starting() {
let (command_tx, command_rx) = channel::unbounded();
let (ready_tx, ready_rx) = channel::unbounded();
let shutdown_signal = Arc::new(AtomicBool::new(false));
let invoked = Arc::new(AtomicBool::new(false));
let invoked_by_factory = Arc::clone(&invoked);
let worker = unsafe {
AsyncWorker::new(
command_rx,
async |_, _| (),
BlockingWorker::new(BlockingPool::new(None), Sink::noop()),
InactiveClock::default(),
Arc::new(WorkerSignal::default()),
Arc::clone(&shutdown_signal),
ready_tx,
)
};
let _ = worker.executor.as_ref().unwrap().execute_cycle();
ready_rx.recv().unwrap();
command_tx
.send(AsyncWorkerCommand::EnqueueTask {
future_factory: Some(Box::new(move |(), _| {
invoked_by_factory.store(true, Ordering::Relaxed);
})),
})
.unwrap();
shutdown_signal.store(true, Ordering::Release);
worker.run();
assert!(!invoked.load(Ordering::Relaxed));
}
#[test]
fn shutdown_before_initialization_discards_the_constructor() {
let initialized = Arc::new(AtomicBool::new(false));
let observed = Arc::clone(&initialized);
let (_commands, receiver) = channel::unbounded();
let blocking_worker = BlockingWorker::new(BlockingPool::new(None), Sink::noop());
let mut worker = unsafe {
AsyncWorker::new(
receiver,
async move |tasks, _| {
observed.store(true, Ordering::Relaxed);
TestTaskContext::new(tasks)
},
Arc::clone(&blocking_worker),
InactiveClock::default(),
Arc::new(WorkerSignal::default()),
Arc::new(AtomicBool::new(false)),
channel::unbounded().0,
)
};
worker.process_commands();
worker.begin_shutdown();
worker.process_commands();
worker.run();
assert!(!initialized.load(Ordering::Relaxed));
assert!(is_blocking_worker_shutting_down(&blocking_worker));
}
#[test]
fn queued_factory_drop_panics_are_contained() {
struct PanicOnDrop;
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("queued factory drop");
}
}
let panic_on_drop = PanicOnDrop;
let command = AsyncWorkerCommand::<()>::EnqueueTask {
future_factory: Some(Box::new(move |(), _: &TaskSet| {
let _panic_on_drop = panic_on_drop;
})),
};
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(command))).unwrap();
}
}