Lifetime errors on async function which accepts async bodies

Hi there,

I was trying to implement an async function retry_on_failure that receives a closure future_callback which returns a future. The main purpose of the retry_on_failure is to catch the result returned from the future_callback and if it is an Err variant and the error is known, perform some operation that will prevent the error happening second time and recall the future_callback again. However, I am having some problem with lifetimes since both retry_on_failure and future_callback requires a reference to self.

Here is a playground link that shows the problem with minimal effort. And the code is right below:

use std::future::Future;

struct Client {
    id: i32,
    token: String
}

impl Client {
    async fn retry_on_fail<'a, 'b, T, R, L>(
        &'a mut self,
        future_callback: &(dyn Sync + Fn(&'b Self) -> R),
        login: &(dyn Sync + Fn(i32) -> L)
    ) -> Result<T, &'static str>
    where
        'a: 'b,
        R: Future<Output = Result<T, &'static str>>,
        L: Future<Output = Result<String, &'static str>>,
    {
        let token = match future_callback(self).await {
            Err("retry") => {
                match login(self.id).await {
                    Ok(token) => token,
                    Err(e) => return Err(e),
                }
            },
            res => return res,
        };
        
        self.token = token;
        
        future_callback(self).await
    }
    
    async fn fetch_logs(&self) -> Result<Vec<()>, &'static str> {
        Err("retry")
    }
}

async fn login(id: i32) -> Result<String, &'static str> {
    Ok(format!("New token for {id}"))
}

pub fn main() {
    async {
        let _ = Client { id: 1, token: "Old token".to_string() }
            .retry_on_fail(&|client| client.fetch_logs(), &login)
            .await;
    };
}

The error says that:

   Compiling playground v0.0.1 (/playground)
error[E0506]: cannot assign to `self.token` because it is borrowed
  --> src/main.rs:29:9
   |
19 |         let token = match future_callback(self).await {
   |                                           ---- `self.token` is borrowed here
...
29 |         self.token = token;
   |         ^^^^^^^^^^ `self.token` is assigned to here but it was already borrowed
30 |         
31 |         future_callback(self).await
   |         --------------- borrow later used here

For more information about this error, try `rustc --explain E0506`.
error: could not compile `playground` (bin "playground") due to previous error

When I look the error, I feel like the borrow at line 19 should end before the line 29 since the future constructed in line 19 is also moved and dropped at there. Hence, the mutable borrow at line 29 should not cause a problem for the borrow at line 31.

I have multiple feelings about the error. One is that it is not possible to implement retry_on_failure without passing ownership of the self to the future_callback. Second is that I am missing some lifetime bounds on the retry_on_failure method.

Thirdly, it might be a limitation of the async rust or even a bug. I found this issue on the github which may be related to this one.

I have found a solution that overcomes the lifetime errors in this playground, however, it requires an allocation if Client has additional fields like another String.

Can anyone explain the reasons behind this error or point me out to a solution?

Thanks in advance.

Just tested the sync version of retry_on_failure and it works as expected. I expected that the async version should work similar to its sync version. Here is the link for the sync version.