Skip to main content

spg_engine/
cancel.rs

1//! Cooperative query cancellation, split out of `lib.rs` (lib.rs split
2//! 19). `CancelToken` is the lightweight handle threaded through every
3//! scanning loop: it wraps an optional `&AtomicBool` flag (the server's
4//! per-query watchdog) and an optional monotonic deadline (PG
5//! `statement_timeout`), and `check()` returns `EngineError::Cancelled`
6//! when either trips. `none()` is the zero-cost default for the
7//! uncancellable path. Public API — `spg-server` / `spg-embedded`
8//! construct tokens via `CancelToken::none().with_deadline(...)`.
9
10use crate::EngineError;
11
12/// v7.17.0 Phase 2.3 — monotonic time source for deadline-aware
13/// cancellation (PG `statement_timeout`). Returns microseconds
14/// since some host-stable monotonic origin (typically the first
15/// call into `Instant::now()` on the server). The engine never
16/// calls `Instant::now()` directly so the crate stays `#![no_std]`.
17pub type MonotonicNowFn = fn() -> u64;
18
19#[derive(Debug, Clone, Copy)]
20struct Deadline {
21    now_fn: MonotonicNowFn,
22    /// Absolute deadline in `now_fn()` units (microseconds).
23    deadline_us: u64,
24}
25
26#[derive(Debug, Clone, Copy)]
27pub struct CancelToken<'a> {
28    flag: Option<&'a core::sync::atomic::AtomicBool>,
29    // v7.17.0 Phase 2.3 — when set, every existing `cancel.check()`
30    // checkpoint also fires `EngineError::Cancelled` once
31    // `(now_fn)() >= deadline_us`. No new check sites, no thread
32    // spawn per query — the monotonic now-fn read is a vDSO
33    // `clock_gettime(CLOCK_MONOTONIC)` (~20ns) and only runs when
34    // the host actually wired a deadline (statement_timeout > 0).
35    deadline: Option<Deadline>,
36}
37
38impl<'a> CancelToken<'a> {
39    #[must_use]
40    pub const fn none() -> Self {
41        Self {
42            flag: None,
43            deadline: None,
44        }
45    }
46
47    #[must_use]
48    pub const fn from_flag(f: &'a core::sync::atomic::AtomicBool) -> Self {
49        Self {
50            flag: Some(f),
51            deadline: None,
52        }
53    }
54
55    /// v7.17.0 Phase 2.3 — attach a monotonic deadline. `now_fn`
56    /// must return microseconds since a stable origin; the token
57    /// trips when `now_fn() >= deadline_us`. Compose with
58    /// `from_flag(...)` when both a watchdog flag and a per-statement
59    /// timeout are in play (e.g. server-wide `SPG_QUERY_TIMEOUT_MS`
60    /// plus session `statement_timeout`); the tighter of the two
61    /// wins by virtue of either signaling first.
62    #[must_use]
63    pub const fn with_deadline(mut self, now_fn: MonotonicNowFn, deadline_us: u64) -> Self {
64        self.deadline = Some(Deadline {
65            now_fn,
66            deadline_us,
67        });
68        self
69    }
70
71    #[must_use]
72    pub fn is_cancelled(self) -> bool {
73        if self
74            .flag
75            .is_some_and(|f| f.load(core::sync::atomic::Ordering::Relaxed))
76        {
77            return true;
78        }
79        // Deadline check is the second branch so the "no timeout"
80        // hot path (`deadline: None`) elides the now-fn call —
81        // predicted-not-taken on the SLO INSERT loop.
82        if let Some(d) = self.deadline
83            && (d.now_fn)() >= d.deadline_us
84        {
85            return true;
86        }
87        false
88    }
89
90    /// Returns `Err(Cancelled)` if the token has been tripped.
91    /// Used at row-loop checkpoints to bail cooperatively without
92    /// scattering raw `is_cancelled` checks across the executor.
93    #[inline]
94    pub fn check(self) -> Result<(), EngineError> {
95        if self.is_cancelled() {
96            Err(EngineError::Cancelled)
97        } else {
98            Ok(())
99        }
100    }
101}