use std::{future::Future, ops::Deref, time::Duration};
use tokio::runtime::Handle;
use tokio::task::{JoinHandle, LocalSet};
use tokio_util::sync::{CancellationToken, DropGuardRef};
pub use tokio_util::task::task_tracker::{
TaskTracker, TaskTrackerToken, TaskTrackerWaitFuture, TrackedFuture,
};
#[derive(Debug, PartialEq, Eq)]
pub enum CancelOutcome<T> {
Completed(T),
Cancelled,
}
impl<T> CancelOutcome<T> {
#[inline]
pub fn outcome(result: Option<T>) -> CancelOutcome<T> {
match result {
Some(value) => CancelOutcome::Completed(value),
None => CancelOutcome::Cancelled,
}
}
}
#[derive(Clone)]
pub struct TaskSupervisor {
tracker: TaskTracker,
shutdown: CancellationToken,
}
impl TaskSupervisor {
#[must_use]
pub fn new() -> Self {
Self {
tracker: TaskTracker::new(),
shutdown: CancellationToken::new(),
}
}
#[inline]
pub fn tracker(&self) -> &TaskTracker {
&self.tracker
}
#[inline]
pub fn token(&self) -> CancellationToken {
self.shutdown.clone()
}
#[must_use]
#[inline]
pub fn cancel_on_drop(&self) -> DropGuardRef<'_> {
self.shutdown.drop_guard_ref()
}
#[inline]
pub fn is_cancelled(&self) -> bool {
self.shutdown.is_cancelled()
}
#[inline]
pub fn is_closed(&self) -> bool {
self.tracker.is_closed()
}
#[inline]
pub fn len(&self) -> usize {
self.tracker.len()
}
#[inline]
pub fn wait(&self) -> TaskTrackerWaitFuture<'_> {
self.tracker.wait()
}
#[inline]
pub fn cancel(&self) {
self.shutdown.cancel();
}
pub async fn shutdown(&self) {
self.tracker.close();
self.shutdown.cancel();
self.tracker.wait().await;
}
#[inline]
pub async fn shutdown_with_timeout(
&self,
timeout: Duration,
) -> Result<(), tokio::time::error::Elapsed> {
tokio::time::timeout(timeout, self.shutdown()).await
}
#[must_use]
pub fn spawn_with_cancel<F, Fut>(&self, task: F) -> JoinHandle<CancelOutcome<Fut::Output>>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
let token = self.token();
self.tracker
.spawn(async move { CancelOutcome::outcome(token.run_until_cancelled(task()).await) })
}
#[must_use]
pub fn spawn_on_with_cancel<F, Fut>(
&self,
task: F,
handle: &Handle,
) -> JoinHandle<CancelOutcome<Fut::Output>>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
let token = self.token();
self.tracker.spawn_on(
async move { CancelOutcome::outcome(token.run_until_cancelled(task()).await) },
handle,
)
}
#[must_use]
pub fn spawn_local_with_cancel<F, Fut>(&self, task: F) -> JoinHandle<CancelOutcome<Fut::Output>>
where
F: FnOnce() -> Fut + 'static,
Fut: Future + 'static,
Fut::Output: 'static,
{
let token = self.token();
self.tracker.spawn_local(async move {
CancelOutcome::outcome(token.run_until_cancelled(task()).await)
})
}
#[must_use]
pub fn spawn_local_on_with_cancel<F, Fut>(
&self,
task: F,
local_set: &LocalSet,
) -> JoinHandle<CancelOutcome<Fut::Output>>
where
F: FnOnce() -> Fut + 'static,
Fut: Future + 'static,
Fut::Output: 'static,
{
let token = self.token();
self.tracker.spawn_local_on(
async move { CancelOutcome::outcome(token.run_until_cancelled(task()).await) },
local_set,
)
}
#[must_use]
pub fn spawn_with_token<F, Fut>(&self, task: F) -> JoinHandle<Fut::Output>
where
F: FnOnce(CancellationToken) -> Fut + Send + 'static,
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
let token = self.shutdown.child_token();
self.tracker.spawn(async move { task(token).await })
}
#[must_use]
pub fn spawn_on_with_token<F, Fut>(&self, task: F, handle: &Handle) -> JoinHandle<Fut::Output>
where
F: FnOnce(CancellationToken) -> Fut + Send + 'static,
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
let token = self.shutdown.child_token();
self.tracker
.spawn_on(async move { task(token).await }, handle)
}
#[must_use]
pub fn spawn_local_with_token<F, Fut>(&self, task: F) -> JoinHandle<Fut::Output>
where
F: FnOnce(CancellationToken) -> Fut + 'static,
Fut: Future + 'static,
Fut::Output: 'static,
{
let token = self.shutdown.child_token();
self.tracker.spawn_local(async move { task(token).await })
}
#[must_use]
pub fn spawn_local_on_with_token<F, Fut>(
&self,
task: F,
local_set: &LocalSet,
) -> JoinHandle<Fut::Output>
where
F: FnOnce(CancellationToken) -> Fut + 'static,
Fut: Future + 'static,
Fut::Output: 'static,
{
let token = self.shutdown.child_token();
self.tracker
.spawn_local_on(async move { task(token).await }, local_set)
}
#[cfg(not(target_family = "wasm"))]
#[must_use]
pub fn spawn_blocking_with_token<F, T>(&self, task: F) -> JoinHandle<T>
where
F: FnOnce(CancellationToken) -> T + Send + 'static,
T: Send + 'static,
{
let token = self.shutdown.child_token();
self.tracker.spawn_blocking(move || task(token))
}
#[cfg(not(target_family = "wasm"))]
#[must_use]
pub fn spawn_blocking_on_with_token<F, T>(&self, task: F, handle: &Handle) -> JoinHandle<T>
where
F: FnOnce(CancellationToken) -> T + Send + 'static,
T: Send + 'static,
{
let token = self.shutdown.child_token();
self.tracker.spawn_blocking_on(move || task(token), handle)
}
}
impl Default for TaskSupervisor {
fn default() -> Self {
Self::new()
}
}
impl Deref for TaskSupervisor {
type Target = TaskTracker;
fn deref(&self) -> &Self::Target {
&self.tracker
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::time::{sleep, Duration};
#[tokio::test]
async fn test_spawn_with_token_provides_token() {
let supervisor = TaskSupervisor::new();
let handle = supervisor.spawn_with_token(|token| async move { token.is_cancelled() });
let result = handle.await.unwrap();
assert!(!result);
}
#[tokio::test]
#[cfg(feature = "rt")]
async fn test_spawn_on_with_token_provides_token() {
let supervisor = TaskSupervisor::new();
let handle = tokio::runtime::Handle::current();
let result = supervisor
.spawn_on_with_token(|token| async move { token.is_cancelled() }, &handle)
.await
.unwrap();
assert!(!result);
}
#[tokio::test]
#[cfg(all(feature = "rt", not(target_family = "wasm")))]
async fn test_spawn_blocking_with_token_provides_token() {
let supervisor = TaskSupervisor::new();
let handle = supervisor.spawn_blocking_with_token(move |token| token.is_cancelled());
let result = handle.await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn test_cancel_sets_cancelled_state() {
let supervisor = TaskSupervisor::new();
assert!(!supervisor.is_cancelled());
supervisor.cancel();
assert!(supervisor.is_cancelled());
}
#[tokio::test]
async fn test_cancel_propagates_to_all_tasks() {
let supervisor = TaskSupervisor::new();
let count = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let count_clone = count.clone();
let _ = supervisor.spawn_with_token(|token| async move {
token.cancelled().await;
count_clone.fetch_add(1, Ordering::SeqCst);
});
}
sleep(Duration::from_millis(50)).await;
supervisor.cancel();
sleep(Duration::from_millis(100)).await;
assert_eq!(count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_shutdown_cancels_and_waits() {
let supervisor = TaskSupervisor::new();
let task_finished = Arc::new(AtomicBool::new(false));
let task_finished_clone = task_finished.clone();
let _ = supervisor.spawn_with_token(|_token| async move {
sleep(Duration::from_millis(100)).await;
task_finished_clone.store(true, Ordering::SeqCst);
});
assert!(!supervisor.is_cancelled());
assert!(!supervisor.is_closed());
supervisor.shutdown().await;
assert!(supervisor.is_cancelled());
assert!(supervisor.is_closed());
assert!(task_finished.load(Ordering::SeqCst));
assert_eq!(supervisor.len(), 0);
}
#[tokio::test]
async fn test_shutdown_with_timeout_completes_in_time() {
let supervisor = TaskSupervisor::new();
let _ = supervisor.spawn_with_token(|_token| async move {
sleep(Duration::from_millis(50)).await;
});
let result = supervisor
.shutdown_with_timeout(Duration::from_secs(1))
.await;
assert!(result.is_ok());
assert!(supervisor.is_cancelled());
assert!(supervisor.is_closed());
}
#[tokio::test]
async fn test_shutdown_with_timeout_times_out() {
let supervisor = TaskSupervisor::new();
let _ = supervisor.tracker().spawn(async {
sleep(Duration::from_secs(10)).await;
});
let result = supervisor
.shutdown_with_timeout(Duration::from_millis(50))
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cooperative_cancellation_in_loop() {
let supervisor = TaskSupervisor::new();
let iterations = Arc::new(AtomicUsize::new(0));
let iterations_clone = iterations.clone();
let _ = supervisor.spawn_with_token(|token| async move {
loop {
if token.is_cancelled() {
break;
}
iterations_clone.fetch_add(1, Ordering::SeqCst);
sleep(Duration::from_millis(10)).await;
}
});
sleep(Duration::from_millis(55)).await;
supervisor.cancel();
sleep(Duration::from_millis(50)).await;
let count = iterations.load(Ordering::SeqCst);
assert!(count >= 3 && count < 20);
}
#[tokio::test]
async fn test_spawn_with_cancel_completes() {
let supervisor = TaskSupervisor::new();
let handle = supervisor.spawn_with_cancel(|| async move {
sleep(Duration::from_millis(30)).await;
42
});
match handle.await.unwrap() {
CancelOutcome::Completed(value) => assert_eq!(value, 42),
CancelOutcome::Cancelled => panic!("task should have completed"),
}
}
#[tokio::test]
async fn test_spawn_with_cancel_reports_cancellation() {
let supervisor = TaskSupervisor::new();
let handle = supervisor.spawn_with_cancel(|| async move {
loop {
sleep(Duration::from_millis(10)).await;
}
});
sleep(Duration::from_millis(35)).await;
supervisor.cancel();
match handle.await.unwrap() {
CancelOutcome::Completed(_) => panic!("task should have been cancelled"),
CancelOutcome::Cancelled => {}
}
}
#[tokio::test]
async fn test_deref_allows_direct_tracker_access() {
let supervisor = TaskSupervisor::new();
let handle = supervisor.spawn(async {
sleep(Duration::from_millis(50)).await;
42
});
assert_eq!(supervisor.len(), 1);
assert!(!supervisor.is_closed());
let result = handle.await.unwrap();
assert_eq!(result, 42);
}
}