use std::{fmt, sync::Arc};
use super::{
node::{Node, Slot},
wait::Cancelled,
};
#[derive(Clone)]
pub struct CancelToken {
node: Arc<Node>,
}
impl CancelToken {
pub fn cancel(&self) {
self.node.cancel();
}
#[must_use]
pub fn cancelled(&self) -> Cancelled<'_> {
Cancelled::new(&self.node)
}
#[must_use]
pub fn child(&self) -> Self {
Self {
node: Node::child(&self.node),
}
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.node.is_cancelled()
}
#[must_use]
pub fn never() -> Self {
Self { node: Node::root() }
}
pub fn on_cancel<F>(&self, waker: F) -> CancelWakerGuard
where
F: Fn() + Send + Sync + 'static,
{
let id = self.node.register(Slot::Sync(Arc::new(waker)));
CancelWakerGuard {
node: id.map(|id| (Arc::clone(&self.node), id)),
}
}
#[must_use]
pub fn root() -> Self {
Self { node: Node::root() }
}
}
impl fmt::Debug for CancelToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CancelToken")
.field("cancelled", &self.is_cancelled())
.finish()
}
}
#[must_use = "dropping the guard immediately unregisters the cancel waker"]
pub struct CancelWakerGuard {
node: Option<(Arc<Node>, u64)>,
}
impl Drop for CancelWakerGuard {
fn drop(&mut self) {
if let Some((node, id)) = &self.node {
node.unregister(*id);
}
}
}
impl fmt::Debug for CancelWakerGuard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CancelWakerGuard")
.field("registered", &self.node.is_some())
.finish()
}
}
#[cfg(test)]
mod tests {
use std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use kithara_test_utils::kithara;
use tokio::{spawn, task, time as tokio_time};
use super::CancelToken;
#[kithara::test(timeout(Duration::from_secs(5)))]
fn fresh_token_not_cancelled() {
let c = CancelToken::root();
assert!(!c.is_cancelled());
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn cancel_sets_lock_free_flag() {
let c = CancelToken::root();
c.cancel();
assert!(c.is_cancelled());
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn child_cancel_does_not_cancel_parent() {
let parent = CancelToken::root();
let child = parent.child();
child.cancel();
assert!(child.is_cancelled());
assert!(
!parent.is_cancelled(),
"child cancel must not cancel the parent"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn child_cancel_does_not_cancel_sibling() {
let parent = CancelToken::root();
let a = parent.child();
let b = parent.child();
a.cancel();
assert!(a.is_cancelled());
assert!(!b.is_cancelled(), "sibling cancel must stay independent");
assert!(!parent.is_cancelled());
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn clone_shares_cancel_identity() {
let token = CancelToken::never();
let twin = token.clone();
twin.cancel();
assert!(
token.is_cancelled(),
"clone shares identity with the original"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn child_observes_own_cancel() {
let parent = CancelToken::root();
let child = parent.child();
child.cancel();
assert!(child.is_cancelled());
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn child_observes_parent_cancel_lock_free() {
let master = CancelToken::root();
let worker = master.child();
assert!(!worker.is_cancelled());
std::thread::scope(|s| {
s.spawn(|| master.cancel());
});
assert!(
worker.is_cancelled(),
"worker child must observe master cancel via propagate-down"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn grandchild_observes_master_cancel_lock_free() {
let master = CancelToken::root();
let mid = master.child();
let leaf = mid.child();
master.cancel();
assert!(mid.is_cancelled());
assert!(leaf.is_cancelled());
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn root_cancel_reaches_grandchild_after_intermediate_dropped() {
let master = CancelToken::root();
let leaf = {
let mid = master.child();
mid.child()
};
assert!(!leaf.is_cancelled());
master.cancel();
assert!(
leaf.is_cancelled(),
"root cancel must reach the grandchild after the intermediate token dropped"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn repeat_cancel_is_idempotent() {
let c = CancelToken::root();
c.cancel();
c.cancel();
assert!(c.is_cancelled());
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn late_child_born_cancelled() {
let master = CancelToken::root();
master.cancel();
let child = master.child();
assert!(
child.is_cancelled(),
"a child born after the parent cancelled must be born cancelled"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn child_churn_then_cancel_reaches_survivors() {
let master = CancelToken::root();
for _ in 0..1000 {
let _ = master.child();
}
let survivor = master.child();
let _second = master.child();
master.cancel();
assert!(survivor.is_cancelled());
}
#[kithara::test(tokio, timeout(Duration::from_secs(5)))]
async fn async_cancelled_resolves_on_self_cancel() {
let c = CancelToken::never();
let c2 = c.clone();
let handle = spawn(async move {
c2.cancelled().await;
c2.is_cancelled()
});
task::yield_now().await;
c.cancel();
let flag = tokio_time::timeout(Duration::from_secs(2), handle)
.await
.expect("cancelled() must resolve within the test timeout")
.expect("spawned cancellation task must not panic");
assert!(flag, "flag must be visible once cancelled() resolves");
}
fn counter() -> (Arc<AtomicUsize>, impl Fn() + Send + Sync + 'static) {
let n = Arc::new(AtomicUsize::new(0));
let n2 = Arc::clone(&n);
(n, move || {
n2.fetch_add(1, Ordering::SeqCst);
})
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn on_cancel_fires_on_self_cancel() {
let c = CancelToken::root();
let child = c.child();
let (fired, waker) = counter();
let _guard = child.on_cancel(waker);
assert_eq!(fired.load(Ordering::SeqCst), 0);
child.cancel();
assert_eq!(fired.load(Ordering::SeqCst), 1, "waker must fire on cancel");
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn on_cancel_fires_on_ancestor_cancel() {
let master = CancelToken::root();
let child = master.child();
let (fired, waker) = counter();
let _guard = child.on_cancel(waker);
master.cancel();
assert!(
fired.load(Ordering::SeqCst) >= 1,
"ancestor cancel must wake a descendant's sync waker"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn on_cancel_already_cancelled_fires_immediately() {
let c = CancelToken::never();
c.cancel();
let (fired, waker) = counter();
let _guard = c.on_cancel(waker);
assert_eq!(
fired.load(Ordering::SeqCst),
1,
"registering on an already-cancelled token fires at once"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn on_cancel_guard_drop_unregisters() {
let c = CancelToken::never();
let (fired, waker) = counter();
let guard = c.on_cancel(waker);
drop(guard);
c.cancel();
assert_eq!(
fired.load(Ordering::SeqCst),
0,
"a dropped guard must not fire on a later cancel"
);
}
#[kithara::test(timeout(Duration::from_secs(5)))]
fn on_cancel_sibling_cancel_does_not_fire() {
let parent = CancelToken::root();
let a = parent.child();
let b = parent.child();
let (fired, waker) = counter();
let _guard = a.on_cancel(waker);
b.cancel();
assert_eq!(
fired.load(Ordering::SeqCst),
0,
"a sibling cancel must not fire this token's waker"
);
assert!(!a.is_cancelled());
}
}