use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use futures::{Stream, StreamExt};
use pulsar::consumer::{Consumer, DeadLetterPolicy};
use pulsar::proto::MessageIdData;
use pulsar::{Pulsar, SubType, TokioExecutor};
use ruststream::{AckError, BatchSubscriber, BufferedSubscriber, Seekable, Subscriber};
use tokio::sync::{mpsc, oneshot};
use tokio::time::sleep;
use tracing::warn;
use crate::broker::Core;
use crate::error::{PulsarError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::{LogSeeker, Queued};
use crate::message::{
DriverCmd, NackAfterCmd, PulsarMessage, PulsarPosition, SeekCmd, SettleKind, SettleSender,
send_settle,
};
use crate::pattern::TopicPattern;
use crate::subscription::{PulsarSubscription, SubscriptionType, Topics};
const CHANNEL_CAPACITY: usize = 16;
pub struct PulsarSubscriber {
topic: String,
inner: BufferedSubscriber<Deliveries>,
}
#[derive(Debug)]
enum Deliveries {
Consumer(ConsumerDeliveries),
#[cfg(feature = "testing")]
InProcess(Queued),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Deliveries>() == size_of::<ConsumerDeliveries>());
#[derive(Debug)]
struct ConsumerDeliveries {
rx: mpsc::Receiver<(u64, Result<PulsarMessage, PulsarError>)>,
cmd: SettleSender,
epoch: Arc<AtomicU64>,
}
impl ConsumerDeliveries {
fn poll_next(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Option<Result<PulsarMessage, PulsarError>>> {
loop {
match self.rx.poll_recv(cx) {
Poll::Ready(Some((epoch, item))) => {
if epoch == self.epoch.load(Ordering::Acquire) {
return Poll::Ready(Some(item));
}
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
impl std::fmt::Debug for PulsarSubscriber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PulsarSubscriber")
.field("topic", &self.topic)
.finish_non_exhaustive()
}
}
impl PulsarSubscriber {
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
pub(crate) async fn open(
client: &Pulsar<TokioExecutor>,
descriptor: PulsarSubscription,
core: &Arc<Core>,
) -> Result<Self, PulsarError> {
let display = descriptor.display_topic();
let batch_wait = descriptor.batch_wait;
let mut builder = client
.consumer()
.with_subscription(&descriptor.subscription)
.with_subscription_type(match descriptor.sub_type {
SubscriptionType::Exclusive => SubType::Exclusive,
SubscriptionType::Shared => SubType::Shared,
SubscriptionType::Failover => SubType::Failover,
SubscriptionType::KeyShared => SubType::KeyShared,
});
match &descriptor.topics {
Topics::List(topics) => {
builder = builder.with_topics(topics);
}
Topics::Pattern(pattern) => {
let pattern = TopicPattern::parse(pattern)?;
builder = builder
.with_lookup_namespace(pattern.lookup_namespace())
.with_topic_regex(pattern.regex().clone());
}
}
if let Some(dead_letter) = descriptor.dead_letter_policy()? {
builder = builder.with_dead_letter_policy(DeadLetterPolicy {
max_redeliver_count: usize::try_from(dead_letter.max_deliveries)
.unwrap_or(usize::MAX),
dead_letter_topic: dead_letter.topic,
});
}
if descriptor.ack_timeout.is_some() {
builder = builder.with_unacked_message_resend_delay(descriptor.ack_timeout);
}
let consumer: Consumer<Vec<u8>, TokioExecutor> =
builder.build().await.map_err(|e| PulsarError::Subscribe {
topic: display.clone(),
source: box_err(e),
})?;
let (out_tx, out_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (settle_tx, settle_rx) = mpsc::unbounded_channel();
let epoch = Arc::new(AtomicU64::new(0));
core.runtime.spawn(drive(
consumer,
client.clone(),
Arc::clone(core),
out_tx,
settle_tx.clone(),
settle_rx,
display.clone(),
Arc::clone(&epoch),
descriptor.ack_timeout,
));
Ok(Self::batching(
display,
Deliveries::Consumer(ConsumerDeliveries {
rx: out_rx,
cmd: settle_tx,
epoch,
}),
batch_wait,
))
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(topic: String, queued: Queued, batch_wait: Duration) -> Self {
Self::batching(topic, Deliveries::InProcess(queued), batch_wait)
}
fn batching(topic: String, wire: Deliveries, batch_wait: Duration) -> Self {
Self {
topic,
inner: BufferedSubscriber::new(wire).max_wait(batch_wait),
}
}
}
#[derive(Clone)]
pub struct PulsarSeeker {
inner: SeekerKind,
}
#[derive(Clone)]
enum SeekerKind {
Consumer(SettleSender),
#[cfg(feature = "testing")]
InProcess(LogSeeker),
}
impl std::fmt::Debug for PulsarSeeker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PulsarSeeker").finish_non_exhaustive()
}
}
impl PulsarSeeker {
pub(crate) fn new(cmd: SettleSender) -> Self {
Self {
inner: SeekerKind::Consumer(cmd),
}
}
#[cfg(feature = "testing")]
pub(crate) const fn in_process(seeker: LogSeeker) -> Self {
Self {
inner: SeekerKind::InProcess(seeker),
}
}
}
impl ruststream::Seeker for PulsarSeeker {
type Position = PulsarPosition;
type Error = PulsarError;
async fn seek(&self, to: PulsarPosition) -> Result<(), PulsarError> {
match &self.inner {
SeekerKind::Consumer(cmd) => {
let (done, wait) = oneshot::channel();
cmd.send(DriverCmd::Seek(SeekCmd { position: to, done }))
.map_err(|_| PulsarError::NotConnected)?;
wait.await.map_err(|_| PulsarError::NotConnected)?
}
#[cfg(feature = "testing")]
SeekerKind::InProcess(seeker) => seeker.seek(&to),
}
}
}
impl Seekable for Deliveries {
type Seeker = PulsarSeeker;
fn seeker(&self) -> PulsarSeeker {
match self {
Self::Consumer(consumer) => PulsarSeeker::new(consumer.cmd.clone()),
#[cfg(feature = "testing")]
Self::InProcess(queued) => queued.seeker(),
}
}
}
impl Subscriber for Deliveries {
type Message = PulsarMessage;
type Error = PulsarError;
fn stream(&mut self) -> impl Stream<Item = Result<PulsarMessage, PulsarError>> + Send + '_ {
futures::stream::poll_fn(move |cx| match self {
Self::Consumer(consumer) => consumer.poll_next(cx),
#[cfg(feature = "testing")]
Self::InProcess(queued) => queued.poll_next(cx),
})
}
}
impl Seekable for PulsarSubscriber {
type Seeker = PulsarSeeker;
fn seeker(&self) -> PulsarSeeker {
self.inner.seeker()
}
}
impl Subscriber for PulsarSubscriber {
type Message = PulsarMessage;
type Error = PulsarError;
fn stream(&mut self) -> impl Stream<Item = Result<PulsarMessage, PulsarError>> + Send + '_ {
self.inner.stream()
}
}
impl BatchSubscriber for PulsarSubscriber {
type Batch = Vec<PulsarMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, PulsarError>> + Send + '_ {
self.inner.batches(size)
}
}
#[allow(clippy::too_many_arguments)]
async fn drive(
mut consumer: Consumer<Vec<u8>, TokioExecutor>,
client: Pulsar<TokioExecutor>,
core: Arc<Core>,
out: mpsc::Sender<(u64, Result<PulsarMessage, PulsarError>)>,
settle_tx: SettleSender,
mut settle_rx: mpsc::UnboundedReceiver<DriverCmd>,
topic: String,
epoch: Arc<AtomicU64>,
ack_timeout: Option<Duration>,
) {
let mut pending: Option<(u64, PulsarMessage)> = None;
loop {
if let Some((stamp, msg)) = pending.take() {
tokio::select! {
biased;
cmd = settle_rx.recv() => {
match cmd {
Some(DriverCmd::Seek(seek)) => {
epoch.fetch_add(1, Ordering::Release);
apply_seek(&mut consumer, &client, &core, seek).await;
}
Some(cmd) => {
apply(&mut consumer, &client, &core, cmd).await;
pending = Some((stamp, msg));
}
None => pending = Some((stamp, msg)),
}
}
permit = out.reserve() => match permit {
Ok(permit) => permit.send((stamp, Ok(msg))),
Err(_) => break, },
}
} else {
let current = epoch.load(Ordering::Acquire);
tokio::select! {
biased;
cmd = settle_rx.recv() => {
match cmd {
Some(DriverCmd::Seek(seek)) => {
epoch.fetch_add(1, Ordering::Release);
apply_seek(&mut consumer, &client, &core, seek).await;
}
Some(cmd) => apply(&mut consumer, &client, &core, cmd).await,
None => {}
}
}
() = out.closed() => break, next = consumer.next() => match next {
Some(Ok(message)) => {
pending = Some((
current,
PulsarMessage::new(&message, settle_tx.clone(), ack_timeout),
));
}
Some(Err(err)) => {
if out
.send((
current,
Err(PulsarError::Receive {
topic: topic.clone(),
source: box_err(err),
}),
))
.await
.is_err()
{
break;
}
}
None => {
let _ = out
.send((
current,
Err(PulsarError::Receive {
topic: topic.clone(),
source: Box::from("the consumer stream ended"),
}),
))
.await;
break;
}
},
}
}
}
drop(settle_tx);
while let Some(cmd) = settle_rx.recv().await {
apply(&mut consumer, &client, &core, cmd).await;
}
if let Err(err) = Box::pin(consumer.close()).await {
tracing::debug!(topic = %topic, error = %err, "pulsar consumer close failed");
}
}
async fn apply(
consumer: &mut Consumer<Vec<u8>, TokioExecutor>,
client: &Pulsar<TokioExecutor>,
core: &Core,
cmd: DriverCmd,
) {
match cmd {
DriverCmd::NackAfter(cmd) => wait_then_nack(cmd),
DriverCmd::Settle(cmd) => {
let result = match cmd.kind {
SettleKind::Ack => consumer.ack_with_id(&cmd.topic, cmd.id).await,
SettleKind::Nack => consumer.nack_with_id(&cmd.topic, cmd.id).await,
};
let _ = cmd
.done
.send(result.map_err(|e| AckError::Broker(box_err(e))));
}
DriverCmd::Seek(seek) => apply_seek(consumer, client, core, seek).await,
}
}
fn wait_then_nack(
NackAfterCmd {
topic,
id,
delay,
back,
}: NackAfterCmd,
) {
tokio::spawn(async move {
sleep(delay).await;
let named = topic.clone();
if let Err(err) = send_settle(back, topic, id, SettleKind::Nack).await {
warn!(
target: "ruststream_pulsar::subscriber",
topic = %named,
delay = ?delay,
error = %err,
"a delayed retry could not be negatively acknowledged; the broker redelivers it \
once the consumer's ack timeout elapses",
);
}
});
}
const EARLIEST_MARK: u64 = u64::MAX;
const LATEST_MARK: u64 = i64::MAX.unsigned_abs();
fn end_of_log(mark: u64) -> MessageIdData {
MessageIdData {
ledger_id: mark,
entry_id: mark,
partition: Some(-1),
..MessageIdData::default()
}
}
async fn apply_seek(
consumer: &mut Consumer<Vec<u8>, TokioExecutor>,
client: &Pulsar<TokioExecutor>,
core: &Core,
SeekCmd { position, done }: SeekCmd,
) {
if let Err(closed) = core.ensure_open() {
let _ = done.send(Err(closed));
return;
}
let (message_id, timestamp) = match position {
PulsarPosition::Earliest => (Some(end_of_log(EARLIEST_MARK)), None),
PulsarPosition::Latest => (Some(end_of_log(LATEST_MARK)), None),
PulsarPosition::MessageId(id) => (Some(id), None),
PulsarPosition::Timestamp(millis) => (None, Some(millis)),
};
let topics = consumer.topics();
let result =
Box::pin(consumer.seek(Some(topics.clone()), message_id, timestamp, client.clone()))
.await
.map_err(|e| PulsarError::Receive {
topic: topics.join(","),
source: box_err(e),
});
let _ = done.send(result);
}
#[cfg(test)]
mod tests {
use ruststream::Seeker;
use super::*;
#[tokio::test]
async fn a_seek_after_the_driver_is_gone_reports_not_connected() {
let (cmd, driver) = mpsc::unbounded_channel();
drop(driver);
let refused = PulsarSeeker::new(cmd)
.seek(PulsarPosition::earliest())
.await
.expect_err("no driver is left to seek");
assert!(matches!(refused, PulsarError::NotConnected), "{refused:?}");
}
#[tokio::test]
async fn a_seek_the_driver_drops_unanswered_reports_not_connected() {
let (cmd, mut driver) = mpsc::unbounded_channel();
let seeker = PulsarSeeker::new(cmd);
let (refused, ()) = tokio::join!(seeker.seek(PulsarPosition::earliest()), async {
drop(driver.recv().await);
});
let refused = refused.expect_err("the driver went without answering");
assert!(matches!(refused, PulsarError::NotConnected), "{refused:?}");
}
}