481 lines
16 KiB
Rust
481 lines
16 KiB
Rust
use dever_runtime::task::{Error, Limits, Runtime};
|
|
|
|
mod deadline;
|
|
mod group;
|
|
mod panic;
|
|
use std::sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, Ordering},
|
|
};
|
|
|
|
struct DropNotice(Arc<AtomicBool>);
|
|
|
|
impl Drop for DropNotice {
|
|
fn drop(&mut self) {
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
self.0.store(true, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
fn runtime() -> Runtime {
|
|
Runtime::new(Limits {
|
|
threads: 2,
|
|
workers: 1,
|
|
tasks: 8,
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn scope_waits_for_blocking_work_after_async_parent_is_cancelled() {
|
|
let entered = Arc::new(AtomicBool::new(false));
|
|
let finished = Arc::new(AtomicBool::new(false));
|
|
runtime()
|
|
.run(async |scope| {
|
|
let parent_entered = entered.clone();
|
|
let parent_finished = finished.clone();
|
|
let parent = scope
|
|
.spawn(move |scope| async move {
|
|
scope
|
|
.blocking(move || {
|
|
parent_entered.store(true, Ordering::SeqCst);
|
|
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
parent_finished.store(true, Ordering::SeqCst);
|
|
})
|
|
.await
|
|
.unwrap();
|
|
})
|
|
.unwrap();
|
|
while !entered.load(Ordering::SeqCst) {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert!(parent.stop().await.unwrap().is_none());
|
|
assert!(finished.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn task_errors_and_capacity_are_observable_and_slots_are_reused() {
|
|
let runtime = Runtime::new(Limits {
|
|
threads: 1,
|
|
workers: 1,
|
|
tasks: 1,
|
|
})
|
|
.unwrap();
|
|
runtime
|
|
.run(async |scope| {
|
|
let pending = scope.spawn_leaf(std::future::pending::<()>).unwrap();
|
|
assert!(matches!(scope.spawn_sync(|| ()), Err(Error::Capacity)));
|
|
assert!(pending.stop().await.unwrap().is_none());
|
|
let value = scope.spawn_sync(|| 42).unwrap().wait().await.unwrap();
|
|
assert_eq!(value, 42);
|
|
let failure = scope
|
|
.spawn_leaf(|| async {
|
|
panic!("test panic");
|
|
})
|
|
.unwrap();
|
|
assert_eq!(failure.wait().await, Err(Error::Panicked));
|
|
let failure = scope.blocking(|| panic!("direct worker panic")).await;
|
|
assert_eq!(failure, Err(Error::Panicked));
|
|
assert_eq!(scope.blocking(|| 42).await.unwrap(), 42);
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancelled_function_wait_keeps_descendants_tracked_until_their_real_exit() {
|
|
let finished = Arc::new(AtomicBool::new(false));
|
|
let observed = finished.clone();
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
runtime()
|
|
.run(async |root| {
|
|
let parent = root
|
|
.spawn(move |child| async move {
|
|
let nested = child
|
|
.spawn_sync(move || {
|
|
started.send(()).unwrap();
|
|
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
finished.store(true, Ordering::SeqCst);
|
|
})
|
|
.unwrap();
|
|
nested.wait().await.unwrap();
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
assert!(parent.stop().await.unwrap().is_none());
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancelling_queued_worker_never_executes_its_call() {
|
|
let called = Arc::new(AtomicBool::new(false));
|
|
runtime()
|
|
.run(async |scope| {
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
let (release, blocked) = std::sync::mpsc::channel();
|
|
let first = scope
|
|
.spawn_sync(move || {
|
|
started.send(()).unwrap();
|
|
blocked
|
|
.recv_timeout(std::time::Duration::from_secs(2))
|
|
.unwrap();
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
let work_called = called.clone();
|
|
let queued = scope
|
|
.spawn_sync(move || work_called.store(true, Ordering::SeqCst))
|
|
.unwrap();
|
|
assert!(queued.stop().await.unwrap().is_none());
|
|
release.send(()).unwrap();
|
|
first.wait().await.unwrap();
|
|
assert!(!called.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn claimed_output_can_outlive_its_task_scope() {
|
|
let runtime = runtime();
|
|
for direct in [false, true] {
|
|
let value = runtime
|
|
.run(async |scope| {
|
|
let value = if direct {
|
|
scope
|
|
.blocking(|| String::from("owned output"))
|
|
.await
|
|
.unwrap()
|
|
} else {
|
|
let task = scope.spawn_sync(|| String::from("owned output")).unwrap();
|
|
task.wait().await.unwrap()
|
|
};
|
|
scope.finish().await;
|
|
assert!(matches!(
|
|
scope.spawn_leaf(|| async {}),
|
|
Err(Error::Cancelled)
|
|
));
|
|
assert_eq!(scope.blocking(|| 42).await, Err(Error::Cancelled));
|
|
value
|
|
})
|
|
.unwrap();
|
|
assert_eq!(value, "owned output");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn finished_empty_scope_rejects_new_work() {
|
|
runtime()
|
|
.run(async |scope| {
|
|
scope.finish().await;
|
|
assert!(matches!(
|
|
scope.spawn_leaf(|| async {}),
|
|
Err(Error::Cancelled)
|
|
));
|
|
assert_eq!(scope.blocking(|| 42).await, Err(Error::Cancelled));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn root_panic_drains_workers_from_dropped_function_handles() {
|
|
let runtime = runtime();
|
|
let finished = Arc::new(AtomicBool::new(false));
|
|
let observed = finished.clone();
|
|
let result = runtime.run(async |root| {
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
let _task = root
|
|
.spawn_sync(move || {
|
|
started.send(()).unwrap();
|
|
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
finished.store(true, Ordering::SeqCst);
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
panic!("root failure after worker started");
|
|
});
|
|
assert_eq!(result, Err(Error::Panicked));
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
}
|
|
|
|
#[test]
|
|
fn root_panic_cancels_leaf_work_and_allows_the_next_entry() {
|
|
let runtime = runtime();
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let observed = dropped.clone();
|
|
let result = runtime.run(async |root| {
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
let _task = root
|
|
.spawn_leaf(move || async move {
|
|
let _owned = DropNotice(dropped);
|
|
started.send(()).unwrap();
|
|
std::future::pending::<()>().await;
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
panic!("root failure after leaf started");
|
|
});
|
|
assert_eq!(result, Err(Error::Panicked));
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
let result = runtime.run(async |root| {
|
|
root.spawn_leaf(|| async { Err::<(), _>(String::from("business failure")) })
|
|
.unwrap()
|
|
.wait()
|
|
.await
|
|
.unwrap()
|
|
});
|
|
assert_eq!(result, Ok(Err(String::from("business failure"))));
|
|
}
|
|
|
|
#[test]
|
|
fn abandoned_task_results_are_dropped_before_scope_finish_returns() {
|
|
let runtime = runtime();
|
|
for direct in [false, true] {
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let observed = dropped.clone();
|
|
runtime
|
|
.run(async |scope| {
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
let call = move || {
|
|
started.send(()).unwrap();
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
DropNotice(dropped)
|
|
};
|
|
let task = if direct {
|
|
scope
|
|
.spawn(move |child| async move { child.blocking(call).await.unwrap() })
|
|
.unwrap()
|
|
} else {
|
|
scope.spawn_sync(call).unwrap()
|
|
};
|
|
ready.await.unwrap();
|
|
drop(task);
|
|
scope.finish().await;
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn finished_tasks_drop_unclaimed_outputs_before_scope_returns() {
|
|
let runtime = Runtime::new(Limits {
|
|
threads: 1,
|
|
workers: 1,
|
|
tasks: 8,
|
|
})
|
|
.unwrap();
|
|
for leaf in [false, true] {
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let observed = dropped.clone();
|
|
runtime
|
|
.run(async |root| {
|
|
let (completed, ready) = tokio::sync::oneshot::channel();
|
|
let call = move || async move {
|
|
// 唯一 worker 在本任务完成后才会运行 marker,不用睡眠猜测完成时点。
|
|
tokio::spawn(async move {
|
|
completed.send(()).unwrap();
|
|
});
|
|
DropNotice(dropped)
|
|
};
|
|
let task = if leaf {
|
|
root.spawn_leaf(call).unwrap()
|
|
} else {
|
|
root.spawn(move |_child| call()).unwrap()
|
|
};
|
|
ready.await.unwrap();
|
|
drop(task);
|
|
root.finish().await;
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unclaimed_panicking_leaf_drops_owned_values_before_scope_finishes() {
|
|
let finished = Arc::new(AtomicBool::new(false));
|
|
let observed = finished.clone();
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
runtime()
|
|
.run(async |scope| {
|
|
let task = scope
|
|
.spawn_leaf(move || async move {
|
|
let _owned = DropNotice(finished);
|
|
started.send(()).unwrap();
|
|
panic!("unclaimed leaf panic");
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
drop(task);
|
|
scope.finish().await;
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn parent_panic_drains_started_workers_before_reporting_failure() {
|
|
let finished = Arc::new(AtomicBool::new(false));
|
|
let observed = finished.clone();
|
|
runtime()
|
|
.run(async |root| {
|
|
let parent = root
|
|
.spawn(move |child| async move {
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
let _worker = child
|
|
.spawn_sync(move || {
|
|
started.send(()).unwrap();
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
finished.store(true, Ordering::SeqCst);
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
panic!("parent panic after worker started");
|
|
})
|
|
.unwrap();
|
|
assert_eq!(parent.wait().await, Err(Error::Panicked));
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancelling_a_leaf_waits_for_its_pending_future_destructor() {
|
|
let finished = Arc::new(AtomicBool::new(false));
|
|
let observed = finished.clone();
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
runtime()
|
|
.run(async |root| {
|
|
let leaf = root
|
|
.spawn_leaf(move || async move {
|
|
let _owned = DropNotice(finished);
|
|
started.send(()).unwrap();
|
|
std::future::pending::<()>().await;
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
assert_eq!(leaf.stop().await, Ok(None));
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancelling_a_parent_drains_its_suspended_leaf() {
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let observed = dropped.clone();
|
|
let (started, ready) = tokio::sync::oneshot::channel();
|
|
runtime()
|
|
.run(async |root| {
|
|
let parent = root
|
|
.spawn(move |child| async move {
|
|
let leaf = child
|
|
.spawn_leaf(move || async move {
|
|
let _owned = DropNotice(dropped);
|
|
started.send(()).unwrap();
|
|
std::future::pending::<()>().await;
|
|
})
|
|
.unwrap();
|
|
leaf.wait().await.unwrap();
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
assert_eq!(parent.stop().await, Ok(None));
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancelling_completed_leaves_preserves_business_errors_and_panics() {
|
|
let runtime = Runtime::new(Limits {
|
|
threads: 1,
|
|
workers: 1,
|
|
tasks: 8,
|
|
})
|
|
.unwrap();
|
|
for panicked in [false, true] {
|
|
runtime
|
|
.run(async |root| {
|
|
let (completed, ready) = tokio::sync::oneshot::channel();
|
|
let task = root
|
|
.spawn_leaf(move || async move {
|
|
// 唯一 worker 只能在本任务完成清理后再运行 marker,避免靠睡眠猜测完成。
|
|
tokio::spawn(async move {
|
|
completed.send(()).unwrap();
|
|
});
|
|
if panicked {
|
|
panic!("completed leaf panic");
|
|
}
|
|
Err::<(), _>("completed failure")
|
|
})
|
|
.unwrap();
|
|
ready.await.unwrap();
|
|
let result = task.stop().await;
|
|
if panicked {
|
|
assert_eq!(result, Err(Error::Panicked));
|
|
} else {
|
|
assert_eq!(result, Ok(Some(Err("completed failure"))));
|
|
}
|
|
})
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unpolled_leaf_inputs_are_dropped_before_scope_finishes() {
|
|
let runtime = Runtime::new(Limits {
|
|
threads: 1,
|
|
workers: 1,
|
|
tasks: 8,
|
|
})
|
|
.unwrap();
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let observed = dropped.clone();
|
|
let entered = Arc::new(AtomicBool::new(false));
|
|
let polled = entered.clone();
|
|
runtime
|
|
.run(async |root| {
|
|
// 单线程入口在这里不挂起,子任务第一次 poll 前就会收到中止。
|
|
let input = DropNotice(dropped);
|
|
let task = root
|
|
.spawn_leaf(move || async move {
|
|
entered.store(true, Ordering::SeqCst);
|
|
let _input = input;
|
|
std::future::pending::<()>().await;
|
|
})
|
|
.unwrap();
|
|
drop(task);
|
|
root.finish().await;
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
assert!(!polled.load(Ordering::SeqCst));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn leaf_entries_drop_future_state_and_preserve_results_and_panics() {
|
|
let runtime = runtime();
|
|
for panicked in [false, true] {
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let observed = dropped.clone();
|
|
let result = runtime.run_leaf(async move {
|
|
let _value = DropNotice(dropped);
|
|
tokio::task::yield_now().await;
|
|
if panicked {
|
|
panic!("leaf entry panic");
|
|
}
|
|
String::from("owned result")
|
|
});
|
|
assert!(observed.load(Ordering::SeqCst));
|
|
if panicked {
|
|
assert_eq!(result, Err(Error::Panicked));
|
|
} else {
|
|
assert_eq!(result.unwrap(), "owned result");
|
|
}
|
|
}
|
|
}
|