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}