Skip to main content

rudb_common/
cancel.rs

1//! Stopping a query that is already running.
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::{Duration, Instant};
6
7use crate::error::{Error, Result};
8
9/// A reason for a running query to stop, which is either somebody asking or a clock running out.
10///
11/// Cheap to clone, and a clone shares the flag with the token it came from, so one thread can stop
12/// a query another thread is running. The deadline is not shared, because a deadline belongs to one
13/// statement and the flag belongs to whoever is allowed to interrupt: see [`Cancel::restart`].
14///
15/// # How a query notices
16///
17/// [`Cancel::check`] between chunks, and nowhere else, which is what
18/// `spec/engine/10-scheduler.md` section 10.9 asks for. That is a real limit and it is the one worth
19/// having: an operator cannot notice halfway through a chunk without a branch in the inner loop over
20/// values, and a thousand rows of arithmetic is microseconds, so the response time this gives is
21/// already below anything a person can see. What it does not cover is an operator that spends an
22/// unbounded time inside one call without pulling a chunk from below it, and there is none, because
23/// every loop in `rudb-exec` that can run long runs by pulling chunks.
24///
25/// # Why the flag is read relaxed
26///
27/// The only thing a reader does with it is stop, and nothing it reads afterwards has to have been
28/// written before the flag was set. A relaxed load on one location is still coherent, so a query
29/// that is running when the flag is set sees it on the next chunk or the one after, and a query that
30/// has already finished does not care. An acquire load here would cost a fence per chunk to order
31/// writes that do not exist.
32#[derive(Debug, Clone)]
33pub struct Cancel {
34    stopped: Arc<AtomicBool>,
35    started: Instant,
36    limit: Option<Duration>,
37}
38
39impl Default for Cancel {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl Cancel {
46    /// A token nothing stops on its own, for a query with no time limit on it.
47    #[must_use]
48    pub fn new() -> Self {
49        Self { stopped: Arc::new(AtomicBool::new(false)), started: Instant::now(), limit: None }
50    }
51
52    /// A token that stops itself after this long.
53    #[must_use]
54    pub fn after(timeout: Duration) -> Self {
55        Self {
56            stopped: Arc::new(AtomicBool::new(false)),
57            started: Instant::now(),
58            limit: Some(timeout),
59        }
60    }
61
62    /// The same flag, a fresh clock, and this time limit.
63    ///
64    /// What a connection does at the top of each statement. The flag is shared, so a caller holding
65    /// the connection's token can still stop the new statement, and the clock starts now, so a
66    /// timeout is a limit on this statement rather than on the connection's whole life.
67    ///
68    /// It clears the flag as well, which means an interrupt that arrives between two statements is
69    /// dropped rather than killing the next one. That is the same thing DuckDB does and it is the
70    /// right way round: an interrupt is about the query the person is watching, and carrying one
71    /// forward would stop a statement nobody asked to stop.
72    #[must_use]
73    pub fn restart(&self, timeout: Option<Duration>) -> Self {
74        self.stopped.store(false, Ordering::Relaxed);
75        Self { stopped: Arc::clone(&self.stopped), started: Instant::now(), limit: timeout }
76    }
77
78    /// Stop whatever is running on this token.
79    ///
80    /// Returns immediately. The query stops at its next chunk boundary, so a caller that wants to
81    /// know it has stopped waits for the query's own thread to return an error.
82    pub fn cancel(&self) {
83        self.stopped.store(true, Ordering::Relaxed);
84    }
85
86    /// Whether the query should stop.
87    #[must_use]
88    pub fn is_cancelled(&self) -> bool {
89        self.stopped.load(Ordering::Relaxed) || self.expired()
90    }
91
92    /// How long this token has been running.
93    #[must_use]
94    pub fn elapsed(&self) -> Duration {
95        self.started.elapsed()
96    }
97
98    /// The time limit on it, if it has one.
99    #[must_use]
100    pub fn limit(&self) -> Option<Duration> {
101        self.limit
102    }
103
104    /// An error when the query should stop, and nothing when it should keep going.
105    ///
106    /// The two reasons produce different sentences, because they are different things to a person
107    /// reading a log: one of them says somebody pressed a key and the other says the query needed
108    /// more time than it was given, and a timeout reported as an interrupt sends whoever reads it
109    /// looking for a person who was not there.
110    ///
111    /// # Errors
112    ///
113    /// [`crate::ErrorCode::Interrupt`] when the flag is set or the time is up.
114    pub fn check(&self) -> Result<()> {
115        if self.stopped.load(Ordering::Relaxed) {
116            return Err(Error::interrupt("Interrupted!"));
117        }
118        if self.expired() {
119            let limit = self.limit.unwrap_or_default();
120            return Err(Error::interrupt(format!(
121                "query took longer than the {} millisecond limit it was given",
122                limit.as_millis()
123            )));
124        }
125        Ok(())
126    }
127
128    /// Whether the clock has run out, which is false when there is no clock.
129    ///
130    /// The call to read the time is behind the `Some`, so a query with no limit on it does not read
131    /// the clock once per chunk for an answer that cannot change.
132    fn expired(&self) -> bool {
133        self.limit.is_some_and(|limit| self.started.elapsed() >= limit)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use std::thread;
140    use std::time::Duration;
141
142    use super::Cancel;
143
144    #[test]
145    fn a_fresh_token_lets_the_query_run() {
146        let cancel = Cancel::new();
147        assert!(!cancel.is_cancelled());
148        assert!(cancel.check().is_ok());
149        assert_eq!(cancel.limit(), None);
150    }
151
152    #[test]
153    fn cancelling_one_handle_stops_the_query_holding_another() {
154        let cancel = Cancel::new();
155        let other = cancel.clone();
156        other.cancel();
157        assert!(cancel.is_cancelled());
158        let error = cancel.check().expect_err("it was cancelled");
159        assert_eq!(error.code().duckdb_name(), "Interrupt Error");
160        assert_eq!(error.message(), "Interrupted!");
161    }
162
163    #[test]
164    fn a_token_another_thread_cancels_is_seen_by_the_one_running_the_query() {
165        let cancel = Cancel::new();
166        let other = cancel.clone();
167        let stopper = thread::spawn(move || other.cancel());
168        stopper.join().expect("the thread ran");
169        assert!(cancel.is_cancelled());
170    }
171
172    #[test]
173    fn a_time_limit_runs_out_on_its_own() {
174        let cancel = Cancel::after(Duration::from_millis(1));
175        assert_eq!(cancel.limit(), Some(Duration::from_millis(1)));
176        thread::sleep(Duration::from_millis(5));
177        assert!(cancel.is_cancelled());
178        let error = cancel.check().expect_err("the time is up");
179        assert_eq!(error.code().duckdb_name(), "Interrupt Error");
180        assert!(error.message().contains("longer than the 1 millisecond limit"), "{error}");
181    }
182
183    #[test]
184    fn a_timeout_and_an_interrupt_say_different_things() {
185        // A timeout reported as an interrupt sends whoever reads the log looking for a person who
186        // was not there.
187        let interrupted = Cancel::new();
188        interrupted.cancel();
189        let timed_out = Cancel::after(Duration::from_millis(0));
190        assert_ne!(
191            interrupted.check().expect_err("cancelled").message(),
192            timed_out.check().expect_err("timed out").message()
193        );
194    }
195
196    #[test]
197    fn restarting_shares_the_flag_and_starts_the_clock_again() {
198        let connection = Cancel::new();
199        let statement = connection.restart(Some(Duration::from_secs(60)));
200        assert!(!statement.is_cancelled());
201        connection.cancel();
202        assert!(statement.is_cancelled(), "the flag is shared");
203    }
204
205    #[test]
206    fn an_interrupt_between_two_statements_does_not_stop_the_next_one() {
207        let connection = Cancel::new();
208        connection.cancel();
209        let statement = connection.restart(None);
210        assert!(!statement.is_cancelled());
211        assert!(!connection.is_cancelled(), "and the connection is usable again");
212    }
213
214    #[test]
215    fn a_limit_is_on_the_statement_rather_than_on_the_connection() {
216        let connection = Cancel::after(Duration::from_millis(1));
217        thread::sleep(Duration::from_millis(5));
218        assert!(connection.is_cancelled());
219        let statement = connection.restart(Some(Duration::from_secs(60)));
220        assert!(!statement.is_cancelled(), "the clock started again");
221    }
222}