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}