use std::fmt;
use std::sync::Arc;
use bytes::Bytes;
use rdkafka::ClientConfig;
use rdkafka::error::KafkaError as RdKafkaError;
use rdkafka::types::RDKafkaType;
use regex::Regex;
use ruststream::{HeaderMap, Str};
use tokio::sync::Notify;
use super::log::{Isolation, Stored, WireHeader};
use super::{Cluster, Reset, Strategy};
use crate::error::KafkaError;
use crate::message::PARTITION_KEY_HEADER;
use crate::seek::KafkaPosition;
use crate::subscription::StartOffset;
use crate::tracker::CommitTracker;
#[derive(Debug, Clone)]
pub(crate) enum Reader {
Subscribed {
topics: Vec<String>,
patterns: Vec<Regex>,
},
Assigned { topic: String, partitions: Vec<i32> },
}
#[derive(Debug, Clone)]
pub(crate) struct MemberSpec {
pub(crate) name: String,
pub(crate) group: Option<String>,
pub(crate) reader: Reader,
pub(crate) start: StartOffset,
pub(crate) isolation: Isolation,
pub(crate) reset: Reset,
pub(crate) auto_store: bool,
pub(crate) auto_commit: bool,
pub(crate) strategies: Vec<Strategy>,
pub(crate) tracker: Arc<CommitTracker>,
}
impl MemberSpec {
pub(crate) fn read(
name: String,
group: Option<String>,
names: Option<&[String]>,
assigned: Option<(String, Vec<i32>)>,
start: StartOffset,
config: &ClientConfig,
tracker: Arc<CommitTracker>,
) -> Result<Self, KafkaError> {
super::create_offline(config, RDKafkaType::RD_KAFKA_CONSUMER)
.map_err(KafkaError::subscribe)?;
let native = config
.create_native_config()
.map_err(KafkaError::subscribe)?;
let value = |key: &str| native.get(key).map_err(KafkaError::subscribe);
let reader = match (names, assigned) {
(_, Some((topic, partitions))) => Reader::Assigned { topic, partitions },
(names, None) => {
let mut topics = Vec::new();
let mut patterns = Vec::new();
for entry in names.unwrap_or_default() {
if entry.starts_with('^') {
patterns.push(Regex::new(entry).map_err(|err| {
KafkaError::subscribe(RdKafkaError::Subscription(format!(
"invalid topic regex {entry:?}: {err}"
)))
})?);
} else {
topics.push(entry.clone());
}
}
Reader::Subscribed { topics, patterns }
}
};
let strategies = Strategy::parse(&value("partition.assignment.strategy")?);
Ok(Self {
name,
group,
reader,
start,
isolation: if value("isolation.level")? == "read_uncommitted" {
Isolation::ReadUncommitted
} else {
Isolation::ReadCommitted
},
reset: native
.get("auto.offset.reset")
.map_or(Reset::Latest, |value| Reset::parse(&value)),
auto_store: value("enable.auto.offset.store")? == "true",
auto_commit: value("enable.auto.commit")? == "true",
strategies: if strategies.is_empty() {
vec![Strategy::Range]
} else {
strategies
},
tracker,
})
}
pub(crate) fn reads(&self, topic: &str) -> bool {
match &self.reader {
Reader::Subscribed { topics, patterns } => {
topics.iter().any(|name| name == topic)
|| patterns.iter().any(|pattern| pattern.is_match(topic))
}
Reader::Assigned { topic: named, .. } => named == topic,
}
}
}
pub(crate) struct Member {
cluster: Arc<Cluster>,
id: u64,
wake: Arc<Notify>,
}
impl fmt::Debug for Member {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Member")
.field("id", &self.id)
.finish_non_exhaustive()
}
}
impl Member {
pub(super) fn new(cluster: Arc<Cluster>, id: u64, wake: Arc<Notify>) -> Self {
Self { cluster, id, wake }
}
pub(super) fn id(&self) -> u64 {
self.id
}
pub(crate) fn fetch(self: &Arc<Self>) -> Option<Result<InProcessRecord, KafkaError>> {
self.cluster.fetch(self)
}
pub(crate) async fn next(self: &Arc<Self>) -> Result<InProcessRecord, KafkaError> {
loop {
let woken = self.wake.notified();
if let Some(fetched) = self.fetch() {
return fetched;
}
woken.await;
}
}
pub(crate) fn seek(&self, to: &KafkaPosition) -> Result<(), KafkaError> {
self.cluster.seek(self.id, to)
}
pub(crate) fn seek_partition(
&self,
topic: &str,
partition: i32,
offset: i64,
) -> Result<(), KafkaError> {
self.cluster
.seek_partition(self.id, topic, partition, offset)
}
pub(crate) fn group(&self) -> Option<String> {
self.cluster.group_of(self.id)
}
}
impl Drop for Member {
fn drop(&mut self) {
self.cluster.leave(self.id);
}
}
pub(crate) struct InProcessRecord {
member: Arc<Member>,
topic: String,
partition: i32,
offset: i64,
stored: Arc<Stored>,
}
impl fmt::Debug for InProcessRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InProcessRecord")
.field("topic", &self.topic)
.field("partition", &self.partition)
.field("offset", &self.offset)
.finish_non_exhaustive()
}
}
impl InProcessRecord {
pub(super) fn new(
member: Arc<Member>,
topic: String,
partition: i32,
offset: i64,
stored: Arc<Stored>,
) -> Self {
Self {
member,
topic,
partition,
offset,
stored,
}
}
pub(crate) fn topic(&self) -> &str {
&self.topic
}
pub(crate) fn partition(&self) -> i32 {
self.partition
}
pub(crate) fn offset(&self) -> i64 {
self.offset
}
pub(crate) fn timestamp_millis(&self) -> i64 {
self.stored.timestamp
}
pub(crate) fn payload(&self) -> &[u8] {
&self.stored.payload
}
pub(crate) fn key(&self) -> Option<&[u8]> {
self.stored.key.as_deref()
}
pub(crate) fn headers(&self) -> &[WireHeader] {
&self.stored.headers
}
pub(crate) fn store(&self, topic: &str, partition: i32, position: i64) {
self.member
.cluster
.store(self.member.id, topic, partition, position);
}
}
impl Drop for InProcessRecord {
fn drop(&mut self) {
self.member.cluster.delivery_done();
}
}
pub(super) fn headers_of(stored: &Stored) -> HeaderMap {
let mut headers = HeaderMap::new();
for (name, value) in &stored.headers {
if name.eq_ignore_ascii_case(PARTITION_KEY_HEADER) {
continue;
}
headers.insert(name.as_str(), value.clone().unwrap_or_else(Bytes::new));
}
if let Some(key) = &stored.key {
headers.insert(Str::from_static(PARTITION_KEY_HEADER), key.clone());
}
headers
}