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}