use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use sqlx::Pool;
use tokio::runtime::Handle;
use tokio::sync::Mutex;
use crate::inbox::FormDialect;
use crate::inbox::database::QueueDatabase;
use crate::inbox::delivery::settle::{Step, run_step};
use crate::inbox::engine::{Claimed, Claiming, Events, Savepoint, Settled, Settling};
use crate::inbox::queue::Queue;
use crate::inbox::subscriber::{Failed, InboxSubscriber, Taken, begin, take_group};
#[cfg(feature = "testing")]
use crate::inbox::testing::off_clock;
use crate::inbox::transactional::InboxMode;
use crate::inbox::tx::PoolTx;
impl<DB, Row, Mode> InboxSubscriber<DB, Row, Mode>
where
DB: QueueDatabase,
Row: Events<DB>,
Mode: InboxMode,
{
pub(crate) async fn end_empty(&self, taken: Taken<DB, Row>) {
let Taken::Locked(tx) = taken else {
return;
};
#[cfg(feature = "testing")]
if self.shared.harness.in_process() {
let _ = off_clock(tx.rollback()).await;
return;
}
let _ = tx.rollback().await;
}
}
pub(crate) async fn claim_locked<DB, Row>(
pool: &Pool<DB>,
cx: &Claiming,
ids: &mut Row::Ids,
rows: &mut Vec<Claimed<Row>>,
) -> Result<PoolTx<DB>, Failed>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let queue = cx.queue;
let claim_failed = |source| -> Failed {
let statement = queue.prepared.claim.map_or("claim", |claim| claim.sql);
(statement, source)
};
let mut tx = begin(pool, queue).await?;
let claimed = async {
if take_group::<DB, Row>(&mut tx, cx, None).await? {
Row::claim(&mut tx, cx, None, ids, rows)
.await
.map_err(claim_failed)?;
}
if let Some(savepoint) = queue.prepared.savepoint
&& !rows.is_empty()
{
DB::execute_text(&mut tx, savepoint.set)
.await
.map_err(|source| (savepoint.set, source))?;
}
Ok::<_, Failed>(())
}
.await;
match claimed {
Ok(()) => Ok(tx),
Err(failed) => {
let _ = tx.rollback().await;
Err(failed)
}
}
}
pub(crate) struct BatchTx<DB: QueueDatabase> {
tx: Mutex<Option<PoolTx<DB>>>,
open: AtomicUsize,
written: AtomicUsize,
failed: AtomicBool,
}
impl<DB: QueueDatabase> BatchTx<DB> {
pub(crate) fn new(tx: PoolTx<DB>, deliveries: usize) -> Arc<Self> {
Arc::new(Self {
tx: Mutex::new(Some(tx)),
open: AtomicUsize::new(deliveries),
written: AtomicUsize::new(0),
failed: AtomicBool::new(false),
})
}
async fn finish(&self, wrote: bool, queue: &'static Queue) -> Result<(), sqlx::Error> {
if wrote {
self.written.fetch_add(1, Ordering::AcqRel);
}
if self.open.fetch_sub(1, Ordering::AcqRel) != 1 {
return Ok(());
}
let Some(tx) = self.tx.lock().await.take() else {
return Ok(());
};
if self.failed.load(Ordering::Acquire) {
self.undone(queue);
return tx.rollback().await;
}
if self.written.load(Ordering::Acquire) > 0 {
tx.commit().await
} else {
tx.rollback().await
}
}
pub(crate) fn release(self: Arc<Self>, queue: &'static Queue) {
if self.open.fetch_sub(1, Ordering::AcqRel) != 1 {
return;
}
let Ok(mut held) = self.tx.try_lock() else {
return;
};
let Some(tx) = held.take() else {
return;
};
drop(held);
let failed = self.failed.load(Ordering::Acquire);
if failed {
self.undone(queue);
}
let commit = !failed && self.written.load(Ordering::Acquire) > 0;
if let Ok(runtime) = Handle::try_current() {
runtime.spawn(async move {
let ended = if commit {
tx.commit().await
} else {
tx.rollback().await
};
if let Err(error) = ended {
tracing::warn!(
target: "ruststream_sqlx",
subscription = queue.name,
table = queue.table,
row = queue.row,
commit,
%error,
"a batch's transaction failed to end after a delivery was dropped \
unsettled; its connection closes",
);
}
});
}
}
fn undone(&self, queue: &'static Queue) {
let undone = self.written.load(Ordering::Acquire);
if undone > 0 {
tracing::warn!(
target: "ruststream_sqlx",
subscription = queue.name,
table = queue.table,
row = queue.row,
undone,
"a settlement of the batch failed, so the batch rolls back: settlements that \
succeeded are undone and their rows return",
);
}
}
fn fail(&self) {
self.failed.store(true, Ordering::Release);
}
fn has_failed(&self) -> bool {
self.failed.load(Ordering::Acquire)
}
}
pub(crate) async fn settle_own<DB, Row>(
mut tx: PoolTx<DB>,
cx: &Settling,
id: &Row::Id,
step: Step,
) -> Result<Settled, sqlx::Error>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let settled = match run_step::<DB, Row>(&mut tx, cx, id, None, step).await {
Ok(settled) => settled,
Err(error) => {
let _ = tx.rollback().await;
return Err(error);
}
};
match settled {
Settled::Written => tx.commit().await?,
Settled::Untouched | Settled::Lost => tx.rollback().await?,
}
Ok(settled)
}
pub(crate) async fn settle_batched<DB, Row>(
batch: &BatchTx<DB>,
cx: &Settling,
id: &Row::Id,
step: Step,
) -> Option<Result<Settled, sqlx::Error>>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let outcome = if batch.has_failed() {
None
} else {
let mut guard = batch.tx.lock().await;
Some(match guard.as_mut() {
Some(tx) => run_step::<DB, Row>(tx, cx, id, None, step).await,
None => Err(sqlx::Error::Protocol(
"the batch's transaction ended before this delivery settled".to_owned(),
)),
})
};
if matches!(outcome, Some(Err(_))) {
batch.fail();
}
let wrote = matches!(outcome, Some(Ok(Settled::Written)));
let finished = batch.finish(wrote, cx.queue).await;
Some(outcome?.and_then(|settled| finished.map(|()| settled)))
}
pub(crate) fn savepoint_of<Mode: InboxMode>(form: &FormDialect) -> Option<Savepoint> {
match form {
FormDialect::RowLock(dialect) if Mode::TRANSACTIONAL => Some(Savepoint {
set: dialect.savepoint(),
rollback_to: dialect.rollback_to_savepoint(),
}),
FormDialect::RowLock(_) | FormDialect::Lease(_) | FormDialect::Advisory(_) => None,
}
}