unb_runtime/
cancellation.rs1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, Mutex, Weak};
7use std::task::{Context, Poll, Waker};
8use std::time::Duration;
9
10use n0_future::task::{spawn, JoinHandle};
11
12#[derive(Debug, Default)]
13struct CancelState {
14 cancelled: AtomicBool,
15 next_waiter: AtomicU64,
16 wakers: Mutex<HashMap<u64, Waker>>,
17 children: Mutex<Vec<Weak<CancelState>>>,
18}
19
20impl CancelState {
21 fn wake_all(&self) {
22 let drained: Vec<Waker> = self
23 .wakers
24 .lock()
25 .unwrap()
26 .drain()
27 .map(|(_, waker)| waker)
28 .collect();
29 for waker in drained {
30 waker.wake();
31 }
32 }
33}
34
35pub struct CancellationToken {
36 state: Arc<CancelState>,
37 timeout_handle: Option<JoinHandle<()>>,
38}
39
40impl Clone for CancellationToken {
41 fn clone(&self) -> Self {
42 Self {
43 state: self.state.clone(),
44 timeout_handle: None,
45 }
46 }
47}
48
49impl Debug for CancellationToken {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("CancellationToken")
52 .field("cancelled", &self.is_cancelled())
53 .finish()
54 }
55}
56
57impl Default for CancellationToken {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl Drop for CancellationToken {
64 fn drop(&mut self) {
65 if let Some(handle) = self.timeout_handle.take() {
66 handle.abort();
67 }
68 }
69}
70
71impl CancellationToken {
72 pub fn new() -> Self {
73 Self {
74 state: Arc::new(CancelState::default()),
75 timeout_handle: None,
76 }
77 }
78
79 pub fn timeout(duration: Duration) -> Self {
80 let mut token = CancellationToken::new();
81 let child = token.clone();
82 token.timeout_handle = Some(spawn(async move {
83 n0_future::time::sleep(duration).await;
84 child.cancel();
85 }));
86 token
87 }
88
89 pub fn is_cancelled(&self) -> bool {
90 self.state.cancelled.load(Ordering::Acquire)
91 }
92
93 pub fn cancel_after(&self, duration: Duration) {
94 let token = self.clone();
95 spawn(async move {
96 n0_future::time::sleep(duration).await;
97 token.cancel();
98 });
99 }
100
101 pub fn cancel(&self) {
102 if self.state.cancelled.swap(true, Ordering::AcqRel) {
103 return;
104 }
105 self.state.wake_all();
106 let mut stack: Vec<Arc<CancelState>> = Self::collect_children(&self.state);
107 while let Some(node) = stack.pop() {
108 if !node.cancelled.swap(true, Ordering::AcqRel) {
109 node.wake_all();
110 stack.extend(Self::collect_children(&node));
111 }
112 }
113 }
114
115 fn collect_children(state: &Arc<CancelState>) -> Vec<Arc<CancelState>> {
116 let mut children = state.children.lock().unwrap();
117 let mut alive = Vec::new();
118 children.retain(|weak| match weak.upgrade() {
119 Some(child) => {
120 alive.push(child);
121 true
122 }
123 None => false,
124 });
125 alive
126 }
127
128 pub fn child_token(&self) -> Self {
129 let child = CancellationToken::new();
130 {
131 let mut children = self.state.children.lock().unwrap();
132 children.retain(|weak| weak.strong_count() > 0);
133 children.push(Arc::downgrade(&child.state));
134 }
135 if self.is_cancelled() {
136 child.cancel();
137 }
138 child
139 }
140
141 pub fn cancelled(&self) -> Cancelled {
142 Cancelled {
143 state: Arc::downgrade(&self.state),
144 id: self.state.next_waiter.fetch_add(1, Ordering::Relaxed),
145 }
146 }
147
148 pub fn drop_guard(&self) -> DropGuard {
149 DropGuard::new(self.clone())
150 }
151}
152
153pub struct DropGuard {
154 token: Option<CancellationToken>,
155}
156
157impl DropGuard {
158 pub fn new(token: CancellationToken) -> Self {
159 Self { token: Some(token) }
160 }
161
162 pub fn disarm(&mut self) {
163 self.token = None;
164 }
165}
166
167impl Drop for DropGuard {
168 fn drop(&mut self) {
169 if let Some(token) = &self.token {
170 token.cancel();
171 }
172 }
173}
174
175pub struct Cancelled {
176 state: Weak<CancelState>,
177 id: u64,
178}
179
180impl Future for Cancelled {
181 type Output = ();
182
183 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
184 let Some(state) = self.state.upgrade() else {
185 return Poll::Ready(());
186 };
187 if state.cancelled.load(Ordering::Acquire) {
188 return Poll::Ready(());
189 }
190 state
191 .wakers
192 .lock()
193 .unwrap()
194 .insert(self.id, cx.waker().clone());
195 if state.cancelled.load(Ordering::Acquire) {
196 Poll::Ready(())
197 } else {
198 Poll::Pending
199 }
200 }
201}
202
203impl Drop for Cancelled {
204 fn drop(&mut self) {
205 if let Some(state) = self.state.upgrade() {
206 state.wakers.lock().unwrap().remove(&self.id);
207 }
208 }
209}
210
211#[derive(thiserror::Error, Debug)]
212pub enum TaskErrors {
213 #[error("task cancelled")]
214 Cancelled,
215}
216
217pub trait FutureExtension: Future + Sized {
218 fn with_cancel(
219 self,
220 cancellation: &CancellationToken,
221 ) -> impl Future<Output = Result<Self::Output, TaskErrors>>;
222}
223
224impl<T: Future> FutureExtension for T {
225 async fn with_cancel(
226 self,
227 cancellation: &CancellationToken,
228 ) -> Result<Self::Output, TaskErrors> {
229 if cancellation.is_cancelled() {
230 return Err(TaskErrors::Cancelled);
231 }
232 let this = std::pin::pin!(self);
233 tokio::select! {
234 output = this => Ok(output),
235 () = cancellation.cancelled() => Err(TaskErrors::Cancelled),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn a_fresh_token_is_not_cancelled_until_cancelled() {
246 let token = CancellationToken::new();
247 assert!(!token.is_cancelled());
248 token.cancel();
249 assert!(token.is_cancelled());
250 }
251
252 #[test]
253 fn cancelling_a_parent_cascades_to_the_whole_subtree() {
254 let parent = CancellationToken::new();
255 let child = parent.child_token();
256 let grandchild = child.child_token();
257 parent.cancel();
258 assert!(child.is_cancelled());
259 assert!(grandchild.is_cancelled());
260 }
261
262 #[test]
263 fn cascade_survives_a_dropped_intermediate_that_is_kept_alive() {
264 let root = CancellationToken::new();
265 let intermediate = root.child_token();
266 let leaf = intermediate.child_token();
267 root.cancel();
268 assert!(
269 leaf.is_cancelled(),
270 "an alive intermediate carries the cascade"
271 );
272 }
273
274 #[test]
275 fn a_child_of_an_already_cancelled_parent_is_born_cancelled() {
276 let parent = CancellationToken::new();
277 parent.cancel();
278 assert!(parent.child_token().is_cancelled());
279 }
280
281 #[test]
282 fn dropped_children_are_pruned_so_the_parent_does_not_grow_unbounded() {
283 let parent = CancellationToken::new();
284 for _ in 0..1000 {
285 let _ = parent.child_token();
286 }
287 assert!(
288 parent.state.children.lock().unwrap().len() <= 1,
289 "dead child weaks are reclaimed"
290 );
291 }
292
293 #[tokio::test]
294 async fn every_concurrent_waiter_on_one_token_wakes_on_cancel() {
295 let token = CancellationToken::new();
296 let waiters: Vec<_> = (0..8)
297 .map(|_| {
298 let token = token.clone();
299 tokio::spawn(async move { token.cancelled().await })
300 })
301 .collect();
302 token.cancel();
303 for waiter in waiters {
304 tokio::time::timeout(std::time::Duration::from_secs(5), waiter)
305 .await
306 .expect("a single AtomicWaker would have starved all but one waiter")
307 .unwrap();
308 }
309 }
310
311 #[tokio::test]
312 async fn a_dropped_waiter_leaves_no_registration_behind() {
313 let token = CancellationToken::new();
314 {
315 let fut = token.cancelled();
316 let _ = futures_util::poll!(std::pin::pin!(fut));
317 }
318 assert!(
319 token.state.wakers.lock().unwrap().is_empty(),
320 "drop deregisters the waker"
321 );
322 }
323
324 #[tokio::test]
325 async fn with_cancel_short_circuits_a_pending_future() {
326 let token = CancellationToken::new();
327 token.cancel();
328 let result = std::future::pending::<()>().with_cancel(&token).await;
329 assert!(matches!(result, Err(TaskErrors::Cancelled)));
330 }
331}