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