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}