Skip to main content

zeppelin_embed/lifecycle/
cancel.rs

1//! Query cancellation and deadline types.
2//!
3//! Store queries check cancellation at the start of each partition and every
4//! 64 rows thereafter (every 16 four-row Bit4 batches). This granularity keeps
5//! the hot-loop operation to relaxed atomic loads while bounding wasted work
6//! inside one large partition. The waiting query caller owns deadline timing:
7//! it parks on the completion condition variable until the monotonic deadline,
8//! then flips the same atomic observed by workers. Scan loops therefore make
9//! no clock syscall and take no cancellation mutex.
10
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU8, Ordering};
13use std::time::{Duration, Instant};
14
15use crate::scan::ScanError;
16
17use super::{SnapshotLease, StoreError};
18
19const ACTIVE: u8 = 0;
20const CANCELLED: u8 = 1;
21const TIMED_OUT: u8 = 2;
22
23/// A clonable, thread-safe request to cancel one query without partial results.
24#[derive(Clone, Debug)]
25pub struct CancelToken {
26    state: Arc<AtomicU8>,
27}
28
29impl CancelToken {
30    /// Creates an active token.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            state: Arc::new(AtomicU8::new(ACTIVE)),
35        }
36    }
37
38    /// Requests cancellation. Repeated calls have no additional effect.
39    pub fn cancel(&self) {
40        let _ =
41            self.state
42                .compare_exchange(ACTIVE, CANCELLED, Ordering::Relaxed, Ordering::Relaxed);
43    }
44
45    pub(crate) fn is_cancelled(&self) -> bool {
46        self.state.load(Ordering::Relaxed) == CANCELLED
47    }
48}
49
50impl Default for CancelToken {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56/// A monotonic absolute query deadline.
57#[derive(Clone)]
58pub struct Deadline {
59    at: Instant,
60    clock: Arc<dyn super::MonotonicClock>,
61    state: Arc<AtomicU8>,
62}
63
64impl std::fmt::Debug for Deadline {
65    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        formatter
67            .debug_struct("Deadline")
68            .field("at", &self.at)
69            .field("state", &self.state.load(Ordering::Relaxed))
70            .finish_non_exhaustive()
71    }
72}
73
74impl Deadline {
75    /// Creates a deadline relative to the current monotonic clock.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`DeadlineError`] when the requested duration cannot be
80    /// represented by [`Instant`].
81    pub fn after(duration: Duration) -> Result<Self, DeadlineError> {
82        Self::after_with_clock(duration, Arc::new(super::SystemMonotonicClock))
83    }
84
85    /// Creates a deadline relative to an injected monotonic clock.
86    ///
87    /// This is available only to deterministic test-support builds so a test
88    /// can construct and evaluate the deadline in one clock domain.
89    #[cfg(any(test, feature = "test-seams"))]
90    pub fn after_with_test_clock(
91        duration: Duration,
92        clock: Arc<dyn super::MonotonicClock>,
93    ) -> Result<Self, DeadlineError> {
94        Self::after_with_clock(duration, clock)
95    }
96
97    pub(crate) fn after_with_clock(
98        duration: Duration,
99        clock: Arc<dyn super::MonotonicClock>,
100    ) -> Result<Self, DeadlineError> {
101        let at = clock
102            .now()
103            .checked_add(duration)
104            .ok_or(DeadlineError::OutOfRange)?;
105        Ok(Self {
106            at,
107            clock,
108            state: Arc::new(AtomicU8::new(ACTIVE)),
109        })
110    }
111}
112
113/// Failure while constructing a monotonic deadline.
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub enum DeadlineError {
116    /// The relative duration exceeded the monotonic clock's range.
117    OutOfRange,
118}
119
120impl std::fmt::Display for DeadlineError {
121    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            Self::OutOfRange => formatter.write_str("query deadline is outside Instant's range"),
124        }
125    }
126}
127
128impl std::error::Error for DeadlineError {}
129
130/// The mandatory cooperative stop mechanism carried by one store query.
131#[derive(Clone, Debug)]
132pub enum QueryControl {
133    /// Stop when the monotonic deadline expires.
134    Deadline(Deadline),
135    /// Stop when any clone of the token is cancelled.
136    Cancel(CancelToken),
137}
138
139/// Typed failure from a store-admitted query.
140#[derive(Debug)]
141pub enum QueryError {
142    /// The deadline expired. Partial results are never returned.
143    Timeout {
144        /// Permanently false; included so callers can reject future partial paths.
145        partial: bool,
146    },
147    /// The caller cancelled the token. Partial results are never returned.
148    Cancelled {
149        /// Permanently false; included so callers can reject future partial paths.
150        partial: bool,
151    },
152    /// Store close cancelled an already admitted query.
153    ReadCancelled {
154        /// Permanently false; included so callers can reject future partial paths.
155        partial: bool,
156    },
157    /// Store admission or lifecycle failed.
158    Store(StoreError),
159    /// Scan validation or scoring failed.
160    Scan(ScanError),
161    /// Graph validation or traversal failed.
162    Graph(crate::graph::search::GraphSearchError),
163}
164
165impl std::fmt::Display for QueryError {
166    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            Self::Timeout { partial } => {
169                write!(formatter, "query deadline expired (partial={partial})")
170            }
171            Self::Cancelled { partial } => {
172                write!(formatter, "query was cancelled (partial={partial})")
173            }
174            Self::ReadCancelled { partial } => {
175                write!(formatter, "store close cancelled query (partial={partial})")
176            }
177            Self::Store(error) => error.fmt(formatter),
178            Self::Scan(error) => error.fmt(formatter),
179            Self::Graph(error) => error.fmt(formatter),
180        }
181    }
182}
183
184impl std::error::Error for QueryError {
185    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
186        match self {
187            Self::Store(error) => Some(error),
188            Self::Scan(error) => Some(error),
189            Self::Graph(error) => Some(error),
190            Self::Timeout { .. } | Self::Cancelled { .. } | Self::ReadCancelled { .. } => None,
191        }
192    }
193}
194
195impl QueryControl {
196    /// Checks the same absolute deadline or cancellation token before admission
197    /// and between caller-owned stages such as tokenization and embedding.
198    ///
199    /// This never extends a deadline. An admitted store query also checks its
200    /// snapshot lease so store-close cancellation retains precedence.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`QueryError::Timeout`] or [`QueryError::Cancelled`], always
205    /// with `partial: false`, when the caller's control has stopped the query.
206    pub fn checkpoint(&self) -> Result<(), QueryError> {
207        if self
208            .deadline()
209            .zip(self.now())
210            .is_some_and(|(deadline, now)| now >= deadline)
211        {
212            self.mark_timed_out();
213        }
214        self.error().map_or(Ok(()), Err)
215    }
216
217    pub(crate) fn with_clock(mut self, clock: Arc<dyn super::MonotonicClock>) -> Self {
218        if let Self::Deadline(deadline) = &mut self {
219            deadline.clock = clock;
220        }
221        self
222    }
223
224    pub(crate) fn deadline(&self) -> Option<Instant> {
225        match self {
226            Self::Deadline(deadline) => Some(deadline.at),
227            Self::Cancel(_) => None,
228        }
229    }
230
231    pub(crate) fn now(&self) -> Option<Instant> {
232        match self {
233            Self::Deadline(deadline) => Some(deadline.clock.now()),
234            Self::Cancel(_) => None,
235        }
236    }
237
238    pub(crate) fn state(&self) -> &Arc<AtomicU8> {
239        match self {
240            Self::Deadline(deadline) => &deadline.state,
241            Self::Cancel(token) => &token.state,
242        }
243    }
244
245    pub(crate) fn mark_timed_out(&self) {
246        let _ =
247            self.state()
248                .compare_exchange(ACTIVE, TIMED_OUT, Ordering::Relaxed, Ordering::Relaxed);
249    }
250
251    pub(crate) fn error(&self) -> Option<QueryError> {
252        match self.state().load(Ordering::Relaxed) {
253            CANCELLED => Some(QueryError::Cancelled { partial: false }),
254            TIMED_OUT => Some(QueryError::Timeout { partial: false }),
255            _ => None,
256        }
257    }
258}
259
260/// Cheap cooperative stop check shared by one query and its snapshot lease.
261pub struct QueryCancellation<'a> {
262    control: &'a QueryControl,
263    lease: &'a SnapshotLease,
264}
265
266impl<'a> QueryCancellation<'a> {
267    pub(crate) fn cache_lock<'lock, T>(
268        &self,
269        mutex: &'lock std::sync::Mutex<T>,
270        component: &'static str,
271    ) -> Result<std::sync::MutexGuard<'lock, T>, QueryError> {
272        self.check_graph().map_err(QueryError::Scan)?;
273        let guard = loop {
274            match mutex.try_lock() {
275                Ok(guard) => break guard,
276                Err(std::sync::TryLockError::Poisoned(_)) => {
277                    return Err(QueryError::Store(StoreError::Synchronization { component }));
278                }
279                Err(std::sync::TryLockError::WouldBlock) => {
280                    self.check_graph().map_err(QueryError::Scan)?;
281                    std::thread::park_timeout(Duration::from_millis(1));
282                }
283            }
284        };
285        self.check_graph().map_err(QueryError::Scan)?;
286        Ok(guard)
287    }
288
289    /// Binds caller cancellation/deadline state to the admitted snapshot read.
290    #[must_use]
291    pub const fn new(control: &'a QueryControl, lease: &'a SnapshotLease) -> Self {
292        Self { control, lease }
293    }
294
295    pub(crate) fn check(&self) -> Result<(), ScanError> {
296        if self.lease.is_cancelled() {
297            return Err(ScanError::ReadCancelled { partial: false });
298        }
299        match self.control.state().load(Ordering::Relaxed) {
300            CANCELLED => Err(ScanError::Cancelled { partial: false }),
301            TIMED_OUT => Err(ScanError::Timeout { partial: false }),
302            _ => Ok(()),
303        }
304    }
305
306    pub(crate) fn check_graph(&self) -> Result<(), ScanError> {
307        if self
308            .control
309            .deadline()
310            .zip(self.control.now())
311            .is_some_and(|(deadline, now)| now >= deadline)
312        {
313            self.control.mark_timed_out();
314        }
315        self.check()
316    }
317
318    #[cfg(any(test, feature = "test-seams"))]
319    pub(crate) fn cancel_token_for_test(&self) -> Option<CancelToken> {
320        match self.control {
321            QueryControl::Cancel(token) => Some(token.clone()),
322            QueryControl::Deadline(_) => None,
323        }
324    }
325}