use core::fmt;
use core::pin::Pin;
use core::task::{Context, Poll, Waker};
use std::future::Future;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AbortReason {
Cancelled(Option<CancelReason>),
Expired,
SessionClosed,
}
impl AbortReason {
#[must_use]
pub const fn is_peer_requested(&self) -> bool {
matches!(self, Self::Cancelled(_))
}
}
impl fmt::Display for AbortReason {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cancelled(Some(reason)) => write!(formatter, "cancelled: {reason}"),
Self::Cancelled(None) => formatter.write_str("cancelled"),
Self::Expired => formatter.write_str("expired"),
Self::SessionClosed => formatter.write_str("session closed"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CancelReason(String);
impl CancelReason {
#[must_use]
pub fn new(reason: impl Into<String>) -> Self {
Self(reason.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CancelReason {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Debug, Default)]
struct AbortState {
reason: OnceLock<AbortReason>,
waiters: Mutex<Vec<Waker>>,
}
impl AbortState {
fn waiters(&self) -> MutexGuard<'_, Vec<Waker>> {
self.waiters.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[derive(Debug, Clone, Default)]
pub struct CancellationToken {
state: Arc<AbortState>,
}
impl CancellationToken {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self, reason: AbortReason) -> bool {
let first = self.state.reason.set(reason).is_ok();
if first {
let mut waiters = self.state.waiters();
for waiter in waiters.drain(..) {
waiter.wake();
}
}
first
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.state.reason.get().is_some()
}
#[must_use]
pub fn reason(&self) -> Option<&AbortReason> {
self.state.reason.get()
}
#[must_use]
pub const fn cancelled(&self) -> Cancelled<'_> {
Cancelled { token: self }
}
}
#[derive(Debug)]
pub struct Cancelled<'a> {
token: &'a CancellationToken,
}
impl Future for Cancelled<'_> {
type Output = ();
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
if self.token.is_cancelled() {
return Poll::Ready(());
}
let mut waiters = self.token.state.waiters();
if self.token.is_cancelled() {
return Poll::Ready(());
}
if !waiters.iter().any(|w| w.will_wake(context.waker())) {
waiters.push(context.waker().clone());
}
Poll::Pending
}
}
#[cfg(test)]
mod tests {
use core::future::Future;
use core::pin::Pin;
use core::sync::atomic::{AtomicUsize, Ordering};
use core::task::{Context, Poll};
use std::sync::Arc;
use std::task::{Wake, Waker};
use super::{AbortReason, CancelReason, CancellationToken};
struct Counting {
wakes: AtomicUsize,
}
impl Counting {
fn new() -> Arc<Self> {
Arc::new(Self {
wakes: AtomicUsize::new(0),
})
}
fn wakes(&self) -> usize {
self.wakes.load(Ordering::Relaxed)
}
}
impl Wake for Counting {
fn wake(self: Arc<Self>) {
self.wakes.fetch_add(1, Ordering::Relaxed);
}
fn wake_by_ref(self: &Arc<Self>) {
self.wakes.fetch_add(1, Ordering::Relaxed);
}
}
fn poll_once<F: Future>(future: Pin<&mut F>, waker: &Arc<Counting>) -> Poll<F::Output> {
let waker = Waker::from(waker.clone());
let mut context = Context::from_waker(&waker);
future.poll(&mut context)
}
#[test]
fn an_uncancelled_token_leaves_a_waiter_pending_and_does_not_wake_it() {
let token = CancellationToken::new();
let counter = Counting::new();
let mut signal = Box::pin(token.cancelled());
assert_eq!(poll_once(signal.as_mut(), &counter), Poll::Pending);
assert_eq!(counter.wakes(), 0, "nothing happened, so nothing to wake");
assert!(!token.is_cancelled());
}
#[test]
fn aborting_wakes_exactly_the_waiters_that_registered() {
let token = CancellationToken::new();
let counter = Counting::new();
let mut signal = Box::pin(token.cancelled());
assert_eq!(poll_once(signal.as_mut(), &counter), Poll::Pending);
assert!(token.cancel(AbortReason::Cancelled(Some(CancelReason::new("stop")))));
assert_eq!(counter.wakes(), 1, "the registered waiter was woken");
assert_eq!(poll_once(signal.as_mut(), &counter), Poll::Ready(()));
}
#[test]
fn a_token_cancelled_before_anything_waits_still_reads_as_cancelled() {
let token = CancellationToken::new();
let counter = Counting::new();
assert!(token.cancel(AbortReason::Expired));
assert_eq!(counter.wakes(), 0);
let mut signal = Box::pin(token.cancelled());
assert_eq!(poll_once(signal.as_mut(), &counter), Poll::Ready(()));
}
#[test]
fn a_second_abort_is_reported_as_a_no_op_and_keeps_the_first_reason() {
let token = CancellationToken::new();
assert!(token.cancel(AbortReason::Cancelled(Some(CancelReason::new("first")))));
assert!(
!token.cancel(AbortReason::Expired),
"the first reason is the one that happened"
);
let reason = token.reason();
assert!(
matches!(reason, Some(AbortReason::Cancelled(Some(inner))) if inner.as_str() == "first"),
"the first reason is the one that stuck, got {reason:?}"
);
}
#[test]
fn only_a_peer_cancellation_counts_as_peer_requested() {
assert!(AbortReason::Cancelled(None).is_peer_requested());
assert!(AbortReason::Cancelled(Some(CancelReason::new("why"))).is_peer_requested());
assert!(!AbortReason::Expired.is_peer_requested());
assert!(!AbortReason::SessionClosed.is_peer_requested());
}
}