Skip to main content

ruststream_sqlx/inbox/
broker.rs

1//! `SqlxBroker`: its configuration and the connection it makes; the connected form and the closed
2//! witness live in `connected`.
3
4use std::borrow::Cow;
5use std::fmt;
6use std::num::NonZeroU32;
7use std::sync::Arc;
8use std::time::Duration;
9
10use ruststream::Broker;
11#[cfg(any(
12    feature = "postgres",
13    feature = "mysql",
14    feature = "sqlite",
15    feature = "any"
16))]
17use ruststream::{DescribeServer, ServerSpec};
18use ruststream_sqlx_dialect::{Dialect, Opens};
19#[cfg(feature = "any")]
20use sqlx::Any;
21#[cfg(any(feature = "postgres", feature = "mysql", feature = "any"))]
22use sqlx::ConnectOptions;
23#[cfg(feature = "mysql")]
24use sqlx::MySql;
25#[cfg(feature = "postgres")]
26use sqlx::Postgres;
27#[cfg(feature = "sqlite")]
28use sqlx::Sqlite;
29use sqlx::{Database, Pool};
30use tokio::runtime::Handle;
31
32use super::database::notify::{StartListening, start_listening};
33use super::database::{BuiltIn, BuiltInDialect, Notifies, QueueDatabase};
34use super::engine::Events;
35use super::error::SqlxBrokerError;
36use super::events::Publish;
37use super::publish::Routes;
38use super::{FormDialect, FormOn, PayloadRow};
39
40mod connected;
41
42pub(crate) use connected::Shared;
43pub use connected::{ClosedSqlxBroker, ConnectedSqlxBroker};
44
45/// How long a subscription waits between claims that found its queue empty, unless it names
46/// another interval.
47const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
48
49/// How long a claim leases a row of a table in the lease form, unless the subscription names
50/// another lease.
51const DEFAULT_LEASE: Duration = Duration::from_secs(30);
52
53/// The dialect a broker will build statements with: one built into the crate, picked from the
54/// connection `connect` checks, or the service's own.
55pub(crate) enum DialectChoice<DB: Database, D> {
56    Picked(fn(&DB::Connection) -> Result<D, SqlxBrokerError>),
57    Given(Arc<D>),
58}
59
60impl<DB: Database, D: Dialect> DialectChoice<DB, D> {
61    /// The dialect of the database `conn` reaches.
62    pub(crate) fn resolve(&self, conn: &DB::Connection) -> Result<Arc<D>, SqlxBrokerError> {
63        match self {
64            Self::Picked(pick) => pick(conn).map(Arc::new),
65            Self::Given(dialect) => Ok(Arc::clone(dialect)),
66        }
67    }
68
69    /// The dialect's name, once there is one to name.
70    fn name(&self) -> &'static str {
71        match self {
72            Self::Picked(_) => "built in, picked at connect",
73            Self::Given(dialect) => dialect.name(),
74        }
75    }
76}
77
78impl<DB: Database, D> Clone for DialectChoice<DB, D> {
79    fn clone(&self) -> Self {
80        match self {
81            Self::Picked(pick) => Self::Picked(*pick),
82            Self::Given(dialect) => Self::Given(Arc::clone(dialect)),
83        }
84    }
85}
86
87/// The built-in dialect of the database `conn` reaches, or the error that names a database no
88/// built-in dialect serves.
89fn built_in<DB: BuiltInDialect>(conn: &DB::Connection) -> Result<BuiltIn<DB>, SqlxBrokerError> {
90    DB::dialect(conn).ok_or_else(|| SqlxBrokerError::Backend {
91        backend: DB::backend(conn).to_owned(),
92    })
93}
94
95/// How a route reaches its table's form on the dialect the broker connects with.
96pub(crate) type FormOf<D> = fn(&Arc<D>) -> FormDialect;
97
98/// The inbox broker: task queues in the service's own tables, served through a sqlx pool.
99///
100/// [`new`](Self::new) records the pool and does no I/O; the pool belongs to the service, and the
101/// broker never closes it. [`connect`](Broker::connect) takes one connection to check the
102/// database, and picks the built-in dialect by it where the service passed none. A message in
103/// work holds one of the pool's connections until it settles (in the lease form outside
104/// transactional mode only while it settles), and a publish takes another, so the pool is sized
105/// for both.
106/// Subscriptions read tables through [`InboxQueue`](crate::InboxQueue) descriptors; publishing
107/// writes them through [`Repository`](crate::Repository) policies or through the routes this
108/// builder records.
109///
110/// # Examples
111///
112/// ```no_run
113/// # #[cfg(all(feature = "postgres", feature = "chrono"))]
114/// # mod demo {
115/// use std::error::Error;
116///
117/// use ruststream::OutgoingMessage;
118/// use ruststream::prelude::*;
119/// use ruststream_sqlx::{Inbox, InboxQueue, Publish, SqlxBroker};
120/// use serde::Deserialize;
121/// use sqlx::postgres::{PgConnectOptions, PgPool};
122/// use sqlx::{PgConnection, Postgres};
123///
124/// #[derive(Inbox, sqlx::FromRow)]
125/// #[inbox(table = "email_jobs")]
126/// pub struct SendEmail {
127///     #[field(id, generated)]
128///     job_id: i64,
129///     #[field(group)]
130///     name: String,
131///     #[field(payload)]
132///     payload: Vec<u8>,
133/// }
134///
135/// impl Publish<Postgres> for SendEmail {
136///     async fn publish(
137///         conn: &mut PgConnection,
138///         message: &OutgoingMessage<'_>,
139///     ) -> Result<(), sqlx::Error> {
140///         sqlx::query("INSERT INTO email_jobs (name, payload) VALUES ($1, $2)")
141///             .bind(message.name())
142///             .bind(message.payload())
143///             .execute(conn)
144///             .await?;
145///         Ok(())
146///     }
147/// }
148///
149/// #[derive(Deserialize)]
150/// struct Email {
151///     to: String,
152/// }
153///
154/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
155/// async fn send(email: &Email) -> HandlerOutcome {
156///     tracing::info!(to = %email.to, "sending");
157///     HandlerOutcome::ack()
158/// }
159///
160/// pub fn app(pool: PgPool) -> RustStream {
161///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(
162///         SqlxBroker::new(pool).route::<SendEmail>("emails"),
163///         |b| {
164///             b.include(send);
165///         },
166///     )
167/// }
168///
169/// // sqlx builds a pool only inside a Tokio runtime, so the service builds it before the app.
170/// // The service owns the pool and closes it.
171/// #[tokio::main]
172/// async fn main() -> Result<(), Box<dyn Error>> {
173///     let pool = PgPool::connect_with(PgConnectOptions::new()).await?;
174///     app(pool.clone()).run().await?;
175///     pool.close().await;
176///     Ok(())
177/// }
178/// # }
179/// # fn main() {}
180/// ```
181pub struct SqlxBroker<DB: Database, D = BuiltIn<DB>> {
182    pub(crate) pool: Pool<DB>,
183    pub(crate) dialect: DialectChoice<DB, D>,
184    routes: Routes<DB, FormOf<D>>,
185    poll_interval: Duration,
186    lease: Duration,
187    connection_limit: Option<NonZeroU32>,
188    /// The start of the listening connection, with `listen_notify`.
189    pub(crate) listen: Option<StartListening<DB>>,
190}
191
192impl<DB: Database, D: Dialect> fmt::Debug for SqlxBroker<DB, D> {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        f.debug_struct("SqlxBroker")
195            .field("dialect", &self.dialect.name())
196            .field("routes", &self.routes)
197            .field("poll_interval", &self.poll_interval)
198            .field("lease", &self.lease)
199            .field("connection_limit", &self.connection_limit)
200            .field("listen_notify", &self.listen.is_some())
201            .finish_non_exhaustive()
202    }
203}
204
205impl<DB: BuiltInDialect> SqlxBroker<DB, BuiltIn<DB>> {
206    /// A broker on `pool`, with the dialect built into the crate for its database.
207    ///
208    /// Synchronous and free of I/O: the database type comes from the pool, and nothing connects
209    /// before [`connect`](Broker::connect), which picks the dialect from the connection it checks.
210    /// An `AnyPool` reaches a database named only then, and a database whose dialect's feature is
211    /// off stops `connect` with [`SqlxBrokerError::Backend`].
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// # #[cfg(feature = "postgres")]
217    /// # mod demo {
218    /// use std::error::Error;
219    ///
220    /// use ruststream_sqlx::prelude::*;
221    /// use serde::Deserialize;
222    /// use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions};
223    ///
224    /// #[derive(Inbox, sqlx::FromRow)]
225    /// #[inbox(table = "email_jobs")]
226    /// pub struct SendEmail {
227    ///     #[field(id, generated)]
228    ///     job_id: i64,
229    ///     #[field(group)]
230    ///     name: String,
231    ///     #[field(payload)]
232    ///     payload: Vec<u8>,
233    /// }
234    ///
235    /// #[derive(Deserialize)]
236    /// struct Email {
237    ///     to: String,
238    /// }
239    ///
240    /// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
241    /// async fn send(email: &Email) -> HandlerOutcome {
242    ///     tracing::info!(to = %email.to, "sending");
243    ///     HandlerOutcome::ack()
244    /// }
245    ///
246    /// pub fn app(pool: PgPool) -> RustStream {
247    ///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
248    ///         b.include(send);
249    ///     })
250    /// }
251    ///
252    /// #[tokio::main]
253    /// pub async fn main() -> Result<(), Box<dyn Error>> {
254    ///     // Nothing connects here: the pool connects lazily, and the broker once the service
255    ///     // starts. sqlx builds even a lazy pool only inside a Tokio runtime, so it is built
256    ///     // here rather than in the app's builder. The options read the `PG*` environment
257    ///     // variables.
258    ///     let pool = PgPoolOptions::new().connect_lazy_with(PgConnectOptions::new());
259    ///     let app = app(pool);
260    /// #   // The example runs without a database: it stops before the app connects.
261    /// #   drop(app);
262    /// #   return Ok(());
263    ///     app.run().await?;
264    ///     Ok(())
265    /// }
266    /// # }
267    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
268    /// #     #[cfg(feature = "postgres")]
269    /// #     demo::main()?;
270    /// #     Ok(())
271    /// # }
272    /// ```
273    #[must_use]
274    pub fn new(pool: Pool<DB>) -> Self {
275        Self::built(pool, DialectChoice::Picked(built_in::<DB>))
276    }
277}
278
279impl<DB: QueueDatabase, D: Dialect + 'static> SqlxBroker<DB, D> {
280    /// A broker on `pool` whose statements `dialect`, a dialect of the service's own, builds: a
281    /// driver this crate has no dialect for, or a built-in dialect the service wraps to write a
282    /// statement its own way.
283    ///
284    /// The dialect's type is the broker's second type parameter, so what a subscription asks of
285    /// it is checked where the subscription mounts: a table in the row lock form needs the
286    /// dialect to implement [`RowLock`](crate::dialect::RowLock), a lease table
287    /// [`Lease`](crate::dialect::Lease), and a table that names an isolation level or a SQLite
288    /// mode [`Opens`] for it. The dialect runs while subscriptions start; messages travel through
289    /// the statements it built.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// # #[cfg(feature = "postgres")]
295    /// # mod demo {
296    /// # use std::num::NonZeroUsize;
297    /// # use ruststream_sqlx::dialect::{self, ClaimShape, Dialect, Opening, RowLock, Statement, StatementError, TableName, TableSpec};
298    /// use ruststream_sqlx::prelude::*;
299    /// use serde::Deserialize;
300    /// use sqlx::PgPool;
301    ///
302    /// /// Postgres with statements of the service's own, written out in the crate overview.
303    /// #[derive(Debug)]
304    /// pub struct Audited;
305    /// # impl Dialect for Audited {
306    /// #     fn name(&self) -> &'static str { "audited" }
307    /// #     fn quote_into(&self, ident: &str, out: &mut String) { dialect::Postgres.quote_into(ident, out) }
308    /// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { dialect::Postgres.placeholder_into(index, out) }
309    /// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { dialect::Postgres.fetch(spec) }
310    /// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { dialect::Postgres.ack(spec) }
311    /// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { dialect::Postgres.retry(spec) }
312    /// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { dialect::Postgres.retry_after(spec) }
313    /// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { dialect::Postgres.discard(spec) }
314    /// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { dialect::Postgres.dead_letter_group(spec) }
315    /// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { dialect::Postgres.dead_letter_table(spec, target) }
316    /// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { dialect::Postgres.insert(spec) }
317    /// #     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> { dialect::Postgres.begin(opening) }
318    /// #     fn fifo_guard(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { dialect::Postgres.fifo_guard(spec) }
319    /// # }
320    /// # impl RowLock for Audited {
321    /// #     fn lock_claim(&self, spec: &TableSpec<'_>, shape: ClaimShape) -> Result<Statement, StatementError> { dialect::Postgres.lock_claim(spec, shape) }
322    /// # }
323    ///
324    /// #[derive(Inbox, sqlx::FromRow)]
325    /// #[inbox(table = "email_jobs")]
326    /// pub struct SendEmail {
327    ///     #[field(id, generated)]
328    ///     job_id: i64,
329    ///     #[field(group)]
330    ///     name: String,
331    ///     #[field(payload)]
332    ///     payload: Vec<u8>,
333    /// }
334    ///
335    /// #[derive(Deserialize)]
336    /// struct Email {
337    ///     to: String,
338    /// }
339    ///
340    /// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
341    /// async fn send(email: &Email) -> HandlerOutcome {
342    ///     tracing::info!(to = %email.to, "sending");
343    ///     HandlerOutcome::ack()
344    /// }
345    ///
346    /// pub fn app(pool: PgPool) -> RustStream {
347    ///     // A `SqlxBroker<Postgres, Audited>`: `send` mounts only because `Audited` implements
348    ///     // `RowLock`, the form of `email_jobs`.
349    ///     RustStream::new(AppInfo::new("mailer", "0.1.0"))
350    ///         .with_broker(SqlxBroker::with_dialect(pool, Audited), |b| {
351    ///             b.include(send);
352    ///         })
353    /// }
354    /// # }
355    /// # fn main() {}
356    /// ```
357    #[must_use]
358    pub fn with_dialect(pool: Pool<DB>, dialect: D) -> Self {
359        Self::built(pool, DialectChoice::Given(Arc::new(dialect)))
360    }
361
362    fn built(pool: Pool<DB>, dialect: DialectChoice<DB, D>) -> Self {
363        Self {
364            pool,
365            dialect,
366            routes: Routes::default(),
367            poll_interval: DEFAULT_POLL_INTERVAL,
368            lease: DEFAULT_LEASE,
369            connection_limit: None,
370            listen: None,
371        }
372    }
373
374    /// Leads publishes to `name` into the table of `Row`, through its [`Publish`], and opens a
375    /// subscription by that name (`#[subscriber("name")]`) on the same table.
376    ///
377    /// A name ending in `*` leads every name that starts with what precedes it; an exact name
378    /// wins over a prefix, and a longer prefix over a shorter one. Routing a name twice leads it
379    /// to the last row type. The broker's default publisher is this route table: a reply or an
380    /// `Out` slot without a policy of its own publishes through it.
381    ///
382    /// A route asks of the broker's dialect what an [`InboxQueue`](crate::InboxQueue) of the row
383    /// asks: the table's form, and the isolation level or SQLite mode the struct declares. A
384    /// by-name subscription names no row type, so the route is where its table is checked.
385    ///
386    /// # Examples
387    ///
388    /// ```
389    /// # #[cfg(feature = "postgres")]
390    /// # mod demo {
391    /// use ruststream::OutgoingMessage;
392    /// use ruststream_sqlx::prelude::*;
393    /// use serde::{Deserialize, Serialize};
394    /// use sqlx::{PgConnection, PgPool, Postgres};
395    ///
396    /// #[derive(Inbox, sqlx::FromRow)]
397    /// #[inbox(table = "report_jobs")]
398    /// pub struct Report {
399    ///     #[field(id, generated)]
400    ///     id: i64,
401    ///     #[field(group)]
402    ///     name: String,
403    ///     #[field(payload)]
404    ///     payload: Vec<u8>,
405    /// }
406    ///
407    /// impl Publish<Postgres> for Report {
408    ///     async fn publish(
409    ///         conn: &mut PgConnection,
410    ///         message: &OutgoingMessage<'_>,
411    ///     ) -> Result<(), sqlx::Error> {
412    ///         sqlx::query("INSERT INTO report_jobs (name, payload) VALUES ($1, $2)")
413    ///             .bind(message.name())
414    ///             .bind(message.payload())
415    ///             .execute(conn)
416    ///             .await?;
417    ///         Ok(())
418    ///     }
419    /// }
420    ///
421    /// #[derive(Deserialize)]
422    /// struct Daily {
423    ///     day: u32,
424    /// }
425    ///
426    /// #[derive(Serialize, Outgoing)]
427    /// #[outgoing(name = "reports.weekly")]
428    /// struct Weekly {
429    ///     week: u32,
430    /// }
431    ///
432    /// // Read by name from `report_jobs`, answered with a row of `report_jobs` in group `reports.weekly`.
433    /// #[subscriber("reports.daily", reply)]
434    /// async fn roll_up(daily: &Daily) -> Weekly {
435    ///     Weekly { week: daily.day / 7 }
436    /// }
437    ///
438    /// pub fn app(pool: PgPool) -> RustStream {
439    ///     // `reports.daily` and `reports.weekly` both lead into `report_jobs`.
440    ///     let broker = SqlxBroker::new(pool).route::<Report>("reports.*");
441    ///     RustStream::new(AppInfo::new("reports", "0.1.0")).with_broker(broker, |b| {
442    ///         b.include(roll_up);
443    ///     })
444    /// }
445    /// # }
446    /// # fn main() {}
447    /// ```
448    #[must_use]
449    pub fn route<Row>(mut self, name: impl Into<Cow<'static, str>>) -> Self
450    where
451        Row: Publish<DB> + Events<DB> + PayloadRow,
452        Row::Form: FormOn<D>,
453        D: Opens<Row::Opening>,
454    {
455        self.routes
456            .add::<Row>(name.into(), <Row::Form as FormOn<D>>::erase);
457        self
458    }
459
460    /// How long a subscription waits between claims that found its queue empty; one second
461    /// unless set. A subscription's own [`poll_interval`](crate::InboxQueue::poll_interval)
462    /// overrides it. A publish through this broker wakes the subscriptions of its table and group
463    /// before then, and so does a notification with [`listen_notify`](Self::listen_notify)
464    /// ([waking a subscription](crate#waking-a-subscription)).
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// # #[cfg(feature = "postgres")]
470    /// # mod demo {
471    /// use std::time::Duration;
472    ///
473    /// use ruststream_sqlx::prelude::*;
474    /// use serde::Deserialize;
475    /// use sqlx::PgPool;
476    ///
477    /// #[derive(Inbox, sqlx::FromRow)]
478    /// #[inbox(table = "email_jobs")]
479    /// pub struct SendEmail {
480    ///     #[field(id, generated)]
481    ///     job_id: i64,
482    ///     #[field(group)]
483    ///     name: String,
484    ///     #[field(payload)]
485    ///     payload: Vec<u8>,
486    /// }
487    ///
488    /// #[derive(Deserialize)]
489    /// struct Email {
490    ///     to: String,
491    /// }
492    ///
493    /// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
494    /// async fn send(email: &Email) -> HandlerOutcome {
495    ///     tracing::info!(to = %email.to, "sending");
496    ///     HandlerOutcome::ack()
497    /// }
498    ///
499    /// pub fn app(pool: PgPool) -> RustStream {
500    ///     // Every queue on this broker checks twice a second when it runs dry.
501    ///     let broker = SqlxBroker::new(pool).poll_interval(Duration::from_millis(500));
502    ///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(broker, |b| {
503    ///         b.include(send);
504    ///     })
505    /// }
506    /// # }
507    /// # fn main() {}
508    /// ```
509    #[must_use]
510    pub const fn poll_interval(mut self, interval: Duration) -> Self {
511        self.poll_interval = interval;
512        self
513    }
514
515    /// How long a claim leases a row of a table in the lease form (one with a
516    /// `#[field(locked_until)]` field); thirty seconds unless set. A subscription's own
517    /// [`lease`](crate::InboxQueue::lease) overrides it, and a table in another form ignores it.
518    ///
519    /// A lease is whole seconds, at least one: a shorter one is rounded up. The subscription
520    /// extends the lease of every delivery in work each half lease, so a handler may run longer
521    /// than its lease; the lease is how long a row stays out of the queue after its process
522    /// crashed. A delivery dropped unsettled releases its row at once. A lease runs out under a
523    /// running handler only when its extensions fail or stop (the database out of reach, the
524    /// subscription closed, the broker [shut down](ruststream::ConnectedBroker::shutdown)): the
525    /// row then goes to the next claim, and the late settlement fails with
526    /// [`SqlxBrokerError::LeaseLost`].
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// # #[cfg(all(feature = "postgres", feature = "chrono"))]
532    /// # mod demo {
533    /// use std::time::Duration;
534    ///
535    /// use chrono::{DateTime, Utc};
536    /// use ruststream_sqlx::prelude::*;
537    /// use serde::Deserialize;
538    /// use sqlx::PgPool;
539    ///
540    /// #[derive(Inbox, sqlx::FromRow)]
541    /// #[inbox(table = "video_jobs")]
542    /// pub struct Transcode {
543    ///     #[field(id, generated)]
544    ///     id: i64,
545    ///     #[field(locked_until)]
546    ///     locked_until: Option<DateTime<Utc>>,
547    ///     #[field(payload)]
548    ///     payload: Vec<u8>,
549    /// }
550    ///
551    /// #[derive(Deserialize)]
552    /// struct Video {
553    ///     path: String,
554    /// }
555    ///
556    /// #[subscriber(InboxQueue::<Transcode>::new("videos"))]
557    /// async fn transcode(video: &Video) -> HandlerOutcome {
558    ///     tracing::info!(path = %video.path, "transcoding");
559    ///     HandlerOutcome::ack()
560    /// }
561    ///
562    /// pub fn app(pool: PgPool) -> RustStream {
563    ///     // A video whose process crashed goes back to the queue once its ten-second lease runs out.
564    ///     let broker = SqlxBroker::new(pool).lease(Duration::from_secs(10));
565    ///     RustStream::new(AppInfo::new("transcoder", "0.1.0")).with_broker(broker, |b| {
566    ///         b.include(transcode);
567    ///     })
568    /// }
569    /// # }
570    /// # fn main() {}
571    /// ```
572    #[must_use]
573    pub const fn lease(mut self, lease: Duration) -> Self {
574        self.lease = lease;
575        self
576    }
577
578    /// The most connections the service opens on the database: the pool's size, and the
579    /// connections of the dedicated threads of every subscription mounted with
580    /// [`on_threads`](crate::InboxSettings::on_threads). No limit unless set.
581    ///
582    /// Each such subscription counts its threads times their connections when it opens. One
583    /// that would take the sum past the limit refuses to open, with
584    /// [`SqlxBrokerError::ConnectionLimit`] naming the numbers, and the service does not start.
585    /// Set it to what the database lets this service open.
586    ///
587    /// # Examples
588    ///
589    /// ```
590    /// # #[cfg(feature = "postgres")]
591    /// # mod demo {
592    /// use ruststream::nonzero;
593    /// use ruststream_sqlx::prelude::*;
594    /// use sqlx::{PgPool, Postgres};
595    /// # #[derive(Inbox, sqlx::FromRow)]
596    /// # #[inbox(table = "scan_jobs")]
597    /// # pub struct ScanJob { #[field(id)] id: i64, #[field(payload)] payload: Vec<u8> }
598    /// # #[derive(serde::Deserialize)]
599    /// # struct Image { path: String }
600    ///
601    /// #[subscriber(InboxQueue::<ScanJob>::new("scans"))]
602    /// async fn scan(image: &Image, Ctx(pool): Ctx<keys::Pool<Postgres>>) -> HandlerOutcome {
603    ///     let noted = sqlx::query("INSERT INTO scanned (path) VALUES ($1)")
604    ///         .bind(&image.path)
605    ///         .execute(&pool)
606    ///         .await;
607    ///     if noted.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
608    /// }
609    ///
610    /// pub fn app(pool: PgPool) -> RustStream {
611    ///     // The database lets this service open 20 connections: the pool's and 4 threads' own.
612    ///     let broker = SqlxBroker::new(pool).connection_limit(nonzero!(20u32));
613    ///     RustStream::new(AppInfo::new("scanner", "0.1.0")).with_broker(broker, |b| {
614    ///         b.include(scan.on_threads(InboxThreads::new(nonzero!(4))));
615    ///     })
616    /// }
617    /// # }
618    /// # fn main() {}
619    /// ```
620    #[must_use]
621    pub const fn connection_limit(mut self, limit: NonZeroU32) -> Self {
622        self.connection_limit = Some(limit);
623        self
624    }
625
626    /// Wakes the broker's subscriptions on Postgres notifications: a row another process announced
627    /// with `pg_notify` is claimed at once, not on the next poll.
628    ///
629    /// The broker holds one connection of the pool for its life, from
630    /// [`connect`](Broker::connect) to [`shutdown`](ruststream::ConnectedBroker::shutdown), and
631    /// listens there.
632    /// Each subscription listens on the channel of its table, qualified with its schema, as it
633    /// opens; a notification whose payload names a group wakes that group's subscription, and one
634    /// with an empty payload wakes every subscription of the table. Each publish of the broker
635    /// sends `SELECT pg_notify(table, group)` on the connection that wrote the row, right after
636    /// the write: one more statement per publish. A publish whose notification fails is still a
637    /// publish; the row waits for the poll interval and the failure is logged.
638    ///
639    /// The poll interval stays: a notification sent while the listening connection was lost is
640    /// gone, so after a reconnect every subscription claims once, and a row written by the
641    /// service's own SQL or a handler's [`Tx`](crate::Tx) wakes nobody unless the service sends
642    /// the notification itself. It is opt-in because of its cost: the pool connection it holds,
643    /// the statement each publish adds, and the lock, global to the server, that Postgres takes
644    /// at the commit of each transaction that notifies, so those commits run one after another,
645    /// which limits many concurrent writers.
646    ///
647    /// A table whose qualified name is longer than Postgres's 63-byte channel names stops its
648    /// subscription at startup with [`SqlxBrokerError::Declaration`].
649    ///
650    /// # Examples
651    ///
652    /// ```
653    /// # #[cfg(feature = "postgres")]
654    /// # mod demo {
655    /// use std::time::Duration;
656    ///
657    /// use ruststream_sqlx::prelude::*;
658    /// use serde::Deserialize;
659    /// use sqlx::PgPool;
660    ///
661    /// #[derive(Inbox, sqlx::FromRow)]
662    /// #[inbox(table = "email_jobs")]
663    /// pub struct SendEmail {
664    ///     #[field(id, generated)]
665    ///     job_id: i64,
666    ///     #[field(group)]
667    ///     name: String,
668    ///     #[field(payload)]
669    ///     payload: Vec<u8>,
670    /// }
671    ///
672    /// #[derive(Deserialize)]
673    /// struct Email {
674    ///     to: String,
675    /// }
676    ///
677    /// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
678    /// async fn send(email: &Email) -> HandlerOutcome {
679    ///     tracing::info!(to = %email.to, "sending");
680    ///     HandlerOutcome::ack()
681    /// }
682    ///
683    /// pub fn app(pool: PgPool) -> RustStream {
684    ///     // A row another service writes and announces with
685    ///     // `SELECT pg_notify('email_jobs', 'emails')` is sent at once; the poll each minute
686    ///     // catches a row whose notification was lost.
687    ///     let broker = SqlxBroker::new(pool)
688    ///         .poll_interval(Duration::from_secs(60))
689    ///         .listen_notify();
690    ///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(broker, |b| {
691    ///         b.include(send);
692    ///     })
693    /// }
694    /// # }
695    /// # fn main() {}
696    /// ```
697    #[must_use]
698    pub fn listen_notify(mut self) -> Self
699    where
700        DB: Notifies,
701    {
702        self.listen = Some(start_listening::<DB>());
703        self
704    }
705}
706
707impl<DB: QueueDatabase, D: Dialect + 'static> Broker for SqlxBroker<DB, D> {
708    type Error = SqlxBrokerError;
709    type Connected = ConnectedSqlxBroker<DB, D>;
710
711    async fn connect(self) -> Result<Self::Connected, Self::Error> {
712        let conn = self
713            .pool
714            .acquire()
715            .await
716            .map_err(|source| SqlxBrokerError::Connect { source })?;
717        let dialect = self.dialect.resolve(&conn)?;
718        drop(conn);
719        let listening = match self.listen {
720            Some(start) => Some(
721                start(self.pool.clone())
722                    .await
723                    .map_err(|source| SqlxBrokerError::Connect { source })?,
724            ),
725            None => None,
726        };
727        Ok(ConnectedSqlxBroker::new(
728            self,
729            dialect,
730            Handle::current(),
731            listening,
732        ))
733    }
734}
735
736#[cfg(feature = "postgres")]
737impl<D: Dialect + 'static> DescribeServer for SqlxBroker<Postgres, D> {
738    fn describe_server(&self) -> ServerSpec {
739        ServerSpec::from_url(
740            self.pool.connect_options().to_url_lossy().as_str(),
741            "postgres",
742        )
743    }
744}
745
746#[cfg(feature = "mysql")]
747impl<D: Dialect + 'static> DescribeServer for SqlxBroker<MySql, D> {
748    fn describe_server(&self) -> ServerSpec {
749        ServerSpec::from_url(self.pool.connect_options().to_url_lossy().as_str(), "mysql")
750    }
751}
752
753/// SQLite runs inside the service, on a file or in memory: the description names the protocol and
754/// no host, so it holds neither the database's path nor any other part of its URL, and SQLite takes
755/// no credentials.
756#[cfg(feature = "sqlite")]
757impl<D: Dialect + 'static> DescribeServer for SqlxBroker<Sqlite, D> {
758    fn describe_server(&self) -> ServerSpec {
759        // Why not `from_url`: SQLite's URL holds a path, not a host, and sqlx's `to_url_lossy`
760        // panics on a database named `file:..`, the name `sqlite::memory:` takes.
761        // FIXME(sqlx-sqlite 0.9.0): `SqliteConnectOptions::to_url_lossy` panics with "BUG:
762        // generated un-parseable URL: InvalidPort" on every database named `file:..`. Once sqlx
763        // fixes it, the panic stops ruling out reading the URL here and this note goes; the
764        // description stays in-process, since SQLite has no host either way.
765        ServerSpec::in_process("sqlite")
766    }
767}
768
769/// An `AnyPool` is described by the scheme of its URL, as the broker of that database describes
770/// itself: the host and port of a server, or SQLite with no host.
771#[cfg(feature = "any")]
772impl<D: Dialect + 'static> DescribeServer for SqlxBroker<Any, D> {
773    fn describe_server(&self) -> ServerSpec {
774        // `Any` keeps the URL it was given, so reading it back is free of the SQLite panic above.
775        let url = self.pool.connect_options().to_url_lossy();
776        match url.scheme() {
777            "sqlite" => ServerSpec::in_process("sqlite"),
778            "postgres" | "postgresql" => ServerSpec::from_url(url.as_str(), "postgres"),
779            "mysql" | "mariadb" => ServerSpec::from_url(url.as_str(), "mysql"),
780            scheme => ServerSpec::from_url(url.as_str(), scheme),
781        }
782    }
783}