use sqlx::{Database, Pool, SqlStr};
use sqlx_core::transaction::TransactionManager;
use super::events::Candidates;
use super::session::{Closing, Session};
use super::{LockBook, LockHold};
use crate::inbox::database::QueueDatabase;
use crate::inbox::engine::{self, Claimed, Claiming, Events, Prepared, Settling};
use crate::inbox::subscriber::Failed;
pub(crate) struct Advised<DB: QueueDatabase, Row: Events<DB>> {
pub(crate) holds: Vec<LockHold<DB>>,
pub(crate) candidates: Candidates<Row::Id>,
pub(crate) taken: Vec<usize>,
}
impl<DB: QueueDatabase, Row: Events<DB>> Default for Advised<DB, Row> {
fn default() -> Self {
Self {
holds: Vec::new(),
candidates: Candidates::default(),
taken: Vec::new(),
}
}
}
pub(crate) async fn claim_advised<DB, Row>(
pool: &Pool<DB>,
book: &'static LockBook<DB>,
cx: &Claiming,
limit: usize,
beside: usize,
rows: &mut Vec<Claimed<Row>>,
advised: &mut Advised<DB, Row>,
) -> Result<(), Failed>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let prepared = &cx.queue.prepared;
let named = |statement: Option<engine::Stmt>, event: &'static str| {
move |source| -> Failed { (statement.map_or(event, |statement| statement.sql), source) }
};
let settling = Settling {
queue: cx.queue,
now: cx.now,
};
let Advised {
holds,
candidates,
taken,
} = advised;
let mut taking = Taking {
rows,
holds,
done: false,
};
let mut session = Session::acquire(pool, book.closing())
.await
.map_err(|source| ("acquire", source))?;
select_candidates::<DB, Row>(&mut session, book, cx, beside, candidates)
.await
.map_err(named(prepared.claim, "claim"))?;
let mut spare = Some(session);
for index in 0..candidates.len() {
let Some((id, key)) = candidates.get(index) else {
continue;
};
let held_already = taken.iter().any(|&earlier| {
candidates
.get(earlier)
.is_some_and(|(_, other)| other == key)
});
if held_already {
continue;
}
if spare.is_none() {
spare = next_session(pool, book.closing()).await;
}
let Some(session) = spare.take() else {
break;
};
let mut locked = book.locked(session, key);
let took = if book.process() {
locked.take_in_process()
} else {
locked.session().set_locked(true);
let took = Row::lock(locked.session().conn(), cx, key)
.await
.map_err(named(prepared.lock, "lock"))?;
if !took {
locked.session().set_locked(false);
}
took
};
if !took {
spare = Some(locked.into_session());
continue;
}
let before = taking.rows.len();
let found = Row::take(locked.session().conn(), cx, id, taking.rows)
.await
.map_err(named(prepared.take, "take"))?;
if !found {
let freed = free_key::<DB, Row>(locked.session(), book, &settling, key).await?;
let session = locked.into_session();
if freed {
spare = Some(session);
} else {
drop(session);
spare = None;
}
continue;
}
let read = taking.rows.len() - before;
if read != 1 {
return Err((
prepared.take.map_or("take", |take| take.sql),
sqlx::Error::Protocol(format!("the take of one candidate read {read} rows")),
));
}
if prepared.transactional {
begin_on(locked.session(), prepared).await?;
}
taking.holds.push(book.enter(key, locked.into_session()));
taken.push(index);
if taking.holds.len() >= limit {
break;
}
}
drop(spare);
taking.done = true;
Ok(())
}
async fn select_candidates<DB, Row>(
session: &mut Session<DB>,
book: &LockBook<DB>,
cx: &Claiming,
beside: usize,
candidates: &mut Candidates<Row::Id>,
) -> Result<(), sqlx::Error>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let unseen = i64::try_from(book.unseen_in_work().saturating_add(beside)).unwrap_or(i64::MAX);
let reach = Claiming {
limit: cx.limit.saturating_add(unseen),
..*cx
};
Row::candidates(session.conn(), &reach, candidates).await
}
async fn next_session<DB: Database>(
pool: &Pool<DB>,
closing: &'static Closing,
) -> Option<Session<DB>> {
if let Some(session) = Session::try_acquire(pool, closing) {
return Some(session);
}
if pool.size() >= pool.options().get_max_connections() {
return None;
}
Session::acquire(pool, closing).await.ok()
}
struct Taking<'a, DB: QueueDatabase, Row: Events<DB>> {
rows: &'a mut Vec<Claimed<Row>>,
holds: &'a mut Vec<LockHold<DB>>,
done: bool,
}
impl<DB: QueueDatabase, Row: Events<DB>> Drop for Taking<'_, DB, Row> {
fn drop(&mut self) {
if !self.done {
self.holds.clear();
self.rows.clear();
}
}
}
async fn free_key<DB, Row>(
session: &mut Session<DB>,
book: &LockBook<DB>,
cx: &Settling,
key: &str,
) -> Result<bool, Failed>
where
DB: QueueDatabase,
Row: Events<DB>,
{
if book.process() {
session.free_in_process();
return Ok(true);
}
let unlocked = Row::unlock(session.conn(), cx, key)
.await
.map_err(|source| {
let unlock = cx.queue.prepared.unlock;
(unlock.map_or("unlock", |unlock| unlock.sql), source)
})?;
if unlocked {
session.set_locked(false);
} else {
tracing::warn!(
target: "ruststream_sqlx",
subscription = cx.queue.name,
table = cx.queue.table,
row = cx.queue.row,
key,
"the unlock of a candidate's key found the session not holding it; the session \
closes",
);
}
Ok(unlocked)
}
async fn begin_on<DB: QueueDatabase>(
session: &mut Session<DB>,
prepared: &Prepared,
) -> Result<(), Failed> {
session.set_open(true);
let begin = prepared.begin_work;
DB::TransactionManager::begin(session.conn(), begin.map(SqlStr::from_static))
.await
.map_err(|source| (begin.unwrap_or("BEGIN"), source))
}