Async closure lifetime problem

I have an async database connection object and want to write a transaction helper function which takes the connection and a closure which contains the actual transaction logic. The transaction function is supposed to repeatedly run the closure until it succeeds.

I have working code which requires the closure's return value to be 'static (except for a reference to the database connection object). In the following code, F is the type of the closure:

use futures::FutureExt;
use std::future::Future;
use std::pin::Pin;

struct DBConnection;
struct DBError;

impl DBConnection {
    async fn query(&'_ mut self, key: &'_ str) -> Result<usize, DBError> {
        Ok(key.len())
    }
}

async fn transaction<T: 'static, F>(connection: &'_ mut DBConnection, fun: F) -> Result<T, DBError>
where
    F: for<'c> Fn(
        &'c mut DBConnection,
    ) -> Pin<Box<dyn Future<Output = Result<Option<T>, DBError>> + 'c>>,
{
    // Retry the transaction function as long as it returns `None`
    loop {
        let response: Option<T> = fun(connection).await?;
        match response {
            None => continue, // Retry the transaction
            Some(value) => {
                return Ok(value);
            }
        }
    }
}

As you can see I'm using an HRTB for<'c> on the closure to be able to express the fact that the returned future is allowed to hold on to a reference to the database connection. This works well.
The problem is that since the closure must be valid for any possible lifetime 'c, it specifically must also be valid for 'static. This means the closure is not allowed to return a Future which references non-'static things (except for the connection).

Take the following example:

async fn test_transaction() {
    let key = "database_key".to_string();
    // The lambda returns a Future which is tied to the lifetime of "key" and
    // "connection", but the output type of that Future is tied to neither
    transaction(&mut DBConnection {}, |connection| {
        // We want to move a reference to "key" into the Future, not "key" itself
        let key = &key;
        async move {
            let value = connection.query(key).await?;
            Ok(Some(value))
        }
        .boxed()
    })
    .await;
}

Because I don't want to clone key for every invocation of the closure, I just move a reference to key into the Future. This of course won't compile since the Future is not valid for any 'c anymore, but only as long as key lives. Is there a solution to this problem? Can I express that whatever Future the closure returns need not be able to live longer than the point transaction.await returns?

Here is a playground link to the above code: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e93955b9796de85dd86fbcb869e2aa84

PS: This is an attempt to port the redis::transaction function to the async setting.

I don't think Rust's syntax is powerful enough to express that — for<'c> doesn't let me add lifetime bounds, and Box won't let me add two lifetimes.

Here's a workaround I found: let transaction take a context that it will pass down to the closure

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0a048e019e56075cb4efb53261e88213

1 Like

That's a nice trick, thanks! I also stumbled over the fact that HRTB cannot be bounded themselves and that you cannot take the union of two lifetimes by doing + 'a + 'b.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.