use std::convert::Infallible;
use std::fmt;
use std::sync::Arc;
use bytes::Bytes;
use rdkafka::consumer::{Consumer as _, StreamConsumer};
use ruststream::{AckError, Headers, IncomingMessage, Partitioned};
use crate::retry::{
DLQ_SOURCE_OFFSET_HEADER, DLQ_SOURCE_PARTITION_HEADER, DLQ_SOURCE_TOPIC_HEADER,
RETRY_COUNT_HEADER, Retry, RetryContext,
};
use crate::tracker::{CommitTracker, TrackingContext};
pub const PARTITION_KEY_HEADER: &str = "kafka-partition-key";
pub const PARTITION_HEADER: &str = "kafka-partition";
pub(crate) enum Settlement {
Advisory,
Tracked {
consumer: Arc<StreamConsumer<TrackingContext>>,
tracker: Arc<CommitTracker>,
},
Transactional { tracker: Arc<CommitTracker> },
}
#[derive(Debug)]
pub struct KafkaMessage {
payload: Bytes,
headers: Headers,
topic: String,
partition: i32,
offset: i64,
timestamp_millis: Option<i64>,
settlement: Settlement,
lane: Option<Bytes>,
retry: Option<Arc<RetryContext>>,
}
impl fmt::Debug for Settlement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Advisory => f.write_str("Advisory"),
Self::Tracked { .. } => f.debug_struct("Tracked").finish_non_exhaustive(),
Self::Transactional { .. } => f.debug_struct("Transactional").finish_non_exhaustive(),
}
}
}
impl KafkaMessage {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
payload: Bytes,
headers: Headers,
topic: String,
partition: i32,
offset: i64,
timestamp_millis: Option<i64>,
settlement: Settlement,
lane: Option<Bytes>,
retry: Option<Arc<RetryContext>>,
) -> Self {
Self {
payload,
headers,
topic,
partition,
offset,
timestamp_millis,
settlement,
lane,
retry,
}
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
#[must_use]
pub fn partition(&self) -> i32 {
self.partition
}
#[must_use]
pub fn offset(&self) -> i64 {
self.offset
}
#[must_use]
pub fn timestamp_millis(&self) -> Option<i64> {
self.timestamp_millis
}
#[must_use]
pub fn key(&self) -> Option<&[u8]> {
self.headers.get(PARTITION_KEY_HEADER)
}
#[cfg(feature = "schema-registry")]
pub(crate) fn replace_payload(&mut self, payload: Bytes) {
self.payload = payload;
}
fn settle(self) -> Result<(), AckError> {
match self.settlement {
Settlement::Advisory => Ok(()),
Settlement::Tracked { consumer, tracker } => tracker
.settle_with(&self.topic, self.partition, self.offset, |position| {
consumer.store_offset(&self.topic, self.partition, position)
})
.map_err(|err| AckError::Broker(Box::new(err))),
Settlement::Transactional { tracker } => {
let infallible: Result<(), Infallible> =
tracker
.settle_with(&self.topic, self.partition, self.offset, |_position| Ok(()));
infallible.expect("no-op store cannot fail");
Ok(())
}
}
}
fn retry_attempts(&self) -> u32 {
self.headers
.get_str(RETRY_COUNT_HEADER)
.and_then(|value| value.parse().ok())
.unwrap_or(0)
}
async fn retry_requeue(self, retry: Arc<RetryContext>) -> Result<(), AckError> {
match retry.policy() {
Some(Retry::Topic(topic)) => {
let next_delivery = self.retry_attempts() + 2;
if retry.over_cap(next_delivery) {
return self.drop_path(&retry).await;
}
let mut headers = self.headers.clone();
headers.insert(RETRY_COUNT_HEADER, (self.retry_attempts() + 1).to_string());
retry
.republish(topic, &self.payload, &headers)
.await
.map_err(|err| AckError::Broker(Box::new(err)))?;
self.settle()
}
Some(Retry::SeekBack) => {
let next_delivery =
retry.next_seek_delivery(&self.topic, self.partition, self.offset);
if retry.over_cap(next_delivery) {
retry.forget_seeks(&self.topic, self.partition, self.offset);
return self.drop_path(&retry).await;
}
retry.record_seek(&self.topic, self.partition, self.offset);
retry
.seek_back(&self.topic, self.partition, self.offset)
.map_err(|err| AckError::Broker(Box::new(err)))
}
Some(Retry::Drop) | None => self.drop_path(&retry).await,
}
}
async fn drop_path(self, retry: &RetryContext) -> Result<(), AckError> {
if let Some(dlq) = retry.dead_letter() {
let mut headers = self.headers.clone();
headers.insert(DLQ_SOURCE_TOPIC_HEADER, self.topic.clone());
headers.insert(DLQ_SOURCE_PARTITION_HEADER, self.partition.to_string());
headers.insert(DLQ_SOURCE_OFFSET_HEADER, self.offset.to_string());
retry
.republish(dlq, &self.payload, &headers)
.await
.map_err(|err| AckError::Broker(Box::new(err)))?;
}
self.settle()
}
}
impl IncomingMessage for KafkaMessage {
fn payload(&self) -> &[u8] {
&self.payload
}
fn headers(&self) -> &Headers {
&self.headers
}
async fn ack(self) -> Result<(), AckError> {
self.settle()
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
match (self.retry.clone(), requeue) {
(Some(retry), true) => self.retry_requeue(retry).await,
(Some(retry), false) => self.drop_path(&retry).await,
(None, true) => Ok(()),
(None, false) => self.settle(),
}
}
fn partition_key(&self) -> Option<&[u8]> {
self.lane.as_deref()
}
}
impl Partitioned for KafkaMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.lane.as_deref()
}
}