use std::fmt;
use std::future::{Future, poll_fn};
use std::marker::PhantomData;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use futures::Stream;
use ruststream::{BatchSubscriber, Subscriber};
use ruststream_sqlx_dialect::Role;
use sqlx::Pool;
use sync_wrapper::SyncWrapper;
use tokio::sync::Notify;
use tokio_util::sync::DropGuard;
use super::batch::{BatchClaim, BatchLane};
use super::broker::Shared;
use super::claims::{Claims, Flow, InWork, Settled};
use super::database::QueueDatabase;
use super::delivery::InboxDelivery;
use super::engine::{self, Claimed, Claiming, Events, Leasing, Now};
use super::error::SqlxBrokerError;
use super::form::advisory::claim::{Advised, claim_advised};
use super::form::advisory::{LockBook, LockHold};
use super::form::lease::LeaseBook;
use super::form::lease::claim::{begin_work, claim_leased};
use super::form::row_lock::claim_locked;
use super::queue::{Queue, Registration};
#[cfg(feature = "testing")]
use super::testing::{cancelled, off_clock};
use super::transactional::{InboxMode, Plain, TxBook};
use super::tx::PoolTx;
use crate::inbox::pools::ServicePool;
const CLAIM_RETRY: Duration = Duration::from_secs(1);
pub struct InboxSubscriber<DB: QueueDatabase, Row: Events<DB>, Mode = Plain> {
pub(super) shared: Arc<Shared<DB>>,
queue: &'static Queue,
holding: Holding<DB, Row>,
pool: &'static ServicePool<DB>,
lending: Option<&'static TxBook<DB>>,
claimed: ClaimBuffers<DB, Row>,
wait: Option<Wait>,
flow: Flow,
made: usize,
settled: &'static Settled,
wake: &'static Notify,
_registration: Registration<DB>,
_keeper: Option<DropGuard>,
_mode: PhantomData<fn() -> Mode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Wait {
Interval,
Failed,
}
pub(crate) struct Opened<DB: QueueDatabase, Row: Events<DB>> {
pub(crate) queue: &'static Queue,
pub(crate) holding: Holding<DB, Row>,
pub(crate) registration: Registration<DB>,
pub(crate) keeper: Option<DropGuard>,
pub(crate) pool: &'static ServicePool<DB>,
pub(crate) lending: Option<&'static TxBook<DB>>,
pub(crate) wake: &'static Notify,
}
pub(crate) enum Holding<DB: QueueDatabase, Row: Events<DB>> {
Transaction,
Leases(&'static LeaseBook<DB, Row>),
Advisory(&'static LockBook<DB>),
}
impl<DB: QueueDatabase, Row: Events<DB>> Clone for Holding<DB, Row> {
fn clone(&self) -> Self {
*self
}
}
impl<DB: QueueDatabase, Row: Events<DB>> Copy for Holding<DB, Row> {}
pub(crate) enum Taken<DB: QueueDatabase, Row: Events<DB>> {
Locked(PoolTx<DB>),
Leased(&'static LeaseBook<DB, Row>, Row::Token),
Advised,
}
pub(crate) struct ClaimBuffers<DB: QueueDatabase, Row: Events<DB>> {
rows: Vec<Claimed<Row>>,
ids: Row::Ids,
advised: Option<Box<Advised<DB, Row>>>,
transactions: SyncWrapper<Vec<PoolTx<DB>>>,
}
impl<DB: QueueDatabase, Row: Events<DB>> Default for ClaimBuffers<DB, Row> {
fn default() -> Self {
Self {
rows: Vec::new(),
ids: Row::Ids::default(),
advised: None,
transactions: SyncWrapper::new(Vec::new()),
}
}
}
impl<DB: QueueDatabase, Row: Events<DB>> ClaimBuffers<DB, Row> {
fn clear(&mut self) {
self.rows.clear();
self.transactions.get_mut().clear();
if let Some(advised) = self.advised.as_deref_mut() {
advised.holds.clear();
advised.taken.clear();
}
}
}
impl<DB: QueueDatabase, Row: Events<DB>, Mode> fmt::Debug for InboxSubscriber<DB, Row, Mode> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InboxSubscriber")
.field("subscription", &self.queue.name)
.field("table", &self.queue.table)
.field("row", &self.queue.row)
.finish_non_exhaustive()
}
}
impl<DB, Row, Mode> InboxSubscriber<DB, Row, Mode>
where
DB: QueueDatabase,
Row: Events<DB>,
Mode: InboxMode,
{
pub(crate) fn new(shared: Arc<Shared<DB>>, opened: Opened<DB, Row>) -> Self {
Self {
shared,
queue: opened.queue,
holding: opened.holding,
pool: opened.pool,
lending: opened.lending,
claimed: ClaimBuffers::default(),
wait: None,
flow: Flow::new(
opened.pool.options().get_max_connections(),
!matches!(opened.holding, Holding::Leases(_))
|| opened.queue.prepared.transactional,
one_claim(opened.queue),
),
made: 0,
settled: Box::leak(Box::default()),
wake: opened.wake,
_registration: opened.registration,
_keeper: opened.keeper,
_mode: PhantomData,
}
}
fn failed(&self, statement: &'static str, source: sqlx::Error) -> SqlxBrokerError {
SqlxBrokerError::Sqlx {
subscription: self.queue.name.to_owned(),
table: self.queue.table.to_owned(),
row: self.queue.row,
statement,
source: Box::new(source),
}
}
pub(crate) async fn claim(
&mut self,
limit: usize,
) -> Option<Result<(Taken<DB, Row>, usize), SqlxBrokerError>> {
loop {
match self.wait.take() {
Some(Wait::Interval) => tokio::select! {
() = tokio::time::sleep(self.queue.poll_interval) => {}
() = self.wake.notified() => {}
() = self.shared.stopping.cancelled() => return None,
},
Some(Wait::Failed) => tokio::select! {
() = tokio::time::sleep(CLAIM_RETRY) => {}
() = self.shared.stopping.cancelled() => return None,
},
None => {}
}
if self.shared.is_closed() {
return None;
}
let claimed = self.claim_once(limit).await;
match claimed {
Ok((taken, 0)) => {
self.end_empty(taken).await;
self.wait = Some(Wait::Interval);
}
Ok((taken, count)) => {
self.wait = (count < limit).then_some(Wait::Interval);
return Some(Ok((taken, count)));
}
Err(error) => {
self.wait = Some(Wait::Failed);
return Some(Err(error));
}
}
}
}
async fn claim_once(
&mut self,
limit: usize,
) -> Result<(Taken<DB, Row>, usize), SqlxBrokerError> {
#[cfg(feature = "testing")]
if self.shared.harness.in_process() {
return self.claim_in_process(limit).await;
}
let taken = claim_rows(
&self.shared.pool,
self.queue,
self.holding,
limit,
0,
Now::default(),
&mut self.claimed,
)
.await
.map_err(|(statement, source)| self.failed(statement, source))?;
Ok((taken, self.claimed.rows.len()))
}
#[cfg(feature = "testing")]
async fn claim_in_process(
&mut self,
limit: usize,
) -> Result<(Taken<DB, Row>, usize), SqlxBrokerError> {
let pool = self.shared.pool.clone();
let queue = self.queue;
let holding = self.holding;
let now = self.shared.harness.now();
let mut buffers = std::mem::take(&mut self.claimed);
let (claimed, buffers) = off_clock(async move {
let claimed = claim_rows(&pool, queue, holding, limit, 0, now, &mut buffers).await;
(claimed, buffers)
})
.await
.unwrap_or_else(|| (Err(("BEGIN", cancelled())), ClaimBuffers::default()));
self.claimed = buffers;
let taken = claimed.map_err(|(statement, source)| self.failed(statement, source))?;
self.shared
.harness
.claimed(queue.name, self.claimed.rows.len());
Ok((taken, self.claimed.rows.len()))
}
pub(crate) fn take_rows(&mut self) -> impl Iterator<Item = Claimed<Row>> + '_ {
self.claimed.rows.drain(..)
}
pub(crate) fn take_advised(
&mut self,
) -> impl Iterator<Item = (Claimed<Row>, LockHold<DB>)> + '_ {
let ClaimBuffers { rows, advised, .. } = &mut self.claimed;
let holds = advised
.as_deref_mut()
.map(|advised| advised.holds.drain(..));
rows.drain(..).zip(holds.into_iter().flatten())
}
pub(crate) const fn queue(&self) -> &'static Queue {
self.queue
}
pub(crate) const fn pool(&self) -> &'static ServicePool<DB> {
self.pool
}
fn in_work(&self) -> usize {
self.made.wrapping_sub(self.settled.count())
}
fn one(&self, running: usize) -> ClaimOne<DB, Row> {
ClaimOne {
pool: self.pool,
queue: self.queue,
holding: self.holding,
beside: running,
#[cfg(feature = "testing")]
in_process: self
.shared
.harness
.in_process()
.then(|| Arc::clone(&self.shared)),
}
}
async fn next_one<Make, Fut>(
&mut self,
claims: &mut Claims<ClaimOne<DB, Row>, ClaimBuffers<DB, Row>, Make, Fut>,
) -> Option<Result<InboxDelivery<DB, Row, Mode>, SqlxBrokerError>>
where
Make: Fn(ClaimOne<DB, Row>, ClaimBuffers<DB, Row>) -> Fut,
Fut: Future<Output = OneClaimed<DB, Row>>,
{
self.flow.polled(self.in_work());
loop {
if claims.running() == 0 {
match self.wait.take() {
Some(Wait::Interval) => tokio::select! {
() = tokio::time::sleep(self.queue.poll_interval) => {}
() = self.wake.notified() => {}
() = self.shared.stopping.cancelled() => return None,
},
Some(Wait::Failed) => tokio::select! {
() = tokio::time::sleep(CLAIM_RETRY) => {}
() = self.shared.stopping.cancelled() => return None,
},
None => {}
}
if self.shared.is_closed() {
return None;
}
let in_work = self.in_work();
if self.flow.waits_for_a_settlement(0, in_work) {
let seen = self.settled.count();
tokio::select! {
() = self.settled.change(seen) => {}
() = self.shared.stopping.cancelled() => return None,
}
continue;
}
}
let in_work = self.in_work();
let (claimed, mut buffers) = if claims.running() == 0 && self.flow.wanted(in_work) == 1
{
claim_one(self.one(0), claims.buffers()).await
} else {
let Some(done) = poll_fn(|cx| self.turn(claims, cx)).await else {
continue;
};
done
};
match claimed {
Ok(taken) if buffers.rows.is_empty() => {
self.end_empty(taken).await;
claims.put_back(buffers);
self.flow.found_nothing();
if claims.running() == 0 {
self.wait = Some(Wait::Interval);
}
}
Ok(taken) => {
self.flow.took();
let delivery = self.deliver(taken, &mut buffers);
claims.put_back(buffers);
return delivery.map(Ok);
}
Err((statement, source)) => {
claims.put_back(buffers);
self.flow.found_nothing();
self.wait = Some(Wait::Failed);
return Some(Err(self.failed(statement, source)));
}
}
}
}
fn turn<Make, Fut>(
&self,
claims: &mut Claims<ClaimOne<DB, Row>, ClaimBuffers<DB, Row>, Make, Fut>,
cx: &mut Context<'_>,
) -> Poll<Option<OneClaimed<DB, Row>>>
where
Make: Fn(ClaimOne<DB, Row>, ClaimBuffers<DB, Row>) -> Fut,
Fut: Future<Output = OneClaimed<DB, Row>>,
{
if self.wait.is_none() && !self.shared.is_closed() {
let in_work = self.in_work();
while self
.flow
.may_start(claims.running(), in_work, || spare(self.pool))
{
let started = claims.start(self.one(claims.running()));
if let Poll::Ready(done) = claims.poll_slot(started, cx) {
return Poll::Ready(Some(done));
}
}
}
claims.poll_any(cx)
}
fn deliver(
&mut self,
taken: Taken<DB, Row>,
buffers: &mut ClaimBuffers<DB, Row>,
) -> Option<InboxDelivery<DB, Row, Mode>> {
let (queue, pool) = (self.queue, self.pool);
let delivery = match taken {
Taken::Locked(tx) => {
let claimed = buffers.rows.pop()?;
match self.lending {
Some(book) => InboxDelivery::lent(claimed, book.enter(tx), queue, pool),
None => InboxDelivery::own(claimed, tx, queue, pool),
}
}
Taken::Leased(book, lease) => {
let claimed = buffers.rows.pop()?;
let lent = self
.lending
.zip(buffers.transactions.get_mut().pop())
.map(|(lending, tx)| lending.enter(tx));
InboxDelivery::leased(claimed, book, lease, lent, queue, pool)
}
Taken::Advised => {
let claimed = buffers.rows.pop()?;
let hold = buffers.advised.as_deref_mut()?.holds.pop()?;
InboxDelivery::advised(claimed, hold, queue, pool)
}
};
self.made = self.made.wrapping_add(1);
let delivery = delivery.counted(InWork::new(self.settled));
#[cfg(feature = "testing")]
let delivery = delivery.on(&self.shared);
Some(delivery)
}
pub(crate) fn into_stream(
self,
) -> impl Stream<Item = Result<InboxDelivery<DB, Row, Mode>, SqlxBrokerError>> + Send + 'static
{
let claims = Claims::new(claim_one::<DB, Row>);
futures::stream::unfold((self, claims), |(mut subscriber, mut claims)| async move {
let next = subscriber.next_one(&mut claims).await?;
Some((next, (subscriber, claims)))
})
}
}
pub(crate) struct ClaimOne<DB: QueueDatabase, Row: Events<DB>> {
pool: &'static ServicePool<DB>,
queue: &'static Queue,
holding: Holding<DB, Row>,
beside: usize,
#[cfg(feature = "testing")]
in_process: Option<Arc<Shared<DB>>>,
}
pub(crate) type OneClaimed<DB, Row> = (Result<Taken<DB, Row>, Failed>, ClaimBuffers<DB, Row>);
async fn claim_one<DB, Row>(
one: ClaimOne<DB, Row>,
mut buffers: ClaimBuffers<DB, Row>,
) -> OneClaimed<DB, Row>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let ClaimOne {
pool,
queue,
holding,
beside,
..
} = one;
#[cfg(feature = "testing")]
if let Some(shared) = one.in_process {
let now = shared.harness.now();
let (claimed, buffers) = off_clock(async move {
let claimed = claim_rows(pool, queue, holding, 1, beside, now, &mut buffers).await;
(claimed, buffers)
})
.await
.unwrap_or_else(|| (Err(("BEGIN", cancelled())), ClaimBuffers::default()));
if claimed.is_ok() {
shared.harness.claimed(queue.name, buffers.rows.len());
}
return (claimed, buffers);
}
let claimed = claim_rows(
pool,
queue,
holding,
1,
beside,
Now::default(),
&mut buffers,
)
.await;
(claimed, buffers)
}
fn one_claim(queue: &Queue) -> bool {
queue.prepared.fifo_guard.is_some()
|| queue.spec.column(Role::PartitionKey).is_some()
|| queue.one_writer
|| queue.prepared.stamps
}
fn spare<DB: sqlx::Database>(pool: &Pool<DB>) -> usize {
let room = pool
.options()
.get_max_connections()
.saturating_sub(pool.size());
pool.num_idle()
.saturating_add(usize::try_from(room).unwrap_or(usize::MAX))
}
pub(crate) type Failed = (&'static str, sqlx::Error);
async fn claim_rows<DB, Row>(
pool: &Pool<DB>,
queue: &'static Queue,
holding: Holding<DB, Row>,
limit: usize,
beside: usize,
now: Now,
claimed: &mut ClaimBuffers<DB, Row>,
) -> Result<Taken<DB, Row>, Failed>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let cx = Claiming {
queue,
limit: i64::try_from(limit).unwrap_or(i64::MAX),
now,
};
claimed.clear();
let ClaimBuffers {
rows,
ids,
advised,
transactions,
} = claimed;
let book = match holding {
Holding::Transaction => {
return claim_locked::<DB, Row>(pool, &cx, ids, rows)
.await
.map(Taken::Locked);
}
Holding::Leases(book) => book,
Holding::Advisory(book) => {
let advised = advised.get_or_insert_with(Box::default);
claim_advised::<DB, Row>(pool, book, &cx, limit, beside, rows, advised).await?;
return Ok(Taken::Advised);
}
};
let lease = claim_leased::<DB, Row>(pool, &cx, now, ids, rows).await?;
if queue.prepared.transactional {
for _ in 0..rows.len() {
transactions.get_mut().push(begin_work(pool, queue).await?);
}
}
Ok(Taken::Leased(book, lease))
}
pub(crate) async fn take_group<DB, Row>(
conn: &mut DB::Connection,
cx: &Claiming,
lease: Option<&Leasing<Row::Token>>,
) -> Result<bool, Failed>
where
DB: QueueDatabase,
Row: Events<DB>,
{
let Some(guard) = cx.queue.prepared.fifo_guard else {
return Ok(true);
};
engine::take_group::<DB, Row>(conn, cx, guard, lease)
.await
.map_err(|source| (guard.sql, source))
}
pub(crate) async fn begin<DB: QueueDatabase>(
pool: &Pool<DB>,
queue: &Queue,
) -> Result<PoolTx<DB>, Failed> {
PoolTx::begin(pool, queue.begin_claim)
.await
.map_err(|source| (queue.begin_claim.unwrap_or("BEGIN"), source))
}
impl<DB, Row, Mode> Subscriber for InboxSubscriber<DB, Row, Mode>
where
DB: QueueDatabase,
Row: Events<DB>,
Mode: InboxMode,
{
type Message = InboxDelivery<DB, Row, Mode>;
type Error = SqlxBrokerError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
let claims = Claims::new(claim_one::<DB, Row>);
futures::stream::unfold((self, claims), |(subscriber, mut claims)| async move {
let next = subscriber.next_one(&mut claims).await?;
Some((next, (subscriber, claims)))
})
}
}
impl<DB, Row> BatchSubscriber for InboxSubscriber<DB, Row, Plain>
where
DB: QueueDatabase,
Row: Events<DB>,
Row::Lane: BatchLane<DB, Row>,
{
type Batch = <Row::Lane as BatchLane<DB, Row>>::Batch;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
futures::stream::unfold(self, move |subscriber| async move {
let (taken, count) = match subscriber.claim(size.get()).await? {
Ok(claimed) => claimed,
Err(error) => return Some((Err(error), subscriber)),
};
let batch =
<Row::Lane as BatchLane<DB, Row>>::batch(BatchClaim::new(subscriber, taken, count));
Some((Ok(batch), subscriber))
})
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use ruststream_sqlx_dialect::{Column, Form, TableSpec};
use super::one_claim;
use crate::inbox::engine::{IdAt, Prepared};
use crate::inbox::queue::Queue;
fn queue(spec: &TableSpec<'static>, one_writer: bool) -> Queue {
Queue {
name: "jobs",
table: "jobs",
row: "app::Job",
spec: *spec,
id_at: IdAt::First,
native_retry_after: false,
kinds: None,
prepared: Prepared::default(),
begin_claim: None,
counted_attempt: false,
one_writer,
poll_interval: Duration::from_secs(1),
lease: None,
cap: None,
}
}
const PLAIN: TableSpec<'static> = TableSpec::new("jobs", Column::new("id"), Form::RowLock);
#[test]
fn a_plain_table_on_a_server_claims_for_every_free_worker() {
assert!(!one_claim(&queue(&PLAIN, false)));
}
#[test]
fn a_keyed_table_keeps_its_claim_order() {
let keyed = PLAIN.partition_key(Column::new("customer"));
assert!(one_claim(&queue(&keyed, false)));
}
#[test]
fn a_lease_claim_that_stamps_in_a_transaction_keeps_one_claim() {
let mut stamping = queue(&PLAIN, false);
stamping.prepared.stamps = true;
assert!(one_claim(&stamping));
}
#[test]
fn a_database_of_one_writer_keeps_one_claim() {
assert!(one_claim(&queue(&PLAIN, true)));
}
}