TLDR: a function won't cut it, use a macro instead:
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
Using a function
Limitations
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:
- it can be called multiple times within
retry
- it can mutate its environment, which in theory means we can use mutable variables in the
async
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
.
Making it work anyway
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
.
The language fix
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:
Macros to the rescue
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