use async_trait::async_trait;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error, info, warn};
use crate::error::{StateError, StateResult};
use crate::state::StateBackend;
use crate::watermark::Watermark;
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct TopicPartition {
pub topic: String,
pub partition: u32,
}
impl TopicPartition {
pub fn new(topic: String, partition: u32) -> Self {
Self { topic, partition }
}
pub fn to_key(&self) -> String {
format!("{}:{}", self.topic, self.partition)
}
}
impl std::fmt::Display for TopicPartition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.topic, self.partition)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OffsetInfo {
pub offset: i64,
pub high_watermark: Option<i64>,
pub last_updated: DateTime<Utc>,
pub metadata: Option<String>,
}
impl OffsetInfo {
pub fn new(offset: i64) -> Self {
Self {
offset,
high_watermark: None,
last_updated: Utc::now(),
metadata: None,
}
}
pub fn with_high_watermark(offset: i64, high_watermark: i64) -> Self {
Self {
offset,
high_watermark: Some(high_watermark),
last_updated: Utc::now(),
metadata: None,
}
}
pub fn lag(&self) -> Option<i64> {
self.high_watermark.map(|hwm| hwm - self.offset)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OffsetCommitStrategy {
Periodic {
interval: Duration,
},
EveryNEvents {
n: usize,
},
OnWatermark,
Manual,
}
impl Default for OffsetCommitStrategy {
fn default() -> Self {
Self::Periodic {
interval: Duration::from_secs(5),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum OffsetResetStrategy {
Earliest,
Latest,
Specific(i64),
Timestamp(i64),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OffsetStats {
pub total_commits: u64,
pub successful_commits: u64,
pub failed_commits: u64,
pub total_offsets_committed: u64,
pub last_commit_time: Option<DateTime<Utc>>,
pub avg_commit_duration_ms: u64,
}
#[async_trait]
pub trait OffsetStore: Send + Sync {
async fn store_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
offset_info: &OffsetInfo,
) -> StateResult<()>;
async fn get_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
) -> StateResult<Option<OffsetInfo>>;
async fn get_all_offsets(
&self,
consumer_group: &str,
) -> StateResult<HashMap<TopicPartition, OffsetInfo>>;
async fn delete_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
) -> StateResult<()>;
async fn clear_offsets(&self, consumer_group: &str) -> StateResult<()>;
}
pub struct InMemoryOffsetStore {
offsets: Arc<DashMap<String, HashMap<TopicPartition, OffsetInfo>>>,
}
impl InMemoryOffsetStore {
pub fn new() -> Self {
Self {
offsets: Arc::new(DashMap::new()),
}
}
}
impl Default for InMemoryOffsetStore {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl OffsetStore for InMemoryOffsetStore {
async fn store_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
offset_info: &OffsetInfo,
) -> StateResult<()> {
self.offsets
.entry(consumer_group.to_string())
.or_insert_with(HashMap::new)
.insert(topic_partition.clone(), offset_info.clone());
Ok(())
}
async fn get_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
) -> StateResult<Option<OffsetInfo>> {
Ok(self
.offsets
.get(consumer_group)
.and_then(|offsets| offsets.get(topic_partition).cloned()))
}
async fn get_all_offsets(
&self,
consumer_group: &str,
) -> StateResult<HashMap<TopicPartition, OffsetInfo>> {
Ok(self
.offsets
.get(consumer_group)
.map(|offsets| offsets.clone())
.unwrap_or_default())
}
async fn delete_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
) -> StateResult<()> {
if let Some(mut offsets) = self.offsets.get_mut(consumer_group) {
offsets.remove(topic_partition);
}
Ok(())
}
async fn clear_offsets(&self, consumer_group: &str) -> StateResult<()> {
self.offsets.remove(consumer_group);
Ok(())
}
}
pub struct StateBackendOffsetStore<B: StateBackend> {
backend: Arc<B>,
prefix: String,
}
impl<B: StateBackend> StateBackendOffsetStore<B> {
pub fn new(backend: Arc<B>) -> Self {
Self::with_prefix(backend, "kafka_offset:".to_string())
}
pub fn with_prefix(backend: Arc<B>, prefix: String) -> Self {
Self { backend, prefix }
}
fn build_key(&self, consumer_group: &str, topic_partition: &TopicPartition) -> Vec<u8> {
format!(
"{}{}:{}",
self.prefix,
consumer_group,
topic_partition.to_key()
)
.into_bytes()
}
fn build_group_prefix(&self, consumer_group: &str) -> Vec<u8> {
format!("{}{}:", self.prefix, consumer_group).into_bytes()
}
}
#[async_trait]
impl<B: StateBackend> OffsetStore for StateBackendOffsetStore<B> {
async fn store_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
offset_info: &OffsetInfo,
) -> StateResult<()> {
let key = self.build_key(consumer_group, topic_partition);
let value = bincode::serialize(offset_info).map_err(|e| {
StateError::SerializationFailed {
key: String::from_utf8_lossy(&key).to_string(),
reason: e.to_string(),
}
})?;
self.backend.put(&key, &value).await
}
async fn get_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
) -> StateResult<Option<OffsetInfo>> {
let key = self.build_key(consumer_group, topic_partition);
match self.backend.get(&key).await? {
Some(value) => {
let offset_info = bincode::deserialize(&value).map_err(|e| {
StateError::DeserializationFailed {
key: String::from_utf8_lossy(&key).to_string(),
reason: e.to_string(),
}
})?;
Ok(Some(offset_info))
}
None => Ok(None),
}
}
async fn get_all_offsets(
&self,
consumer_group: &str,
) -> StateResult<HashMap<TopicPartition, OffsetInfo>> {
let prefix = self.build_group_prefix(consumer_group);
let keys = self.backend.list_keys(&prefix).await?;
let mut offsets = HashMap::new();
for key in keys {
if let Some(value) = self.backend.get(&key).await? {
if let Ok(offset_info) = bincode::deserialize::<OffsetInfo>(&value) {
let key_str = String::from_utf8_lossy(&key);
if let Some(tp_str) = key_str.strip_prefix(&format!("{}{}:", self.prefix, consumer_group)) {
if let Some((topic, partition_str)) = tp_str.rsplit_once(':') {
if let Ok(partition) = partition_str.parse::<u32>() {
let tp = TopicPartition::new(topic.to_string(), partition);
offsets.insert(tp, offset_info);
}
}
}
}
}
}
Ok(offsets)
}
async fn delete_offset(
&self,
consumer_group: &str,
topic_partition: &TopicPartition,
) -> StateResult<()> {
let key = self.build_key(consumer_group, topic_partition);
self.backend.delete(&key).await
}
async fn clear_offsets(&self, consumer_group: &str) -> StateResult<()> {
let prefix = self.build_group_prefix(consumer_group);
let keys = self.backend.list_keys(&prefix).await?;
for key in keys {
self.backend.delete(&key).await?;
}
Ok(())
}
}
pub struct OffsetManager<B: StateBackend, S: OffsetStore> {
consumer_group: String,
backend: Arc<B>,
store: Arc<S>,
strategy: OffsetCommitStrategy,
pending_offsets: Arc<DashMap<TopicPartition, OffsetInfo>>,
committed_offsets: Arc<DashMap<TopicPartition, OffsetInfo>>,
stats: Arc<RwLock<OffsetStats>>,
last_commit: Arc<Mutex<DateTime<Utc>>>,
event_counter: Arc<Mutex<usize>>,
last_watermark: Arc<Mutex<Option<Watermark>>>,
}
impl<B: StateBackend, S: OffsetStore> OffsetManager<B, S> {
pub fn new(
consumer_group: String,
backend: Arc<B>,
store: Arc<S>,
strategy: OffsetCommitStrategy,
) -> Self {
Self {
consumer_group,
backend,
store,
strategy,
pending_offsets: Arc::new(DashMap::new()),
committed_offsets: Arc::new(DashMap::new()),
stats: Arc::new(RwLock::new(OffsetStats::default())),
last_commit: Arc::new(Mutex::new(Utc::now())),
event_counter: Arc::new(Mutex::new(0)),
last_watermark: Arc::new(Mutex::new(None)),
}
}
pub async fn update_offset(
&mut self,
topic: &str,
partition: u32,
offset: i64,
) -> StateResult<()> {
let tp = TopicPartition::new(topic.to_string(), partition);
let offset_info = OffsetInfo::new(offset);
self.pending_offsets.insert(tp.clone(), offset_info);
debug!(
consumer_group = %self.consumer_group,
topic = topic,
partition = partition,
offset = offset,
"Updated offset"
);
if self.should_auto_commit().await {
self.commit().await?;
}
Ok(())
}
pub async fn update_offset_with_watermark(
&mut self,
topic: &str,
partition: u32,
offset: i64,
high_watermark: i64,
) -> StateResult<()> {
let tp = TopicPartition::new(topic.to_string(), partition);
let offset_info = OffsetInfo::with_high_watermark(offset, high_watermark);
self.pending_offsets.insert(tp.clone(), offset_info);
debug!(
consumer_group = %self.consumer_group,
topic = topic,
partition = partition,
offset = offset,
high_watermark = high_watermark,
"Updated offset with high watermark"
);
if self.should_auto_commit().await {
self.commit().await?;
}
Ok(())
}
pub async fn get_offset(&self, topic: &str, partition: u32) -> StateResult<Option<i64>> {
let tp = TopicPartition::new(topic.to_string(), partition);
if let Some(info) = self.committed_offsets.get(&tp) {
return Ok(Some(info.offset));
}
if let Some(info) = self.store.get_offset(&self.consumer_group, &tp).await? {
return Ok(Some(info.offset));
}
Ok(None)
}
pub async fn get_offset_info(
&self,
topic: &str,
partition: u32,
) -> StateResult<Option<OffsetInfo>> {
let tp = TopicPartition::new(topic.to_string(), partition);
if let Some(info) = self.committed_offsets.get(&tp) {
return Ok(Some(info.clone()));
}
self.store.get_offset(&self.consumer_group, &tp).await
}
pub async fn commit(&mut self) -> StateResult<usize> {
let start_time = Utc::now();
let pending: Vec<_> = self
.pending_offsets
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect();
if pending.is_empty() {
debug!(consumer_group = %self.consumer_group, "No pending offsets to commit");
return Ok(0);
}
info!(
consumer_group = %self.consumer_group,
count = pending.len(),
"Committing offsets"
);
let mut stats = self.stats.write().await;
stats.total_commits += 1;
let mut committed_count = 0;
let mut failed = false;
for (tp, offset_info) in pending {
match self
.store
.store_offset(&self.consumer_group, &tp, &offset_info)
.await
{
Ok(()) => {
self.committed_offsets.insert(tp.clone(), offset_info.clone());
self.pending_offsets.remove(&tp);
committed_count += 1;
debug!(topic_partition = %tp, offset = offset_info.offset, "Committed offset");
}
Err(e) => {
error!(
topic_partition = %tp,
error = %e,
"Failed to commit offset"
);
failed = true;
}
}
}
if failed {
stats.failed_commits += 1;
} else {
stats.successful_commits += 1;
}
stats.total_offsets_committed += committed_count as u64;
stats.last_commit_time = Some(start_time);
let duration = Utc::now().signed_duration_since(start_time);
let duration_ms = duration.num_milliseconds() as u64;
if stats.avg_commit_duration_ms == 0 {
stats.avg_commit_duration_ms = duration_ms;
} else {
stats.avg_commit_duration_ms =
(stats.avg_commit_duration_ms * 9 + duration_ms) / 10;
}
drop(stats);
*self.last_commit.lock().await = Utc::now();
info!(
consumer_group = %self.consumer_group,
committed = committed_count,
duration_ms = duration_ms,
"Offset commit completed"
);
if failed {
Err(StateError::TransactionFailed {
operation: "offset_commit".to_string(),
reason: format!("Failed to commit some offsets, {} succeeded", committed_count),
})
} else {
Ok(committed_count)
}
}
pub async fn lag(
&self,
topic: &str,
partition: u32,
high_watermark: i64,
) -> StateResult<i64> {
let current_offset = self.get_offset(topic, partition).await?.unwrap_or(0);
Ok(high_watermark - current_offset)
}
pub async fn total_lag(&self) -> StateResult<i64> {
let mut total = 0i64;
for entry in self.committed_offsets.iter() {
if let Some(lag) = entry.value().lag() {
total += lag;
}
}
Ok(total)
}
pub async fn restore(&mut self) -> StateResult<usize> {
info!(consumer_group = %self.consumer_group, "Restoring offsets");
let offsets = self.store.get_all_offsets(&self.consumer_group).await?;
let count = offsets.len();
for (tp, offset_info) in offsets {
self.committed_offsets.insert(tp.clone(), offset_info);
debug!(topic_partition = %tp, "Restored offset");
}
info!(
consumer_group = %self.consumer_group,
count = count,
"Offset restore completed"
);
Ok(count)
}
pub async fn reset_offset(
&mut self,
topic: &str,
partition: u32,
strategy: OffsetResetStrategy,
partition_high_watermark: Option<i64>,
) -> StateResult<i64> {
let tp = TopicPartition::new(topic.to_string(), partition);
let new_offset = match strategy {
OffsetResetStrategy::Earliest => 0,
OffsetResetStrategy::Latest => partition_high_watermark.unwrap_or(0),
OffsetResetStrategy::Specific(offset) => offset,
OffsetResetStrategy::Timestamp(ts) => {
warn!(
topic = topic,
partition = partition,
timestamp = ts,
"Timestamp-based reset not fully implemented, using timestamp as offset"
);
ts
}
};
let offset_info = OffsetInfo::new(new_offset);
self.store
.store_offset(&self.consumer_group, &tp, &offset_info)
.await?;
self.committed_offsets.insert(tp.clone(), offset_info);
self.pending_offsets.remove(&tp);
info!(
consumer_group = %self.consumer_group,
topic = topic,
partition = partition,
new_offset = new_offset,
strategy = ?strategy,
"Reset offset"
);
Ok(new_offset)
}
async fn should_auto_commit(&mut self) -> bool {
match &self.strategy {
OffsetCommitStrategy::Periodic { interval } => {
let last_commit = *self.last_commit.lock().await;
let elapsed = Utc::now().signed_duration_since(last_commit);
elapsed.num_milliseconds() as u64 >= interval.as_millis() as u64
}
OffsetCommitStrategy::EveryNEvents { n } => {
let mut counter = self.event_counter.lock().await;
*counter += 1;
if *counter >= *n {
*counter = 0;
true
} else {
false
}
}
OffsetCommitStrategy::OnWatermark => false, OffsetCommitStrategy::Manual => false,
}
}
pub async fn notify_watermark(&mut self, watermark: Watermark) -> StateResult<bool> {
let mut last_wm = self.last_watermark.lock().await;
if matches!(self.strategy, OffsetCommitStrategy::OnWatermark) {
if last_wm.is_none() || watermark > last_wm.unwrap() {
*last_wm = Some(watermark);
drop(last_wm);
self.commit().await?;
return Ok(true);
}
}
Ok(false)
}
pub async fn stats(&self) -> OffsetStats {
self.stats.read().await.clone()
}
pub async fn get_all_committed(&self) -> HashMap<TopicPartition, OffsetInfo> {
self.committed_offsets
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect()
}
pub async fn get_all_pending(&self) -> HashMap<TopicPartition, OffsetInfo> {
self.pending_offsets
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect()
}
pub async fn clear(&mut self) -> StateResult<()> {
info!(consumer_group = %self.consumer_group, "Clearing all offsets");
self.store.clear_offsets(&self.consumer_group).await?;
self.committed_offsets.clear();
self.pending_offsets.clear();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::MemoryStateBackend;
#[test]
fn test_topic_partition() {
let tp = TopicPartition::new("test-topic".to_string(), 5);
assert_eq!(tp.topic, "test-topic");
assert_eq!(tp.partition, 5);
assert_eq!(tp.to_key(), "test-topic:5");
assert_eq!(tp.to_string(), "test-topic:5");
}
#[test]
fn test_offset_info_lag() {
let info = OffsetInfo::with_high_watermark(100, 200);
assert_eq!(info.lag(), Some(100));
let info_no_hwm = OffsetInfo::new(100);
assert_eq!(info_no_hwm.lag(), None);
}
#[tokio::test]
async fn test_in_memory_offset_store() {
let store = InMemoryOffsetStore::new();
let tp = TopicPartition::new("test".to_string(), 0);
let offset_info = OffsetInfo::new(100);
store
.store_offset("group1", &tp, &offset_info)
.await
.unwrap();
let retrieved = store.get_offset("group1", &tp).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().offset, 100);
let all = store.get_all_offsets("group1").await.unwrap();
assert_eq!(all.len(), 1);
store.delete_offset("group1", &tp).await.unwrap();
let retrieved = store.get_offset("group1", &tp).await.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn test_state_backend_offset_store() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = StateBackendOffsetStore::new(backend.clone());
let tp = TopicPartition::new("test-topic".to_string(), 3);
let offset_info = OffsetInfo::new(500);
store
.store_offset("group1", &tp, &offset_info)
.await
.unwrap();
let retrieved = store.get_offset("group1", &tp).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().offset, 500);
let all = store.get_all_offsets("group1").await.unwrap();
assert_eq!(all.len(), 1);
assert_eq!(all.get(&tp).unwrap().offset, 500);
store.clear_offsets("group1").await.unwrap();
let all = store.get_all_offsets("group1").await.unwrap();
assert_eq!(all.len(), 0);
}
#[tokio::test]
async fn test_offset_manager_update_and_commit() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store.clone(),
OffsetCommitStrategy::Manual,
);
manager.update_offset("topic1", 0, 100).await.unwrap();
let pending = manager.get_all_pending().await;
assert_eq!(pending.len(), 1);
let committed = manager.commit().await.unwrap();
assert_eq!(committed, 1);
let offset = manager.get_offset("topic1", 0).await.unwrap();
assert_eq!(offset, Some(100));
let pending = manager.get_all_pending().await;
assert_eq!(pending.len(), 0);
}
#[tokio::test]
async fn test_offset_manager_lag() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Manual,
);
manager.update_offset("topic1", 0, 100).await.unwrap();
manager.commit().await.unwrap();
let lag = manager.lag("topic1", 0, 200).await.unwrap();
assert_eq!(lag, 100);
}
#[tokio::test]
async fn test_offset_manager_restore() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager1 = OffsetManager::new(
"test-group".to_string(),
backend.clone(),
store.clone(),
OffsetCommitStrategy::Manual,
);
manager1.update_offset("topic1", 0, 100).await.unwrap();
manager1.update_offset("topic1", 1, 200).await.unwrap();
manager1.commit().await.unwrap();
let mut manager2 = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Manual,
);
let restored = manager2.restore().await.unwrap();
assert_eq!(restored, 2);
assert_eq!(manager2.get_offset("topic1", 0).await.unwrap(), Some(100));
assert_eq!(manager2.get_offset("topic1", 1).await.unwrap(), Some(200));
}
#[tokio::test]
async fn test_offset_manager_reset() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Manual,
);
manager.update_offset("topic1", 0, 100).await.unwrap();
manager.commit().await.unwrap();
let new_offset = manager
.reset_offset("topic1", 0, OffsetResetStrategy::Earliest, None)
.await
.unwrap();
assert_eq!(new_offset, 0);
assert_eq!(manager.get_offset("topic1", 0).await.unwrap(), Some(0));
let new_offset = manager
.reset_offset("topic1", 0, OffsetResetStrategy::Latest, Some(500))
.await
.unwrap();
assert_eq!(new_offset, 500);
assert_eq!(manager.get_offset("topic1", 0).await.unwrap(), Some(500));
let new_offset = manager
.reset_offset("topic1", 0, OffsetResetStrategy::Specific(250), None)
.await
.unwrap();
assert_eq!(new_offset, 250);
assert_eq!(manager.get_offset("topic1", 0).await.unwrap(), Some(250));
}
#[tokio::test]
async fn test_offset_manager_periodic_strategy() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Periodic {
interval: Duration::from_millis(100),
},
);
manager.update_offset("topic1", 0, 100).await.unwrap();
assert_eq!(manager.get_all_pending().await.len(), 1);
tokio::time::sleep(Duration::from_millis(150)).await;
manager.update_offset("topic1", 0, 200).await.unwrap();
assert_eq!(manager.get_offset("topic1", 0).await.unwrap(), Some(200));
}
#[tokio::test]
async fn test_offset_manager_watermark_strategy() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::OnWatermark,
);
manager.update_offset("topic1", 0, 100).await.unwrap();
assert_eq!(manager.get_all_pending().await.len(), 1);
let watermark = Watermark::new(1000);
let committed = manager.notify_watermark(watermark).await.unwrap();
assert!(committed);
assert_eq!(manager.get_offset("topic1", 0).await.unwrap(), Some(100));
assert_eq!(manager.get_all_pending().await.len(), 0);
}
#[tokio::test]
async fn test_offset_manager_stats() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Manual,
);
let stats = manager.stats().await;
assert_eq!(stats.total_commits, 0);
manager.update_offset("topic1", 0, 100).await.unwrap();
manager.commit().await.unwrap();
let stats = manager.stats().await;
assert_eq!(stats.total_commits, 1);
assert_eq!(stats.successful_commits, 1);
assert_eq!(stats.total_offsets_committed, 1);
assert!(stats.last_commit_time.is_some());
}
#[tokio::test]
async fn test_offset_manager_multiple_partitions() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Manual,
);
manager.update_offset("topic1", 0, 100).await.unwrap();
manager.update_offset("topic1", 1, 200).await.unwrap();
manager.update_offset("topic2", 0, 300).await.unwrap();
let pending = manager.get_all_pending().await;
assert_eq!(pending.len(), 3);
let committed = manager.commit().await.unwrap();
assert_eq!(committed, 3);
assert_eq!(manager.get_offset("topic1", 0).await.unwrap(), Some(100));
assert_eq!(manager.get_offset("topic1", 1).await.unwrap(), Some(200));
assert_eq!(manager.get_offset("topic2", 0).await.unwrap(), Some(300));
}
#[tokio::test]
async fn test_offset_manager_with_high_watermark() {
let backend = Arc::new(MemoryStateBackend::new(None));
let store = Arc::new(InMemoryOffsetStore::new());
let mut manager = OffsetManager::new(
"test-group".to_string(),
backend,
store,
OffsetCommitStrategy::Manual,
);
manager
.update_offset_with_watermark("topic1", 0, 100, 200)
.await
.unwrap();
manager.commit().await.unwrap();
let info = manager.get_offset_info("topic1", 0).await.unwrap();
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.offset, 100);
assert_eq!(info.high_watermark, Some(200));
assert_eq!(info.lag(), Some(100));
let total_lag = manager.total_lag().await.unwrap();
assert_eq!(total_lag, 100);
}
}