use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Notify;
tokio::task_local! {
static CURRENT: CancelHandle;
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CancelHandle {
cancelled: Arc<AtomicBool>,
notify: Arc<Notify>,
}
impl CancelHandle {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
self.notify.notify_waiters();
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
pub async fn cancelled(&self) {
if self.is_cancelled() {
return;
}
loop {
let notified = self.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.is_cancelled() {
return;
}
notified.await;
if self.is_cancelled() {
return;
}
}
}
}
pub(crate) async fn scope<F, T>(cancel: CancelHandle, fut: F) -> T
where
F: Future<Output = T>,
{
CURRENT.scope(cancel, fut).await
}
pub(crate) fn current() -> Option<CancelHandle> {
CURRENT.try_with(Clone::clone).ok()
}
pub(crate) async fn wait_cancelled() {
match CURRENT.try_with(Clone::clone) {
Ok(handle) => handle.cancelled().await,
Err(_) => std::future::pending::<()>().await,
}
}
pub(crate) fn is_cancelled() -> bool {
CURRENT
.try_with(CancelHandle::is_cancelled)
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::sync::oneshot;
const fn _assert_auto_traits() {
const fn assert_send_sync_static<T: Send + Sync + 'static>() {}
assert_send_sync_static::<CancelHandle>();
}
#[test]
fn cancel_handle_public_construction_surface() {
let a = CancelHandle::new();
let b = CancelHandle::default();
let c = a.clone();
assert!(!a.is_cancelled() && !b.is_cancelled() && !c.is_cancelled());
a.cancel();
assert!(
a.is_cancelled() && c.is_cancelled(),
"clones share the flag"
);
}
#[tokio::test]
async fn pre_cancelled_wait_returns_immediately() {
let handle = CancelHandle::new();
handle.cancel();
tokio::time::timeout(Duration::from_secs(1), handle.cancelled())
.await
.expect("a pre-cancelled handle resolves immediately");
}
#[tokio::test]
async fn repeated_cancel_is_idempotent() {
let handle = CancelHandle::new();
handle.cancel();
handle.cancel();
assert!(handle.is_cancelled());
tokio::time::timeout(Duration::from_secs(1), handle.cancelled())
.await
.expect("idempotent cancel keeps the handle resolved");
}
#[tokio::test]
async fn cancel_wakes_waiter() {
let handle = CancelHandle::new();
let waiter = handle.clone();
let (ready_tx, ready_rx) = oneshot::channel();
let join = tokio::spawn(async move {
let _ = ready_tx.send(());
waiter.cancelled().await;
});
ready_rx.await.expect("waiter signals readiness");
assert!(!handle.is_cancelled());
handle.cancel();
tokio::time::timeout(Duration::from_secs(1), join)
.await
.expect("waiter must finish after cancel")
.expect("join ok");
assert!(handle.is_cancelled());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn multiple_waiters_all_wake_on_a_single_cancel() {
let handle = CancelHandle::new();
let mut joins = Vec::new();
for _ in 0..8 {
let waiter = handle.clone();
joins.push(tokio::spawn(async move { waiter.cancelled().await }));
}
handle.cancel();
for join in joins {
tokio::time::timeout(Duration::from_secs(1), join)
.await
.expect("every waiter must wake on one cancel")
.expect("join ok");
}
}
#[tokio::test]
async fn dropping_a_pending_wait_does_not_panic_or_affect_clones() {
let handle = CancelHandle::new();
{
let waiter = handle.clone();
let fut = waiter.cancelled();
drop(fut); }
assert!(!handle.is_cancelled(), "dropping a waiter changes no state");
handle.cancel();
assert!(handle.is_cancelled());
}
#[tokio::test]
async fn a_cloned_handle_propagates_cancel_across_a_spawn_boundary() {
let parent = CancelHandle::new();
let child = parent.clone();
let (ready_tx, ready_rx) = oneshot::channel();
let join = tokio::spawn(async move {
let _ = ready_tx.send(());
child.cancelled().await;
});
ready_rx.await.expect("child signals readiness");
parent.cancel();
tokio::time::timeout(Duration::from_secs(1), join)
.await
.expect("a spawned clone must observe the parent's cancel")
.expect("join ok");
}
#[tokio::test]
async fn current_reports_absent_and_present_context() {
assert!(current().is_none(), "no scope installed => no handle");
let handle = CancelHandle::new();
let probe = handle.clone();
scope(handle, async {
let got = current().expect("an installed scope exposes its handle");
assert!(!got.is_cancelled());
probe.cancel();
assert!(
current().expect("still present").is_cancelled(),
"the exposed handle reflects cancellation"
);
})
.await;
assert!(
current().is_none(),
"the handle is gone after the scope exits"
);
}
#[tokio::test]
async fn missing_scope_wait_stays_pending() {
let elapsed = tokio::time::timeout(Duration::from_millis(50), wait_cancelled()).await;
assert!(
elapsed.is_err(),
"wait_cancelled must stay pending without an installed scope"
);
assert!(
!is_cancelled(),
"is_cancelled is false with no installed scope"
);
}
#[tokio::test]
async fn nested_scopes_use_the_innermost_handle() {
let outer = CancelHandle::new();
let inner = CancelHandle::new();
let inner_probe = inner.clone();
scope(outer, async move {
scope(inner, async {
assert!(!is_cancelled());
inner_probe.cancel();
assert!(is_cancelled(), "the innermost scope's handle is observed");
wait_cancelled().await;
})
.await;
})
.await;
}
#[tokio::test]
async fn cancel_between_check_and_wait_is_not_lost() {
let handle = CancelHandle::new();
let notified = handle.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
handle.cancel();
tokio::time::timeout(Duration::from_secs(1), notified)
.await
.expect("an enabled waiter must observe a cancel signaled before it awaited");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_cancel_never_hangs_a_waiter() {
for _ in 0..200 {
let handle = CancelHandle::new();
let waiter = handle.clone();
let join = tokio::spawn(async move { waiter.cancelled().await });
handle.cancel();
tokio::time::timeout(Duration::from_secs(1), join)
.await
.expect("a waiter racing cancel must never hang")
.expect("join ok");
}
}
#[tokio::test]
async fn scope_exposes_handle_to_wait_cancelled() {
let handle = CancelHandle::new();
let cancel = handle.clone();
let (ready_tx, ready_rx) = oneshot::channel();
let done = tokio::spawn(async move {
scope(handle, async {
let _ = ready_tx.send(());
wait_cancelled().await;
})
.await;
});
ready_rx.await.expect("scoped task signals readiness");
cancel.cancel();
tokio::time::timeout(Duration::from_secs(1), done)
.await
.expect("scoped wait must finish")
.expect("join ok");
}
}