Skip to main content

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}