ruststream_lapin/subscriber.rs
1//! The subscriber: a stream of AMQP deliveries from one queue consumer, batched on the client for
2//! the handlers that take a batch.
3
4use std::fmt;
5use std::num::NonZeroUsize;
6use std::sync::Arc;
7use std::time::Duration;
8
9use futures::future::Either;
10use futures::stream::unfold;
11use futures::{Stream, StreamExt};
12use lapin::{Channel, Consumer};
13use ruststream::{BatchSubscriber, BufferedSubscriber, Subscriber};
14
15use crate::broker::AmqpConnection;
16use crate::delay::DelayContext;
17use crate::error::AmqpError;
18#[cfg(feature = "testing")]
19use crate::in_process::BusDeliveries;
20use crate::message::LapinMessage;
21use crate::recovery::RecoveringConsumer;
22
23/// A consumer on one queue, yielding [`LapinMessage`] deliveries.
24///
25/// Created by subscribing a [`RabbitQueue`](crate::RabbitQueue) descriptor (or a bare queue name)
26/// through [`LapinBroker`](crate::LapinBroker). The subscriber owns a dedicated channel;
27/// dropping it closes that channel and the broker redelivers whatever was unacknowledged.
28///
29/// Back-pressure: the broker stops pushing once
30/// [`prefetch`](crate::LapinBroker::prefetch) unacknowledged deliveries are in flight, so
31/// consuming slower slows the producer side down instead of buffering without bound.
32///
33/// It is a [`BatchSubscriber`] as well as a [`Subscriber`]: AMQP has no wire batch, so a batch
34/// handler's size is honoured by collecting the deliveries here (see the
35/// [impl](#impl-BatchSubscriber-for-LapinSubscriber)).
36pub struct LapinSubscriber {
37 deliveries: BufferedSubscriber<Deliveries>,
38 queue: String,
39}
40
41impl LapinSubscriber {
42 pub(crate) fn new(
43 connection: Arc<AmqpConnection>,
44 channel: Channel,
45 consumer: Consumer,
46 queue: String,
47 batch_wait: Duration,
48 delay: Option<DelayContext>,
49 ) -> Self {
50 // Keep the usual consumer path free of recovery checks unless the service opts in.
51 let consumer = if connection.auto_recover() {
52 Either::Right(RecoveringConsumer::new(
53 Arc::clone(&connection),
54 channel.clone(),
55 consumer,
56 ))
57 } else {
58 Either::Left(consumer)
59 };
60 let deliveries = Deliveries::Amqp(AmqpDeliveries {
61 connection,
62 _channel: channel,
63 consumer,
64 delay,
65 #[cfg(feature = "testing")]
66 queue: queue.clone(),
67 });
68 Self {
69 deliveries: BufferedSubscriber::new(deliveries).max_wait(batch_wait),
70 queue,
71 }
72 }
73
74 /// A subscriber on the in-process transport, batching on the client with the descriptor's
75 /// own deadline as the live subscriber does.
76 #[cfg(feature = "testing")]
77 pub(crate) fn in_process(
78 deliveries: BusDeliveries,
79 queue: String,
80 batch_wait: Duration,
81 ) -> Self {
82 Self {
83 deliveries: BufferedSubscriber::new(Deliveries::InProcess(deliveries))
84 .max_wait(batch_wait),
85 queue,
86 }
87 }
88
89 /// The queue this subscriber consumes from.
90 #[must_use]
91 pub fn queue(&self) -> &str {
92 &self.queue
93 }
94}
95
96impl fmt::Debug for LapinSubscriber {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.debug_struct("LapinSubscriber")
99 .field("queue", &self.queue)
100 .finish_non_exhaustive()
101 }
102}
103
104impl Subscriber for LapinSubscriber {
105 type Message = LapinMessage;
106 type Error = AmqpError;
107
108 /// Streams deliveries as they arrive; the stream ends when the consumer is cancelled or the
109 /// connection fails permanently. Recoverable disconnects pause delivery until Lapin resumes.
110 ///
111 /// # Cancel safety
112 ///
113 /// Polling is cancel safe (no delivery is lost by dropping the stream between polls), and
114 /// the stream can be re-created by calling `stream` again: deliveries buffer in the
115 /// consumer, not in the returned stream.
116 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
117 self.deliveries.stream()
118 }
119}
120
121/// Batches are assembled on the client: AMQP delivers one `basic.deliver` at a time, so there is
122/// no wire batch to ask the broker for, and a batch closes on the registration's size or on the
123/// descriptor's [`batch_wait`](crate::RabbitQueue::batch_wait), whichever comes first.
124///
125/// A batch can only hold what the broker has already pushed, so a subscription whose
126/// [`prefetch`](crate::RabbitQueue::prefetch) window is narrower than the registration's batch size
127/// yields batches capped by the window rather than by the size.
128impl BatchSubscriber for LapinSubscriber {
129 type Batch = Vec<LapinMessage>;
130
131 /// # Cancel safety
132 ///
133 /// As cancel safe as [`stream`](Subscriber::stream) between polls; dropping the returned
134 /// stream abandons the batch being assembled, and the broker redelivers those deliveries when
135 /// the channel closes.
136 fn batches(
137 &mut self,
138 size: NonZeroUsize,
139 ) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
140 self.deliveries.batches(size)
141 }
142}
143
144/// One delivery at a time, from whichever transport the broker was connected to: everything above
145/// it batches on the client.
146///
147/// Without the `testing` feature the wire consumer is the only variant, so the type is the
148/// consumer and the `match` in [`stream`](Subscriber::stream) resolves at compile time.
149// The live consumer is the larger variant on purpose: it is the one a production build has, and
150// boxing it would put an allocation on the service's own path to shrink a test build.
151#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
152enum Deliveries {
153 Amqp(AmqpDeliveries),
154 #[cfg(feature = "testing")]
155 InProcess(BusDeliveries),
156}
157
158#[cfg(not(feature = "testing"))]
159const _: () = assert!(
160 size_of::<Deliveries>() == size_of::<AmqpDeliveries>(),
161 "production deliveries must have no in-process enum overhead"
162);
163
164impl Subscriber for Deliveries {
165 type Message = LapinMessage;
166 type Error = AmqpError;
167
168 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
169 match self {
170 #[cfg(not(feature = "testing"))]
171 Self::Amqp(consumer) => consumer.stream(),
172 #[cfg(feature = "testing")]
173 Self::Amqp(consumer) => Either::Left(consumer.stream()),
174 #[cfg(feature = "testing")]
175 Self::InProcess(deliveries) => Either::Right(deliveries.stream()),
176 }
177 }
178}
179
180/// The wire consumer, one `basic.deliver` at a time.
181struct AmqpDeliveries {
182 // Threaded into every delivery, so a settlement after shutdown is refused instead of waiting
183 // on an answer lapin never gives.
184 connection: Arc<AmqpConnection>,
185 // Keep the subscription's channel alive with either kind of consumer.
186 _channel: Channel,
187 consumer: Either<Consumer, RecoveringConsumer>,
188 // Present only when the descriptor opted into a native delay queue; threaded into every
189 // delivery so `nack_after` can re-publish to the waiting queue.
190 delay: Option<DelayContext>,
191 // The queue a delivery came off, which the test harness reads to learn where a server-picked
192 // route went.
193 #[cfg(feature = "testing")]
194 queue: String,
195}
196
197impl AmqpDeliveries {
198 fn stream(&mut self) -> impl Stream<Item = Result<LapinMessage, AmqpError>> + Send + '_ {
199 let delay = self.delay.clone();
200 #[cfg(feature = "testing")]
201 let queue = self.queue.as_str();
202 let connection = &self.connection;
203 unfold(
204 (&mut self.consumer, delay),
205 async move |(consumer, delay)| {
206 let item = match consumer {
207 Either::Left(consumer) => consumer
208 .next()
209 .await
210 .map(|item| item.map_err(AmqpError::consume)),
211 Either::Right(consumer) => consumer.next().await,
212 }?;
213 let mapped = match item {
214 Ok(delivery) => Ok({
215 #[cfg(feature = "testing")]
216 connection.routes().delivered(
217 delivery.exchange.as_str(),
218 delivery.routing_key.as_str(),
219 queue,
220 );
221 LapinMessage::from_delivery(
222 delivery,
223 Some(Arc::clone(connection)),
224 delay.clone(),
225 )
226 }),
227 Err(err) => Err(err),
228 };
229 Some((mapped, (consumer, delay)))
230 },
231 )
232 }
233}