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