use std::fmt;
use std::future::Future;
use std::num::NonZeroUsize;
use std::pin::{Pin, pin};
use std::sync::{Arc, OnceLock};
use std::task::{Context, Poll};
#[cfg(feature = "schema-registry")]
use bytes::Bytes;
#[cfg(feature = "testing")]
use futures::future::Either;
use futures::{Stream, ready};
use rdkafka::consumer::Consumer as _;
use rdkafka::error::RDKafkaErrorCode;
#[cfg(feature = "schema-registry")]
use ruststream::IncomingMessage;
use ruststream::{BatchSubscriber, Seekable, Str, Subscriber};
use tracing::{debug, warn};
use crate::broker::ConnState;
use crate::convert;
use crate::eos::EOS_SOURCE_HEADER;
use crate::error::KafkaError;
#[cfg(feature = "testing")]
use crate::in_process::Member;
use crate::message::{KafkaMessage, Lane, Settlement};
use crate::record::{Cart, HeldRecord, LiveShared, Shared};
#[cfg(feature = "schema-registry")]
use crate::schema_registry::{SchemaPrefetch, SchemaRegistry};
use crate::seek::KafkaSeeker;
use crate::subscription::{Commit, LaneKey};
use crate::tracker::{CommitTracker, TrackedConsumer};
fn is_transient(err: &rdkafka::error::KafkaError) -> bool {
err.rdkafka_error_code() == Some(RDKafkaErrorCode::UnknownTopicOrPartition)
}
#[derive(Debug)]
pub(crate) enum DeliveredTopic {
One(Str),
PerRecord(Option<Str>),
}
impl DeliveredTopic {
fn shared(&self, declared: &str) -> Str {
match self {
Self::One(name) => name.clone(),
Self::PerRecord(_) => Str::from(declared),
}
}
fn own(&mut self, record: &HeldRecord) -> Option<Str> {
let Self::PerRecord(last) = self else {
return None;
};
let name = record.topic();
if let Some(known) = last
&& &**known == name
{
return Some(known.clone());
}
let minted = Str::from(name);
*last = Some(minted.clone());
Some(minted)
}
}
pub struct KafkaSubscriber {
source: Source,
topic: String,
delivered_topic: DeliveredTopic,
commit: Commit,
tracker: Arc<CommitTracker>,
lane_key: LaneKey,
seeker: Arc<KafkaSeeker>,
#[cfg(feature = "schema-registry")]
schema_registry: Option<SchemaRegistry>,
#[cfg(feature = "schema-registry")]
schema_prefetch: Option<SchemaPrefetch>,
in_transient_episode: bool,
}
enum Source {
Kafka(Cart),
#[cfg(feature = "testing")]
InProcess(Arc<Member>, Arc<Shared>),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Source>() == size_of::<Cart>());
impl KafkaSubscriber {
pub(crate) fn new(
consumer: Arc<TrackedConsumer>,
topic: String,
delivered_topic: DeliveredTopic,
commit: Commit,
tracker: Arc<CommitTracker>,
lane_key: LaneKey,
connection: Arc<ConnState>,
) -> Self {
let seeker = Arc::new(KafkaSeeker::new(
Arc::clone(&consumer),
Arc::clone(&tracker),
connection,
Str::from(topic.as_str()),
));
let cart = Arc::new(LiveShared {
consumer,
shared: Arc::new(Shared::new(
Arc::clone(&tracker),
Arc::clone(&seeker),
delivered_topic.shared(&topic),
)),
});
Self {
source: Source::Kafka(cart),
topic,
delivered_topic,
commit,
tracker,
lane_key,
seeker,
#[cfg(feature = "schema-registry")]
schema_registry: None,
#[cfg(feature = "schema-registry")]
schema_prefetch: None,
in_transient_episode: false,
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(
member: Arc<Member>,
topic: String,
delivered_topic: DeliveredTopic,
commit: Commit,
tracker: Arc<CommitTracker>,
lane_key: LaneKey,
connection: Arc<ConnState>,
) -> Self {
let seeker = Arc::new(KafkaSeeker::in_process(
Arc::clone(&member),
connection,
Str::from(topic.as_str()),
));
let shared = Arc::new(Shared::new(
Arc::clone(&tracker),
Arc::clone(&seeker),
delivered_topic.shared(&topic),
));
Self {
source: Source::InProcess(member, shared),
topic,
delivered_topic,
commit,
tracker,
lane_key,
seeker,
#[cfg(feature = "schema-registry")]
schema_registry: None,
#[cfg(feature = "schema-registry")]
schema_prefetch: None,
in_transient_episode: false,
}
}
#[cfg(feature = "schema-registry")]
pub(crate) fn with_schema_registry(mut self, registry: Option<SchemaRegistry>) -> Self {
self.schema_registry = registry;
self
}
#[cfg(feature = "schema-registry")]
pub(crate) fn with_schema_prefetch(mut self, prefetch: Option<SchemaPrefetch>) -> Self {
self.schema_prefetch = prefetch;
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",
);
}
}
pub(crate) fn cart(&self) -> Cart {
match &self.source {
Source::Kafka(cart) => Arc::clone(cart),
#[cfg(feature = "testing")]
Source::InProcess(..) => unreachable!("an in-process subscription is never kept"),
}
}
#[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(&mut self, record: HeldRecord) -> KafkaMessage {
let own_topic = self.delivered_topic.own(&record);
let topic = record.shared().topic(own_topic.as_ref());
let partition = record.partition();
let offset = record.offset();
let headers = OnceLock::new();
if matches!(self.commit, Commit::Transactional(_)) {
let mut map = convert::headers_from_message(&record);
map.insert(
Str::from_static(EOS_SOURCE_HEADER),
crate::eos::encode_source(topic, partition, offset),
);
headers.set(map).expect("the cell was just created");
}
let settlement = match &self.commit {
Commit::Auto => Settlement::Advisory,
Commit::Tracked => Settlement::Tracked {
slot: self.tracker.delivered(topic, partition, offset),
},
Commit::Transactional(_) => Settlement::Transactional {
slot: self.tracker.delivered(topic, partition, offset),
},
};
let lane = match self.lane_key {
LaneKey::RecordKey => Lane::RecordKey,
LaneKey::Partition => Lane::Partition(OnceLock::new()),
};
KafkaMessage::new(
record, headers, own_topic, partition, offset, settlement, lane,
)
}
}
impl Drop for KafkaSubscriber {
fn drop(&mut self) {
self.tracker.attention.close();
match &self.source {
Source::Kafka(cart) => cart.consumer.context().startup.release(),
#[cfg(feature = "testing")]
Source::InProcess(..) => {}
}
}
}
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 + '_ {
let cart = match &self.source {
Source::Kafka(cart) => Arc::clone(cart),
#[cfg(feature = "testing")]
Source::InProcess(member, shared) => {
let (member, shared) = (Arc::clone(member), Arc::clone(shared));
return Either::Right(in_process::stream(self, member, shared));
}
};
cart.consumer
.context()
.startup
.hand_over(&cart.consumer, &self.tracker);
let stream = KafkaStream {
#[cfg(feature = "schema-registry")]
registry: self.schema_registry.clone(),
#[cfg(feature = "schema-registry")]
prefetch: self.schema_prefetch.clone(),
#[cfg(feature = "schema-registry")]
finishing: None,
tracker: Arc::clone(&self.tracker),
sub: self,
cart,
waiting: None,
};
#[cfg(feature = "testing")]
let stream = Either::Left(stream);
stream
}
}
pub(crate) struct KafkaStream<'a> {
sub: &'a mut KafkaSubscriber,
tracker: Arc<CommitTracker>,
cart: Cart,
waiting: Option<Pin<Box<dyn Future<Output = ColdTurn> + Send>>>,
#[cfg(feature = "schema-registry")]
registry: Option<SchemaRegistry>,
#[cfg(feature = "schema-registry")]
prefetch: Option<SchemaPrefetch>,
#[cfg(feature = "schema-registry")]
finishing: Option<Pin<Box<dyn Future<Output = KafkaMessage> + Send>>>,
}
enum ColdTurn {
Record(Result<HeldRecord, rdkafka::error::KafkaError>),
Woken,
}
async fn cold_turn(tracker: Arc<CommitTracker>, cart: Cart) -> ColdTurn {
let mut woken = pin!(tracker.attention.waiter());
woken.as_mut().enable();
if tracker.attention.raised() {
return ColdTurn::Woken;
}
tokio::select! {
biased;
() = woken => ColdTurn::Woken,
received = HeldRecord::next(&cart) => ColdTurn::Record(received),
}
}
fn attended(tracker: &CommitTracker) -> Option<Result<KafkaMessage, KafkaError>> {
if let Some(err) = tracker.attention.take_start_failure() {
return Some(Err(err));
}
while let Some(delivery) = tracker.attention.take_requeued() {
if delivery.is_current() {
return Some(Ok(delivery));
}
}
None
}
#[cfg(feature = "schema-registry")]
async fn finish_delivery(
registry: Option<SchemaRegistry>,
prefetch: Option<SchemaPrefetch>,
mut item: KafkaMessage,
) -> KafkaMessage {
if let Some(prefetch) = &prefetch {
prefetch
.warm_delivery(IncomingMessage::payload(&item))
.await;
}
if let Some(registry) = ®istry
&& let Some(json) = registry
.incoming_to_json(IncomingMessage::payload(&item))
.await
{
item.replace_payload(Bytes::from(json));
}
item
}
impl KafkaStream<'_> {
fn deliver(&mut self, record: HeldRecord) -> Poll<Option<Result<KafkaMessage, KafkaError>>> {
self.sub.note_recovered();
let item = self.sub.map_delivery(record);
#[cfg(feature = "schema-registry")]
if self.registry.is_some() || self.prefetch.is_some() {
self.finishing = Some(Box::pin(finish_delivery(
self.registry.clone(),
self.prefetch.clone(),
item,
)));
return Poll::Pending;
}
Poll::Ready(Some(Ok(item)))
}
}
impl Stream for KafkaStream<'_> {
type Item = Result<KafkaMessage, KafkaError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
#[cfg(feature = "schema-registry")]
if let Some(finishing) = &mut this.finishing {
let item = ready!(finishing.as_mut().poll(cx));
this.finishing = None;
return Poll::Ready(Some(Ok(item)));
}
if this.tracker.attention.raised()
&& let Some(item) = attended(&this.tracker)
{
return Poll::Ready(Some(item));
}
if let Some(waiting) = &mut this.waiting {
let turn = ready!(waiting.as_mut().poll(cx));
this.waiting = None;
match turn {
ColdTurn::Woken => continue,
ColdTurn::Record(Ok(record)) => {
if let Poll::Ready(item) = this.deliver(record) {
return Poll::Ready(item);
}
continue;
}
ColdTurn::Record(Err(err)) if is_transient(&err) => {
this.sub.note_transient(&err);
continue;
}
ColdTurn::Record(Err(err)) => {
return Poll::Ready(Some(Err(KafkaError::consume(err))));
}
}
}
match HeldRecord::ready(&this.cart) {
Some(Ok(record)) => {
if let Poll::Ready(item) = this.deliver(record) {
return Poll::Ready(item);
}
}
Some(Err(err)) if is_transient(&err) => this.sub.note_transient(&err),
Some(Err(err)) => return Poll::Ready(Some(Err(KafkaError::consume(err)))),
None => {
this.waiting = Some(Box::pin(cold_turn(
Arc::clone(&this.tracker),
Arc::clone(&this.cart),
)));
}
}
}
}
}
impl Seekable for KafkaSubscriber {
type Seeker = KafkaSeeker;
fn seeker(&self) -> Self::Seeker {
KafkaSeeker::clone(&self.seeker)
}
}
impl BatchSubscriber for KafkaSubscriber {
type Batch = Vec<KafkaMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
let size = size.get();
let cart = match &self.source {
Source::Kafka(cart) => Arc::clone(cart),
#[cfg(feature = "testing")]
Source::InProcess(member, shared) => {
let (member, shared) = (Arc::clone(member), Arc::clone(shared));
return Either::Right(in_process::batches(self, member, shared, size));
}
};
cart.consumer
.context()
.startup
.hand_over(&cart.consumer, &self.tracker);
let tracker = Arc::clone(&self.tracker);
let batches = futures::stream::unfold(
(self, tracker, cart),
move |(sub, tracker, cart)| async move {
let first = loop {
let woken = tracker.attention.waiter();
match attended(&tracker) {
Some(Ok(delivery)) => break delivery,
Some(Err(err)) => {
drop(woken);
return Some((Err(err), (sub, tracker, cart)));
}
None => {}
}
let received = tokio::select! {
biased;
() = woken => continue,
received = HeldRecord::next(&cart) => received,
};
match received {
Ok(record) => break sub.map_delivery(record),
Err(err) if is_transient(&err) => sub.note_transient(&err),
Err(err) => {
return Some((Err(KafkaError::consume(err)), (sub, tracker, cart)));
}
}
};
sub.note_recovered();
let mut batch = Vec::with_capacity(size.min(64));
batch.push(first);
while batch.len() < size {
let Some(result) = HeldRecord::ready(&cart) else {
break;
};
match result {
Ok(record) => {
let item = sub.map_delivery(record);
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, tracker, cart)))
},
);
#[cfg(feature = "testing")]
let batches = Either::Left(batches);
batches
}
}
#[cfg(feature = "testing")]
mod in_process {
use std::sync::Arc;
use futures::Stream;
use super::{KafkaSubscriber, attended};
use crate::error::KafkaError;
use crate::in_process::{InProcessRecord, Member};
use crate::message::KafkaMessage;
use crate::record::{HeldRecord, Shared};
use crate::tracker::CommitTracker;
enum Turn {
Attended(Result<KafkaMessage, KafkaError>),
Fetched(Result<InProcessRecord, KafkaError>),
}
async fn turn(member: &Arc<Member>, tracker: &CommitTracker) -> Turn {
loop {
let woken = tracker.attention.waiter();
if let Some(item) = attended(tracker) {
return Turn::Attended(item);
}
tokio::select! {
biased;
() = woken => {}
fetched = member.next() => return Turn::Fetched(fetched),
}
}
}
fn deliver(
sub: &mut KafkaSubscriber,
shared: &Arc<Shared>,
record: InProcessRecord,
) -> KafkaMessage {
sub.map_delivery(HeldRecord::InProcess {
shared: Arc::clone(shared),
record: Box::new(record),
})
}
#[cfg(feature = "schema-registry")]
async fn finish(sub: &KafkaSubscriber, item: KafkaMessage) -> KafkaMessage {
super::finish_delivery(
sub.schema_registry.clone(),
sub.schema_prefetch.clone(),
item,
)
.await
}
pub(super) fn stream(
sub: &mut KafkaSubscriber,
member: Arc<Member>,
shared: Arc<Shared>,
) -> impl Stream<Item = Result<KafkaMessage, KafkaError>> + Send + '_ {
futures::stream::unfold((sub, member, shared), async move |(sub, member, shared)| {
let item = match turn(&member, &shared.tracker).await {
Turn::Attended(item) => item,
Turn::Fetched(Ok(record)) => {
let item = deliver(sub, &shared, record);
#[cfg(feature = "schema-registry")]
let item = finish(sub, item).await;
Ok(item)
}
Turn::Fetched(Err(err)) => Err(err),
};
Some((item, (sub, member, shared)))
})
}
pub(super) fn batches(
sub: &mut KafkaSubscriber,
member: Arc<Member>,
shared: Arc<Shared>,
size: usize,
) -> impl Stream<Item = Result<Vec<KafkaMessage>, KafkaError>> + Send + '_ {
futures::stream::unfold(
(sub, member, shared, size, None::<KafkaError>),
async move |(sub, member, shared, size, failed)| {
if let Some(err) = failed {
return Some((Err(err), (sub, member, shared, size, None)));
}
let first = match turn(&member, &shared.tracker).await {
Turn::Attended(Ok(delivery)) => delivery,
Turn::Fetched(Ok(record)) => deliver(sub, &shared, record),
Turn::Attended(Err(err)) | Turn::Fetched(Err(err)) => {
return Some((Err(err), (sub, member, shared, size, None)));
}
};
let mut batch = Vec::with_capacity(size.min(64));
batch.push(first);
let mut failed = None;
while batch.len() < size {
match member.fetch() {
Some(Ok(record)) => batch.push(deliver(sub, &shared, record)),
Some(Err(err)) => {
failed = Some(err);
break;
}
None => break,
}
}
#[cfg(feature = "schema-registry")]
let batch = {
let mut finished = Vec::with_capacity(batch.len());
for item in batch {
finished.push(finish(sub, item).await);
}
finished
};
Some((Ok(batch), (sub, member, shared, size, failed)))
},
)
}
}