use futures::future::BoxFuture;
use super::Orm;
use super::savepoint::{self, ManagedSavepoint, SharedTransaction};
use crate::post_commit::PostCommitScope;
impl Orm {
#[tracing::instrument(
name = "rullst.orm.transaction.begin",
target = "rullst_orm",
fields(orm.driver = tracing::field::Empty)
)]
pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
Self::record_driver();
let pool = Self::pool()?;
pool.begin().await.map_err(Into::into)
}
#[tracing::instrument(
name = "rullst.orm.transaction",
target = "rullst_orm",
skip(f),
fields(
orm.driver = tracing::field::Empty,
orm.outcome = tracing::field::Empty
)
)]
pub async fn transaction<F, R, E>(f: F) -> Result<R, crate::Error>
where
F: FnOnce(
SharedTransaction,
)
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<R, E>> + Send>>
+ Send,
E: std::fmt::Display,
{
Self::record_driver();
crate::__transaction_access::ensure_allowed()?;
let nested = match crate::CURRENT_TX.try_with(Clone::clone) {
Ok(outer) => ManagedSavepoint::begin(outer.clone())
.await?
.map(|savepoint| (outer, savepoint)),
Err(_) => None,
};
let (transaction, savepoint) = match nested {
Some((outer, savepoint)) => (outer, Some(savepoint)),
None => {
let tx = Self::begin_transaction().await?;
(std::sync::Arc::new(tokio::sync::Mutex::new(Some(tx))), None)
}
};
let post_commit = PostCommitScope::new();
let closure = savepoint::run_level(transaction.clone(), f(transaction.clone()));
let result = match savepoint {
Some(_) => post_commit.scope(closure).await,
None => {
post_commit
.scope(crate::CURRENT_TX.scope(transaction.clone(), closure))
.await
}
};
let (value, failure) = match result {
Ok(value) => (Some(value), None),
Err(error) => (None, Some(error.to_string())),
};
let finished = match savepoint {
Some(savepoint) => finish_savepoint(savepoint, post_commit, failure).await,
None => finish_transaction(transaction, post_commit, failure).await,
};
finished?;
value.ok_or_else(|| {
crate::Error::Internal("managed transaction finished without a value".to_string())
})
}
fn record_driver() {
if let Ok(driver) = Self::try_driver() {
tracing::Span::current().record("orm.driver", driver);
}
}
}
fn record_outcome(outcome: &'static str) {
tracing::Span::current().record("orm.outcome", outcome);
}
fn finish_transaction(
transaction: SharedTransaction,
post_commit: PostCommitScope,
failure: Option<String>,
) -> BoxFuture<'static, Result<(), crate::Error>> {
Box::pin(async move {
let owned = transaction.lock_owned().await.take();
let Some(mut owned) = owned else {
let (outcome, action) = if failure.is_some() {
("rollback_ownership_missing", "rollback")
} else {
("commit_ownership_missing", "commit")
};
record_outcome(outcome);
return Err(crate::Error::Internal(format!(
"managed transaction ownership was removed before automatic {action}"
)));
};
let leaked = match savepoint::unwind_leaked(&mut owned).await {
Ok(leaked) => leaked,
Err(unwind_error) => {
drop(owned);
record_outcome("rollback_failed");
let failure = failure.map_or_else(String::new, |error| format!("{error}; "));
return Err(crate::Error::DatabaseError(format!(
"Transaction failed: {failure}rolling back a nested savepoint left open also failed: {unwind_error}",
)));
}
};
if let Some(error) = failure {
if let Err(rollback_error) = owned.rollback().await {
record_outcome("rollback_failed");
return Err(crate::Error::DatabaseError(format!(
"Transaction failed: {error}; rollback also failed: {rollback_error}",
)));
}
record_outcome("rolled_back");
return Err(crate::Error::DatabaseError(format!(
"Transaction failed: {error}",
)));
}
if leaked {
if let Err(rollback_error) = owned.rollback().await {
record_outcome("rollback_failed");
return Err(crate::Error::DatabaseError(format!(
"a nested savepoint was left open and the transaction rollback failed: {rollback_error}",
)));
}
record_outcome("rolled_back_unbalanced");
return Err(crate::Error::Internal(
"a nested transaction savepoint was left open, so the managed transaction was rolled back instead of committed"
.to_string(),
));
}
if let Err(error) = owned.commit().await {
record_outcome("commit_failed");
return Err(error.into());
}
if let Err(error) = post_commit.commit().await {
record_outcome("committed_post_commit_failed");
return Err(error);
}
record_outcome("committed");
Ok(())
})
}
fn finish_savepoint(
mut savepoint: ManagedSavepoint,
post_commit: PostCommitScope,
failure: Option<String>,
) -> BoxFuture<'static, Result<(), crate::Error>> {
Box::pin(async move {
if let Some(error) = failure {
drop(post_commit);
if let Err(rollback_error) = savepoint.rollback().await {
record_outcome("savepoint_rollback_failed");
return Err(crate::Error::DatabaseError(format!(
"Transaction failed: {error}; savepoint rollback also failed: {rollback_error}",
)));
}
record_outcome("savepoint_rolled_back");
return Err(crate::Error::DatabaseError(format!(
"Transaction failed: {error}",
)));
}
if let Err(error) = savepoint.release().await {
record_outcome("savepoint_release_failed");
return Err(error);
}
if let Err(error) = post_commit.promote_to_parent().await {
record_outcome("savepoint_released_post_commit_failed");
return Err(error);
}
record_outcome("savepoint_released");
Ok(())
})
}