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}