use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Clone, Debug, Default)]
pub struct ScanCancel(Arc<AtomicBool>);
impl ScanCancel {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.0.store(true, Ordering::Relaxed);
}
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
pub fn check(&self) -> crate::Result<()> {
if self.is_cancelled() {
Err(crate::Error::Cancelled)
} else {
Ok(())
}
}
pub async fn checkpoint(&self, tick: usize) -> crate::Result<()> {
if tick % YIELD_STRIDE == 0 {
self.check()?;
tokio::task::yield_now().await;
}
Ok(())
}
pub async fn checkpoint_polled(&self, tick: usize) -> crate::Result<()> {
self.check()?;
if tick % YIELD_STRIDE == 0 {
tokio::task::yield_now().await;
}
Ok(())
}
pub async fn checkpoint_now(&self) -> crate::Result<()> {
self.check()?;
tokio::task::yield_now().await;
Ok(())
}
}
pub const YIELD_STRIDE: usize = 256;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_uncancelled_and_cancels() {
let c = ScanCancel::new();
assert!(!c.is_cancelled());
assert!(c.check().is_ok());
c.cancel();
assert!(c.is_cancelled());
assert!(matches!(c.check(), Err(crate::Error::Cancelled)));
}
#[test]
fn clones_share_state() {
let a = ScanCancel::new();
let b = a.clone();
a.cancel();
assert!(b.is_cancelled(), "cancel on one clone is seen by another");
}
#[tokio::test]
async fn checkpoint_checks_and_yields_at_the_stride_only() {
let c = ScanCancel::new();
assert!(c.checkpoint(0).await.is_ok());
assert!(c.checkpoint(1).await.is_ok());
assert!(c.checkpoint_now().await.is_ok());
c.cancel();
for tick in [0usize, YIELD_STRIDE] {
assert!(matches!(
c.checkpoint(tick).await,
Err(crate::Error::Cancelled)
));
}
for tick in [1usize, YIELD_STRIDE + 1] {
assert!(
c.checkpoint(tick).await.is_ok(),
"an off-stride checkpoint must be a no-op even once cancelled"
);
}
assert!(matches!(
c.checkpoint_now().await,
Err(crate::Error::Cancelled)
));
}
#[tokio::test]
async fn checkpoint_makes_a_scan_loop_interruptible_by_timeout() {
let c = ScanCancel::new();
let out = tokio::time::timeout(std::time::Duration::from_millis(1), async {
for tick in 0..usize::MAX {
c.checkpoint(tick).await?;
}
Ok::<(), crate::Error>(())
})
.await;
assert!(
out.is_err(),
"a loop whose only yield point is the checkpoint MUST be interruptible by an enclosing timeout"
);
}
#[test]
fn default_is_never_cancelled() {
let c = ScanCancel::default();
assert!(!c.is_cancelled());
assert!(c.check().is_ok());
}
}