use std::{collections::BTreeMap, fmt, sync::Arc};
use crate::{MqError, MqResult};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TopicPartition {
pub topic: String,
pub partition: i32,
}
impl TopicPartition {
#[must_use]
pub fn new(topic: impl Into<String>, partition: i32) -> Self {
Self {
topic: topic.into(),
partition,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OffsetCommit {
pub topic: String,
pub partition: i32,
pub offset: i64,
}
impl OffsetCommit {
#[must_use]
pub fn topic_partition(&self) -> TopicPartition {
TopicPartition::new(self.topic.clone(), self.partition)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct OffsetPosition {
pub(crate) topic: String,
pub(crate) partition: i32,
pub(crate) offset: i64,
}
impl OffsetPosition {
pub(crate) fn new(topic: impl Into<String>, partition: i32, offset: i64) -> Self {
Self {
topic: topic.into(),
partition,
offset,
}
}
pub(crate) fn topic_partition(&self) -> TopicPartition {
TopicPartition::new(self.topic.clone(), self.partition)
}
}
pub(crate) trait OffsetCommitter: Send + Sync {
fn stage_offset(&self, position: OffsetPosition) -> MqResult<()>;
fn commit_offset(&self, position: OffsetPosition) -> MqResult<()>;
}
#[derive(Clone)]
pub struct KafkaOffset {
position: OffsetPosition,
committer: Option<Arc<dyn OffsetCommitter>>,
}
impl KafkaOffset {
pub(crate) fn new(position: OffsetPosition, committer: Arc<dyn OffsetCommitter>) -> Self {
Self {
position,
committer: Some(committer),
}
}
#[must_use]
pub fn detached(topic: impl Into<String>, partition: i32, offset: i64) -> Self {
Self {
position: OffsetPosition::new(topic, partition, offset),
committer: None,
}
}
#[must_use]
pub fn topic(&self) -> &str {
&self.position.topic
}
#[must_use]
pub fn partition(&self) -> i32 {
self.position.partition
}
#[must_use]
pub fn offset(&self) -> i64 {
self.position.offset
}
#[must_use]
pub fn next_offset(&self) -> i64 {
self.position.offset + 1
}
#[must_use]
pub fn topic_partition(&self) -> TopicPartition {
self.position.topic_partition()
}
pub fn commit(&self) -> MqResult<()> {
let Some(committer) = &self.committer else {
return Err(MqError::ControlUnavailable);
};
committer.commit_offset(self.position.clone())
}
pub fn mark_processed(&self) -> MqResult<()> {
let Some(committer) = &self.committer else {
return Err(MqError::ControlUnavailable);
};
committer.stage_offset(self.position.clone())
}
}
impl fmt::Debug for KafkaOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KafkaOffset")
.field("topic", &self.position.topic)
.field("partition", &self.position.partition)
.field("offset", &self.position.offset)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub struct KafkaOffsetBatch {
offsets: Vec<KafkaOffset>,
}
impl KafkaOffsetBatch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_offsets<I>(offsets: I) -> Self
where
I: IntoIterator<Item = KafkaOffset>,
{
Self {
offsets: offsets.into_iter().collect(),
}
}
pub fn push(&mut self, offset: KafkaOffset) {
self.offsets.push(offset);
}
#[must_use]
pub fn len(&self) -> usize {
self.offsets.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.offsets.is_empty()
}
pub fn commit(&self) -> MqResult<()> {
let mut by_partition = BTreeMap::<TopicPartition, &KafkaOffset>::new();
for offset in &self.offsets {
by_partition
.entry(offset.topic_partition())
.and_modify(|current| {
if offset.offset() > current.offset() {
*current = offset;
}
})
.or_insert(offset);
}
for offset in by_partition.values() {
offset.commit()?;
}
Ok(())
}
}
#[derive(Clone)]
pub struct KafkaBatchOffset {
positions: Vec<OffsetPosition>,
committer: KafkaBatchCommitter,
}
#[derive(Clone)]
enum KafkaBatchCommitter {
Offset(Option<Arc<dyn OffsetCommitter>>),
Custom(Arc<dyn Fn() -> MqResult<()> + Send + Sync>),
}
impl KafkaBatchOffset {
pub(crate) fn custom<F>(positions: Vec<OffsetPosition>, commit: F) -> Self
where
F: Fn() -> MqResult<()> + Send + Sync + 'static,
{
Self {
positions,
committer: KafkaBatchCommitter::Custom(Arc::new(commit)),
}
}
#[must_use]
pub fn detached<I>(positions: I) -> Self
where
I: IntoIterator<Item = OffsetCommit>,
{
Self {
positions: positions
.into_iter()
.map(|position| {
OffsetPosition::new(position.topic, position.partition, position.offset - 1)
})
.collect(),
committer: KafkaBatchCommitter::Offset(None),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.positions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
pub fn commit(&self) -> MqResult<()> {
match &self.committer {
KafkaBatchCommitter::Offset(Some(committer)) => {
for position in &self.positions {
committer.commit_offset(position.clone())?;
}
Ok(())
}
KafkaBatchCommitter::Offset(None) => Err(MqError::ControlUnavailable),
KafkaBatchCommitter::Custom(commit) => commit(),
}
}
}
impl fmt::Debug for KafkaBatchOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KafkaBatchOffset")
.field("positions", &self.positions)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn pos(offset: i64) -> OffsetPosition {
OffsetPosition::new("topic", 0, offset)
}
#[derive(Debug, Default)]
struct FakeCommitter {
staged: Mutex<Vec<OffsetPosition>>,
committed: Mutex<Vec<OffsetPosition>>,
}
impl OffsetCommitter for FakeCommitter {
fn stage_offset(&self, position: OffsetPosition) -> MqResult<()> {
self.staged.lock().expect("staged lock").push(position);
Ok(())
}
fn commit_offset(&self, position: OffsetPosition) -> MqResult<()> {
self.committed
.lock()
.expect("committed lock")
.push(position);
Ok(())
}
}
#[test]
fn offset_batch_commits_last_offset_per_partition() {
let committer = Arc::new(FakeCommitter::default());
let offsets = [
KafkaOffset::new(pos(0), committer.clone()),
KafkaOffset::new(OffsetPosition::new("topic", 1, 0), committer.clone()),
KafkaOffset::new(pos(1), committer.clone()),
KafkaOffset::new(OffsetPosition::new("topic", 1, 1), committer.clone()),
];
KafkaOffsetBatch::from_offsets(offsets)
.commit()
.expect("batch commits");
let staged = committer.staged.lock().expect("staged lock");
let committed = committer.committed.lock().expect("committed lock");
assert_eq!(staged.len(), 0);
assert_eq!(committed.len(), 2);
assert!(committed.iter().all(|position| position.offset == 1));
}
}