use rdkafka::TopicPartitionList;
use rdkafka::client::ClientContext;
use rdkafka::consumer::{BaseConsumer, Consumer, ConsumerContext};
use rdkafka::statistics::Statistics;
use rdkafka::types::RDKafkaRespErr;
use std::collections::VecDeque;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug)]
pub(crate) enum Intent {
Assign(TopicPartitionList),
Revoke(TopicPartitionList),
Error(rdkafka::error::RDKafkaErrorCode),
}
impl Intent {
pub(crate) fn kind(&self) -> &'static str {
match self {
Intent::Assign(_) => "assign",
Intent::Revoke(_) => "revoke",
Intent::Error(_) => "error",
}
}
}
#[derive(Debug, Default)]
pub(crate) struct SourceContext {
pub(crate) intents: Mutex<VecDeque<Intent>>,
pub(crate) stats: Mutex<Option<Box<Statistics>>>,
pub(crate) closing: AtomicBool,
}
impl ClientContext for SourceContext {
fn stats(&self, statistics: Statistics) {
*self.stats.lock().expect("stats lock") = Some(Box::new(statistics));
}
fn log(&self, level: rdkafka::config::RDKafkaLogLevel, fac: &str, log_message: &str) {
use rdkafka::config::RDKafkaLogLevel as L;
match level {
L::Emerg | L::Alert | L::Critical | L::Error => {
tracing::error!(target: "librdkafka", fac, "{log_message}");
}
L::Warning => tracing::warn!(target: "librdkafka", fac, "{log_message}"),
L::Notice | L::Info => tracing::info!(target: "librdkafka", fac, "{log_message}"),
L::Debug => tracing::debug!(target: "librdkafka", fac, "{log_message}"),
}
}
fn error(&self, error: rdkafka::error::KafkaError, reason: &str) {
tracing::warn!(target: "librdkafka", %error, "{reason}");
}
}
impl ConsumerContext for SourceContext {
fn rebalance(
&self,
base_consumer: &BaseConsumer<Self>,
err: RDKafkaRespErr,
tpl: &mut TopicPartitionList,
) {
if self.closing.load(Ordering::Acquire) {
let result = match err {
RDKafkaRespErr::RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS => base_consumer.assign(tpl),
_ => base_consumer.unassign(),
};
if let Err(e) = result {
tracing::warn!(error = %e, "rebalance completion during close failed");
}
return;
}
let intent = match err {
RDKafkaRespErr::RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS => Intent::Assign(tpl.clone()),
RDKafkaRespErr::RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS => Intent::Revoke(tpl.clone()),
other => {
if let Err(e) = base_consumer.unassign() {
tracing::warn!(error = %e, "unassign after a rebalance error failed");
}
let code: rdkafka::error::RDKafkaErrorCode = other.into();
Intent::Error(code)
}
};
self.intents.lock().expect("intent lock").push_back(intent);
}
}
#[cfg(test)]
mod tests {
use super::*;
use rdkafka::ClientConfig;
use rdkafka::config::FromClientConfigAndContext;
fn consumer() -> BaseConsumer<SourceContext> {
let mut cc = ClientConfig::new();
cc.set("bootstrap.servers", "127.0.0.1:1");
cc.set("group.id", "ctx-test");
BaseConsumer::from_config_and_context(&cc, SourceContext::default()).expect("consumer")
}
#[test]
fn an_arbitrary_rebalance_error_unassigns_and_queues_the_code() {
let consumer = consumer();
let mut tpl = TopicPartitionList::new();
tpl.add_partition("orders", 0);
consumer.assign(&tpl).expect("local assign");
assert_eq!(consumer.assignment().expect("assignment").count(), 1);
let mut cb_tpl = TopicPartitionList::new();
consumer.context().rebalance(
&consumer,
RDKafkaRespErr::RD_KAFKA_RESP_ERR_REBALANCE_IN_PROGRESS,
&mut cb_tpl,
);
assert_eq!(
consumer.assignment().expect("assignment").count(),
0,
"the error arm must synchronize state with unassign"
);
let intents = consumer.context().intents.lock().expect("intent lock");
assert!(
matches!(
intents.front(),
Some(Intent::Error(
rdkafka::error::RDKafkaErrorCode::RebalanceInProgress
))
),
"the typed code is queued for poll_events: {intents:?}"
);
}
#[test]
fn assign_and_revoke_events_stay_deferred() {
let consumer = consumer();
let mut cb_tpl = TopicPartitionList::new();
cb_tpl.add_partition("orders", 0);
consumer.context().rebalance(
&consumer,
RDKafkaRespErr::RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS,
&mut cb_tpl,
);
assert_eq!(
consumer.assignment().expect("assignment").count(),
0,
"an assign event must not be accepted from the callback"
);
let kinds: Vec<&'static str> = consumer
.context()
.intents
.lock()
.expect("intent lock")
.iter()
.map(Intent::kind)
.collect();
assert_eq!(kinds, vec!["assign"]);
}
}