Skip to main content

query_runtime/runtime/context/
cancellation.rs

1//! Cooperative cancellation token for native query execution.
2
3use std::time::{Duration, Instant};
4
5use query_planning::error::{QueryError, QueryErrorCode, Result};
6use tokio::time::sleep_until;
7use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
8
9use super::ExecutionContext;
10use crate::{
11    observability::{PlanExecutionId, RuntimeExecutionStageContext, RuntimeObservationHandle, RuntimeSpanId},
12    runtime::deadline::block_until,
13};
14
15/// Shared cooperative cancellation token for native query execution.
16///
17/// Cancellation is observed only at native-engine boundaries. It does not
18/// preempt a libcudf/CUDA call already in flight, but it prevents the next
19/// source chunk, pipeline task, operator, or materialization step from starting
20/// once it is requested. A wait can also resolve on it through
21/// [`Self::cancelled_or_expired`].
22#[derive(Debug, Clone)]
23pub struct ExecutionCancellation {
24    token: CancellationToken,
25    deadline: Option<Instant>,
26}
27
28impl ExecutionCancellation {
29    /// Creates a non-cancelled execution flag.
30    pub fn new() -> Self {
31        Self::with_deadline(None)
32    }
33
34    /// Creates a cancellation token with an optional absolute deadline.
35    ///
36    /// The deadline is checked at the same cooperative native-engine
37    /// boundaries as caller cancellation. It cannot preempt a CUDA call
38    /// already in flight, but it prevents the next safe boundary from
39    /// starting more native work after the deadline has elapsed.
40    pub fn with_deadline(deadline: Option<Instant>) -> Self {
41        Self { token: CancellationToken::new(), deadline }
42    }
43
44    /// Marks the execution as cancelled and wakes every
45    /// [`Self::cancelled_or_expired`] waiter.
46    pub fn cancel(&self) {
47        self.token.cancel();
48    }
49
50    /// Resolves once the execution is cancelled or its deadline elapses, so a
51    /// wait with no other wakeup for either can race it. [`Self::check`]
52    /// still tells the two apart. Must run inside a Tokio runtime with the
53    /// time driver enabled when a deadline is set.
54    pub async fn cancelled_or_expired(&self) {
55        match self.deadline {
56            Some(deadline) => tokio::select! {
57                () = self.token.cancelled() => {},
58                () = sleep_until(deadline.into()) => {},
59            },
60            None => self.token.cancelled().await,
61        }
62    }
63
64    /// Resolves to the typed error that ends a wait: the cancellation, or the
65    /// deadline once it elapses. A waiter returns it directly and does not
66    /// read the clock again, since a paused Tokio clock can disagree with a
67    /// fresh reading.
68    pub async fn terminated(&self, operation: &'static str) -> QueryError {
69        self.cancelled_or_expired().await;
70        match self.check(operation) {
71            Err(error) => error,
72            Ok(()) => deadline_expired_error(operation),
73        }
74    }
75
76    /// Resolves once cancellation is requested, ignoring the deadline. It
77    /// needs no Tokio timer, so a blocking wait races it and bounds itself by
78    /// [`Self::deadline`].
79    pub(crate) fn cancel_requested(&self) -> WaitForCancellationFuture<'_> {
80        self.token.cancelled()
81    }
82
83    /// Blocks the calling thread for `duration`. Cancellation or an elapsed
84    /// deadline ends the wait early with the typed error [`Self::check`]
85    /// reports.
86    pub fn sleep(&self, duration: Duration, operation: &'static str) -> Result<()> {
87        let until = Instant::now() + duration;
88        let until = self.deadline.map_or(until, |deadline| deadline.min(until));
89        block_until(self.cancel_requested(), Some(until));
90        self.check(operation)
91    }
92
93    /// Returns whether this execution must stop at its next cooperative safe
94    /// boundary.
95    ///
96    /// An elapsed deadline is intentionally an effective cancellation here.
97    /// Callers that need to distinguish an explicit caller action from expiry
98    /// must use [`Self::check`], which preserves explicit cancellation as the
99    /// primary reason.
100    pub fn is_cancelled(&self) -> bool {
101        self.cancellation_requested() || self.is_expired()
102    }
103
104    /// Returns the optional absolute deadline shared by every clone.
105    pub fn deadline(&self) -> Option<Instant> {
106        self.deadline
107    }
108
109    /// Returns whether the optional deadline has elapsed.
110    pub fn is_expired(&self) -> bool {
111        self.deadline.is_some_and(|deadline| Instant::now() >= deadline)
112    }
113
114    /// Returns a structured native error if cancellation has been requested or the deadline has
115    /// elapsed. Explicit cancellation takes precedence over expiry.
116    pub fn check(&self, operation: &'static str) -> Result<()> {
117        if self.cancellation_requested() {
118            return Err(QueryError::new(QueryErrorCode::Cancelled, operation, "native query execution was cancelled")
119                .with_fact("cancellation_requested", true));
120        }
121        if self.is_expired() {
122            return Err(deadline_expired_error(operation));
123        }
124        Ok(())
125    }
126
127    fn cancellation_requested(&self) -> bool {
128        self.token.is_cancelled()
129    }
130}
131
132fn deadline_expired_error(operation: &'static str) -> QueryError {
133    QueryError::new(QueryErrorCode::Cancelled, operation, "native query execution deadline expired")
134        .with_fact("cancellation_requested", true)
135        .with_fact("reason", "deadline_expired")
136}
137
138impl Default for ExecutionCancellation {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl RuntimeExecutionStageContext for ExecutionContext {
145    fn runtime_operator_stage_context(
146        &self,
147    ) -> (RuntimeObservationHandle, Option<PlanExecutionId>, Option<RuntimeSpanId>) {
148        let observation = self.runtime_observation().clone();
149        let (plan_execution_id, parent_span_id) = self
150            .runtime_execution_scope()
151            .map(|scope| (Some(scope.plan_execution_id().clone()), Some(scope.plan_span_id())))
152            .unwrap_or((None, None));
153        (observation, plan_execution_id, parent_span_id)
154    }
155}