Let's say I have a function like below that could fail. The function is also async
async fn can_fail() -> Result<i32, Box<dyn std::error::Error>> {
let mut rng = rand::thread_rng();
let random: u8 = rng.gen();
if random % 2u8 == 0 {
Ok(42)
} else {
Err("error".to_string().into())
}
}
Now I will like to implement a retry function that can be used to retry a function like can_fail.
I came up with this in my attempt
fn retry<F: Fn() -> Result<i32, Box<dyn std::error::Error>>>(f: F, retries: i32) -> Result<i32, Box<dyn std::error::Error>>
{
let mut count = 0;
loop {
let result = f();
if result.is_ok() {
break result;
} else {
if count > retries {
break result
}
count += 1;
}
}
}
Then in my attempt to use, I tried to put can_fail into a closure like this
let my_closure: Box<dyn Fn() -> Result<i32, Box<dyn std::error::Error>>> = Box::new(|| {
can_fail().await
});
But this fails with the error
error[E0728]: `await` is only allowed inside `async` functions and blocks
--> src/main.rs:208:19
|
207 | let my_closure: Box<dyn Fn() -> Result<i32, Box<dyn std::error::Error>>> = Box::new(|| {
| -- this is not `async`
208 | can_fail().await
| ^^^^^^ only allowed inside `async` functions and blocks
So I am kinda stuck. So my question are:
retry I came up with do the job? I cannot tell as I cannot even pass in a closure to itawait is only allowed inside async functions and blocks` error in this scenario?TLDR: on stable Rust a function won't cut it, use a macro instead. On nighty Rust with the async_closure unstable feature you can get away with a function.
Here is a solution on stable Rust using a macro:
macro_rules! retry {
($f:expr, $count:expr, $interval:expr) => {{
let mut retries = 0;
let result = loop {
let result = $f;
if result.is_ok() {
break result;
} else if retries > $count {
break result;
} else {
retries += 1;
tokio::time::sleep(std::time::Duration::from_millis($interval)).await;
}
};
result
}};
($f:expr) => {
retry!($f, 5, 100)
};
}
Compiling example in the playground
The accepted answer suggests to pass an FnMut closure which returns a Future to the retry function. But that approach is limited to futures that don't capture their environment. For instance, this won't compile:
use core::convert::Infallible;
use core::future::Future;
async fn retry<T, E, Fut, F>(retries: usize, mut f: F) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut count = 0;
loop {
let result = f().await;
if result.is_ok() {
return result;
} else {
count += 1;
if count >= retries {
return result;
}
}
}
}
struct Foo {}
impl Foo {
async fn call(&self) -> Result<(), Infallible> {
unimplemented!()
}
async fn call_mut(&mut self) -> Result<(), Infallible> {
unimplemented!()
}
}
async fn do_things() {
let mut foo = Foo {};
// All good
retry(1, || async { foo.call().await }).await.unwrap();
// Boom
retry(1, || async { foo.call_mut().await }).await.unwrap();
}
The problem here is this bit:
|| async { foo.call_mut().await }
We want the closure to be an FnMut so that:
retryasync block. This is what we want here: call_mut requires a mutable reference to foo.However:
error: captured variable cannot escape `FnMut` closure body
--> src/lib.rs:42:17
|
38 | let mut foo = Foo {};
| ------- variable defined here
...
42 | retry(1, || async { foo.call_mut().await }).await.unwrap();
| - ^^^^^^^^---^^^^^^^^^^^^^^^^^^^
| | | |
| | | variable captured here
| | returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
| inferred to be a `FnMut` closure
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
The error message is quite clear: the async block needs a &mut Foo, so foo must be moved into the future, to guarantee the only reference to foo is held by the future. But by moving foo into the future, we move it out of the closure. If this was not the case, we could have a situation where the closure is called multiple times, creating multiple futures holding a &mut Foo, which is invalid in Rust.
Another way to look at it, is that this closure can only be called once. It is an FnOnce, whereas our retry function takes an FnMut.
So the problem is that the compiler wants to prevents us from creating multiple futures that hold the same mutable reference to foo. However, we know that our retry function won't do such thing, since it waits for the future to complete before calling the closure again.
So we could make things work:
let rc_foo = Rc::new(RefCell::new(foo));
retry(1, || async { rc_foo.borrow_mut().call_mut().await })
.await
.unwrap();
Here, we've basically turned an FnOnce into an Fn. But that's arguably quite ugly, and need to be adapted to each case where we want to use retry.
What we need to express is that retry takes a closure that "lends" foo to the Future it returns. When the Future is dropped, foo is released, and the closure can be called again. This kind of LendingFnMut is being discussed and might be implemented one day:
This currently works on rust nightly with the unstable async_closure feature:
#![feature(async_closure)]
use std::ops::AsyncFnMut;
use std::time::Duration;
async fn retry<O, E, F>(mut f: F, retries: i32, interval: Duration) -> Result<O, E>
where
F: AsyncFnMut() -> Result<O, E>,
{
let mut count = 0;
loop {
match f().await {
Ok(output) => break Ok(output),
Err(e) => {
count += 1;
if count == retries {
return Err(e);
}
tokio::time::sleep(interval).await;
}
}
}
}
See a full example in the playground
For more information about async closures, see the announcement of the async closure MVP
The issue we just discussed can simply be avoided by using a macro. This also has the advantage of making the count and interval arguments optional.
use core::convert::Infallible;
macro_rules! retry {
($f:expr, $count:expr, $interval:expr) => {{
let mut retries = 0;
let result = loop {
let result = $f;
if result.is_ok() {
break result;
} else if retries > $count {
break result;
} else {
retries += 1;
tokio::time::sleep(std::time::Duration::from_millis($interval)).await;
}
};
result
}};
($f:expr) => {
retry!($f, 5, 100)
};
}
struct Foo {}
impl Foo {
async fn call(&self) -> Result<(), Infallible> {
unimplemented!()
}
async fn call_mut(&mut self) -> Result<(), Infallible> {
unimplemented!()
}
}
async fn do_things() {
let mut foo = Foo {};
retry!(foo.call().await).unwrap();
retry!(foo.call_mut().await).unwrap();
}
The problem is that this limits the possibilities in terms of control flow, since you cannot have a ? or return in the expression you pass to the macro:
retry! {{
foo.call_mut().await?;
foo.call_mut().await?;
foo.call_mut().await?;
}}.unwrap()
will result in:
error[E0277]: the `?` operator can only be used in an async function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/lib.rs:39:25
|
36 | async fn do_things() {
| ______________________-
37 | | let mut foo = Foo {};
38 | | retry! {{
39 | | foo.call_mut().await?;
| | ^ cannot use the `?` operator in an async function that returns `()`
... |
42 | | }}.unwrap()
43 | | }
| |_- this function should return `Result` or `Option` to accept `?`
So you'll have to do something like this instead:
retry!{
async {
foo.call_mut().await?;
foo.call_mut().await
}.await
};
For more discussion, see this users.rust-lang.org thread
Your retry() function looks OK, but to pass an async function into it you need to fix it to accept a function returning Future, and to be able to .await it, make it async:
async fn retry_async<Fut, F: Fn() -> Fut>(f: F, retries: i32) -> Result<i32, Box<dyn std::error::Error>>
where
Fut: Future<Output = Result<i32, Box<dyn std::error::Error>>>,
{
let mut count = 0;
loop {
let result = f().await;
if result.is_ok() {
break result;
} else {
if count > retries {
break result;
}
count += 1;
}
}
}
retry(can_fail, 3).await.expect("failed to many times");
You can also make the function more generic by:
FnMut instead of Fn.async fn retry_async<T, E, Fut, F: FnMut() -> Fut>(mut f: F, retries: i32) -> Result<T, E>
where
Fut: Future<Output = Result<T, E>>,
{
let mut count = 0;
loop {
let result = f().await;
if result.is_ok() {
break result;
} else {
if count > retries {
break result;
}
count += 1;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With