use std::fmt;
use std::mem::{self, ManuallyDrop};
use std::sync::{Mutex, MutexGuard, PoisonError};
use sqlx::Database;
use super::tx::Lender;
use crate::inbox::tx::PoolTx;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TxSlot {
index: usize,
generation: u64,
}
pub(crate) struct TxBook<DB: Database> {
slots: Mutex<TxSlots<DB>>,
}
struct TxSlots<DB: Database> {
entries: Vec<TxEntry<DB>>,
free: Vec<usize>,
}
struct TxEntry<DB: Database> {
generation: u64,
standing: TxStanding<DB>,
}
enum TxStanding<DB: Database> {
Free,
Held(Returned<DB>),
Lent,
}
pub(crate) struct Returned<DB: Database> {
pub(crate) tx: PoolTx<DB>,
pub(crate) panicked: bool,
}
impl<DB: Database> TxBook<DB> {
pub(crate) fn leak() -> &'static Self {
Box::leak(Box::new(Self {
slots: Mutex::new(TxSlots {
entries: Vec::new(),
free: Vec::new(),
}),
}))
}
pub(crate) fn enter(&'static self, tx: PoolTx<DB>) -> TxHold<DB> {
let mut slots = self.slots();
let TxSlots { entries, free } = &mut *slots;
let reused = free
.pop()
.and_then(|index| Some((index, entries.get_mut(index)?)));
let held = TxStanding::Held(Returned {
tx,
panicked: false,
});
let slot = if let Some((index, entry)) = reused {
entry.generation = entry.generation.wrapping_add(1);
entry.standing = held;
TxSlot {
index,
generation: entry.generation,
}
} else {
entries.push(TxEntry {
generation: 0,
standing: held,
});
TxSlot {
index: entries.len() - 1,
generation: 0,
}
};
drop(slots);
TxHold { book: self, slot }
}
pub(super) fn lend(&self, slot: TxSlot) -> Option<PoolTx<DB>> {
let mut slots = self.slots();
let lent = slots
.entries
.get_mut(slot.index)
.filter(|entry| entry.generation == slot.generation)
.and_then(
|entry| match mem::replace(&mut entry.standing, TxStanding::Lent) {
TxStanding::Held(held) => Some(held.tx),
other => {
entry.standing = other;
None
}
},
);
drop(slots);
lent
}
pub(super) fn give_back(&self, slot: TxSlot, tx: PoolTx<DB>, panicked: bool) {
let mut slots = self.slots();
let refused = match slots.entries.get_mut(slot.index) {
Some(entry)
if entry.generation == slot.generation
&& matches!(entry.standing, TxStanding::Lent) =>
{
entry.standing = TxStanding::Held(Returned { tx, panicked });
None
}
_ => Some(tx),
};
drop(slots);
drop(refused);
}
fn leave(&self, slot: TxSlot) -> Option<Returned<DB>> {
let mut slots = self.slots();
let TxSlots { entries, free } = &mut *slots;
let entry = entries
.get_mut(slot.index)
.filter(|entry| entry.generation == slot.generation)?;
let left = mem::replace(&mut entry.standing, TxStanding::Free);
if matches!(left, TxStanding::Free) {
return None;
}
free.push(slot.index);
drop(slots);
match left {
TxStanding::Held(held) => Some(held),
TxStanding::Free | TxStanding::Lent => None,
}
}
fn slots(&self) -> MutexGuard<'_, TxSlots<DB>> {
self.slots.lock().unwrap_or_else(PoisonError::into_inner)
}
}
pub(crate) struct TxHold<DB: Database> {
book: &'static TxBook<DB>,
slot: TxSlot,
}
impl<DB: Database> TxHold<DB> {
pub(crate) const fn lender(&self) -> Lender<DB> {
Lender::Tx(self.book, self.slot)
}
pub(crate) fn settle(self) -> Option<Returned<DB>> {
let hold = ManuallyDrop::new(self);
hold.book.leave(hold.slot)
}
}
impl<DB: Database> Drop for TxHold<DB> {
fn drop(&mut self) {
drop(self.book.leave(self.slot));
}
}
impl<DB: Database> fmt::Debug for TxHold<DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TxHold")
.field("slot", &self.slot)
.finish_non_exhaustive()
}
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::{Error, Sqlite, SqlitePool};
use super::{Lender, TxBook, TxSlot};
use crate::inbox::tx::PoolTx;
async fn pool() -> Result<SqlitePool, Error> {
SqlitePoolOptions::new()
.max_connections(4)
.connect("sqlite::memory:")
.await
}
fn slot(lender: Lender<Sqlite>) -> TxSlot {
let Lender::Tx(_, slot) = lender else {
panic!("a transaction book lends from its own slots")
};
slot
}
#[tokio::test]
async fn the_handler_borrows_the_transaction_once_and_gives_it_back() -> Result<(), Error> {
let pool = pool().await?;
let book = TxBook::<Sqlite>::leak();
let hold = book.enter(PoolTx::begin(&pool, None).await?);
let lender = hold.lender();
let lent = lender
.lend()
.expect("the book lends the delivery's transaction");
assert!(lender.lend().is_none(), "a second borrower finds it lent");
lent.give_back(false);
let returned = hold.settle().expect("the settlement finds it back");
assert!(!returned.panicked);
returned.tx.rollback().await
}
#[tokio::test]
async fn a_panic_marks_the_transaction_it_gives_back() -> Result<(), Error> {
let pool = pool().await?;
let book = TxBook::<Sqlite>::leak();
let hold = book.enter(PoolTx::begin(&pool, None).await?);
let lent = hold.lender().lend().expect("lends");
lent.give_back(true);
let returned = hold.settle().expect("the settlement finds it back");
assert!(
returned.panicked,
"the settlement discards what the handler wrote"
);
returned.tx.rollback().await
}
#[tokio::test]
async fn a_transaction_kept_past_its_settlement_never_reaches_the_next_delivery()
-> Result<(), Error> {
let pool = pool().await?;
let book = TxBook::<Sqlite>::leak();
let first = book.enter(PoolTx::begin(&pool, None).await?);
let stale = first.lender();
let kept = stale.lend().expect("lends");
assert!(
first.settle().is_none(),
"a settlement finds the transaction still lent"
);
let second = book.enter(PoolTx::begin(&pool, None).await?);
assert_eq!(slot(second.lender()).index, slot(stale).index);
let borrowed = second.lender().lend().expect("lends");
kept.give_back(false);
assert!(
second.lender().lend().is_none(),
"the slot still waits for the second delivery's own transaction"
);
borrowed.give_back(false);
let returned = second
.settle()
.expect("the settlement finds its own transaction");
returned.tx.rollback().await
}
#[tokio::test]
async fn a_hold_dropped_unsettled_frees_its_slot() -> Result<(), Error> {
let pool = pool().await?;
let book = TxBook::<Sqlite>::leak();
let dropped = book.enter(PoolTx::begin(&pool, None).await?);
let stale = dropped.lender();
drop(dropped);
assert!(
stale.lend().is_none(),
"the dropped delivery's transaction ended"
);
let next = book.enter(PoolTx::begin(&pool, None).await?);
assert_eq!(
slot(next.lender()),
TxSlot {
index: slot(stale).index,
generation: slot(stale).generation + 1,
},
"the next delivery takes the slot"
);
let returned = next.settle().expect("held");
returned.tx.rollback().await
}
}