use std::future::Future;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use sqlx::pool::PoolConnection;
use sqlx::postgres::PgConnectOptions;
use sqlx::{Database, PgConnection, PgPool, Postgres, TransactionManager};
use tokio::sync::{Mutex, MutexGuard};
type Tm = <Postgres as Database>::TransactionManager;
tokio::task_local! {
static WRITE_TX: Arc<WriteTx>;
}
pub struct WriteTx {
conn: Arc<Mutex<PoolConnection<Postgres>>>,
owned_pool: Option<Arc<PgConnectOptions>>,
}
impl WriteTx {
pub async fn conn(&self) -> WriteTxConn<'_> {
WriteTxConn(self.conn.lock().await)
}
fn accepts(&self, pool: &PgPool) -> bool {
match &self.owned_pool {
None => true,
Some(own) => Arc::ptr_eq(own, &pool.connect_options()),
}
}
}
pub struct WriteTxConn<'a>(MutexGuard<'a, PoolConnection<Postgres>>);
impl Deref for WriteTxConn<'_> {
type Target = PgConnection;
fn deref(&self) -> &PgConnection {
&self.0
}
}
impl DerefMut for WriteTxConn<'_> {
fn deref_mut(&mut self) -> &mut PgConnection {
&mut self.0
}
}
pub fn current_write_tx() -> Option<Arc<WriteTx>> {
WRITE_TX.try_with(|tx| tx.clone()).ok()
}
pub fn in_write_tx_scope() -> bool {
WRITE_TX.try_with(|_| ()).is_ok()
}
pub(crate) fn current_conn() -> Option<Arc<Mutex<PoolConnection<Postgres>>>> {
WRITE_TX.try_with(|tx| tx.conn.clone()).ok()
}
pub async fn in_write_tx<F, T, E>(pool: &PgPool, f: F) -> Result<T, E>
where
F: Future<Output = Result<T, E>>,
E: From<sqlx::Error>,
{
if let Some(ambient) = current_write_tx() {
if ambient.accepts(pool) {
let unit = TxUnit::begin_on(ambient.conn.clone()).await?;
let result = f.await;
return unit.finish(result).await;
}
}
let (holder, owned_pool) = match crate::company_scope::request_conn_only() {
Some(conn) => (conn, None),
None => {
let conn = pool.acquire().await?;
(Arc::new(Mutex::new(conn)), Some(pool.connect_options()))
}
};
let owned = owned_pool.is_some();
let unit = TxUnit::begin_on(holder.clone()).await?;
if owned {
let bound = {
let mut conn = holder.lock().await;
bind_task_scope(&mut conn).await
};
if let Err(e) = bound {
return unit.finish(Err(e.into())).await;
}
}
let tx = Arc::new(WriteTx {
conn: holder,
owned_pool,
});
let result = WRITE_TX.scope(tx, f).await;
unit.finish(result).await
}
#[derive(Debug)]
pub enum WriteTxFailure<E> {
Inner(E),
Db(sqlx::Error),
}
impl<E> From<sqlx::Error> for WriteTxFailure<E> {
fn from(e: sqlx::Error) -> Self {
Self::Db(e)
}
}
pub async fn in_write_tx_result<F, T, E>(pool: &PgPool, f: F) -> Result<T, WriteTxFailure<E>>
where
F: Future<Output = Result<T, E>>,
{
in_write_tx(pool, async { f.await.map_err(WriteTxFailure::Inner) }).await
}
async fn bind_task_scope(conn: &mut PgConnection) -> Result<(), sqlx::Error> {
match crate::org_scope::current_org_scope() {
Some(scope) => crate::org_scope::bind_org_scope_on(conn, &scope).await,
None => crate::company_scope::bind_current_company(conn).await,
}
}
pub(crate) async fn within_savepoint<T, F>(f: F) -> Result<T, sqlx::Error>
where
F: Future<Output = Result<T, sqlx::Error>>,
{
let Some(conn) = current_conn() else {
return f.await;
};
let unit = TxUnit::begin_on(conn).await?;
let result = f.await;
unit.finish(result).await
}
pub(crate) async fn begin_unit(
pool: &PgPool,
use_request_conn: bool,
) -> Result<TxUnit, sqlx::Error> {
if let Some(conn) = current_conn() {
return TxUnit::begin_on(conn).await;
}
if use_request_conn {
if let Some(conn) = crate::company_scope::request_conn_only() {
return TxUnit::begin_on(conn).await;
}
}
let conn = pool.acquire().await?;
let unit = TxUnit::begin_on(Arc::new(Mutex::new(conn))).await?;
let bound = {
let mut conn = unit.conn().await;
crate::company_scope::bind_current_company(&mut conn).await
};
match bound {
Ok(()) => Ok(unit),
Err(e) => Err(unit.finish::<(), sqlx::Error>(Err(e)).await.unwrap_err()),
}
}
pub(crate) struct TxUnit {
holder: Arc<Mutex<PoolConnection<Postgres>>>,
base_depth: usize,
done: bool,
}
impl TxUnit {
async fn begin_on(holder: Arc<Mutex<PoolConnection<Postgres>>>) -> Result<Self, sqlx::Error> {
let base_depth = {
let mut conn = holder.lock().await;
let base = Tm::get_transaction_depth(&conn);
Tm::begin(&mut conn, None).await?;
base
};
Ok(Self {
holder,
base_depth,
done: false,
})
}
pub(crate) async fn conn(&self) -> WriteTxConn<'_> {
WriteTxConn(self.holder.lock().await)
}
pub(crate) async fn finish<T, E>(mut self, result: Result<T, E>) -> Result<T, E>
where
E: From<sqlx::Error>,
{
self.done = true;
let mut conn = self.holder.lock().await;
match result {
Ok(value) => {
if self.base_depth == 0 {
if let Err(e) = sqlx::query("SELECT 1").execute(&mut **conn).await {
rollback_to(&mut conn, self.base_depth).await;
return Err(e.into());
}
}
match Tm::commit(&mut conn).await {
Ok(()) => Ok(value),
Err(e) => {
rollback_to(&mut conn, self.base_depth).await;
Err(e.into())
}
}
}
Err(e) => {
rollback_to(&mut conn, self.base_depth).await;
Err(e)
}
}
}
}
impl Drop for TxUnit {
fn drop(&mut self) {
if self.done {
return;
}
let holder = self.holder.clone();
let base = self.base_depth;
match tokio::runtime::Handle::try_current() {
Ok(runtime) => {
runtime.spawn(async move {
let mut conn = holder.lock().await;
rollback_to(&mut conn, base).await;
});
}
Err(_) => {
if let Ok(mut conn) = holder.try_lock() {
conn.close_on_drop();
}
}
}
}
}
async fn rollback_to(conn: &mut PoolConnection<Postgres>, base: usize) {
while Tm::get_transaction_depth(conn) > base {
if let Err(e) = Tm::rollback(conn).await {
tracing::error!(
target: "backbone_orm::write_tx",
error = %e,
"a write transaction could not be rolled back; closing its connection instead of \
returning it to the pool",
);
conn.close_on_drop();
return;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_write_transaction_outside_a_scope() {
assert!(current_write_tx().is_none());
assert!(!in_write_tx_scope());
assert!(current_conn().is_none());
}
#[tokio::test]
async fn a_transaction_on_its_own_connection_carries_the_ambient_audit_actor() {
let Ok(dsn) = std::env::var("BACKBONE_ORM_RLS_DSN") else {
eprintln!("skipping: BACKBONE_ORM_RLS_DSN not set");
return;
};
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.connect(&dsn)
.await
.unwrap();
let audit = crate::audit_context::RequestAuditContext::new("tx-actor");
let actor_inside: Result<String, sqlx::Error> =
crate::audit_context::with_request_audit(audit, async {
in_write_tx(&pool, async {
let tx = current_write_tx().expect("a write transaction is active");
let mut conn = tx.conn().await;
sqlx::query_scalar("SELECT coalesce(current_setting('app.actor', true), '')")
.fetch_one(&mut *conn)
.await
})
.await
})
.await;
assert_eq!(actor_inside.unwrap(), "tx-actor");
let actor_after: String =
sqlx::query_scalar("SELECT coalesce(current_setting('app.actor', true), '')")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
actor_after, "",
"the actor must not ride the pooled connection into the next request"
);
}
}