Skip to main content

ruststream_pulsar/
subscriber.rs

1//! [`PulsarSubscriber`]: a stream of deliveries backed by a driver task.
2//!
3//! The client's acknowledgement API needs `&mut Consumer` while an ack token must be
4//! `Send + 'static`, so the crate owns a driver task per subscription: it polls the consumer
5//! stream, forwards deliveries into a bounded channel, and applies settlement commands shipped
6//! back from message handles.
7
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use futures::{Stream, StreamExt};
12
13use pulsar::consumer::{Consumer, DeadLetterPolicy};
14use pulsar::proto::MessageIdData;
15use pulsar::{SubType, TokioExecutor};
16use ruststream::{AckError, Subscriber};
17use tokio::sync::mpsc;
18
19use crate::broker::Core;
20use crate::error::{PulsarError, box_err};
21use crate::message::{DriverCmd, PulsarMessage, PulsarPosition, SeekCmd, SettleKind, SettleSender};
22use crate::subscription::{PulsarSubscription, SubscriptionType, Topics};
23
24/// How many undelivered messages may sit between the driver and the consumer. Real prefetch is
25/// the client's own flow control (`batch_size` permits); this only decouples the two loops.
26const CHANNEL_CAPACITY: usize = 16;
27
28/// A subscription to one or more Pulsar topics; yields [`PulsarMessage`]s.
29///
30/// Dropping the subscriber stops the driver task, which closes the client consumer.
31pub struct PulsarSubscriber {
32    topic: String,
33    rx: mpsc::Receiver<(u64, Result<PulsarMessage, PulsarError>)>,
34    cmd: SettleSender,
35    /// The delivery generation: a seek bumps it, and items queued under an older generation
36    /// are discarded on the way out - a reposition must not deliver stale buffered messages.
37    epoch: Arc<AtomicU64>,
38}
39
40impl std::fmt::Debug for PulsarSubscriber {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("PulsarSubscriber")
43            .field("topic", &self.topic)
44            .finish_non_exhaustive()
45    }
46}
47
48impl PulsarSubscriber {
49    /// The topic list or pattern this subscription consumes from.
50    #[must_use]
51    pub fn topic(&self) -> &str {
52        &self.topic
53    }
54
55    pub(crate) async fn open(
56        core: &Core,
57        descriptor: PulsarSubscription,
58    ) -> Result<Self, PulsarError> {
59        let display = descriptor.display_topic();
60        let mut builder = core
61            .client
62            .consumer()
63            .with_subscription(&descriptor.subscription)
64            .with_subscription_type(match descriptor.sub_type {
65                SubscriptionType::Exclusive => SubType::Exclusive,
66                SubscriptionType::Shared => SubType::Shared,
67                SubscriptionType::Failover => SubType::Failover,
68                SubscriptionType::KeyShared => SubType::KeyShared,
69            });
70        match &descriptor.topics {
71            Topics::List(topics) => {
72                builder = builder.with_topics(topics);
73            }
74            Topics::Pattern(pattern) => {
75                let regex = regex::Regex::new(pattern)
76                    .map_err(|e| PulsarError::Invalid(format!("invalid pattern: {e}")))?;
77                builder = builder.with_topic_regex(regex);
78            }
79        }
80        if let Some(dead_letter) = &descriptor.dead_letter {
81            builder = builder.with_dead_letter_policy(DeadLetterPolicy {
82                max_redeliver_count: dead_letter.max_deliveries,
83                dead_letter_topic: dead_letter.topic.clone(),
84            });
85        }
86        if descriptor.ack_timeout.is_some() {
87            builder = builder.with_unacked_message_resend_delay(descriptor.ack_timeout);
88        }
89
90        let consumer: Consumer<Vec<u8>, TokioExecutor> =
91            builder.build().await.map_err(|e| PulsarError::Subscribe {
92                topic: display.clone(),
93                source: box_err(e),
94            })?;
95
96        let (out_tx, out_rx) = mpsc::channel(CHANNEL_CAPACITY);
97        let (settle_tx, settle_rx) = mpsc::unbounded_channel();
98        let epoch = Arc::new(AtomicU64::new(0));
99        tokio::spawn(drive(
100            consumer,
101            core.client.clone(),
102            out_tx,
103            settle_tx.clone(),
104            settle_rx,
105            display.clone(),
106            Arc::clone(&epoch),
107        ));
108
109        Ok(Self {
110            topic: display,
111            rx: out_rx,
112            cmd: settle_tx,
113            epoch,
114        })
115    }
116}
117
118/// Repositions a [`PulsarSubscriber`] while its stream runs; minted by
119/// [`Seekable::seeker`](ruststream::Seekable::seeker).
120///
121/// A seek covers every topic (and partition) of the subscription's consumer, and the broker
122/// redelivers from the new position; per-message acknowledgement state needs no reset.
123#[derive(Clone)]
124pub struct PulsarSeeker {
125    cmd: SettleSender,
126}
127
128impl std::fmt::Debug for PulsarSeeker {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("PulsarSeeker").finish_non_exhaustive()
131    }
132}
133
134impl ruststream::Seeker for PulsarSeeker {
135    type Position = PulsarPosition;
136    type Error = PulsarError;
137
138    async fn seek(&self, to: PulsarPosition) -> Result<(), PulsarError> {
139        let (done, wait) = tokio::sync::oneshot::channel();
140        self.cmd
141            .send(DriverCmd::Seek(SeekCmd { position: to, done }))
142            .map_err(|_| PulsarError::Receive {
143                topic: String::new(),
144                source: Box::from("the subscription's driver task has shut down"),
145            })?;
146        wait.await.map_err(|_| PulsarError::Receive {
147            topic: String::new(),
148            source: Box::from("the subscription's driver task has shut down"),
149        })?
150    }
151}
152
153impl ruststream::Seekable for PulsarSubscriber {
154    type Seeker = PulsarSeeker;
155
156    fn seeker(&self) -> PulsarSeeker {
157        PulsarSeeker {
158            cmd: self.cmd.clone(),
159        }
160    }
161}
162
163impl Subscriber for PulsarSubscriber {
164    type Message = PulsarMessage;
165    type Error = PulsarError;
166
167    fn stream(&mut self) -> impl Stream<Item = Result<PulsarMessage, PulsarError>> + Send + '_ {
168        // Poll the channel in place rather than wrapping it in an owning stream, so `stream`
169        // can be called again after the returned stream is dropped (the runtime and the
170        // conformance helpers re-enter it per call). Items queued under an older generation
171        // (before a seek) are discarded here.
172        futures::stream::poll_fn(move |cx| {
173            loop {
174                match self.rx.poll_recv(cx) {
175                    std::task::Poll::Ready(Some((epoch, item))) => {
176                        if epoch == self.epoch.load(Ordering::Acquire) {
177                            return std::task::Poll::Ready(Some(item));
178                        }
179                    }
180                    std::task::Poll::Ready(None) => return std::task::Poll::Ready(None),
181                    std::task::Poll::Pending => return std::task::Poll::Pending,
182                }
183            }
184        })
185    }
186}
187
188async fn drive(
189    mut consumer: Consumer<Vec<u8>, TokioExecutor>,
190    client: pulsar::Pulsar<TokioExecutor>,
191    out: mpsc::Sender<(u64, Result<PulsarMessage, PulsarError>)>,
192    settle_tx: SettleSender,
193    mut settle_rx: mpsc::UnboundedReceiver<DriverCmd>,
194    topic: String,
195    epoch: Arc<AtomicU64>,
196) {
197    // Deliveries carry the generation captured when they were pulled off the consumer:
198    // stamping at send time would let a seek's bump - which lands before the seek command is
199    // processed - leak onto a delivery positioned before the seek.
200    let mut pending: Option<(u64, PulsarMessage)> = None;
201    loop {
202        if let Some((stamp, msg)) = pending.take() {
203            // A delivery is waiting for channel capacity; keep settling while it waits so an
204            // unpolled stream can never wedge in-flight acks.
205            tokio::select! {
206                biased;
207                cmd = settle_rx.recv() => {
208                    match cmd {
209                        Some(DriverCmd::Seek(seek)) => {
210                            // The reposition drops the delivery waiting for capacity too.
211                            epoch.fetch_add(1, Ordering::Release);
212                            apply_seek(&mut consumer, &client, seek).await;
213                        }
214                        Some(cmd) => {
215                            apply(&mut consumer, &client, cmd).await;
216                            pending = Some((stamp, msg));
217                        }
218                        None => pending = Some((stamp, msg)),
219                    }
220                }
221                permit = out.reserve() => match permit {
222                    Ok(permit) => permit.send((stamp, Ok(msg))),
223                    Err(_) => break, // subscriber dropped
224                },
225            }
226        } else {
227            let current = epoch.load(Ordering::Acquire);
228            tokio::select! {
229                biased;
230                cmd = settle_rx.recv() => {
231                    match cmd {
232                        Some(DriverCmd::Seek(seek)) => {
233                            epoch.fetch_add(1, Ordering::Release);
234                            apply_seek(&mut consumer, &client, seek).await;
235                        }
236                        Some(cmd) => apply(&mut consumer, &client, cmd).await,
237                        None => {}
238                    }
239                }
240                () = out.closed() => break, // subscriber dropped
241                next = consumer.next() => match next {
242                    Some(Ok(message)) => {
243                        pending = Some((current, PulsarMessage::new(&message, settle_tx.clone())));
244                    }
245                    Some(Err(err)) => {
246                        // Single-topic consumers surface transient errors here while the
247                        // client reconnects underneath; forward and keep going.
248                        if out
249                            .send((
250                                current,
251                                Err(PulsarError::Receive {
252                                    topic: topic.clone(),
253                                    source: box_err(err),
254                                }),
255                            ))
256                            .await
257                            .is_err()
258                        {
259                            break;
260                        }
261                    }
262                    None => {
263                        // The engine gave up (retries exhausted): the stream is dead for good.
264                        let _ = out
265                            .send((
266                                current,
267                                Err(PulsarError::Receive {
268                                    topic: topic.clone(),
269                                    source: Box::from("the consumer stream ended"),
270                                }),
271                            ))
272                            .await;
273                        break;
274                    }
275                },
276            }
277        }
278    }
279
280    // Outstanding message handles may still settle; serve them until every clone of the
281    // settle sender is gone.
282    drop(settle_tx);
283    while let Some(cmd) = settle_rx.recv().await {
284        apply(&mut consumer, &client, cmd).await;
285    }
286    if let Err(err) = Box::pin(consumer.close()).await {
287        tracing::debug!(topic = %topic, error = %err, "pulsar consumer close failed");
288    }
289}
290
291async fn apply(
292    consumer: &mut Consumer<Vec<u8>, TokioExecutor>,
293    client: &pulsar::Pulsar<TokioExecutor>,
294    cmd: DriverCmd,
295) {
296    match cmd {
297        DriverCmd::Settle(cmd) => {
298            let result = match cmd.kind {
299                SettleKind::Ack => consumer.ack_with_id(&cmd.topic, cmd.id).await,
300                SettleKind::Nack => consumer.nack_with_id(&cmd.topic, cmd.id).await,
301            };
302            let _ = cmd
303                .done
304                .send(result.map_err(|e| AckError::Broker(box_err(e))));
305        }
306        DriverCmd::Seek(seek) => apply_seek(consumer, client, seek).await,
307    }
308}
309
310/// The ids the protocol reserves for the two ends of a log, spelled `-1` for the beginning and
311/// `i64::MAX` for the tip (the Java and Go clients agree on both). `ledger_id` and `entry_id`
312/// are unsigned on the wire, so the beginning travels as the all-ones pattern.
313const EARLIEST_MARK: u64 = u64::MAX;
314const LATEST_MARK: u64 = i64::MAX.unsigned_abs();
315
316fn end_of_log(mark: u64) -> MessageIdData {
317    MessageIdData {
318        ledger_id: mark,
319        entry_id: mark,
320        // The sentinel addresses the topic as a whole, which is what -1 means here.
321        partition: Some(-1),
322        ..MessageIdData::default()
323    }
324}
325
326async fn apply_seek(
327    consumer: &mut Consumer<Vec<u8>, TokioExecutor>,
328    client: &pulsar::Pulsar<TokioExecutor>,
329    SeekCmd { position, done }: SeekCmd,
330) {
331    let (message_id, timestamp) = match position {
332        PulsarPosition::Earliest => (Some(end_of_log(EARLIEST_MARK)), None),
333        PulsarPosition::Latest => (Some(end_of_log(LATEST_MARK)), None),
334        PulsarPosition::MessageId(id) => (Some(id), None),
335        PulsarPosition::Timestamp(millis) => (None, Some(millis)),
336    };
337    // A multi-topic consumer (a topic list or a pattern) seeks per topic and rejects an
338    // unnamed set, so the subscription's own topics are always spelled out; the single-topic
339    // consumer ignores the list.
340    let topics = consumer.topics();
341    let result =
342        Box::pin(consumer.seek(Some(topics.clone()), message_id, timestamp, client.clone()))
343            .await
344            .map_err(|e| PulsarError::Receive {
345                topic: topics.join(","),
346                source: box_err(e),
347            });
348    let _ = done.send(result);
349}