Skip to main content

leviath_runtime/
cancel.rs

1//! A one-shot "stop what you're doing" signal for work already in flight.
2//!
3//! Cancelling an agent sets its status, which stops the dispatch systems from
4//! starting *new* work - but an inference request or tool batch handed to the
5//! async lanes before that keeps running to completion. For a stalled provider
6//! call that is up to the job timeout; for a tool batch there is no bound at
7//! all, and the batch holds tool-lane capacity the whole time.
8//!
9//! A [`CancelToken`] is handed to each async job when it is dispatched and kept
10//! on the agent, so cancelling the agent drops the in-flight future at its next
11//! await point: the HTTP request is aborted, the pool permit and lane capacity
12//! are released, and the result is never applied.
13//!
14//! This is deliberately a few lines rather than a dependency: the whole contract
15//! is "set a flag once, wake anyone waiting".
16
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, Ordering};
19
20use tokio::sync::Notify;
21
22#[derive(Default)]
23struct Inner {
24    cancelled: AtomicBool,
25    notify: Notify,
26}
27
28/// A cloneable cancellation signal. Every clone observes the same state, and
29/// cancelling is idempotent - a token that fires twice (say, a cancel racing a
30/// reap) is not an error.
31#[derive(Clone, Default)]
32pub struct CancelToken {
33    inner: Arc<Inner>,
34}
35
36impl CancelToken {
37    /// A fresh, un-cancelled token.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Fire the signal, waking every waiter. Idempotent.
43    pub fn cancel(&self) {
44        self.inner.cancelled.store(true, Ordering::SeqCst);
45        self.inner.notify.notify_waiters();
46    }
47
48    /// Whether this token has already fired.
49    pub fn is_cancelled(&self) -> bool {
50        self.inner.cancelled.load(Ordering::SeqCst)
51    }
52
53    /// Resolve once the token fires, immediately if it already has.
54    ///
55    /// The waiter is armed *before* the flag is re-checked. `notify_waiters`
56    /// only wakes waiters registered at the time it runs, so checking first and
57    /// waiting second would lose a cancel that landed in between - and the job
58    /// would then run to completion despite having been cancelled.
59    pub async fn cancelled(&self) {
60        let notified = self.inner.notify.notified();
61        tokio::pin!(notified);
62        notified.as_mut().enable();
63        if self.is_cancelled() {
64            return;
65        }
66        notified.await;
67    }
68}
69
70impl std::fmt::Debug for CancelToken {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("CancelToken")
73            .field("cancelled", &self.is_cancelled())
74            .finish()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use std::time::Duration;
82
83    #[tokio::test]
84    async fn cancelled_resolves_when_the_token_fires() {
85        let token = CancelToken::new();
86        assert!(!token.is_cancelled());
87
88        let waiter = tokio::spawn({
89            let token = token.clone();
90            async move { token.cancelled().await }
91        });
92        // Let the waiter arm itself, then fire.
93        tokio::task::yield_now().await;
94        token.cancel();
95
96        tokio::time::timeout(Duration::from_secs(5), waiter)
97            .await
98            .expect("a fired token wakes its waiter")
99            .unwrap();
100        assert!(token.is_cancelled());
101    }
102
103    #[tokio::test]
104    async fn cancelled_returns_immediately_for_an_already_fired_token() {
105        let token = CancelToken::new();
106        token.cancel();
107        tokio::time::timeout(Duration::from_secs(5), token.cancelled())
108            .await
109            .expect("no wait for a token that already fired");
110    }
111
112    /// A cancel concurrent with the wait is still observed. This exercises the
113    /// path but cannot *prove* the arm-then-check ordering: the window it guards
114    /// is a few instructions with no await point in it, so a test cannot land in
115    /// it reliably. The ordering is enforced by construction (see `cancelled`),
116    /// not by this test.
117    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
118    async fn a_cancel_concurrent_with_the_wait_is_observed() {
119        for _ in 0..500 {
120            let token = CancelToken::new();
121            let firing = tokio::spawn({
122                let token = token.clone();
123                async move { token.cancel() }
124            });
125            tokio::time::timeout(Duration::from_secs(5), token.cancelled())
126                .await
127                .expect("the cancel was observed");
128            firing.await.unwrap();
129        }
130    }
131
132    #[tokio::test]
133    async fn cancel_is_idempotent_and_wakes_every_clone() {
134        let token = CancelToken::new();
135        let waiters: Vec<_> = (0..3)
136            .map(|_| {
137                let token = token.clone();
138                tokio::spawn(async move { token.cancelled().await })
139            })
140            .collect();
141        tokio::task::yield_now().await;
142        token.cancel();
143        token.cancel(); // twice is fine
144
145        for waiter in waiters {
146            tokio::time::timeout(Duration::from_secs(5), waiter)
147                .await
148                .expect("every clone observes the cancel")
149                .unwrap();
150        }
151    }
152
153    #[test]
154    fn debug_reports_the_state() {
155        let token = CancelToken::new();
156        assert!(format!("{token:?}").contains("cancelled: false"));
157        token.cancel();
158        assert!(format!("{token:?}").contains("cancelled: true"));
159    }
160}