kithara_platform/common/cancel/
token.rs1use std::{fmt, sync::Arc};
2
3use super::{
4 node::{Node, Slot},
5 wait::Cancelled,
6};
7
8#[derive(Clone)]
18pub struct CancelToken {
19 node: Arc<Node>,
20}
21
22impl CancelToken {
23 pub fn cancel(&self) {
25 self.node.cancel();
26 }
27
28 #[must_use]
31 pub fn cancelled(&self) -> Cancelled<'_> {
32 Cancelled::new(&self.node)
33 }
34
35 #[must_use]
37 pub fn child(&self) -> Self {
38 Self {
39 node: Node::child(&self.node),
40 }
41 }
42
43 #[must_use]
44 pub fn is_cancelled(&self) -> bool {
45 self.node.is_cancelled()
46 }
47
48 #[must_use]
52 pub fn never() -> Self {
53 Self { node: Node::root() }
54 }
55
56 pub fn on_cancel<F>(&self, waker: F) -> CancelWakerGuard
66 where
67 F: Fn() + Send + Sync + 'static,
68 {
69 let id = self.node.register(Slot::Sync(Arc::new(waker)));
70 CancelWakerGuard {
71 node: id.map(|id| (Arc::clone(&self.node), id)),
72 }
73 }
74
75 #[must_use]
82 pub fn root() -> Self {
83 Self { node: Node::root() }
84 }
85}
86
87impl fmt::Debug for CancelToken {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct("CancelToken")
90 .field("cancelled", &self.is_cancelled())
91 .finish()
92 }
93}
94
95#[must_use = "dropping the guard immediately unregisters the cancel waker"]
98pub struct CancelWakerGuard {
99 node: Option<(Arc<Node>, u64)>,
100}
101
102impl Drop for CancelWakerGuard {
103 fn drop(&mut self) {
104 if let Some((node, id)) = &self.node {
105 node.unregister(*id);
106 }
107 }
108}
109
110impl fmt::Debug for CancelWakerGuard {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.debug_struct("CancelWakerGuard")
113 .field("registered", &self.node.is_some())
114 .finish()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use std::{
121 sync::{
122 Arc,
123 atomic::{AtomicUsize, Ordering},
124 },
125 time::Duration,
126 };
127
128 use kithara_test_utils::kithara;
129 use tokio::{spawn, task, time as tokio_time};
130
131 use super::CancelToken;
132
133 #[kithara::test(timeout(Duration::from_secs(5)))]
134 fn fresh_token_not_cancelled() {
135 let c = CancelToken::root();
136 assert!(!c.is_cancelled());
137 }
138
139 #[kithara::test(timeout(Duration::from_secs(5)))]
140 fn cancel_sets_lock_free_flag() {
141 let c = CancelToken::root();
142 c.cancel();
143 assert!(c.is_cancelled());
144 }
145
146 #[kithara::test(timeout(Duration::from_secs(5)))]
147 fn child_cancel_does_not_cancel_parent() {
148 let parent = CancelToken::root();
150 let child = parent.child();
151 child.cancel();
152 assert!(child.is_cancelled());
153 assert!(
154 !parent.is_cancelled(),
155 "child cancel must not cancel the parent"
156 );
157 }
158
159 #[kithara::test(timeout(Duration::from_secs(5)))]
160 fn child_cancel_does_not_cancel_sibling() {
161 let parent = CancelToken::root();
162 let a = parent.child();
163 let b = parent.child();
164 a.cancel();
165 assert!(a.is_cancelled());
166 assert!(!b.is_cancelled(), "sibling cancel must stay independent");
167 assert!(!parent.is_cancelled());
168 }
169
170 #[kithara::test(timeout(Duration::from_secs(5)))]
171 fn clone_shares_cancel_identity() {
172 let token = CancelToken::never();
175 let twin = token.clone();
176 twin.cancel();
177 assert!(
178 token.is_cancelled(),
179 "clone shares identity with the original"
180 );
181 }
182
183 #[kithara::test(timeout(Duration::from_secs(5)))]
184 fn child_observes_own_cancel() {
185 let parent = CancelToken::root();
186 let child = parent.child();
187 child.cancel();
188 assert!(child.is_cancelled());
189 }
190
191 #[kithara::test(timeout(Duration::from_secs(5)))]
195 fn child_observes_parent_cancel_lock_free() {
196 let master = CancelToken::root();
197 let worker = master.child();
198 assert!(!worker.is_cancelled());
199
200 std::thread::scope(|s| {
201 s.spawn(|| master.cancel());
202 });
203
204 assert!(
205 worker.is_cancelled(),
206 "worker child must observe master cancel via propagate-down"
207 );
208 }
209
210 #[kithara::test(timeout(Duration::from_secs(5)))]
211 fn grandchild_observes_master_cancel_lock_free() {
212 let master = CancelToken::root();
213 let mid = master.child();
214 let leaf = mid.child();
215
216 master.cancel();
217 assert!(mid.is_cancelled());
218 assert!(leaf.is_cancelled());
219 }
220
221 #[kithara::test(timeout(Duration::from_secs(5)))]
226 fn root_cancel_reaches_grandchild_after_intermediate_dropped() {
227 let master = CancelToken::root();
228 let leaf = {
229 let mid = master.child();
230 mid.child()
231 };
233 assert!(!leaf.is_cancelled());
234 master.cancel();
235 assert!(
236 leaf.is_cancelled(),
237 "root cancel must reach the grandchild after the intermediate token dropped"
238 );
239 }
240
241 #[kithara::test(timeout(Duration::from_secs(5)))]
242 fn repeat_cancel_is_idempotent() {
243 let c = CancelToken::root();
244 c.cancel();
245 c.cancel();
246 assert!(c.is_cancelled());
247 }
248
249 #[kithara::test(timeout(Duration::from_secs(5)))]
250 fn late_child_born_cancelled() {
251 let master = CancelToken::root();
252 master.cancel();
253 let child = master.child();
254 assert!(
255 child.is_cancelled(),
256 "a child born after the parent cancelled must be born cancelled"
257 );
258 }
259
260 #[kithara::test(timeout(Duration::from_secs(5)))]
261 fn child_churn_then_cancel_reaches_survivors() {
262 let master = CancelToken::root();
267 for _ in 0..1000 {
268 let _ = master.child();
269 }
270 let survivor = master.child();
271 let _second = master.child();
272 master.cancel();
273 assert!(survivor.is_cancelled());
274 }
275
276 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
277 async fn async_cancelled_resolves_on_self_cancel() {
278 let c = CancelToken::never();
280 let c2 = c.clone();
281 let handle = spawn(async move {
282 c2.cancelled().await;
283 c2.is_cancelled()
284 });
285 task::yield_now().await;
286
287 c.cancel();
288
289 let flag = tokio_time::timeout(Duration::from_secs(2), handle)
290 .await
291 .expect("cancelled() must resolve within the test timeout")
292 .expect("spawned cancellation task must not panic");
293 assert!(flag, "flag must be visible once cancelled() resolves");
294 }
295
296 fn counter() -> (Arc<AtomicUsize>, impl Fn() + Send + Sync + 'static) {
297 let n = Arc::new(AtomicUsize::new(0));
298 let n2 = Arc::clone(&n);
299 (n, move || {
300 n2.fetch_add(1, Ordering::SeqCst);
301 })
302 }
303
304 #[kithara::test(timeout(Duration::from_secs(5)))]
305 fn on_cancel_fires_on_self_cancel() {
306 let c = CancelToken::root();
307 let child = c.child();
308 let (fired, waker) = counter();
309 let _guard = child.on_cancel(waker);
310 assert_eq!(fired.load(Ordering::SeqCst), 0);
311 child.cancel();
312 assert_eq!(fired.load(Ordering::SeqCst), 1, "waker must fire on cancel");
313 }
314
315 #[kithara::test(timeout(Duration::from_secs(5)))]
316 fn on_cancel_fires_on_ancestor_cancel() {
317 let master = CancelToken::root();
320 let child = master.child();
321 let (fired, waker) = counter();
322 let _guard = child.on_cancel(waker);
323 master.cancel();
324 assert!(
325 fired.load(Ordering::SeqCst) >= 1,
326 "ancestor cancel must wake a descendant's sync waker"
327 );
328 }
329
330 #[kithara::test(timeout(Duration::from_secs(5)))]
331 fn on_cancel_already_cancelled_fires_immediately() {
332 let c = CancelToken::never();
333 c.cancel();
334 let (fired, waker) = counter();
335 let _guard = c.on_cancel(waker);
336 assert_eq!(
337 fired.load(Ordering::SeqCst),
338 1,
339 "registering on an already-cancelled token fires at once"
340 );
341 }
342
343 #[kithara::test(timeout(Duration::from_secs(5)))]
344 fn on_cancel_guard_drop_unregisters() {
345 let c = CancelToken::never();
346 let (fired, waker) = counter();
347 let guard = c.on_cancel(waker);
348 drop(guard);
349 c.cancel();
350 assert_eq!(
351 fired.load(Ordering::SeqCst),
352 0,
353 "a dropped guard must not fire on a later cancel"
354 );
355 }
356
357 #[kithara::test(timeout(Duration::from_secs(5)))]
358 fn on_cancel_sibling_cancel_does_not_fire() {
359 let parent = CancelToken::root();
360 let a = parent.child();
361 let b = parent.child();
362 let (fired, waker) = counter();
363 let _guard = a.on_cancel(waker);
364 b.cancel();
365 assert_eq!(
366 fired.load(Ordering::SeqCst),
367 0,
368 "a sibling cancel must not fire this token's waker"
369 );
370 assert!(!a.is_cancelled());
371 }
372}