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};
58pub use transactional::{InboxMode, InboxSettings, Plain, Transactional, TransactionalStep, Tx};
59
60/// A struct that describes a queue table; `#[derive(Inbox)]` implements it.
61///
62/// The struct puts its table in one of two modes. In payload mode a `#[field(payload)]` field
63/// holds the message, and a codec decodes it for the handler ([`PayloadRow`]). In row mode the
64/// struct has no such field, and the handler takes the struct itself, `&Row` or `&[Row]` (see
65/// [Row mode](crate#row-mode)).
66///
67/// # Examples
68///
69/// ```
70/// # #[cfg(feature = "postgres")]
71/// # mod demo {
72/// use ruststream_sqlx::prelude::*;
73/// use serde::Deserialize;
74/// use sqlx::PgPool;
75///
76/// // The derive implements `InboxRow` for `SendEmail`: the table `app.email_jobs`, its id and
77/// // its payload column.
78/// #[derive(Inbox, sqlx::FromRow)]
79/// #[inbox(table = "email_jobs", schema = "app")]
80/// pub struct SendEmail {
81///     #[field(id)]
82///     job_id: i64,
83///     #[field(payload)]
84///     payload: Vec<u8>,
85/// }
86///
87/// #[derive(Deserialize)]
88/// struct Email {
89///     to: String,
90/// }
91///
92/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
93/// async fn send(email: &Email) -> HandlerOutcome {
94///     tracing::info!(to = %email.to, "sending");
95///     HandlerOutcome::ack()
96/// }
97///
98/// pub fn app(pool: PgPool) -> RustStream {
99///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
100///         b.include(send);
101///     })
102/// }
103/// # }
104/// # fn main() {}
105/// ```
106#[diagnostic::on_unimplemented(
107    message = "`{Self}` does not describe a queue table",
108    label = "not an inbox row",
109    note = "derive it: `#[derive(Inbox)]` with `#[inbox(table = \"..\")]` and a `#[field(id)]` field, \
110            or describe the table by hand: `impl InboxTable`"
111)]
112pub trait InboxRow: QueueRow {
113    /// The table the struct describes: its name, its columns and their roles, and the form its
114    /// rows are claimed in.
115    const SPEC: TableSpec<'static>;
116
117    /// The form the table's rows are claimed in, as a type: a subscription requires its dialect
118    /// to serve it ([`FormOn`]). Machinery; the crate reads it off the table's description.
119    #[doc(hidden)]
120    type Form;
121
122    /// What the table's transactions open at, as a type: a [`level`](crate::dialect::level)
123    /// marker for the isolation level or SQLite mode the struct declares, `()` where it declares
124    /// neither. A subscription requires its dialect to open it
125    /// ([`Opens`](crate::dialect::Opens)). Machinery; the crate reads it off the table's
126    /// description.
127    #[doc(hidden)]
128    type Opening;
129}
130
131/// A row a subscription delivers, the type of its id, and how its message reaches a handler.
132/// Machinery: `#[derive(Inbox)]` implements it beside [`InboxRow`], and the row of a by-name
133/// subscription implements it without a table description of its own.
134#[doc(hidden)]
135pub trait QueueRow: Sized + Send + Sync + 'static {
136    /// The type of the field that plays `id`; logs name a row by it, and a lease subscription
137    /// keeps a copy of each id in work to extend its lease.
138    type Id: Clone + Debug + Send + Sync + 'static;
139
140    /// How a delivery hands the row's message to its handler: [`PayloadLane`] for a struct with a
141    /// `#[field(payload)]` field, [`RowLane`] for one without.
142    type Lane: Lane<Self>;
143}
144
145/// How a delivery hands a row's message to its handler. Machinery; the crate's two lanes are the
146/// only ones.
147#[doc(hidden)]
148pub trait Lane<Row>: Send + Sync + 'static {
149    /// Whether the handler takes the row itself.
150    const ROWS: bool;
151
152    /// What a delivery keeps of whether its handler borrowed the row: nothing in payload mode, a
153    /// flag in row mode.
154    type Lent: Default + Send + Sync;
155
156    /// The bytes a delivery lends as its payload: the payload column in payload mode, none in row
157    /// mode.
158    fn payload(row: &Row) -> &[u8];
159
160    /// Whether a payload read now comes from a delivery whose row its handler never borrowed, by
161    /// what `lent` keeps: a handler that decodes a payload, mounted on a table in row mode. Never
162    /// in payload mode.
163    fn unlent(lent: &Self::Lent) -> bool;
164}
165
166/// Payload mode: the handler takes the payload decoded by a codec.
167#[doc(hidden)]
168#[derive(Debug)]
169pub enum PayloadLane {}
170
171/// Row mode: the handler takes the row itself, `&Row`, or a batch of them, `&[Row]`.
172#[doc(hidden)]
173#[derive(Debug)]
174pub enum RowLane {}
175
176impl<Row: PayloadRow> Lane<Row> for PayloadLane {
177    const ROWS: bool = false;
178
179    // Nothing to keep, so the field takes no room in a payload-mode delivery.
180    type Lent = ();
181
182    fn payload(row: &Row) -> &[u8] {
183        row.payload()
184    }
185
186    fn unlent((): &()) -> bool {
187        false
188    }
189}
190
191// No `Clone` here: the carried lane asks it of the row where the derive writes `Input`, and one
192// check gives one error.
193impl<Row> Lane<Row> for RowLane {
194    const ROWS: bool = true;
195
196    type Lent = AtomicBool;
197
198    fn payload(_row: &Row) -> &[u8] {
199        &[]
200    }
201
202    fn unlent(lent: &AtomicBool) -> bool {
203        // Relaxed: one task handles a delivery, and it stored the flag when its handler borrowed
204        // the row.
205        !lent.load(Ordering::Relaxed)
206    }
207}
208
209/// A queue row that carries its message as bytes: payload mode.
210///
211/// The derive implements it for a struct with a `#[field(payload)]` field; a table described by
212/// hand implements it where its description sets [`InboxSpec::payload`]. A subscription hands the
213/// handler the payload decoded by a codec or a `Deserialized` type, as on any broker, and the
214/// bytes are lent from the row without a copy. Routes and by-name subscriptions read tables in
215/// this mode.
216///
217/// A struct without a payload field is in row mode: its handler takes the row itself, and the
218/// derive's [`insert`](crate::Insert::insert) or a [`Repository`] over the service's own
219/// [`Publish`] writes its tasks (see [Row mode](crate#row-mode)).
220///
221/// # Examples
222///
223/// ```
224/// # #[cfg(feature = "postgres")]
225/// # mod demo {
226/// use ruststream::OutgoingMessage;
227/// use ruststream_sqlx::prelude::*;
228/// use serde::Deserialize;
229/// use sqlx::{PgConnection, PgPool, Postgres};
230///
231/// // `body` holds the message: `Job` is in payload mode, so a route can lead a name to it.
232/// #[derive(Inbox, sqlx::FromRow)]
233/// #[inbox(table = "jobs")]
234/// pub struct Job {
235///     #[field(id, generated)]
236///     id: i64,
237///     #[field(group)]
238///     name: String,
239///     #[field(payload)]
240///     body: Vec<u8>,
241/// }
242///
243/// impl Publish<Postgres> for Job {
244///     async fn publish(
245///         conn: &mut PgConnection,
246///         message: &OutgoingMessage<'_>,
247///     ) -> Result<(), sqlx::Error> {
248///         sqlx::query("INSERT INTO jobs (name, body) VALUES ($1, $2)")
249///             .bind(message.name())
250///             .bind(message.payload())
251///             .execute(conn)
252///             .await?;
253///         Ok(())
254///     }
255/// }
256///
257/// #[derive(Deserialize)]
258/// struct Email {
259///     to: String,
260/// }
261///
262/// // The codec decodes `Email` from the bytes lent from `body`.
263/// #[subscriber("emails")]
264/// async fn send(email: &Email) -> HandlerOutcome {
265///     tracing::info!(to = %email.to, "sending");
266///     HandlerOutcome::ack()
267/// }
268///
269/// pub fn app(pool: PgPool) -> RustStream {
270///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(
271///         SqlxBroker::new(pool).route::<Job>("emails"),
272///         |b| {
273///             b.include(send);
274///         },
275///     )
276/// }
277/// # }
278/// # fn main() {}
279/// ```
280///
281/// A table described by hand names the payload column's type, which a by-name subscription
282/// reads it in:
283///
284/// ```
285/// # #[cfg(all(feature = "sqlite", feature = "chrono"))]
286/// # mod demo {
287/// use chrono::{DateTime, Utc};
288/// use ruststream_sqlx::dialect::Column;
289/// use ruststream_sqlx::prelude::*;
290/// use ruststream_sqlx::spec::{Lease, Payload};
291/// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
292/// use serde::Deserialize;
293/// use sqlx::{Sqlite, SqlitePool};
294///
295/// #[derive(sqlx::FromRow)]
296/// pub struct Job {
297///     id: i64,
298///     body: Vec<u8>,
299/// }
300///
301/// impl InboxTable for Job {
302///     type Id = i64;
303///     type Table = InboxSpec<(Lease<DateTime<Utc>>, Payload)>;
304///     const TABLE: Self::Table = InboxSpec::new("jobs", Column::new("id").generated())
305///         .lease(Column::new("locked_until"))
306///         .group(Column::new("name"))
307///         .payload(Column::new("body"));
308///
309///     fn id(&self) -> &i64 {
310///         &self.id
311///     }
312/// }
313///
314/// impl PayloadRow for Job {
315///     type Column = Vec<u8>;
316///
317///     fn payload(&self) -> &[u8] {
318///         &self.body
319///     }
320/// }
321///
322/// #[derive(Deserialize)]
323/// struct Email {
324///     to: String,
325/// }
326///
327/// // The codec decodes `Email` from the bytes lent from `body`.
328/// #[subscriber(InboxQueue::<Job>::new("emails"))]
329/// async fn send(email: &Email) -> HandlerOutcome {
330///     tracing::info!(to = %email.to, "sending");
331///     HandlerOutcome::ack()
332/// }
333///
334/// pub fn app(pool: SqlitePool) -> RustStream {
335///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(
336///         SqlxBroker::<Sqlite>::new(pool),
337///         |b| {
338///             b.include(send);
339///         },
340///     )
341/// }
342/// # }
343/// # fn main() {}
344/// ```
345#[diagnostic::on_unimplemented(
346    message = "`{Self}` lends no payload, so no column holds a message's bytes",
347    label = "no `PayloadRow` for this row",
348    note = "a table in row mode: write its rows with the derive's `insert`, or with `Repository` \
349            over a `Publish` of the service's own",
350    note = "a route and a by-name subscription read a table whose `#[field(payload)]` field holds \
351            the message",
352    note = "a table described by hand that sets `.payload(..)` implements `PayloadRow` for the \
353            field the column decodes into"
354)]
355pub trait PayloadRow: QueueRow {
356    /// The payload column's type, as the row decodes it: `Vec<u8>` or `String`, which a by-name
357    /// subscription reads and writes the column as.
358    type Column: 'static;
359
360    /// The message bytes, lent from the row.
361    fn payload(&self) -> &[u8];
362}