use bytes::Bytes;
use kafka_client_protocol::Message;
use std::collections::HashMap;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{RwLock, mpsc};
use tokio::time::interval;
use tracing::{debug, warn};
use crate::cluster::ClusterClient;
use crate::error::{KafkaError, Result};
use crate::protocol::{
ConsumerProtocolAssignment, FetchPartition, FetchRequest, FetchResponse, FetchTopic,
FindCoordinatorRequest, FindCoordinatorResponse, HeartbeatRequest, JoinGroupRequest,
JoinGroupRequestProtocol, JoinGroupResponse, LeaveGroupRequest, ListOffsetsPartition,
ListOffsetsRequest, ListOffsetsTopic, OffsetCommitRequest, OffsetCommitRequestPartition,
OffsetCommitRequestTopic, OffsetFetchRequest, OffsetFetchRequestGroup, OffsetFetchRequestTopic,
OffsetFetchRequestTopics, SyncGroupRequest, SyncGroupRequestAssignment, SyncGroupResponse,
TopicPartition,
};
enum ConsumerCommand {
Subscribe { topics: Vec<String> },
}
#[derive(Debug, Clone)]
struct FetchParams {
timeout_ms: i32,
min_bytes: i32,
max_bytes: i32,
partition_max_bytes: i32,
}
#[derive(Debug, Clone)]
pub struct ConsumerConfig {
pub group_id: String,
pub auto_commit: bool,
pub auto_commit_interval: Duration,
pub auto_offset_reset: AutoOffsetReset,
pub min_bytes: i32,
pub max_bytes: i32,
pub partition_max_bytes: i32,
pub max_wait: Duration,
pub session_timeout: Duration,
pub rebalance_timeout: Duration,
pub heartbeat_interval: Duration,
pub partition_assignment_strategy: PartitionAssignmentStrategy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoOffsetReset {
Earliest,
Latest,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartitionAssignmentStrategy {
Range,
RoundRobin,
CooperativeSticky,
}
impl Default for ConsumerConfig {
fn default() -> Self {
Self {
group_id: format!("{}-consumer", crate::NAME),
auto_commit: true,
auto_commit_interval: Duration::from_secs(5),
auto_offset_reset: AutoOffsetReset::Latest,
min_bytes: 1,
max_bytes: 50 * 1024 * 1024,
partition_max_bytes: 1024 * 1024,
max_wait: Duration::from_millis(500),
session_timeout: Duration::from_secs(10),
rebalance_timeout: Duration::from_secs(30),
heartbeat_interval: Duration::from_secs(3),
partition_assignment_strategy: PartitionAssignmentStrategy::Range,
}
}
}
#[derive(Debug, Clone)]
pub struct Header {
pub key: String,
pub value: Bytes,
}
#[derive(Debug, Clone)]
pub struct ConsumerRecord {
pub topic: String,
pub partition: i32,
pub offset: i64,
pub timestamp: i64,
pub key: Option<Bytes>,
pub value: Bytes,
pub headers: Vec<Header>,
}
#[derive(Debug, Clone, Default)]
struct GroupState {
member_id: String,
generation_id: i32,
leader: String,
protocol_name: Option<String>,
assigned_partitions: HashMap<String, Vec<i32>>,
}
pub struct Consumer {
cluster: Arc<ClusterClient>,
subscribed_topics: Vec<String>,
offsets: Arc<RwLock<HashMap<String, HashMap<i32, i64>>>>,
config: ConsumerConfig,
group_state: Arc<RwLock<GroupState>>,
coordinator: Arc<RwLock<Option<std::net::SocketAddr>>>,
running: Arc<std::sync::atomic::AtomicBool>,
command_tx: mpsc::UnboundedSender<ConsumerCommand>,
}
impl Consumer {
pub(crate) fn new(cluster: Arc<ClusterClient>, config: ConsumerConfig) -> Self {
let (command_tx, command_rx) = mpsc::unbounded_channel();
let consumer = Self {
cluster: cluster.clone(),
subscribed_topics: Vec::new(),
offsets: Arc::new(RwLock::new(HashMap::new())),
config: config.clone(),
group_state: Arc::new(RwLock::new(GroupState::default())),
coordinator: Arc::new(RwLock::new(None)),
running: Arc::new(std::sync::atomic::AtomicBool::new(true)),
command_tx,
};
consumer.start(cluster, config, command_rx);
consumer
}
fn start(
&self,
cluster: Arc<ClusterClient>,
config: ConsumerConfig,
mut command_rx: mpsc::UnboundedReceiver<ConsumerCommand>,
) {
let offsets = self.offsets.clone();
let group_state = self.group_state.clone();
let coordinator = self.coordinator.clone();
let running = self.running.clone();
let mut commit_interval = interval(config.auto_commit_interval);
let mut heartbeat_interval = interval(config.heartbeat_interval);
let mut current_topics: Vec<String> = Vec::new();
let mut needs_rejoin = false;
tokio::spawn(async move {
loop {
tokio::select! {
_ = commit_interval.tick() => {
if !running.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
if config.auto_commit && !config.group_id.is_empty() {
let (generation_id, member_id) = {
let gs = group_state.read().await;
(gs.generation_id, if gs.member_id.is_empty() { None } else { Some(gs.member_id.clone()) })
};
if let Err(e) = Self::do_commit(
&cluster,
&config.group_id,
&offsets,
&coordinator,
generation_id,
member_id,
)
.await
{
debug!("Auto commit failed: {}", e);
}
}
}
_ = heartbeat_interval.tick() => {
if !running.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
if !config.group_id.is_empty() {
if let Err(e) = Self::background_heartbeat(
&cluster,
&config.group_id,
&group_state,
&coordinator,
)
.await
{
debug!("Heartbeat failed: {}", e);
needs_rejoin = true;
}
}
}
cmd = command_rx.recv() => {
if !running.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
match cmd {
Some(ConsumerCommand::Subscribe { topics }) => {
current_topics = topics;
needs_rejoin = true;
let mut gs = group_state.write().await;
gs.assigned_partitions.clear();
gs.member_id.clear();
*coordinator.write().await = None;
}
None => break,
}
}
}
if needs_rejoin && !current_topics.is_empty() && !config.group_id.is_empty() {
needs_rejoin = false;
match Self::background_join_group(
&cluster,
&config.group_id,
&group_state,
&coordinator,
&offsets,
&config,
¤t_topics,
)
.await
{
Ok(()) => {}
Err(e) => {
warn!("background_join_group failed: {:?}, will retry", e);
let mut gs = group_state.write().await;
gs.assigned_partitions.clear();
gs.member_id.clear();
*coordinator.write().await = None;
needs_rejoin = true;
tokio::time::sleep(Duration::from_millis(
1000 + (rand::random::<u64>() % 1000),
))
.await;
}
}
}
}
if !config.group_id.is_empty() {
let coord_addr = {
let c = coordinator.read().await;
*c
};
if let Some(coord_addr) = coord_addr {
let member_id = {
let gs = group_state.read().await;
gs.member_id.clone()
};
if !member_id.is_empty() {
use crate::protocol::leave_group_request::MemberIdentity;
let request = LeaveGroupRequest {
group_id: config.group_id.clone(),
member_id: member_id.clone(),
members: vec![MemberIdentity {
member_id,
group_instance_id: None,
reason: None,
}],
};
let leave_response: std::result::Result<
crate::protocol::LeaveGroupResponse,
_,
> = cluster.send_to_broker(coord_addr, &request).await;
if let Err(e) = leave_response {
debug!("LeaveGroup on shutdown failed: {}", e);
}
}
}
}
});
}
pub async fn subscribe(&mut self, topics: Vec<String>) -> Result<()> {
self.subscribed_topics = topics.clone();
if self.config.group_id.is_empty() {
self.init_offsets_simple(topics).await?;
} else {
let _ = self.command_tx.send(ConsumerCommand::Subscribe {
topics: topics.clone(),
});
}
Ok(())
}
async fn background_heartbeat(
cluster: &Arc<ClusterClient>,
group_id: &str,
group_state: &Arc<RwLock<GroupState>>,
coordinator: &Arc<RwLock<Option<std::net::SocketAddr>>>,
) -> Result<()> {
if group_id.is_empty() {
return Ok(());
}
let (generation_id, member_id) = {
let gs = group_state.read().await;
if gs.assigned_partitions.is_empty() || gs.member_id.is_empty() {
return Ok(());
}
(gs.generation_id, gs.member_id.clone())
};
let coord_addr = {
let c = coordinator.read().await;
match *c {
Some(addr) => addr,
None => return Ok(()),
}
};
let request = HeartbeatRequest {
group_id: group_id.to_string(),
generation_id,
member_id: member_id.clone(),
group_instance_id: None,
};
let response: crate::protocol::HeartbeatResponse =
cluster.send_to_broker(coord_addr, &request).await?;
if response.error_code == 27 {
warn!(
"Heartbeat REBALANCE_IN_PROGRESS for {}, will rejoin on next tick",
member_id
);
let mut gs = group_state.write().await;
gs.assigned_partitions.clear();
gs.member_id.clear();
*coordinator.write().await = None;
return Err(KafkaError::Protocol("Rebalance required".to_string()));
} else if response.error_code != 0 {
debug!(
"Heartbeat error for {}: error_code={}",
member_id, response.error_code
);
return Err(KafkaError::Protocol(format!(
"Heartbeat failed: error {}",
response.error_code
)));
}
Ok(())
}
async fn compute_assignment(
topics: &[String],
join_response: &JoinGroupResponse,
cluster: &Arc<ClusterClient>,
strategy: PartitionAssignmentStrategy,
) -> Result<Bytes> {
let all_members: Vec<&str> = join_response
.members
.iter()
.map(|m| m.member_id.as_str())
.collect();
if all_members.is_empty() {
return Ok(Bytes::new());
}
let mut assignments: HashMap<String, Vec<TopicPartition>> = HashMap::new();
for member_id in &all_members {
assignments.insert(member_id.to_string(), Vec::new());
}
for topic in topics {
let partitions = cluster
.metadata()
.get_partitions(topic)
.await
.unwrap_or_default();
match strategy {
PartitionAssignmentStrategy::Range => {
let partitions_per_member = partitions.len() / all_members.len();
let remainder = partitions.len() % all_members.len();
let mut partition_idx = 0;
for (i, member_id) in all_members.iter().enumerate() {
let count = partitions_per_member + if i < remainder { 1 } else { 0 };
if count > 0 {
let member_partitions: Vec<i32> =
partitions[partition_idx..partition_idx + count].to_vec();
assignments
.get_mut(*member_id)
.unwrap()
.push(TopicPartition {
topic: topic.to_string(),
partitions: member_partitions,
});
partition_idx += count;
}
}
}
PartitionAssignmentStrategy::RoundRobin => {
for (i, &partition) in partitions.iter().enumerate() {
let member_idx = i % all_members.len();
let member_id = all_members[member_idx];
assignments
.get_mut(member_id)
.unwrap()
.push(TopicPartition {
topic: topic.to_string(),
partitions: vec![partition],
});
}
}
PartitionAssignmentStrategy::CooperativeSticky => {
let partitions_per_member = partitions.len() / all_members.len();
let remainder = partitions.len() % all_members.len();
let mut partition_idx = 0;
for (i, member_id) in all_members.iter().enumerate() {
let count = partitions_per_member + if i < remainder { 1 } else { 0 };
if count > 0 {
let member_partitions: Vec<i32> =
partitions[partition_idx..partition_idx + count].to_vec();
assignments
.get_mut(*member_id)
.unwrap()
.push(TopicPartition {
topic: topic.to_string(),
partitions: member_partitions,
});
partition_idx += count;
}
}
}
}
}
let current_member_id = join_response.member_id.clone();
let current_assignment = assignments
.get(current_member_id.as_str())
.cloned()
.unwrap_or_default();
let protocol_assignment = ConsumerProtocolAssignment {
assigned_partitions: current_assignment,
user_data: None,
};
let mut buf = bytes::BytesMut::new();
protocol_assignment
.encode(&mut buf, 0)
.map_err(|e| KafkaError::Protocol(e.to_string()))?;
Ok(buf.freeze())
}
async fn init_offsets_for_partitions(&self, partitions: Vec<(String, i32)>) -> Result<()> {
if partitions.is_empty() {
return Ok(());
}
let timestamp = match self.config.auto_offset_reset {
AutoOffsetReset::Latest => -1i64,
AutoOffsetReset::Earliest => -2i64,
AutoOffsetReset::None => return Err(KafkaError::NoOffsetStored),
};
for (topic, partition) in partitions {
let offset = self.list_offset(&topic, partition, timestamp).await?;
let mut offsets = self.offsets.write().await;
offsets
.entry(topic)
.or_insert_with(HashMap::new)
.insert(partition, offset);
}
Ok(())
}
async fn init_offsets_simple(&self, topics: Vec<String>) -> Result<()> {
let mut needs_initialization: Vec<(String, i32)> = Vec::new();
{
let mut offsets = self.offsets.write().await;
for topic in &topics {
let partitions = self
.cluster
.metadata()
.get_partitions(topic)
.await
.ok_or_else(|| KafkaError::TopicNotFound(topic.clone()))?;
let topic_offsets = offsets.entry(topic.clone()).or_insert_with(HashMap::new);
for partition in &partitions {
if !topic_offsets.contains_key(partition) {
topic_offsets.insert(*partition, -1);
needs_initialization.push((topic.clone(), *partition));
}
}
}
}
self.init_offsets_for_partitions(needs_initialization)
.await?;
Ok(())
}
async fn list_offset(&self, topic: &str, partition: i32, timestamp: i64) -> Result<i64> {
let leader_addr = self
.cluster
.metadata()
.get_partition_leader(topic, partition)
.await
.ok_or_else(|| KafkaError::PartitionNotFound(topic.to_string(), partition))?;
let request = ListOffsetsRequest {
replica_id: -1,
isolation_level: 0,
topics: vec![ListOffsetsTopic {
name: topic.to_string(),
partitions: vec![ListOffsetsPartition {
partition_index: partition,
current_leader_epoch: -1,
timestamp,
}],
}],
timeout_ms: -1,
};
let response = self
.cluster
.send_to_broker::<ListOffsetsRequest, crate::protocol::ListOffsetsResponse>(
leader_addr,
&request,
)
.await?;
for topic_response in response.topics {
if topic_response.name == topic {
for partition_response in topic_response.partitions {
if partition_response.partition_index == partition {
if partition_response.error_code != 0 {
return Err(KafkaError::OffsetNotFound(topic.to_string(), partition));
}
return Ok(partition_response.offset);
}
}
}
}
Err(KafkaError::OffsetNotFound(topic.to_string(), partition))
}
async fn background_join_group(
cluster: &Arc<ClusterClient>,
group_id: &str,
group_state: &Arc<RwLock<GroupState>>,
coordinator: &Arc<RwLock<Option<std::net::SocketAddr>>>,
offsets: &Arc<RwLock<HashMap<String, HashMap<i32, i64>>>>,
config: &ConsumerConfig,
topics: &[String],
) -> Result<()> {
let coord_addr = {
let request = FindCoordinatorRequest {
key: group_id.to_string(),
key_type: 0,
coordinator_keys: vec![group_id.to_string()],
};
let response: FindCoordinatorResponse = cluster.send_to_any_broker(&request).await?;
if response.error_code == 15 {
return Err(KafkaError::NoCoordinator);
}
if response.error_code != 0 {
return Err(KafkaError::NoCoordinator);
}
let (host, port) = if !response.host.is_empty() {
(response.host.clone(), response.port)
} else if let Some(coord) = response.coordinators.first() {
if coord.error_code == 15 {
return Err(KafkaError::NoCoordinator);
}
if coord.error_code != 0 {
return Err(KafkaError::NoCoordinator);
}
(coord.host.clone(), coord.port)
} else {
return Err(KafkaError::NoCoordinator);
};
format!("{}:{}", host, port)
.to_socket_addrs()
.map_err(|_| KafkaError::NoCoordinator)?
.next()
.ok_or(KafkaError::NoCoordinator)?
};
*coordinator.write().await = Some(coord_addr);
let protocol_metadata = {
let mut buf = bytes::BytesMut::new();
use bytes::BufMut;
buf.put_i16(2);
buf.put_i32(topics.len() as i32);
for t in topics {
buf.put_i16(t.len() as i16);
buf.put_slice(t.as_bytes());
}
buf.put_i32(-1);
buf.put_i32(0);
buf.put_i32(-1);
buf.freeze()
};
let mut member_id = String::new();
for _join_attempt in 0..10u32 {
let request = JoinGroupRequest {
group_id: group_id.to_string(),
session_timeout_ms: config.session_timeout.as_millis() as i32,
rebalance_timeout_ms: config.rebalance_timeout.as_millis() as i32,
member_id: member_id.clone(),
group_instance_id: None,
protocol_type: "consumer".to_string(),
protocols: vec![JoinGroupRequestProtocol {
name: match config.partition_assignment_strategy {
PartitionAssignmentStrategy::Range => "range".to_string(),
PartitionAssignmentStrategy::RoundRobin => "roundrobin".to_string(),
PartitionAssignmentStrategy::CooperativeSticky => {
"cooperative-sticky".to_string()
}
},
metadata: protocol_metadata.clone(),
}],
reason: None,
};
let response: JoinGroupResponse = cluster.send_to_broker(coord_addr, &request).await?;
if response.error_code == 0 {
let generation_id = response.generation_id;
let is_leader = response.leader == response.member_id;
let protocol_name = response.protocol_name.clone();
let new_member_id = response.member_id.clone();
let assignment_bytes = if is_leader {
Self::compute_assignment(
topics,
&response,
cluster,
config.partition_assignment_strategy,
)
.await?
} else {
Bytes::new()
};
let sync_request = SyncGroupRequest {
group_id: group_id.to_string(),
generation_id,
member_id: new_member_id.clone(),
group_instance_id: None,
protocol_type: Some("consumer".to_string()),
protocol_name: protocol_name.clone(),
assignments: if is_leader {
response
.members
.iter()
.map(|m| SyncGroupRequestAssignment {
member_id: m.member_id.clone(),
assignment: if m.member_id == response.leader {
assignment_bytes.clone()
} else {
Bytes::new()
},
})
.collect()
} else {
vec![]
},
};
let sync_response: SyncGroupResponse =
cluster.send_to_broker(coord_addr, &sync_request).await?;
if sync_response.error_code != 0 {
return Err(KafkaError::Protocol(format!(
"SyncGroup error: {}",
sync_response.error_code
)));
}
let assignment = {
let mut buf_data = sync_response.assignment;
let assignment_result = ConsumerProtocolAssignment::decode(&mut buf_data, 0)
.map_err(|e| KafkaError::Protocol(e.to_string()))?;
let mut result = HashMap::new();
for tp in assignment_result.assigned_partitions {
result.insert(tp.topic, tp.partitions);
}
result
};
{
let mut gs = group_state.write().await;
gs.member_id = new_member_id;
gs.generation_id = generation_id;
gs.leader = response.leader;
gs.protocol_name = protocol_name;
gs.assigned_partitions = assignment.clone();
}
Self::init_offsets_for_group(
cluster,
group_id,
offsets,
coordinator,
&assignment,
config.auto_offset_reset,
)
.await?;
debug!(
"background_join_group: joined group, assigned partitions={:?}",
assignment
);
return Ok(());
}
if response.error_code == 79 {
if response.member_id.is_empty() || response.member_id == member_id {
return Err(KafkaError::Protocol(
"MEMBER_ID_REQUIRED with no new member_id".to_string(),
));
}
warn!(
"MEMBER_ID_REQUIRED, retrying with member_id={}",
response.member_id
);
member_id = response.member_id.clone();
continue;
}
return Err(KafkaError::Protocol(format!(
"JoinGroup error: {}",
response.error_code
)));
}
Err(KafkaError::Protocol(
"JoinGroup retry exhausted".to_string(),
))
}
async fn fetch_committed_offsets(
cluster: &Arc<ClusterClient>,
group_id: &str,
coordinator: &Arc<RwLock<Option<std::net::SocketAddr>>>,
assignment: &HashMap<String, Vec<i32>>,
) -> Result<HashMap<String, HashMap<i32, i64>>> {
let coord_addr = {
let c = coordinator.read().await;
match *c {
Some(addr) => addr,
None => return Err(KafkaError::NoCoordinator),
}
};
let mut topic_ids = Vec::with_capacity(assignment.len());
for topic in assignment.keys() {
let tid = cluster
.metadata()
.get_topic(topic)
.await
.map(|t| t.topic_id)
.unwrap_or_else(uuid::Uuid::nil);
topic_ids.push(tid);
}
let topics: Vec<OffsetFetchRequestTopics> = assignment
.iter()
.zip(topic_ids)
.map(|((topic, partitions), topic_id)| OffsetFetchRequestTopics {
name: topic.clone(),
topic_id,
partition_indexes: partitions.clone(),
})
.collect();
let legacy_topics: Vec<OffsetFetchRequestTopic> = assignment
.iter()
.map(|(topic, partitions)| OffsetFetchRequestTopic {
name: topic.clone(),
partition_indexes: partitions.clone(),
})
.collect();
let request = OffsetFetchRequest {
group_id: String::new(),
topics: if !legacy_topics.is_empty() {
Some(legacy_topics)
} else {
None
},
groups: vec![OffsetFetchRequestGroup {
group_id: group_id.to_string(),
member_id: None,
member_epoch: -1,
topics: Some(topics),
}],
require_stable: false,
};
let response: crate::protocol::OffsetFetchResponse =
cluster.send_to_broker(coord_addr, &request).await?;
let mut result: HashMap<String, HashMap<i32, i64>> = HashMap::new();
for group in response.groups {
if group.group_id == group_id {
for topic in group.topics {
let topic_name = if !topic.name.is_empty() {
topic.name.clone()
} else if !topic.topic_id.is_nil() {
cluster
.metadata()
.get_topic_name_by_id(topic.topic_id)
.await
.unwrap_or_else(|| format!("unknown-{}", topic.topic_id))
} else {
continue;
};
let entry = result.entry(topic_name).or_default();
for p in topic.partitions {
if p.error_code == 0 && p.committed_offset >= 0 {
entry.insert(p.partition_index, p.committed_offset);
}
}
}
}
}
Ok(result)
}
async fn init_offsets_for_group(
cluster: &Arc<ClusterClient>,
group_id: &str,
offsets: &Arc<RwLock<HashMap<String, HashMap<i32, i64>>>>,
coordinator: &Arc<RwLock<Option<std::net::SocketAddr>>>,
assignment: &HashMap<String, Vec<i32>>,
auto_offset_reset: AutoOffsetReset,
) -> Result<()> {
let committed = Self::fetch_committed_offsets(cluster, group_id, coordinator, assignment)
.await
.unwrap_or_default();
let default_offset = match auto_offset_reset {
AutoOffsetReset::Earliest => -2i64,
AutoOffsetReset::Latest => -1i64,
AutoOffsetReset::None => return Err(KafkaError::NoOffsetStored),
};
let mut offsets_writer = offsets.write().await;
for (topic, partitions) in assignment {
let topic_offsets = offsets_writer
.entry(topic.clone())
.or_insert_with(HashMap::new);
let committed_topic = committed.get(topic);
for partition in partitions {
if topic_offsets.contains_key(partition) {
continue;
}
if let Some(offset) = committed_topic.and_then(|p| p.get(partition)) {
topic_offsets.insert(*partition, *offset);
} else {
topic_offsets.insert(*partition, default_offset);
}
}
}
Ok(())
}
pub async fn heartbeat(&self) -> Result<()> {
let group_state = self.group_state.read().await;
if group_state.member_id.is_empty() {
return Ok(());
}
let coordinator = self.find_coordinator().await?;
let request = HeartbeatRequest {
group_id: self.config.group_id.clone(),
generation_id: group_state.generation_id,
member_id: group_state.member_id.clone(),
group_instance_id: None,
};
let response: crate::protocol::HeartbeatResponse =
self.cluster.send_to_broker(coordinator, &request).await?;
if response.error_code != 0 {
if response.error_code == 27 {
warn!("Heartbeat indicates rebalance needed, rejoining group");
*self.coordinator.write().await = None;
return Err(KafkaError::Protocol("Rebalance required".to_string()));
}
return Err(KafkaError::Protocol(format!(
"Heartbeat failed: error {}",
response.error_code
)));
}
Ok(())
}
pub async fn poll(&mut self, timeout_ms: i32) -> Result<Vec<ConsumerRecord>> {
if !self.config.group_id.is_empty() {
let needs_join = {
let gs = self.group_state.read().await;
gs.assigned_partitions.is_empty()
};
if needs_join {
return Ok(Vec::new());
}
}
let fetch_targets: Vec<(String, i32, i64)> = {
let group_state = self.group_state.read().await;
let offsets = self.offsets.read().await;
let assigned = if !group_state.assigned_partitions.is_empty() {
group_state.assigned_partitions.clone()
} else {
let mut all: HashMap<String, Vec<i32>> = HashMap::new();
for topic in &self.subscribed_topics {
if let Some(partitions) = self.cluster.metadata().get_partitions(topic).await {
all.insert(topic.clone(), partitions);
}
}
all
};
let mut targets = Vec::new();
for (topic, partitions) in assigned {
for partition in partitions {
let offset = offsets
.get(&topic)
.and_then(|t| t.get(&partition))
.copied()
.unwrap_or(-1);
if offset >= 0 {
targets.push((topic.clone(), partition, offset));
}
}
}
targets
};
if fetch_targets.is_empty() {
self.lazy_init_offsets().await;
let targets = self.collect_fetch_targets().await;
if targets.is_empty() {
return Ok(Vec::new());
}
return self.execute_fetch(targets, timeout_ms).await;
}
self.execute_fetch(fetch_targets, timeout_ms).await
}
async fn lazy_init_offsets(&self) {
let needs_init: Vec<(String, i32)> = {
let offsets = self.offsets.read().await;
let group_state = self.group_state.read().await;
let assigned = if !group_state.assigned_partitions.is_empty() {
group_state.assigned_partitions.clone()
} else {
HashMap::new()
};
let mut result = Vec::new();
for (topic, partitions) in assigned {
for partition in partitions {
let offset = offsets
.get(&topic)
.and_then(|t| t.get(&partition))
.copied()
.unwrap_or(-1);
if offset < 0 {
result.push((topic.clone(), partition));
}
}
}
result
};
for (topic, partition) in needs_init {
let timestamp = match self.config.auto_offset_reset {
AutoOffsetReset::Latest => -1i64,
AutoOffsetReset::Earliest => -2i64,
AutoOffsetReset::None => continue,
};
match self.list_offset(&topic, partition, timestamp).await {
Ok(real_offset) => {
let mut offsets = self.offsets.write().await;
offsets
.entry(topic.clone())
.or_insert_with(HashMap::new)
.insert(partition, real_offset);
}
Err(e) => {
warn!(
"Failed to resolve offset for {}/{}: {}",
topic, partition, e
);
}
}
}
}
async fn collect_fetch_targets(&self) -> Vec<(String, i32, i64)> {
let group_state = self.group_state.read().await;
let offsets = self.offsets.read().await;
let assigned = if !group_state.assigned_partitions.is_empty() {
group_state.assigned_partitions.clone()
} else {
return Vec::new();
};
let mut targets = Vec::new();
for (topic, partitions) in assigned {
for partition in partitions {
let offset = offsets
.get(&topic)
.and_then(|t| t.get(&partition))
.copied()
.unwrap_or(-1);
if offset >= 0 {
targets.push((topic.clone(), partition, offset));
}
}
}
targets
}
async fn execute_fetch(
&self,
fetch_targets: Vec<(String, i32, i64)>,
timeout_ms: i32,
) -> Result<Vec<ConsumerRecord>> {
let mut all_records = Vec::new();
let fetch_params = FetchParams {
timeout_ms,
min_bytes: self.config.min_bytes,
max_bytes: self.config.max_bytes,
partition_max_bytes: self.config.partition_max_bytes,
};
let futures: Vec<_> =
fetch_targets
.into_iter()
.map(|(topic, partition, offset)| {
let cluster = self.cluster.clone();
let params = fetch_params.clone();
async move {
Self::fetch_partition(&cluster, &topic, partition, offset, ¶ms).await
}
})
.collect();
let results = futures::future::join_all(futures).await;
{
let mut offsets = self.offsets.write().await;
for result in results {
match result {
Ok(records) => {
if let Some(last) = records.last() {
let topic_offsets = offsets
.entry(last.topic.clone())
.or_insert_with(HashMap::new);
topic_offsets.insert(last.partition, last.offset + 1);
}
all_records.extend(records);
}
Err(e) => {
warn!("Failed to fetch partition: {}", e);
return Err(e);
}
}
}
}
Ok(all_records)
}
async fn fetch_partition(
cluster: &Arc<ClusterClient>,
topic: &str,
partition: i32,
offset: i64,
params: &FetchParams,
) -> Result<Vec<ConsumerRecord>> {
if cluster.metadata().is_expired().await {
cluster.refresh_metadata().await?;
}
let leader_addr = cluster
.metadata()
.get_partition_leader(topic, partition)
.await
.ok_or_else(|| KafkaError::PartitionNotFound(topic.to_string(), partition))?;
let topic_id = cluster
.metadata()
.get_topic(topic)
.await
.map(|t| t.topic_id)
.unwrap_or_else(uuid::Uuid::nil);
let request = FetchRequest {
cluster_id: None,
replica_id: -1,
replica_state: Default::default(),
max_wait_ms: params.timeout_ms,
min_bytes: params.min_bytes,
max_bytes: params.max_bytes,
isolation_level: 0,
session_id: 0,
session_epoch: -1,
topics: vec![FetchTopic {
topic: topic.to_string(),
topic_id,
partitions: vec![FetchPartition {
partition,
current_leader_epoch: -1,
fetch_offset: offset,
last_fetched_epoch: -1,
log_start_offset: -1,
partition_max_bytes: params.partition_max_bytes,
replica_directory_id: uuid::Uuid::nil(),
high_watermark: 0, }],
}],
forgotten_topics_data: vec![],
rack_id: String::new(),
};
let response: FetchResponse = cluster.send_to_broker(leader_addr, &request).await?;
Self::parse_fetch_response(response, topic, topic_id, partition)
}
fn parse_fetch_response(
response: FetchResponse,
topic_name: &str,
topic_id: uuid::Uuid,
partition_index: i32,
) -> Result<Vec<ConsumerRecord>> {
let mut records = Vec::new();
for topic_response in response.responses {
let name_matches =
!topic_response.topic.is_empty() && topic_response.topic == topic_name;
let id_matches =
!topic_response.topic_id.is_nil() && topic_response.topic_id == topic_id;
if !name_matches && !id_matches {
continue;
}
for partition_response in topic_response.partitions {
if partition_response.partition_index != partition_index {
continue;
}
if partition_response.error_code != 0 {
if partition_response.error_code == 27 {
return Err(KafkaError::ProduceError(partition_response.error_code));
}
warn!(
"Fetch error for partition {}: error_code={}",
partition_response.partition_index, partition_response.error_code
);
continue;
}
let Some(batch) = partition_response.records else {
continue;
};
let base_offset = batch.base_offset;
let first_timestamp = batch.first_timestamp;
for (idx, record) in batch.records.into_iter().enumerate() {
let offset = base_offset + idx as i64;
let timestamp = first_timestamp + record.timestamp_delta;
let headers = record
.headers
.into_iter()
.map(|h| Header {
key: h.key,
value: h.value.unwrap_or_default(),
})
.collect();
records.push(ConsumerRecord {
topic: topic_name.to_string(),
partition: partition_index,
offset,
timestamp,
key: record.key,
value: record.value.unwrap_or_default(),
headers,
});
}
}
}
Ok(records)
}
pub async fn commit(&self) -> Result<()> {
let (generation_id, member_id) = {
let gs = self.group_state.read().await;
(
gs.generation_id,
if gs.member_id.is_empty() {
None
} else {
Some(gs.member_id.clone())
},
)
};
Self::do_commit(
&self.cluster,
&self.config.group_id,
&self.offsets,
&self.coordinator,
generation_id,
member_id,
)
.await
}
async fn do_commit(
cluster: &Arc<ClusterClient>,
group_id: &str,
offsets: &Arc<RwLock<HashMap<String, HashMap<i32, i64>>>>,
coordinator: &Arc<RwLock<Option<std::net::SocketAddr>>>,
generation_id: i32,
member_id: Option<String>,
) -> Result<()> {
if group_id.is_empty() {
return Ok(());
}
let topic_partitions: HashMap<String, Vec<(i32, i64)>> = {
let offsets_guard = offsets.read().await;
offsets_guard
.iter()
.map(|(topic, partitions)| {
let entries: Vec<(i32, i64)> =
partitions.iter().map(|(p, o)| (*p, *o)).collect();
(topic.clone(), entries)
})
.collect()
};
if topic_partitions.is_empty() {
return Ok(());
}
let coordinator_addr = {
let mut coord = coordinator.write().await;
if let Some(addr) = *coord {
addr
} else {
let request = FindCoordinatorRequest {
key: group_id.to_string(),
key_type: 0,
coordinator_keys: vec![group_id.to_string()],
};
let response: FindCoordinatorResponse =
cluster.send_to_any_broker(&request).await?;
if response.error_code != 0 {
return Err(KafkaError::NoCoordinator);
}
if let Some(coord) = response.coordinators.first() {
if coord.error_code != 0 {
return Err(KafkaError::NoCoordinator);
}
}
let (host, port) = if !response.host.is_empty() {
(response.host.clone(), response.port)
} else if let Some(coord) = response.coordinators.first() {
(coord.host.clone(), coord.port)
} else {
return Err(KafkaError::NoCoordinator);
};
let addr: std::net::SocketAddr = format!("{}:{}", host, port)
.to_socket_addrs()
.map_err(|_| KafkaError::NoCoordinator)?
.next()
.ok_or(KafkaError::NoCoordinator)?;
*coord = Some(addr);
addr
}
};
let topic_ids: Vec<uuid::Uuid> = {
let mut ids = Vec::with_capacity(topic_partitions.len());
for topic in topic_partitions.keys() {
let tid = cluster
.metadata()
.get_topic(topic)
.await
.map(|t| t.topic_id)
.unwrap_or_else(uuid::Uuid::nil);
ids.push(tid);
}
ids
};
let topics: Vec<OffsetCommitRequestTopic> = topic_partitions
.into_iter()
.zip(topic_ids)
.map(|((topic, partitions), topic_id)| OffsetCommitRequestTopic {
name: topic,
topic_id,
partitions: partitions
.into_iter()
.map(
|(partition_index, committed_offset)| OffsetCommitRequestPartition {
partition_index,
committed_offset,
committed_leader_epoch: -1,
committed_metadata: None,
},
)
.collect(),
})
.collect();
let request = OffsetCommitRequest {
group_id: group_id.to_string(),
generation_id_or_member_epoch: generation_id,
member_id: member_id.unwrap_or_default(),
group_instance_id: None,
retention_time_ms: -1,
topics,
};
let response: Result<crate::protocol::OffsetCommitResponse> =
cluster.send_to_broker(coordinator_addr, &request).await;
match response {
Ok(resp) => {
for topic_response in resp.topics {
for partition_response in topic_response.partitions {
if partition_response.error_code != 0 {
return Err(KafkaError::OffsetCommitError(
partition_response.error_code,
));
}
}
}
Ok(())
}
Err(KafkaError::ConnectionClosed | KafkaError::Io(_)) => {
warn!(
"Connection to coordinator {} closed, clearing cache",
coordinator_addr
);
*coordinator.write().await = None;
Err(KafkaError::ConnectionClosed)
}
Err(e) => Err(e),
}
}
async fn find_coordinator(&self) -> Result<std::net::SocketAddr> {
let mut coord = self.coordinator.write().await;
if let Some(addr) = *coord {
return Ok(addr);
}
let request = FindCoordinatorRequest {
key: self.config.group_id.clone(),
key_type: 0,
coordinator_keys: vec![self.config.group_id.clone()],
};
let response: FindCoordinatorResponse = self.cluster.send_to_any_broker(&request).await?;
if response.error_code != 0 {
return Err(KafkaError::NoCoordinator);
}
if let Some(coord) = response.coordinators.first() {
if coord.error_code != 0 {
return Err(KafkaError::NoCoordinator);
}
}
let (host, port) = if !response.host.is_empty() {
(response.host.clone(), response.port)
} else if let Some(coord) = response.coordinators.first() {
(coord.host.clone(), coord.port)
} else {
return Err(KafkaError::NoCoordinator);
};
let addr: std::net::SocketAddr = format!("{}:{}", host, port)
.to_socket_addrs()
.map_err(|_| KafkaError::NoCoordinator)?
.next()
.ok_or(KafkaError::NoCoordinator)?;
*coord = Some(addr);
Ok(addr)
}
pub async fn leave_group(&self) -> Result<()> {
let member_id = {
let gs = self.group_state.read().await;
gs.member_id.clone()
};
if member_id.is_empty() {
return Ok(());
}
if self.config.auto_commit {
if let Err(e) = self.commit().await {
warn!("Commit before leave failed: {}", e);
}
}
let coordinator = self.find_coordinator().await?;
use crate::protocol::leave_group_request::MemberIdentity;
let request = LeaveGroupRequest {
group_id: self.config.group_id.clone(),
member_id: member_id.clone(),
members: vec![MemberIdentity {
member_id,
group_instance_id: None,
reason: None,
}],
};
let response: crate::protocol::LeaveGroupResponse =
self.cluster.send_to_broker(coordinator, &request).await?;
if response.error_code != 0 {
warn!("LeaveGroup failed: error {}", response.error_code);
}
Ok(())
}
pub fn get_offset(&self, topic: &str, partition: i32) -> Option<i64> {
let offsets = self.offsets.blocking_read();
offsets.get(topic)?.get(&partition).copied()
}
pub async fn set_offset(&self, topic: &str, partition: i32, offset: i64) {
let mut offsets = self.offsets.write().await;
offsets
.entry(topic.to_string())
.or_insert_with(HashMap::new)
.insert(partition, offset);
}
pub async fn assignment(&self) -> HashMap<String, Vec<i32>> {
self.group_state.read().await.assigned_partitions.clone()
}
pub async fn close(&self) -> Result<()> {
self.running
.store(false, std::sync::atomic::Ordering::Relaxed);
if !self.config.group_id.is_empty() {
if let Err(e) = self.leave_group().await {
warn!("Error leaving group: {}", e);
}
}
Ok(())
}
}
impl Drop for Consumer {
fn drop(&mut self) {
self.running
.store(false, std::sync::atomic::Ordering::Relaxed);
}
}