Skip to main content

ease_off/
futures.rs

1//! Backoff support for `async`/`await`.
2
3use crate::{EaseOff, Error, ResultWrapper, TimeoutError};
4
5use pin_project::pin_project;
6use std::future::{Future, IntoFuture};
7use std::marker::PhantomPinned;
8use std::pin::Pin;
9use std::task::{ready, Context, Poll};
10use std::time::Instant;
11
12/// Backoff support for `async`/`await`.
13///
14/// ### Note: Behavior at Deadline
15/// Unless otherwise stated, async operations are _not_ cancelled at the [deadline][Self::deadline()]
16/// once they are in-progress.
17///
18/// More specifically, if the deadline elapses after an async operation has begun, i.e.
19/// the future returned by these methods is `.await`ed or polled,
20/// it will be allowed to run to completion.
21///
22/// To cancel an in-progress operation when the deadline elapses,
23/// use [`TryAsync::enforce_deadline_with()`].
24impl<E> EaseOff<E> {
25    /// Attempt an async operation.
26    ///
27    /// The operation is immediately cancelled without being polled
28    /// if the deadline has already elapsed. Otherwise, it is run to completion.
29    ///
30    /// See the note on this impl block for details.
31    pub fn try_async<T, Fut>(&mut self, op: Fut) -> TryAsync<'_, E, impl FnOnce() -> Fut>
32    where
33        Fut: Future<Output = Result<T, E>>,
34    {
35        self.try_async_with(move || op)
36    }
37
38    /// Attempt the async operation returned by the given closure.
39    ///
40    /// This allows for some lazy computation that is not executed if the deadline
41    /// has already elapsed.
42    ///
43    /// The closure is not called if the deadline has elapsed by the time the returned `Future`
44    /// is polled. If the deadline elapses after the operation has begun, it is allowed
45    /// to run to completion.
46    ///
47    /// See the note on this impl block for details.
48    pub fn try_async_with<T, F, Fut>(&mut self, op: F) -> TryAsync<'_, E, F>
49    where
50        F: FnOnce() -> Fut,
51        Fut: Future<Output = Result<T, E>>,
52    {
53        TryAsync { ease_off: self, op }
54    }
55}
56
57/// `.await`able type returned by [`EaseOff::try_async()`] and [`EaseOff::try_async_with()`].
58///
59/// ### Panics
60/// If an async runtime is not available for sleeping between retries.
61///
62/// ### Note: Behavior at Deadline
63/// Unless otherwise stated, async operations are _not_ cancelled at the [deadline][EaseOff::deadline()]
64/// once they are in-progress.
65///
66/// More specifically, if the deadline elapses after an async operation has begun, i.e.
67/// the future returned by these methods is `.await`ed or polled,
68/// it will be allowed to run to completion.
69///
70/// To cancel an in-progress operation when the deadline elapses,
71/// use [`Self::enforce_deadline_with()`].
72#[must_use = "futures do nothing unless `.await`ed or polled"]
73pub struct TryAsync<'a, E, F> {
74    ease_off: &'a mut EaseOff<E>,
75    op: F,
76}
77
78/// [`Future`] returned by [`TryAsync::into_future()`], [`TryAsync::enforce_deadline_with()`].
79///
80/// If the current state of the [`EaseOff`] prescribes a sleep before the next attempt,
81/// the future will not be invoked immediately.
82///
83/// ### Panics
84/// If an async runtime is not available for sleeping between retries.
85#[pin_project]
86pub struct TryAsyncFuture<'a, E, F, Fut> {
87    // Wrapped in `Option` so we can take and subsequently return ownership in `poll()`
88    ease_off: Option<&'a mut EaseOff<E>>,
89    #[pin]
90    op: LazyOp<F, Fut>,
91    #[pin]
92    sleep: Sleep,
93}
94
95#[pin_project(project = LazyOpPinned)]
96enum LazyOp<F, Fut> {
97    NotStarted(Option<F>),
98    Started(#[pin] Fut),
99}
100
101// Tried to make this work with `pin-project-lite`, but couldn't
102#[pin_project(project = SleepPinned)]
103enum Sleep {
104    Unset,
105    Skipped,
106    // #[pin_project] errors if no variant with a field is enabled
107    Forever(#[pin] PhantomPinned),
108    #[cfg(feature = "tokio")]
109    Tokio(#[pin] tokio::time::Sleep),
110    #[cfg(feature = "async-io-2")]
111    AsyncIo2(async_io_2::Timer),
112}
113
114#[pin_project]
115struct Timeout<Fut> {
116    #[pin]
117    sleep: Sleep,
118    #[pin]
119    future: Fut,
120}
121
122impl<'a, T, E, F, Fut> IntoFuture for TryAsync<'a, E, F>
123where
124    F: FnOnce() -> Fut,
125    Fut: Future<Output = Result<T, E>>,
126{
127    type Output = ResultWrapper<'a, T, E>;
128    type IntoFuture = TryAsyncFuture<'a, E, F, Fut>;
129
130    fn into_future(self) -> Self::IntoFuture {
131        TryAsyncFuture {
132            ease_off: Some(self.ease_off),
133            sleep: Sleep::Unset,
134            op: LazyOp::NotStarted(Some(self.op)),
135        }
136    }
137}
138
139impl<'a, T, E, F, Fut> TryAsync<'a, E, F>
140where
141    F: FnOnce() -> Fut,
142    Fut: Future<Output = Result<T, E>>,
143{
144    // TODO: design an API that automatically creates an `E` for convenience/reusability
145    /// Cancel the operation as soon as the [deadline][EaseOff::deadline()] elapses, if set.
146    ///
147    /// The closure will be called to produce the error that will be returned;
148    /// if the operation failed on a previous attempt, that error is included.
149    ///
150    /// ### Panics
151    /// If an async runtime is not available for managing the timeout.
152    ///
153    /// ### Example
154    ///
155    /// ```rust
156    /// # #[tokio::main(flavor = "current_thread")]
157    /// # async fn main() {
158    /// use std::time::Duration;
159    /// use ease_off::EaseOff;
160    ///
161    /// let mut ease_off = EaseOff::start_timeout(Duration::from_secs(5));
162    ///
163    /// let result = ease_off
164    ///     // An async operation that will never complete.
165    ///     .try_async(std::future::pending::<Result<String, String>>())
166    ///     // You may either use the last error (`_e`) or create a new one
167    ///     .enforce_deadline_with(|_e: Option<String>| "deadline elapsed".to_string())
168    ///     .await
169    ///     .or_retry_if(|_e| false);
170    ///
171    /// assert_eq!(result.unwrap_err(), "deadline elapsed");
172    /// # }
173    pub async fn enforce_deadline_with(
174        self,
175        make_error: impl FnOnce(Option<E>) -> E,
176    ) -> ResultWrapper<'a, T, E> {
177        let res = Timeout {
178            sleep: self
179                .ease_off
180                .deadline
181                .map_or(Sleep::Forever(PhantomPinned), Sleep::until),
182            future: (self.op)(),
183        }
184        .await
185        .map_or_else(
186            |_| {
187                Err(Error::TimedOut(TimeoutError {
188                    last_error: make_error(self.ease_off.last_error.take()),
189                }))
190            },
191            |res| res.map_err(Error::MaybeRetryable),
192        );
193
194        self.ease_off.wrap_result(res)
195    }
196}
197
198impl<'a, T, E, F, Fut> Future for TryAsyncFuture<'a, E, F, Fut>
199where
200    F: FnOnce() -> Fut,
201    Fut: Future<Output = Result<T, E>>,
202{
203    type Output = ResultWrapper<'a, T, E>;
204
205    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
206        let mut this = self.project();
207
208        if this.sleep.is_unset() {
209            let ease_off = this
210                .ease_off
211                .as_deref_mut()
212                .expect("BUG: this.ease_off already taken");
213
214            match ease_off.next_retry_at() {
215                Ok(Some(retry_at)) => {
216                    this.sleep.set(Sleep::until(retry_at));
217                }
218                Ok(None) => {
219                    this.sleep.set(Sleep::Skipped);
220                }
221                Err(e) => {
222                    return Poll::Ready(
223                        this.ease_off
224                            .take()
225                            .expect("BUG: this.ease_off already taken")
226                            .wrap_result(Err(e)),
227                    );
228                }
229            }
230        }
231
232        ready!(this.sleep.as_mut().poll(cx));
233
234        let res = ready!(this.op.poll(cx)).map_err(Error::MaybeRetryable);
235
236        Poll::Ready(
237            this.ease_off
238                .take()
239                .expect("BUG: this.ease_off already taken")
240                .wrap_result(res),
241        )
242    }
243}
244
245impl<T, E, F, Fut> Future for LazyOp<F, Fut>
246where
247    F: FnOnce() -> Fut,
248    Fut: Future<Output = Result<T, E>>,
249{
250    type Output = Result<T, E>;
251
252    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
253        loop {
254            match self.as_mut().project() {
255                LazyOpPinned::NotStarted(op) => {
256                    let op = op.take().expect("`op` already taken");
257                    self.set(LazyOp::Started(op()));
258                }
259                LazyOpPinned::Started(fut) => {
260                    return fut.poll(cx);
261                }
262            }
263        }
264    }
265}
266
267impl Sleep {
268    fn until(instant: Instant) -> Self {
269        #[cfg(feature = "tokio")]
270        if tokio::runtime::Handle::try_current().is_ok() {
271            return Self::Tokio(tokio::time::sleep_until(instant.into()));
272        }
273
274        #[cfg(feature = "async-io-2")]
275        {
276            Self::AsyncIo2(async_io_2::Timer::at(instant))
277        }
278
279        #[cfg(not(feature = "async-io-2"))]
280        if cfg!(feature = "tokio") {
281            panic!("no Tokio runtime available")
282        } else {
283            panic!("no async runtime enabled")
284        }
285    }
286
287    fn is_unset(&self) -> bool {
288        matches!(self, Sleep::Unset)
289    }
290}
291
292impl Future for Sleep {
293    type Output = ();
294
295    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
296        match self.project() {
297            SleepPinned::Unset | SleepPinned::Skipped => Poll::Ready(()),
298            SleepPinned::Forever(..) => Poll::Pending,
299            #[cfg(feature = "tokio")]
300            SleepPinned::Tokio(sleep) => sleep.poll(cx),
301            #[cfg(feature = "async-io-2")]
302            SleepPinned::AsyncIo2(sleep) => Pin::new(sleep).poll(cx).map(|_| ()),
303        }
304    }
305}
306
307impl<Fut: Future> Future for Timeout<Fut> {
308    type Output = Result<Fut::Output, ()>;
309
310    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
311        let this = self.project();
312
313        if let Poll::Ready(ready) = this.future.poll(cx) {
314            return Poll::Ready(Ok(ready));
315        }
316
317        if let Poll::Ready(()) = this.sleep.poll(cx) {
318            return Poll::Ready(Err(()));
319        }
320
321        Poll::Pending
322    }
323}