Skip to main content

unb_runtime/
cancellation.rs

1use 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}