Skip to main content

ruststream_amqp/
address.rs

1//! [`AmqpAddress`]: the subscription descriptor.
2//!
3//! `AMQP` 1.0 standardises the wire but not the meaning of an address, so addressing stays
4//! explicit: `queue` for anycast (competing consumers), `topic` for multicast (fan-out), and
5//! `raw` for deployments with their own convention. The queue/topic constructors advertise the
6//! matching terminus capability (`"queue"` / `"topic"`), which is how `ActiveMQ` Artemis and other
7//! products disambiguate; `raw` sends the address verbatim with no capability.
8
9use std::future::{Future, ready};
10use std::num::NonZeroU32;
11use std::time::Duration;
12
13#[cfg(feature = "asyncapi")]
14use ruststream::asyncapi::Bindings;
15use ruststream::runtime::IntoSource;
16use ruststream::{
17    AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource, nonzero,
18};
19
20#[cfg(feature = "asyncapi")]
21use crate::bindings;
22
23use crate::broker::ConnectedAmqpBroker;
24use crate::error::AmqpError;
25use crate::subscriber::AmqpSubscriber;
26
27/// Default link credit granted to a subscription: the window of deliveries the broker may send
28/// ahead of the handler's settlements.
29pub const DEFAULT_CREDIT: NonZeroU32 = nonzero!(256);
30
31/// Default deadline closing a partial batch on a batch subscription.
32pub const DEFAULT_BATCH_WAIT: Duration = Duration::from_millis(10);
33
34/// Delivery guarantee of a subscription, set with [`AmqpAddress::settle`].
35///
36/// Under [`AtLeastOnce`](Self::AtLeastOnce) the handler's outcome is the delivery's disposition,
37/// and the runtime caps the attempts: `AMQP` 1.0 declares no node with a delivery limit and a
38/// dead-letter address, so `max_attempts` and `dead_letter` read the same here as on every broker.
39///
40/// # Examples
41///
42/// Five deliveries per order, then the `orders.dead` address. An order that is not ready yet
43/// comes back in half a minute, through a retry publisher that stamps each copy:
44///
45/// ```
46/// # mod demo {
47/// use std::time::Duration;
48///
49/// use ruststream::runtime::{Outgoing, PublishContext};
50/// use ruststream_amqp::prelude::*;
51/// use serde::Deserialize;
52///
53/// #[derive(Deserialize)]
54/// struct Order {
55///     id: u64,
56/// }
57///
58/// #[subscriber(AmqpAddress::queue("orders").settle(Settle::AtLeastOnce))]
59/// async fn fulfil(order: &Order) -> HandlerOutcome {
60///     if order.id == 0 {
61///         // Not ready yet: ask for the delivery again in half a minute.
62///         return HandlerOutcome::retry_after(Duration::from_secs(30));
63///     }
64///     tracing::info!(order.id, "fulfilled");
65///     HandlerOutcome::ack()
66/// }
67///
68/// /// Stamps a deferred copy with the address it came from, so a redelivery is recognisable.
69/// struct Retried;
70///
71/// impl<C, Options> PublishTransform<ForReply<C>, Options> for Retried {
72///     type Destination = Reads;
73///
74///     fn apply(
75///         &self,
76///         out: &mut Outgoing<'_>,
77///         _options: &mut Option<Options>,
78///         cx: &PublishContext<'_, C>,
79///     ) {
80///         out.headers_mut()
81///             .insert("x-retried-from", cx.name().to_owned());
82///     }
83/// }
84///
85/// #[ruststream::app]
86/// fn app() -> impl App {
87///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
88///         AmqpBroker::new("amqp://artemis:artemis@localhost:5672"),
89///         |b| {
90///             b.include(fulfil)
91///                 .max_attempts(nonzero!(5u32))
92///                 .dead_letter("orders.dead")
93///                 .out_retry(Publish)
94///                 .transform(Retried);
95///         },
96///     )
97/// }
98/// # }
99/// # fn main() {}
100/// ```
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
102pub enum Settle {
103    /// Deliveries are settled by the handler. The default.
104    ///
105    /// `ack` sends `accepted`. A retry, `nack(true)`, sends `modified` with `delivery-failed`,
106    /// never `released`, so the broker counts the attempt in `delivery-count`. A drop,
107    /// `nack(false)`, sends `rejected`, and the broker's dead-letter policy decides what follows.
108    #[default]
109    AtLeastOnce,
110    /// Deliveries are settled on receipt, so a message is gone whether the handler finished or
111    /// not; `ack` and `nack` report
112    /// [`AckError::Unsupported`](ruststream::AckError::Unsupported).
113    AtMostOnce,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117enum Kind {
118    Queue,
119    Topic,
120    Raw,
121}
122
123/// How the subscriptions on one address share its traffic: the terminus capability seen from the
124/// consuming end.
125///
126/// This is the difference between a work queue and a broadcast, so the in-process transport
127/// reproduces it instead of handing every message to everyone and hoping the deployment agrees.
128#[cfg(feature = "testing")]
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub(crate) enum Routing {
131    /// Competing consumers: each message goes to exactly one of the subscriptions.
132    Anycast,
133    /// Fan-out: every subscription gets its own copy.
134    Multicast,
135}
136
137/// A subscription descriptor for an `AMQP` 1.0 address.
138///
139/// Implements [`SubscriptionSource`], so it can sit inline in the `#[subscriber(..)]` decorator:
140///
141/// ```
142/// # mod demo {
143/// use ruststream_amqp::prelude::*;
144/// use serde::Deserialize;
145///
146/// #[derive(Deserialize)]
147/// struct Order {
148///     id: u64,
149/// }
150///
151/// #[subscriber(AmqpAddress::queue("orders").credit(nonzero!(64)))]
152/// async fn fulfil(order: &Order) -> HandlerOutcome {
153///     tracing::info!(order.id, "fulfilling");
154///     HandlerOutcome::ack()
155/// }
156/// #
157/// # #[ruststream::app]
158/// # fn app() -> impl App {
159/// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
160/// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
161/// #             b.include(fulfil);
162/// #         })
163/// # }
164/// # }
165/// # fn main() {}
166/// ```
167#[derive(Debug, Clone, PartialEq, Eq)]
168#[must_use]
169pub struct AmqpAddress {
170    address: String,
171    kind: Kind,
172    credit: NonZeroU32,
173    settle: Settle,
174    batch_wait: Duration,
175}
176
177impl AmqpAddress {
178    fn of(address: String, kind: Kind) -> Self {
179        Self {
180            address,
181            kind,
182            credit: DEFAULT_CREDIT,
183            settle: Settle::default(),
184            batch_wait: DEFAULT_BATCH_WAIT,
185        }
186    }
187
188    /// An anycast address: competing consumers, each message delivered to one of them.
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// # mod demo {
194    /// use ruststream_amqp::prelude::*;
195    /// use serde::Deserialize;
196    ///
197    /// #[derive(Deserialize)]
198    /// struct Order {
199    ///     id: u64,
200    /// }
201    ///
202    /// // Every replica of the service competes for "orders": each order is handled once.
203    /// #[subscriber(AmqpAddress::queue("orders"))]
204    /// async fn fulfil(order: &Order) -> HandlerOutcome {
205    ///     tracing::info!(order.id, "fulfilling");
206    ///     HandlerOutcome::ack()
207    /// }
208    /// #
209    /// # #[ruststream::app]
210    /// # fn app() -> impl App {
211    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
212    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
213    /// #             b.include(fulfil);
214    /// #         })
215    /// # }
216    /// # }
217    /// # fn main() {}
218    /// ```
219    pub fn queue(name: impl Into<String>) -> Self {
220        Self::of(name.into(), Kind::Queue)
221    }
222
223    /// A multicast address: fan-out, each message delivered to every subscriber.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// # mod demo {
229    /// use ruststream_amqp::prelude::*;
230    /// use serde::Deserialize;
231    ///
232    /// #[derive(Deserialize)]
233    /// struct Event {
234    ///     kind: String,
235    /// }
236    ///
237    /// // Every service subscribed to "events" gets its own copy of each event.
238    /// #[subscriber(AmqpAddress::topic("events"))]
239    /// async fn audit(event: &Event) -> HandlerOutcome {
240    ///     tracing::info!(kind = %event.kind, "audited");
241    ///     HandlerOutcome::ack()
242    /// }
243    /// #
244    /// # #[ruststream::app]
245    /// # fn app() -> impl App {
246    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
247    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
248    /// #             b.include(audit);
249    /// #         })
250    /// # }
251    /// # }
252    /// # fn main() {}
253    /// ```
254    pub fn topic(name: impl Into<String>) -> Self {
255        Self::of(name.into(), Kind::Topic)
256    }
257
258    /// A verbatim address, for deployments with their own addressing convention
259    /// (`"/queues/orders"` on `RabbitMQ` 4.x, a fully qualified queue on Artemis).
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// # mod demo {
265    /// use ruststream_amqp::prelude::*;
266    /// use serde::Deserialize;
267    ///
268    /// #[derive(Deserialize)]
269    /// struct Order {
270    ///     id: u64,
271    /// }
272    ///
273    /// // RabbitMQ 4.x addresses a queue by its v2 path; the string reaches the broker as written.
274    /// #[subscriber(AmqpAddress::raw("/queues/orders"))]
275    /// async fn fulfil(order: &Order) -> HandlerOutcome {
276    ///     tracing::info!(order.id, "fulfilling");
277    ///     HandlerOutcome::ack()
278    /// }
279    /// #
280    /// # #[ruststream::app]
281    /// # fn app() -> impl App {
282    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
283    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
284    /// #             b.include(fulfil);
285    /// #         })
286    /// # }
287    /// # }
288    /// # fn main() {}
289    /// ```
290    pub fn raw(address: impl Into<String>) -> Self {
291        Self::of(address.into(), Kind::Raw)
292    }
293
294    /// Sets the link credit: the window of deliveries the broker may send ahead of the handler's
295    /// settlements. Defaults to [`DEFAULT_CREDIT`].
296    ///
297    /// The subscription grants the full credit when it attaches, and grants it again each time
298    /// half of it has been settled. The broker then never runs more than `credit` deliveries
299    /// ahead of the last grant, but the deliveries held unsettled can outnumber the credit: with a
300    /// credit of 4, the second settlement brings four more while two are still open.
301    ///
302    /// The count is a [`NonZeroU32`] because a subscription granted no credit receives nothing:
303    /// zero is not a quieter setting but a stalled subscription, so it is unrepresentable here.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// # mod demo {
309    /// use ruststream_amqp::prelude::*;
310    /// use serde::Deserialize;
311    ///
312    /// #[derive(Deserialize)]
313    /// struct Order {
314    ///     id: u64,
315    /// }
316    ///
317    /// // The broker sends up to 64 orders ahead of the handler's settlements.
318    /// #[subscriber(AmqpAddress::queue("orders").credit(nonzero!(64)))]
319    /// async fn fulfil(order: &Order) -> HandlerOutcome {
320    ///     tracing::info!(order.id, "fulfilling");
321    ///     HandlerOutcome::ack()
322    /// }
323    /// #
324    /// # #[ruststream::app]
325    /// # fn app() -> impl App {
326    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
327    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
328    /// #             b.include(fulfil);
329    /// #         })
330    /// # }
331    /// # }
332    /// # fn main() {}
333    /// ```
334    ///
335    /// A zero is rejected while the service is compiled, not when the subscription opens:
336    ///
337    /// ```compile_fail
338    /// use ruststream::nonzero;
339    /// use ruststream_amqp::AmqpAddress;
340    ///
341    /// let source = AmqpAddress::queue("orders").credit(nonzero!(0));
342    /// # let _ = source;
343    /// ```
344    pub fn credit(mut self, credit: NonZeroU32) -> Self {
345        self.credit = credit;
346        self
347    }
348
349    /// Sets the delivery guarantee. Defaults to [`Settle::AtLeastOnce`].
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// # mod demo {
355    /// use ruststream_amqp::prelude::*;
356    /// use serde::Deserialize;
357    ///
358    /// #[derive(Deserialize)]
359    /// struct Event {
360    ///     kind: String,
361    /// }
362    ///
363    /// // Settled on receipt: the audit may miss an event when the process stops, and the broker
364    /// // never waits for it.
365    /// #[subscriber(AmqpAddress::topic("events").settle(Settle::AtMostOnce))]
366    /// async fn audit(events: &[Event]) -> HandlerOutcome {
367    ///     for event in events {
368    ///         tracing::info!(kind = %event.kind, "audited");
369    ///     }
370    ///     HandlerOutcome::ack()
371    /// }
372    ///
373    /// #[ruststream::app]
374    /// fn app() -> impl App {
375    ///     RustStream::new(AppInfo::new("audit", "0.1.0"))
376    ///         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
377    ///             b.include(audit.batch(nonzero!(32)));
378    ///         })
379    /// }
380    /// # }
381    /// # fn main() {}
382    /// ```
383    pub fn settle(mut self, settle: Settle) -> Self {
384        self.settle = settle;
385        self
386    }
387
388    /// Caps how long a partial batch waits for more deliveries on a batch subscription, counted
389    /// from its first one. Defaults to [`DEFAULT_BATCH_WAIT`].
390    ///
391    /// `AMQP` 1.0 has no batch pull, so the batches are assembled on the client and this deadline
392    /// is what trades latency for fuller batches under a trickle of traffic. It has no effect on a
393    /// single-message subscription, where every delivery goes out as it arrives.
394    ///
395    /// # Examples
396    ///
397    /// ```
398    /// # mod demo {
399    /// use std::time::Duration;
400    ///
401    /// use ruststream_amqp::prelude::*;
402    /// use serde::Deserialize;
403    ///
404    /// #[derive(Deserialize)]
405    /// struct Order {
406    ///     id: u64,
407    /// }
408    ///
409    /// // Up to 32 orders per call, or whatever arrived within 50 ms of the first one.
410    /// #[subscriber(AmqpAddress::queue("orders").batch_wait(Duration::from_millis(50)))]
411    /// async fn fulfil(orders: &[Order]) -> HandlerOutcome {
412    ///     tracing::info!(count = orders.len(), "fulfilling");
413    ///     HandlerOutcome::ack()
414    /// }
415    ///
416    /// #[ruststream::app]
417    /// fn app() -> impl App {
418    ///     RustStream::new(AppInfo::new("orders", "0.1.0"))
419    ///         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
420    ///             b.include(fulfil.batch(nonzero!(32)));
421    ///         })
422    /// }
423    /// # }
424    /// # fn main() {}
425    /// ```
426    pub fn batch_wait(mut self, batch_wait: Duration) -> Self {
427        self.batch_wait = batch_wait;
428        self
429    }
430
431    /// The address string sent to the broker.
432    #[must_use]
433    pub fn address(&self) -> &str {
434        &self.address
435    }
436
437    pub(crate) fn credit_value(&self) -> u32 {
438        self.credit.get()
439    }
440
441    pub(crate) fn settle_value(&self) -> Settle {
442        self.settle
443    }
444
445    pub(crate) fn batch_wait_value(&self) -> Duration {
446        self.batch_wait
447    }
448
449    /// Where a publisher on this broker reaches this subscription again.
450    ///
451    /// One `AMQP` 1.0 node serves both roles: a receiver attaches its source to the address, a
452    /// sender its target. The address is therefore the answer for all three kinds, which is what
453    /// lets the framework's deferred `retry_after` fallback work on this broker without native
454    /// delayed redelivery. On a `queue` terminus the copy competes for consumers like any other
455    /// message, and on a `topic` terminus every subscriber sees it: that is the terminus the
456    /// declaration asked for, not a property of the retry.
457    fn redelivery_target(&self) -> RedeliveryAddress {
458        RedeliveryAddress::new(self.address.clone())
459    }
460
461    /// How this descriptor's terminus shares the address's traffic, for the in-process transport.
462    ///
463    /// `queue` competes, `topic` fans out. A `raw` address declares no capability, so on a server
464    /// the peer's own configuration decides; in process there is no configuration to consult, and
465    /// the transport delivers each message once rather than inventing a fan-out the deployment may
466    /// not have. A service that wants the broadcast asserted says `topic`, which is also what the
467    /// products needing the capability have to be told.
468    #[cfg(feature = "testing")]
469    pub(crate) fn routing(&self) -> Routing {
470        match self.kind {
471            Kind::Topic => Routing::Multicast,
472            Kind::Queue | Kind::Raw => Routing::Anycast,
473        }
474    }
475
476    /// The terminus capability this descriptor advertises, when one applies.
477    pub(crate) fn capability(&self) -> Option<&'static str> {
478        match self.kind {
479            Kind::Queue => Some("queue"),
480            Kind::Topic => Some("topic"),
481            Kind::Raw => None,
482        }
483    }
484
485    /// What this subscription tells a reader of the generated document: the node it reads, the
486    /// terminus it asked for, and the link it reads over.
487    ///
488    /// The answer does not depend on the transport, so a document built from a test says what the
489    /// deployment's does.
490    #[cfg(feature = "asyncapi")]
491    fn describe_channel(&self) -> Bindings {
492        bindings::channel(
493            &self.address,
494            self.capability(),
495            self.credit.get(),
496            self.settle,
497        )
498    }
499
500    /// Rejects descriptors that cannot form a subscription, before any I/O.
501    ///
502    /// Only the address is checked here: the credit is a [`NonZeroU32`], so an unusable one
503    /// cannot reach this point.
504    pub(crate) fn validate(&self) -> Result<(), AmqpError> {
505        if self.address.is_empty() {
506            return Err(AmqpError::InvalidAddress(
507                "address must be non-empty".into(),
508            ));
509        }
510        Ok(())
511    }
512}
513
514/// The descriptor mounts on the manual path the way it does on the attribute: the
515/// `subscriber(..)` constructor takes it as its source, unchanged.
516///
517/// # Examples
518///
519/// ```
520/// # mod demo {
521/// use std::future::{Future, ready};
522///
523/// use ruststream_amqp::prelude::*;
524/// # use ruststream::schemars::JsonSchema;
525/// use serde::Deserialize;
526///
527/// # // The doctest links the framework with its `asyncapi` feature on, where a manual
528/// # // registration owes the generated document the message's schema.
529/// # #[derive(JsonSchema)]
530/// # #[schemars(crate = "ruststream::schemars")]
531/// #[derive(Deserialize)]
532/// struct Order {
533///     id: u64,
534/// }
535///
536/// struct TakeOrder;
537///
538/// impl Handle<Order> for TakeOrder {
539///     fn handle(
540///         &self,
541///         order: &Order,
542///         _outs: &(),
543///         _ctx: &mut Context<'_>,
544///     ) -> impl Future<Output = Result<(), HandlerOutcome>> {
545///         tracing::info!(order.id, "order taken");
546///         ready(Ok(()))
547///     }
548/// }
549///
550/// fn app() -> RustStream {
551///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
552///         AmqpBroker::new("amqp://localhost:5672"),
553///         |b| {
554///             b.include(subscriber(AmqpAddress::queue("orders"), TakeOrder).build());
555///         },
556///     )
557/// }
558/// # }
559/// # fn main() {}
560/// ```
561impl IntoSource for AmqpAddress {
562    type Source = Self;
563
564    fn into_source(self) -> Self {
565        self
566    }
567}
568
569impl SubscriptionSource<ConnectedAmqpBroker> for AmqpAddress {
570    type Subscriber = AmqpSubscriber;
571
572    /// `AMQP` 1.0 has no delayed redelivery and no wire-level way to declare a node with a
573    /// delivery limit and a dead-letter address, so a spent delivery is this process's to move:
574    /// the runtime publishes the copies and this descriptor says where they land.
575    type Copies = AddressedCopies;
576
577    fn name(&self) -> &str {
578        self.address()
579    }
580
581    async fn subscribe(self, connected: &ConnectedAmqpBroker) -> Result<AmqpSubscriber, AmqpError> {
582        connected.subscribe_address(self).await
583    }
584
585    #[cfg(feature = "asyncapi")]
586    fn channel_bindings(&self) -> Bindings {
587        self.describe_channel()
588    }
589}
590
591impl RedeliveryAddressed<ConnectedAmqpBroker> for AmqpAddress {
592    // The answer is the descriptor's own address, so nothing is asked of the connection and the
593    // call needs no state machine.
594    fn redelivery_address(
595        &self,
596        _connected: &ConnectedAmqpBroker,
597    ) -> impl Future<Output = Result<RedeliveryAddress, AmqpError>> + Send {
598        ready(Ok(self.redelivery_target()))
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    #[test]
607    fn empty_address_is_rejected_before_io() {
608        assert!(matches!(
609            AmqpAddress::queue("").validate(),
610            Err(AmqpError::InvalidAddress(_))
611        ));
612    }
613
614    /// `credit(0)` does not compile: `nonzero!(0)` fails const evaluation, and a runtime zero
615    /// cannot be built either. What is left to pin is that the default is the one that applies.
616    #[test]
617    fn the_credit_defaults_and_takes_an_override() {
618        assert_eq!(AmqpAddress::queue("orders").credit_value(), 256);
619        assert_eq!(
620            AmqpAddress::queue("orders")
621                .credit(nonzero!(64))
622                .credit_value(),
623            64
624        );
625    }
626
627    #[test]
628    fn the_batch_deadline_defaults_and_takes_an_override() {
629        assert_eq!(
630            AmqpAddress::queue("q").batch_wait_value(),
631            DEFAULT_BATCH_WAIT
632        );
633        assert_eq!(
634            AmqpAddress::queue("q")
635                .batch_wait(Duration::from_millis(50))
636                .batch_wait_value(),
637            Duration::from_millis(50)
638        );
639    }
640
641    #[test]
642    fn constructors_pick_the_matching_capability() {
643        assert_eq!(AmqpAddress::queue("q").capability(), Some("queue"));
644        assert_eq!(AmqpAddress::topic("t").capability(), Some("topic"));
645        assert_eq!(AmqpAddress::raw("/queues/q").capability(), None);
646    }
647}