Skip to main content

vv_agent/runtime/
cancellation.rs

1use std::cell::RefCell;
2use std::panic::{catch_unwind, AssertUnwindSafe};
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5
6thread_local! {
7    static CANCELLATION_SCOPES: RefCell<Vec<CancellationToken>> = const { RefCell::new(Vec::new()) };
8}
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct CancelledError {
12    message: String,
13}
14
15impl CancelledError {
16    pub fn new(message: impl Into<String>) -> Self {
17        Self {
18            message: message.into(),
19        }
20    }
21
22    pub fn message(&self) -> &str {
23        &self.message
24    }
25}
26
27impl std::fmt::Display for CancelledError {
28    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        formatter.write_str(&self.message)
30    }
31}
32
33impl std::error::Error for CancelledError {}
34
35#[derive(Clone, Default)]
36pub struct CancellationToken {
37    inner: Arc<CancellationState>,
38}
39
40#[derive(Default)]
41struct CancellationState {
42    cancelled: AtomicBool,
43    reason: Mutex<Option<String>>,
44    callbacks: Mutex<Vec<Arc<dyn Fn() + Send + Sync + 'static>>>,
45}
46
47fn invoke_callback(callback: &Arc<dyn Fn() + Send + Sync + 'static>) {
48    let _ = catch_unwind(AssertUnwindSafe(|| callback()));
49}
50
51pub(crate) struct CancellationScope {
52    active: bool,
53}
54
55impl Drop for CancellationScope {
56    fn drop(&mut self) {
57        if self.active {
58            CANCELLATION_SCOPES.with(|scopes| {
59                scopes.borrow_mut().pop();
60            });
61        }
62    }
63}
64
65impl std::fmt::Debug for CancellationToken {
66    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        formatter
68            .debug_struct("CancellationToken")
69            .field("cancelled", &self.is_cancelled())
70            .finish()
71    }
72}
73
74impl CancellationToken {
75    pub fn cancel(&self) {
76        self.cancel_with_reason("Operation was cancelled");
77    }
78
79    pub fn cancel_with_reason(&self, reason: impl Into<String>) {
80        let reason = reason.into();
81        let reason = if reason.trim().is_empty() {
82            "Operation was cancelled".to_string()
83        } else {
84            reason
85        };
86        let mut stored_reason = self
87            .inner
88            .reason
89            .lock()
90            .unwrap_or_else(std::sync::PoisonError::into_inner);
91        if self.inner.cancelled.load(Ordering::SeqCst) {
92            return;
93        }
94        *stored_reason = Some(reason);
95        self.inner.cancelled.store(true, Ordering::SeqCst);
96        drop(stored_reason);
97        let callbacks = std::mem::take(
98            &mut *self
99                .inner
100                .callbacks
101                .lock()
102                .unwrap_or_else(std::sync::PoisonError::into_inner),
103        );
104        for callback in callbacks {
105            invoke_callback(&callback);
106        }
107    }
108
109    pub fn is_cancelled(&self) -> bool {
110        self.inner.cancelled.load(Ordering::SeqCst)
111    }
112
113    pub fn cancelled(&self) -> bool {
114        self.is_cancelled()
115    }
116
117    pub fn reason(&self) -> Option<String> {
118        self.inner
119            .reason
120            .lock()
121            .unwrap_or_else(std::sync::PoisonError::into_inner)
122            .clone()
123    }
124
125    pub fn check(&self) -> Result<(), CancelledError> {
126        if self.is_cancelled() {
127            Err(CancelledError::new(
128                self.reason()
129                    .unwrap_or_else(|| "Operation was cancelled".to_string()),
130            ))
131        } else {
132            Ok(())
133        }
134    }
135
136    pub fn on_cancel(&self, callback: impl Fn() + Send + Sync + 'static) {
137        let callback: Arc<dyn Fn() + Send + Sync + 'static> = Arc::new(callback);
138        let call_immediately = {
139            let mut callbacks = self
140                .inner
141                .callbacks
142                .lock()
143                .unwrap_or_else(std::sync::PoisonError::into_inner);
144            if self.is_cancelled() {
145                true
146            } else {
147                callbacks.push(callback.clone());
148                false
149            }
150        };
151        if call_immediately {
152            invoke_callback(&callback);
153        }
154    }
155
156    pub fn child(&self) -> Self {
157        let child = Self::default();
158        let child_to_cancel = child.clone();
159        let parent = Arc::downgrade(&self.inner);
160        self.on_cancel(move || {
161            let reason = parent
162                .upgrade()
163                .and_then(|inner| {
164                    inner
165                        .reason
166                        .lock()
167                        .unwrap_or_else(std::sync::PoisonError::into_inner)
168                        .clone()
169                })
170                .unwrap_or_else(|| "Operation was cancelled".to_string());
171            child_to_cancel.cancel_with_reason(reason);
172        });
173        child
174    }
175
176    pub(crate) fn enter_scope(token: Option<&Self>) -> CancellationScope {
177        let active = if let Some(token) = token {
178            CANCELLATION_SCOPES.with(|scopes| scopes.borrow_mut().push(token.clone()));
179            true
180        } else {
181            false
182        };
183        CancellationScope { active }
184    }
185
186    pub(crate) fn child_of_current() -> Option<Self> {
187        CANCELLATION_SCOPES.with(|scopes| scopes.borrow().last().map(Self::child))
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use std::panic::{catch_unwind, AssertUnwindSafe};
194    use std::sync::atomic::{AtomicUsize, Ordering};
195    use std::sync::Arc;
196
197    use super::CancellationToken;
198
199    #[test]
200    fn cancel_is_idempotent_and_callbacks_run_once() {
201        let token = CancellationToken::default();
202        let calls = Arc::new(AtomicUsize::new(0));
203        let calls_for_callback = calls.clone();
204        token.on_cancel(move || {
205            calls_for_callback.fetch_add(1, Ordering::SeqCst);
206        });
207
208        token.cancel();
209        token.cancel();
210
211        assert!(token.is_cancelled());
212        assert_eq!(calls.load(Ordering::SeqCst), 1);
213    }
214
215    #[test]
216    fn cancel_isolates_callback_panics_and_still_cancels_children() {
217        let token = CancellationToken::default();
218        let calls = Arc::new(AtomicUsize::new(0));
219        token.on_cancel(|| panic!("callback panic"));
220        let calls_for_callback = calls.clone();
221        token.on_cancel(move || {
222            calls_for_callback.fetch_add(1, Ordering::SeqCst);
223        });
224        let child = token.child();
225
226        let outcome = catch_unwind(AssertUnwindSafe(|| token.cancel()));
227
228        assert!(outcome.is_ok());
229        assert_eq!(calls.load(Ordering::SeqCst), 1);
230        assert!(child.is_cancelled());
231    }
232
233    #[test]
234    fn callbacks_registered_after_cancellation_are_individually_isolated() {
235        let token = CancellationToken::default();
236        token.cancel();
237
238        let panic_outcome = catch_unwind(AssertUnwindSafe(|| {
239            token.on_cancel(|| panic!("late callback panic"));
240        }));
241        let calls = Arc::new(AtomicUsize::new(0));
242        let calls_for_callback = calls.clone();
243        token.on_cancel(move || {
244            calls_for_callback.fetch_add(1, Ordering::SeqCst);
245        });
246
247        assert!(panic_outcome.is_ok());
248        assert_eq!(calls.load(Ordering::SeqCst), 1);
249    }
250
251    #[test]
252    fn current_scope_derives_one_way_child_cancellation() {
253        let parent = CancellationToken::default();
254        let child = {
255            let _scope = CancellationToken::enter_scope(Some(&parent));
256            CancellationToken::child_of_current().expect("derived child")
257        };
258
259        child.cancel();
260        assert!(!parent.is_cancelled());
261
262        let second_child = {
263            let _scope = CancellationToken::enter_scope(Some(&parent));
264            CancellationToken::child_of_current().expect("derived child")
265        };
266        parent.cancel();
267        assert!(second_child.is_cancelled());
268    }
269
270    #[test]
271    fn child_preserves_parent_cancellation_reason() {
272        let parent = CancellationToken::default();
273        let child = parent.child();
274
275        parent.cancel_with_reason("host requested cancellation");
276
277        assert_eq!(
278            child.reason().as_deref(),
279            Some("host requested cancellation")
280        );
281        assert_eq!(
282            child.check().expect_err("child cancellation").message(),
283            "host requested cancellation"
284        );
285    }
286}