use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct Cancel {
stopped: Arc<AtomicBool>,
started: Instant,
limit: Option<Duration>,
}
impl Default for Cancel {
fn default() -> Self {
Self::new()
}
}
impl Cancel {
#[must_use]
pub fn new() -> Self {
Self { stopped: Arc::new(AtomicBool::new(false)), started: Instant::now(), limit: None }
}
#[must_use]
pub fn after(timeout: Duration) -> Self {
Self {
stopped: Arc::new(AtomicBool::new(false)),
started: Instant::now(),
limit: Some(timeout),
}
}
#[must_use]
pub fn restart(&self, timeout: Option<Duration>) -> Self {
self.stopped.store(false, Ordering::Relaxed);
Self { stopped: Arc::clone(&self.stopped), started: Instant::now(), limit: timeout }
}
pub fn cancel(&self) {
self.stopped.store(true, Ordering::Relaxed);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.stopped.load(Ordering::Relaxed) || self.expired()
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.started.elapsed()
}
#[must_use]
pub fn limit(&self) -> Option<Duration> {
self.limit
}
pub fn check(&self) -> Result<()> {
if self.stopped.load(Ordering::Relaxed) {
return Err(Error::interrupt("Interrupted!"));
}
if self.expired() {
let limit = self.limit.unwrap_or_default();
return Err(Error::interrupt(format!(
"query took longer than the {} millisecond limit it was given",
limit.as_millis()
)));
}
Ok(())
}
fn expired(&self) -> bool {
self.limit.is_some_and(|limit| self.started.elapsed() >= limit)
}
}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::Duration;
use super::Cancel;
#[test]
fn a_fresh_token_lets_the_query_run() {
let cancel = Cancel::new();
assert!(!cancel.is_cancelled());
assert!(cancel.check().is_ok());
assert_eq!(cancel.limit(), None);
}
#[test]
fn cancelling_one_handle_stops_the_query_holding_another() {
let cancel = Cancel::new();
let other = cancel.clone();
other.cancel();
assert!(cancel.is_cancelled());
let error = cancel.check().expect_err("it was cancelled");
assert_eq!(error.code().duckdb_name(), "Interrupt Error");
assert_eq!(error.message(), "Interrupted!");
}
#[test]
fn a_token_another_thread_cancels_is_seen_by_the_one_running_the_query() {
let cancel = Cancel::new();
let other = cancel.clone();
let stopper = thread::spawn(move || other.cancel());
stopper.join().expect("the thread ran");
assert!(cancel.is_cancelled());
}
#[test]
fn a_time_limit_runs_out_on_its_own() {
let cancel = Cancel::after(Duration::from_millis(1));
assert_eq!(cancel.limit(), Some(Duration::from_millis(1)));
thread::sleep(Duration::from_millis(5));
assert!(cancel.is_cancelled());
let error = cancel.check().expect_err("the time is up");
assert_eq!(error.code().duckdb_name(), "Interrupt Error");
assert!(error.message().contains("longer than the 1 millisecond limit"), "{error}");
}
#[test]
fn a_timeout_and_an_interrupt_say_different_things() {
let interrupted = Cancel::new();
interrupted.cancel();
let timed_out = Cancel::after(Duration::from_millis(0));
assert_ne!(
interrupted.check().expect_err("cancelled").message(),
timed_out.check().expect_err("timed out").message()
);
}
#[test]
fn restarting_shares_the_flag_and_starts_the_clock_again() {
let connection = Cancel::new();
let statement = connection.restart(Some(Duration::from_secs(60)));
assert!(!statement.is_cancelled());
connection.cancel();
assert!(statement.is_cancelled(), "the flag is shared");
}
#[test]
fn an_interrupt_between_two_statements_does_not_stop_the_next_one() {
let connection = Cancel::new();
connection.cancel();
let statement = connection.restart(None);
assert!(!statement.is_cancelled());
assert!(!connection.is_cancelled(), "and the connection is usable again");
}
#[test]
fn a_limit_is_on_the_statement_rather_than_on_the_connection() {
let connection = Cancel::after(Duration::from_millis(1));
thread::sleep(Duration::from_millis(5));
assert!(connection.is_cancelled());
let statement = connection.restart(Some(Duration::from_secs(60)));
assert!(!statement.is_cancelled(), "the clock started again");
}
}