Lifetime issues in async transaction wrapper with closures in Rust

I’m working on a Rust project using the repository pattern and want to implement a transaction execution wrapper to handle commit/rollback functionality. However, I’m facing difficulties managing lifetimes with futures in closures.

Environment:

  • Rust: rustc 1.82.0 (f6e511eec 2024-10-15)
  • Cargo: cargo 1.82.0 (8f40fc59f 2024-08-21)

Issue:

I get the following lifetime error:

error: lifetime may not live long enough
   --> src/questions.rs:111:22
    |
111 |                 |tx| async move { self.persistence_repository.next_user_id(tx).await },
    |                  --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
    |                  | |
    |                  | return type of closure `{async block@src/questions.rs:111:22: 111:32}` contains a lifetime `'2`
    |                  has type `&'1 mut questions::Transaction<'_>`

Context:

The problem occurs when I pass a closure to the with_transaction function. Here is the simplified reproduction code:

[Reproduction code as provided in the question]

What I’ve Tried:

  • Adjusting the lifetime annotations for Transaction<'a> and the closure.
  • Experimenting with for<'a> in the with_transaction trait method signature.
  • Restructuring the async block in the closure.

Desired Behavior:

I want the transaction wrapper function with_transaction to:

  1. Execute the provided closure (transactional logic) with a mutable transaction reference.
  2. Commit or rollback the transaction based on the closure’s result.

Question:

How can I correctly manage lifetimes in this case to avoid the lifetime may not live long enough error? Is there a better way to design this transaction wrapper to handle async closures cleanly in Rust?

Reproduction code:

use anyhow::Context;
use sqlx::{PgPool, Postgres, Transaction as SqlxTransaction};
use std::future::Future;

pub trait PersistenceRepository {
    async fn next_user_id<'a>(
        &self,
        transaction: &mut Transaction<'a>,
    ) -> Result<String, anyhow::Error>;
}

pub struct AuthService<P, T> {
    persistence_repository: P,
    transaction_factory: T,
}

impl<P: Send + Sync, T: Send + Sync> AuthService<P, T> {
    pub fn new(persistence_repository: P, transaction_factory: T) -> Self {
        Self {
            persistence_repository,
            transaction_factory,
        }
    }
}

impl<P: PersistenceRepository, T: TransactionFactory> AuthService<P, T> {
    pub async fn create_user(&self) -> Result<String, anyhow::Error> {
        self.transaction_factory
            .with_transaction(
                |tx| async move { self.persistence_repository.next_user_id(tx).await },
            )
            .await
    }
}

pub trait TransactionFactory {
    async fn begin_transaction(&self) -> Result<Transaction<'_>, anyhow::Error>;
    async fn commit_transaction(&self, transaction: Transaction<'_>) -> Result<(), anyhow::Error>;
    async fn rollback_transaction(&self, transaction: Transaction<'_>)
        -> Result<(), anyhow::Error>;

    async fn with_transaction<'b, F, Fut, R, E>(&'b self, f: F) -> Result<R, E>
    where
        F: for<'a> FnOnce(&'a mut Transaction<'b>) -> Fut,
        Fut: Future<Output = Result<R, E>>,
        E: From<anyhow::Error>;
}

pub struct Transaction<'a> {
    pub inner: SqlxTransaction<'a, Postgres>,
}

#[derive(Clone)]
pub struct SqlxTransactionFactory {
    pool: PgPool,
}

impl SqlxTransactionFactory {
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

impl TransactionFactory for SqlxTransactionFactory {
    async fn begin_transaction(&self) -> Result<Transaction<'_>, anyhow::Error> {
        let transaction = self
            .pool
            .begin()
            .await
            .context("failed to begin transaction")?;
        Ok(Transaction { inner: transaction })
    }

    async fn commit_transaction(&self, transaction: Transaction<'_>) -> Result<(), anyhow::Error> {
        transaction
            .inner
            .commit()
            .await
            .context("failed to commit transaction")?;
        Ok(())
    }

    async fn rollback_transaction(
        &self,
        transaction: Transaction<'_>,
    ) -> Result<(), anyhow::Error> {
        transaction
            .inner
            .rollback()
            .await
            .context("failed to rollback transaction")?;
        Ok(())
    }

    async fn with_transaction<'b, F, Fut, R, E>(&'b self, f: F) -> Result<R, E>
    where
        F: for<'a> FnOnce(&'a mut Transaction<'b>) -> Fut,
        Fut: Future<Output = Result<R, E>>,
        E: From<anyhow::Error>,
    {
        let mut tx = self.begin_transaction().await.unwrap();
        let result = f(&mut tx).await;

        match result {
            Ok(value) => {
                self.commit_transaction(tx).await;
                Ok(value)
            }
            Err(error) => {
                self.rollback_transaction(tx).await;
                Err(error)
            }
        }
    }
}