use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Notify;
#[derive(Debug, Default)]
pub struct StickyCancel {
flag: AtomicBool,
notify: Notify,
}
impl StickyCancel {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.flag.store(true, Ordering::SeqCst);
self.notify.notify_waiters();
}
pub fn is_cancelled(&self) -> bool {
self.flag.load(Ordering::SeqCst)
}
pub async fn cancelled(&self) {
loop {
let notified = self.notify.notified();
tokio::pin!(notified);
if self.flag.load(Ordering::SeqCst) {
return;
}
notified.await;
if self.flag.load(Ordering::SeqCst) {
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[tokio::test]
async fn cancel_before_waiter_is_sticky() {
let c = Arc::new(StickyCancel::new());
c.cancel();
tokio::time::timeout(std::time::Duration::from_millis(50), c.cancelled())
.await
.expect("sticky cancel must resolve without a prior waiter");
}
#[tokio::test]
async fn cancel_wakes_existing_waiter() {
let c = Arc::new(StickyCancel::new());
let c2 = Arc::clone(&c);
let join = tokio::spawn(async move {
c2.cancelled().await;
});
tokio::task::yield_now().await;
c.cancel();
tokio::time::timeout(std::time::Duration::from_millis(50), join)
.await
.expect("waiter joined")
.expect("task ok");
}
}