How to write a retry function in Rust that involves async
Asked Answered
B

2

7

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:

  1. Is the retry I came up with do the job? I cannot tell as I cannot even pass in a closure to it
  2. How do I fix the await is only allowed inside async functions and blocks` error in this scenario?
  3. Also is it possible to make retry maybe more generic? To get started I hard coded the return type of the function to be returned. In real life I would want that to be generic. How can that be achieved?
Bedplate answered 22/2, 2023 at 13:49 Comment(0)
M
6

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:

  • Taking FnMut instead of Fn.
  • Allowing a generic return type.
  • Allowing a generic error type.
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;
        }
    }
}
Mauer answered 22/2, 2023 at 14:0 Comment(0)
K
10

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

Kurzawa answered 22/1 at 11:26 Comment(0)
M
6

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:

  • Taking FnMut instead of Fn.
  • Allowing a generic return type.
  • Allowing a generic error type.
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;
        }
    }
}
Mauer answered 22/2, 2023 at 14:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.