I am trying to create an Axum layer which begins a database transaction, passes into the request handler via the request extension, and then commits or rolls the transaction back once the request is complete. This is similar to what the axum_sqlx_tx crate is doing, but I am using a wrapper around the DB connection pool which allows me to change the DB engine at runtime, rather than use sqlx directly.
I am struggling to get it to compile though. Here is my axum extension:
/*01*/ async fn db_middleware<'pool, 'tx>(
/*02*/ State(state): State<AppState<'pool>>,
/*03*/ mut request: Request,
/*04*/ next: Next,
/*05*/ ) -> Response {
/*06*/ let pool = state.pool.clone();
/*07*/ let f = pool.get_connection();
/*08*/ let conn_res = f.await;
/*09*/ let mut conn = match conn_res {
/*10*/ Ok(conn) => conn,
/*11*/ Err(err) => return AppErrorWrapper(AppError::UnhandledDbError(err)).into_response(),
/*12*/ };
/*13*/ {
/*14*/ let mut tx = match conn.begin().await {
/*15*/ Ok(tx) => tx,
/*16*/ Err(err) => return AppErrorWrapper(AppError::UnhandledDbError(err)).into_response(),
/*17*/ };
/*18*/
/*19*/ request.extensions_mut().insert(&mut tx); // <-- error goes away if I comment this line out
/*20*/
/*21*/ let response = next.run(request).await;
/*22*/
/*23*/ if response.status().is_success() {
/*24*/ if let Err(err) = tx.commit().await {
/*25*/ return AppErrorWrapper(AppError::UnhandledDbError(err)).into_response();
/*26*/ }
/*27*/ } else {
/*28*/ // ignore rollback errors
/*29*/ let _ = tx.rollback().await;
/*30*/ }
/*31*/ response
/*32*/ }
/*33*/}
The errors are related to lifetimes:
- line 19
tx does not live long enough
- on line 6:
borrowed data escapes outside of function - __arg0 escapes the function body here [E0521]
borrowed data escapes outside of function - argument requires that 'pool must outlive 'static
- line 14:
*conn does not live long enough - borrowed value does not live long enough [E0597]
argument requires that
*connis borrowed for 'static
- line 24:
- cannot move out of
tx
because it is borrowed - move out oftx
occurs here
- cannot move out of
These errors all go away if I comment out line 19.
The AppState struct & companion structs / traits are as follows:
pub struct AppState<'pool> {
pub pool: Arc<dyn Pool<'pool> + Send + Sync>,
}
#[async_trait]
pub trait Pool<'pool> {
fn db_pool(&self) -> &DbPool;
async fn get_connection<'conn>(&self) -> Result<TConnection<'conn>, DatabaseError>
where
'pool: 'conn;
}
pub enum DbPool {
Postgres(SqlxPool<SqlxPostgres>),
MySql(SqlxPool<SqlxMySql>),
Sqlite(SqlxPool<SqlxSqlite>),
}
#[async_trait]
pub trait Connection<'conn> {
fn db_connection(&self) -> &DbConnection;
async fn begin<'tx>(&'tx mut self) -> Result<TTransaction<'tx>, DatabaseError>
where
'conn: 'tx;
}
pub enum DbConnection {
Postgres(SqlxPoolConnection<SqlxPostgres>),
MySql(SqlxPoolConnection<SqlxMySql>),
Sqlite(SqlxPoolConnection<SqlxSqlite>),
}
pub type TConnection<'conn> = Box<dyn Connection<'conn> + Send + Sync + 'conn>;
#[async_trait]
pub trait Transaction<'tx> {
fn db_tx(&self) -> &DbTransaction;
fn db_tx_mut(&mut self) -> &mut DbTransaction<'tx>;
async fn commit(self: Box<Self>) -> Result<(), DatabaseError>;
async fn rollback(self: Box<Self>) -> Result<(), DatabaseError>;
}
pub enum DbTransaction<'tx> {
Postgres(SqlxTransaction<'tx, SqlxPostgres>),
MySql(SqlxTransaction<'tx, SqlxMySql>),
Sqlite(SqlxTransaction<'tx, SqlxSqlite>),
}
pub type TTransaction<'tx> = Box<dyn Transaction<'tx> + Send + Sync + 'tx>;
Then with implementations for the different DB types (mostly using sqlx currently), for instance:
pub struct PostgresConnection {
conn: DbConnection,
}
#[async_trait]
impl<'conn> Connection<'conn> for PostgresConnection {
fn db_connection(&self) -> &DbConnection {
&self.conn
}
async fn begin<'tx>(&'tx mut self) -> Result<TTransaction<'tx>, DatabaseError>
where
'conn: 'tx,
{
match &mut self.conn {
DbConnection::Postgres(c) => {
let tx = c.begin().await?;
Ok(Box::new(PostgresTransaction {
transaction: DbTransaction::Postgres(tx),
}))
}
_ => Err(DatabaseError::UnexpectedDatabase),
}
}
}
Am I right in thinking that the error is because I am passing passing a borrowed value into a value which is used in a nested async
block - but all values passed into a nested async
block must be 'static
, because of the possibility the parent async
block may be terminated by the scheduler before the child completes?
What is the best way to work around this (ideally without making all the lifetimes in my structs/traits 'static
?)