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: Rust Playground
PS: This is an attempt to port the redis::transaction function to the async setting.