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