Skip to main content

formualizer_eval/engine/
cancel.rs

1//! Cooperative cancellation for engine calls.
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5
6/// A cooperative cancellation signal for a single engine call.
7///
8/// Cancellation is checked at engine checkpoints during both target preparation
9/// and evaluation. Setting it does not abort work in progress; it causes the next
10/// checkpoint to return [`ExcelErrorKind::Cancelled`](formualizer_common::ExcelErrorKind).
11///
12/// The token is a handle: cloning shares one signal, so a caller can hold a clone
13/// and cancel from another thread.
14///
15/// ```
16/// use formualizer_eval::engine::CancelToken;
17///
18/// let token = CancelToken::new();
19/// let remote = token.clone();
20/// assert!(!token.is_cancelled());
21/// remote.cancel();
22/// assert!(token.is_cancelled());
23/// ```
24///
25/// The underlying representation is deliberately private so that richer
26/// cancellation (cancellation reasons, linked child tokens) can be added without
27/// a breaking change. Use [`CancelToken::from_flag`] to adopt a flag you already
28/// own.
29#[derive(Clone, Debug, Default)]
30pub struct CancelToken(Arc<AtomicBool>);
31
32impl CancelToken {
33    /// Creates a token that has not been cancelled.
34    pub fn new() -> Self {
35        Self(Arc::new(AtomicBool::new(false)))
36    }
37
38    /// Signals cancellation. Idempotent, and safe to call from another thread.
39    pub fn cancel(&self) {
40        self.0.store(true, Ordering::Relaxed);
41    }
42
43    /// Returns whether cancellation has been signalled.
44    #[inline]
45    pub fn is_cancelled(&self) -> bool {
46        self.0.load(Ordering::Relaxed)
47    }
48
49    /// Adopts an existing flag, for callers that already own one or share it with
50    /// non-engine code.
51    pub fn from_flag(flag: Arc<AtomicBool>) -> Self {
52        Self(flag)
53    }
54
55    /// Borrows the underlying flag as a legacy/interoperability bridge.
56    ///
57    /// New code should poll [`CancelToken::is_cancelled`] instead. The raw flag
58    /// may not represent future richer cancellation semantics.
59    pub fn as_flag(&self) -> &Arc<AtomicBool> {
60        &self.0
61    }
62}
63
64impl From<Arc<AtomicBool>> for CancelToken {
65    fn from(flag: Arc<AtomicBool>) -> Self {
66        Self::from_flag(flag)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn clones_share_one_signal() {
76        let token = CancelToken::new();
77        let clone = token.clone();
78        assert!(!token.is_cancelled());
79        clone.cancel();
80        assert!(token.is_cancelled());
81    }
82
83    #[test]
84    fn adopting_a_flag_observes_external_cancellation() {
85        let flag = Arc::new(AtomicBool::new(false));
86        let token = CancelToken::from_flag(Arc::clone(&flag));
87        assert!(!token.is_cancelled());
88        flag.store(true, Ordering::Relaxed);
89        assert!(token.is_cancelled());
90    }
91
92    #[test]
93    fn default_token_is_not_cancelled() {
94        assert!(!CancelToken::default().is_cancelled());
95    }
96}