use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use rdkafka::Offset;
use rdkafka::consumer::{Consumer as _, StreamConsumer};
use rdkafka::producer::FutureRecord;
use rdkafka::util::Timeout;
use ruststream::Headers;
use crate::broker::SharedConn;
use crate::convert;
use crate::error::KafkaError;
use crate::tracker::TrackingContext;
pub const RETRY_COUNT_HEADER: &str = "kafka-retry-count";
pub const DLQ_SOURCE_TOPIC_HEADER: &str = "kafka-dlq-source-topic";
pub const DLQ_SOURCE_PARTITION_HEADER: &str = "kafka-dlq-source-partition";
pub const DLQ_SOURCE_OFFSET_HEADER: &str = "kafka-dlq-source-offset";
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Retry {
Topic(String),
SeekBack,
Drop,
}
pub(crate) struct RetryContext {
policy: Option<Retry>,
max_deliveries: Option<u32>,
dead_letter: Option<String>,
conn: SharedConn,
consumer: Arc<StreamConsumer<TrackingContext>>,
seeks: Mutex<HashMap<(String, i32, i64), u32>>,
}
impl std::fmt::Debug for RetryContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RetryContext")
.field("policy", &self.policy)
.field("max_deliveries", &self.max_deliveries)
.field("dead_letter", &self.dead_letter)
.finish_non_exhaustive()
}
}
impl RetryContext {
pub(crate) fn new(
policy: Option<Retry>,
max_deliveries: Option<u32>,
dead_letter: Option<String>,
conn: SharedConn,
consumer: Arc<StreamConsumer<TrackingContext>>,
) -> Self {
Self {
policy,
max_deliveries,
dead_letter,
conn,
consumer,
seeks: Mutex::new(HashMap::new()),
}
}
pub(crate) fn policy(&self) -> Option<&Retry> {
self.policy.as_ref()
}
pub(crate) fn dead_letter(&self) -> Option<&str> {
self.dead_letter.as_deref()
}
pub(crate) fn over_cap(&self, delivery: u32) -> bool {
self.max_deliveries.is_some_and(|cap| delivery > cap)
}
pub(crate) fn next_seek_delivery(&self, topic: &str, partition: i32, offset: i64) -> u32 {
let mut seeks = self.seeks.lock().expect("seek counter mutex poisoned");
let seeks_done = *seeks
.entry((topic.to_owned(), partition, offset))
.or_insert(0);
drop(seeks);
seeks_done + 2
}
pub(crate) fn record_seek(&self, topic: &str, partition: i32, offset: i64) {
let mut seeks = self.seeks.lock().expect("seek counter mutex poisoned");
*seeks
.entry((topic.to_owned(), partition, offset))
.or_insert(0) += 1;
}
pub(crate) fn forget_seeks(&self, topic: &str, partition: i32, offset: i64) {
let mut seeks = self.seeks.lock().expect("seek counter mutex poisoned");
seeks.remove(&(topic.to_owned(), partition, offset));
}
pub(crate) fn seek_back(
&self,
topic: &str,
partition: i32,
offset: i64,
) -> Result<(), KafkaError> {
self.consumer
.seek(
topic,
partition,
Offset::Offset(offset),
Duration::from_secs(5),
)
.map_err(KafkaError::consume)
}
pub(crate) async fn republish(
&self,
topic: &str,
payload: &[u8],
headers: &Headers,
) -> Result<(), KafkaError> {
let state = self.conn.get().ok_or(KafkaError::NotConnected)?;
let parts = convert::headers_for_publish(headers)?;
let mut record = FutureRecord::<[u8], [u8]>::to(topic).payload(payload);
if let Some(key) = &parts.key {
record = record.key(key.as_ref());
}
if let Some(partition) = parts.partition {
record = record.partition(partition);
}
if let Some(native) = parts.headers {
record = record.headers(native);
}
state
.producer()
.send(record, Timeout::Never)
.await
.map(|_delivery| ())
.map_err(|(err, _record)| KafkaError::publish(err))
}
}