Skip to main content

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}