kithara_platform/common/cancel/
group.rs1use std::{
2 fmt,
3 future::Future,
4 ops::BitOr,
5 pin::Pin,
6 sync::Arc,
7 task::{Context, Poll},
8};
9
10use super::{token::CancelToken, wait::Cancelled};
11
12#[derive(Clone)]
25pub struct CancelGroup {
26 sources: Arc<[CancelToken]>,
27}
28
29impl CancelGroup {
30 #[must_use]
31 pub fn new(sources: Vec<CancelToken>) -> Self {
32 Self {
33 sources: sources.into(),
34 }
35 }
36
37 #[must_use]
41 pub fn cancelled(&self) -> GroupCancelled<'_> {
42 GroupCancelled {
43 sources: &self.sources,
44 slots: Vec::new(),
45 done: false,
46 }
47 }
48
49 #[must_use]
50 pub fn is_cancelled(&self) -> bool {
51 self.sources.iter().any(CancelToken::is_cancelled)
52 }
53
54 fn tokens(&self) -> &[CancelToken] {
55 &self.sources
56 }
57}
58
59impl fmt::Debug for CancelGroup {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.debug_struct("CancelGroup")
62 .field("sources", &self.sources.len())
63 .field("cancelled", &self.is_cancelled())
64 .finish()
65 }
66}
67
68impl PartialEq for CancelGroup {
71 fn eq(&self, other: &Self) -> bool {
72 Arc::ptr_eq(&self.sources, &other.sources)
73 }
74}
75
76impl From<CancelToken> for CancelGroup {
77 fn from(token: CancelToken) -> Self {
78 Self::new(vec![token])
79 }
80}
81
82impl From<Vec<CancelToken>> for CancelGroup {
83 fn from(tokens: Vec<CancelToken>) -> Self {
84 Self::new(tokens)
85 }
86}
87
88impl BitOr for CancelGroup {
89 type Output = Self;
90
91 fn bitor(self, rhs: Self) -> Self {
92 let mut tokens = self.tokens().to_vec();
93 tokens.extend_from_slice(rhs.tokens());
94 Self::new(tokens)
95 }
96}
97
98impl BitOr<CancelToken> for CancelGroup {
99 type Output = Self;
100
101 fn bitor(self, rhs: CancelToken) -> Self {
102 let mut tokens = self.tokens().to_vec();
103 tokens.push(rhs);
104 Self::new(tokens)
105 }
106}
107
108impl BitOr<CancelGroup> for CancelToken {
109 type Output = CancelGroup;
110
111 fn bitor(self, rhs: CancelGroup) -> CancelGroup {
112 let mut tokens = vec![self];
113 tokens.extend_from_slice(rhs.tokens());
114 CancelGroup::new(tokens)
115 }
116}
117
118pub struct GroupCancelled<'a> {
121 sources: &'a [CancelToken],
122 slots: Vec<Cancelled<'a>>,
123 done: bool,
124}
125
126impl Future for GroupCancelled<'_> {
127 type Output = ();
128
129 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
130 let me = self.get_mut();
131 if me.done {
132 return Poll::Ready(());
133 }
134 if me.slots.is_empty() {
135 if me.sources.is_empty() {
136 return Poll::Pending;
137 }
138 me.slots = me.sources.iter().map(CancelToken::cancelled).collect();
139 }
140 for slot in &mut me.slots {
141 if Pin::new(slot).poll(cx).is_ready() {
142 me.done = true;
143 me.slots.clear();
144 return Poll::Ready(());
145 }
146 }
147 Poll::Pending
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use std::time::Duration;
154
155 use kithara_test_utils::kithara;
156 use tokio::{spawn, task, time as tokio_time};
157
158 use super::CancelGroup;
159 use crate::common::cancel::CancelToken;
160
161 #[derive(Clone, Debug)]
162 enum Src {
163 Fresh,
164 ChildOf(usize),
165 PreCancelled,
166 }
167
168 #[derive(Clone, Debug)]
169 enum Act {
170 Source(usize),
171 Parent(usize),
172 None,
173 }
174
175 struct Setup {
176 group: CancelGroup,
177 parents: Vec<CancelToken>,
178 sources: Vec<CancelToken>,
179 }
180
181 fn build(spec: &[Src]) -> Setup {
182 let mut parents: Vec<CancelToken> = Vec::new();
183 let mut sources: Vec<CancelToken> = Vec::new();
184
185 for s in spec {
186 match s {
187 Src::Fresh => sources.push(CancelToken::never()),
188 Src::ChildOf(idx) => {
189 while parents.len() <= *idx {
190 parents.push(CancelToken::root());
191 }
192 sources.push(parents[*idx].child());
193 }
194 Src::PreCancelled => {
195 let tok = CancelToken::never();
196 tok.cancel();
197 sources.push(tok);
198 }
199 }
200 }
201
202 let group = CancelGroup::new(sources.clone());
203 Setup {
204 group,
205 parents,
206 sources,
207 }
208 }
209
210 fn fire(act: &Act, s: &Setup) {
211 match act {
212 Act::Source(i) => s.sources[*i].cancel(),
213 Act::Parent(i) => s.parents[*i].cancel(),
214 Act::None => {}
215 }
216 }
217
218 macro_rules! sync_cancel_tests {
219 ($($name:ident: $spec:expr, $action:expr, $expected:expr;)*) => {
220 $(
221 #[kithara::test(timeout(Duration::from_secs(5)))]
222 fn $name() {
223 let s = build(&$spec);
224 fire(&$action, &s);
225 assert_eq!(s.group.is_cancelled(), $expected);
226 }
227 )*
228 }
229 }
230
231 sync_cancel_tests! {
232 two_fresh_cancel_first:
233 [Src::Fresh, Src::Fresh], Act::Source(0), true;
234 two_fresh_cancel_second:
235 [Src::Fresh, Src::Fresh], Act::Source(1), true;
236 single_cancel:
237 [Src::Fresh], Act::Source(0), true;
238 two_fresh_no_cancel:
239 [Src::Fresh, Src::Fresh], Act::None, false;
240 pre_cancelled_plus_fresh:
241 [Src::PreCancelled, Src::Fresh], Act::None, true;
242 fresh_and_child_cancel_fresh:
243 [Src::Fresh, Src::ChildOf(0)], Act::Source(0), true;
244 fresh_and_child_cancel_parent:
245 [Src::Fresh, Src::ChildOf(0)], Act::Parent(0), true;
246 two_children_same_parent_cancel_parent:
247 [Src::ChildOf(0), Src::ChildOf(0)], Act::Parent(0), true;
248 two_children_diff_parents_cancel_first:
249 [Src::ChildOf(0), Src::ChildOf(1)], Act::Parent(0), true;
250 two_children_diff_parents_cancel_second:
251 [Src::ChildOf(0), Src::ChildOf(1)], Act::Parent(1), true;
252 two_children_diff_parents_no_cancel:
253 [Src::ChildOf(0), Src::ChildOf(1)], Act::None, false;
254 mixed_with_pre_cancelled:
255 [Src::Fresh, Src::ChildOf(0), Src::PreCancelled], Act::None, true;
256 }
257
258 macro_rules! async_cancel_tests {
259 ($($name:ident: $spec:expr, $action:expr;)*) => {
260 $(
261 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
262 async fn $name() {
263 let s = build(&$spec);
264 let group2 = s.group.clone();
265 let handle = spawn(async move { group2.cancelled().await });
266
267 task::yield_now().await;
268
269 assert!(!s.group.is_cancelled(), "must not be cancelled before action");
270 fire(&$action, &s);
271
272 tokio_time::timeout(Duration::from_secs(2), handle)
273 .await
274 .expect("BUG: cancelled() must resolve within the test timeout")
275 .expect("BUG: spawned cancellation task must not panic");
276 }
277 )*
278 }
279 }
280
281 async_cancel_tests! {
282 async_two_fresh_cancel_first:
283 [Src::Fresh, Src::Fresh], Act::Source(0);
284 async_two_fresh_cancel_second:
285 [Src::Fresh, Src::Fresh], Act::Source(1);
286 async_single_cancel:
287 [Src::Fresh], Act::Source(0);
288 async_fresh_and_child_cancel_parent:
289 [Src::Fresh, Src::ChildOf(0)], Act::Parent(0);
290 async_two_children_same_parent:
291 [Src::ChildOf(0), Src::ChildOf(0)], Act::Parent(0);
292 async_two_children_diff_parents_cancel_first:
293 [Src::ChildOf(0), Src::ChildOf(1)], Act::Parent(0);
294 async_two_children_diff_parents_cancel_second:
295 [Src::ChildOf(0), Src::ChildOf(1)], Act::Parent(1);
296 }
297
298 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
299 async fn cancelled_resolves_immediately_when_pre_cancelled() {
300 let tok = CancelToken::never();
301 tok.cancel();
302 let group = CancelGroup::new(vec![tok, CancelToken::never()]);
303
304 tokio_time::timeout(Duration::from_secs(1), group.cancelled())
305 .await
306 .expect("BUG: cancelled() must return immediately for a pre-cancelled source");
307 }
308
309 #[kithara::test(timeout(Duration::from_secs(5)))]
310 fn empty_group_is_not_cancelled() {
311 let group = CancelGroup::new(vec![]);
312 assert!(!group.is_cancelled());
313 }
314
315 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
316 async fn empty_group_cancelled_never_resolves() {
317 let group = CancelGroup::new(vec![]);
318 let result = tokio_time::timeout(Duration::from_millis(50), group.cancelled()).await;
319 assert!(
320 result.is_err(),
321 "cancelled() on empty group must not resolve"
322 );
323 }
324
325 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
326 async fn clone_observes_same_cancellation() {
327 let tok = CancelToken::never();
328 let group = CancelGroup::new(vec![tok.clone()]);
329 let cloned = group.clone();
330
331 tok.cancel();
332 assert!(group.is_cancelled());
333 assert!(cloned.is_cancelled());
334 }
335
336 #[kithara::test(timeout(Duration::from_secs(5)))]
337 fn ptr_eq_via_partial_eq() {
338 let tok = CancelToken::never();
340 let group = CancelGroup::from(tok.clone());
341 let cloned = group.clone();
342 let rebuilt = CancelGroup::from(tok);
343 assert_eq!(group, cloned);
344 assert_ne!(group, rebuilt);
345 }
346
347 #[kithara::test(timeout(Duration::from_secs(5)))]
348 fn token_bitor_token() {
349 let a = CancelToken::never();
350 let b = CancelToken::never();
351 let group = CancelGroup::from(a.clone()) | b.clone();
352
353 assert!(!group.is_cancelled());
354 a.cancel();
355 assert!(group.is_cancelled());
356 }
357
358 #[kithara::test(timeout(Duration::from_secs(5)))]
359 fn group_bitor_token() {
360 let a = CancelToken::never();
361 let b = CancelToken::never();
362 let group = CancelGroup::from(a.clone()) | b.clone();
363
364 assert!(!group.is_cancelled());
365 b.cancel();
366 assert!(group.is_cancelled());
367 }
368
369 #[kithara::test(timeout(Duration::from_secs(5)))]
370 fn token_bitor_group() {
371 let a = CancelToken::never();
372 let b = CancelToken::never();
373 let group = a.clone() | CancelGroup::from(b.clone());
374
375 assert!(!group.is_cancelled());
376 a.cancel();
377 assert!(group.is_cancelled());
378 }
379
380 #[kithara::test(timeout(Duration::from_secs(5)))]
381 fn group_bitor_group() {
382 let a = CancelToken::never();
383 let b = CancelToken::never();
384 let g1 = CancelGroup::from(a.clone());
385 let g2 = CancelGroup::from(b.clone());
386 let merged = g1 | g2;
387
388 assert!(!merged.is_cancelled());
389 b.cancel();
390 assert!(merged.is_cancelled());
391 }
392
393 #[kithara::test(timeout(Duration::from_secs(5)))]
394 fn chained_bitor() {
395 let a = CancelToken::never();
396 let b = CancelToken::never();
397 let c = CancelToken::never();
398 let group = CancelGroup::from(a.clone()) | b.clone() | c.clone();
399
400 assert!(!group.is_cancelled());
401 c.cancel();
402 assert!(group.is_cancelled());
403 }
404
405 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
406 async fn bitor_async_cancelled() {
407 let a = CancelToken::never();
408 let b = CancelToken::never();
409 let group = CancelGroup::from(a.clone()) | b.clone();
410
411 let g2 = group.clone();
412 let handle = spawn(async move { g2.cancelled().await });
413 task::yield_now().await;
414
415 assert!(!group.is_cancelled());
416 b.cancel();
417
418 tokio_time::timeout(Duration::from_secs(2), handle)
419 .await
420 .expect("BUG: cancelled() must resolve once one source has cancelled")
421 .expect("BUG: spawned task awaiting cancellation must not panic");
422 }
423}