Skip to main content

pistonite_cu/co/
handle.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicU8, Ordering};
3
4use tokio::task::{JoinError, JoinHandle};
5
6use crate::co::{co_util, runtime};
7
8/// Join handle for async task
9///
10/// This is a wrapper around `tokio`'s `JoinHandle` type.
11pub struct Handle<T>(pub(crate) JoinHandle<T>);
12impl<T> Handle<T> {
13    /// Convert this handle into a [`RobustHandle`].
14    /// with a more robust mechanism for aborting.
15    pub fn into_robust(self) -> RobustHandle<T> {
16        self.into()
17    }
18
19    /// Test if the task has finished and is ready to be joined
20    pub fn is_finished(&self) -> bool {
21        self.0.is_finished()
22    }
23
24    /// Abort the task, trying to `join` or `co_join` an aborted
25    /// task (if it's not already completed) will return an error indicating
26    /// it's already aborted.
27    ///
28    /// If the task was completed before joining, however, it may not
29    /// see this abort call. If you always want `join` to indicate
30    /// aborted, see [`RobustHandle`]
31    pub fn abort(&self) {
32        self.0.abort();
33    }
34
35    /// Return a handle to remotely abort the task
36    pub fn abort_handle(&self) -> AbortHandle {
37        self.0.abort_handle()
38    }
39
40    /// Block the current thread to join the task
41    ///
42    /// Panics are caught by the runtime, and will be returned as an Err.
43    ///
44    /// Will error if the task was aborted or panicked.
45    /// If you want to handle the abort, use [`join_maybe_aborted`](Self::join_maybe_aborted)
46    ///
47    /// # Blocking
48    /// **Do not use this in an async context**, since it will block the runtime,
49    /// and will panic if the thread is currently driving IO.
50    /// Use [`co_join().await`](`Self::co_join`) instead.
51    #[inline]
52    pub fn join(self) -> crate::Result<T> {
53        Self::handle_error(runtime::foreground().block_on(self.0))
54    }
55
56    /// Wait for the task asynchronously
57    ///
58    /// Panics are caught by the runtime, and will be returned as an Err.
59    ///
60    /// Will error if the task was aborted or panicked.
61    /// If you want to handle the abort, use [`co_join_maybe_aborted`](Self::join_maybe_aborted)
62    #[inline]
63    pub async fn co_join(self) -> crate::Result<T> {
64        Self::handle_error(self.0.await)
65    }
66
67    /// Like [`join`](Self::join), but returns `None` if the task was aborted.
68    ///
69    /// Panics are caught by the runtime, and will be returned as an Err.
70    ///
71    /// # Blocking
72    /// **Do not use this in an async context**, since it will block the runtime,
73    /// and will panic if the thread is currently driving IO.
74    /// Use [`co_join_maybe_aborted().await`](`Self::co_join_maybe_aborted`) instead.
75    #[inline]
76    pub fn join_maybe_aborted(self) -> crate::Result<Option<T>> {
77        Self::handle_error_maybe_aborted(runtime::foreground().block_on(self.0))
78    }
79
80    /// Like [`co_join`](Self::co_join), but returns `None` if the task was aborted.
81    #[inline]
82    pub async fn co_join_maybe_aborted(self) -> crate::Result<Option<T>> {
83        Self::handle_error_maybe_aborted(self.0.await)
84    }
85
86    #[inline]
87    fn handle_error(e: Result<T, JoinError>) -> crate::Result<T> {
88        match Self::handle_error_maybe_aborted(e) {
89            Ok(Some(x)) => Ok(x),
90            Ok(None) => crate::bail!("aborted"),
91            Err(e) => Err(e),
92        }
93    }
94
95    fn handle_error_maybe_aborted(e: Result<T, JoinError>) -> crate::Result<Option<T>> {
96        let e = match e {
97            Ok(x) => return Ok(Some(x)),
98            Err(e) => e,
99        };
100        co_util::handle_join_error(e)?;
101        Ok(None)
102    }
103}
104
105pub type AbortHandle = tokio::task::AbortHandle;
106
107/// Join handle for async task, like [`Handle`], with a more robust mechanism for aborting.
108///
109/// This is more robust than [`Handle::abort`], that aborting
110/// a completed task is possible as long as it's not joined yet
111/// (even if `join` has already been called)
112pub struct RobustHandle<T> {
113    inner: Handle<T>,
114    aborted: Arc<AtomicU8>,
115}
116impl<T> From<Handle<T>> for RobustHandle<T> {
117    fn from(value: Handle<T>) -> Self {
118        Self {
119            inner: value,
120            aborted: Arc::new(AtomicU8::new(0)),
121        }
122    }
123}
124
125impl<T> RobustHandle<T> {
126    /// Abort the [`RobustHandle`] pointed to by this handle.
127    ///
128    /// Returns if the abort was successful. If `false` is returned,
129    /// that means the task has already been joined.
130    ///
131    /// See documentation [above](RobustHandle) for more detail
132    pub fn abort(&self) -> bool {
133        let ok = RobustAbortHandle::abort_internal(&self.aborted);
134        if ok {
135            self.inner.abort();
136        }
137        ok
138    }
139
140    /// Return a handle to remotely and robustly abort the task
141    pub fn abort_handle(&self) -> RobustAbortHandle {
142        RobustAbortHandle {
143            inner: self.inner.abort_handle(),
144            aborted: Arc::clone(&self.aborted),
145        }
146    }
147
148    /// Block the current thread to join the task
149    ///
150    /// Panics are caught by the runtime, and will be returned as an Err.
151    ///
152    /// Will error if the task was aborted or panicked.
153    /// If you want to handle the abort, use [`join_maybe_aborted`](Self::join_maybe_aborted)
154    ///
155    /// # Blocking
156    /// **Do not use this in an async context**, since it will block the runtime,
157    /// and will panic if the thread is currently driving IO.
158    /// Use [`co_join().await`](`Self::co_join`) instead.
159    #[inline]
160    pub fn join(self) -> crate::Result<T> {
161        match self.join_maybe_aborted()? {
162            Some(x) => Ok(x),
163            None => crate::bail!("aborted"),
164        }
165    }
166
167    /// Wait for the task asynchronously
168    ///
169    /// Panics are caught by the runtime, and will be returned as an Err.
170    ///
171    /// Will error if the task was aborted or panicked.
172    /// If you want to handle the abort, use [`co_join_maybe_aborted`](Self::join_maybe_aborted)
173    #[inline]
174    pub async fn co_join(self) -> crate::Result<T> {
175        match self.co_join_maybe_aborted().await? {
176            Some(x) => Ok(x),
177            None => crate::bail!("aborted"),
178        }
179    }
180
181    /// Like [`join`](Self::join), but returns `None` if the task was aborted.
182    ///
183    /// Panics are caught by the runtime, and will be returned as an Err.
184    ///
185    /// The task may still be completed with a value if it was aborted.
186    /// If you want to access it, see [`join_maybe_aborted_robust`](Self::join_maybe_aborted_robust)
187    ///
188    /// # Blocking
189    /// **Do not use this in an async context**, since it will block the runtime,
190    /// and will panic if the thread is currently driving IO.
191    /// Use [`co_join_maybe_aborted().await`](`Self::co_join_maybe_aborted`) instead.
192    #[inline]
193    pub fn join_maybe_aborted(self) -> crate::Result<Option<T>> {
194        match self.join_maybe_aborted_robust()? {
195            Ok(x) => Ok(Some(x)),
196            Err(_) => Ok(None),
197        }
198    }
199
200    /// Like [`join`](Self::join), but returns `None` if the task was aborted.
201    ///
202    /// Panics are caught by the runtime, and will be returned as an Err.
203    ///
204    /// The task may still be completed with a value if it was aborted.
205    /// If you want to access it, see [`co_join_maybe_aborted_robust`](Self::co_join_maybe_aborted_robust)
206    #[inline]
207    pub async fn co_join_maybe_aborted(self) -> crate::Result<Option<T>> {
208        match self.co_join_maybe_aborted_robust().await? {
209            Ok(x) => Ok(Some(x)),
210            Err(_) => Ok(None),
211        }
212    }
213
214    /// Block the current thread to join the task
215    ///
216    /// Panics are caught by the runtime, and will be returned as an Err.
217    ///
218    /// # Return Value
219    /// The outer `Result` checks if joining is successful, and the inner
220    /// indicates if the task was aborted. If the task was completed before
221    /// it was aborted, the value is also returned.
222    ///
223    /// - Returns `Ok(Ok(T))` if join is successful and task is not aborted
224    /// - Returns `Ok(Err(None))` if join is successful, and task is aborted
225    ///   without finishing
226    /// - Returns `Ok(Err(Some(T)))` if join is successful, and task is aborted,
227    ///   but it was finished anyway.
228    /// - Returns `Err` if join fails.
229    ///
230    /// ```rust
231    /// # use pistonite_cu as cu;
232    /// use std::time::Duration;
233    ///
234    /// let handle = cu::co::spawn(async move {
235    ///     tokio::time::sleep(Duration::from_millis(100)).await;
236    ///     42
237    /// }).into_robust();
238    ///
239    /// std::thread::sleep(Duration::from_millis(200));
240    /// assert!(handle.abort(), "task is not joined yet, so abort is possible");
241    /// match handle.join_maybe_aborted_robust() {
242    ///     Err(e) => panic!("join failed: {e}"),
243    ///     Ok(Ok(x)) => {
244    ///         assert!(false, "abort was called, so it can't return Ok(Ok)")
245    ///     }
246    ///     Ok(Err(Some(x))) => {
247    ///         assert_eq!(x, 42, "abort was called after task has already produced value 42");
248    ///     }
249    ///     Ok(Err(None)) => {
250    ///         assert!(false, "abort was not called when the task was still running ")
251    ///     }
252    /// }
253    /// ```
254    ///
255    /// # Blocking
256    /// Use [`co_join_maybe_aborted_robust().await`](`Self::co_join_maybe_aborted_robust`) instead.
257    pub fn join_maybe_aborted_robust(self) -> crate::Result<Result<T, Option<T>>> {
258        Self::handle_error_maybe_aborted_robust(
259            runtime::foreground().block_on(self.inner.0),
260            &self.aborted,
261        )
262    }
263
264    /// Wait for the task asynchronously
265    ///
266    /// Panics are caught by the runtime, and will be returned as an Err.
267    ///
268    /// # Return Value
269    /// See [`join_maybe_aborted_robust`](Self::join_maybe_aborted_robust)
270    pub async fn co_join_maybe_aborted_robust(self) -> crate::Result<Result<T, Option<T>>> {
271        Self::handle_error_maybe_aborted_robust(self.inner.0.await, &self.aborted)
272    }
273
274    fn handle_error_maybe_aborted_robust(
275        e: Result<T, JoinError>,
276        aborted: &AtomicU8,
277    ) -> crate::Result<Result<T, Option<T>>> {
278        let e = match e {
279            Ok(x) => {
280                if Self::check_aborted(aborted) {
281                    return Ok(Err(Some(x)));
282                } else {
283                    return Ok(Ok(x));
284                }
285            }
286            Err(e) => e,
287        };
288        if Self::check_aborted(aborted) {
289            return Ok(Err(None));
290        }
291        co_util::handle_join_error(e)?;
292        Ok(Err(None))
293    }
294
295    fn check_aborted(aborted: &AtomicU8) -> bool {
296        loop {
297            let status = aborted.load(Ordering::Relaxed);
298            // aborted
299            if status == 1 {
300                return true;
301            }
302            debug_assert_eq!(0, status, "only the join handle can set the status to 2");
303            if aborted
304                .compare_exchange_weak(0, 2, Ordering::Acquire, Ordering::Relaxed)
305                .is_ok()
306            {
307                return false;
308            }
309            std::hint::spin_loop();
310        }
311    }
312}
313
314pub struct RobustAbortHandle {
315    inner: AbortHandle,
316    aborted: Arc<AtomicU8>,
317}
318impl RobustAbortHandle {
319    /// Abort the [`RobustHandle`] pointed to by this handle.
320    ///
321    /// See documentation for [`RobustHandle`] for more details.
322    ///
323    /// Returns if the abort was successful. If `false` is returned,
324    /// that means the task has already been joined.
325    pub fn abort(&self) -> bool {
326        let ok = Self::abort_internal(&self.aborted);
327        if ok {
328            self.inner.abort();
329        }
330        ok
331    }
332
333    fn abort_internal(aborted: &AtomicU8) -> bool {
334        loop {
335            let status = aborted.load(Ordering::Relaxed);
336            // already joined
337            if status == 2 {
338                return false;
339            }
340            // aborted
341            if status != 0 {
342                debug_assert_eq!(status, 1);
343                return true;
344            }
345            if aborted
346                .compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
347                .is_ok()
348            {
349                return true;
350            }
351            std::hint::spin_loop();
352        }
353    }
354}