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::{
16    AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource, nonzero,
17};
18
19#[cfg(feature = "asyncapi")]
20use crate::bindings;
21
22use crate::broker::ConnectedAmqpBroker;
23use crate::error::AmqpError;
24use crate::subscriber::AmqpSubscriber;
25
26/// Default protocol-level credit (prefetch) granted to a subscription.
27pub const DEFAULT_CREDIT: NonZeroU32 = nonzero!(256);
28
29/// Default deadline closing a partial batch on a batch subscription.
30pub const DEFAULT_BATCH_WAIT: Duration = Duration::from_millis(10);
31
32/// Delivery guarantee of a subscription.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum Settle {
35    /// Deliveries are settled by the handler: `ack` accepts, `nack` releases or rejects. The
36    /// default.
37    #[default]
38    AtLeastOnce,
39    /// Deliveries are settled on receipt; `ack`/`nack` report
40    /// [`AckError::Unsupported`](ruststream::AckError::Unsupported).
41    AtMostOnce,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45enum Kind {
46    Queue,
47    Topic,
48    Raw,
49}
50
51/// How the subscriptions on one address share its traffic: the terminus capability seen from the
52/// consuming end.
53///
54/// This is the difference between a work queue and a broadcast, so the in-process transport
55/// reproduces it instead of handing every message to everyone and hoping the deployment agrees.
56#[cfg(feature = "testing")]
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum Routing {
59    /// Competing consumers: each message goes to exactly one of the subscriptions.
60    Anycast,
61    /// Fan-out: every subscription gets its own copy.
62    Multicast,
63}
64
65/// A subscription descriptor for an `AMQP` 1.0 address.
66///
67/// Implements [`SubscriptionSource`], so it can sit inline in the `#[subscriber(..)]` decorator:
68///
69/// ```
70/// # mod demo {
71/// use ruststream_amqp::prelude::*;
72/// use serde::Deserialize;
73///
74/// #[derive(Deserialize)]
75/// struct Order {
76///     id: u64,
77/// }
78///
79/// #[subscriber(AmqpAddress::queue("orders").credit(nonzero!(64)))]
80/// async fn fulfil(order: &Order) -> HandlerOutcome {
81///     println!("fulfilling order {}", order.id);
82///     HandlerOutcome::ack()
83/// }
84/// #
85/// # #[ruststream::app]
86/// # fn app() -> impl App {
87/// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
88/// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
89/// #             b.include(fulfil);
90/// #         })
91/// # }
92/// # }
93/// # fn main() {}
94/// ```
95#[derive(Debug, Clone, PartialEq, Eq)]
96#[must_use]
97pub struct AmqpAddress {
98    address: String,
99    kind: Kind,
100    credit: NonZeroU32,
101    settle: Settle,
102    batch_wait: Duration,
103}
104
105impl AmqpAddress {
106    fn of(address: String, kind: Kind) -> Self {
107        Self {
108            address,
109            kind,
110            credit: DEFAULT_CREDIT,
111            settle: Settle::default(),
112            batch_wait: DEFAULT_BATCH_WAIT,
113        }
114    }
115
116    /// An anycast address: competing consumers, each message delivered to one of them.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// # mod demo {
122    /// use ruststream_amqp::prelude::*;
123    /// use serde::Deserialize;
124    ///
125    /// #[derive(Deserialize)]
126    /// struct Order {
127    ///     id: u64,
128    /// }
129    ///
130    /// // Every replica of the service competes for "orders": each order is handled once.
131    /// #[subscriber(AmqpAddress::queue("orders"))]
132    /// async fn fulfil(order: &Order) -> HandlerOutcome {
133    ///     println!("fulfilling order {}", order.id);
134    ///     HandlerOutcome::ack()
135    /// }
136    /// #
137    /// # #[ruststream::app]
138    /// # fn app() -> impl App {
139    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
140    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
141    /// #             b.include(fulfil);
142    /// #         })
143    /// # }
144    /// # }
145    /// # fn main() {}
146    /// ```
147    pub fn queue(name: impl Into<String>) -> Self {
148        Self::of(name.into(), Kind::Queue)
149    }
150
151    /// A multicast address: fan-out, each message delivered to every subscriber.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// # mod demo {
157    /// use ruststream_amqp::prelude::*;
158    /// use serde::Deserialize;
159    ///
160    /// #[derive(Deserialize)]
161    /// struct Event {
162    ///     kind: String,
163    /// }
164    ///
165    /// // Every service subscribed to "events" gets its own copy of each event.
166    /// #[subscriber(AmqpAddress::topic("events"))]
167    /// async fn audit(event: &Event) -> HandlerOutcome {
168    ///     println!("audit: {}", event.kind);
169    ///     HandlerOutcome::ack()
170    /// }
171    /// #
172    /// # #[ruststream::app]
173    /// # fn app() -> impl App {
174    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
175    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
176    /// #             b.include(audit);
177    /// #         })
178    /// # }
179    /// # }
180    /// # fn main() {}
181    /// ```
182    pub fn topic(name: impl Into<String>) -> Self {
183        Self::of(name.into(), Kind::Topic)
184    }
185
186    /// A verbatim address, for deployments with their own addressing convention
187    /// (`"/queues/orders"` on `RabbitMQ` 4.x, a fully qualified queue on Artemis).
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// # mod demo {
193    /// use ruststream_amqp::prelude::*;
194    /// use serde::Deserialize;
195    ///
196    /// #[derive(Deserialize)]
197    /// struct Order {
198    ///     id: u64,
199    /// }
200    ///
201    /// // RabbitMQ 4.x addresses a queue by its v2 path; the string reaches the broker as written.
202    /// #[subscriber(AmqpAddress::raw("/queues/orders"))]
203    /// async fn fulfil(order: &Order) -> HandlerOutcome {
204    ///     println!("fulfilling order {}", order.id);
205    ///     HandlerOutcome::ack()
206    /// }
207    /// #
208    /// # #[ruststream::app]
209    /// # fn app() -> impl App {
210    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
211    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
212    /// #             b.include(fulfil);
213    /// #         })
214    /// # }
215    /// # }
216    /// # fn main() {}
217    /// ```
218    pub fn raw(address: impl Into<String>) -> Self {
219        Self::of(address.into(), Kind::Raw)
220    }
221
222    /// Sets the protocol-level credit (prefetch): how many unsettled deliveries the broker may
223    /// have in flight to this subscription. Defaults to [`DEFAULT_CREDIT`].
224    ///
225    /// The count is a [`NonZeroU32`] because a subscription granted no credit receives nothing:
226    /// zero is not a quieter setting but a stalled subscription, so it is unrepresentable here.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// # mod demo {
232    /// use ruststream_amqp::prelude::*;
233    /// use serde::Deserialize;
234    ///
235    /// #[derive(Deserialize)]
236    /// struct Order {
237    ///     id: u64,
238    /// }
239    ///
240    /// // At most 64 unsettled orders in flight to this service at once.
241    /// #[subscriber(AmqpAddress::queue("orders").credit(nonzero!(64)))]
242    /// async fn fulfil(order: &Order) -> HandlerOutcome {
243    ///     println!("fulfilling order {}", order.id);
244    ///     HandlerOutcome::ack()
245    /// }
246    /// #
247    /// # #[ruststream::app]
248    /// # fn app() -> impl App {
249    /// #     RustStream::new(AppInfo::new("orders", "0.1.0"))
250    /// #         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
251    /// #             b.include(fulfil);
252    /// #         })
253    /// # }
254    /// # }
255    /// # fn main() {}
256    /// ```
257    ///
258    /// A zero is rejected while the service is compiled, not when the subscription opens:
259    ///
260    /// ```compile_fail
261    /// use ruststream::nonzero;
262    /// use ruststream_amqp::AmqpAddress;
263    ///
264    /// let source = AmqpAddress::queue("orders").credit(nonzero!(0));
265    /// # let _ = source;
266    /// ```
267    pub fn credit(mut self, credit: NonZeroU32) -> Self {
268        self.credit = credit;
269        self
270    }
271
272    /// Sets the delivery guarantee. Defaults to [`Settle::AtLeastOnce`].
273    pub fn settle(mut self, settle: Settle) -> Self {
274        self.settle = settle;
275        self
276    }
277
278    /// Caps how long a partial batch waits for more deliveries on a batch subscription, counted
279    /// from its first one. Defaults to [`DEFAULT_BATCH_WAIT`].
280    ///
281    /// `AMQP` 1.0 has no batch pull, so the batches are assembled on the client and this deadline
282    /// is what trades latency for fuller batches under a trickle of traffic. It has no effect on a
283    /// single-message subscription, where every delivery goes out as it arrives.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// # mod demo {
289    /// use std::time::Duration;
290    ///
291    /// use ruststream_amqp::prelude::*;
292    /// use serde::Deserialize;
293    ///
294    /// #[derive(Deserialize)]
295    /// struct Order {
296    ///     id: u64,
297    /// }
298    ///
299    /// // Up to 32 orders per call, or whatever arrived within 50 ms of the first one.
300    /// #[subscriber(AmqpAddress::queue("orders").batch_wait(Duration::from_millis(50)))]
301    /// async fn fulfil(orders: &[Order]) -> HandlerOutcome {
302    ///     let ids: Vec<u64> = orders.iter().map(|order| order.id).collect();
303    ///     println!("fulfilling orders {ids:?}");
304    ///     HandlerOutcome::ack()
305    /// }
306    ///
307    /// #[ruststream::app]
308    /// fn app() -> impl App {
309    ///     RustStream::new(AppInfo::new("orders", "0.1.0"))
310    ///         .with_broker(AmqpBroker::new("amqp://localhost:5672"), |b| {
311    ///             b.include(fulfil.batch(nonzero!(32)));
312    ///         })
313    /// }
314    /// # }
315    /// # fn main() {}
316    /// ```
317    pub fn batch_wait(mut self, batch_wait: Duration) -> Self {
318        self.batch_wait = batch_wait;
319        self
320    }
321
322    /// The address string sent to the broker.
323    #[must_use]
324    pub fn address(&self) -> &str {
325        &self.address
326    }
327
328    pub(crate) fn credit_value(&self) -> u32 {
329        self.credit.get()
330    }
331
332    pub(crate) fn settle_value(&self) -> Settle {
333        self.settle
334    }
335
336    pub(crate) fn batch_wait_value(&self) -> Duration {
337        self.batch_wait
338    }
339
340    /// Where a publisher on this broker reaches this subscription again.
341    ///
342    /// One `AMQP` 1.0 node serves both roles: a receiver attaches its source to the address, a
343    /// sender its target. The address is therefore the answer for all three kinds, which is what
344    /// lets the framework's deferred `retry_after` fallback work on this broker without native
345    /// delayed redelivery. On a `queue` terminus the copy competes for consumers like any other
346    /// message, and on a `topic` terminus every subscriber sees it: that is the terminus the
347    /// declaration asked for, not a property of the retry.
348    fn redelivery_target(&self) -> RedeliveryAddress {
349        RedeliveryAddress::new(self.address.clone())
350    }
351
352    /// How this descriptor's terminus shares the address's traffic, for the in-process transport.
353    ///
354    /// `queue` competes, `topic` fans out. A `raw` address declares no capability, so on a server
355    /// the peer's own configuration decides; in process there is no configuration to consult, and
356    /// the transport delivers each message once rather than inventing a fan-out the deployment may
357    /// not have. A service that wants the broadcast asserted says `topic`, which is also what the
358    /// products needing the capability have to be told.
359    #[cfg(feature = "testing")]
360    pub(crate) fn routing(&self) -> Routing {
361        match self.kind {
362            Kind::Topic => Routing::Multicast,
363            Kind::Queue | Kind::Raw => Routing::Anycast,
364        }
365    }
366
367    /// The terminus capability this descriptor advertises, when one applies.
368    pub(crate) fn capability(&self) -> Option<&'static str> {
369        match self.kind {
370            Kind::Queue => Some("queue"),
371            Kind::Topic => Some("topic"),
372            Kind::Raw => None,
373        }
374    }
375
376    /// What this subscription tells a reader of the generated document: the node it reads, the
377    /// terminus it asked for, and the link it reads over.
378    ///
379    /// The answer does not depend on the transport, so a document built from a test says what the
380    /// deployment's does.
381    #[cfg(feature = "asyncapi")]
382    fn describe_channel(&self) -> Bindings {
383        bindings::channel(
384            &self.address,
385            self.capability(),
386            self.credit.get(),
387            self.settle,
388        )
389    }
390
391    /// Rejects descriptors that cannot form a subscription, before any I/O.
392    ///
393    /// Only the address is checked here: the credit is a [`NonZeroU32`], so an unusable one
394    /// cannot reach this point.
395    pub(crate) fn validate(&self) -> Result<(), AmqpError> {
396        if self.address.is_empty() {
397            return Err(AmqpError::InvalidAddress(
398                "address must be non-empty".into(),
399            ));
400        }
401        Ok(())
402    }
403}
404
405impl SubscriptionSource<ConnectedAmqpBroker> for AmqpAddress {
406    type Subscriber = AmqpSubscriber;
407
408    /// `AMQP` 1.0 has no delayed redelivery and no wire-level way to declare a node with a
409    /// delivery limit and a dead-letter address, so a spent delivery is this process's to move:
410    /// the runtime publishes the copies and this descriptor says where they land.
411    type Copies = AddressedCopies;
412
413    fn name(&self) -> &str {
414        self.address()
415    }
416
417    async fn subscribe(self, connected: &ConnectedAmqpBroker) -> Result<AmqpSubscriber, AmqpError> {
418        connected.subscribe_address(self).await
419    }
420
421    #[cfg(feature = "asyncapi")]
422    fn channel_bindings(&self) -> Bindings {
423        self.describe_channel()
424    }
425}
426
427impl RedeliveryAddressed<ConnectedAmqpBroker> for AmqpAddress {
428    // The answer is the descriptor's own address, so nothing is asked of the connection and the
429    // call needs no state machine.
430    fn redelivery_address(
431        &self,
432        _connected: &ConnectedAmqpBroker,
433    ) -> impl Future<Output = Result<RedeliveryAddress, AmqpError>> + Send {
434        ready(Ok(self.redelivery_target()))
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn empty_address_is_rejected_before_io() {
444        assert!(matches!(
445            AmqpAddress::queue("").validate(),
446            Err(AmqpError::InvalidAddress(_))
447        ));
448    }
449
450    /// `credit(0)` does not compile: `nonzero!(0)` fails const evaluation, and a runtime zero
451    /// cannot be built either. What is left to pin is that the default is the one that applies.
452    #[test]
453    fn the_credit_defaults_and_takes_an_override() {
454        assert_eq!(AmqpAddress::queue("orders").credit_value(), 256);
455        assert_eq!(
456            AmqpAddress::queue("orders")
457                .credit(nonzero!(64))
458                .credit_value(),
459            64
460        );
461    }
462
463    #[test]
464    fn the_batch_deadline_defaults_and_takes_an_override() {
465        assert_eq!(
466            AmqpAddress::queue("q").batch_wait_value(),
467            DEFAULT_BATCH_WAIT
468        );
469        assert_eq!(
470            AmqpAddress::queue("q")
471                .batch_wait(Duration::from_millis(50))
472                .batch_wait_value(),
473            Duration::from_millis(50)
474        );
475    }
476
477    #[test]
478    fn constructors_pick_the_matching_capability() {
479        assert_eq!(AmqpAddress::queue("q").capability(), Some("queue"));
480        assert_eq!(AmqpAddress::topic("t").capability(), Some("topic"));
481        assert_eq!(AmqpAddress::raw("/queues/q").capability(), None);
482    }
483}