Skip to main content

ruststream_sqlx/inbox/
subscriber.rs

1//! `InboxSubscriber`: the claim loop a subscription's stream runs.
2
3use std::fmt;
4use std::future::{Future, poll_fn};
5use std::marker::PhantomData;
6use std::num::NonZeroUsize;
7use std::sync::Arc;
8use std::task::{Context, Poll};
9use std::time::Duration;
10
11use futures::Stream;
12use ruststream::{BatchSubscriber, Subscriber};
13use ruststream_sqlx_dialect::Role;
14use sqlx::Pool;
15use sync_wrapper::SyncWrapper;
16use tokio::sync::Notify;
17use tokio_util::sync::DropGuard;
18
19use super::batch::{BatchClaim, BatchLane};
20use super::broker::Shared;
21use super::claims::{Claims, Flow, InWork, Settled};
22use super::database::QueueDatabase;
23use super::delivery::InboxDelivery;
24use super::engine::{self, Claimed, Claiming, Events, Leasing, Now};
25use super::error::SqlxBrokerError;
26use super::form::advisory::claim::{Advised, claim_advised};
27use super::form::advisory::{LockBook, LockHold};
28use super::form::lease::LeaseBook;
29use super::form::lease::claim::{begin_work, claim_leased};
30use super::form::row_lock::claim_locked;
31use super::queue::{Queue, Registration};
32#[cfg(feature = "testing")]
33use super::testing::{cancelled, off_clock};
34use super::transactional::{InboxMode, Plain, TxBook};
35use super::tx::PoolTx;
36use crate::inbox::pools::ServicePool;
37
38/// How long a subscription waits after a claim failed, so a persistent failure cannot spin the
39/// loop.
40const CLAIM_RETRY: Duration = Duration::from_secs(1);
41
42/// The subscriber an [`InboxQueue`](crate::InboxQueue) opens: a stream of deliveries claimed from
43/// the table.
44///
45/// The stream claims when it is polled: up to the batch size for a batch handler, and one row for
46/// a single-message handler, with a claim in flight for each free worker of a handler mounted with
47/// `workers(n)`. The subscription holds at most the pool's size less one connection, and a claim
48/// beside another one starts only while the pool has a connection to spare, so the pool keeps one
49/// for the handlers. It keeps one claim in flight on SQLite, which takes one writer at a time, and
50/// on a table whose groups keep their order or that has a `partition_key` column, whose order claims
51/// that run at once could break, and where a lease claim stamps its rows in a transaction, as on
52/// MySQL and MariaDB. After a claim that filled its limit the next one runs at once;
53/// after one that found fewer rows it waits the poll interval, or until a publisher of the same
54/// broker writes a row of its table and group (a row the service writes through its own SQL or a
55/// handler's transaction waits for the interval). A write that lands while the subscription
56/// claims ends its next wait at once. A failed claim reaches the stream as an error item and the
57/// next claim waits one second. After `shutdown` the stream ends.
58///
59/// In the row lock form a message in work holds one connection, and a batch one for all its
60/// messages. A batch's settlements take effect together, when the last of its deliveries
61/// finishes: a settlement whose statement fails rolls the whole batch back, and its rows return.
62/// A batch that holds a row whose statement always fails therefore rolls back and returns all of
63/// its rows on every attempt, until the service's SQL or schema is fixed.
64///
65/// In the lease form the claim commits at once and holds no connection: each delivery, in a batch
66/// or alone, takes a connection only while it settles, and its settlement takes effect on its own;
67/// in transactional mode a delivery holds its transaction's connection until it settles.
68/// A task on the runtime the broker connected on extends the lease of every delivery in work each
69/// half lease, on one connection, until the subscriber drops or the broker shuts down.
70///
71/// In the advisory lock form each message in work holds a connection of its own, in a batch too,
72/// and its session holds the lock on the row's key: outside transactional mode no transaction
73/// stays open while the handler works. A claim selects candidates, locks each key on a session
74/// and takes the row while it is still claimable; a batch takes idle connections first and opens
75/// new ones while the pool has room, so a batch larger than the pool shrinks instead of waiting. A
76/// settlement runs its statement on the session, then releases the lock and returns the connection
77/// to the pool. A delivery dropped unsettled closes its connection on the runtime the broker
78/// connected on, after releasing the lock, and its row returns at once. SQLite keeps no such
79/// locks: the process keeps the keys in work instead, so one process serves a database file.
80///
81/// In transactional mode, a registration mounted with
82/// [`transactional`](crate::InboxSettings::transactional), each delivery lends its handler the
83/// transaction it settles in, opened at the table's isolation level or SQLite mode. In the row lock
84/// form that is the claim's: the subscription sets a savepoint after each claim that took a row, so
85/// a settlement other than acknowledgement can discard what the handler wrote. In the lease form
86/// the subscription opens a transaction for each delivery once its claim committed; in the advisory
87/// lock form, on the delivery's session once its row is taken. Transactional mode serves single
88/// deliveries: the subscriber delivers no batches in it.
89///
90/// A table whose groups keep their order (`#[field(group, fifo = true)]`) has one row of a group
91/// in work at a time. A claim takes the group's head, its first unfinished row in claim order, and
92/// takes nothing while another row of the group is in work, a row that entered the group ahead of
93/// it included. The claim first takes the group in its transaction with the guard its dialect
94/// builds, so subscriptions to one group on separate brokers keep the order too. A batch of such a
95/// table holds one row, and a handler mounted with `workers(n)` handles the group's rows one after
96/// another.
97///
98/// # Examples
99///
100/// ```
101/// # #[cfg(feature = "postgres")]
102/// # mod demo {
103/// use ruststream_sqlx::prelude::*;
104/// use serde::Deserialize;
105/// use sqlx::PgPool;
106///
107/// /// Changes to push to a CRM, one group per customer, each group in order.
108/// #[derive(Inbox, sqlx::FromRow)]
109/// #[inbox(table = "crm_jobs")]
110/// pub struct SyncChange {
111///     #[field(id, generated)]
112///     id: i64,
113///     #[field(group, fifo = true)]
114///     customer: String,
115///     #[field(payload)]
116///     payload: Vec<u8>,
117/// }
118///
119/// #[derive(Deserialize)]
120/// struct Change {
121///     field: String,
122/// }
123///
124/// // The mount's subscription is an `InboxSubscriber<Postgres, SyncChange>` reading group `acme`.
125/// #[subscriber(InboxQueue::<SyncChange>::new("acme"))]
126/// async fn push(change: &Change) -> HandlerOutcome {
127///     tracing::info!(field = %change.field, "pushing");
128///     HandlerOutcome::ack()
129/// }
130///
131/// pub fn app(pool: PgPool) -> RustStream {
132///     RustStream::new(AppInfo::new("crm-sync", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
133///         // Four workers, and still one change of the group at a time, in order.
134///         b.include(push.workers(nonzero!(4)));
135///     })
136/// }
137/// # }
138/// # fn main() {}
139/// ```
140pub struct InboxSubscriber<DB: QueueDatabase, Row: Events<DB>, Mode = Plain> {
141    pub(super) shared: Arc<Shared<DB>>,
142    queue: &'static Queue,
143    holding: Holding<DB, Row>,
144    /// The subscription's handle on the pool, which its deliveries lend their handlers.
145    pool: &'static ServicePool<DB>,
146    /// Where a transactional delivery's transaction waits while its handler does not hold it, in
147    /// the row lock and lease forms; `None` in the plain mode, and in the advisory lock form, whose
148    /// book keeps the session that holds the transaction.
149    lending: Option<&'static TxBook<DB>>,
150    /// What the last claim took, its storage reused from claim to claim.
151    claimed: ClaimBuffers<DB, Row>,
152    /// What the next claim waits for first.
153    wait: Option<Wait>,
154    /// How many claims of single deliveries the subscription keeps in flight.
155    flow: Flow,
156    /// The deliveries of single claims made so far; less those `settled` counts, the ones in work.
157    made: usize,
158    /// The deliveries of single claims settled or dropped so far.
159    settled: &'static Settled,
160    /// What the connection's publishers wake the subscription with after they write a row of its
161    /// table and group.
162    wake: &'static Notify,
163    _registration: Registration<DB>,
164    /// Stops the lease keeper when the subscriber drops; `None` outside the lease form.
165    _keeper: Option<DropGuard>,
166    _mode: PhantomData<fn() -> Mode>,
167}
168
169/// What a claim loop waits for before its next claim.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171enum Wait {
172    /// The poll interval, or a publish of the connection into the subscription's group.
173    Interval,
174    /// A second after a failed claim, whatever is published meanwhile: a persistent failure
175    /// cannot spin the loop at the rate of the publishes.
176    Failed,
177}
178
179/// What opening a subscription made for its subscriber.
180pub(crate) struct Opened<DB: QueueDatabase, Row: Events<DB>> {
181    pub(crate) queue: &'static Queue,
182    pub(crate) holding: Holding<DB, Row>,
183    pub(crate) registration: Registration<DB>,
184    pub(crate) keeper: Option<DropGuard>,
185    pub(crate) pool: &'static ServicePool<DB>,
186    pub(crate) lending: Option<&'static TxBook<DB>>,
187    pub(crate) wake: &'static Notify,
188}
189
190/// How a subscription holds the rows it claimed until they settle.
191pub(crate) enum Holding<DB: QueueDatabase, Row: Events<DB>> {
192    /// In the claim's transaction: the row lock form.
193    Transaction,
194    /// By the lease the claim wrote and committed, each delivery's kept in the subscription's
195    /// book: the lease form.
196    Leases(&'static LeaseBook<DB, Row>),
197    /// By the lock on each row's key, held by the session of its delivery, which the
198    /// subscription's book keeps: the advisory lock form.
199    Advisory(&'static LockBook<DB>),
200}
201
202impl<DB: QueueDatabase, Row: Events<DB>> Clone for Holding<DB, Row> {
203    fn clone(&self) -> Self {
204        *self
205    }
206}
207
208impl<DB: QueueDatabase, Row: Events<DB>> Copy for Holding<DB, Row> {}
209
210/// What a claim leaves the deliveries of its rows.
211pub(crate) enum Taken<DB: QueueDatabase, Row: Events<DB>> {
212    /// The claim's transaction, which holds the rows until they settle.
213    Locked(PoolTx<DB>),
214    /// The lease the claim wrote and committed, and the book its deliveries enter.
215    Leased(&'static LeaseBook<DB, Row>, Row::Token),
216    /// Each row's place in the book, whose session holds the row's lock, next to the row.
217    Advised,
218}
219
220/// What a claim took, kept by the subscriber from claim to claim so their storage is reused.
221pub(crate) struct ClaimBuffers<DB: QueueDatabase, Row: Events<DB>> {
222    /// The claim's rows, oldest first.
223    rows: Vec<Claimed<Row>>,
224    /// The ids a claim of ids for a fetch of the service's own reads, before the fetch reads their
225    /// rows; nothing for a table whose claim reads its rows whole.
226    ids: Row::Ids,
227    /// What only the advisory lock form keeps, made at its first claim: a subscription in another
228    /// form carries the pointer alone.
229    advised: Option<Box<Advised<DB, Row>>>,
230    /// In the lease form's transactional mode, the transaction of each row's delivery, opened once
231    /// the claim committed its leases; empty otherwise. Wrapped because a connection is not
232    /// `Sync`, and only the subscriber's own `&mut` reaches it.
233    transactions: SyncWrapper<Vec<PoolTx<DB>>>,
234}
235
236impl<DB: QueueDatabase, Row: Events<DB>> Default for ClaimBuffers<DB, Row> {
237    fn default() -> Self {
238        Self {
239            rows: Vec::new(),
240            ids: Row::Ids::default(),
241            advised: None,
242            transactions: SyncWrapper::new(Vec::new()),
243        }
244    }
245}
246
247impl<DB: QueueDatabase, Row: Events<DB>> ClaimBuffers<DB, Row> {
248    /// Empties them for the next claim. A hold left from a claim that failed midway drops here,
249    /// which ends its session, and so does a transaction, which closes its connection.
250    fn clear(&mut self) {
251        self.rows.clear();
252        self.transactions.get_mut().clear();
253        if let Some(advised) = self.advised.as_deref_mut() {
254            advised.holds.clear();
255            advised.taken.clear();
256        }
257    }
258}
259
260impl<DB: QueueDatabase, Row: Events<DB>, Mode> fmt::Debug for InboxSubscriber<DB, Row, Mode> {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        f.debug_struct("InboxSubscriber")
263            .field("subscription", &self.queue.name)
264            .field("table", &self.queue.table)
265            .field("row", &self.queue.row)
266            .finish_non_exhaustive()
267    }
268}
269
270impl<DB, Row, Mode> InboxSubscriber<DB, Row, Mode>
271where
272    DB: QueueDatabase,
273    Row: Events<DB>,
274    Mode: InboxMode,
275{
276    pub(crate) fn new(shared: Arc<Shared<DB>>, opened: Opened<DB, Row>) -> Self {
277        Self {
278            shared,
279            queue: opened.queue,
280            holding: opened.holding,
281            pool: opened.pool,
282            lending: opened.lending,
283            claimed: ClaimBuffers::default(),
284            wait: None,
285            flow: Flow::new(
286                opened.pool.options().get_max_connections(),
287                !matches!(opened.holding, Holding::Leases(_))
288                    || opened.queue.prepared.transactional,
289                one_claim(opened.queue),
290            ),
291            made: 0,
292            // Once per subscription, as its queue and books are: deliveries outlive the borrow of
293            // the subscriber.
294            settled: Box::leak(Box::default()),
295            wake: opened.wake,
296            _registration: opened.registration,
297            _keeper: opened.keeper,
298            _mode: PhantomData,
299        }
300    }
301
302    fn failed(&self, statement: &'static str, source: sqlx::Error) -> SqlxBrokerError {
303        SqlxBrokerError::Sqlx {
304            subscription: self.queue.name.to_owned(),
305            table: self.queue.table.to_owned(),
306            row: self.queue.row,
307            statement,
308            source: Box::new(source),
309        }
310    }
311
312    /// Waits for what the last claim asked, then claims up to `limit` rows. `None` once the
313    /// broker is shut down.
314    pub(crate) async fn claim(
315        &mut self,
316        limit: usize,
317    ) -> Option<Result<(Taken<DB, Row>, usize), SqlxBrokerError>> {
318        loop {
319            match self.wait.take() {
320                Some(Wait::Interval) => tokio::select! {
321                    () = tokio::time::sleep(self.queue.poll_interval) => {}
322                    () = self.wake.notified() => {}
323                    () = self.shared.stopping.cancelled() => return None,
324                },
325                Some(Wait::Failed) => tokio::select! {
326                    () = tokio::time::sleep(CLAIM_RETRY) => {}
327                    () = self.shared.stopping.cancelled() => return None,
328                },
329                None => {}
330            }
331            if self.shared.is_closed() {
332                return None;
333            }
334            let claimed = self.claim_once(limit).await;
335            match claimed {
336                Ok((taken, 0)) => {
337                    self.end_empty(taken).await;
338                    self.wait = Some(Wait::Interval);
339                }
340                Ok((taken, count)) => {
341                    self.wait = (count < limit).then_some(Wait::Interval);
342                    return Some(Ok((taken, count)));
343                }
344                Err(error) => {
345                    self.wait = Some(Wait::Failed);
346                    return Some(Err(error));
347                }
348            }
349        }
350    }
351
352    async fn claim_once(
353        &mut self,
354        limit: usize,
355    ) -> Result<(Taken<DB, Row>, usize), SqlxBrokerError> {
356        #[cfg(feature = "testing")]
357        if self.shared.harness.in_process() {
358            return self.claim_in_process(limit).await;
359        }
360        let taken = claim_rows(
361            &self.shared.pool,
362            self.queue,
363            self.holding,
364            limit,
365            0,
366            Now::default(),
367            &mut self.claimed,
368        )
369        .await
370        .map_err(|(statement, source)| self.failed(statement, source))?;
371        Ok((taken, self.claimed.rows.len()))
372    }
373
374    /// The claim of an in-process connection: on the test's clock, off a paused one, and in the
375    /// harness's books.
376    #[cfg(feature = "testing")]
377    async fn claim_in_process(
378        &mut self,
379        limit: usize,
380    ) -> Result<(Taken<DB, Row>, usize), SqlxBrokerError> {
381        let pool = self.shared.pool.clone();
382        let queue = self.queue;
383        let holding = self.holding;
384        let now = self.shared.harness.now();
385        let mut buffers = std::mem::take(&mut self.claimed);
386        let (claimed, buffers) = off_clock(async move {
387            let claimed = claim_rows(&pool, queue, holding, limit, 0, now, &mut buffers).await;
388            (claimed, buffers)
389        })
390        .await
391        .unwrap_or_else(|| (Err(("BEGIN", cancelled())), ClaimBuffers::default()));
392        self.claimed = buffers;
393        let taken = claimed.map_err(|(statement, source)| self.failed(statement, source))?;
394        self.shared
395            .harness
396            .claimed(queue.name, self.claimed.rows.len());
397        Ok((taken, self.claimed.rows.len()))
398    }
399
400    /// The deliveries of the last claim, oldest first.
401    pub(crate) fn take_rows(&mut self) -> impl Iterator<Item = Claimed<Row>> + '_ {
402        self.claimed.rows.drain(..)
403    }
404
405    /// The deliveries of the last advisory claim, oldest first, each with its place in the book.
406    pub(crate) fn take_advised(
407        &mut self,
408    ) -> impl Iterator<Item = (Claimed<Row>, LockHold<DB>)> + '_ {
409        let ClaimBuffers { rows, advised, .. } = &mut self.claimed;
410        let holds = advised
411            .as_deref_mut()
412            .map(|advised| advised.holds.drain(..));
413        rows.drain(..).zip(holds.into_iter().flatten())
414    }
415
416    pub(crate) const fn queue(&self) -> &'static Queue {
417        self.queue
418    }
419
420    /// The subscription's handle on the pool, which its deliveries lend their handlers.
421    pub(crate) const fn pool(&self) -> &'static ServicePool<DB> {
422        self.pool
423    }
424
425    /// The deliveries of single claims in work.
426    fn in_work(&self) -> usize {
427        self.made.wrapping_sub(self.settled.count())
428    }
429
430    /// What a single claim of the subscription runs with, beside `running` claims in flight.
431    fn one(&self, running: usize) -> ClaimOne<DB, Row> {
432        ClaimOne {
433            pool: self.pool,
434            queue: self.queue,
435            holding: self.holding,
436            beside: running,
437            #[cfg(feature = "testing")]
438            in_process: self
439                .shared
440                .harness
441                .in_process()
442                .then(|| Arc::clone(&self.shared)),
443        }
444    }
445
446    /// The next single delivery: a claim in flight for each free worker, within the connections
447    /// the subscription may hold, and the first one that took a row. `None` once the broker is
448    /// shut down.
449    async fn next_one<Make, Fut>(
450        &mut self,
451        claims: &mut Claims<ClaimOne<DB, Row>, ClaimBuffers<DB, Row>, Make, Fut>,
452    ) -> Option<Result<InboxDelivery<DB, Row, Mode>, SqlxBrokerError>>
453    where
454        Make: Fn(ClaimOne<DB, Row>, ClaimBuffers<DB, Row>) -> Fut,
455        Fut: Future<Output = OneClaimed<DB, Row>>,
456    {
457        self.flow.polled(self.in_work());
458        loop {
459            if claims.running() == 0 {
460                match self.wait.take() {
461                    Some(Wait::Interval) => tokio::select! {
462                        () = tokio::time::sleep(self.queue.poll_interval) => {}
463                        () = self.wake.notified() => {}
464                        () = self.shared.stopping.cancelled() => return None,
465                    },
466                    Some(Wait::Failed) => tokio::select! {
467                        () = tokio::time::sleep(CLAIM_RETRY) => {}
468                        () = self.shared.stopping.cancelled() => return None,
469                    },
470                    None => {}
471                }
472                if self.shared.is_closed() {
473                    return None;
474                }
475                let in_work = self.in_work();
476                if self.flow.waits_for_a_settlement(0, in_work) {
477                    let seen = self.settled.count();
478                    tokio::select! {
479                        () = self.settled.change(seen) => {}
480                        () = self.shared.stopping.cancelled() => return None,
481                    }
482                    continue;
483                }
484            }
485            let in_work = self.in_work();
486            let (claimed, mut buffers) = if claims.running() == 0 && self.flow.wanted(in_work) == 1
487            {
488                // One claim and nothing beside it, as a plain mount always claims: awaited in place,
489                // so it costs no slot.
490                claim_one(self.one(0), claims.buffers()).await
491            } else {
492                let Some(done) = poll_fn(|cx| self.turn(claims, cx)).await else {
493                    continue;
494                };
495                done
496            };
497            match claimed {
498                Ok(taken) if buffers.rows.is_empty() => {
499                    self.end_empty(taken).await;
500                    claims.put_back(buffers);
501                    self.flow.found_nothing();
502                    if claims.running() == 0 {
503                        self.wait = Some(Wait::Interval);
504                    }
505                }
506                Ok(taken) => {
507                    self.flow.took();
508                    let delivery = self.deliver(taken, &mut buffers);
509                    claims.put_back(buffers);
510                    return delivery.map(Ok);
511                }
512                Err((statement, source)) => {
513                    claims.put_back(buffers);
514                    self.flow.found_nothing();
515                    self.wait = Some(Wait::Failed);
516                    return Some(Err(self.failed(statement, source)));
517                }
518            }
519        }
520    }
521
522    /// Starts the claims the flow allows, then polls every claim in flight. `None` when none runs.
523    fn turn<Make, Fut>(
524        &self,
525        claims: &mut Claims<ClaimOne<DB, Row>, ClaimBuffers<DB, Row>, Make, Fut>,
526        cx: &mut Context<'_>,
527    ) -> Poll<Option<OneClaimed<DB, Row>>>
528    where
529        Make: Fn(ClaimOne<DB, Row>, ClaimBuffers<DB, Row>) -> Fut,
530        Fut: Future<Output = OneClaimed<DB, Row>>,
531    {
532        if self.wait.is_none() && !self.shared.is_closed() {
533            let in_work = self.in_work();
534            while self
535                .flow
536                .may_start(claims.running(), in_work, || spare(self.pool))
537            {
538                // Each claim is polled once as it starts, so it takes its connection before the
539                // pool is read for the next one.
540                let started = claims.start(self.one(claims.running()));
541                if let Poll::Ready(done) = claims.poll_slot(started, cx) {
542                    return Poll::Ready(Some(done));
543                }
544            }
545        }
546        claims.poll_any(cx)
547    }
548
549    /// The delivery of the row a single claim took into `buffers`.
550    fn deliver(
551        &mut self,
552        taken: Taken<DB, Row>,
553        buffers: &mut ClaimBuffers<DB, Row>,
554    ) -> Option<InboxDelivery<DB, Row, Mode>> {
555        let (queue, pool) = (self.queue, self.pool);
556        let delivery = match taken {
557            Taken::Locked(tx) => {
558                let claimed = buffers.rows.pop()?;
559                match self.lending {
560                    // The claim's transaction waits in the book for the handler to borrow it.
561                    Some(book) => InboxDelivery::lent(claimed, book.enter(tx), queue, pool),
562                    None => InboxDelivery::own(claimed, tx, queue, pool),
563                }
564            }
565            Taken::Leased(book, lease) => {
566                let claimed = buffers.rows.pop()?;
567                // In transactional mode the delivery's own transaction waits in the book for the
568                // handler to borrow it.
569                let lent = self
570                    .lending
571                    .zip(buffers.transactions.get_mut().pop())
572                    .map(|(lending, tx)| lending.enter(tx));
573                InboxDelivery::leased(claimed, book, lease, lent, queue, pool)
574            }
575            Taken::Advised => {
576                let claimed = buffers.rows.pop()?;
577                let hold = buffers.advised.as_deref_mut()?.holds.pop()?;
578                InboxDelivery::advised(claimed, hold, queue, pool)
579            }
580        };
581        self.made = self.made.wrapping_add(1);
582        let delivery = delivery.counted(InWork::new(self.settled));
583        #[cfg(feature = "testing")]
584        let delivery = delivery.on(&self.shared);
585        Some(delivery)
586    }
587
588    /// The subscription's deliveries, as a stream that owns it.
589    pub(crate) fn into_stream(
590        self,
591    ) -> impl Stream<Item = Result<InboxDelivery<DB, Row, Mode>, SqlxBrokerError>> + Send + 'static
592    {
593        let claims = Claims::new(claim_one::<DB, Row>);
594        futures::stream::unfold((self, claims), |(mut subscriber, mut claims)| async move {
595            let next = subscriber.next_one(&mut claims).await?;
596            Some((next, (subscriber, claims)))
597        })
598    }
599}
600
601/// What a single claim runs with.
602pub(crate) struct ClaimOne<DB: QueueDatabase, Row: Events<DB>> {
603    pool: &'static ServicePool<DB>,
604    queue: &'static Queue,
605    holding: Holding<DB, Row>,
606    /// The other claims of the subscription in flight.
607    beside: usize,
608    /// The connection, where it runs in process.
609    #[cfg(feature = "testing")]
610    in_process: Option<Arc<Shared<DB>>>,
611}
612
613/// What a single claim ends with: its outcome, and the buffers it hands back.
614pub(crate) type OneClaimed<DB, Row> = (Result<Taken<DB, Row>, Failed>, ClaimBuffers<DB, Row>);
615
616/// Claims one row with `one`, into `buffers` it owns while it runs, so several run at once.
617async fn claim_one<DB, Row>(
618    one: ClaimOne<DB, Row>,
619    mut buffers: ClaimBuffers<DB, Row>,
620) -> OneClaimed<DB, Row>
621where
622    DB: QueueDatabase,
623    Row: Events<DB>,
624{
625    let ClaimOne {
626        pool,
627        queue,
628        holding,
629        beside,
630        ..
631    } = one;
632    // In process the claim runs on the test's clock, off a paused one, and in the harness's books.
633    #[cfg(feature = "testing")]
634    if let Some(shared) = one.in_process {
635        let now = shared.harness.now();
636        let (claimed, buffers) = off_clock(async move {
637            let claimed = claim_rows(pool, queue, holding, 1, beside, now, &mut buffers).await;
638            (claimed, buffers)
639        })
640        .await
641        .unwrap_or_else(|| (Err(("BEGIN", cancelled())), ClaimBuffers::default()));
642        if claimed.is_ok() {
643            shared.harness.claimed(queue.name, buffers.rows.len());
644        }
645        return (claimed, buffers);
646    }
647    let claimed = claim_rows(
648        pool,
649        queue,
650        holding,
651        1,
652        beside,
653        Now::default(),
654        &mut buffers,
655    )
656    .await;
657    (claimed, buffers)
658}
659
660/// Whether a subscription to `queue` keeps one claim in flight: claims that run at once may
661/// finish out of their claim order, which a table whose groups keep their order and a keyed table
662/// read their order from; on a database of one writer they only collide; and a lease claim that
663/// stamps its rows in a transaction of several statements locks the rows it reads, leases in work
664/// included, until it commits, so a claim nobody polls would hold up their settlements.
665fn one_claim(queue: &Queue) -> bool {
666    queue.prepared.fifo_guard.is_some()
667        || queue.spec.column(Role::PartitionKey).is_some()
668        || queue.one_writer
669        || queue.prepared.stamps
670}
671
672/// The connections `pool` can lend at once: its idle ones, and room for new ones.
673fn spare<DB: sqlx::Database>(pool: &Pool<DB>) -> usize {
674    let room = pool
675        .options()
676        .get_max_connections()
677        .saturating_sub(pool.size());
678    pool.num_idle()
679        .saturating_add(usize::try_from(room).unwrap_or(usize::MAX))
680}
681
682/// A statement that failed, and why.
683pub(crate) type Failed = (&'static str, sqlx::Error);
684
685/// Claims up to `limit` rows of `queue` into `claimed`, holding them as `holding` says: in a
686/// transaction of `pool` it returns open, by a lease it commits, or by the lock on each row's key,
687/// held by a session of its own. An advisory claim reads a candidate more for each of `beside`
688/// claims of the subscription in flight.
689///
690/// The claim of a table whose groups keep their order takes the group first, in the claim's
691/// transaction, and takes nothing while another transaction holds it.
692async fn claim_rows<DB, Row>(
693    pool: &Pool<DB>,
694    queue: &'static Queue,
695    holding: Holding<DB, Row>,
696    limit: usize,
697    beside: usize,
698    now: Now,
699    claimed: &mut ClaimBuffers<DB, Row>,
700) -> Result<Taken<DB, Row>, Failed>
701where
702    DB: QueueDatabase,
703    Row: Events<DB>,
704{
705    let cx = Claiming {
706        queue,
707        limit: i64::try_from(limit).unwrap_or(i64::MAX),
708        now,
709    };
710    claimed.clear();
711    let ClaimBuffers {
712        rows,
713        ids,
714        advised,
715        transactions,
716    } = claimed;
717    let book = match holding {
718        Holding::Transaction => {
719            return claim_locked::<DB, Row>(pool, &cx, ids, rows)
720                .await
721                .map(Taken::Locked);
722        }
723        Holding::Leases(book) => book,
724        Holding::Advisory(book) => {
725            let advised = advised.get_or_insert_with(Box::default);
726            claim_advised::<DB, Row>(pool, book, &cx, limit, beside, rows, advised).await?;
727            return Ok(Taken::Advised);
728        }
729    };
730    let lease = claim_leased::<DB, Row>(pool, &cx, now, ids, rows).await?;
731    // In transactional mode each row's delivery writes in a transaction of its own, opened once
732    // the claim committed: the lease, not this transaction, keeps other claims off the row. A
733    // transaction that fails to open fails the claim, and the rows it took return once their
734    // leases run out, as after a crash.
735    if queue.prepared.transactional {
736        for _ in 0..rows.len() {
737            transactions.get_mut().push(begin_work(pool, queue).await?);
738        }
739    }
740    Ok(Taken::Leased(book, lease))
741}
742
743/// Takes the group of `cx` for the claim's transaction on `conn`, where its table keeps its groups
744/// in order: `false` when another transaction holds the group. A queue without a guard takes no
745/// group and claims at once.
746pub(crate) async fn take_group<DB, Row>(
747    conn: &mut DB::Connection,
748    cx: &Claiming,
749    lease: Option<&Leasing<Row::Token>>,
750) -> Result<bool, Failed>
751where
752    DB: QueueDatabase,
753    Row: Events<DB>,
754{
755    let Some(guard) = cx.queue.prepared.fifo_guard else {
756        return Ok(true);
757    };
758    engine::take_group::<DB, Row>(conn, cx, guard, lease)
759        .await
760        .map_err(|source| (guard.sql, source))
761}
762
763/// Opens a claim's transaction on a connection of `pool`, with the statement `queue`'s dialect
764/// opens it with, or `BEGIN`.
765pub(crate) async fn begin<DB: QueueDatabase>(
766    pool: &Pool<DB>,
767    queue: &Queue,
768) -> Result<PoolTx<DB>, Failed> {
769    PoolTx::begin(pool, queue.begin_claim)
770        .await
771        .map_err(|source| (queue.begin_claim.unwrap_or("BEGIN"), source))
772}
773
774impl<DB, Row, Mode> Subscriber for InboxSubscriber<DB, Row, Mode>
775where
776    DB: QueueDatabase,
777    Row: Events<DB>,
778    Mode: InboxMode,
779{
780    type Message = InboxDelivery<DB, Row, Mode>;
781    type Error = SqlxBrokerError;
782
783    fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
784        let claims = Claims::new(claim_one::<DB, Row>);
785        futures::stream::unfold((self, claims), |(subscriber, mut claims)| async move {
786            let next = subscriber.next_one(&mut claims).await?;
787            Some((next, (subscriber, claims)))
788        })
789    }
790}
791
792// Transactional mode serves single deliveries: a batch handler reads no delivery's context, so
793// it could take no delivery's transaction. The table's mode picks the batch: a payload-mode
794// table's is its deliveries, a row-mode table's lends its rows as one slice.
795impl<DB, Row> BatchSubscriber for InboxSubscriber<DB, Row, Plain>
796where
797    DB: QueueDatabase,
798    Row: Events<DB>,
799    Row::Lane: BatchLane<DB, Row>,
800{
801    type Batch = <Row::Lane as BatchLane<DB, Row>>::Batch;
802
803    fn batches(
804        &mut self,
805        size: NonZeroUsize,
806    ) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
807        futures::stream::unfold(self, move |subscriber| async move {
808            let (taken, count) = match subscriber.claim(size.get()).await? {
809                Ok(claimed) => claimed,
810                Err(error) => return Some((Err(error), subscriber)),
811            };
812            let batch =
813                <Row::Lane as BatchLane<DB, Row>>::batch(BatchClaim::new(subscriber, taken, count));
814            Some((Ok(batch), subscriber))
815        })
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use std::time::Duration;
822
823    use ruststream_sqlx_dialect::{Column, Form, TableSpec};
824
825    use super::one_claim;
826    use crate::inbox::engine::{IdAt, Prepared};
827    use crate::inbox::queue::Queue;
828
829    fn queue(spec: &TableSpec<'static>, one_writer: bool) -> Queue {
830        Queue {
831            name: "jobs",
832            table: "jobs",
833            row: "app::Job",
834            spec: *spec,
835            id_at: IdAt::First,
836            native_retry_after: false,
837            kinds: None,
838            prepared: Prepared::default(),
839            begin_claim: None,
840            counted_attempt: false,
841            one_writer,
842            poll_interval: Duration::from_secs(1),
843            lease: None,
844            cap: None,
845        }
846    }
847
848    const PLAIN: TableSpec<'static> = TableSpec::new("jobs", Column::new("id"), Form::RowLock);
849
850    #[test]
851    fn a_plain_table_on_a_server_claims_for_every_free_worker() {
852        assert!(!one_claim(&queue(&PLAIN, false)));
853    }
854
855    #[test]
856    fn a_keyed_table_keeps_its_claim_order() {
857        let keyed = PLAIN.partition_key(Column::new("customer"));
858        assert!(one_claim(&queue(&keyed, false)));
859    }
860
861    #[test]
862    fn a_lease_claim_that_stamps_in_a_transaction_keeps_one_claim() {
863        let mut stamping = queue(&PLAIN, false);
864        stamping.prepared.stamps = true;
865        assert!(one_claim(&stamping));
866    }
867
868    #[test]
869    fn a_database_of_one_writer_keeps_one_claim() {
870        assert!(one_claim(&queue(&PLAIN, true)));
871    }
872}