Skip to main content

ruststream_sqlx/
inbox.rs

1//! The inbox: task queues in tables a service describes with its own structs.
2
3pub(crate) mod batch;
4mod broker;
5mod claims;
6mod columns;
7mod database;
8mod delivery;
9pub(crate) mod engine;
10mod error;
11mod events;
12pub(crate) mod form;
13pub(crate) mod headers;
14pub mod keys;
15pub(crate) mod manual;
16pub(crate) mod named;
17mod pools;
18mod publish;
19pub(crate) mod queue;
20pub mod spec;
21mod subscriber;
22#[cfg(feature = "testing")]
23mod testing;
24mod threads;
25mod time;
26mod transactional;
27mod tx;
28
29use std::fmt::Debug;
30use std::sync::atomic::{AtomicBool, Ordering};
31
32use ruststream_sqlx_dialect::TableSpec;
33
34pub use batch::{RowBatch, RowDeliveries};
35pub use broker::{ClosedSqlxBroker, ConnectedSqlxBroker, SqlxBroker};
36pub use columns::{AttemptColumn, KeyColumn};
37#[cfg(feature = "any")]
38pub use database::AnyDialect;
39pub use database::{
40    BuiltIn, BuiltInDialect, InsertSql, Notifies, OnConnection, QueueDatabase, no_insert,
41};
42pub use delivery::InboxDelivery;
43pub use error::SqlxBrokerError;
44pub use events::{
45    Ack, Claim, DeadLetter, Discard, Extend, Fetch, Insert, Lock, Publish, Retry, RetryAfter,
46    Unlock,
47};
48pub use form::{AdvisoryForm, FormDialect, FormOn, LeaseForm, RowLockForm};
49pub use headers::{HeaderField, InboxHeaders};
50pub use manual::{AttemptRow, HeaderFields, KeyRow, put_header};
51pub use named::{ByName, NamedDelivery, NamedSubscriber, NamedTime};
52pub use publish::{Repository, RepositoryPublisher, Routed, RoutedPublisher};
53pub use queue::InboxQueue;
54pub use spec::builder::{InboxSpec, InboxTable};
55pub use subscriber::InboxSubscriber;
56pub use threads::{InboxThreads, ThreadsStep};
57pub use time::{Clock, DatabaseClock, LeaseRow, QueueTime, SystemClock, TimeColumn, TimeSource};
58#[cfg(feature = "outbox")]
59pub(crate) use transactional::writers;
60pub use transactional::{InboxMode, InboxSettings, Plain, Transactional, TransactionalStep, Tx};
61
62/// A struct that describes a queue table; `#[derive(Inbox)]` implements it.
63///
64/// The struct puts its table in one of two modes. In payload mode a `#[field(payload)]` field
65/// holds the message, and a codec decodes it for the handler ([`PayloadRow`]). In row mode the
66/// struct has no such field, and the handler takes the struct itself, `&Row` or `&[Row]` (see
67/// [row mode](https://powersemmi.github.io/ruststream-sqlx/latest/components/messages/rows/)).
68///
69/// # Examples
70///
71/// ```
72/// # #[cfg(feature = "postgres")]
73/// # mod demo {
74/// use ruststream_sqlx::prelude::*;
75/// use serde::Deserialize;
76/// use sqlx::PgPool;
77///
78/// // The derive implements `InboxRow` for `SendEmail`: the table `app.email_jobs`, its id and
79/// // its payload column.
80/// #[derive(Inbox, sqlx::FromRow)]
81/// #[inbox(table = "email_jobs", schema = "app")]
82/// pub struct SendEmail {
83///     #[field(id)]
84///     job_id: i64,
85///     #[field(payload)]
86///     payload: Vec<u8>,
87/// }
88///
89/// #[derive(Deserialize)]
90/// struct Email {
91///     to: String,
92/// }
93///
94/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
95/// async fn send(email: &Email) -> HandlerOutcome {
96///     tracing::info!(to = %email.to, "sending");
97///     HandlerOutcome::ack()
98/// }
99///
100/// pub fn app(pool: PgPool) -> RustStream {
101///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
102///         b.include(send);
103///     })
104/// }
105/// # }
106/// # fn main() {}
107/// ```
108///
109/// A struct in row mode hands its handler the row itself: one row, the rows of one claim as a
110/// slice, or one row beside its delivery's transaction:
111///
112/// ```no_run
113/// # #[cfg(all(feature = "inbox", feature = "postgres"))]
114/// # mod demo {
115/// use ruststream_sqlx::prelude::*;
116/// use serde::{Deserialize, Serialize};
117/// use sqlx::{PgPool, Postgres};
118///
119/// // email_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT, attempt SMALLINT DEFAULT 1, "to" TEXT,
120/// // subject TEXT NULL, attachments JSONB
121/// /// One mail to send: the table has no payload column, so a handler takes the row itself.
122/// #[derive(Inbox, sqlx::FromRow, Clone)]
123/// #[inbox(table = "email_jobs")]
124/// pub struct SendEmail {
125///     #[field(id, generated)]
126///     job_id: i64,
127///     #[field(group)]
128///     name: String,
129///     #[field(attempt, generated)]
130///     attempt: i16,
131///     to: String,
132///     subject: Option<String>,
133///     #[sqlx(json)]
134///     attachments: Vec<Attachment>,
135/// }
136///
137/// #[derive(Clone, Serialize, Deserialize)]
138/// pub struct Attachment {
139///     file: String,
140/// }
141///
142/// # async fn deliver(_: &SendEmail) -> bool { true }
143/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
144/// async fn send(email: &SendEmail) -> HandlerOutcome {
145///     if deliver(email).await {
146///         HandlerOutcome::ack()
147///     } else {
148///         HandlerOutcome::retry()
149///     }
150/// }
151///
152/// /// One SMTP session per claim of up to 64 rows.
153/// #[subscriber(InboxQueue::<SendEmail>::new("newsletters"))]
154/// async fn send_newsletter(emails: &[SendEmail]) -> HandlerOutcome {
155///     tracing::info!(count = emails.len(), "sent a newsletter page");
156///     HandlerOutcome::ack()
157/// }
158///
159/// /// The audit row commits with the acknowledgement of the mail's row, or neither does.
160/// #[subscriber(InboxQueue::<SendEmail>::new("receipts"))]
161/// async fn send_receipt(email: &SendEmail, Ctx(mut tx): Ctx<keys::Tx<Postgres>>) -> HandlerOutcome {
162///     let logged = sqlx::query("INSERT INTO sent_mail (job_id, recipient) VALUES ($1, $2)")
163///         .bind(email.job_id)
164///         .bind(&email.to)
165///         .execute(&mut *tx)
166///         .await;
167///     if logged.is_ok() && deliver(email).await {
168///         HandlerOutcome::ack()
169///     } else {
170///         HandlerOutcome::retry()
171///     }
172/// }
173///
174/// pub fn app(pool: PgPool) -> RustStream {
175///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
176///         b.include(send);
177///         b.include(send_newsletter.batch(nonzero!(64)));
178///         b.include(send_receipt.transactional());
179///     })
180/// }
181/// # }
182/// # fn main() {}
183/// ```
184#[diagnostic::on_unimplemented(
185    message = "`{Self}` does not describe a queue table",
186    label = "not an inbox row",
187    note = "derive it: `#[derive(Inbox)]` with `#[inbox(table = \"..\")]` and a `#[field(id)]` field, \
188            or describe the table by hand: `impl InboxTable`"
189)]
190pub trait InboxRow: QueueRow {
191    /// The table the struct describes: its name, its columns and their roles, and the form its
192    /// rows are claimed in.
193    const SPEC: TableSpec<'static>;
194
195    /// The form the table's rows are claimed in, as a type: a subscription requires its dialect
196    /// to serve it ([`FormOn`]). Machinery; the crate reads it off the table's description.
197    #[doc(hidden)]
198    type Form;
199
200    /// What the table's transactions open at, as a type: a [`level`](crate::dialect::level)
201    /// marker for the isolation level or SQLite mode the struct declares, `()` where it declares
202    /// neither. A subscription requires its dialect to open it
203    /// ([`Opens`](crate::dialect::Opens)). Machinery; the crate reads it off the table's
204    /// description.
205    #[doc(hidden)]
206    type Opening;
207}
208
209/// A row a subscription delivers, the type of its id, and how its message reaches a handler.
210/// Machinery: `#[derive(Inbox)]` implements it beside [`InboxRow`], and the row of a by-name
211/// subscription implements it without a table description of its own.
212#[doc(hidden)]
213pub trait QueueRow: Sized + Send + Sync + 'static {
214    /// The type of the field that plays `id`; logs name a row by it, and a lease subscription
215    /// keeps a copy of each id in work to extend its lease.
216    type Id: Clone + Debug + Send + Sync + 'static;
217
218    /// How a delivery hands the row's message to its handler: [`PayloadLane`] for a struct with a
219    /// `#[field(payload)]` field, [`RowLane`] for one without.
220    type Lane: Lane<Self>;
221}
222
223/// How a delivery hands a row's message to its handler. Machinery; the crate's two lanes are the
224/// only ones.
225#[doc(hidden)]
226pub trait Lane<Row>: Send + Sync + 'static {
227    /// Whether the handler takes the row itself.
228    const ROWS: bool;
229
230    /// What a delivery keeps of whether its handler borrowed the row: nothing in payload mode, a
231    /// flag in row mode.
232    type Lent: Default + Send + Sync;
233
234    /// The bytes a delivery lends as its payload: the payload column in payload mode, none in row
235    /// mode.
236    fn payload(row: &Row) -> &[u8];
237
238    /// Whether a payload read now comes from a delivery whose row its handler never borrowed, by
239    /// what `lent` keeps: a handler that decodes a payload, mounted on a table in row mode. Never
240    /// in payload mode.
241    fn unlent(lent: &Self::Lent) -> bool;
242}
243
244/// Payload mode: the handler takes the payload decoded by a codec.
245#[doc(hidden)]
246#[derive(Debug)]
247pub struct PayloadLane;
248
249/// Row mode: the handler takes the row itself, `&Row`, or a batch of them, `&[Row]`.
250#[doc(hidden)]
251#[derive(Debug)]
252pub struct RowLane;
253
254impl<Row: PayloadRow> Lane<Row> for PayloadLane {
255    const ROWS: bool = false;
256
257    // Nothing to keep, so the field takes no room in a payload-mode delivery.
258    type Lent = ();
259
260    fn payload(row: &Row) -> &[u8] {
261        row.payload()
262    }
263
264    fn unlent((): &()) -> bool {
265        false
266    }
267}
268
269// No `Clone` here: the carried lane asks it of the row where the derive writes `Input`, and one
270// check gives one error.
271impl<Row> Lane<Row> for RowLane {
272    const ROWS: bool = true;
273
274    type Lent = AtomicBool;
275
276    fn payload(_row: &Row) -> &[u8] {
277        &[]
278    }
279
280    fn unlent(lent: &AtomicBool) -> bool {
281        // Relaxed: one task handles a delivery, and it stored the flag when its handler borrowed
282        // the row.
283        !lent.load(Ordering::Relaxed)
284    }
285}
286
287/// A queue row that carries its message as bytes: payload mode.
288///
289/// The derive implements it for a struct with a `#[field(payload)]` field; a table described by
290/// hand implements it where its description sets [`InboxSpec::payload`]. A subscription hands the
291/// handler the payload decoded by a codec or a `Deserialized` type, as on any broker, and the
292/// bytes are lent from the row without a copy. Routes and by-name subscriptions read tables in
293/// this mode.
294///
295/// A struct without a payload field is in row mode: its handler takes the row itself, and the
296/// derive's [`insert`](crate::Insert::insert) or a [`Repository`] over the service's own
297/// [`Publish`] writes its tasks (see
298/// [row mode](https://powersemmi.github.io/ruststream-sqlx/latest/components/messages/rows/)).
299///
300/// # Examples
301///
302/// ```
303/// # #[cfg(feature = "postgres")]
304/// # mod demo {
305/// use ruststream::OutgoingMessage;
306/// use ruststream_sqlx::prelude::*;
307/// use serde::Deserialize;
308/// use sqlx::{PgConnection, PgPool, Postgres};
309///
310/// // `body` holds the message: `Job` is in payload mode, so a route can lead a name to it.
311/// #[derive(Inbox, sqlx::FromRow)]
312/// #[inbox(table = "jobs")]
313/// pub struct Job {
314///     #[field(id, generated)]
315///     id: i64,
316///     #[field(group)]
317///     name: String,
318///     #[field(payload)]
319///     body: Vec<u8>,
320/// }
321///
322/// impl Publish<Postgres> for Job {
323///     async fn publish(
324///         conn: &mut PgConnection,
325///         message: &OutgoingMessage<'_>,
326///     ) -> Result<(), sqlx::Error> {
327///         sqlx::query("INSERT INTO jobs (name, body) VALUES ($1, $2)")
328///             .bind(message.name())
329///             .bind(message.payload())
330///             .execute(conn)
331///             .await?;
332///         Ok(())
333///     }
334/// }
335///
336/// #[derive(Deserialize)]
337/// struct Email {
338///     to: String,
339/// }
340///
341/// // The codec decodes `Email` from the bytes lent from `body`.
342/// #[subscriber("emails")]
343/// async fn send(email: &Email) -> HandlerOutcome {
344///     tracing::info!(to = %email.to, "sending");
345///     HandlerOutcome::ack()
346/// }
347///
348/// pub fn app(pool: PgPool) -> RustStream {
349///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(
350///         SqlxBroker::new(pool).route::<Job>("emails"),
351///         |b| {
352///             b.include(send);
353///         },
354///     )
355/// }
356/// # }
357/// # fn main() {}
358/// ```
359///
360/// A table described by hand names the payload column's type, which a by-name subscription
361/// reads it in:
362///
363/// ```
364/// # #[cfg(all(feature = "sqlite", feature = "chrono"))]
365/// # mod demo {
366/// use chrono::{DateTime, Utc};
367/// use ruststream_sqlx::dialect::Column;
368/// use ruststream_sqlx::prelude::*;
369/// use ruststream_sqlx::spec::{Lease, Payload};
370/// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
371/// use serde::Deserialize;
372/// use sqlx::{Sqlite, SqlitePool};
373///
374/// #[derive(sqlx::FromRow)]
375/// pub struct Job {
376///     id: i64,
377///     body: Vec<u8>,
378/// }
379///
380/// impl InboxTable for Job {
381///     type Id = i64;
382///     type Table = InboxSpec<(Lease<DateTime<Utc>>, Payload)>;
383///     const TABLE: Self::Table = InboxSpec::new("jobs", Column::new("id").generated())
384///         .lease(Column::new("locked_until"))
385///         .group(Column::new("name"))
386///         .payload(Column::new("body"));
387///
388///     fn id(&self) -> &i64 {
389///         &self.id
390///     }
391/// }
392///
393/// impl PayloadRow for Job {
394///     type Column = Vec<u8>;
395///
396///     fn payload(&self) -> &[u8] {
397///         &self.body
398///     }
399/// }
400///
401/// #[derive(Deserialize)]
402/// struct Email {
403///     to: String,
404/// }
405///
406/// // The codec decodes `Email` from the bytes lent from `body`.
407/// #[subscriber(InboxQueue::<Job>::new("emails"))]
408/// async fn send(email: &Email) -> HandlerOutcome {
409///     tracing::info!(to = %email.to, "sending");
410///     HandlerOutcome::ack()
411/// }
412///
413/// pub fn app(pool: SqlitePool) -> RustStream {
414///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(
415///         SqlxBroker::<Sqlite>::new(pool),
416///         |b| {
417///             b.include(send);
418///         },
419///     )
420/// }
421/// # }
422/// # fn main() {}
423/// ```
424#[diagnostic::on_unimplemented(
425    message = "`{Self}` lends no payload, so no column holds a message's bytes",
426    label = "no `PayloadRow` for this row",
427    note = "a table in row mode: write its rows with the derive's `insert`, or with `Repository` \
428            over a `Publish` of the service's own",
429    note = "a route and a by-name subscription read a table whose `#[field(payload)]` field holds \
430            the message",
431    note = "a table described by hand that sets `.payload(..)` implements `PayloadRow` for the \
432            field the column decodes into"
433)]
434pub trait PayloadRow: QueueRow {
435    /// The payload column's type, as the row decodes it: `Vec<u8>` or `String`, which a by-name
436    /// subscription reads and writes the column as.
437    type Column: 'static;
438
439    /// The message bytes, lent from the row.
440    fn payload(&self) -> &[u8];
441}