Skip to main content

ruststream_lapin/
publish_policy.rs

1//! The declaration half of publishing: the policies and what they pair into.
2//!
3//! A policy holds nothing but publish options, so it is constructible anywhere - in a router
4//! definition, in configuration, before anything connects. Pairing it with a
5//! [`ConnectedLapinBroker`] produces the live publisher (see [`crate::publisher`]), which is the
6//! only value with a publish surface. The publishing mode is a policy transition:
7//! [`LapinPublish::confirms`] and [`LapinPublish::server_tx`] move to the transactional policies,
8//! keeping the options.
9
10use std::future::ready;
11use std::time::Duration;
12
13#[cfg(feature = "asyncapi")]
14use ruststream::asyncapi::Bindings;
15use ruststream::{PairError, PublishPolicy};
16
17use crate::broker::{ConnectedLapinBroker, check_server_tx};
18#[cfg(feature = "testing")]
19use crate::error::AmqpError;
20#[cfg(feature = "testing")]
21use crate::in_process::Bus;
22use crate::publish_step::LapinPublishOptions;
23#[cfg(feature = "testing")]
24use crate::publisher::Buffered;
25use crate::publisher::{ConfirmsPublisher, LapinPublisher, ServerTxPublisher};
26
27use self::sealed::Sealed;
28
29pub(crate) mod sealed {
30    /// Seals [`LapinPublishPolicy`](super::LapinPublishPolicy): pairing an AMQP publisher opens
31    /// no channel of its own, and the synchronous
32    /// [`publisher`](crate::ConnectedLapinBroker::publisher) accessor depends on that.
33    use crate::requester::LapinRequest;
34
35    pub trait Sealed {}
36
37    impl Sealed for super::LapinPublish {}
38    impl Sealed for super::ConfirmsPublish {}
39    impl Sealed for super::ServerTxPublish {}
40    impl Sealed for LapinRequest {}
41}
42
43/// What every `RabbitMQ` publish policy carries: where to publish, and the per-message properties
44/// a publish through it takes unless its own call site says otherwise.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub(crate) struct PublishOptions {
47    pub(crate) exchange: String,
48    pub(crate) defaults: LapinPublishOptions,
49}
50
51impl PublishOptions {
52    /// The settings one publish carries: what its call site adjusted, over the policy's defaults.
53    pub(crate) fn resolve(&self, call: Option<&LapinPublishOptions>) -> LapinPublishOptions {
54        call.map_or(self.defaults, |call| call.over(&self.defaults))
55    }
56}
57
58#[cfg(feature = "testing")]
59impl PublishOptions {
60    /// Publishes `buffered` onto the in-process transport in order, each message's settings
61    /// resolved over these, and stops at the first one the transport refuses: the messages already
62    /// published stay published, as they do when a live flush stops.
63    pub(crate) fn flush_in_process(
64        &self,
65        bus: &Bus,
66        buffered: &[Buffered],
67    ) -> Result<(), AmqpError> {
68        for entry in buffered {
69            bus.publish(
70                &self.exchange,
71                &entry.routing_key,
72                &entry.payload,
73                &entry.headers,
74                &self.resolve(Some(&entry.options)),
75            )?;
76        }
77        Ok(())
78    }
79}
80
81impl Default for PublishOptions {
82    fn default() -> Self {
83        Self {
84            exchange: String::new(),
85            defaults: LapinPublishOptions {
86                persistent: Some(true),
87                ..LapinPublishOptions::default()
88            },
89        }
90    }
91}
92
93/// Writes the four policy setters every publish policy of this crate shares.
94///
95/// They differ only in the type they return, and a policy is a newtype over [`PublishOptions`],
96/// so the bodies would be four copies each. The documentation is written once here and reaches
97/// every policy's rustdoc.
98macro_rules! publish_policy_settings {
99    ($policy:ident) => {
100        impl $policy {
101            /// What this policy hands the publisher it pairs into.
102            ///
103            /// The publishers take the value by move at `bind`; this borrow is for the document,
104            /// which reads it.
105            #[cfg(feature = "asyncapi")]
106            pub(crate) const fn publish_options(&self) -> &PublishOptions {
107                &self.0
108            }
109
110            /// Publishes to `exchange` instead of the default exchange.
111            pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
112                self.0.exchange = exchange.into();
113                self
114            }
115
116            /// Whether messages are marked persistent (delivery mode 2). Defaults to `true`.
117            ///
118            /// One message departs from this with the publish builder's
119            /// [`persistent`](crate::LapinPublishSteps::persistent) step.
120            pub fn persistent(mut self, persistent: bool) -> Self {
121                self.0.defaults.persistent = Some(persistent);
122                self
123            }
124
125            /// The AMQP `priority` property messages carry. Unset by default, so the broker sees
126            /// no priority at all.
127            ///
128            /// It only orders deliveries on a queue declared with `x-max-priority`. One message
129            /// departs from this with the publish builder's
130            /// [`priority`](crate::LapinPublishSteps::priority) step.
131            pub fn priority(mut self, priority: u8) -> Self {
132                self.0.defaults.priority = Some(priority);
133                self
134            }
135
136            /// The AMQP per-message `expiration` (TTL) messages carry: the broker drops one once
137            /// `ttl` has passed without it being consumed. Unset by default, so messages do not
138            /// expire.
139            ///
140            /// One message departs from this with the publish builder's
141            /// [`expiration`](crate::LapinPublishSteps::expiration) step.
142            pub fn expiration(mut self, ttl: Duration) -> Self {
143                self.0.defaults.expiration = Some(ttl);
144                self
145            }
146        }
147    };
148}
149
150/// Writes the three document methods every publish policy of this crate answers the same way.
151///
152/// A policy describes the channel it publishes to, the properties its messages carry, and where a
153/// client reads the address of an answer: this crate routes a reply by the `reply-to` header
154/// whichever publisher carries it. The bodies are the same for every policy, and the core copies
155/// nothing between them, so they are written once here.
156///
157/// The core names the hooks' parameter `channel`, the `AsyncAPI` word for what a message is
158/// published to. It is the routing key here, and `channel` is an AMQP connection's own word in
159/// this crate, so the parameter is `destination` on this side of the call.
160macro_rules! publish_policy_bindings {
161    () => {
162        #[cfg(feature = "asyncapi")]
163        fn channel_bindings(&self, destination: &str) -> Bindings {
164            crate::bindings::publish_channel(self.publish_options(), destination)
165        }
166
167        #[cfg(feature = "asyncapi")]
168        fn operation_bindings(&self, _destination: &str) -> Bindings {
169            crate::bindings::publish_operation(self.publish_options())
170        }
171
172        #[cfg(feature = "asyncapi")]
173        fn reply_address_location(&self) -> Option<&'static str> {
174            Some(crate::bindings::REPLY_ADDRESS_LOCATION)
175        }
176    };
177}
178
179pub(crate) use publish_policy_bindings;
180
181/// A publish policy that pairs with a connected `RabbitMQ` broker without opening a channel.
182///
183/// All of this crate's policies hold nothing but publish options, so bringing one alive is a
184/// constructor call rather than broker work. That is what lets
185/// [`ConnectedLapinBroker::publisher`] be synchronous; [`PublishPolicy::pair`], the
186/// framework-side entry point, delegates here.
187pub trait LapinPublishPolicy: PublishPolicy<ConnectedLapinBroker> + Sealed {
188    /// Pairs the policy with the connected broker, producing the live publisher.
189    #[must_use]
190    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live;
191}
192
193/// The fire-and-forget publish policy: pure declaration, constructible anywhere.
194///
195/// [`OutgoingMessage::name`](ruststream::OutgoingMessage::name) is the routing key; the target
196/// exchange is a property of the policy (the default exchange unless
197/// [`exchange`](Self::exchange) says otherwise). On the default exchange the routing key
198/// addresses the queue with that name.
199///
200/// The policy is also where the per-message AMQP properties get their defaults:
201/// [`persistent`](Self::persistent), [`priority`](Self::priority) and
202/// [`expiration`](Self::expiration) fix what every publish through it carries, and the publish
203/// builder's own steps ([`LapinPublishSteps`](crate::LapinPublishSteps)) adjust one message over
204/// them.
205///
206/// It pairs into [`LapinPublisher`], and it is the broker's
207/// [`DefaultPublish`](ruststream::DefaultPublish) policy, so a `reply("dest")` handler mounted
208/// without an explicit `out_reply` policy replies through it. [`confirms`](Self::confirms) and
209/// [`server_tx`](Self::server_tx) move to the transactional policies, keeping the settings.
210///
211/// # Examples
212///
213/// ```
214/// use ruststream_lapin::prelude::*;
215/// use serde::{Deserialize, Serialize};
216///
217/// #[derive(Deserialize)]
218/// struct Order {
219///     id: u64,
220/// }
221///
222/// #[derive(Serialize, Outgoing)]
223/// #[outgoing(name = "order.placed")]
224/// struct OrderPlaced {
225///     id: u64,
226/// }
227///
228/// #[subscriber("orders")]
229/// async fn announce(order: &Order, Out(events): Out<impl Publisher>) -> HandlerOutcome {
230///     let placed = OrderPlaced { id: order.id };
231///     if events.message(&placed).publish().await.is_err() {
232///         return HandlerOutcome::retry();
233///     }
234///     HandlerOutcome::ack()
235/// }
236///
237/// // Every announcement goes to the `events` exchange under routing key `order.placed`.
238/// fn app() -> RustStream {
239///     let broker = LapinBroker::new("amqp://localhost:5672");
240///     let events = LapinPublish::default().exchange("events").priority(3);
241///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
242///         b.include(announce).out(DefaultSlot, events).build();
243///     })
244/// }
245/// ```
246#[derive(Debug, Clone, Default, PartialEq, Eq)]
247#[must_use]
248pub struct LapinPublish(PublishOptions);
249
250impl LapinPublish {
251    /// Moves to the policy that awaits broker confirms, with buffering transactions.
252    ///
253    /// The recommended transactional publisher: durable and much faster than AMQP server
254    /// transactions.
255    pub fn confirms(self) -> ConfirmsPublish {
256        ConfirmsPublish(self.0)
257    }
258
259    /// Moves to the policy backed by AMQP server transactions (`tx.select`).
260    ///
261    /// Server-side atomicity, at the cost of a synchronous commit round trip that is
262    /// significantly slower than [`confirms`](Self::confirms).
263    pub fn server_tx(self) -> ServerTxPublish {
264        ServerTxPublish(self.0)
265    }
266}
267
268publish_policy_settings!(LapinPublish);
269
270impl PublishPolicy<ConnectedLapinBroker> for LapinPublish {
271    type Live = LapinPublisher;
272
273    fn pair(
274        self,
275        connected: &ConnectedLapinBroker,
276    ) -> impl Future<Output = Result<Self::Live, PairError>> {
277        ready(Ok(self.bind(connected)))
278    }
279
280    publish_policy_bindings!();
281}
282
283impl LapinPublishPolicy for LapinPublish {
284    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
285        LapinPublisher::new(connected, self.0)
286    }
287}
288
289/// The confirm-transactional publish policy: same options as [`LapinPublish`], pairing into
290/// [`ConfirmsPublisher`].
291///
292/// Reached with [`LapinPublish::confirms`].
293///
294/// # Examples
295///
296/// ```
297/// use ruststream_lapin::prelude::*;
298/// use serde::{Deserialize, Serialize};
299///
300/// #[derive(Deserialize)]
301/// struct Order {
302///     id: u64,
303/// }
304///
305/// #[derive(Serialize, Outgoing)]
306/// #[outgoing(name = "shipment.requested")]
307/// struct Shipment {
308///     order_id: u64,
309/// }
310///
311/// // The order is acknowledged only once the broker confirmed the shipment.
312/// #[subscriber("orders")]
313/// async fn ship(order: &Order, Out(shipments): Out<impl TransactionalPublisher>) -> HandlerOutcome {
314///     let shipment = Shipment { order_id: order.id };
315///     if shipments.message(&shipment).publish().await.is_err() {
316///         return HandlerOutcome::retry();
317///     }
318///     HandlerOutcome::ack()
319/// }
320///
321/// fn app() -> RustStream {
322///     let broker = LapinBroker::new("amqp://localhost:5672");
323///     let shipments = LapinPublish::default().exchange("shipments").confirms();
324///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
325///         b.include(ship).out(DefaultSlot, shipments).build();
326///     })
327/// }
328/// ```
329#[derive(Debug, Clone, Default, PartialEq, Eq)]
330#[must_use]
331pub struct ConfirmsPublish(PublishOptions);
332
333publish_policy_settings!(ConfirmsPublish);
334
335impl PublishPolicy<ConnectedLapinBroker> for ConfirmsPublish {
336    type Live = ConfirmsPublisher;
337
338    fn pair(
339        self,
340        connected: &ConnectedLapinBroker,
341    ) -> impl Future<Output = Result<Self::Live, PairError>> {
342        ready(Ok(self.bind(connected)))
343    }
344
345    publish_policy_bindings!();
346}
347
348impl LapinPublishPolicy for ConfirmsPublish {
349    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
350        ConfirmsPublisher::new(connected, self.0)
351    }
352}
353
354/// The server-transactional publish policy: same options as [`LapinPublish`], pairing into
355/// [`ServerTxPublisher`].
356///
357/// Reached with [`LapinPublish::server_tx`].
358///
359/// Pairing fails when [`LapinBroker::auto_recover`](crate::LapinBroker::auto_recover) is enabled,
360/// so applications reject this unsupported combination at startup. Keep recovery disabled or use
361/// [`LapinPublish::confirms`] instead.
362///
363/// # Examples
364///
365/// ```
366/// use std::error::Error;
367///
368/// use ruststream_lapin::prelude::*;
369/// use serde::{Deserialize, Serialize};
370///
371/// #[derive(Deserialize)]
372/// struct Transfer {
373///     from: u64,
374///     to: u64,
375///     cents: i64,
376/// }
377///
378/// #[derive(Serialize, Outgoing)]
379/// #[outgoing(name = "ledger.entry")]
380/// struct Entry {
381///     account: u64,
382///     cents: i64,
383/// }
384///
385/// // Both legs of a transfer become visible together, or neither does.
386/// async fn post(
387///     ledger: &impl TransactionalPublisher,
388///     transfer: &Transfer,
389/// ) -> Result<(), Box<dyn Error + Send + Sync>> {
390///     let debit = Entry {
391///         account: transfer.from,
392///         cents: -transfer.cents,
393///     };
394///     let credit = Entry {
395///         account: transfer.to,
396///         cents: transfer.cents,
397///     };
398///     let mut scope = ledger.begin().await?;
399///     for entry in [debit, credit] {
400///         if let Err(err) = scope.message(&entry).publish().await {
401///             scope.abort().await?;
402///             return Err(err.into());
403///         }
404///     }
405///     scope.commit().await?;
406///     Ok(())
407/// }
408///
409/// #[subscriber("transfers")]
410/// async fn book(
411///     transfer: &Transfer,
412///     Out(ledger): Out<impl TransactionalPublisher>,
413/// ) -> HandlerOutcome {
414///     if post(ledger, transfer).await.is_err() {
415///         return HandlerOutcome::retry();
416///     }
417///     HandlerOutcome::ack()
418/// }
419///
420/// fn app() -> RustStream {
421///     let broker = LapinBroker::new("amqp://localhost:5672");
422///     let ledger = LapinPublish::default().exchange("ledger").server_tx();
423///     RustStream::new(AppInfo::new("ledger", "0.1.0")).with_broker(broker, |b| {
424///         b.include(book).out(DefaultSlot, ledger).build();
425///     })
426/// }
427/// ```
428#[derive(Debug, Clone, Default, PartialEq, Eq)]
429#[must_use]
430pub struct ServerTxPublish(PublishOptions);
431
432publish_policy_settings!(ServerTxPublish);
433
434impl PublishPolicy<ConnectedLapinBroker> for ServerTxPublish {
435    type Live = ServerTxPublisher;
436
437    fn pair(
438        self,
439        connected: &ConnectedLapinBroker,
440    ) -> impl Future<Output = Result<Self::Live, PairError>> {
441        ready(
442            check_server_tx(connected.auto_recover())
443                .map(|()| self.bind(connected))
444                .map_err(PairError::new),
445        )
446    }
447
448    publish_policy_bindings!();
449}
450
451impl LapinPublishPolicy for ServerTxPublish {
452    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
453        ServerTxPublisher::new(connected, self.0)
454    }
455}