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::{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}