use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering};
use async_trait::async_trait;
use sqlx::{Database, Pool};
use tokio::sync::{Mutex, MutexGuard};
use vantage_core::{Result, VantageError, error};
use vantage_vista::{Transaction, TxParticipant, WeakTransaction};
const FAILED_STATEMENT: &str = "a statement failed inside the transaction; rolled back";
pub(crate) struct SqlTx<DB: Database> {
pool: Pool<DB>,
begin: &'static str,
slot: Mutex<Option<sqlx::Transaction<'static, DB>>>,
failed: AtomicBool,
closed: AtomicBool,
owner: WeakTransaction,
}
impl<DB: Database> SqlTx<DB> {
pub(crate) fn new(pool: Pool<DB>, begin: &'static str, owner: &Transaction) -> Self {
Self {
pool,
begin,
slot: Mutex::new(None),
failed: AtomicBool::new(false),
closed: AtomicBool::new(false),
owner: owner.downgrade(),
}
}
pub(crate) fn transaction(&self) -> Option<Transaction> {
self.owner.upgrade()
}
pub(crate) async fn connection(&self) -> Result<Open<'_, DB>> {
match self.transaction() {
Some(tx) if !tx.is_finished() => {}
Some(_) => return Err(finished()),
None => {
if let Ok(mut slot) = self.slot.try_lock() {
slot.take();
}
return Err(finished());
}
}
let mut slot = self.slot.lock().await;
if self.closed.load(Ordering::SeqCst) {
return Err(finished());
}
if slot.is_none() {
let begun = self.pool.begin_with(self.begin).await.map_err(|e| {
self.failed.store(true, Ordering::SeqCst);
error!("couldn't begin the transaction", details = e.to_string())
})?;
*slot = Some(begun);
}
Ok(Open(slot))
}
pub(crate) async fn watch<R>(&self, statement: impl Future<Output = Result<R>>) -> Result<R> {
let mut armed = Armed {
failed: &self.failed,
settled: false,
};
let out = statement.await;
armed.settled = out.is_ok();
out
}
async fn close(&self) -> Option<sqlx::Transaction<'static, DB>> {
let mut slot = self.slot.lock().await;
self.closed.store(true, Ordering::SeqCst);
slot.take()
}
}
struct Armed<'a> {
failed: &'a AtomicBool,
settled: bool,
}
impl Drop for Armed<'_> {
fn drop(&mut self) {
if !self.settled {
self.failed.store(true, Ordering::SeqCst);
}
}
}
fn finished() -> VantageError {
error!("transaction finished")
}
pub(crate) struct Open<'a, DB: Database>(MutexGuard<'a, Option<sqlx::Transaction<'static, DB>>>);
impl<DB: Database> Deref for Open<'_, DB> {
type Target = DB::Connection;
fn deref(&self) -> &DB::Connection {
self.0.as_ref().expect("begun in connection()")
}
}
impl<DB: Database> DerefMut for Open<'_, DB> {
fn deref_mut(&mut self) -> &mut DB::Connection {
self.0.as_mut().expect("begun in connection()")
}
}
#[async_trait]
impl<DB: Database> TxParticipant for SqlTx<DB> {
async fn commit(&self) -> Result<()> {
let open = self.close().await;
if self.failed.load(Ordering::SeqCst) {
if let Some(open) = open
&& let Err(e) = open.rollback().await
{
tracing::warn!("transaction rollback failed: {e}");
}
return Err(error!(FAILED_STATEMENT));
}
let Some(open) = open else {
return Ok(());
};
open.commit()
.await
.map_err(|e| error!("transaction commit failed", details = e.to_string()))
}
async fn rollback(&self) -> Result<()> {
let Some(open) = self.close().await else {
return Ok(());
};
open.rollback()
.await
.map_err(|e| error!("transaction rollback failed", details = e.to_string()))
}
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use std::future::pending;
use std::time::Duration;
use sqlx::SqlitePool;
use tokio::time::timeout;
use super::*;
#[tokio::test]
async fn dropped_statement_fails_the_commit() {
let tx = Transaction::new();
let ok = SqlTx::new(
SqlitePool::connect_lazy("sqlite::memory:").unwrap(),
"BEGIN",
&tx,
);
ok.watch(async { Ok(()) }).await.unwrap();
ok.commit().await.unwrap();
let dropped = SqlTx::new(
SqlitePool::connect_lazy("sqlite::memory:").unwrap(),
"BEGIN",
&tx,
);
let cancelled = timeout(
Duration::from_millis(10),
dropped.watch(pending::<Result<()>>()),
);
assert!(cancelled.await.is_err());
let err = dropped.commit().await.unwrap_err();
assert!(err.to_string().contains(FAILED_STATEMENT));
}
}