Skip to main content

topcoat_core/
abort.rs

1//! Aborting a future early with a value.
2//!
3//! Sometimes work running deep inside a future needs to stop the whole future
4//! and hand a value back to its caller, without threading that value up through
5//! the `Output` type of every intermediate future. This module makes that
6//! possible:
7//!
8//! - [`WatchAbort`] wraps a future and watches a shared [`AbortStore`].
9//! - Any code running inside that future calls [`abort`] to stash a value in the store and stop
10//!   making progress.
11//! - The wrapping [`WatchAbort`] then resolves to [`MaybeAborted::Aborted`] carrying that value,
12//!   dropping the rest of the wrapped future. If no abort happens, it resolves to
13//!   [`MaybeAborted::Completed`] with the future's normal output.
14//!
15//! The value travels as a type-erased `Box<dyn Any>`, so the watcher recovers
16//! the concrete type with [`downcast`](Box::downcast).
17//!
18//! ```rust
19//! # use std::boxed::Box;
20//! # use topcoat_core::abort::{AbortStore, WatchAbort, MaybeAborted, abort};
21//! # async fn example() {
22//! let store = AbortStore::new();
23//! let outcome = WatchAbort::new(&store, async {
24//!     abort(&store, Box::new(42i32)).await;
25//!     unreachable!("the future stops at the abort point");
26//! })
27//! .await;
28//!
29//! match outcome {
30//!     MaybeAborted::Completed(value) => { /* the future finished normally */ }
31//!     MaybeAborted::Aborted(value) => {
32//!         assert_eq!(*value.downcast::<i32>().unwrap(), 42);
33//!     }
34//! }
35//! # }
36//! ```
37
38use std::{
39    any::Any,
40    convert::Infallible,
41    pin::Pin,
42    sync::Mutex,
43    task::{Context, Poll},
44};
45
46use pin_project_lite::pin_project;
47
48/// The outcome of a [`WatchAbort`] future.
49///
50/// Either the wrapped future ran to completion, or it was aborted via [`abort`]
51/// before finishing.
52pub enum MaybeAborted<T> {
53    /// The wrapped future finished normally, producing this output.
54    Completed(T),
55    /// The wrapped future was aborted, carrying the type-erased value passed to
56    /// [`abort`]. Recover the original type with
57    /// [`downcast`](Box::downcast).
58    Aborted(Box<dyn Any>),
59}
60
61/// A one-shot slot shared between a [`WatchAbort`] and the [`abort`] calls
62/// running inside it.
63///
64/// It holds the value handed over by an abort until the watching [`WatchAbort`]
65/// takes it out. Aborting the same store more than once before it is observed is
66/// a bug and panics.
67#[derive(Default)]
68pub struct AbortStore {
69    inner: Mutex<Option<Box<dyn Any + Send + Sync>>>,
70}
71
72impl AbortStore {
73    #[must_use]
74    pub fn new() -> Self {
75        AbortStore::default()
76    }
77
78    fn abort(&self, value: Box<dyn Any + Send + Sync>) {
79        let old = self.inner.lock().unwrap().replace(value);
80        assert!(
81            old.is_none(),
82            "aborted request context that was already aborted"
83        );
84    }
85
86    fn take(&self) -> Option<Box<dyn Any + Send + Sync>> {
87        self.inner.lock().unwrap().take()
88    }
89}
90
91impl std::fmt::Debug for AbortStore {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.debug_struct("AbortStore").finish()
94    }
95}
96
97pin_project! {
98    /// A future that drives `f` to completion while watching `store`.
99    ///
100    /// If anything inside `f` calls [`abort`] on the same store, this future
101    /// resolves to [`MaybeAborted::Aborted`] with the stored value and `f` is
102    /// dropped. Otherwise it resolves to [`MaybeAborted::Completed`] with `f`'s
103    /// output.
104    pub struct WatchAbort<'a, F> {
105        store: &'a AbortStore,
106        #[pin]
107        f: F,
108    }
109}
110
111impl<'a, F> WatchAbort<'a, F> {
112    /// Wrap `f` so that aborts on `store` short-circuit it.
113    pub fn new(store: &'a AbortStore, f: F) -> Self {
114        Self { store, f }
115    }
116}
117
118impl<F> Future for WatchAbort<'_, F>
119where
120    F: Future,
121{
122    type Output = MaybeAborted<<F as Future>::Output>;
123
124    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
125        if let Some(value) = self.store.take() {
126            return Poll::Ready(MaybeAborted::Aborted(value));
127        }
128
129        let this = self.project();
130        match this.f.poll(cx) {
131            Poll::Ready(value) => Poll::Ready(MaybeAborted::Completed(value)),
132            Poll::Pending => Poll::Pending,
133        }
134    }
135}
136
137/// A future that stores `value` into the [`AbortStore`] and then never
138/// completes.
139///
140/// On its first poll it deposits the value and yields, leaving the surrounding
141/// [`WatchAbort`] to observe the abort and resolve. This is the building block
142/// behind [`abort`].
143pub struct Abort<'a> {
144    store: &'a AbortStore,
145    value: Option<Box<dyn Any + Send + Sync>>,
146}
147
148impl<'a> Abort<'a> {
149    /// Create a future that will abort `store` with `value`.
150    pub fn new(store: &'a AbortStore, value: Box<dyn Any + Send + Sync>) -> Self {
151        Self {
152            store,
153            value: Some(value),
154        }
155    }
156}
157
158impl Future for Abort<'_> {
159    type Output = Infallible;
160
161    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
162        self.store.abort(self.value.take().unwrap());
163        cx.waker().wake_by_ref();
164        Poll::Pending
165    }
166}
167
168/// Abort the surrounding [`WatchAbort`] with `value`.
169///
170/// Stashes `value` in `store` and yields so the watching [`WatchAbort`] can pick
171/// it up and resolve to [`MaybeAborted::Aborted`]. This call never returns: the
172/// future it lives in stops at this point and is dropped.
173pub async fn abort(store: &AbortStore, value: Box<dyn Any + Send + Sync>) -> ! {
174    match Abort::new(store, value).await {}
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[tokio::test]
182    async fn completes_with_inner_output() {
183        let store = AbortStore::new();
184        let outcome = WatchAbort::new(&store, async { 7u32 }).await;
185        match outcome {
186            MaybeAborted::Completed(value) => assert_eq!(value, 7),
187            MaybeAborted::Aborted(_) => panic!("expected completion"),
188        }
189    }
190
191    #[tokio::test]
192    async fn aborts_with_value() {
193        let store = AbortStore::new();
194        let outcome = WatchAbort::new(&store, async {
195            abort(&store, Box::new(42i32)).await;
196        })
197        .await;
198        match outcome {
199            MaybeAborted::Aborted(value) => assert_eq!(*value.downcast::<i32>().unwrap(), 42),
200            MaybeAborted::Completed(()) => panic!("expected abort"),
201        }
202    }
203
204    #[tokio::test]
205    #[allow(unreachable_code, reason = "`abort` never returns, by design")]
206    async fn abort_skips_remaining_work() {
207        let store = AbortStore::new();
208        let outcome = WatchAbort::new(&store, async {
209            abort(&store, Box::new("stop".to_string())).await;
210            unreachable!("the future continued past the abort point");
211        })
212        .await;
213        match outcome {
214            MaybeAborted::Aborted(value) => {
215                assert_eq!(*value.downcast::<String>().unwrap(), "stop");
216            }
217            MaybeAborted::Completed(()) => panic!("expected abort"),
218        }
219    }
220
221    #[tokio::test]
222    #[should_panic(expected = "already aborted")]
223    async fn double_abort_panics() {
224        let store = AbortStore::new();
225        store.abort(Box::new(1i32));
226        store.abort(Box::new(2i32));
227    }
228}