ruststream_sqlx/inbox/publish/routed.rs
1//! `Routed`: the broker's default publish policy, which publishes where the route table leads a
2//! name.
3
4use std::fmt;
5use std::future::{Future, ready};
6use std::sync::Arc;
7
8use ruststream::{DefaultPublish, Lend, OutgoingMessage, PairError, PublishPolicy, Publisher};
9use ruststream_sqlx_dialect::Dialect;
10use sqlx::Database;
11
12use super::insert_routed;
13use crate::inbox::broker::{ConnectedSqlxBroker, Shared};
14use crate::inbox::database::QueueDatabase;
15use crate::inbox::error::SqlxBrokerError;
16
17/// The broker's default publish policy: a publish goes where the route table leads its name.
18///
19/// A name given per message costs one hash lookup and one dynamic call into the row's
20/// [`Publish`](crate::Publish). Prefix routes are scanned only when no route names the message
21/// exactly. The call's future stays in place, in 1024 bytes, so a publish allocates what a
22/// [`Repository`](crate::Repository) publish does. A row whose `Publish` future is larger is
23/// boxed: one allocation per publish on its route. A name no route leads anywhere fails the
24/// publish with [`SqlxBrokerError::NoRoute`] and logs a warning naming it. Replies and `Out` slots
25/// without a policy of their own publish through it.
26///
27/// # Examples
28///
29/// ```
30/// # #[cfg(feature = "postgres")]
31/// # mod demo {
32/// use ruststream::OutgoingMessage;
33/// use ruststream::prelude::*;
34/// use ruststream_sqlx::{Inbox, InboxQueue, Publish, Routed, SqlxBroker};
35/// use serde::{Deserialize, Serialize};
36/// use sqlx::{PgConnection, PgPool, Postgres};
37///
38/// #[derive(Inbox, sqlx::FromRow)]
39/// #[inbox(table = "jobs")]
40/// pub struct Job {
41/// #[field(id, generated)]
42/// id: i64,
43/// #[field(group)]
44/// name: String,
45/// #[field(payload)]
46/// payload: Vec<u8>,
47/// }
48///
49/// impl Publish<Postgres> for Job {
50/// async fn publish(
51/// conn: &mut PgConnection,
52/// message: &OutgoingMessage<'_>,
53/// ) -> Result<(), sqlx::Error> {
54/// sqlx::query("INSERT INTO jobs (name, payload) VALUES ($1, $2)")
55/// .bind(message.name())
56/// .bind(message.payload())
57/// .execute(conn)
58/// .await?;
59/// Ok(())
60/// }
61/// }
62///
63/// #[derive(Deserialize)]
64/// struct Order {
65/// id: u64,
66/// }
67///
68/// #[derive(Serialize, Outgoing)]
69/// #[outgoing(name = "invoices")]
70/// struct Invoice {
71/// order: u64,
72/// }
73///
74/// #[subscriber(InboxQueue::<Job>::new("orders"), reply)]
75/// async fn bill(order: &Order) -> Invoice {
76/// Invoice { order: order.id }
77/// }
78///
79/// pub fn app(pool: PgPool) -> RustStream {
80/// let broker = SqlxBroker::new(pool).route::<Job>("invoices");
81/// RustStream::new(AppInfo::new("billing", "0.1.0")).with_broker(broker, |b| {
82/// b.include(bill).out_reply(Routed);
83/// })
84/// }
85/// # }
86/// # fn main() {}
87/// ```
88#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
89pub struct Routed;
90
91impl<DB: QueueDatabase, D: Dialect + 'static> PublishPolicy<ConnectedSqlxBroker<DB, D>> for Routed {
92 type Live = RoutedPublisher<DB>;
93
94 fn pair(
95 self,
96 connected: &ConnectedSqlxBroker<DB, D>,
97 ) -> impl Future<Output = Result<Self::Live, PairError>> + Send {
98 ready(Ok(RoutedPublisher {
99 shared: Arc::clone(&connected.shared),
100 }))
101 }
102}
103
104impl<DB: QueueDatabase, D: Dialect + 'static> DefaultPublish for ConnectedSqlxBroker<DB, D> {
105 type Policy = Routed;
106}
107
108/// The live form of [`Routed`]: publishes into the table the route table leads each name to.
109///
110/// # Examples
111///
112/// ```no_run
113/// # #[cfg(feature = "postgres")]
114/// # mod demo {
115/// use ruststream::OutgoingMessage;
116/// use ruststream_sqlx::prelude::*;
117/// use serde::Serialize;
118/// use sqlx::{PgConnection, PgPool, Postgres};
119///
120/// #[derive(Inbox, sqlx::FromRow)]
121/// #[inbox(table = "jobs")]
122/// pub struct Job {
123/// #[field(id, generated)]
124/// id: i64,
125/// #[field(group)]
126/// name: String,
127/// #[field(payload)]
128/// payload: Vec<u8>,
129/// }
130///
131/// impl Publish<Postgres> for Job {
132/// async fn publish(
133/// conn: &mut PgConnection,
134/// message: &OutgoingMessage<'_>,
135/// ) -> Result<(), sqlx::Error> {
136/// sqlx::query("INSERT INTO jobs (name, payload) VALUES ($1, $2)")
137/// .bind(message.name())
138/// .bind(message.payload())
139/// .execute(conn)
140/// .await?;
141/// Ok(())
142/// }
143/// }
144///
145/// #[derive(Serialize, Outgoing)]
146/// #[outgoing(name = "orders")]
147/// pub struct Order {
148/// id: u64,
149/// }
150///
151/// pub async fn run(pool: PgPool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
152/// let broker = SqlxBroker::new(pool).route::<Job>("orders").bindable();
153/// let orders = broker.bind(Routed);
154/// let running = RustStream::new(AppInfo::new("shop", "0.1.0"))
155/// .with_broker(broker, |_b| {})
156/// .start()
157/// .await?;
158///
159/// // The HTTP task owns this `RoutedPublisher`: each order becomes a row of `jobs`.
160/// let publisher = running.publisher(orders).await?;
161/// publisher.message(&Order { id: 7 }).publish().await?;
162///
163/// running.shutdown().await?;
164/// Ok(())
165/// }
166/// # }
167/// # fn main() {}
168/// ```
169pub struct RoutedPublisher<DB: Database> {
170 shared: Arc<Shared<DB>>,
171}
172
173impl<DB: Database> fmt::Debug for RoutedPublisher<DB> {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.debug_struct("RoutedPublisher")
176 .field("routes", &self.shared.routes)
177 .finish()
178 }
179}
180
181impl<DB: QueueDatabase> Publisher for RoutedPublisher<DB> {
182 type Payload = Lend;
183 type Error = SqlxBrokerError;
184 type Options = ();
185
186 async fn publish(
187 &self,
188 msg: OutgoingMessage<'_>,
189 _options: Option<&()>,
190 ) -> Result<(), SqlxBrokerError> {
191 let Some((route, wake)) = self.shared.routes.find_with_wake(msg.name()) else {
192 tracing::warn!(
193 target: "ruststream_sqlx",
194 name = msg.name(),
195 "no route leads the name to a table; the publish fails",
196 );
197 return Err(SqlxBrokerError::NoRoute {
198 name: msg.name().to_owned(),
199 });
200 };
201 #[cfg(feature = "testing")]
202 self.shared.harness.expect(msg.name());
203 let inserted = insert_routed(&self.shared, route, wake, &msg).await;
204 #[cfg(feature = "testing")]
205 match &inserted {
206 Ok(()) => self.shared.harness.published(&msg),
207 Err(_) => self.shared.harness.refused(msg.name()),
208 }
209 inserted
210 }
211}