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#[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}