use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
#[derive(Clone, Debug, Default)]
pub struct CancellationToken(Arc<AtomicBool>);
impl CancellationToken {
pub fn cancel(&self) {
self.0.store(true, Ordering::Relaxed);
}
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ScanAborted {
#[error("scan cancelled")]
Cancelled,
#[error("scan deadline exceeded")]
TimedOut,
}
#[derive(Clone, Debug, Default)]
pub struct ScanControl {
deadline: Option<Instant>,
cancellation: Option<CancellationToken>,
#[cfg(test)]
observer: Option<CheckObserver>,
}
#[cfg(test)]
#[derive(Clone)]
struct CheckObserver(Arc<CheckCallback>);
#[cfg(test)]
type CheckCallback =
dyn Fn(&'static std::panic::Location<'static>) -> Result<(), ScanAborted> + Send + Sync;
#[cfg(test)]
impl std::fmt::Debug for CheckObserver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("CheckObserver")
}
}
impl ScanControl {
pub fn with_deadline(mut self, deadline: Instant) -> Self {
self.deadline = Some(deadline);
self
}
pub fn with_timeout(self, timeout: Duration) -> anyhow::Result<Self> {
let deadline = Instant::now()
.checked_add(timeout)
.ok_or_else(|| anyhow::anyhow!("scan timeout is too large"))?;
Ok(self.with_deadline(deadline))
}
pub fn with_cancellation(mut self, token: CancellationToken) -> Self {
self.cancellation = Some(token);
self
}
pub(crate) fn is_limited(&self) -> bool {
#[cfg(test)]
if self.observer.is_some() {
return true;
}
self.deadline.is_some() || self.cancellation.is_some()
}
#[cfg(test)]
pub(crate) fn with_check_observer(
mut self,
observer: impl Fn(&'static std::panic::Location<'static>) -> Result<(), ScanAborted>
+ Send
+ Sync
+ 'static,
) -> Self {
self.observer = Some(CheckObserver(Arc::new(observer)));
self
}
#[inline]
#[cfg_attr(test, track_caller)]
pub fn check(&self) -> Result<(), ScanAborted> {
#[cfg(test)]
if let Some(observer) = &self.observer {
(observer.0)(std::panic::Location::caller())?;
}
if self.cancellation.as_ref().is_some_and(CancellationToken::is_cancelled) {
return Err(ScanAborted::Cancelled);
}
if self.deadline.is_some_and(|deadline| Instant::now() >= deadline) {
return Err(ScanAborted::TimedOut);
}
Ok(())
}
}