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}