ruststream_sqlx/inbox/delivery.rs
1//! `InboxDelivery`: one claimed row in a handler's hands, and how it settles.
2
3use std::fmt::{self, Debug};
4use std::marker::PhantomData;
5use std::ptr;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, LazyLock, Mutex, PoisonError};
8use std::time::Duration;
9
10use ruststream::codec::CodecError;
11use ruststream::{AckError, Carries, HeaderMap, IncomingMessage};
12use sync_wrapper::SyncWrapper;
13use thiserror::Error;
14use tokio::runtime::Handle;
15
16#[cfg(feature = "testing")]
17use super::broker::Shared;
18use super::claims::InWork;
19use super::database::QueueDatabase;
20use super::engine::{Claimed, Events, Now};
21use super::form::advisory::LockHold;
22use super::form::lease::settle::release;
23#[cfg(feature = "testing")]
24use super::form::lease::settle::release_in_process;
25use super::form::lease::{LeaseBook, Slot};
26use super::form::row_lock::BatchTx;
27use super::headers::HeaderCell;
28use super::queue::Queue;
29#[cfg(feature = "testing")]
30use super::testing::{QueueAt, off_clock};
31use super::transactional::{InboxMode, Plain, TxHold};
32use super::tx::PoolTx;
33use super::{Lane, QueueRow, RowLane};
34use crate::inbox::pools::ServicePool;
35
36pub(crate) mod settle;
37
38use settle::Outcome;
39
40/// What holds a delivery's row until it settles.
41pub(super) enum Hold<DB: QueueDatabase, Row: Events<DB>> {
42 /// The delivery's own transaction: one claim, one row.
43 Own(SyncWrapper<PoolTx<DB>>),
44 /// A batch's transaction: one claim, its rows sharing the transaction.
45 Batch(Arc<BatchTx<DB>>),
46 /// The lease the claim wrote, in the subscription's book; in transactional mode with the
47 /// delivery's own transaction, in the subscription's book of transactions while the handler
48 /// does not borrow it.
49 Lease {
50 book: &'static LeaseBook<DB, Row>,
51 slot: Slot,
52 tx: Option<TxHold<DB>>,
53 },
54 /// The lock on the row's key, held by the delivery's own session in the subscription's book.
55 Advisory(LockHold<DB>),
56 /// The delivery's own transaction, in the subscription's book while the handler does not
57 /// borrow it: one claim, one row, in transactional mode.
58 Lent(TxHold<DB>),
59}
60
61/// One claimed row in a handler's hands.
62///
63/// In payload mode the payload is lent from the row, without a copy. In row mode the delivery
64/// lends its handler the row itself, as the driver read it, with no codec in between. A `headers`
65/// column becomes the delivery's headers, and the row it lends holds that column empty. A claimed
66/// id without a row, and a row the driver could not read, lend no row: the runtime settles them by
67/// the decode policy.
68///
69/// In the row lock form the delivery holds the claim's
70/// transaction: settling it runs one statement and commits, and dropping it unsettled rolls the
71/// transaction back, which returns the row to the queue at once. In the lease form the delivery
72/// holds the lease its claim wrote, which its subscription extends each half lease: settling it
73/// runs one statement on a connection of its own, which takes effect only while the row still
74/// holds that lease. A lease delivery dropped unsettled releases its row at once, on the runtime
75/// the broker connected on; with that runtime gone, the row returns once the lease runs out.
76///
77/// In the advisory lock form the delivery holds a connection whose session holds the lock on the
78/// row's key: settling it runs one statement on that connection, then releases the lock and
79/// returns the connection to the pool. A delivery dropped unsettled closes its connection on the
80/// runtime the broker connected on, after releasing the lock, and its row returns at once; with
81/// that runtime gone the connection closes as it drops, and the server ends its session and its
82/// lock. A settlement after [`shutdown`](ruststream::ConnectedBroker::shutdown) released its lock
83/// fails with [`SqlxBrokerError::Closed`](crate::SqlxBrokerError::Closed).
84///
85/// In transactional mode the delivery lends its handler the transaction it settles in, and the
86/// handler's writes go with the settlement: acknowledgement runs its statement and commits them
87/// together; every other settlement first discards what the handler wrote, then settles as outside
88/// transactional mode. A statement that fails rolls the whole transaction back, and the row
89/// returns at once. In the lease form the acknowledgement runs in the delivery's own transaction by
90/// the lease it holds: a lease lost meanwhile rolls everything back and the settlement fails with
91/// [`SqlxBrokerError::LeaseLost`](crate::SqlxBrokerError::LeaseLost). In the advisory lock form
92/// the transaction runs on the session that holds the key, and ends before the key's release. A
93/// delivery dropped unsettled closes its transaction's connection, and the server rolls the
94/// transaction back.
95///
96/// # Examples
97///
98/// ```
99/// # #[cfg(feature = "postgres")]
100/// # mod demo {
101/// use std::time::Duration;
102///
103/// use ruststream_sqlx::prelude::*;
104/// use serde::Deserialize;
105/// use sqlx::PgPool;
106///
107/// #[derive(Inbox, sqlx::FromRow)]
108/// #[inbox(table = "email_jobs")]
109/// pub struct SendEmail {
110/// #[field(id, generated)]
111/// job_id: i64,
112/// #[field(payload)]
113/// payload: Vec<u8>,
114/// }
115///
116/// #[derive(Deserialize)]
117/// struct Email {
118/// to: String,
119/// }
120///
121/// # enum Sent { Yes, Later, Never }
122/// # async fn deliver(_: &Email) -> Sent { Sent::Yes }
123/// // Each claimed row reaches `send` as an `InboxDelivery<Postgres, SendEmail>`, and what the
124/// // handler returns settles it.
125/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
126/// async fn send(email: &Email) -> HandlerOutcome {
127/// match deliver(email).await {
128/// Sent::Yes => HandlerOutcome::ack(),
129/// // The mail server asked to come back later: the row returns once the minute passed.
130/// Sent::Later => HandlerOutcome::retry_after(Duration::from_secs(60)),
131/// // The address does not exist: the row is finished without a send.
132/// Sent::Never => HandlerOutcome::drop(),
133/// }
134/// }
135///
136/// pub fn app(pool: PgPool) -> RustStream {
137/// RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
138/// b.include(send);
139/// })
140/// }
141/// # }
142/// # fn main() {}
143/// ```
144///
145/// In row mode the handler borrows the row the delivery holds, beside what it reads off the
146/// delivery:
147///
148/// ```
149/// # #[cfg(feature = "postgres")]
150/// # mod demo {
151/// use ruststream_sqlx::prelude::*;
152/// use sqlx::PgPool;
153///
154/// /// A mail to send: the table has no payload column, so a handler takes the row itself.
155/// #[derive(Inbox, sqlx::FromRow, Clone)]
156/// #[inbox(table = "mail_jobs")]
157/// pub struct SendEmail {
158/// #[field(id, generated)]
159/// job_id: i64,
160/// #[field(attempt, generated)]
161/// attempt: i16,
162/// to: String,
163/// }
164///
165/// # async fn deliver(_: &str) -> bool { true }
166/// // The delivery lends `send` its row and reports the attempt the table counted.
167/// #[subscriber(InboxQueue::<SendEmail>::new("mail"))]
168/// async fn send(email: &SendEmail, Ctx(attempt): Ctx<keys::Attempt>) -> HandlerOutcome {
169/// if deliver(&email.to).await {
170/// HandlerOutcome::ack()
171/// } else if attempt < Some(3) {
172/// HandlerOutcome::retry()
173/// } else {
174/// HandlerOutcome::drop()
175/// }
176/// }
177///
178/// pub fn app(pool: PgPool) -> RustStream {
179/// RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
180/// b.include(send);
181/// })
182/// }
183/// # }
184/// # fn main() {}
185/// ```
186///
187/// A claimed row whose columns do not decode settles by the subscription's decode policy before
188/// the handler runs, and still spends its attempt. Here such a row comes back every ten minutes:
189///
190/// ```no_run
191/// # #[cfg(all(feature = "postgres", feature = "chrono"))]
192/// # mod demo {
193/// use std::time::Duration;
194///
195/// use chrono::{DateTime, Utc};
196/// use ruststream_sqlx::prelude::*;
197/// use serde::Deserialize;
198/// use sqlx::PgPool;
199///
200/// #[derive(Inbox, sqlx::FromRow)]
201/// #[inbox(table = "email_jobs")]
202/// pub struct SendEmail {
203/// #[field(id, generated)]
204/// job_id: i64,
205/// #[field(group)]
206/// name: String,
207/// #[field(retry_after, generated)]
208/// retry_after: DateTime<Utc>,
209/// #[field(payload)]
210/// payload: Vec<u8>,
211/// }
212///
213/// #[derive(Deserialize)]
214/// pub struct Email {
215/// to: String,
216/// }
217///
218/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
219/// async fn send(email: &Email) -> HandlerOutcome {
220/// tracing::info!(to = %email.to, "sending");
221/// HandlerOutcome::ack()
222/// }
223///
224/// pub fn app(pool: PgPool) -> RustStream {
225/// // A row that does not decode stays in the table and comes back every ten minutes, until a
226/// // release that reads it ships.
227/// let keep = FailurePolicy::RetryAfter(Duration::from_secs(600));
228/// RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
229/// b.include(send.on_failure(FailurePolicies::default().with_decode(keep)));
230/// })
231/// }
232/// # }
233/// # fn main() {}
234/// ```
235pub struct InboxDelivery<DB: QueueDatabase, Row: Events<DB>, Mode = Plain> {
236 claimed: Claimed<Row>,
237 headers: Row::Headers,
238 pub(super) hold: Option<Hold<DB, Row>>,
239 queue: &'static Queue,
240 /// The subscription's handle on the pool, which the delivery lends its handler.
241 pub(super) pool: &'static ServicePool<DB>,
242 /// In row mode, whether the handler borrowed the row; nothing in payload mode.
243 row_lent: <Row::Lane as Lane<Row>>::Lent,
244 /// The connection of a delivery claimed in process: its settlement keeps the harness's books.
245 #[cfg(feature = "testing")]
246 in_process: Option<Arc<Shared<DB>>>,
247 /// Counts the delivery settled in its subscription's books when it drops; a delivery of a
248 /// batch counts nothing.
249 in_work: Option<InWork>,
250 _mode: PhantomData<fn() -> Mode>,
251}
252
253/// What a delivery of a claimed id without a row reports: the row is gone.
254static ROW_GONE: LazyLock<CodecError> = LazyLock::new(|| CodecError::Decode(Box::new(RowGone)));
255
256/// The row of a claimed id was gone when the claim read it: the fetch returned no row for the id.
257#[derive(Debug, Error)]
258#[error("the claimed row is gone: the fetch returned no row for its id")]
259struct RowGone;
260
261/// Logs a claimed id of `queue` the fetch returned no row for, naming the id.
262fn row_gone(queue: &Queue, id: &impl Debug) {
263 tracing::warn!(
264 target: "ruststream_sqlx",
265 subscription = queue.name,
266 table = queue.table,
267 row = queue.row,
268 ?id,
269 "the fetch returned no row for a claimed id; the decode-failure policy settles its \
270 delivery",
271 );
272}
273
274/// What a subscription to a table in row mode logs when a handler reads a payload from a delivery
275/// whose row it never borrowed: a handler that decodes a payload, mounted where `row`, the struct
276/// that describes the table, is what a handler takes.
277fn unlent_payload(row: &str) -> String {
278 format!(
279 "the table is in row mode: its deliveries lend the row itself and carry no payload, so a \
280 handler that decodes one fails each delivery by the decode policy; take `&{row}` as the \
281 handler's input, `&[{row}]` in a batch"
282 )
283}
284
285/// The subscriptions that logged [`unlent_payload`], by their interned queue: each logs it once.
286static UNLENT_LOGGED: Mutex<Vec<&'static Queue>> = Mutex::new(Vec::new());
287
288/// Logs [`unlent_payload`], once per subscription.
289// Why a runtime warning: the core's codec lane accepts a handler on any subscription, so a handler
290// that decodes a payload mounts on a table in row mode; refusing it at compile time needs a hook
291// in the core. Out of line and cold, as only such a handler reaches it, and the lock with it.
292#[cold]
293#[inline(never)]
294fn read_unlent(queue: &'static Queue) {
295 let mut logged = UNLENT_LOGGED.lock().unwrap_or_else(PoisonError::into_inner);
296 if logged.iter().any(|seen| ptr::eq(*seen, queue)) {
297 return;
298 }
299 logged.push(queue);
300 drop(logged);
301 tracing::warn!(
302 target: "ruststream_sqlx",
303 subscription = queue.name,
304 table = queue.table,
305 row = queue.row,
306 "{}",
307 unlent_payload(queue.row),
308 );
309}
310
311impl<DB: QueueDatabase, Row: Events<DB>, Mode> Debug for InboxDelivery<DB, Row, Mode> {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 f.debug_struct("InboxDelivery")
314 .field("subscription", &self.queue.name)
315 .field("id", self.claimed.id::<DB>())
316 .finish_non_exhaustive()
317 }
318}
319
320impl<DB, Row, Mode> InboxDelivery<DB, Row, Mode>
321where
322 DB: QueueDatabase,
323 Row: Events<DB>,
324 Mode: InboxMode,
325{
326 /// A delivery that owns its claim's transaction.
327 pub(crate) fn own(
328 claimed: Claimed<Row>,
329 tx: PoolTx<DB>,
330 queue: &'static Queue,
331 pool: &'static ServicePool<DB>,
332 ) -> Self {
333 Self::held(claimed, Hold::Own(SyncWrapper::new(tx)), queue, pool)
334 }
335
336 /// A delivery whose claim's transaction waits at `hold` in its subscription's book, for its
337 /// handler to borrow: transactional mode.
338 pub(crate) fn lent(
339 claimed: Claimed<Row>,
340 hold: TxHold<DB>,
341 queue: &'static Queue,
342 pool: &'static ServicePool<DB>,
343 ) -> Self {
344 Self::held(claimed, Hold::Lent(hold), queue, pool)
345 }
346
347 /// A delivery of a batch, sharing its claim's transaction.
348 pub(crate) fn batched(
349 claimed: Claimed<Row>,
350 batch: Arc<BatchTx<DB>>,
351 queue: &'static Queue,
352 pool: &'static ServicePool<DB>,
353 ) -> Self {
354 Self::held(claimed, Hold::Batch(batch), queue, pool)
355 }
356
357 /// A delivery that holds the lease its claim wrote, entered in its subscription's `book`; in
358 /// transactional mode with its own transaction, which waits at `tx` for its handler to borrow.
359 pub(crate) fn leased(
360 claimed: Claimed<Row>,
361 book: &'static LeaseBook<DB, Row>,
362 lease: Row::Token,
363 tx: Option<TxHold<DB>>,
364 queue: &'static Queue,
365 pool: &'static ServicePool<DB>,
366 ) -> Self {
367 let slot = book.enter(claimed.id::<DB>(), lease);
368 Self::held(claimed, Hold::Lease { book, slot, tx }, queue, pool)
369 }
370
371 /// A delivery whose session holds the lock on its row's key, at `hold` in its subscription's
372 /// book.
373 pub(crate) fn advised(
374 claimed: Claimed<Row>,
375 hold: LockHold<DB>,
376 queue: &'static Queue,
377 pool: &'static ServicePool<DB>,
378 ) -> Self {
379 Self::held(claimed, Hold::Advisory(hold), queue, pool)
380 }
381
382 /// The subscription's handle on the pool, which the delivery lends its handler.
383 pub(crate) const fn pool(&self) -> &'static ServicePool<DB> {
384 self.pool
385 }
386
387 fn held(
388 mut claimed: Claimed<Row>,
389 hold: Hold<DB, Row>,
390 queue: &'static Queue,
391 pool: &'static ServicePool<DB>,
392 ) -> Self {
393 // A row's headers column moves into the delivery in both modes: in row mode the row a
394 // handler borrows holds that column empty, and middleware reads the headers off the
395 // delivery. A message assembled from a headers struct builds its map on the first read.
396 let headers = match &mut claimed {
397 Claimed::Row(row) => HeaderCell::take(row),
398 Claimed::Missing(id) => {
399 row_gone(queue, id);
400 Row::Headers::default()
401 }
402 Claimed::Undecodable { id, attempt, error } => {
403 tracing::warn!(
404 target: "ruststream_sqlx",
405 subscription = queue.name,
406 table = queue.table,
407 row = queue.row,
408 ?id,
409 attempt,
410 %error,
411 "the row does not decode into its struct; the decode-failure policy settles \
412 its delivery",
413 );
414 Row::Headers::default()
415 }
416 };
417 Self {
418 claimed,
419 headers,
420 hold: Some(hold),
421 queue,
422 pool,
423 row_lent: Default::default(),
424 #[cfg(feature = "testing")]
425 in_process: None,
426 in_work: None,
427 _mode: PhantomData,
428 }
429 }
430
431 /// The delivery counted in work until it drops.
432 pub(crate) fn counted(mut self, in_work: InWork) -> Self {
433 self.in_work = Some(in_work);
434 self
435 }
436
437 /// The delivery of `connection`, which keeps it when the connection runs in process.
438 #[cfg(feature = "testing")]
439 pub(crate) fn on(mut self, connection: &Arc<Shared<DB>>) -> Self {
440 self.in_process = connection
441 .harness
442 .in_process()
443 .then(|| Arc::clone(connection));
444 self
445 }
446}
447
448impl<DB, Row> InboxDelivery<DB, Row>
449where
450 DB: QueueDatabase,
451 Row: Events<DB> + QueueRow<Lane = RowLane>,
452{
453 /// The delivery of a row a [`RowBatch`](crate::RowBatch) lent its handler, held by `hold`:
454 /// `headers` were taken from the row when the batch was built, and the row counts as lent.
455 pub(super) fn of_batch(
456 row: Row,
457 headers: Row::Headers,
458 hold: Hold<DB, Row>,
459 queue: &'static Queue,
460 pool: &'static ServicePool<DB>,
461 ) -> Self {
462 Self {
463 claimed: Claimed::Row(row),
464 headers,
465 hold: Some(hold),
466 queue,
467 pool,
468 row_lent: AtomicBool::new(true),
469 #[cfg(feature = "testing")]
470 in_process: None,
471 in_work: None,
472 _mode: PhantomData,
473 }
474 }
475}
476
477impl<DB, Row, Mode> Carries<Row> for InboxDelivery<DB, Row, Mode>
478where
479 DB: QueueDatabase,
480 Row: Events<DB> + QueueRow<Lane = RowLane>,
481 Mode: InboxMode,
482{
483 fn carried(&self) -> Option<&Row> {
484 match &self.claimed {
485 Claimed::Row(row) => {
486 // Relaxed: one task handles a delivery, and its own `payload` alone reads the flag.
487 self.row_lent.store(true, Ordering::Relaxed);
488 Some(row)
489 }
490 Claimed::Missing(_) | Claimed::Undecodable { .. } => None,
491 }
492 }
493}
494
495impl<DB, Row, Mode> IncomingMessage for InboxDelivery<DB, Row, Mode>
496where
497 DB: QueueDatabase,
498 Row: Events<DB>,
499 Mode: InboxMode,
500{
501 fn payload(&self) -> &[u8] {
502 match &self.claimed {
503 Claimed::Row(row) => {
504 // A constant `false` in payload mode, where the check folds away.
505 if <Row::Lane as Lane<Row>>::unlent(&self.row_lent) {
506 read_unlent(self.queue);
507 }
508 <Row::Lane as Lane<Row>>::payload(row)
509 }
510 Claimed::Missing(_) | Claimed::Undecodable { .. } => &[],
511 }
512 }
513
514 fn headers(&self) -> &HeaderMap {
515 let row = match &self.claimed {
516 Claimed::Row(row) => Some(row),
517 Claimed::Missing(_) | Claimed::Undecodable { .. } => None,
518 };
519 self.headers.read(row)
520 }
521
522 fn decode_error(&self) -> Option<&CodecError> {
523 match &self.claimed {
524 Claimed::Row(_) => None,
525 Claimed::Missing(_) => Some(LazyLock::force(&ROW_GONE)),
526 Claimed::Undecodable { error, .. } => Some(error),
527 }
528 }
529
530 fn partition_key(&self) -> Option<&[u8]> {
531 match &self.claimed {
532 Claimed::Row(row) => Row::partition_key(row),
533 Claimed::Missing(_) | Claimed::Undecodable { .. } => None,
534 }
535 }
536
537 fn redelivery_count(&self) -> Option<u64> {
538 let carried = match &self.claimed {
539 Claimed::Row(row) => Row::attempt(row),
540 Claimed::Undecodable { attempt, .. } => *attempt,
541 Claimed::Missing(_) => None,
542 };
543 // A claim that returns its rows after counting reports the attempt before its count.
544 if self.queue.counted_attempt {
545 carried.map(|attempt| attempt.saturating_sub(1))
546 } else {
547 carried
548 }
549 }
550
551 async fn ack(self) -> Result<(), AckError> {
552 self.settle(Outcome::Ack).await
553 }
554
555 async fn nack(self, requeue: bool) -> Result<(), AckError> {
556 self.settle(if requeue {
557 Outcome::Retry
558 } else {
559 Outcome::Discard
560 })
561 .await
562 }
563
564 fn supports_nack_after(&self) -> bool {
565 // A delivery whose attempts are spent has no delayed redelivery to offer: the runtime then
566 // settles it with `nack(true)`, which moves the row, and expects nothing back.
567 self.queue.native_retry_after && self.spent().is_none()
568 }
569
570 async fn nack_after(self, delay: Duration) -> Result<(), AckError> {
571 if !self.supports_nack_after() {
572 return Err(AckError::Unsupported);
573 }
574 self.settle(Outcome::RetryAfter(delay)).await
575 }
576}
577
578impl<DB: QueueDatabase, Row: Events<DB>, Mode> Drop for InboxDelivery<DB, Row, Mode> {
579 fn drop(&mut self) {
580 let Some(hold) = self.hold.take() else {
581 return;
582 };
583 #[cfg(feature = "testing")]
584 let in_process = self.in_process.take();
585 match hold {
586 // In process the rollback runs off a paused clock too.
587 #[cfg(feature = "testing")]
588 Hold::Own(tx) if in_process.is_some() => {
589 if let Ok(runtime) = Handle::try_current() {
590 drop(runtime.spawn(off_clock(tx.into_inner().rollback())));
591 }
592 }
593 // An unsettled delivery's transaction rolls back on the runtime, which returns the
594 // row to the queue at once. Without a runtime the transaction drops here: its
595 // connection closes, and the server rolls it back.
596 Hold::Own(tx) => {
597 if let Ok(runtime) = Handle::try_current() {
598 drop(runtime.spawn(tx.into_inner().rollback()));
599 }
600 }
601 Hold::Batch(batch) => batch.release(self.queue),
602 // Nothing async runs in `drop`: the transaction ends as its hold drops, its connection
603 // closed rather than rolled back, as the handler may have left a statement midway on
604 // it; the server rolls the transaction back and the row returns at once.
605 Hold::Lent(hold) => drop(hold),
606 Hold::Advisory(hold) => {
607 #[cfg(feature = "testing")]
608 if let Some(connection) = in_process {
609 // The row comes back once its session ends; it is counted first, so a claim
610 // that takes it at once finds it counted.
611 connection.harness.expect(QueueAt::read(self.queue));
612 drop(hold);
613 connection.harness.released();
614 return;
615 }
616 // Nothing async runs in `drop`: the session closes on the runtime the broker
617 // connected on, after releasing the lock, and the row returns at once.
618 drop(hold);
619 }
620 Hold::Lease { book, slot, tx } => {
621 // The delivery's own transaction ends first: its connection closes, and the server
622 // rolls back what the handler wrote, so the release below waits for nothing of it.
623 drop(tx);
624 let id = self.claimed.id::<DB>().clone();
625 #[cfg(feature = "testing")]
626 if let Some(connection) = in_process {
627 release_in_process(connection, book, slot, self.queue, id);
628 return;
629 }
630 // Nothing async runs in `drop`: the release runs on the runtime the broker
631 // connected on, and returns the row to the queue at once. With that runtime gone
632 // the task never runs, and the row returns once its lease runs out.
633 drop(book.runtime().spawn(release::<DB, Row>(
634 book,
635 slot,
636 self.queue,
637 id,
638 Now::default(),
639 )));
640 }
641 }
642 #[cfg(feature = "testing")]
643 if let Some(connection) = in_process {
644 connection.harness.returned(QueueAt::read(self.queue));
645 }
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use std::collections::BTreeMap;
652 use std::fmt::Debug;
653 use std::mem;
654 use std::sync::{Arc, Mutex};
655 use std::time::Duration;
656
657 use ruststream_sqlx_dialect::{Column, Form, TableSpec};
658 use tracing::field::{Field, Visit};
659 use tracing::span::{Attributes, Id, Record};
660 use tracing::{Event, Level, Metadata, Subscriber};
661
662 use super::{row_gone, unlent_payload};
663 use crate::inbox::engine::{IdAt, Prepared};
664 use crate::inbox::queue::Queue;
665
666 /// The fields of each event logged while it is the default subscriber, by name, with its level.
667 #[derive(Default)]
668 struct Logged(Mutex<Vec<(Level, BTreeMap<String, String>)>>);
669
670 struct Fields<'a>(&'a mut BTreeMap<String, String>);
671
672 impl Visit for Fields<'_> {
673 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
674 self.0.insert(field.name().to_owned(), format!("{value:?}"));
675 }
676
677 fn record_str(&mut self, field: &Field, value: &str) {
678 self.0.insert(field.name().to_owned(), value.to_owned());
679 }
680 }
681
682 impl Subscriber for Logged {
683 fn enabled(&self, _: &Metadata<'_>) -> bool {
684 true
685 }
686
687 fn new_span(&self, _: &Attributes<'_>) -> Id {
688 Id::from_u64(1)
689 }
690
691 fn record(&self, _: &Id, _: &Record<'_>) {}
692
693 fn record_follows_from(&self, _: &Id, _: &Id) {}
694
695 fn event(&self, event: &Event<'_>) {
696 let mut fields = BTreeMap::new();
697 event.record(&mut Fields(&mut fields));
698 self.0
699 .lock()
700 .expect("the log is not poisoned")
701 .push((*event.metadata().level(), fields));
702 }
703
704 fn enter(&self, _: &Id) {}
705
706 fn exit(&self, _: &Id) {}
707 }
708
709 #[test]
710 fn a_claimed_id_without_a_row_is_logged_with_its_id() {
711 let queue = Queue {
712 name: "mail",
713 table: "mail_jobs",
714 row: "app::FetchedMail",
715 spec: TableSpec::new("mail_jobs", Column::new("job_id"), Form::RowLock),
716 id_at: IdAt::First,
717 native_retry_after: false,
718 kinds: None,
719 prepared: Prepared::default(),
720 begin_claim: None,
721 counted_attempt: false,
722 one_writer: false,
723 poll_interval: Duration::from_secs(1),
724 lease: None,
725 cap: None,
726 };
727 let logged = Arc::new(Logged::default());
728 tracing::subscriber::with_default(Arc::clone(&logged), || row_gone(&queue, &42_i64));
729 let logged = mem::take(&mut *logged.0.lock().expect("the log is not poisoned"));
730 let [(level, fields)] = logged.as_slice() else {
731 panic!("one event, not {}", logged.len());
732 };
733 assert_eq!(*level, Level::WARN);
734 assert_eq!(fields.get("id").map(String::as_str), Some("42"));
735 assert_eq!(fields.get("subscription").map(String::as_str), Some("mail"));
736 assert_eq!(fields.get("table").map(String::as_str), Some("mail_jobs"));
737 assert_eq!(
738 fields.get("row").map(String::as_str),
739 Some("app::FetchedMail")
740 );
741 }
742
743 #[test]
744 fn a_payload_read_from_a_row_mode_delivery_names_the_row_to_take() {
745 assert_eq!(
746 unlent_payload("app::SendEmail"),
747 "the table is in row mode: its deliveries lend the row itself and carry no payload, so \
748 a handler that decodes one fails each delivery by the decode policy; take \
749 `&app::SendEmail` as the handler's input, `&[app::SendEmail]` in a batch",
750 );
751 }
752}