use std::convert::Infallible;
use std::fmt;
use std::future::{Future, ready};
use std::io::Write as _;
use std::sync::{Arc, OnceLock};
#[cfg(feature = "schema-registry")]
use bytes::Bytes;
use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned, Positioned, Str};
use crate::convert;
use crate::record::{HeldRecord, Shared};
use crate::seek::{KafkaPosition, KafkaSeeker};
use crate::tracker::PartitionSlot;
pub const PARTITION_KEY_HEADER: &str = "kafka-partition-key";
pub(crate) enum Settlement {
Advisory,
Tracked { slot: PartitionSlot },
Transactional { slot: PartitionSlot },
}
pub(crate) enum Lane {
RecordKey,
Partition(OnceLock<PartitionText>),
}
impl Lane {
fn of<'a>(&'a self, msg: &'a KafkaMessage) -> Option<&'a [u8]> {
match self {
Self::RecordKey => msg.record.key(),
Self::Partition(text) => Some(
text.get_or_init(|| PartitionText::of(msg.partition))
.as_bytes(),
),
}
}
}
impl fmt::Debug for Lane {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RecordKey => f.write_str("RecordKey"),
Self::Partition(_) => f.write_str("Partition"),
}
}
}
#[derive(Debug)]
pub(crate) struct PartitionText {
bytes: [u8; 11],
len: u8,
}
impl PartitionText {
fn of(partition: i32) -> Self {
let mut bytes = [0u8; 11];
let mut cursor = &mut bytes[..];
write!(cursor, "{partition}").expect("eleven bytes hold the widest i32");
let len = u8::try_from(11 - cursor.len()).expect("eleven bytes hold the widest i32");
Self { bytes, len }
}
fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
}
pub struct KafkaMessage {
record: HeldRecord,
#[cfg(feature = "schema-registry")]
transcoded: Option<Bytes>,
headers: OnceLock<HeaderMap>,
topic: Option<Str>,
partition: i32,
offset: i64,
settlement: Settlement,
lane: Lane,
}
impl fmt::Debug for KafkaMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = f.debug_struct("KafkaMessage");
out.field("record", &self.record);
#[cfg(feature = "schema-registry")]
out.field("transcoded", &self.transcoded);
out.field("headers", &self.headers)
.field("topic", self.topic_name())
.field("partition", &self.partition)
.field("offset", &self.offset)
.field("settlement", &self.settlement)
.field("lane", &self.lane)
.finish()
}
}
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(
record: HeldRecord,
headers: OnceLock<HeaderMap>,
topic: Option<Str>,
partition: i32,
offset: i64,
settlement: Settlement,
lane: Lane,
) -> Self {
Self {
record,
#[cfg(feature = "schema-registry")]
transcoded: None,
headers,
topic,
partition,
offset,
settlement,
lane,
}
}
fn topic_name(&self) -> &Str {
self.record.shared().topic(self.topic.as_ref())
}
pub(crate) fn seeker_handle(&self) -> Arc<KafkaSeeker> {
Arc::clone(&self.record.shared().seeker)
}
pub(crate) fn shared_handle(&self) -> Arc<Shared> {
Arc::clone(self.record.shared())
}
pub(crate) fn own_topic(&self) -> Option<Str> {
self.topic.clone()
}
#[must_use]
pub fn topic(&self) -> &str {
self.topic_name()
}
#[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.record.timestamp_millis()
}
#[must_use]
pub fn key(&self) -> Option<&[u8]> {
self.record.key()
}
#[cfg(feature = "schema-registry")]
pub(crate) fn replace_payload(&mut self, payload: Bytes) {
self.transcoded = Some(payload);
}
pub(crate) fn is_current(&self) -> bool {
match &self.settlement {
Settlement::Advisory => true,
Settlement::Tracked { slot } | Settlement::Transactional { slot } => {
self.record.shared().tracker.is_current(*slot)
}
}
}
fn settle(self) -> Result<(), AckError> {
match &self.settlement {
Settlement::Advisory => Ok(()),
Settlement::Tracked { slot } => self
.record
.shared()
.tracker
.settle_with(*slot, self.offset, |position, target| {
self.record.store(self.offset, position, target)
})
.map_err(|err| AckError::Broker(Box::new(err))),
Settlement::Transactional { slot } => {
let infallible: Result<(), Infallible> = self.record.shared().tracker.settle_with(
*slot,
self.offset,
|_position, _target| Ok(()),
);
infallible.expect("no-op store cannot fail");
Ok(())
}
}
}
}
impl IncomingMessage for KafkaMessage {
fn payload(&self) -> &[u8] {
#[cfg(feature = "schema-registry")]
if let Some(transcoded) = &self.transcoded {
return transcoded;
}
self.record.payload()
}
fn headers(&self) -> &HeaderMap {
self.headers
.get_or_init(|| convert::headers_from_message(&self.record))
}
fn ack(self) -> impl Future<Output = Result<(), AckError>> {
ready(self.settle())
}
fn nack(self, requeue: bool) -> impl Future<Output = Result<(), AckError>> {
ready(match (requeue, &self.settlement) {
(true, Settlement::Advisory) => Err(AckError::Unsupported),
(true, Settlement::Tracked { .. }) => {
let tracker = Arc::clone(&self.record.shared().tracker);
tracker.attention.requeue(self);
Ok(())
}
(true, Settlement::Transactional { .. }) => Ok(()),
(false, _) => self.settle(),
})
}
fn partition_key(&self) -> Option<&[u8]> {
self.lane.of(self)
}
}
impl Positioned for KafkaMessage {
type Position = KafkaPosition;
fn position(&self) -> Self::Position {
KafkaPosition::topic_offset(&**self.topic_name(), self.partition, self.offset)
}
}
impl Partitioned for KafkaMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.lane.of(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_partition_lane_key_is_the_partition_in_decimal() {
for partition in [0, 7, 42, i32::MAX, i32::MIN] {
assert_eq!(
PartitionText::of(partition).as_bytes(),
partition.to_string().as_bytes(),
"the lane key of partition {partition}",
);
}
}
}