use std::fmt;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering};
use async_trait::async_trait;
use tokio::sync::{Mutex, MutexGuard};
use vantage_core::{Result, VantageError, error};
use super::{Transaction, TxParticipant, WeakTransaction};
const FAILED_STATEMENT: &str = "a statement failed inside the transaction; rolled back";
#[async_trait]
pub trait TxBackend: Send + Sync + 'static {
type Open: Send + 'static;
async fn begin(&self) -> Result<Self::Open>;
async fn commit(&self, open: Self::Open) -> Result<()>;
async fn rollback(&self, open: Self::Open) -> Result<()>;
}
pub struct TxSlot<B: TxBackend> {
backend: B,
slot: Mutex<Option<B::Open>>,
failed: AtomicBool,
closed: AtomicBool,
owner: WeakTransaction,
}
impl<B: TxBackend> fmt::Debug for TxSlot<B> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TxSlot")
.field("failed", &self.failed)
.field("closed", &self.closed)
.finish_non_exhaustive()
}
}
impl<B: TxBackend> TxSlot<B> {
pub fn new(backend: B, owner: &Transaction) -> Self {
Self {
backend,
slot: Mutex::new(None),
failed: AtomicBool::new(false),
closed: AtomicBool::new(false),
owner: owner.downgrade(),
}
}
pub fn transaction(&self) -> Option<Transaction> {
self.owner.upgrade()
}
pub async fn open(&self) -> Result<TxOpen<'_, B::Open>> {
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.backend.begin().await.inspect_err(|_| {
self.failed.store(true, Ordering::SeqCst);
})?;
*slot = Some(begun);
}
Ok(TxOpen(slot))
}
pub async fn watch<R, E>(
&self,
op: impl IntoFuture<Output = std::result::Result<R, E>>,
) -> std::result::Result<R, E> {
let mut armed = Armed {
failed: &self.failed,
settled: false,
};
let out = op.await;
armed.settled = out.is_ok();
out
}
async fn close(&self) -> Option<B::Open> {
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 struct TxOpen<'a, O>(MutexGuard<'a, Option<O>>);
impl<O> Deref for TxOpen<'_, O> {
type Target = O;
fn deref(&self) -> &O {
self.0.as_ref().expect("begun in open()")
}
}
impl<O> DerefMut for TxOpen<'_, O> {
fn deref_mut(&mut self) -> &mut O {
self.0.as_mut().expect("begun in open()")
}
}
#[async_trait]
impl<B: TxBackend> TxParticipant for TxSlot<B> {
async fn commit(&self) -> Result<()> {
let open = self.close().await;
if self.failed.load(Ordering::SeqCst) {
if let Some(open) = open
&& let Err(e) = self.backend.rollback(open).await
{
tracing::warn!("{e}");
}
return Err(error!(FAILED_STATEMENT));
}
let Some(open) = open else {
return Ok(());
};
self.backend.commit(open).await
}
async fn rollback(&self) -> Result<()> {
let Some(open) = self.close().await else {
return Ok(());
};
self.backend.rollback(open).await
}
}