use std::collections::BTreeMap;
use std::time::Duration;
use barnabas_core::group::{Assignor, Subscription, TopicPartition};
use barnabas_core::member::{codes, GroupMember, Step};
use bytes::Bytes;
use kafka_protocol::messages::{
consumer_protocol_assignment::{
ConsumerProtocolAssignment, TopicPartition as AssignmentTopicPartition,
},
consumer_protocol_subscription::{
ConsumerProtocolSubscription, TopicPartition as SubscriptionTopicPartition,
},
join_group_request::{JoinGroupRequest, JoinGroupRequestProtocol},
offset_commit_request::{OffsetCommitRequestPartition, OffsetCommitRequestTopic},
offset_fetch_request::OffsetFetchRequestTopic,
sync_group_request::{SyncGroupRequest, SyncGroupRequestAssignment},
ApiKey, FindCoordinatorRequest, FindCoordinatorResponse, GroupId, HeartbeatRequest,
HeartbeatResponse, JoinGroupResponse, LeaveGroupRequest, LeaveGroupResponse,
OffsetCommitRequest, OffsetCommitResponse, OffsetFetchRequest, OffsetFetchResponse,
SyncGroupResponse, TopicName,
};
use kafka_protocol::protocol::{Decodable, Encodable, StrBytes};
use crate::cluster::Cluster;
use crate::{Error, Result, Transport};
const CONSUMER_PROTOCOL: &str = "consumer";
const COORDINATOR_RETRIES: usize = 40;
const COORDINATOR_BACKOFF: Duration = Duration::from_millis(250);
const JOIN_SLACK: Duration = Duration::from_secs(5);
const COORDINATOR_TIMEOUT: Duration = Duration::from_secs(30);
const PROTOCOL_VERSION: i16 = 2;
fn read_version(cursor: &mut Bytes) -> Result<i16> {
use bytes::Buf;
if cursor.remaining() < 2 {
return Err(Error::Core(barnabas_core::Error::Codec(
"consumer protocol blob is too short for its version".to_owned(),
)));
}
Ok(cursor.get_i16())
}
#[derive(Debug, Clone)]
pub struct GroupMetadata {
pub(crate) group_id: String,
pub(crate) generation_id: i32,
pub(crate) member_id: String,
pub(crate) group_instance_id: Option<String>,
}
impl GroupMetadata {
#[must_use]
pub fn group_id(&self) -> &str {
&self.group_id
}
}
pub trait GroupProtocol<T: Transport> {
fn advance(
&mut self,
cluster: &mut Cluster<T>,
) -> impl std::future::Future<Output = Result<Membership>>;
fn leave(&mut self, cluster: &mut Cluster<T>) -> impl std::future::Future<Output = Result<()>>;
fn commit(
&mut self,
cluster: &mut Cluster<T>,
offsets: &BTreeMap<TopicPartition, i64>,
) -> impl std::future::Future<Output = Result<()>>;
fn topics(&self) -> Vec<String>;
fn request_rejoin(&mut self);
fn group_metadata(&self) -> Option<GroupMetadata>;
fn committed(
&mut self,
cluster: &mut Cluster<T>,
partitions: &[TopicPartition],
) -> impl std::future::Future<Output = Result<BTreeMap<TopicPartition, i64>>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Membership {
InProgress,
Assigned(Vec<TopicPartition>),
Revoked(Vec<TopicPartition>),
}
pub struct ClassicProtocol {
member: GroupMember,
assignor: Box<dyn Assignor>,
coordinator: Option<String>,
session_timeout_ms: i32,
rebalance_timeout_ms: i32,
announced_revoked: bool,
ever_assigned: bool,
}
impl ClassicProtocol {
#[must_use]
pub fn new(
group_id: impl Into<String>,
topics: Vec<String>,
assignor: Box<dyn Assignor>,
) -> Self {
let protocol = if assignor.name() == "cooperative-sticky" {
barnabas_core::member::RebalanceProtocol::Cooperative
} else {
barnabas_core::member::RebalanceProtocol::Eager
};
Self {
member: GroupMember::new(group_id, topics).with_protocol(protocol),
assignor,
coordinator: None,
session_timeout_ms: 45_000,
rebalance_timeout_ms: 300_000,
announced_revoked: false,
ever_assigned: false,
}
}
#[must_use]
pub fn member(&self) -> &GroupMember {
&self.member
}
pub fn set_session_timeout(&mut self, ms: i32) {
self.session_timeout_ms = ms;
}
pub fn set_rebalance_timeout(&mut self, ms: i32) {
self.rebalance_timeout_ms = ms;
}
async fn coordinator_addr<T: Transport>(&mut self, cluster: &mut Cluster<T>) -> Result<String> {
if let Some(addr) = &self.coordinator {
return Ok(addr.clone());
}
let mut req = FindCoordinatorRequest::default();
req.key = StrBytes::from_string(self.member.group_id().to_owned());
req.key_type = 0;
for attempt in 0..COORDINATOR_RETRIES {
let resp: FindCoordinatorResponse =
cluster.call_any(ApiKey::FindCoordinator, 3, &req).await?;
match resp.error_code {
codes::NONE => {
let addr = format!("{}:{}", resp.host.as_str(), resp.port);
self.coordinator = Some(addr.clone());
return Ok(addr);
}
codes::COORDINATOR_NOT_AVAILABLE | codes::COORDINATOR_LOAD_IN_PROGRESS => {
let _ = attempt;
T::sleep(COORDINATOR_BACKOFF).await;
}
code => crate::check("FindCoordinator", code)?,
}
}
Err(Error::Broker {
op: "FindCoordinator",
code: codes::COORDINATOR_NOT_AVAILABLE,
disposition: barnabas_core::Disposition::Retry,
})
}
async fn join<T: Transport>(&mut self, cluster: &mut Cluster<T>) -> Result<Step> {
let addr = self.coordinator_addr(cluster).await?;
let mut protocol = JoinGroupRequestProtocol::default();
protocol.name = StrBytes::from_string(self.assignor.name().to_owned());
protocol.metadata = encode_subscription(&self.member.subscription())?;
let mut req = JoinGroupRequest::default();
req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
req.session_timeout_ms = self.session_timeout_ms;
req.rebalance_timeout_ms = self.rebalance_timeout_ms;
req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
req.protocol_type = StrBytes::from_static_str(CONSUMER_PROTOCOL);
req.protocols = vec![protocol];
let deadline =
Duration::from_millis(u64::try_from(self.rebalance_timeout_ms).unwrap_or(300_000))
+ JOIN_SLACK;
let resp: JoinGroupResponse = cluster
.call_coordinator(&addr, ApiKey::JoinGroup, 7, &req, deadline)
.await?;
let members = if resp.leader == resp.member_id {
resp.members
.iter()
.map(|m| decode_subscription(m.member_id.as_str(), &m.metadata))
.collect::<Result<Vec<_>>>()?
} else {
Vec::new()
};
if std::env::var("BARNABAS_TRACE").is_ok() {
eprintln!(
"[{}] JOIN<- err={} gen={} id={} leader={}",
self.member.member_id(),
resp.error_code,
resp.generation_id,
resp.member_id.as_str(),
resp.leader.as_str()
);
}
Ok(self.member.on_join(
resp.error_code,
resp.generation_id,
resp.member_id.as_str(),
resp.leader.as_str(),
members,
))
}
async fn sync<T: Transport>(
&mut self,
cluster: &mut Cluster<T>,
assignments: Vec<SyncGroupRequestAssignment>,
) -> Result<Step> {
let addr = self.coordinator_addr(cluster).await?;
if std::env::var("BARNABAS_TRACE").is_ok() {
let sub = self.member.subscription();
eprintln!(
"[{}] SYNC-> gen={} owned={:?} sending={:?}",
self.member.member_id(),
self.member.generation(),
sub.owned.iter().map(|t| t.partition).collect::<Vec<_>>(),
assignments
.iter()
.map(|a| (a.member_id.to_string(), a.assignment.len()))
.collect::<Vec<_>>()
);
}
let mut req = SyncGroupRequest::default();
req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
req.generation_id = self.member.generation();
req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
req.protocol_type = Some(StrBytes::from_static_str(CONSUMER_PROTOCOL));
req.protocol_name = Some(StrBytes::from_string(self.assignor.name().to_owned()));
req.assignments = assignments;
let resp: SyncGroupResponse = cluster
.call_coordinator(&addr, ApiKey::SyncGroup, 4, &req, COORDINATOR_TIMEOUT)
.await?;
let assigned = if resp.error_code == codes::NONE && !resp.assignment.is_empty() {
decode_assignment(&resp.assignment)?
} else {
Vec::new()
};
if std::env::var("BARNABAS_TRACE").is_ok() {
eprintln!(
"[{}] SYNC<- err={} assigned={:?}",
self.member.member_id(),
resp.error_code,
assigned.iter().map(|t| t.partition).collect::<Vec<_>>()
);
}
let step = self.member.on_sync(resp.error_code, assigned);
if std::env::var("BARNABAS_TRACE").is_ok() {
eprintln!(
"[{}] after: gen={} assignment={:?} lost={:?} step={:?}",
self.member.member_id(),
self.member.generation(),
self.member
.assignment()
.iter()
.map(|t| t.partition)
.collect::<Vec<_>>(),
self.member
.lost()
.iter()
.map(|t| t.partition)
.collect::<Vec<_>>(),
step
);
}
Ok(step)
}
async fn heartbeat<T: Transport>(&mut self, cluster: &mut Cluster<T>) -> Result<Step> {
let addr = self.coordinator_addr(cluster).await?;
let mut req = HeartbeatRequest::default();
req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
req.generation_id = self.member.generation();
req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
let resp: HeartbeatResponse = cluster
.call_coordinator(&addr, ApiKey::Heartbeat, 4, &req, COORDINATOR_TIMEOUT)
.await?;
if std::env::var("BARNABAS_TRACE").is_ok() {
eprintln!(
"[{}] HB<- err={} gen={}",
self.member.member_id(),
resp.error_code,
self.member.generation()
);
}
Ok(self.member.on_heartbeat(resp.error_code))
}
async fn assign_and_sync<T: Transport>(
&mut self,
cluster: &mut Cluster<T>,
members: Vec<Subscription>,
) -> Result<Step> {
let mut partitions_per_topic: BTreeMap<String, i32> = BTreeMap::new();
for topic in members.iter().flat_map(|m| m.topics.iter()) {
if !partitions_per_topic.contains_key(topic) {
let count = cluster.partition_count(topic).await?;
partitions_per_topic.insert(topic.clone(), count);
}
}
if std::env::var("BARNABAS_TRACE").is_ok() {
eprintln!(
"[{}] LEADER sees: {:?}",
self.member.member_id(),
members
.iter()
.map(|m| (
m.member_id.clone(),
m.generation,
m.owned.iter().map(|t| t.partition).collect::<Vec<_>>()
))
.collect::<Vec<_>>()
);
}
let assignment = self.assignor.assign(&members, &partitions_per_topic);
if std::env::var("BARNABAS_TRACE").is_ok() {
eprintln!(
"[{}] LEADER assigns: {:?}",
self.member.member_id(),
assignment
.iter()
.map(|(m, p)| (m.clone(), p.iter().map(|t| t.partition).collect::<Vec<_>>()))
.collect::<Vec<_>>()
);
}
let encoded = assignment
.iter()
.map(|(member_id, partitions)| {
let mut entry = SyncGroupRequestAssignment::default();
entry.member_id = StrBytes::from_string(member_id.clone());
entry.assignment = encode_assignment(partitions)?;
Ok(entry)
})
.collect::<Result<Vec<_>>>()?;
self.sync(cluster, encoded).await
}
}
impl<T: Transport> GroupProtocol<T> for ClassicProtocol {
async fn advance(&mut self, cluster: &mut Cluster<T>) -> Result<Membership> {
let step = self.member.step();
let outcome = match step {
Step::Join { .. } => self.join(cluster).await?,
Step::AssignAndSync { members } => self.assign_and_sync(cluster, members).await?,
Step::Sync => self.sync(cluster, Vec::new()).await?,
Step::Heartbeat => self.heartbeat(cluster).await?,
Step::FindCoordinator => {
self.coordinator = None;
Step::Join {
member_id: self.member.member_id().to_owned(),
}
}
};
if outcome == Step::FindCoordinator {
self.coordinator = None;
}
if self.member.state() == barnabas_core::member::MemberState::Stable {
self.announced_revoked = false;
self.ever_assigned = true;
return Ok(Membership::Assigned(self.member.assignment().to_vec()));
}
if self.ever_assigned && !self.announced_revoked {
self.announced_revoked = true;
return Ok(Membership::Revoked(self.member.lost().to_vec()));
}
Ok(Membership::InProgress)
}
async fn commit(
&mut self,
cluster: &mut Cluster<T>,
offsets: &BTreeMap<TopicPartition, i64>,
) -> Result<()> {
self.commit_offsets(cluster, offsets).await
}
fn topics(&self) -> Vec<String> {
self.member.topics().to_vec()
}
fn request_rejoin(&mut self) {
self.member.request_rejoin();
}
fn group_metadata(&self) -> Option<GroupMetadata> {
if !self.member.can_commit() {
return None;
}
Some(GroupMetadata {
group_id: self.member.group_id().to_owned(),
generation_id: self.member.generation(),
member_id: self.member.member_id().to_owned(),
group_instance_id: None,
})
}
async fn committed(
&mut self,
cluster: &mut Cluster<T>,
partitions: &[TopicPartition],
) -> Result<BTreeMap<TopicPartition, i64>> {
self.fetch_offsets(cluster, partitions).await
}
async fn leave(&mut self, cluster: &mut Cluster<T>) -> Result<()> {
if self.member.member_id().is_empty() {
return Ok(());
}
let addr = self.coordinator_addr(cluster).await?;
let mut req = LeaveGroupRequest::default();
req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
let _: std::result::Result<LeaveGroupResponse, Error> = cluster
.call_coordinator(&addr, ApiKey::LeaveGroup, 3, &req, COORDINATOR_TIMEOUT)
.await;
self.member.on_leave();
Ok(())
}
}
impl ClassicProtocol {
async fn commit_offsets<T: Transport>(
&mut self,
cluster: &mut Cluster<T>,
offsets: &BTreeMap<TopicPartition, i64>,
) -> Result<()> {
if offsets.is_empty() {
return Ok(());
}
if !self.member.can_commit() {
return Err(Error::Broker {
op: "OffsetCommit",
code: codes::REBALANCE_IN_PROGRESS,
disposition: barnabas_core::Disposition::Retry,
});
}
let addr = self.coordinator_addr(cluster).await?;
let mut by_topic: BTreeMap<String, Vec<OffsetCommitRequestPartition>> = BTreeMap::new();
for (tp, offset) in offsets {
let mut partition = OffsetCommitRequestPartition::default();
partition.partition_index = tp.partition;
partition.committed_offset = *offset;
partition.committed_leader_epoch = -1;
by_topic
.entry(tp.topic.clone())
.or_default()
.push(partition);
}
let mut req = OffsetCommitRequest::default();
req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
req.generation_id_or_member_epoch = self.member.generation();
req.member_id = StrBytes::from_string(self.member.member_id().to_owned());
req.topics = by_topic
.into_iter()
.map(|(name, partitions)| {
let mut topic = OffsetCommitRequestTopic::default();
topic.name = TopicName(StrBytes::from_string(name));
topic.partitions = partitions;
topic
})
.collect();
let resp: OffsetCommitResponse = cluster
.call_coordinator(&addr, ApiKey::OffsetCommit, 8, &req, COORDINATOR_TIMEOUT)
.await?;
for topic in &resp.topics {
for partition in &topic.partitions {
crate::check("OffsetCommit", partition.error_code)?;
}
}
Ok(())
}
async fn fetch_offsets<T: Transport>(
&mut self,
cluster: &mut Cluster<T>,
partitions: &[TopicPartition],
) -> Result<BTreeMap<TopicPartition, i64>> {
if partitions.is_empty() {
return Ok(BTreeMap::new());
}
let addr = self.coordinator_addr(cluster).await?;
let mut by_topic: BTreeMap<String, Vec<i32>> = BTreeMap::new();
for tp in partitions {
by_topic
.entry(tp.topic.clone())
.or_default()
.push(tp.partition);
}
let mut req = OffsetFetchRequest::default();
req.group_id = GroupId(StrBytes::from_string(self.member.group_id().to_owned()));
req.topics = Some(
by_topic
.into_iter()
.map(|(name, partition_indexes)| {
let mut topic = OffsetFetchRequestTopic::default();
topic.name = TopicName(StrBytes::from_string(name));
topic.partition_indexes = partition_indexes;
topic
})
.collect(),
);
let resp: OffsetFetchResponse = cluster
.call_coordinator(&addr, ApiKey::OffsetFetch, 6, &req, COORDINATOR_TIMEOUT)
.await?;
crate::check("OffsetFetch", resp.error_code)?;
let mut out = BTreeMap::new();
for topic in &resp.topics {
for partition in &topic.partitions {
crate::check("OffsetFetch partition", partition.error_code)?;
if partition.committed_offset >= 0 {
out.insert(
TopicPartition::new(topic.name.0.to_string(), partition.partition_index),
partition.committed_offset,
);
}
}
}
Ok(out)
}
}
fn encode_subscription(subscription: &Subscription) -> Result<Bytes> {
let mut owned: BTreeMap<String, Vec<i32>> = BTreeMap::new();
for tp in &subscription.owned {
owned
.entry(tp.topic.clone())
.or_default()
.push(tp.partition);
}
let mut body = ConsumerProtocolSubscription::default();
body.topics = subscription
.topics
.iter()
.map(|t| StrBytes::from_string(t.clone()))
.collect();
body.generation_id = subscription.generation;
body.owned_partitions = owned
.into_iter()
.map(|(topic, partitions)| {
let mut entry = SubscriptionTopicPartition::default();
entry.topic = TopicName(StrBytes::from_string(topic));
entry.partitions = partitions;
entry
})
.collect();
let mut buf = bytes::BytesMut::new();
buf.extend_from_slice(&PROTOCOL_VERSION.to_be_bytes());
body.encode(&mut buf, PROTOCOL_VERSION)
.map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("subscription: {e}"))))?;
Ok(buf.freeze())
}
fn decode_subscription(member_id: &str, metadata: &Bytes) -> Result<Subscription> {
let mut cursor = metadata.clone();
let version = read_version(&mut cursor)?;
let body = ConsumerProtocolSubscription::decode(&mut cursor, version)
.map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("subscription: {e}"))))?;
Ok(Subscription {
member_id: member_id.to_owned(),
generation: if version >= 2 { body.generation_id } else { -1 },
topics: body.topics.iter().map(|t| t.to_string()).collect(),
owned: body
.owned_partitions
.iter()
.flat_map(|tp| {
let topic = tp.topic.0.to_string();
tp.partitions
.iter()
.map(move |p| TopicPartition::new(topic.clone(), *p))
})
.collect(),
})
}
fn encode_assignment(partitions: &[TopicPartition]) -> Result<Bytes> {
let mut by_topic: BTreeMap<String, Vec<i32>> = BTreeMap::new();
for tp in partitions {
by_topic
.entry(tp.topic.clone())
.or_default()
.push(tp.partition);
}
let mut body = ConsumerProtocolAssignment::default();
body.assigned_partitions = by_topic
.into_iter()
.map(|(topic, partitions)| {
let mut entry = AssignmentTopicPartition::default();
entry.topic = TopicName(StrBytes::from_string(topic));
entry.partitions = partitions;
entry
})
.collect();
let mut buf = bytes::BytesMut::new();
buf.extend_from_slice(&PROTOCOL_VERSION.to_be_bytes());
body.encode(&mut buf, PROTOCOL_VERSION)
.map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("assignment: {e}"))))?;
Ok(buf.freeze())
}
fn decode_assignment(assignment: &Bytes) -> Result<Vec<TopicPartition>> {
let mut cursor = assignment.clone();
let version = read_version(&mut cursor)?;
let body = ConsumerProtocolAssignment::decode(&mut cursor, version)
.map_err(|e| Error::Core(barnabas_core::Error::Codec(format!("assignment: {e}"))))?;
let mut out: Vec<TopicPartition> = body
.assigned_partitions
.iter()
.flat_map(|tp| {
let topic = tp.topic.0.to_string();
tp.partitions
.iter()
.map(move |p| TopicPartition::new(topic.clone(), *p))
})
.collect();
out.sort();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_subscription_round_trips() {
let subscription = Subscription {
member_id: "m-1".to_owned(),
topics: vec!["a".to_owned(), "b".to_owned()],
owned: vec![TopicPartition::new("a", 0), TopicPartition::new("a", 3)],
generation: 7,
};
let encoded = encode_subscription(&subscription).expect("encode");
let decoded = decode_subscription("m-1", &encoded).expect("decode");
assert_eq!(decoded.topics, subscription.topics);
assert_eq!(decoded.owned, subscription.owned);
assert_eq!(
decoded.generation, 7,
"the generation must survive: without it a leader cannot tell a \
live ownership claim from a stale one"
);
}
#[test]
fn an_assignment_round_trips() {
let partitions = vec![
TopicPartition::new("a", 0),
TopicPartition::new("a", 1),
TopicPartition::new("b", 7),
];
let encoded = encode_assignment(&partitions).expect("encode");
assert_eq!(decode_assignment(&encoded).expect("decode"), partitions);
}
#[test]
fn an_empty_assignment_decodes_to_nothing() {
let encoded = encode_assignment(&[]).expect("encode");
assert!(decode_assignment(&encoded).expect("decode").is_empty());
}
}