use std::collections::BTreeMap;
use std::future::Future;
use std::sync::{Arc, Mutex, PoisonError, Weak};
use futures::future::BoxFuture;
use sqlx_core::transaction::TransactionManager;
use crate::database::RullstDatabase;
type Manager = <RullstDatabase as sqlx::Database>::TransactionManager;
type Connection = <RullstDatabase as sqlx::Database>::Connection;
pub(super) type SharedTransaction =
Arc<tokio::sync::Mutex<Option<crate::db::Transaction<'static>>>>;
type Gate = Arc<tokio::sync::Mutex<()>>;
tokio::task_local! {
static LEVEL: Level;
}
struct Level {
transaction: SharedTransaction,
children: Gate,
}
static HANDLE_GATES: Mutex<BTreeMap<usize, Weak<tokio::sync::Mutex<()>>>> =
Mutex::new(BTreeMap::new());
fn handle_gate(transaction: &SharedTransaction) -> Gate {
let key = Arc::as_ptr(transaction).addr();
let mut gates = HANDLE_GATES.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(gate) = gates.get(&key).and_then(Weak::upgrade) {
return gate;
}
gates.retain(|_, gate| gate.strong_count() > 0);
let gate = Gate::default();
gates.insert(key, Arc::downgrade(&gate));
gate
}
fn parent_gate(transaction: &SharedTransaction) -> Gate {
LEVEL
.try_with(|level| {
Arc::ptr_eq(&level.transaction, transaction).then(|| level.children.clone())
})
.ok()
.flatten()
.unwrap_or_else(|| handle_gate(transaction))
}
pub(super) fn run_level<F: Future>(
transaction: SharedTransaction,
future: F,
) -> impl Future<Output = F::Output> {
LEVEL.scope(
Level {
transaction,
children: Gate::default(),
},
future,
)
}
fn current_depth(connection: &Connection) -> usize {
<Manager as TransactionManager>::get_transaction_depth(connection)
}
async fn unwind(connection: &mut Connection, depth: usize) -> Result<(), crate::Error> {
for _ in depth..current_depth(connection) {
<Manager as TransactionManager>::rollback(connection).await?;
}
Ok(())
}
fn queue_unwind(connection: &mut Connection, depth: usize) {
for _ in depth..current_depth(connection) {
<Manager as TransactionManager>::start_rollback(connection);
}
}
pub(super) async fn unwind_leaked(
transaction: &mut crate::db::Transaction<'static>,
) -> Result<bool, crate::Error> {
let leaked = current_depth(transaction) > 1;
unwind(transaction, 1).await?;
Ok(leaked)
}
pub(super) fn release_outside_transaction(
connection: &mut Connection,
_metadata: sqlx::pool::PoolConnectionMetadata,
) -> BoxFuture<'_, Result<bool, sqlx::Error>> {
Box::pin(async move {
if current_depth(connection) == 0 {
return Ok(true);
}
sqlx::Connection::ping(connection).await?;
Ok(current_depth(connection) == 0)
})
}
pub(super) struct ManagedSavepoint {
transaction: SharedTransaction,
depth: usize,
open: bool,
_turn: tokio::sync::OwnedMutexGuard<()>,
}
impl ManagedSavepoint {
pub(super) fn begin(
transaction: SharedTransaction,
) -> BoxFuture<'static, Result<Option<Self>, crate::Error>> {
Box::pin(async move {
let turn = parent_gate(&transaction).lock_owned().await;
let mut savepoint = Self {
transaction: transaction.clone(),
depth: 0,
open: false,
_turn: turn,
};
let mut guard = transaction.lock_owned().await;
let Some(tx) = guard.as_mut() else {
return Ok(None);
};
savepoint.depth = current_depth(tx) + 1;
savepoint.open = true;
<Manager as TransactionManager>::begin(&mut **tx, None).await?;
drop(guard);
Ok(Some(savepoint))
})
}
pub(super) async fn release(&mut self) -> Result<(), crate::Error> {
self.finish(false).await
}
pub(super) async fn rollback(&mut self) -> Result<(), crate::Error> {
self.finish(true).await
}
async fn finish(&mut self, rollback: bool) -> Result<(), crate::Error> {
let mut guard = self.transaction.clone().lock_owned().await;
let Some(tx) = guard.as_mut() else {
self.open = false;
return Err(crate::Error::Internal(
"managed transaction ownership was removed before its nested savepoint finished"
.to_string(),
));
};
let current = current_depth(tx);
if current < self.depth {
self.open = false;
return Err(crate::Error::Internal(
"nested transaction savepoint was already closed by another operation".to_string(),
));
}
let parent = self.depth.saturating_sub(1);
let result = if current > self.depth {
unwind(tx, parent).await.and(Err(crate::Error::Internal(
"nested transaction savepoints were left unbalanced; the nested work was rolled back"
.to_string(),
)))
} else if rollback {
unwind(tx, parent).await
} else {
<Manager as TransactionManager>::commit(&mut **tx)
.await
.map_err(Into::into)
};
if result.is_err() {
queue_unwind(tx, parent);
}
self.open = false;
result
}
}
impl Drop for ManagedSavepoint {
fn drop(&mut self) {
if !self.open {
return;
}
let Ok(mut guard) = self.transaction.try_lock() else {
return;
};
if let Some(tx) = guard.as_mut() {
queue_unwind(tx, self.depth.saturating_sub(1));
}
}
}