use std::time::Duration;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
#[derive(Debug)]
pub struct SpawnedTask {
cancel: CancellationToken,
handle: JoinHandle<()>,
}
impl SpawnedTask {
pub fn spawn<F, Fut>(body: F) -> Self
where
F: FnOnce(CancellationToken) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
{
let cancel = CancellationToken::new();
let handle = tokio::spawn(body(cancel.clone()));
Self { cancel, handle }
}
pub fn spawn_with<Fut>(cancel: CancellationToken, body: Fut) -> Self
where
Fut: Future<Output = ()> + Send + 'static,
{
let handle = tokio::spawn(body);
Self { cancel, handle }
}
#[must_use]
pub const fn from_parts(cancel: CancellationToken, handle: JoinHandle<()>) -> Self {
Self { cancel, handle }
}
pub fn cancel(&self) {
self.cancel.cancel();
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.handle.is_finished()
}
#[must_use]
pub fn cancellation(&self) -> CancellationToken {
self.cancel.clone()
}
pub async fn shutdown(self, grace: Duration) {
self.cancel.cancel();
let mut handle = self.handle;
if tokio::time::timeout(grace, &mut handle).await.is_err() {
handle.abort();
let _ = handle.await;
}
}
pub fn abort(self) {
self.handle.abort();
}
pub async fn join(self) {
let _ = self.handle.await;
}
}
#[derive(Debug, Default)]
pub struct TaskGroup {
tasks: Vec<SpawnedTask>,
}
impl TaskGroup {
#[must_use]
pub const fn new() -> Self {
Self { tasks: Vec::new() }
}
pub fn push(&mut self, task: SpawnedTask) {
self.tasks.push(task);
}
#[must_use]
pub const fn len(&self) -> usize {
self.tasks.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
pub fn cancel_all(&self) {
for task in &self.tasks {
task.cancel();
}
}
pub async fn shutdown(&mut self, grace: Duration) {
let tasks = std::mem::take(&mut self.tasks);
for task in &tasks {
task.cancel();
}
for task in tasks {
task.shutdown(grace).await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[tokio::test]
async fn shutdown_cancels_cooperative_task() {
let ran = Arc::new(AtomicBool::new(false));
let r = ran.clone();
let task = SpawnedTask::spawn(move |cancel| async move {
cancel.cancelled().await;
r.store(true, Ordering::SeqCst);
});
task.shutdown(Duration::from_secs(1)).await;
assert!(ran.load(Ordering::SeqCst));
}
#[tokio::test(start_paused = true)]
async fn shutdown_aborts_wedged_task() {
let task = SpawnedTask::spawn(|_cancel| async move {
std::future::pending::<()>().await;
});
task.shutdown(Duration::from_millis(50)).await;
}
#[tokio::test]
async fn group_shuts_down_all() {
let mut group = TaskGroup::new();
for _ in 0..3 {
group.push(SpawnedTask::spawn(|cancel| async move {
cancel.cancelled().await;
}));
}
assert_eq!(group.len(), 3);
group.shutdown(Duration::from_secs(1)).await;
assert!(group.is_empty());
}
#[tokio::test]
async fn task_constructors_cancel_abort_and_join_paths() {
let token = CancellationToken::new();
let finished = Arc::new(AtomicBool::new(false));
let done = Arc::clone(&finished);
let task = SpawnedTask::spawn_with(token.clone(), async move {
token.cancelled().await;
done.store(true, Ordering::SeqCst);
});
assert!(!task.is_finished());
let cancellation = task.cancellation();
task.cancel();
task.join().await;
assert!(cancellation.is_cancelled());
assert!(finished.load(Ordering::SeqCst));
let token = CancellationToken::new();
let handle = tokio::spawn(async move {
std::future::pending::<()>().await;
});
let task = SpawnedTask::from_parts(token, handle);
task.abort();
}
#[tokio::test]
async fn cancel_all_cancels_without_waiting() {
let mut group = TaskGroup::new();
let token = CancellationToken::new();
let task = SpawnedTask::from_parts(
token.clone(),
tokio::spawn(async move {
std::future::pending::<()>().await;
}),
);
group.push(task);
group.cancel_all();
assert!(token.is_cancelled());
group.shutdown(Duration::from_millis(1)).await;
}
}