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}