Recursive async method causes cycle error

I'm trying to write an async method with a retry logic like this:

async fn find_or_refetch<'a>(
        &'a mut self,
        kid: &'a str,
        i: i32,
    ) -> BoxFuture<'a, Result<(), ()>> {
        async move {
            if let Some(jwk) = self.jwks.find(kid) {
                Ok(())
            } else if i < 2 {
                self.find_or_refetch(kid, i + 1).await.await
            } else {
                Err(())
            }
        }
        .boxed()
    }

But it's causing a hard to understand (and really long) compiler error, here's a piece of it:

error[E0391]: cycle detected when computing type of `auth::<impl at sbox_http/src/auth.rs:41:1: 41:15>::find_or_refetch::{opaque#0}`
  --> sbox_http/src/auth.rs:66:10
   |
66 |     ) -> BoxFuture<'a, Result<(), ()>> {
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: ...which requires borrow-checking `auth::<impl at sbox_http/src/auth.rs:41:1: 41:15>::find_or_refetch`...
  --> sbox_http/src/auth.rs:62:5
   |
62 | /     async fn find_or_refetch<'a>(
63 | |         &'a mut self,
64 | |         kid: &'a str,
65 | |         i: i32,
66 | |     ) -> BoxFuture<'a, Result<(), ()>> {
   | |______________________________________^
note: ...which requires processing `auth::<impl at sbox_http/src/auth.rs:41:1: 41:15>::find_or_refetch`...
  --> sbox_http/src/auth.rs:62:5

There are many more notes:

  • which requires unsaftey-checking
  • which requires building MIR...
  • which requires borrow checking...
  • which requires processing...
  • which requires building THIR...
  • which requires type-checking...
    etc.

Very grateful for any pointers as to what i need to do to make this work? Or is there a better way to write an async method with retry/backoff logic in rust?

You're getting a cycle because async fn() -> T is really just syntax sugar for fn() -> impl Future<Output = T>. In your case, that means borrow checking your function needs to know if your function passes the borrow checker, which is a cycle. Returning a future from an async function is almost always the wrong thing, just convert it to an ordinary function returning a boxed future instead.

2 Likes

Or you can make it regular async fn, and modify the recursive calling code like this:

self.find_or_refetch(...).boxed().await
1 Like

@Aiden2207 & @Hyeonu - Thank you for the swift reply, that indeed solved it!
This now seems to check out with the compiler - seems about right to you?

fn find_or_refetch<'a>(&'a mut self, kid: &'a str, i: i32) -> BoxFuture<Result<&'a Jwk, ()>> {
        if let Some(jwk) = self.jwks.find(kid) {
            async move { Ok(jwk) }.boxed()
        } else if i < 2 {
            self.find_or_refetch(kid, i + 1)
        } else {
            async move { Err(()) }.boxed()
        }
    }

ah I was a bit too quick to think it all wokred :thinking: I need to await another async function within this recursive method. So this is really what I need:

async fn find_or_refetch<'a>(&'a mut self, kid: &'a str, i: i32) -> Result<Jwk, ()> {
        async move {
            if let Some(jwk) = self.jwks.find(kid).cloned() {
                Ok(jwk)
            } else if (i < 2) {
                self.refresh().await;
                self.find_or_refetch(kid, i + 1).await
            } else {
                Err(())
            }
        }
        .boxed()
        .await
    }

but now I'm back at the first error...

The whole point of the suggestion was to not .await the result of the recursive call directly, since that's exactly what is causing the problem. You'd rewrite this as

async fn find_or_refetch<'a>(&'a mut self, kid: &'a str, i: i32) -> Result<Jwk, ()> {
    if let Some(jwk) = self.jwks.find(kid).cloned() {
        Ok(jwk)
    } else if (i < 2) {
        self.refresh().await;
        self.find_or_refetch(kid, i + 1).boxed().await
    } else {
        Err(())
    }
}

@H2CO3 I'm confused :confused: that just seems to cause the exact same compiler error cycle detected.... I'm using rust rustc 1.65.0 (897e37553 2022-11-02).

Will try to setup a playground to reproduce the error.

You're not expected to use both async fn and async block. You're expected to use either the async fn with raw code inside it (like H2CO3 suggested), or the ordinary fn returning BoxFuture (that might be easier, since there's no anonymous types).

2 Likes

Hmm, indeed. Rewriting as a regular function returning BoxFuture and applying the then combinator instead of .await works though: Playground.

1 Like

This finally seems to both compile and do what I need:

fn find_or_refetch<'a>(&'a mut self, kid: &'a str, i: i32) -> BoxFuture<Result<Jwk, ()>> {
        async move {
            if let Some(jwk) = self.jwks.find(kid).cloned() {
                Ok(jwk)
            } else if (i < 2) {
                self.refresh().await;
                self.find_or_refetch(kid, i + 1).await
            } else {
                Err(())
            }
        }
        .boxed()
    }

I indeed got it wrong in defining the function as async as @Cerber-Ursi pointed out.
Thank you all for the great input :slight_smile: I think I almost got it now