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}