cobalt_async/
try_finally.rs

1use futures::prelude::*;
2use std::panic::{AssertUnwindSafe, UnwindSafe};
3
4/// An approximation of a "try/finally" block for async Result-returning code.
5///
6/// This function can be used to simulate a "try/finally" block that may be
7/// familiar from languages with an exception system. The provided `body` closure
8/// will be called and awaited and its result returned. The provided `finally` closure
9/// will be called and awaited at the completion of the body, regardless of whether
10/// the body returned successfully, returned an error, or panicked.
11///
12/// This pattern can be useful for running cleanup code, for example in cleaning up
13/// after running a test. Rust code would typically use a scope guard with a [`Drop`] impl
14/// for doing such cleanup work, but that doesn't handle code that needs to be async.
15///
16/// # Example
17///
18/// ```
19/// use cobalt_async::try_finally;
20/// use anyhow::Ok;
21///
22/// # tokio_test::block_on(async {
23/// let mut cleaned_up = false;
24///
25/// let res = try_finally(
26///   || async {
27///     Ok("the body ran successfully")
28///   },
29///   || async {
30///     cleaned_up = true;
31///     Ok(())
32///   }
33/// ).await.unwrap();
34///
35/// assert_eq!(res, "the body ran successfully");
36/// assert!(cleaned_up);
37/// # })
38/// ```
39///
40/// If the body closure references state that is not [`UnwindSafe`], you may need
41/// to manually annotate it with [`AssertUnwindSafe`], and ensure that the finally
42/// closure does not manipulate that state in a way that would violate unwind safety.
43///
44/// ```
45/// use cobalt_async::try_finally;
46/// use anyhow::Ok;
47/// use std::cell::RefCell;
48///
49/// # tokio_test::block_on(async {
50/// let mut cleaned_up = false;
51/// let mut data = RefCell::new(0);
52///
53/// // If the body code panics, we promise not to look at `data` in the finally code.
54/// let data = std::panic::AssertUnwindSafe(data);
55///
56/// let res = try_finally(
57///   || async {
58///     Ok(data.replace(42))
59///   },
60///   || async {
61///     cleaned_up = true;
62///     Ok(())
63///   }
64/// ).await.unwrap();
65///
66/// assert_eq!(res, 0);
67/// assert_eq!(data.take(), 42);
68/// assert!(cleaned_up);
69/// # })
70/// ```
71///
72/// # Notes
73///
74/// * As with `Drop`, running the `finally` closure on panic is not guaranteed.
75///   In particular it will not be run when the panic mode is set to abort.
76///
77/// * Panics in the `finally` code will not be caught, and may potentially mask
78///   panics from the `body` code.
79///
80/// * Errors returned by the `finally` code will be returned to the caller, potentially
81///   masking errors returned by the `body` code.
82///
83pub async fn try_finally<B, F, BFut, FFut, Out, Err>(body: B, finally: F) -> Result<Out, Err>
84where
85    B: FnOnce() -> BFut,
86    B: UnwindSafe,
87    BFut: Future<Output = Result<Out, Err>>,
88    F: FnOnce() -> FFut,
89    FFut: Future<Output = Result<(), Err>>,
90{
91    // Here we want to call the "body closure" to produce a "body future",
92    // then attempt to run the body-future to completion. Regardless of whether
93    // that succeeds or fails, we want to call a "finally closure" to produce a
94    // "finally future" that we run to completion before returning the result
95    // of the body-future to the caller.
96    //
97    // Importantly, we want to run the finally-future even if the body-future panics,
98    // so that this helper can be conveniently used for writing tests. We therefore need
99    // to be careful about "exception safety" as described in [1].
100    //
101    // This is all safe code, so the key thing we need to ensure here is that
102    // we do not allow the code to observe any state invariants that may have been
103    // left in a broken state by the panic. We reason as follows:
104    //
105    //  * The body-closure is constrained to be `UnwindSafe`, so any outside state
106    //    that it depends on will not have invariants that need to be maintained
107    //    across panics.
108    //
109    //  * The body-future is not necessarily `UnwindSafe`, since it may contain any
110    //    arbitrary data that's owned by the future. To be able to deal with panics
111    //    resulting from polling the body-future, we must wrap it in an `AssertUnwindSafe`,
112    //    making us responsible for preventing anyone from observing it in an invalid state.
113    //
114    //  * If the body-future does panic, we will execute the finally-future and then
115    //    re-trigger the panic. So the only thing that could observe broken invariants
116    //    in the state of the body-future, is the finally-future.
117    //
118    //  * The finally-future can't see any data that's referenced by the body-future, unless
119    //    that data was closed over by both the body-closure and the finally-closure.
120    //
121    //  * But we know that anything closed over by the body-closure is `UnwindSafe`.
122    //
123    //  * So, we know that we can run the finally-future to completion without it observing any
124    //    broken invariants from the panic, and our `AssertUnwindSafe` is sound.
125    //
126    // [1] https://github.com/rust-lang/rfcs/blob/master/text/1236-stabilize-catch-panic.md
127
128    // So, first we run the body code and catch any unwinding panics.
129    // (This won't help if doing an aborting panic, but then there's *nothing* we can do).
130    let body = AssertUnwindSafe(body());
131    let body_res = body.catch_unwind().await;
132
133    // Run the finally code.
134    // Note that this may itself panic, which we do not catch.
135    let finally_res = finally().await;
136
137    // The final result depends on both body behaviour, and finally behaviour.
138    match (body_res, finally_res) {
139        // If the body call panicked, trigger it again after cleanup.
140        (Err(cause), _) => std::panic::resume_unwind(cause),
141
142        // If the cleanup failed, that error takes precedence over the body result.
143        (_, Err(finally_err)) => Err(finally_err),
144
145        // Otherwise, the result from the body is returned.
146        (Ok(res), Ok(())) => res,
147    }
148}
149
150#[cfg(test)]
151mod test {
152
153    use super::*;
154    use anyhow::{bail, Ok};
155
156    #[tokio::test]
157    async fn test_finally_runs_after_success() {
158        let mut cleaned_up = false;
159
160        let res = try_finally(
161            || async { Ok("ok!") },
162            || async {
163                cleaned_up = true;
164                Ok(())
165            },
166        )
167        .await
168        .unwrap();
169
170        assert_eq!(res, "ok!");
171        assert!(cleaned_up);
172    }
173
174    #[tokio::test]
175    async fn test_finally_runs_after_failure() {
176        let mut cleaned_up = false;
177
178        let err = try_finally(
179            || async {
180                bail!("oh no!");
181                // The below is for type inference.
182                #[allow(unreachable_code)]
183                Ok(())
184            },
185            || async {
186                cleaned_up = true;
187                Ok(())
188            },
189        )
190        .await
191        .unwrap_err();
192
193        assert_eq!(err.to_string(), "oh no!");
194        assert!(cleaned_up);
195    }
196
197    #[tokio::test]
198    #[should_panic(expected = "in the cleanup!")]
199    async fn test_finally_runs_after_panic() {
200        try_finally(
201            || async {
202                panic!("at the disco!");
203                // The below is for type inference.
204                #[allow(unreachable_code)]
205                Ok(())
206            },
207            || async {
208                // Panic again in the cleanup code, with a different message.
209                // This lets us use `#[should_panic]` rather than trying to catch
210                // the panic *again* in the test and assert that cleanup code was run.
211                panic!("in the cleanup!")
212            },
213        )
214        .await
215        .unwrap();
216    }
217
218    #[tokio::test]
219    async fn test_finally_errors_are_propagated() {
220        let err = try_finally(
221            || async { Ok("sucess!") },
222            || async { bail!("cleanup failed!") },
223        )
224        .await
225        .unwrap_err();
226
227        assert_eq!(err.to_string(), "cleanup failed!");
228    }
229
230    #[tokio::test]
231    async fn test_finally_errors_mask_body_errors() {
232        // This behaviour is a little unfortunate, but it's not obvious how to
233        // implement something better in a generic way.
234        let err = try_finally(
235            || async {
236                bail!("body failed!");
237                // The below is for type inference.
238                #[allow(unreachable_code)]
239                Ok(())
240            },
241            || async { bail!("cleanup failed!") },
242        )
243        .await
244        .unwrap_err();
245
246        assert_eq!(err.to_string(), "cleanup failed!");
247    }
248}