use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
use std::sync::Arc;
#[cfg(test)]
use std::time::Duration;
use std::time::Instant;
use crate::result::QueryError;
const STATE_RUNNING: u8 = 0;
const STATE_TIMEOUT: u8 = 1;
const STATE_DISCONNECT: u8 = 2;
static ACTIVE_INSTALLS: AtomicUsize = AtomicUsize::new(0);
#[inline]
pub(crate) fn has_active_install() -> bool {
ACTIVE_INSTALLS.load(Ordering::Relaxed) != 0
}
const CHECK_INTERVAL_MASK: u32 = 0xFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CancelReason {
Timeout,
Disconnect,
}
#[derive(Debug)]
pub struct ExecCancel {
state: AtomicU8,
deadline: Option<Instant>,
timeout_ms: u64,
#[cfg(test)]
checkpoints: AtomicUsize,
#[cfg(test)]
block_at_checkpoint: AtomicUsize,
}
impl ExecCancel {
pub fn new() -> Self {
Self {
state: AtomicU8::new(STATE_RUNNING),
deadline: None,
timeout_ms: 0,
#[cfg(test)]
checkpoints: AtomicUsize::new(0),
#[cfg(test)]
block_at_checkpoint: AtomicUsize::new(0),
}
}
pub fn with_deadline(deadline: Instant, timeout_ms: u64) -> Self {
Self {
state: AtomicU8::new(STATE_RUNNING),
deadline: Some(deadline),
timeout_ms,
#[cfg(test)]
checkpoints: AtomicUsize::new(0),
#[cfg(test)]
block_at_checkpoint: AtomicUsize::new(0),
}
}
pub fn cancel(&self, reason: CancelReason) {
let want = match reason {
CancelReason::Timeout => STATE_TIMEOUT,
CancelReason::Disconnect => STATE_DISCONNECT,
};
let _ =
self.state
.compare_exchange(STATE_RUNNING, want, Ordering::Relaxed, Ordering::Relaxed);
}
pub fn is_cancelled(&self) -> bool {
self.reason().is_some()
}
pub fn reason(&self) -> Option<CancelReason> {
match self.state.load(Ordering::Relaxed) {
STATE_TIMEOUT => return Some(CancelReason::Timeout),
STATE_DISCONNECT => return Some(CancelReason::Disconnect),
_ => {}
}
if let Some(deadline) = self.deadline {
if Instant::now() >= deadline {
let _ = self.state.compare_exchange(
STATE_RUNNING,
STATE_TIMEOUT,
Ordering::Relaxed,
Ordering::Relaxed,
);
return match self.state.load(Ordering::Relaxed) {
STATE_DISCONNECT => Some(CancelReason::Disconnect),
_ => Some(CancelReason::Timeout),
};
}
}
None
}
fn error(&self) -> Option<QueryError> {
#[cfg(test)]
{
let count = self.checkpoints.fetch_add(1, Ordering::Relaxed) + 1;
let target = self.block_at_checkpoint.load(Ordering::Relaxed);
if target != 0 && count >= target && self.reason().is_none() {
let give_up = Instant::now() + Duration::from_secs(3);
while self.reason().is_none() && Instant::now() < give_up {
std::thread::yield_now();
}
}
}
match self.reason()? {
CancelReason::Timeout => Some(QueryError::Timeout {
timeout_ms: self.timeout_ms,
}),
CancelReason::Disconnect => Some(QueryError::Cancelled),
}
}
#[cfg(test)]
pub(crate) fn checkpoint_count(&self) -> usize {
self.checkpoints.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn block_at_checkpoint(&self, target: usize) {
self.block_at_checkpoint.store(target, Ordering::Relaxed);
}
}
impl Default for ExecCancel {
fn default() -> Self {
Self::new()
}
}
thread_local! {
static CURRENT: RefCell<Option<Arc<ExecCancel>>> = const { RefCell::new(None) };
}
#[must_use = "the guard must be held for the duration of the query"]
pub fn install(token: Arc<ExecCancel>) -> InstallGuard {
let prev = CURRENT.with(|c| c.borrow_mut().replace(token));
ACTIVE_INSTALLS.fetch_add(1, Ordering::Relaxed);
InstallGuard {
prev,
_thread_bound: PhantomData,
}
}
pub struct InstallGuard {
prev: Option<Arc<ExecCancel>>,
_thread_bound: PhantomData<Rc<()>>,
}
impl Drop for InstallGuard {
fn drop(&mut self) {
CURRENT.with(|c| *c.borrow_mut() = self.prev.take());
ACTIVE_INSTALLS.fetch_sub(1, Ordering::Relaxed);
}
}
#[inline]
pub fn check() -> Result<(), QueryError> {
if !has_active_install() {
return Ok(());
}
CURRENT.with(|c| match c.borrow().as_ref() {
Some(token) => match token.error() {
Some(err) => Err(err),
None => Ok(()),
},
None => Ok(()),
})
}
#[derive(Debug)]
pub struct CancelCheck {
n: u32,
}
impl CancelCheck {
#[inline]
pub fn new() -> Self {
Self { n: 0 }
}
#[inline]
pub fn tick(&mut self) -> Result<(), QueryError> {
self.n = self.n.wrapping_add(1);
if self.n & CHECK_INTERVAL_MASK == 0 {
check()
} else {
Ok(())
}
}
}
impl Default for CancelCheck {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn no_token_installed_never_cancels() {
let mut cc = CancelCheck::new();
for _ in 0..100_000 {
cc.tick().expect("no token means never cancelled");
}
assert!(check().is_ok());
}
#[test]
fn explicit_cancel_trips_check() {
let token = Arc::new(ExecCancel::new());
let _guard = install(Arc::clone(&token));
assert!(check().is_ok());
token.cancel(CancelReason::Disconnect);
match check() {
Err(QueryError::Cancelled) => {}
other => panic!("expected Cancelled, got {other:?}"),
}
}
#[test]
fn passed_deadline_reports_timeout() {
let token = Arc::new(ExecCancel::with_deadline(
Instant::now() - Duration::from_millis(1),
2000,
));
let _guard = install(Arc::clone(&token));
match check() {
Err(QueryError::Timeout { timeout_ms }) => assert_eq!(timeout_ms, 2000),
other => panic!("expected Timeout, got {other:?}"),
}
token.cancel(CancelReason::Disconnect);
assert_eq!(
token.reason(),
Some(CancelReason::Timeout),
"an observed timeout must remain the stable cancellation reason"
);
}
#[test]
fn explicit_timeout_beats_disconnect_ordering() {
let token = ExecCancel::new();
token.cancel(CancelReason::Timeout);
token.cancel(CancelReason::Disconnect);
assert_eq!(token.reason(), Some(CancelReason::Timeout));
}
#[test]
fn guard_restores_previous_token_on_drop() {
let outer = Arc::new(ExecCancel::new());
let guard_outer = install(Arc::clone(&outer));
{
let inner = Arc::new(ExecCancel::new());
let _guard_inner = install(Arc::clone(&inner));
inner.cancel(CancelReason::Disconnect);
assert!(check().is_err(), "inner token active");
}
assert!(check().is_ok(), "outer token restored and running");
outer.cancel(CancelReason::Timeout);
assert!(check().is_err());
drop(guard_outer);
assert!(check().is_ok(), "no token after outermost guard drops");
}
#[test]
fn tick_checks_at_interval_boundary() {
let token = Arc::new(ExecCancel::new());
let _guard = install(Arc::clone(&token));
token.cancel(CancelReason::Timeout);
let mut cc = CancelCheck::new();
for _ in 0..CHECK_INTERVAL_MASK {
cc.tick().expect("pre-boundary ticks skip the real check");
}
assert!(cc.tick().is_err(), "the 4096th tick runs the real check");
}
}