use std::fmt;
use std::sync::Arc;
use bytes::Bytes;
use futures::Stream;
use futures::future::FutureExt as _;
use rdkafka::Message as _;
use rdkafka::consumer::StreamConsumer;
use rdkafka::error::RDKafkaErrorCode;
#[cfg(feature = "schema-registry")]
use ruststream::IncomingMessage;
use ruststream::{BatchSubscriber, Subscriber};
use tracing::{debug, warn};
use crate::convert;
use crate::eos::EOS_SOURCE_HEADER;
use crate::error::KafkaError;
use crate::message::{KafkaMessage, PARTITION_KEY_HEADER, Settlement};
use crate::retry::RetryContext;
use crate::topic::{Commit, LaneKey};
use crate::tracker::{CommitTracker, TrackingContext};
fn is_transient(err: &rdkafka::error::KafkaError) -> bool {
err.rdkafka_error_code() == Some(RDKafkaErrorCode::UnknownTopicOrPartition)
}
pub struct KafkaSubscriber {
consumer: Arc<StreamConsumer<TrackingContext>>,
topic: String,
commit: Commit,
tracker: Arc<CommitTracker>,
lane_key: LaneKey,
retry: Option<Arc<RetryContext>>,
#[cfg(feature = "schema-registry")]
schema_registry: Option<crate::schema_registry::SchemaRegistry>,
in_transient_episode: bool,
}
impl KafkaSubscriber {
pub(crate) fn new(
consumer: Arc<StreamConsumer<TrackingContext>>,
topic: String,
commit: Commit,
tracker: Arc<CommitTracker>,
lane_key: LaneKey,
retry: Option<Arc<RetryContext>>,
) -> Self {
Self {
consumer,
topic,
commit,
tracker,
lane_key,
retry,
#[cfg(feature = "schema-registry")]
schema_registry: None,
in_transient_episode: false,
}
}
#[cfg(feature = "schema-registry")]
pub(crate) fn with_schema_registry(
mut self,
registry: Option<crate::schema_registry::SchemaRegistry>,
) -> Self {
self.schema_registry = registry;
self
}
fn note_transient(&mut self, err: &rdkafka::error::KafkaError) {
if self.in_transient_episode {
debug!(
target: "ruststream_rdkafka",
topic = %self.topic,
error = %err,
"transient consume error (repeat)",
);
} else {
self.in_transient_episode = true;
warn!(
target: "ruststream_rdkafka",
topic = %self.topic,
error = %err,
"transient consume error; librdkafka keeps retrying",
);
}
}
fn note_recovered(&mut self) {
if self.in_transient_episode {
self.in_transient_episode = false;
debug!(
target: "ruststream_rdkafka",
topic = %self.topic,
"recovered from transient consume errors",
);
}
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
#[cfg(feature = "schema-registry")]
async fn transcode(&self, item: &mut KafkaMessage) {
if let Some(registry) = &self.schema_registry
&& let Some(json) = registry
.incoming_to_json(IncomingMessage::payload(item))
.await
{
item.replace_payload(Bytes::from(json));
}
}
fn map_delivery(&self, delivery: &rdkafka::message::BorrowedMessage<'_>) -> KafkaMessage {
let mut headers = convert::headers_from_message(delivery);
if matches!(self.commit, Commit::Transactional(_)) {
headers.insert(
EOS_SOURCE_HEADER,
crate::eos::encode_source(
delivery.topic(),
delivery.partition(),
delivery.offset(),
),
);
}
let payload = delivery
.payload()
.map_or_else(Bytes::new, Bytes::copy_from_slice);
let settlement = match &self.commit {
Commit::Auto => Settlement::Advisory,
Commit::Tracked => {
self.tracker
.delivered(delivery.topic(), delivery.partition(), delivery.offset());
Settlement::Tracked {
consumer: Arc::clone(&self.consumer),
tracker: Arc::clone(&self.tracker),
}
}
Commit::Transactional(_) => {
self.tracker
.delivered(delivery.topic(), delivery.partition(), delivery.offset());
Settlement::Transactional {
tracker: Arc::clone(&self.tracker),
}
}
};
let lane = match self.lane_key {
LaneKey::RecordKey => headers
.get(PARTITION_KEY_HEADER)
.map(Bytes::copy_from_slice),
LaneKey::Partition => Some(Bytes::from(delivery.partition().to_string())),
};
KafkaMessage::new(
payload,
headers,
delivery.topic().to_owned(),
delivery.partition(),
delivery.offset(),
delivery.timestamp().to_millis(),
settlement,
lane,
self.retry.clone(),
)
}
}
impl fmt::Debug for KafkaSubscriber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KafkaSubscriber")
.field("topic", &self.topic)
.field("commit", &self.commit)
.finish_non_exhaustive()
}
}
impl Subscriber for KafkaSubscriber {
type Message = KafkaMessage;
type Error = KafkaError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
futures::stream::unfold(self, |sub| async move {
loop {
match sub.consumer.recv().await {
Ok(delivery) => {
#[allow(unused_mut)] let mut item = sub.map_delivery(&delivery);
drop(delivery);
#[cfg(feature = "schema-registry")]
sub.transcode(&mut item).await;
sub.note_recovered();
return Some((Ok(item), sub));
}
Err(err) if is_transient(&err) => sub.note_transient(&err),
Err(err) => return Some((Err(KafkaError::consume(err)), sub)),
}
}
})
}
}
impl BatchSubscriber for KafkaSubscriber {
type Batch = Vec<KafkaMessage>;
fn batches(
&mut self,
) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
futures::stream::unfold(self, |sub| async move {
let first = loop {
match sub.consumer.recv().await {
Ok(delivery) => break sub.map_delivery(&delivery),
Err(err) if is_transient(&err) => sub.note_transient(&err),
Err(err) => return Some((Err(KafkaError::consume(err)), sub)),
}
};
sub.note_recovered();
let mut batch = vec![first];
while let Some(result) = sub.consumer.recv().now_or_never() {
match result {
Ok(delivery) => {
let item = sub.map_delivery(&delivery);
batch.push(item);
}
Err(err) if is_transient(&err) => sub.note_transient(&err),
Err(_) => break,
}
}
#[cfg(feature = "schema-registry")]
for item in &mut batch {
sub.transcode(item).await;
}
Some((Ok(batch), sub))
})
}
}