use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
#[derive(Clone, Debug, Default)]
pub struct CancellationToken(Arc<AtomicBool>);
impl CancellationToken {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CancellationCause {
Cancelled,
Timeout {
elapsed: Duration,
},
}
#[derive(Clone, Copy, Debug)]
pub struct CancellationChecker<'a> {
token: Option<&'a CancellationToken>,
deadline: Option<Instant>,
}
impl<'a> CancellationChecker<'a> {
#[must_use]
pub const fn new(token: Option<&'a CancellationToken>, deadline: Option<Instant>) -> Self {
Self { token, deadline }
}
#[must_use]
pub const fn disabled() -> Self {
Self {
token: None,
deadline: None,
}
}
#[must_use]
#[inline(always)]
pub const fn is_disabled(&self) -> bool {
self.token.is_none() && self.deadline.is_none()
}
#[inline]
pub fn check(&self) -> Result<(), CancellationCause> {
if self.token.is_some_and(CancellationToken::is_cancelled) {
return Err(CancellationCause::Cancelled);
}
if let Some(deadline) = self.deadline {
let now = Instant::now();
if now >= deadline {
return Err(CancellationCause::Timeout {
elapsed: now.duration_since(deadline),
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_checker_never_trips() {
let checker = CancellationChecker::disabled();
assert!(checker.is_disabled());
assert_eq!(checker.check(), Ok(()));
}
#[test]
fn checker_with_token_is_not_disabled() {
let token = CancellationToken::new();
let checker = CancellationChecker::new(Some(&token), None);
assert!(!checker.is_disabled());
}
#[test]
fn checker_with_deadline_is_not_disabled() {
let deadline = Instant::now();
let checker = CancellationChecker::new(None, Some(deadline));
assert!(!checker.is_disabled());
}
#[test]
fn token_wins_over_deadline_when_both_tripped() {
let token = CancellationToken::new();
token.cancel();
let elapsed_deadline = Instant::now() - Duration::from_secs(1);
let checker = CancellationChecker::new(Some(&token), Some(elapsed_deadline));
assert_eq!(checker.check(), Err(CancellationCause::Cancelled));
}
#[test]
fn deadline_reported_when_only_deadline_tripped() {
let elapsed_deadline = Instant::now() - Duration::from_secs(1);
let checker = CancellationChecker::new(None, Some(elapsed_deadline));
assert!(matches!(
checker.check(),
Err(CancellationCause::Timeout { .. })
));
}
#[test]
fn live_token_with_future_deadline_passes() {
let token = CancellationToken::new();
let future_deadline = Instant::now() + Duration::from_secs(3600);
let checker = CancellationChecker::new(Some(&token), Some(future_deadline));
assert_eq!(checker.check(), Ok(()));
}
}