vv_agent/runtime/
cancellation.rs1use 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}