Skip to main content

ruststream_lapin/queue/
classic.rs

1//! The classic queue descriptor.
2
3use std::future::{Future, ready};
4use std::num::NonZeroU16;
5use std::time::Duration;
6
7use lapin::types::{AMQPValue, FieldTable};
8#[cfg(feature = "asyncapi")]
9use ruststream::asyncapi::Bindings;
10use ruststream::runtime::IntoSource;
11use ruststream::{AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource};
12
13use super::spec::{
14    Binding, DEAD_LETTER_EXCHANGE, DEAD_LETTER_ROUTING_KEY, QueueKind, QueueSpec, queue_settings,
15};
16use crate::broker::ConnectedLapinBroker;
17use crate::delay::Delay;
18use crate::error::AmqpError;
19use crate::exchange::RabbitExchange;
20use crate::subscriber::LapinSubscriber;
21
22/// Describes one classic-queue subscription: the queue, its expected settings, and its bindings.
23///
24/// The classic queue is the single-node implementation, and it counts nothing: a delivery says
25/// only that it has been seen before, never how often. So a registration's `max_attempts(n)` is
26/// the runtime's to apply, counted with the framework's own header on the copies it publishes,
27/// and `out_retry(policy)` names the publisher those copies leave through. A queue that should
28/// end a poison loop on the server instead is a [`RabbitQuorumQueue`](crate::RabbitQuorumQueue).
29///
30/// Descriptors describe the EXPECTED topology for routing; by default nothing is created on the
31/// broker (managing infrastructure is the user's job). Opt in to declaration per broker with
32/// [`declare_topology(true)`](crate::LapinBroker::declare_topology).
33///
34/// # Examples
35///
36/// ```
37/// use ruststream_lapin::prelude::*;
38/// use serde::Deserialize;
39///
40/// #[derive(Deserialize)]
41/// struct OrderPlaced {
42///     id: u64,
43/// }
44///
45/// #[subscriber(RabbitQueue::new("orders")
46///     .bind(RabbitExchange::topic("events"), "order.*")
47///     .dead_letter_exchange("dead-letters"))]
48/// async fn on_order(event: &OrderPlaced) -> HandlerOutcome {
49///     if event.id == 0 {
50///         // Rejected without requeue: the queue's dead-letter exchange takes it.
51///         return HandlerOutcome::drop();
52///     }
53///     HandlerOutcome::ack()
54/// }
55///
56/// fn app() -> RustStream {
57///     let broker = LapinBroker::new("amqp://localhost:5672").declare_topology(true);
58///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
59///         b.include(on_order);
60///     })
61/// }
62/// ```
63///
64/// A slice parameter asks for a batch, and the mount site says how big it may get:
65///
66/// ```
67/// use std::time::Duration;
68///
69/// use ruststream_lapin::prelude::*;
70/// use serde::Deserialize;
71///
72/// #[derive(Deserialize)]
73/// struct Settlement {
74///     id: u64,
75/// }
76///
77/// #[subscriber(RabbitQueue::new("settlements")
78///     .prefetch(nonzero!(64))
79///     .batch_wait(Duration::from_millis(200)))]
80/// async fn on_settlements(settlements: &[Settlement]) -> HandlerOutcome {
81///     tracing::info!(count = settlements.len(), "settling");
82///     HandlerOutcome::ack()
83/// }
84///
85/// fn app() -> RustStream {
86///     let broker = LapinBroker::new("amqp://localhost:5672").declare_topology(true);
87///     RustStream::new(AppInfo::new("settlements", "0.1.0")).with_broker(broker, |b| {
88///         b.include(on_settlements.batch(nonzero!(32)));
89///     })
90/// }
91/// ```
92///
93/// The runtime caps the retries of a classic queue and publishes each copy through the
94/// `out_retry` policy, here the confirms one:
95///
96/// ```
97/// use ruststream_lapin::prelude::*;
98/// use serde::Deserialize;
99///
100/// #[derive(Deserialize)]
101/// struct Refund {
102///     id: u64,
103/// }
104///
105/// #[subscriber(RabbitQueue::new("refunds"))]
106/// async fn on_refund(refund: &Refund) -> HandlerOutcome {
107///     tracing::warn!(refund.id, "the provider is down, trying again");
108///     HandlerOutcome::retry()
109/// }
110///
111/// fn app() -> RustStream {
112///     let broker = LapinBroker::new("amqp://localhost:5672").declare_topology(true);
113///     RustStream::new(AppInfo::new("refunds", "0.1.0")).with_broker(broker, |b| {
114///         b.include(on_refund)
115///             .max_attempts(nonzero!(5u32))
116///             .dead_letter("refunds.dead")
117///             .out_retry(TransactionalPublish::default());
118///     })
119/// }
120/// ```
121#[derive(Debug, Clone, PartialEq)]
122pub struct RabbitQueue {
123    spec: QueueSpec,
124}
125
126impl RabbitQueue {
127    /// Describes the classic queue `name` with the defaults: durable, shared, not auto-deleted.
128    #[must_use]
129    pub fn new(name: impl Into<String>) -> Self {
130        Self {
131            spec: QueueSpec::new(QueueKind::Classic, name.into()),
132        }
133    }
134
135    /// Whether the queue survives a broker restart. Defaults to `true`.
136    ///
137    /// `RabbitMQ` 4 denies transient non-exclusive queues, so a queue declared `durable(false)`
138    /// has to be [`exclusive`](Self::exclusive) too.
139    #[must_use]
140    pub fn durable(mut self, durable: bool) -> Self {
141        self.spec.durable = durable;
142        self
143    }
144
145    /// Whether the queue is exclusive to this connection. Defaults to `false`.
146    #[must_use]
147    pub fn exclusive(mut self, exclusive: bool) -> Self {
148        self.spec.exclusive = exclusive;
149        self
150    }
151
152    /// Whether the queue is deleted when its last consumer disconnects. Defaults to `false`.
153    #[must_use]
154    pub fn auto_delete(mut self, auto_delete: bool) -> Self {
155        self.spec.auto_delete = auto_delete;
156        self
157    }
158
159    pub(crate) const fn spec(&self) -> &QueueSpec {
160        &self.spec
161    }
162}
163
164queue_settings!(RabbitQueue);
165
166/// The descriptor is its own source, so the manual path's `subscriber(source, body)` takes a
167/// `RabbitQueue` where the attribute path writes it in the decorator.
168impl IntoSource for RabbitQueue {
169    type Source = Self;
170
171    fn into_source(self) -> Self {
172        self
173    }
174}
175
176impl SubscriptionSource<ConnectedLapinBroker> for RabbitQueue {
177    type Subscriber = LapinSubscriber;
178
179    /// The service publishes the copies of a retry, and the queue name is where they go: on the
180    /// default exchange a routing key addresses the queue that carries it.
181    ///
182    /// A classic queue keeps no counter and has no delivery limit, so there is nothing on the
183    /// server to end a retry loop; the runtime counts and the mount site names the publisher the
184    /// copies leave through with `out_retry(policy)`.
185    type Copies = AddressedCopies;
186
187    fn name(&self) -> &str {
188        &self.spec.name
189    }
190
191    async fn subscribe(
192        self,
193        connected: &ConnectedLapinBroker,
194    ) -> Result<Self::Subscriber, AmqpError> {
195        connected.subscribe(self).await
196    }
197
198    /// The queue this subscription consumes, as the `amqp` binding names its settings.
199    #[cfg(feature = "asyncapi")]
200    fn channel_bindings(&self) -> Bindings {
201        crate::bindings::queue_channel(&self.spec)
202    }
203
204    /// The consumer's own half: this crate acknowledges by hand on every subscription.
205    #[cfg(feature = "asyncapi")]
206    fn operation_bindings(&self) -> Bindings {
207        crate::bindings::consumer_operation()
208    }
209}
210
211/// The queue name: on the default exchange a routing key addresses the queue that carries it, so
212/// that is where the runtime's retry copy reaches this subscription again.
213///
214/// The answer holds for a retry publisher on the default exchange, which is what
215/// [`LapinPublish`](crate::LapinPublish) is unless configured otherwise. A publisher aimed at a
216/// topic or direct exchange reaches the queue only through a binding under this name, so bind it
217/// there or leave the retry publisher on the default exchange.
218impl RedeliveryAddressed<ConnectedLapinBroker> for RabbitQueue {
219    fn redelivery_address(
220        &self,
221        _connected: &ConnectedLapinBroker,
222    ) -> impl Future<Output = Result<RedeliveryAddress, AmqpError>> + Send {
223        // The name is on the descriptor, so nothing has to be asked of the broker.
224        ready(Ok(RedeliveryAddress::new(self.spec.name.clone())))
225    }
226}