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::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/// ```
186pub struct InboxDelivery<DB: QueueDatabase, Row: Events<DB>, Mode = Plain> {
187 claimed: Claimed<Row>,
188 headers: Row::Headers,
189 pub(super) hold: Option<Hold<DB, Row>>,
190 queue: &'static Queue,
191 /// The subscription's handle on the pool, which the delivery lends its handler.
192 pub(super) pool: &'static ServicePool<DB>,
193 /// In row mode, whether the handler borrowed the row; nothing in payload mode.
194 row_lent: <Row::Lane as Lane<Row>>::Lent,
195 /// The connection of a delivery claimed in process: its settlement keeps the harness's books.
196 #[cfg(feature = "testing")]
197 in_process: Option<Arc<Shared<DB>>>,
198 /// Counts the delivery settled in its subscription's books when it drops; a delivery of a
199 /// batch counts nothing.
200 in_work: Option<InWork>,
201 _mode: PhantomData<fn() -> Mode>,
202}
203
204/// What a delivery of a claimed id without a row reports: the row is gone.
205static ROW_GONE: LazyLock<CodecError> = LazyLock::new(|| CodecError::Decode(Box::new(RowGone)));
206
207/// The row of a claimed id was gone when the claim read it: the fetch returned no row for the id.
208#[derive(Debug, Error)]
209#[error("the claimed row is gone: the fetch returned no row for its id")]
210struct RowGone;
211
212/// Logs a claimed id of `queue` the fetch returned no row for, naming the id.
213fn row_gone(queue: &Queue, id: &impl Debug) {
214 tracing::warn!(
215 target: "ruststream_sqlx",
216 subscription = queue.name,
217 table = queue.table,
218 row = queue.row,
219 ?id,
220 "the fetch returned no row for a claimed id; the decode-failure policy settles its \
221 delivery",
222 );
223}
224
225/// What a subscription to a table in row mode logs when a handler reads a payload from a delivery
226/// whose row it never borrowed: a handler that decodes a payload, mounted where `row`, the struct
227/// that describes the table, is what a handler takes.
228fn unlent_payload(row: &str) -> String {
229 format!(
230 "the table is in row mode: its deliveries lend the row itself and carry no payload, so a \
231 handler that decodes one fails each delivery by the decode policy; take `&{row}` as the \
232 handler's input, `&[{row}]` in a batch"
233 )
234}
235
236/// The subscriptions that logged [`unlent_payload`], by their interned queue: each logs it once.
237static UNLENT_LOGGED: Mutex<Vec<&'static Queue>> = Mutex::new(Vec::new());
238
239/// Logs [`unlent_payload`], once per subscription.
240// Why a runtime warning: the core's codec lane accepts a handler on any subscription, so a handler
241// that decodes a payload mounts on a table in row mode; refusing it at compile time needs a hook
242// in the core. Out of line and cold, as only such a handler reaches it, and the lock with it.
243#[cold]
244#[inline(never)]
245fn read_unlent(queue: &'static Queue) {
246 let mut logged = UNLENT_LOGGED.lock().unwrap_or_else(PoisonError::into_inner);
247 if logged.iter().any(|seen| ptr::eq(*seen, queue)) {
248 return;
249 }
250 logged.push(queue);
251 drop(logged);
252 tracing::warn!(
253 target: "ruststream_sqlx",
254 subscription = queue.name,
255 table = queue.table,
256 row = queue.row,
257 "{}",
258 unlent_payload(queue.row),
259 );
260}
261
262impl<DB: QueueDatabase, Row: Events<DB>, Mode> Debug for InboxDelivery<DB, Row, Mode> {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 f.debug_struct("InboxDelivery")
265 .field("subscription", &self.queue.name)
266 .field("id", self.claimed.id::<DB>())
267 .finish_non_exhaustive()
268 }
269}
270
271impl<DB, Row, Mode> InboxDelivery<DB, Row, Mode>
272where
273 DB: QueueDatabase,
274 Row: Events<DB>,
275 Mode: InboxMode,
276{
277 /// A delivery that owns its claim's transaction.
278 pub(crate) fn own(
279 claimed: Claimed<Row>,
280 tx: PoolTx<DB>,
281 queue: &'static Queue,
282 pool: &'static ServicePool<DB>,
283 ) -> Self {
284 Self::held(claimed, Hold::Own(SyncWrapper::new(tx)), queue, pool)
285 }
286
287 /// A delivery whose claim's transaction waits at `hold` in its subscription's book, for its
288 /// handler to borrow: transactional mode.
289 pub(crate) fn lent(
290 claimed: Claimed<Row>,
291 hold: TxHold<DB>,
292 queue: &'static Queue,
293 pool: &'static ServicePool<DB>,
294 ) -> Self {
295 Self::held(claimed, Hold::Lent(hold), queue, pool)
296 }
297
298 /// A delivery of a batch, sharing its claim's transaction.
299 pub(crate) fn batched(
300 claimed: Claimed<Row>,
301 batch: Arc<BatchTx<DB>>,
302 queue: &'static Queue,
303 pool: &'static ServicePool<DB>,
304 ) -> Self {
305 Self::held(claimed, Hold::Batch(batch), queue, pool)
306 }
307
308 /// A delivery that holds the lease its claim wrote, entered in its subscription's `book`; in
309 /// transactional mode with its own transaction, which waits at `tx` for its handler to borrow.
310 pub(crate) fn leased(
311 claimed: Claimed<Row>,
312 book: &'static LeaseBook<DB, Row>,
313 lease: Row::Token,
314 tx: Option<TxHold<DB>>,
315 queue: &'static Queue,
316 pool: &'static ServicePool<DB>,
317 ) -> Self {
318 let slot = book.enter(claimed.id::<DB>(), lease);
319 Self::held(claimed, Hold::Lease { book, slot, tx }, queue, pool)
320 }
321
322 /// A delivery whose session holds the lock on its row's key, at `hold` in its subscription's
323 /// book.
324 pub(crate) fn advised(
325 claimed: Claimed<Row>,
326 hold: LockHold<DB>,
327 queue: &'static Queue,
328 pool: &'static ServicePool<DB>,
329 ) -> Self {
330 Self::held(claimed, Hold::Advisory(hold), queue, pool)
331 }
332
333 /// The subscription's handle on the pool, which the delivery lends its handler.
334 pub(crate) const fn pool(&self) -> &'static ServicePool<DB> {
335 self.pool
336 }
337
338 fn held(
339 mut claimed: Claimed<Row>,
340 hold: Hold<DB, Row>,
341 queue: &'static Queue,
342 pool: &'static ServicePool<DB>,
343 ) -> Self {
344 // A row's headers column moves into the delivery in both modes: in row mode the row a
345 // handler borrows holds that column empty, and middleware reads the headers off the
346 // delivery. A message assembled from a headers struct builds its map on the first read.
347 let headers = match &mut claimed {
348 Claimed::Row(row) => HeaderCell::take(row),
349 Claimed::Missing(id) => {
350 row_gone(queue, id);
351 Row::Headers::default()
352 }
353 Claimed::Undecodable { id, attempt, error } => {
354 tracing::warn!(
355 target: "ruststream_sqlx",
356 subscription = queue.name,
357 table = queue.table,
358 row = queue.row,
359 ?id,
360 attempt,
361 %error,
362 "the row does not decode into its struct; the decode-failure policy settles \
363 its delivery",
364 );
365 Row::Headers::default()
366 }
367 };
368 Self {
369 claimed,
370 headers,
371 hold: Some(hold),
372 queue,
373 pool,
374 row_lent: Default::default(),
375 #[cfg(feature = "testing")]
376 in_process: None,
377 in_work: None,
378 _mode: PhantomData,
379 }
380 }
381
382 /// The delivery counted in work until it drops.
383 pub(crate) fn counted(mut self, in_work: InWork) -> Self {
384 self.in_work = Some(in_work);
385 self
386 }
387
388 /// The delivery of `connection`, which keeps it when the connection runs in process.
389 #[cfg(feature = "testing")]
390 pub(crate) fn on(mut self, connection: &Arc<Shared<DB>>) -> Self {
391 self.in_process = connection
392 .harness
393 .in_process()
394 .then(|| Arc::clone(connection));
395 self
396 }
397}
398
399impl<DB, Row> InboxDelivery<DB, Row>
400where
401 DB: QueueDatabase,
402 Row: Events<DB> + QueueRow<Lane = RowLane>,
403{
404 /// The delivery of a row a [`RowBatch`](crate::RowBatch) lent its handler, held by `hold`:
405 /// `headers` were taken from the row when the batch was built, and the row counts as lent.
406 pub(super) fn of_batch(
407 row: Row,
408 headers: Row::Headers,
409 hold: Hold<DB, Row>,
410 queue: &'static Queue,
411 pool: &'static ServicePool<DB>,
412 ) -> Self {
413 Self {
414 claimed: Claimed::Row(row),
415 headers,
416 hold: Some(hold),
417 queue,
418 pool,
419 row_lent: AtomicBool::new(true),
420 #[cfg(feature = "testing")]
421 in_process: None,
422 in_work: None,
423 _mode: PhantomData,
424 }
425 }
426}
427
428impl<DB, Row, Mode> Carries<Row> for InboxDelivery<DB, Row, Mode>
429where
430 DB: QueueDatabase,
431 Row: Events<DB> + QueueRow<Lane = RowLane>,
432 Mode: InboxMode,
433{
434 fn carried(&self) -> Option<&Row> {
435 match &self.claimed {
436 Claimed::Row(row) => {
437 // Relaxed: one task handles a delivery, and its own `payload` alone reads the flag.
438 self.row_lent.store(true, Ordering::Relaxed);
439 Some(row)
440 }
441 Claimed::Missing(_) | Claimed::Undecodable { .. } => None,
442 }
443 }
444}
445
446impl<DB, Row, Mode> IncomingMessage for InboxDelivery<DB, Row, Mode>
447where
448 DB: QueueDatabase,
449 Row: Events<DB>,
450 Mode: InboxMode,
451{
452 fn payload(&self) -> &[u8] {
453 match &self.claimed {
454 Claimed::Row(row) => {
455 // A constant `false` in payload mode, where the check folds away.
456 if <Row::Lane as Lane<Row>>::unlent(&self.row_lent) {
457 read_unlent(self.queue);
458 }
459 <Row::Lane as Lane<Row>>::payload(row)
460 }
461 Claimed::Missing(_) | Claimed::Undecodable { .. } => &[],
462 }
463 }
464
465 fn headers(&self) -> &HeaderMap {
466 let row = match &self.claimed {
467 Claimed::Row(row) => Some(row),
468 Claimed::Missing(_) | Claimed::Undecodable { .. } => None,
469 };
470 self.headers.read(row)
471 }
472
473 fn decode_error(&self) -> Option<&CodecError> {
474 match &self.claimed {
475 Claimed::Row(_) => None,
476 Claimed::Missing(_) => Some(LazyLock::force(&ROW_GONE)),
477 Claimed::Undecodable { error, .. } => Some(error),
478 }
479 }
480
481 fn partition_key(&self) -> Option<&[u8]> {
482 match &self.claimed {
483 Claimed::Row(row) => Row::partition_key(row),
484 Claimed::Missing(_) | Claimed::Undecodable { .. } => None,
485 }
486 }
487
488 fn redelivery_count(&self) -> Option<u64> {
489 let carried = match &self.claimed {
490 Claimed::Row(row) => Row::attempt(row),
491 Claimed::Undecodable { attempt, .. } => *attempt,
492 Claimed::Missing(_) => None,
493 };
494 // A claim that returns its rows after counting reports the attempt before its count.
495 if self.queue.counted_attempt {
496 carried.map(|attempt| attempt.saturating_sub(1))
497 } else {
498 carried
499 }
500 }
501
502 async fn ack(self) -> Result<(), AckError> {
503 self.settle(Outcome::Ack).await
504 }
505
506 async fn nack(self, requeue: bool) -> Result<(), AckError> {
507 self.settle(if requeue {
508 Outcome::Retry
509 } else {
510 Outcome::Discard
511 })
512 .await
513 }
514
515 fn supports_nack_after(&self) -> bool {
516 // A delivery whose attempts are spent has no delayed redelivery to offer: the runtime then
517 // settles it with `nack(true)`, which moves the row, and expects nothing back.
518 self.queue.native_retry_after && self.spent().is_none()
519 }
520
521 async fn nack_after(self, delay: Duration) -> Result<(), AckError> {
522 if !self.supports_nack_after() {
523 return Err(AckError::Unsupported);
524 }
525 self.settle(Outcome::RetryAfter(delay)).await
526 }
527}
528
529impl<DB: QueueDatabase, Row: Events<DB>, Mode> Drop for InboxDelivery<DB, Row, Mode> {
530 fn drop(&mut self) {
531 let Some(hold) = self.hold.take() else {
532 return;
533 };
534 #[cfg(feature = "testing")]
535 let in_process = self.in_process.take();
536 match hold {
537 // In process the rollback runs off a paused clock too.
538 #[cfg(feature = "testing")]
539 Hold::Own(tx) if in_process.is_some() => {
540 if let Ok(runtime) = Handle::try_current() {
541 drop(runtime.spawn(off_clock(tx.into_inner().rollback())));
542 }
543 }
544 // An unsettled delivery's transaction rolls back on the runtime, which returns the
545 // row to the queue at once. Without a runtime the transaction drops here: its
546 // connection closes, and the server rolls it back.
547 Hold::Own(tx) => {
548 if let Ok(runtime) = Handle::try_current() {
549 drop(runtime.spawn(tx.into_inner().rollback()));
550 }
551 }
552 Hold::Batch(batch) => batch.release(self.queue),
553 // Nothing async runs in `drop`: the transaction ends as its hold drops, its connection
554 // closed rather than rolled back, as the handler may have left a statement midway on
555 // it; the server rolls the transaction back and the row returns at once.
556 Hold::Lent(hold) => drop(hold),
557 Hold::Advisory(hold) => {
558 #[cfg(feature = "testing")]
559 if let Some(connection) = in_process {
560 // The row comes back once its session ends; it is counted first, so a claim
561 // that takes it at once finds it counted.
562 connection.harness.expect(self.queue.name);
563 drop(hold);
564 connection.harness.released();
565 return;
566 }
567 // Nothing async runs in `drop`: the session closes on the runtime the broker
568 // connected on, after releasing the lock, and the row returns at once.
569 drop(hold);
570 }
571 Hold::Lease { book, slot, tx } => {
572 // The delivery's own transaction ends first: its connection closes, and the server
573 // rolls back what the handler wrote, so the release below waits for nothing of it.
574 drop(tx);
575 let id = self.claimed.id::<DB>().clone();
576 #[cfg(feature = "testing")]
577 if let Some(connection) = in_process {
578 release_in_process(connection, book, slot, self.queue, id);
579 return;
580 }
581 // Nothing async runs in `drop`: the release runs on the runtime the broker
582 // connected on, and returns the row to the queue at once. With that runtime gone
583 // the task never runs, and the row returns once its lease runs out.
584 drop(book.runtime().spawn(release::<DB, Row>(
585 book,
586 slot,
587 self.queue,
588 id,
589 Now::default(),
590 )));
591 }
592 }
593 #[cfg(feature = "testing")]
594 if let Some(connection) = in_process {
595 connection.harness.returned(self.queue.name);
596 }
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use std::collections::BTreeMap;
603 use std::fmt::Debug;
604 use std::mem;
605 use std::sync::{Arc, Mutex};
606 use std::time::Duration;
607
608 use ruststream_sqlx_dialect::{Column, Form, TableSpec};
609 use tracing::field::{Field, Visit};
610 use tracing::span::{Attributes, Id, Record};
611 use tracing::{Event, Level, Metadata, Subscriber};
612
613 use super::{row_gone, unlent_payload};
614 use crate::inbox::engine::{IdAt, Prepared};
615 use crate::inbox::queue::Queue;
616
617 /// The fields of each event logged while it is the default subscriber, by name, with its level.
618 #[derive(Default)]
619 struct Logged(Mutex<Vec<(Level, BTreeMap<String, String>)>>);
620
621 struct Fields<'a>(&'a mut BTreeMap<String, String>);
622
623 impl Visit for Fields<'_> {
624 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
625 self.0.insert(field.name().to_owned(), format!("{value:?}"));
626 }
627
628 fn record_str(&mut self, field: &Field, value: &str) {
629 self.0.insert(field.name().to_owned(), value.to_owned());
630 }
631 }
632
633 impl Subscriber for Logged {
634 fn enabled(&self, _: &Metadata<'_>) -> bool {
635 true
636 }
637
638 fn new_span(&self, _: &Attributes<'_>) -> Id {
639 Id::from_u64(1)
640 }
641
642 fn record(&self, _: &Id, _: &Record<'_>) {}
643
644 fn record_follows_from(&self, _: &Id, _: &Id) {}
645
646 fn event(&self, event: &Event<'_>) {
647 let mut fields = BTreeMap::new();
648 event.record(&mut Fields(&mut fields));
649 self.0
650 .lock()
651 .expect("the log is not poisoned")
652 .push((*event.metadata().level(), fields));
653 }
654
655 fn enter(&self, _: &Id) {}
656
657 fn exit(&self, _: &Id) {}
658 }
659
660 #[test]
661 fn a_claimed_id_without_a_row_is_logged_with_its_id() {
662 let queue = Queue {
663 name: "mail",
664 table: "mail_jobs",
665 row: "app::FetchedMail",
666 spec: TableSpec::new("mail_jobs", Column::new("job_id"), Form::RowLock),
667 id_at: IdAt::First,
668 native_retry_after: false,
669 kinds: None,
670 prepared: Prepared::default(),
671 begin_claim: None,
672 counted_attempt: false,
673 one_writer: false,
674 poll_interval: Duration::from_secs(1),
675 lease: None,
676 cap: None,
677 };
678 let logged = Arc::new(Logged::default());
679 tracing::subscriber::with_default(Arc::clone(&logged), || row_gone(&queue, &42_i64));
680 let logged = mem::take(&mut *logged.0.lock().expect("the log is not poisoned"));
681 let [(level, fields)] = logged.as_slice() else {
682 panic!("one event, not {}", logged.len());
683 };
684 assert_eq!(*level, Level::WARN);
685 assert_eq!(fields.get("id").map(String::as_str), Some("42"));
686 assert_eq!(fields.get("subscription").map(String::as_str), Some("mail"));
687 assert_eq!(fields.get("table").map(String::as_str), Some("mail_jobs"));
688 assert_eq!(
689 fields.get("row").map(String::as_str),
690 Some("app::FetchedMail")
691 );
692 }
693
694 #[test]
695 fn a_payload_read_from_a_row_mode_delivery_names_the_row_to_take() {
696 assert_eq!(
697 unlent_payload("app::SendEmail"),
698 "the table is in row mode: its deliveries lend the row itself and carry no payload, so \
699 a handler that decodes one fails each delivery by the decode policy; take \
700 `&app::SendEmail` as the handler's input, `&[app::SendEmail]` in a batch",
701 );
702 }
703}