Skip to main content

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