use std::collections::HashMap;
use std::time::Duration;
use bytes::{BufMut, Bytes, BytesMut};
use crate::error::{ErrorCode, Result};
use crate::protocol::ApiKey;
use crate::protocol::{Decode, Encode, KafkaString, TaggedField, TryEncode};
use super::state::{
BrokerTransaction, ClassicGroupState, ClusterState, CommittedOffset,
ConsumerGroupHeartbeatSeen, GroupMember, LeaveGroupMemberSeen, ListOffsetsLookup,
SequenceCheck, ShareSession, ShareSessionClose, TxnStatus,
};
use super::wire::*;
pub(crate) fn supported_versions() -> Vec<(ApiKey, i16, i16)> {
vec![
(ApiKey::ApiVersions, 0, 0),
(ApiKey::Metadata, 12, 12),
(ApiKey::Produce, 10, 12),
(ApiKey::Fetch, 11, 11),
(ApiKey::ListOffsets, 5, 11),
(ApiKey::FindCoordinator, 2, 2),
(ApiKey::JoinGroup, 5, 5),
(ApiKey::SyncGroup, 3, 3),
(ApiKey::Heartbeat, 3, 3),
(ApiKey::LeaveGroup, 3, 3),
(ApiKey::OffsetCommit, 7, 9),
(ApiKey::OffsetFetch, 5, 5),
(ApiKey::ConsumerGroupHeartbeat, 1, 1),
(ApiKey::InitProducerId, 0, 5),
(ApiKey::AddPartitionsToTxn, 0, 0),
(ApiKey::AddOffsetsToTxn, 0, 0),
(ApiKey::TxnOffsetCommit, 5, 5),
(ApiKey::EndTxn, 3, 5),
(ApiKey::CreateTopics, 4, 4),
(ApiKey::DeleteTopics, 3, 3),
(ApiKey::ShareGroupHeartbeat, 1, 1),
(ApiKey::ShareFetch, 1, 2),
(ApiKey::ShareAcknowledge, 1, 2),
(ApiKey::UpdateFeatures, 2, 2),
(ApiKey::StreamsGroupDescribe, 0, 0),
(ApiKey::DescribeGroups, 4, 4),
]
}
#[derive(Debug, Default)]
pub(crate) struct LongPoll {
pub expired: bool,
pub share_acks: Option<HashMap<([u8; 16], i32), ErrorCode>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Served {
Done,
Wait(Duration),
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn dispatch(
api_key: ApiKey,
api_version: i16,
body: &mut Bytes,
node_id: i32,
client_id: Option<&str>,
state: &mut ClusterState,
poll: &mut LongPoll,
out: &mut BytesMut,
) -> Result<Served> {
match api_key {
ApiKey::Fetch => return fetch_inner(body, node_id, state, poll, out, false),
ApiKey::ShareFetch => return share_fetch(body, api_version, node_id, state, poll, out),
_ => {}
}
match api_key {
ApiKey::ApiVersions => api_versions(api_version, state, out),
ApiKey::Metadata => metadata(body, state, out),
ApiKey::Produce => produce(body, api_version, node_id, state, out),
ApiKey::ListOffsets => list_offsets(body, api_version, node_id, state, out),
ApiKey::FindCoordinator => find_coordinator(body, state, out),
ApiKey::JoinGroup => join_group(body, node_id, client_id, state, out),
ApiKey::SyncGroup => sync_group(body, node_id, state, out),
ApiKey::Heartbeat => heartbeat(body, node_id, state, out),
ApiKey::LeaveGroup => leave_group(body, node_id, state, out),
ApiKey::OffsetCommit => offset_commit(body, api_version, node_id, state, out),
ApiKey::OffsetFetch => offset_fetch(body, node_id, state, out),
ApiKey::ConsumerGroupHeartbeat => consumer_group_heartbeat(body, node_id, state, out),
ApiKey::InitProducerId => init_producer_id(body, api_version, node_id, state, out),
ApiKey::AddPartitionsToTxn => add_partitions_to_txn(body, api_version, node_id, state, out),
ApiKey::AddOffsetsToTxn => add_offsets_to_txn(body, api_version, node_id, state, out),
ApiKey::TxnOffsetCommit => txn_offset_commit(body, node_id, state, out),
ApiKey::EndTxn => end_txn(body, api_version, node_id, state, out),
ApiKey::CreateTopics => create_topics(body, node_id, state, out),
ApiKey::DeleteTopics => delete_topics(body, node_id, state, out),
ApiKey::ShareGroupHeartbeat => share_group_heartbeat(body, node_id, state, out),
ApiKey::ShareAcknowledge => share_acknowledge(body, api_version, node_id, state, out),
ApiKey::UpdateFeatures => update_features(body, api_version, node_id, state, out),
ApiKey::StreamsGroupDescribe => streams_group_describe(body, node_id, state, out),
ApiKey::DescribeGroups => describe_groups(body, node_id, state, out),
ApiKey::GetTelemetrySubscriptions => get_telemetry_subscriptions(body, state, out),
ApiKey::PushTelemetry => push_telemetry(body, state, out),
other => Err(crate::error::KrafkaError::protocol_kind(
crate::error::ProtocolErrorKind::UnknownApiVersion,
format!("fake broker has no handler for {other:?}"),
)),
}?;
Ok(Served::Done)
}
pub(crate) fn sasl_handshake(body: &mut Bytes, out: &mut BytesMut) -> Result<()> {
let mechanism = KafkaString::decode(body)?.0.unwrap_or_default();
if mechanism == "PLAIN" {
write_error(out, ErrorCode::None);
} else {
write_error(out, ErrorCode::UnsupportedSaslMechanism);
}
write_array_len(out, 1)?;
write_string(out, "PLAIN")?;
Ok(())
}
pub(crate) fn sasl_authenticate(
body: &mut Bytes,
credentials: &(String, String),
out: &mut BytesMut,
) -> Result<bool> {
let auth = crate::protocol::KafkaBytes::decode(body)?
.0
.unwrap_or_default();
let expected = format!("\0{}\0{}", credentials.0, credentials.1);
let ok = auth.as_ref() == expected.as_bytes();
if ok {
write_error(out, ErrorCode::None);
(-1i16).encode(out); } else {
write_error(out, ErrorCode::SaslAuthenticationFailed);
write_string(out, "Authentication failed: invalid credentials")?;
}
0i32.encode(out); 0i64.encode(out); Ok(ok)
}
pub(crate) fn dispatch_error(
api_key: ApiKey,
api_version: i16,
body: &mut Bytes,
code: ErrorCode,
out: &mut BytesMut,
) -> Result<()> {
match api_key {
ApiKey::ApiVersions => {
if code == ErrorCode::UnsupportedVersion {
let (min_version, max_version) = API_VERSIONS_RANGE;
write_error(out, code);
write_array_len(out, 1)?;
ApiKey::ApiVersions.to_i16().encode(out);
min_version.encode(out);
max_version.encode(out);
return Ok(());
}
write_error(out, code);
if api_version >= 3 {
write_compact_array_len(out, 0)?;
out.put_i32(0); write_empty_tagged_fields(out)
} else {
write_array_len(out, 0)?;
if api_version >= 1 {
out.put_i32(0); }
Ok(())
}
}
ApiKey::ConsumerGroupHeartbeat => {
let _req = ConsumerGroupHeartbeatReq::read(body)?;
out.put_i32(0); write_error(out, code);
write_compact_nullable_string(out, Some(&format!("injected {code:?}")))?;
write_compact_nullable_string(out, None)?; out.put_i32(0); out.put_i32(HEARTBEAT_INTERVAL_MS);
write_heartbeat_assignment(out, None)?;
write_empty_tagged_fields(out)
}
ApiKey::ShareGroupHeartbeat => {
let req = ShareGroupHeartbeatReq::read(body)?;
write_heartbeat_error(out, code, Some(&req.member_id), 0)
}
ApiKey::Metadata => {
let req = MetadataReq::read_v12(body)?;
out.put_i32(0); write_compact_array_len(out, 0)?; write_compact_nullable_string(out, None)?; out.put_i32(-1); let names = req.topics.unwrap_or_default();
write_compact_array_len(out, names.len())?;
for name in &names {
write_error(out, code);
write_compact_nullable_string(out, Some(name))?;
out.put_slice(&[0u8; 16]); out.put_u8(0); write_compact_array_len(out, 0)?; out.put_i32(i32::MIN); write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)
}
ApiKey::Produce => {
let req = ProduceReq::read(body)?;
write_compact_array_len(out, req.topics.len())?;
for topic in &req.topics {
KafkaString::new(&topic.name).try_encode_compact(out)?;
write_compact_array_len(out, topic.partitions.len())?;
for partition in &topic.partitions {
write_produce_partition(out, partition.index, code, -1, -1, None)?;
}
write_empty_tagged_fields(out)?;
}
out.put_i32(0);
write_empty_tagged_fields(out)
}
ApiKey::Fetch => {
let req = FetchReq::read(body)?;
out.put_i32(0);
write_error(out, ErrorCode::None);
out.put_i32(req.session_id);
write_array_len(out, req.topics.len())?;
for topic in &req.topics {
write_string(out, &topic.topic)?;
write_array_len(out, topic.partitions.len())?;
for partition in &topic.partitions {
write_fetch_partition(out, partition.partition, code, 0, 0, None)?;
}
}
Ok(())
}
ApiKey::ListOffsets => {
let req = ListOffsetsReq::read(body, api_version)?;
let topics: Vec<(String, Vec<ListOffsetsAnswer>)> = req
.topics
.iter()
.map(|t| {
let answers = t
.partitions
.iter()
.map(|p| ListOffsetsAnswer::error(p.partition_index, code))
.collect();
(t.name.clone(), answers)
})
.collect();
write_list_offsets_response(out, api_version, &topics)
}
ApiKey::FindCoordinator => {
let _ = FindCoordinatorReq::read(body)?;
write_find_coordinator(out, code, -1, "", -1)
}
ApiKey::JoinGroup => {
let req = JoinGroupReq::read(body)?;
out.put_i32(0);
write_error(out, code);
out.put_i32(-1);
write_nullable_string(out, None)?;
write_string(out, "")?;
write_string(out, &req.member_id)?;
write_array_len(out, 0)
}
ApiKey::SyncGroup => {
let _ = SyncGroupReq::read(body)?;
out.put_i32(0);
write_error(out, code);
write_nullable_bytes(out, Some(&Bytes::new()))
}
ApiKey::Heartbeat => {
let _ = HeartbeatReq::read(body)?;
out.put_i32(0);
write_error(out, code);
Ok(())
}
ApiKey::LeaveGroup => {
let _ = LeaveGroupReq::read(body)?;
out.put_i32(0);
write_error(out, code);
write_array_len(out, 0)
}
ApiKey::OffsetCommit => {
let req = OffsetCommitReq::read(body, api_version)?;
write_offset_commit_response(out, api_version, &req, code)
}
ApiKey::OffsetFetch => {
let _ = OffsetFetchReq::read(body)?;
out.put_i32(0);
write_array_len(out, 0)?;
write_error(out, code);
Ok(())
}
ApiKey::InitProducerId => {
let _ = InitProducerIdReq::read(body, api_version)?;
write_init_producer_id(out, api_version, 0, code, -1, -1, (-1, -1))
}
ApiKey::AddPartitionsToTxn => {
let req = AddPartitionsToTxnReq::read(body)?;
let mut by_topic: Vec<(String, Vec<i32>)> = Vec::new();
for (topic, partition) in req.partitions {
match by_topic.iter_mut().find(|(name, _)| *name == topic) {
Some((_, partitions)) => partitions.push(partition),
None => by_topic.push((topic, vec![partition])),
}
}
out.put_i32(0);
write_array_len(out, by_topic.len())?;
for (topic, partitions) in &by_topic {
write_string(out, topic)?;
write_array_len(out, partitions.len())?;
for partition in partitions {
out.put_i32(*partition);
write_error(out, code);
}
}
Ok(())
}
ApiKey::AddOffsetsToTxn => {
let _ = AddOffsetsToTxnReq::read(body)?;
out.put_i32(0);
write_error(out, code);
Ok(())
}
ApiKey::TxnOffsetCommit => {
let req = TxnOffsetCommitReq::read(body)?;
let mut by_topic: Vec<(String, Vec<i32>)> = Vec::new();
for offset in req.offsets {
match by_topic.iter_mut().find(|(name, _)| *name == offset.topic) {
Some((_, partitions)) => partitions.push(offset.partition),
None => by_topic.push((offset.topic, vec![offset.partition])),
}
}
out.put_i32(0);
write_compact_array_len(out, by_topic.len())?;
for (topic, partitions) in &by_topic {
write_compact_string(out, topic)?;
write_compact_array_len(out, partitions.len())?;
for partition in partitions {
out.put_i32(*partition);
write_error(out, code);
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)
}
ApiKey::EndTxn => {
let _ = EndTxnReq::read(body)?;
out.put_i32(0);
write_error(out, code);
if api_version >= 5 {
out.put_i64(-1);
out.put_i16(-1);
}
write_empty_tagged_fields(out)
}
ApiKey::ShareFetch => {
let req = ShareFetchReq::read(body, api_version)?;
if !share_partition_scoped(code) {
return write_share_fetch_error(out, code, "injected by the fake broker");
}
let (error, ack_error) = if code == ErrorCode::InvalidRecordState {
(ErrorCode::None, code)
} else {
(code, ErrorCode::None)
};
out.put_i32(0);
write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?;
out.put_i32(ACQUISITION_LOCK_TIMEOUT_MS);
write_share_partitions(out, &req.topics, |out, partition| {
write_share_fetch_partition(out, partition, error, ack_error, -1, -1, None, &[])
})?;
write_compact_array_len(out, 0)?;
write_empty_tagged_fields(out)
}
ApiKey::ShareAcknowledge => {
let req = ShareAcknowledgeReq::read(body, api_version)?;
if !share_partition_scoped(code) {
return write_share_acknowledge_error(
out,
api_version,
code,
"injected by the fake broker",
);
}
out.put_i32(0);
write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?;
if api_version >= 2 {
out.put_i32(ACQUISITION_LOCK_TIMEOUT_MS);
}
write_share_partitions(out, &req.topics, |out, partition| {
out.put_i32(partition);
write_error(out, code);
write_compact_nullable_string(out, None)?;
out.put_i32(-1);
out.put_i32(-1);
write_empty_tagged_fields(out)?;
write_empty_tagged_fields(out)
})?;
write_compact_array_len(out, 0)?;
write_empty_tagged_fields(out)
}
ApiKey::UpdateFeatures => {
let req = UpdateFeaturesReq::read(body, api_version)?;
out.put_i32(0);
write_error(out, code);
write_compact_nullable_string(out, Some("injected by the fake broker"))?;
if api_version < 2 {
write_compact_array_len(out, req.feature_updates.len())?;
for update in &req.feature_updates {
write_compact_string(out, &update.feature)?;
write_error(out, code);
write_compact_nullable_string(out, None)?;
write_empty_tagged_fields(out)?;
}
}
write_empty_tagged_fields(out)
}
ApiKey::StreamsGroupDescribe => {
let req = StreamsGroupDescribeReq::read(body)?;
out.put_i32(0);
write_compact_array_len(out, req.group_ids.len())?;
for group_id in &req.group_ids {
write_error(out, code);
write_compact_nullable_string(out, None)?;
write_compact_string(out, group_id)?;
write_compact_string(out, "")?;
out.put_i32(0);
out.put_i32(0);
write_presence(out, false);
write_compact_array_len(out, 0)?;
out.put_i32(i32::MIN);
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)
}
ApiKey::CreateTopics => {
let req = CreateTopicsReq::read(body)?;
out.put_i32(0);
write_array_len(out, req.topics.len())?;
for topic in &req.topics {
write_string(out, &topic.name)?;
write_error(out, code);
write_nullable_string(out, Some("injected by the fake broker"))?;
}
Ok(())
}
ApiKey::DeleteTopics => {
let req = DeleteTopicsReq::read(body)?;
out.put_i32(0);
write_array_len(out, req.topic_names.len())?;
for name in &req.topic_names {
write_nullable_string(out, Some(name))?;
write_error(out, code);
}
Ok(())
}
ApiKey::DescribeGroups => {
let req = DescribeGroupsReq::read(body)?;
out.put_i32(0);
write_array_len(out, req.groups.len())?;
for group_id in &req.groups {
write_error(out, code);
write_string(out, group_id)?;
write_string(out, "")?; write_string(out, "")?; write_string(out, "")?; write_array_len(out, 0)?; out.put_i32(i32::MIN); }
Ok(())
}
ApiKey::GetTelemetrySubscriptions => {
out.put_i32(0); write_error(out, code);
out.put_slice(&[0; 16]); out.put_i32(0); write_compact_array_len(out, 0)?; out.put_i32(0); out.put_i32(0); out.put_u8(0); write_compact_array_len(out, 0)?; write_empty_tagged_fields(out)
}
ApiKey::PushTelemetry => {
out.put_i32(0); write_error(out, code);
write_empty_tagged_fields(out)
}
other => Err(crate::error::KrafkaError::protocol_kind(
crate::error::ProtocolErrorKind::UnknownApiVersion,
format!("fake broker cannot synthesize an error for {other:?}"),
)),
}
}
fn read_uuid(body: &mut Bytes) -> Result<[u8; 16]> {
if body.len() < 16 {
return Err(crate::error::KrafkaError::protocol_kind(
crate::error::ProtocolErrorKind::TruncatedFrame,
"uuid",
));
}
let mut id = [0; 16];
id.copy_from_slice(&body.split_to(16));
Ok(id)
}
fn get_telemetry_subscriptions(
body: &mut Bytes,
state: &ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let requested_id = read_uuid(body)?;
skip_tagged_fields(body)?;
let Some(subscription) = &state.telemetry else {
return dispatch_error(
ApiKey::GetTelemetrySubscriptions,
0,
body,
ErrorCode::UnsupportedVersion,
out,
);
};
out.put_i32(0); write_error(out, ErrorCode::None);
if requested_id == [0; 16] {
out.put_slice(&subscription.client_instance_id);
} else {
out.put_slice(&requested_id);
}
out.put_i32(subscription.subscription_id);
write_compact_array_len(out, subscription.accepted_compression_types.len())?;
for codec in &subscription.accepted_compression_types {
codec.encode(out);
}
out.put_i32(i32::try_from(subscription.push_interval.as_millis()).unwrap_or(i32::MAX));
out.put_i32(1024 * 1024); out.put_u8(u8::from(subscription.delta_temporality));
write_compact_array_len(out, subscription.requested_metrics.len())?;
for name in &subscription.requested_metrics {
write_compact_string(out, name)?;
}
write_empty_tagged_fields(out)
}
fn push_telemetry(body: &mut Bytes, state: &mut ClusterState, out: &mut BytesMut) -> Result<()> {
let client_instance_id = read_uuid(body)?;
let subscription_id = i32::decode(body)?;
let terminating = bool::decode(body)?;
let compression_type = i8::decode(body)?;
let metrics = read_compact_nullable_bytes(body)?.unwrap_or_default();
skip_tagged_fields(body)?;
let code = match &state.telemetry {
Some(subscription) if subscription.subscription_id == subscription_id => {
state.telemetry_pushes.push(super::TelemetryPush {
client_instance_id,
subscription_id,
terminating,
compression_type,
metrics,
});
ErrorCode::None
}
_ => ErrorCode::UnknownSubscriptionId,
};
out.put_i32(0); write_error(out, code);
write_empty_tagged_fields(out)
}
pub(crate) const API_VERSIONS_RANGE: (i16, i16) = (0, 4);
fn api_versions(request_version: i16, state: &ClusterState, out: &mut BytesMut) -> Result<()> {
let (min_version, max_version) = API_VERSIONS_RANGE;
if request_version < min_version || request_version > max_version {
write_error(out, ErrorCode::UnsupportedVersion);
write_array_len(out, 1)?;
ApiKey::ApiVersions.to_i16().encode(out);
min_version.encode(out);
max_version.encode(out);
return Ok(());
}
let flexible = request_version >= 3;
let mut versions = supported_versions();
if state.telemetry.is_some() {
versions.push((ApiKey::GetTelemetrySubscriptions, 0, 0));
versions.push((ApiKey::PushTelemetry, 0, 0));
}
if state.sasl_plain.is_some() {
versions.push((ApiKey::SaslHandshake, 1, 1));
versions.push((ApiKey::SaslAuthenticate, 1, 1));
}
for (&api_key, &(lo, hi)) in &state.api_version_overrides {
if !versions.iter().any(|(k, _, _)| *k == api_key) {
versions.push((api_key, lo, hi));
}
}
write_error(out, ErrorCode::None);
if flexible {
write_compact_array_len(out, versions.len())?;
} else {
write_array_len(out, versions.len())?;
}
for (api_key, min, max) in versions {
let (lo, hi) = if let Some(&range) = state.api_version_overrides.get(&api_key) {
range
} else if api_key == ApiKey::ApiVersions {
(min_version, max_version)
} else {
(min, max)
};
api_key.to_i16().encode(out);
lo.encode(out);
hi.encode(out);
if flexible {
write_empty_tagged_fields(out)?;
}
}
if request_version >= 1 {
out.put_i32(0);
}
if flexible {
write_feature_tagged_fields(state, out)?;
}
Ok(())
}
fn write_feature_tagged_fields(state: &ClusterState, out: &mut BytesMut) -> Result<()> {
if state.finalized_features.is_empty() {
return write_empty_tagged_fields(out);
}
let mut features: Vec<(&String, &i16)> = state.finalized_features.iter().collect();
features.sort_by_key(|(name, _)| (*name).clone());
let mut supported = BytesMut::new();
write_compact_array_len(&mut supported, features.len())?;
for (name, level) in &features {
write_compact_string(&mut supported, name)?;
supported.put_i16(1); supported.put_i16(**level); write_empty_tagged_fields(&mut supported)?;
}
let mut epoch = BytesMut::new();
epoch.put_i64(state.finalized_features_epoch);
let mut finalized = BytesMut::new();
write_compact_array_len(&mut finalized, features.len())?;
for (name, level) in &features {
write_compact_string(&mut finalized, name)?;
finalized.put_i16(**level); finalized.put_i16(1); write_empty_tagged_fields(&mut finalized)?;
}
write_tagged_fields(
out,
vec![
TaggedField {
tag: 0,
data: supported.freeze(),
},
TaggedField {
tag: 1,
data: epoch.freeze(),
},
TaggedField {
tag: 2,
data: finalized.freeze(),
},
],
)
}
fn metadata(body: &mut Bytes, state: &mut ClusterState, out: &mut BytesMut) -> Result<()> {
let req = MetadataReq::read_v12(body)?;
let requested: Vec<String> = match &req.topics {
Some(names) => {
for name in names {
if !state.topics.contains_key(name)
&& state.auto_create_topics
&& req.allow_auto_topic_creation
{
let partitions = state.default_partitions;
state.create_topic(name, partitions);
}
}
names.clone()
}
None => {
let mut all: Vec<String> = state.topics.keys().cloned().collect();
all.sort();
all
}
};
out.put_i32(0);
write_compact_array_len(out, state.brokers.len())?;
for broker in &state.brokers {
out.put_i32(broker.node_id);
write_compact_nullable_string(out, Some(&broker.host))?;
out.put_i32(broker.port);
write_compact_nullable_string(out, broker.rack.as_deref())?;
write_empty_tagged_fields(out)?;
}
write_compact_nullable_string(out, Some(&state.cluster_id))?;
out.put_i32(state.controller_id);
write_compact_array_len(out, requested.len())?;
for name in &requested {
match state.topics.get(name) {
None => {
write_error(out, ErrorCode::UnknownTopicOrPartition);
write_compact_nullable_string(out, Some(name))?;
out.put_slice(&[0u8; 16]); out.put_u8(0); write_compact_array_len(out, 0)?;
out.put_i32(i32::MIN); write_empty_tagged_fields(out)?;
}
Some(topic) => {
write_error(out, ErrorCode::None);
write_compact_nullable_string(out, Some(name))?;
out.put_slice(&topic.topic_id);
out.put_u8(0); write_compact_array_len(out, topic.partitions.len())?;
for (index, partition) in topic.partitions.iter().enumerate() {
write_error(out, ErrorCode::None);
out.put_i32(index as i32);
out.put_i32(partition.leader);
out.put_i32(partition.leader_epoch);
write_compact_i32_array(out, &partition.replicas)?;
write_compact_i32_array(out, &partition.isr)?;
write_compact_i32_array(out, &[])?; write_empty_tagged_fields(out)?;
}
out.put_i32(i32::MIN); write_empty_tagged_fields(out)?;
}
}
}
write_empty_tagged_fields(out)
}
fn write_compact_i32_array(out: &mut BytesMut, values: &[i32]) -> Result<()> {
write_compact_array_len(out, values.len())?;
for value in values {
out.put_i32(*value);
}
Ok(())
}
fn produce(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = ProduceReq::read(body)?;
let mut hinted_leaders: Vec<i32> = Vec::new();
write_compact_array_len(out, req.topics.len())?;
for topic in &req.topics {
KafkaString::new(&topic.name).try_encode_compact(out)?;
write_compact_array_len(out, topic.partitions.len())?;
for partition in &topic.partitions {
let leader = state
.partition(&topic.name, partition.index)
.map(|p| (p.leader, p.leader_epoch));
match leader {
None => write_produce_partition(
out,
partition.index,
ErrorCode::UnknownTopicOrPartition,
-1,
-1,
None,
)?,
Some((leader_id, leader_epoch)) if leader_id != node_id => {
if !hinted_leaders.contains(&leader_id) {
hinted_leaders.push(leader_id);
}
write_produce_partition(
out,
partition.index,
ErrorCode::NotLeaderForPartition,
-1,
-1,
Some((leader_id, leader_epoch)),
)?;
}
Some(_) => {
let (code, base_offset, log_start_offset) = append_produce_partition(
state,
api_version,
req.transactional_id.as_deref(),
&topic.name,
partition.index,
partition.records.as_ref(),
);
write_produce_partition(
out,
partition.index,
code,
base_offset,
log_start_offset,
None,
)?;
}
}
}
write_empty_tagged_fields(out)?; }
out.put_i32(state.throttle(ApiKey::Produce));
write_produce_node_endpoints(out, &hinted_leaders, state)
}
fn append_produce_partition(
state: &mut ClusterState,
api_version: i16,
transactional_id: Option<&str>,
topic: &str,
partition: i32,
records: Option<&Bytes>,
) -> (ErrorCode, i64, i64) {
let Some(records) = records else {
let (next, start) = state
.partition(topic, partition)
.map_or((-1, -1), |p| (p.next_offset, p.log_start_offset));
return (ErrorCode::None, next, start);
};
let producer = batch_producer(records).filter(|b| b.producer_id >= 0);
let Some(producer) = producer.filter(|_| state.idempotence) else {
let txn_id = transactional_id
.filter(|id| state.transactions.contains_key(*id))
.map(str::to_string);
let Some(p) = state.partition_mut(topic, partition) else {
return (ErrorCode::UnknownTopicOrPartition, -1, -1);
};
let base_offset = p.append(records);
if txn_id.is_some() {
let producer_id = producer.map_or(-1, |b| b.producer_id);
p.open_transactions
.entry(producer_id)
.or_insert(base_offset);
}
let log_start_offset = p.log_start_offset;
if let Some(id) = txn_id
&& let Some(txn) = state.transactions.get_mut(&id)
{
txn.begin();
let entry = (topic.to_string(), partition);
if !txn.partitions.contains(&entry) {
txn.partitions.push(entry);
}
}
return (ErrorCode::None, base_offset, log_start_offset);
};
let log_start_offset = state
.partition(topic, partition)
.map_or(-1, |p| p.log_start_offset);
let reject = |code: ErrorCode| (code, -1, log_start_offset);
let mut joins_transaction: Option<String> = None;
if producer.transactional {
let txn_id = transactional_id
.map(str::to_string)
.or_else(|| state.transaction_for_producer(producer.producer_id));
let Some((txn_id, txn)) = txn_id.and_then(|id| {
let txn = state.transactions.get(&id)?.clone();
Some((id, txn))
}) else {
return reject(ErrorCode::InvalidProducerIdMapping);
};
if txn.producer_id != producer.producer_id {
return reject(ErrorCode::InvalidProducerIdMapping);
}
if txn.producer_epoch != producer.producer_epoch {
return reject(ErrorCode::InvalidProducerEpoch);
}
if matches!(
txn.status,
TxnStatus::PrepareCommit | TxnStatus::PrepareAbort
) {
return reject(ErrorCode::ConcurrentTransactions);
}
let registered = txn.is_open()
&& txn
.partitions
.iter()
.any(|(t, p)| t == topic && *p == partition);
if !registered {
if api_version < 12 {
return reject(ErrorCode::InvalidTxnState);
}
joins_transaction = Some(txn_id);
}
}
let pre_kip360 = state.pre_kip360();
let Some(p) = state.partition_mut(topic, partition) else {
return reject(ErrorCode::UnknownTopicOrPartition);
};
if !producer.transactional && p.open_transactions.contains_key(&producer.producer_id) {
return reject(ErrorCode::InvalidTxnState);
}
match p.check_sequence(
producer.producer_id,
producer.producer_epoch,
producer.base_sequence,
producer.record_count,
pre_kip360,
) {
SequenceCheck::Reject(code) => return reject(code),
SequenceCheck::Duplicate(base_offset) => {
return (ErrorCode::None, base_offset, p.log_start_offset);
}
SequenceCheck::Append => {}
}
let base_offset = p.append(records);
p.record_batch(
producer.producer_id,
producer.producer_epoch,
producer.base_sequence,
producer.record_count,
base_offset,
);
if producer.transactional {
p.open_transactions
.entry(producer.producer_id)
.or_insert(base_offset);
}
let log_start_offset = p.log_start_offset;
if let Some(txn_id) = joins_transaction
&& let Some(txn) = state.transactions.get_mut(&txn_id)
{
txn.begin();
let entry = (topic.to_string(), partition);
if !txn.partitions.contains(&entry) {
txn.partitions.push(entry);
}
}
(ErrorCode::None, base_offset, log_start_offset)
}
fn write_produce_node_endpoints(
out: &mut BytesMut,
leaders: &[i32],
state: &ClusterState,
) -> Result<()> {
if leaders.is_empty() {
return write_empty_tagged_fields(out);
}
let endpoints: Vec<(i32, &str, i32)> = leaders
.iter()
.filter_map(|id| {
state
.brokers
.iter()
.find(|b| b.node_id == *id)
.map(|b| (b.node_id, b.host.as_str(), b.port))
})
.collect();
if endpoints.is_empty() {
return write_empty_tagged_fields(out);
}
write_tagged_fields(out, vec![node_endpoints_field(&endpoints)?])
}
fn write_produce_partition(
out: &mut BytesMut,
index: i32,
code: ErrorCode,
base_offset: i64,
log_start_offset: i64,
current_leader: Option<(i32, i32)>,
) -> Result<()> {
out.put_i32(index);
write_error(out, code);
out.put_i64(base_offset);
out.put_i64(-1); out.put_i64(log_start_offset);
write_compact_array_len(out, 0)?; write_compact_nullable_string(out, None)?; match current_leader {
Some((leader_id, leader_epoch)) => {
write_tagged_fields(out, vec![current_leader_field(leader_id, leader_epoch)])
}
None => write_empty_tagged_fields(out),
}
}
pub(crate) fn dispatch_corrupt(
api_key: ApiKey,
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
match api_key {
ApiKey::Fetch => {
let mut poll = LongPoll {
expired: true,
..LongPoll::default()
};
fetch_inner(body, node_id, state, &mut poll, out, true).map(|_| ())
}
other => Err(crate::error::KrafkaError::protocol_kind(
crate::error::ProtocolErrorKind::UnknownApiVersion,
format!(
"fake broker models record corruption only for Fetch, not {other:?}; \
asserting on Control::CorruptRecords here would prove nothing"
),
)),
}
}
fn corrupt_record_bytes(records: &Bytes) -> Bytes {
const CRC_REGION_START: usize = 21;
if records.len() <= CRC_REGION_START {
return records.clone();
}
let mut bytes = records.to_vec();
bytes[CRC_REGION_START] ^= 0xFF;
Bytes::from(bytes)
}
fn fetch_inner(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
poll: &mut LongPoll,
out: &mut BytesMut,
corrupt: bool,
) -> Result<Served> {
let req = FetchReq::read(body)?;
out.put_i32(state.throttle(ApiKey::Fetch));
write_error(out, ErrorCode::None);
out.put_i32(req.session_id);
let mut record_bytes = 0usize;
let mut any_error = false;
write_array_len(out, req.topics.len())?;
for topic in &req.topics {
write_string(out, &topic.topic)?;
write_array_len(out, topic.partitions.len())?;
for partition in &topic.partitions {
let p = state.partition(&topic.topic, partition.partition);
let error = match p {
None => Some(ErrorCode::UnknownTopicOrPartition),
Some(p) if p.leader != node_id => Some(ErrorCode::NotLeaderForPartition),
Some(p)
if partition.current_leader_epoch >= 0
&& partition.current_leader_epoch != p.leader_epoch =>
{
Some(if partition.current_leader_epoch < p.leader_epoch {
ErrorCode::FencedLeaderEpoch
} else {
ErrorCode::UnknownLeaderEpoch
})
}
Some(p) if partition.fetch_offset > p.next_offset => {
Some(ErrorCode::OffsetOutOfRange)
}
Some(_) => None,
};
let Some(p) = p.filter(|_| error.is_none()) else {
any_error = true;
let (high_watermark, log_start_offset) =
p.map_or((0, 0), |p| (p.next_offset, p.log_start_offset));
write_fetch_partition(
out,
partition.partition,
error.unwrap_or(ErrorCode::UnknownServerError),
high_watermark,
log_start_offset,
None,
)?;
continue;
};
let read_committed = req.isolation_level == 1;
let records = if read_committed {
p.read_range(partition.fetch_offset, p.last_stable_offset())
} else {
p.read_from(partition.fetch_offset)
};
record_bytes += records.len();
let records = if corrupt {
corrupt_record_bytes(&records)
} else {
records
};
let aborted = if read_committed {
p.aborted_transactions_from(partition.fetch_offset)
} else {
Vec::new()
};
write_fetch_partition_with_aborted(
out,
partition.partition,
ErrorCode::None,
p.next_offset,
p.last_stable_offset(),
p.log_start_offset,
&aborted,
Some(&records),
)?;
}
}
let min_bytes = usize::try_from(req.min_bytes.max(1)).unwrap_or(1);
if !poll.expired && !any_error && req.max_wait_ms > 0 && record_bytes < min_bytes {
return Ok(Served::Wait(Duration::from_millis(req.max_wait_ms as u64)));
}
Ok(Served::Done)
}
fn write_fetch_partition(
out: &mut BytesMut,
partition: i32,
code: ErrorCode,
high_watermark: i64,
log_start_offset: i64,
records: Option<&Bytes>,
) -> Result<()> {
write_fetch_partition_with_aborted(
out,
partition,
code,
high_watermark,
high_watermark,
log_start_offset,
&[],
records,
)
}
#[allow(clippy::too_many_arguments)]
fn write_fetch_partition_with_aborted(
out: &mut BytesMut,
partition: i32,
code: ErrorCode,
high_watermark: i64,
last_stable_offset: i64,
log_start_offset: i64,
aborted: &[(i64, i64)],
records: Option<&Bytes>,
) -> Result<()> {
out.put_i32(partition);
write_error(out, code);
out.put_i64(high_watermark);
out.put_i64(last_stable_offset);
out.put_i64(log_start_offset);
write_array_len(out, aborted.len())?;
for (producer_id, first_offset) in aborted {
out.put_i64(*producer_id);
out.put_i64(*first_offset);
}
out.put_i32(-1); write_nullable_bytes(out, records)?;
Ok(())
}
pub(crate) const HEARTBEAT_INTERVAL_MS: i32 = 1_000;
const CONSUMER_SESSION_TIMEOUT: Duration = Duration::from_secs(45);
pub(crate) const ACQUISITION_LOCK_TIMEOUT_MS: i32 = 30_000;
fn consumer_group_heartbeat(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = ConsumerGroupHeartbeatReq::read(body)?;
if state.group_coordinator(&req.group_id) != node_id {
return write_heartbeat_error(out, ErrorCode::NotCoordinator, None, 0);
}
state
.consumer_group_heartbeats
.push(ConsumerGroupHeartbeatSeen {
group_id: req.group_id.clone(),
member_id: req.member_id.clone(),
member_epoch: req.member_epoch,
instance_id: req.instance_id.clone(),
server_assignor: req.server_assignor.clone(),
full: req.subscribed_topic_names.is_some(),
});
let now = tokio::time::Instant::now();
if let Some(group) = state.groups.get_mut(&req.group_id) {
let before = group.consumer_members.len();
group.consumer_members.retain(|id, member| {
*id == req.member_id
|| member
.last_heartbeat
.is_none_or(|at| now.duration_since(at) < CONSUMER_SESSION_TIMEOUT)
});
if group.consumer_members.len() != before {
group.group_epoch += 1;
}
}
if req.member_epoch < 0 {
if let Some(group) = state.groups.get_mut(&req.group_id) {
group.consumer_members.remove(&req.member_id);
group.group_epoch += 1;
}
out.put_i32(0); write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?; write_compact_nullable_string(out, Some(&req.member_id))?;
out.put_i32(req.member_epoch); out.put_i32(HEARTBEAT_INTERVAL_MS);
write_heartbeat_assignment(out, None)?;
return write_empty_tagged_fields(out);
}
if let Some(assignor) = req.server_assignor.as_deref()
&& !matches!(assignor, "uniform" | "range")
{
return write_heartbeat_error(out, ErrorCode::UnsupportedAssignor, Some(&req.member_id), 0);
}
let partition_counts: HashMap<String, i32> = state
.topics
.iter()
.map(|(name, t)| (name.clone(), t.partitions.len() as i32))
.collect();
let topic_ids: HashMap<String, [u8; 16]> = state
.topics
.iter()
.map(|(name, t)| (name.clone(), t.topic_id))
.collect();
let group = state.groups.entry(req.group_id.clone()).or_default();
let known = group.consumer_members.get(&req.member_id).cloned();
if let Some(existing) = &known
&& req.member_epoch != 0
&& req.member_epoch != existing.member_epoch
{
return write_heartbeat_error(out, ErrorCode::FencedMemberEpoch, Some(&req.member_id), 0);
}
if known.is_none() && req.member_epoch != 0 {
return write_heartbeat_error(out, ErrorCode::UnknownMemberId, Some(&req.member_id), 0);
}
let subscribed = req
.subscribed_topic_names
.clone()
.or_else(|| known.as_ref().map(|m| m.subscribed_topics.clone()))
.unwrap_or_default();
let reported_owned: Option<HashMap<String, Vec<i32>>> =
req.topic_partitions.as_ref().map(|tps| {
let mut owned: HashMap<String, Vec<i32>> = HashMap::new();
for tp in tps {
if let Some((name, _)) = topic_ids.iter().find(|(_, id)| **id == tp.topic_id) {
owned.insert(name.clone(), tp.partitions.clone());
}
}
owned
});
let is_new = known.is_none();
let rejoining = known.is_some() && req.member_epoch == 0;
let subscription_changed = known
.as_ref()
.is_some_and(|m| m.subscribed_topics != subscribed);
if is_new || rejoining || subscription_changed {
group.group_epoch += 1;
}
let group_epoch = group.group_epoch;
{
let member = group
.consumer_members
.entry(req.member_id.clone())
.or_default();
member.instance_id = req.instance_id.clone();
member.subscribed_topics = subscribed.clone();
member.last_heartbeat = Some(now);
if let Some(owned) = reported_owned {
member.owned = owned;
}
if is_new || rejoining {
member.owned.clear();
member.assignment.clear();
}
}
let targets = compute_target_assignment(group, &partition_counts);
let empty_target: HashMap<String, Vec<i32>> = HashMap::new();
let target = targets.get(&req.member_id).unwrap_or(&empty_target);
let member_owned = group
.consumer_members
.get(&req.member_id)
.map(|m| m.owned.clone())
.unwrap_or_default();
let owns_beyond_target = member_owned.iter().any(|(topic, partitions)| {
let keep = target.get(topic);
partitions
.iter()
.any(|p| !keep.is_some_and(|k| k.contains(p)))
});
let held_elsewhere: HashMap<String, Vec<i32>> = {
let mut held: HashMap<String, Vec<i32>> = HashMap::new();
for (id, m) in &group.consumer_members {
if *id == req.member_id {
continue;
}
for (topic, partitions) in &m.owned {
held.entry(topic.clone()).or_default().extend(partitions);
}
}
held
};
let (granted, advance_epoch) = if owns_beyond_target {
let mut keep: HashMap<String, Vec<i32>> = HashMap::new();
for (topic, partitions) in &member_owned {
if let Some(target_partitions) = target.get(topic) {
let retained: Vec<i32> = partitions
.iter()
.copied()
.filter(|p| target_partitions.contains(p))
.collect();
if !retained.is_empty() {
keep.insert(topic.clone(), retained);
}
}
}
(keep, false)
} else {
let mut grant: HashMap<String, Vec<i32>> = HashMap::new();
for (topic, partitions) in target {
let blocked = held_elsewhere.get(topic);
let available: Vec<i32> = partitions
.iter()
.copied()
.filter(|p| !blocked.is_some_and(|b| b.contains(p)))
.collect();
if !available.is_empty() {
grant.insert(topic.clone(), available);
}
}
let complete = grant == *target;
(grant, complete)
};
let (member_epoch, send_assignment) = {
let member = group
.consumer_members
.entry(req.member_id.clone())
.or_default();
if advance_epoch || member.member_epoch == 0 {
member.member_epoch = group_epoch;
}
let changed = member.assignment != granted;
member.assignment = granted.clone();
if changed {
member.assignment_dirty = true;
}
for (topic, partitions) in &granted {
let held = member.owned.entry(topic.clone()).or_default();
for p in partitions {
if !held.contains(p) {
held.push(*p);
}
}
held.sort_unstable();
}
let dirty = member.assignment_dirty || is_new || rejoining || subscription_changed;
member.assignment_dirty = false;
(member.member_epoch, dirty)
};
let wire_assignment: Vec<HeartbeatTopicPartitions> = granted
.iter()
.filter_map(|(topic, partitions)| {
topic_ids.get(topic).map(|id| HeartbeatTopicPartitions {
topic_id: *id,
partitions: partitions.clone(),
})
})
.collect();
out.put_i32(0); write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?; write_compact_nullable_string(out, Some(&req.member_id))?;
out.put_i32(member_epoch);
out.put_i32(HEARTBEAT_INTERVAL_MS);
write_heartbeat_assignment(
out,
if send_assignment {
Some(&wire_assignment)
} else {
None
},
)?;
write_empty_tagged_fields(out)
}
fn compute_target_assignment(
group: &super::state::GroupState,
partition_counts: &HashMap<String, i32>,
) -> HashMap<String, HashMap<String, Vec<i32>>> {
let mut member_ids: Vec<&String> = group.consumer_members.keys().collect();
member_ids.sort();
let mut targets: HashMap<String, HashMap<String, Vec<i32>>> = member_ids
.iter()
.map(|id| ((*id).clone(), HashMap::new()))
.collect();
let mut topics: Vec<&String> = group
.consumer_members
.values()
.flat_map(|m| m.subscribed_topics.iter())
.collect();
topics.sort();
topics.dedup();
for topic in topics {
let subscribers: Vec<&String> = member_ids
.iter()
.copied()
.filter(|id| {
group
.consumer_members
.get(*id)
.is_some_and(|m| m.subscribed_topics.contains(topic))
})
.collect();
if subscribers.is_empty() {
continue;
}
let count = partition_counts.get(topic).copied().unwrap_or(0);
for partition in 0..count {
let owner = subscribers[(partition as usize) % subscribers.len()];
targets
.entry(owner.clone())
.or_default()
.entry(topic.clone())
.or_default()
.push(partition);
}
}
targets
}
fn write_heartbeat_error(
out: &mut BytesMut,
code: ErrorCode,
member_id: Option<&str>,
member_epoch: i32,
) -> Result<()> {
out.put_i32(0); write_error(out, code);
write_compact_nullable_string(out, Some(&format!("{code:?}")))?;
write_compact_nullable_string(out, member_id)?;
out.put_i32(member_epoch);
out.put_i32(HEARTBEAT_INTERVAL_MS);
write_heartbeat_assignment(out, None)?;
write_empty_tagged_fields(out)
}
const TIMESTAMP_EARLIEST: i64 = -2;
const TIMESTAMP_LATEST: i64 = -1;
fn list_offsets(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = ListOffsetsReq::read(body, api_version)?;
let mut topics: Vec<(String, Vec<ListOffsetsAnswer>)> = Vec::with_capacity(req.topics.len());
for topic in &req.topics {
let mut answers = Vec::with_capacity(topic.partitions.len());
for partition in &topic.partitions {
state.list_offsets_lookups.push(ListOffsetsLookup {
node_id,
api_version,
topic: topic.name.clone(),
partition: partition.partition_index,
timestamp: partition.timestamp,
});
let index = partition.partition_index;
answers.push(match state.partition(&topic.name, index) {
None => ListOffsetsAnswer::error(index, ErrorCode::UnknownTopicOrPartition),
Some(p) if p.leader != node_id => {
ListOffsetsAnswer::error(index, ErrorCode::NotLeaderForPartition)
}
Some(p)
if partition.current_leader_epoch >= 0
&& partition.current_leader_epoch != p.leader_epoch =>
{
let code = if partition.current_leader_epoch < p.leader_epoch {
ErrorCode::FencedLeaderEpoch
} else {
ErrorCode::UnknownLeaderEpoch
};
ListOffsetsAnswer::error(index, code)
}
Some(p) => {
let (timestamp, offset) = match partition.timestamp {
TIMESTAMP_LATEST if req.isolation_level == 1 => {
(-1, p.last_stable_offset())
}
TIMESTAMP_LATEST => (-1, p.next_offset),
TIMESTAMP_EARLIEST => (-1, p.log_start_offset),
ts if ts >= 0 => first_at_or_after(p, ts)?.unwrap_or((-1, -1)),
_ => (-1, p.log_start_offset),
};
ListOffsetsAnswer {
partition_index: index,
error_code: ErrorCode::None,
timestamp,
offset,
leader_epoch: p.leader_epoch,
}
}
});
}
topics.push((topic.name.clone(), answers));
}
write_list_offsets_response(out, api_version, &topics)
}
fn first_at_or_after(
partition: &super::state::PartitionState,
timestamp: i64,
) -> Result<Option<(i64, i64)>> {
for stored in &partition.log {
let mut buf = stored.clone();
let batch = crate::protocol::RecordBatch::decode(&mut buf)?;
if batch.attributes.is_control_batch || batch.max_timestamp < timestamp {
continue;
}
for record in &batch.records {
let offset = batch
.base_offset
.saturating_add(i64::from(record.offset_delta));
let record_timestamp = batch.base_timestamp.saturating_add(record.timestamp_delta);
if offset >= partition.log_start_offset && record_timestamp >= timestamp {
return Ok(Some((record_timestamp, offset)));
}
}
}
Ok(None)
}
const COORDINATOR_TYPE_TRANSACTION: i8 = 1;
fn find_coordinator(body: &mut Bytes, state: &mut ClusterState, out: &mut BytesMut) -> Result<()> {
let req = FindCoordinatorReq::read(body)?;
let node_id = if req.key_type == COORDINATOR_TYPE_TRANSACTION {
state.txn_coordinator(&req.key)
} else {
state.group_coordinator(&req.key)
};
match state.broker(node_id).filter(|b| b.online).cloned() {
Some(broker) => write_find_coordinator(
out,
ErrorCode::None,
broker.node_id,
&broker.host,
broker.port,
),
None => write_find_coordinator(out, ErrorCode::CoordinatorNotAvailable, -1, "", -1),
}
}
fn write_find_coordinator(
out: &mut BytesMut,
code: ErrorCode,
node_id: i32,
host: &str,
port: i32,
) -> Result<()> {
out.put_i32(0); write_error(out, code);
write_nullable_string(out, None)?; out.put_i32(node_id);
write_string(out, host)?;
out.put_i32(port);
Ok(())
}
fn coordinator_check(state: &ClusterState, group_id: &str, node_id: i32) -> Option<ErrorCode> {
let coordinator = state.group_coordinator(group_id);
if coordinator == node_id {
None
} else if state.broker(coordinator).map(|b| b.online) == Some(true) {
Some(ErrorCode::NotCoordinator)
} else {
Some(ErrorCode::CoordinatorNotAvailable)
}
}
fn join_group(
body: &mut Bytes,
node_id: i32,
client_id: Option<&str>,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = JoinGroupReq::read(body)?;
if let Some(code) = coordinator_check(state, &req.group_id, node_id) {
out.put_i32(0);
write_error(out, code);
out.put_i32(-1);
write_nullable_string(out, None)?;
write_string(out, "")?;
write_string(out, &req.member_id)?;
return write_array_len(out, 0);
}
let member_id = if req.member_id.is_empty() {
state.next_member_id(&req.group_id)
} else {
req.member_id.clone()
};
let protocol_name = req.protocols.first().map(|p| p.name.clone());
let metadata = req
.protocols
.first()
.map(|p| p.metadata.clone())
.unwrap_or_default();
let group = state.groups.entry(req.group_id.clone()).or_default();
group.protocol_type = req.protocol_type.clone();
group.protocol_name = protocol_name.clone();
group.generation_id += 1;
group.members = vec![GroupMember {
member_id: member_id.clone(),
group_instance_id: req.group_instance_id.clone(),
metadata: metadata.clone(),
client_id: client_id.unwrap_or_default().to_string(),
client_host: "/127.0.0.1".to_string(),
}];
group.leader = member_id.clone();
group.assignments.clear();
group.state = ClassicGroupState::CompletingRebalance;
let generation_id = group.generation_id;
let members = group.members.clone();
out.put_i32(0); write_error(out, ErrorCode::None);
out.put_i32(generation_id);
write_nullable_string(out, protocol_name.as_deref())?;
write_string(out, &member_id)?; write_string(out, &member_id)?;
write_array_len(out, members.len())?;
for member in &members {
write_string(out, &member.member_id)?;
write_nullable_string(out, member.group_instance_id.as_deref())?;
write_nullable_bytes(out, Some(&member.metadata))?;
}
Ok(())
}
fn sync_group(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = SyncGroupReq::read(body)?;
if let Some(code) = coordinator_check(state, &req.group_id, node_id) {
out.put_i32(0);
write_error(out, code);
return write_nullable_bytes(out, Some(&Bytes::new()));
}
let group = state.groups.entry(req.group_id.clone()).or_default();
if group.generation_id != req.generation_id {
out.put_i32(0);
write_error(out, ErrorCode::IllegalGeneration);
return write_nullable_bytes(out, Some(&Bytes::new()));
}
for assignment in &req.assignments {
group
.assignments
.insert(assignment.member_id.clone(), assignment.assignment.clone());
}
if !group.members.is_empty() {
group.state = ClassicGroupState::Stable;
}
let assignment = group
.assignments
.get(&req.member_id)
.cloned()
.unwrap_or_default();
out.put_i32(0); write_error(out, ErrorCode::None);
write_nullable_bytes(out, Some(&assignment))
}
fn describe_groups(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = DescribeGroupsReq::read(body)?;
out.put_i32(0); write_array_len(out, req.groups.len())?;
for group_id in &req.groups {
if let Some(code) = coordinator_check(state, group_id, node_id) {
write_error(out, code);
write_string(out, group_id)?;
write_string(out, "")?; write_string(out, "")?; write_string(out, "")?; write_array_len(out, 0)?;
out.put_i32(i32::MIN); continue;
}
let Some(group) = state.groups.get(group_id) else {
write_error(out, ErrorCode::None);
write_string(out, group_id)?;
write_string(out, "Dead")?;
write_string(out, "")?;
write_string(out, "")?;
write_array_len(out, 0)?;
out.put_i32(i32::MIN);
continue;
};
write_error(out, ErrorCode::None);
write_string(out, group_id)?;
write_string(out, group.state.as_str())?;
write_string(out, &group.protocol_type)?;
write_string(out, group.protocol_name.as_deref().unwrap_or(""))?;
write_array_len(out, group.members.len())?;
for member in &group.members {
write_string(out, &member.member_id)?;
write_nullable_string(out, member.group_instance_id.as_deref())?;
write_string(out, &member.client_id)?;
write_string(out, &member.client_host)?;
write_nullable_bytes(out, Some(&member.metadata))?;
let assignment = group.assignments.get(&member.member_id);
write_nullable_bytes(out, Some(assignment.unwrap_or(&EMPTY_ASSIGNMENT)))?;
}
out.put_i32(if req.include_authorized_operations {
i32::MAX
} else {
i32::MIN
});
}
Ok(())
}
const EMPTY_ASSIGNMENT: Bytes = Bytes::from_static(&[]);
fn heartbeat(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = HeartbeatReq::read(body)?;
let code = match coordinator_check(state, &req.group_id, node_id) {
Some(code) => code,
None => match state.groups.get(&req.group_id) {
Some(group) if group.generation_id != req.generation_id => ErrorCode::IllegalGeneration,
Some(group) if !group.members.iter().any(|m| m.member_id == req.member_id) => {
ErrorCode::UnknownMemberId
}
_ => ErrorCode::None,
},
};
out.put_i32(0); write_error(out, code);
Ok(())
}
fn leave_group(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = LeaveGroupReq::read(body)?;
if let Some(code) = coordinator_check(state, &req.group_id, node_id) {
out.put_i32(0);
write_error(out, code);
return write_array_len(out, 0);
}
for (member_id, instance) in &req.members {
state.leave_group_members.push(LeaveGroupMemberSeen {
group_id: req.group_id.clone(),
member_id: member_id.clone(),
group_instance_id: instance.clone(),
});
}
if let Some(group) = state.groups.get_mut(&req.group_id) {
group
.members
.retain(|m| !req.members.iter().any(|(id, _)| *id == m.member_id));
if group.members.is_empty() {
group.state = ClassicGroupState::Empty;
group.assignments.clear();
}
}
out.put_i32(0); write_error(out, ErrorCode::None);
write_array_len(out, req.members.len())?;
for (member_id, instance) in &req.members {
write_string(out, member_id)?;
write_nullable_string(out, instance.as_deref())?;
write_error(out, ErrorCode::None);
}
Ok(())
}
fn offset_commit(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = OffsetCommitReq::read(body, api_version)?;
let rejection = coordinator_check(state, &req.group_id, node_id).or_else(|| {
let group = state.groups.get(&req.group_id)?;
if !group.consumer_members.is_empty() {
let Some(member) = group.consumer_members.get(&req.member_id) else {
return Some(ErrorCode::UnknownMemberId);
};
return if api_version < 9 {
Some(ErrorCode::UnsupportedVersion)
} else if req.generation_id < member.member_epoch {
Some(ErrorCode::StaleMemberEpoch)
} else if req.generation_id > member.member_epoch {
Some(ErrorCode::FencedMemberEpoch)
} else {
None
};
}
if req.generation_id >= 0 && group.generation_id != req.generation_id {
Some(ErrorCode::IllegalGeneration)
} else if !req.member_id.is_empty()
&& !group.members.iter().any(|m| m.member_id == req.member_id)
{
Some(ErrorCode::UnknownMemberId)
} else {
None
}
});
if rejection.is_none() {
let group = state.groups.entry(req.group_id.clone()).or_default();
for topic in &req.topics {
for partition in &topic.partitions {
group.offsets.insert(
(topic.name.clone(), partition.partition_index),
CommittedOffset {
offset: partition.committed_offset,
leader_epoch: partition.committed_leader_epoch,
metadata: partition.committed_metadata.clone(),
},
);
}
}
}
write_offset_commit_response(out, api_version, &req, rejection.unwrap_or(ErrorCode::None))
}
fn write_offset_commit_response(
out: &mut BytesMut,
api_version: i16,
req: &OffsetCommitReq,
code: ErrorCode,
) -> Result<()> {
out.put_i32(0); if api_version < 8 {
write_array_len(out, req.topics.len())?;
for topic in &req.topics {
write_string(out, &topic.name)?;
write_array_len(out, topic.partitions.len())?;
for partition in &topic.partitions {
out.put_i32(partition.partition_index);
write_error(out, code);
}
}
return Ok(());
}
write_compact_array_len(out, req.topics.len())?;
for topic in &req.topics {
write_compact_string(out, &topic.name)?;
write_compact_array_len(out, topic.partitions.len())?;
for partition in &topic.partitions {
out.put_i32(partition.partition_index);
write_error(out, code);
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)
}
const NO_COMMITTED_OFFSET: i64 = -1;
fn offset_fetch(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = OffsetFetchReq::read(body)?;
if let Some(code) = coordinator_check(state, &req.group_id, node_id) {
out.put_i32(0);
write_array_len(out, 0)?;
write_error(out, code);
return Ok(());
}
let group = state.groups.entry(req.group_id.clone()).or_default();
let requested: Vec<(String, Vec<i32>)> = match req.topics {
Some(topics) => topics,
None => {
let mut by_topic: std::collections::BTreeMap<String, Vec<i32>> = Default::default();
for (topic, partition) in group.offsets.keys() {
by_topic.entry(topic.clone()).or_default().push(*partition);
}
by_topic
.into_iter()
.map(|(topic, mut parts)| {
parts.sort_unstable();
(topic, parts)
})
.collect()
}
};
out.put_i32(0); write_array_len(out, requested.len())?;
for (name, partitions) in &requested {
write_string(out, name)?;
write_array_len(out, partitions.len())?;
for partition in partitions {
out.put_i32(*partition);
match group.offsets.get(&(name.clone(), *partition)) {
Some(committed) => {
out.put_i64(committed.offset);
out.put_i32(committed.leader_epoch);
write_nullable_string(out, committed.metadata.as_deref())?;
write_error(out, ErrorCode::None);
}
None => {
out.put_i64(NO_COMMITTED_OFFSET);
out.put_i32(-1);
write_nullable_string(out, None)?;
write_error(out, ErrorCode::None);
}
}
}
}
write_error(out, ErrorCode::None); Ok(())
}
fn init_producer_id(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = InitProducerIdReq::read(body, api_version)?;
let mut ongoing = (-1, -1);
let outcome = match &req.transactional_id {
None => Ok(state.allocate_producer_id()),
Some(transactional_id) => {
init_transactional_producer(state, transactional_id, &req, api_version, node_id).map(
|(identity, kept)| {
if let Some(kept) = kept {
ongoing = kept;
}
identity
},
)
}
};
let (code, (producer_id, producer_epoch)) = match outcome {
Ok(identity) => (ErrorCode::None, identity),
Err(code) => (code, (-1, -1)),
};
write_init_producer_id(
out,
api_version,
state.throttle(ApiKey::InitProducerId),
code,
producer_id,
producer_epoch,
ongoing,
)
}
#[allow(clippy::type_complexity)]
fn init_transactional_producer(
state: &mut ClusterState,
transactional_id: &str,
req: &InitProducerIdReq,
api_version: i16,
node_id: i32,
) -> std::result::Result<((i64, i16), Option<(i64, i16)>), ErrorCode> {
if let Some(code) = txn_coordinator_check(state, transactional_id, node_id) {
return Err(code);
}
if req.transaction_timeout_ms <= 0
|| req.transaction_timeout_ms > state.transaction_max_timeout_ms
{
return Err(ErrorCode::InvalidTransactionTimeout);
}
let fenced = fenced_code(ApiKey::InitProducerId, api_version);
let expected = (req.producer_id >= 0 && req.producer_epoch >= 0)
.then_some((req.producer_id, req.producer_epoch));
let Some(txn) = state.transactions.get(transactional_id) else {
if expected.is_some() {
return Err(ErrorCode::InvalidProducerIdMapping);
}
let (producer_id, producer_epoch) = state.allocate_producer_id();
state.transactions.insert(
transactional_id.to_string(),
BrokerTransaction {
producer_id,
producer_epoch,
last_producer_epoch: -1,
transaction_timeout_ms: req.transaction_timeout_ms,
..BrokerTransaction::default()
},
);
return Ok(((producer_id, producer_epoch), None));
};
if let Some((producer_id, _)) = expected
&& producer_id != txn.producer_id
{
return Err(ErrorCode::InvalidProducerIdMapping);
}
match txn.status {
TxnStatus::PrepareCommit | TxnStatus::PrepareAbort => {
return Err(ErrorCode::ConcurrentTransactions);
}
TxnStatus::Ongoing if req.keep_prepared_txn => {
let identity = (txn.producer_id, txn.producer_epoch);
return Ok((identity, Some(identity)));
}
TxnStatus::Ongoing => {
state.fence_transaction(transactional_id);
return Err(ErrorCode::ConcurrentTransactions);
}
_ => {}
}
let Some(txn) = state.transactions.get_mut(transactional_id) else {
return Err(ErrorCode::InvalidProducerIdMapping);
};
match expected {
Some((_, epoch)) if epoch == txn.producer_epoch => txn.bump_epoch(),
Some((_, epoch)) if epoch == txn.last_producer_epoch => {}
Some(_) => return Err(fenced),
None => txn.bump_epoch(),
}
txn.transaction_timeout_ms = req.transaction_timeout_ms;
Ok(((txn.producer_id, txn.producer_epoch), None))
}
fn write_init_producer_id(
out: &mut BytesMut,
api_version: i16,
throttle_time_ms: i32,
code: ErrorCode,
producer_id: i64,
producer_epoch: i16,
ongoing: (i64, i16),
) -> Result<()> {
out.put_i32(throttle_time_ms);
write_error(out, code);
out.put_i64(producer_id);
out.put_i16(producer_epoch);
if api_version >= 6 {
out.put_i64(ongoing.0);
out.put_i16(ongoing.1);
}
if api_version >= 2 {
write_empty_tagged_fields(out)?;
}
Ok(())
}
fn fenced_code(api_key: ApiKey, api_version: i16) -> ErrorCode {
let first = match api_key {
ApiKey::InitProducerId => 4,
ApiKey::AddPartitionsToTxn | ApiKey::AddOffsetsToTxn | ApiKey::EndTxn => 2,
_ => i16::MAX,
};
if api_version >= first {
ErrorCode::ProducerFenced
} else {
ErrorCode::InvalidProducerEpoch
}
}
fn txn_coordinator_check(
state: &ClusterState,
transactional_id: &str,
node_id: i32,
) -> Option<ErrorCode> {
let coordinator = state.txn_coordinator(transactional_id);
if coordinator == node_id {
None
} else if state.broker(coordinator).map(|b| b.online) == Some(true) {
Some(ErrorCode::NotCoordinator)
} else {
Some(ErrorCode::CoordinatorNotAvailable)
}
}
fn txn_check(
state: &ClusterState,
api_key: ApiKey,
api_version: i16,
transactional_id: &str,
producer_id: i64,
producer_epoch: i16,
node_id: i32,
) -> Option<ErrorCode> {
if let Some(code) = txn_coordinator_check(state, transactional_id, node_id) {
return Some(code);
}
match state.transactions.get(transactional_id) {
None => Some(ErrorCode::InvalidProducerIdMapping),
Some(txn) if txn.producer_id != producer_id => Some(ErrorCode::InvalidProducerIdMapping),
Some(txn) if txn.producer_epoch != producer_epoch => {
Some(fenced_code(api_key, api_version))
}
Some(txn)
if matches!(
txn.status,
TxnStatus::PrepareCommit | TxnStatus::PrepareAbort
) =>
{
Some(ErrorCode::ConcurrentTransactions)
}
Some(_) => None,
}
}
fn add_partitions_to_txn(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = AddPartitionsToTxnReq::read(body)?;
let rejection = txn_check(
state,
ApiKey::AddPartitionsToTxn,
api_version,
&req.transactional_id,
req.producer_id,
req.producer_epoch,
node_id,
);
if rejection.is_none()
&& let Some(txn) = state.transactions.get_mut(&req.transactional_id)
{
txn.begin();
for entry in &req.partitions {
if !txn.partitions.contains(entry) {
txn.partitions.push(entry.clone());
}
}
}
let mut by_topic: Vec<(String, Vec<i32>)> = Vec::new();
for (topic, partition) in &req.partitions {
match by_topic.iter_mut().find(|(name, _)| name == topic) {
Some((_, partitions)) => partitions.push(*partition),
None => by_topic.push((topic.clone(), vec![*partition])),
}
}
out.put_i32(0); write_array_len(out, by_topic.len())?;
for (topic, partitions) in &by_topic {
write_string(out, topic)?;
write_array_len(out, partitions.len())?;
for partition in partitions {
out.put_i32(*partition);
write_error(out, rejection.unwrap_or(ErrorCode::None));
}
}
Ok(())
}
fn add_offsets_to_txn(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = AddOffsetsToTxnReq::read(body)?;
let rejection = txn_check(
state,
ApiKey::AddOffsetsToTxn,
api_version,
&req.transactional_id,
req.producer_id,
req.producer_epoch,
node_id,
);
if rejection.is_none()
&& let Some(txn) = state.transactions.get_mut(&req.transactional_id)
{
txn.begin();
txn.staged_offsets.entry(req.group_id).or_default();
}
out.put_i32(0); write_error(out, rejection.unwrap_or(ErrorCode::None));
Ok(())
}
fn txn_offset_commit(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = TxnOffsetCommitReq::read(body)?;
let rejection = coordinator_check(state, &req.group_id, node_id).or_else(|| {
match state.transactions.get(&req.transactional_id) {
None => Some(ErrorCode::InvalidProducerIdMapping),
Some(txn) if txn.producer_id != req.producer_id => {
Some(ErrorCode::InvalidProducerIdMapping)
}
Some(txn) if txn.producer_epoch != req.producer_epoch => {
Some(ErrorCode::InvalidProducerEpoch)
}
Some(_) => state.groups.get(&req.group_id).and_then(|group| {
if req.generation_id >= 0 && group.generation_id != req.generation_id {
Some(ErrorCode::IllegalGeneration)
} else if !req.member_id.is_empty()
&& !group.members.is_empty()
&& !group.members.iter().any(|m| {
m.member_id == req.member_id
|| (req.group_instance_id.is_some()
&& m.group_instance_id == req.group_instance_id)
})
{
Some(ErrorCode::UnknownMemberId)
} else {
None
}
}),
}
});
if rejection.is_none()
&& let Some(txn) = state.transactions.get_mut(&req.transactional_id)
{
txn.begin();
let staged = txn.staged_offsets.entry(req.group_id.clone()).or_default();
for offset in &req.offsets {
staged.insert(
(offset.topic.clone(), offset.partition),
CommittedOffset {
offset: offset.committed_offset,
leader_epoch: offset.committed_leader_epoch,
metadata: offset.metadata.clone(),
},
);
}
}
let mut by_topic: Vec<(String, Vec<i32>)> = Vec::new();
for offset in &req.offsets {
match by_topic.iter_mut().find(|(name, _)| *name == offset.topic) {
Some((_, partitions)) => partitions.push(offset.partition),
None => by_topic.push((offset.topic.clone(), vec![offset.partition])),
}
}
out.put_i32(0); write_compact_array_len(out, by_topic.len())?;
for (topic, partitions) in &by_topic {
KafkaString::new(topic).try_encode_compact(out)?;
write_compact_array_len(out, partitions.len())?;
for partition in partitions {
out.put_i32(*partition);
write_error(out, rejection.unwrap_or(ErrorCode::None));
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)
}
fn end_txn(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = EndTxnReq::read(body)?;
let outcome = end_txn_outcome(state, &req, api_version, node_id);
let (producer_id, producer_epoch) =
match (&outcome, state.transactions.get(&req.transactional_id)) {
(None, Some(txn)) => (txn.producer_id, txn.producer_epoch),
_ => (-1, -1),
};
out.put_i32(state.throttle(ApiKey::EndTxn));
write_error(out, outcome.unwrap_or(ErrorCode::None));
if api_version >= 5 {
out.put_i64(producer_id);
out.put_i16(producer_epoch);
}
write_empty_tagged_fields(out)
}
fn end_txn_outcome(
state: &mut ClusterState,
req: &EndTxnReq,
api_version: i16,
node_id: i32,
) -> Option<ErrorCode> {
if let Some(code) = txn_coordinator_check(state, &req.transactional_id, node_id) {
return Some(code);
}
let Some(txn) = state.transactions.get_mut(&req.transactional_id) else {
return Some(ErrorCode::InvalidProducerIdMapping);
};
if txn.producer_id != req.producer_id {
return Some(ErrorCode::InvalidProducerIdMapping);
}
let tv2 = api_version >= 5;
let current = req.producer_epoch == txn.producer_epoch;
let retry =
tv2 && txn.last_producer_epoch >= 0 && req.producer_epoch == txn.last_producer_epoch;
let same_outcome = |committed: bool| committed == req.committed;
let status = txn.status;
match status {
TxnStatus::Ongoing if current => {
state.end_transaction(&req.transactional_id, req.committed, tv2);
None
}
TxnStatus::PrepareCommit | TxnStatus::PrepareAbort if (current && !tv2) || retry => {
if same_outcome(status == TxnStatus::PrepareCommit) {
Some(ErrorCode::ConcurrentTransactions)
} else {
Some(ErrorCode::InvalidTxnState)
}
}
TxnStatus::CompleteCommit | TxnStatus::CompleteAbort if (current && !tv2) || retry => {
if same_outcome(status == TxnStatus::CompleteCommit) {
None
} else {
Some(ErrorCode::InvalidTxnState)
}
}
TxnStatus::Empty | TxnStatus::CompleteCommit | TxnStatus::CompleteAbort
if current && tv2 && !req.committed =>
{
if let Some(txn) = state.transactions.get_mut(&req.transactional_id) {
txn.bump_epoch();
txn.status = TxnStatus::CompleteAbort;
}
None
}
_ if current => Some(ErrorCode::InvalidTxnState),
_ => Some(fenced_code(ApiKey::EndTxn, api_version)),
}
}
fn create_topics(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = CreateTopicsReq::read(body)?;
if state.controller_id != node_id {
out.put_i32(0);
write_array_len(out, req.topics.len())?;
for topic in &req.topics {
write_string(out, &topic.name)?;
write_error(out, ErrorCode::NotController);
write_nullable_string(out, Some("this broker is not the controller"))?;
}
return Ok(());
}
out.put_i32(0); write_array_len(out, req.topics.len())?;
for topic in &req.topics {
let partitions = if topic.num_partitions > 0 {
topic.num_partitions
} else {
state.default_partitions
};
let (code, message) = if state.topics.contains_key(&topic.name) {
(ErrorCode::TopicAlreadyExists, Some("topic already exists"))
} else {
if !req.validate_only {
state.create_topic(&topic.name, partitions);
}
(ErrorCode::None, None)
};
write_string(out, &topic.name)?;
write_error(out, code);
write_nullable_string(out, message)?;
}
Ok(())
}
fn delete_topics(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = DeleteTopicsReq::read(body)?;
if state.controller_id != node_id {
out.put_i32(0);
write_array_len(out, req.topic_names.len())?;
for name in &req.topic_names {
write_nullable_string(out, Some(name))?;
write_error(out, ErrorCode::NotController);
}
return Ok(());
}
out.put_i32(0); write_array_len(out, req.topic_names.len())?;
for name in &req.topic_names {
let code = if state.delete_topic(name) {
ErrorCode::None
} else {
ErrorCode::UnknownTopicOrPartition
};
write_nullable_string(out, Some(name))?;
write_error(out, code);
}
Ok(())
}
fn share_group_heartbeat(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = ShareGroupHeartbeatReq::read(body)?;
if state.group_coordinator(&req.group_id) != node_id {
return write_heartbeat_error(out, ErrorCode::NotCoordinator, None, 0);
}
if req.member_id.is_empty() {
return write_heartbeat_error(out, ErrorCode::InvalidRequest, None, 0);
}
if req.member_epoch < 0 {
if let Some(group) = state.share_groups.get_mut(&req.group_id) {
group.members.remove(&req.member_id);
group.group_epoch += 1;
group.release_member(&req.member_id);
}
return write_share_heartbeat(out, &req.member_id, req.member_epoch, None);
}
if req.member_epoch == 0 && req.subscribed_topic_names.is_none() {
return write_heartbeat_error(out, ErrorCode::InvalidRequest, Some(&req.member_id), 0);
}
let partition_counts: HashMap<String, i32> = state
.topics
.iter()
.map(|(name, t)| (name.clone(), t.partitions.len() as i32))
.collect();
let topic_ids: HashMap<String, [u8; 16]> = state
.topics
.iter()
.map(|(name, t)| (name.clone(), t.topic_id))
.collect();
let group = state.share_groups.entry(req.group_id.clone()).or_default();
let known = group.members.get(&req.member_id).cloned();
if let Some(existing) = &known
&& req.member_epoch != 0
&& req.member_epoch != existing.member_epoch
{
return write_heartbeat_error(out, ErrorCode::FencedMemberEpoch, Some(&req.member_id), 0);
}
if known.is_none() && req.member_epoch != 0 {
return write_heartbeat_error(out, ErrorCode::UnknownMemberId, Some(&req.member_id), 0);
}
let subscribed = req
.subscribed_topic_names
.clone()
.or_else(|| known.as_ref().map(|m| m.subscribed_topics.clone()))
.unwrap_or_default();
let is_new = known.is_none();
let rejoining = known.is_some() && req.member_epoch == 0;
let subscription_changed = known
.as_ref()
.is_some_and(|m| m.subscribed_topics != subscribed);
if is_new || rejoining || subscription_changed {
group.group_epoch += 1;
}
let group_epoch = group.group_epoch;
{
let member = group.members.entry(req.member_id.clone()).or_default();
member.subscribed_topics = subscribed.clone();
}
let mut member_ids: Vec<String> = group.members.keys().cloned().collect();
member_ids.sort();
let mut targets: HashMap<String, HashMap<String, Vec<i32>>> = HashMap::new();
let mut topics: Vec<&String> = partition_counts.keys().collect();
topics.sort();
for topic in topics {
let subscribers: Vec<&String> = member_ids
.iter()
.filter(|id| {
group
.members
.get(*id)
.is_some_and(|m| m.subscribed_topics.contains(topic))
})
.collect();
if subscribers.is_empty() {
continue;
}
let count = partition_counts.get(topic).copied().unwrap_or(0);
for partition in 0..count {
let owner = subscribers[(partition as usize) % subscribers.len()];
targets
.entry(owner.clone())
.or_default()
.entry(topic.clone())
.or_default()
.push(partition);
}
}
let granted = targets.remove(&req.member_id).unwrap_or_default();
let (member_epoch, send_assignment) = {
let member = group.members.entry(req.member_id.clone()).or_default();
if member.assignment != granted {
member.assignment = granted.clone();
member.assignment_dirty = true;
}
if is_new || rejoining || subscription_changed || member.member_epoch == 0 {
member.member_epoch = group_epoch;
}
let dirty = member.assignment_dirty || is_new || rejoining;
member.assignment_dirty = false;
(member.member_epoch, dirty)
};
let wire_assignment: Vec<HeartbeatTopicPartitions> = granted
.iter()
.filter_map(|(topic, partitions)| {
topic_ids.get(topic).map(|id| HeartbeatTopicPartitions {
topic_id: *id,
partitions: partitions.clone(),
})
})
.collect();
write_share_heartbeat(
out,
&req.member_id,
member_epoch,
if send_assignment {
Some(&wire_assignment)
} else {
None
},
)
}
fn write_share_heartbeat(
out: &mut BytesMut,
member_id: &str,
member_epoch: i32,
assignment: Option<&[HeartbeatTopicPartitions]>,
) -> Result<()> {
out.put_i32(0); write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?; write_compact_nullable_string(out, Some(member_id))?;
out.put_i32(member_epoch);
out.put_i32(HEARTBEAT_INTERVAL_MS);
write_heartbeat_assignment(out, assignment)?;
write_empty_tagged_fields(out)
}
fn topic_name_for_id(state: &ClusterState, topic_id: [u8; 16]) -> Option<String> {
state
.topics
.iter()
.find(|(_, t)| t.topic_id == topic_id)
.map(|(name, _)| name.clone())
}
#[allow(clippy::too_many_arguments)]
fn apply_share_acks(
state: &mut ClusterState,
api_version: i16,
group_id: &str,
member_id: &str,
topic: &str,
partition: i32,
batches: &[ShareAckBatch],
) -> ErrorCode {
let mut plan: Vec<(i64, i8)> = Vec::new();
for batch in batches {
if batch.last_offset < batch.first_offset {
return ErrorCode::InvalidRequest;
}
let span = batch.last_offset - batch.first_offset + 1;
let types: Vec<i8> = match batch.acknowledge_types.len() {
1 => vec![batch.acknowledge_types[0]; span as usize],
n if n as i64 == span => batch.acknowledge_types.clone(),
_ => return ErrorCode::InvalidRequest,
};
for (i, &ack_type) in types.iter().enumerate() {
if !(0..=4).contains(&ack_type) || (ack_type == 4 && api_version < 2) {
return ErrorCode::InvalidRequest;
}
plan.push((batch.first_offset + i as i64, ack_type));
}
}
if plan.is_empty() {
return ErrorCode::None;
}
let share_partition = state
.share_groups
.entry(group_id.to_string())
.or_default()
.partitions
.entry((topic.to_string(), partition))
.or_default();
if !plan
.iter()
.all(|&(offset, _)| share_partition.held_by(offset, offset, member_id))
{
return ErrorCode::InvalidRecordState;
}
for (offset, ack_type) in plan {
share_partition.acknowledge(offset, ack_type);
}
ErrorCode::None
}
fn required_share_identity(
group_id: &Option<String>,
member_id: &Option<String>,
) -> Option<(String, String)> {
let group = group_id.as_deref().filter(|g| !g.is_empty())?;
let member = member_id.as_deref().filter(|m| !m.is_empty())?;
Some((group.to_string(), member.to_string()))
}
fn record_share_session_close(
state: &mut ClusterState,
api_key: ApiKey,
node_id: i32,
group_id: &str,
member_id: &str,
share_session_epoch: i32,
) {
if share_session_epoch == -1 {
state.share_session_closes.push(ShareSessionClose {
api_key,
node_id,
group_id: group_id.to_string(),
member_id: member_id.to_string(),
});
}
}
#[allow(clippy::too_many_arguments)]
fn check_share_session(
state: &mut ClusterState,
api_key: ApiKey,
node_id: i32,
group_id: &str,
member_id: &str,
epoch: i32,
has_acks: bool,
requested: &[(String, i32)],
forgotten: &[(String, i32)],
) -> Option<ErrorCode> {
let key = (node_id, group_id.to_string(), member_id.to_string());
if epoch == 0 {
if api_key == ApiKey::ShareAcknowledge {
return Some(ErrorCode::InvalidShareSessionEpoch);
}
if has_acks {
return Some(ErrorCode::InvalidRequest);
}
state.share_sessions.insert(
key,
ShareSession {
epoch: 1,
partitions: requested.iter().cloned().collect(),
},
);
return None;
}
let Some(session) = state.share_sessions.get_mut(&key) else {
return Some(ErrorCode::ShareSessionNotFound);
};
if epoch == -1 {
state.share_sessions.remove(&key);
return None;
}
if epoch != session.epoch {
return Some(ErrorCode::InvalidShareSessionEpoch);
}
session.advance();
session.partitions.extend(requested.iter().cloned());
for entry in forgotten {
session.partitions.remove(entry);
}
None
}
fn write_share_fetch_error(out: &mut BytesMut, code: ErrorCode, message: &str) -> Result<()> {
out.put_i32(0); write_error(out, code);
write_compact_nullable_string(out, Some(message))?;
out.put_i32(ACQUISITION_LOCK_TIMEOUT_MS);
write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_empty_tagged_fields(out)
}
fn write_share_acknowledge_error(
out: &mut BytesMut,
api_version: i16,
code: ErrorCode,
message: &str,
) -> Result<()> {
out.put_i32(0); write_error(out, code);
write_compact_nullable_string(out, Some(message))?;
if api_version >= 2 {
out.put_i32(ACQUISITION_LOCK_TIMEOUT_MS);
}
write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_empty_tagged_fields(out)
}
fn share_partition_names(state: &ClusterState, ids: &[([u8; 16], i32)]) -> Vec<(String, i32)> {
ids.iter()
.filter_map(|(topic_id, partition)| {
topic_name_for_id(state, *topic_id).map(|name| (name, *partition))
})
.collect()
}
fn acquire_share_records(
state: &mut ClusterState,
group_id: &str,
member_id: &str,
topic: &str,
partition: i32,
budget: &mut i64,
record_limit: bool,
) -> (Bytes, Vec<(i64, i64, i16)>) {
let Some(log) = state.partition(topic, partition).map(|p| p.log.clone()) else {
return (Bytes::new(), Vec::new());
};
let share_partition = state
.share_groups
.entry(group_id.to_string())
.or_default()
.partitions
.entry((topic.to_string(), partition))
.or_default();
let mut records = Vec::new();
let mut acquired: Vec<(i64, i16)> = Vec::new();
for batch in &log {
if *budget <= 0 {
break;
}
let base = batch_base_offset(batch).unwrap_or(0);
let count = batch_record_count(batch).unwrap_or(0);
let mut taken = 0;
for offset in base..base + count {
if record_limit && taken >= *budget {
break;
}
if share_partition.is_available(offset) {
acquired.push((offset, share_partition.acquire(offset, member_id)));
taken += 1;
}
}
if taken > 0 {
records.extend_from_slice(batch);
*budget -= taken;
}
}
let mut ranges: Vec<(i64, i64, i16)> = Vec::new();
for (offset, delivery_count) in acquired {
match ranges.last_mut() {
Some((_, last, count)) if *last + 1 == offset && *count == delivery_count => {
*last = offset;
}
_ => ranges.push((offset, offset, delivery_count)),
}
}
(Bytes::from(records), ranges)
}
fn share_fetch(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
poll: &mut LongPoll,
out: &mut BytesMut,
) -> Result<Served> {
let req = ShareFetchReq::read(body, api_version)?;
let Some((group_id, member_id)) = required_share_identity(&req.group_id, &req.member_id) else {
write_share_fetch_error(
out,
ErrorCode::InvalidRequest,
"ShareFetch requires a group ID and member ID",
)?;
return Ok(Served::Done);
};
let ack_errors = match poll.share_acks.take() {
Some(acks) => acks,
None => {
record_share_session_close(
state,
ApiKey::ShareFetch,
node_id,
&group_id,
&member_id,
req.share_session_epoch,
);
let requested: Vec<([u8; 16], i32)> = req
.topics
.iter()
.map(|tp| (tp.topic_id, tp.partition_index))
.collect();
let requested = share_partition_names(state, &requested);
let forgotten = share_partition_names(state, &req.forgotten);
let has_acks = req
.topics
.iter()
.any(|tp| !tp.acknowledgement_batches.is_empty());
if let Some(code) = check_share_session(
state,
ApiKey::ShareFetch,
node_id,
&group_id,
&member_id,
req.share_session_epoch,
has_acks,
&requested,
&forgotten,
) {
write_share_fetch_error(out, code, &format!("{code:?}"))?;
return Ok(Served::Done);
}
let mut acks = HashMap::new();
for entry in &req.topics {
let Some(topic) = topic_name_for_id(state, entry.topic_id) else {
continue;
};
if state
.partition(&topic, entry.partition_index)
.is_none_or(|p| p.leader != node_id)
{
continue;
}
let code = apply_share_acks(
state,
api_version,
&group_id,
&member_id,
&topic,
entry.partition_index,
&entry.acknowledgement_batches,
);
acks.insert((entry.topic_id, entry.partition_index), code);
}
if req.share_session_epoch == -1
&& let Some(group) = state.share_groups.get_mut(&group_id)
{
group.release_member(&member_id);
}
acks
}
};
let fetching = req.share_session_epoch != -1 && !req.is_renew_ack;
let mut targets: Vec<([u8; 16], i32, bool)> = req
.topics
.iter()
.map(|tp| (tp.topic_id, tp.partition_index, true))
.collect();
let session_key = (node_id, group_id.clone(), member_id.clone());
if fetching && let Some(session) = state.share_sessions.get(&session_key) {
for (topic, partition) in &session.partitions {
let Some(topic_id) = state.topics.get(topic).map(|t| t.topic_id) else {
continue;
};
if !targets
.iter()
.any(|(id, p, _)| *id == topic_id && p == partition)
{
targets.push((topic_id, *partition, false));
}
}
}
let mut budget = if req.max_records > 0 {
i64::from(req.max_records)
} else {
i64::MAX
};
let record_limit = req.share_acquire_mode == 1;
let mut any_error = false;
let mut any_acquired = false;
type ShareFetchEntry = (
[u8; 16],
i32,
ErrorCode,
ErrorCode,
i32,
i32,
Bytes,
Vec<(i64, i64, i16)>,
);
let mut entries: Vec<ShareFetchEntry> = Vec::new();
for (topic_id, partition, requested) in targets {
let ack_error = ack_errors
.get(&(topic_id, partition))
.copied()
.unwrap_or(ErrorCode::None);
let Some(topic) = topic_name_for_id(state, topic_id) else {
any_error = true;
entries.push((
topic_id,
partition,
ErrorCode::UnknownTopicId,
ack_error,
-1,
-1,
Bytes::new(),
Vec::new(),
));
continue;
};
let Some((leader, leader_epoch)) = state
.partition(&topic, partition)
.map(|p| (p.leader, p.leader_epoch))
else {
any_error = true;
entries.push((
topic_id,
partition,
ErrorCode::UnknownTopicOrPartition,
ack_error,
-1,
-1,
Bytes::new(),
Vec::new(),
));
continue;
};
if leader != node_id {
any_error = true;
entries.push((
topic_id,
partition,
ErrorCode::NotLeaderForPartition,
ack_error,
leader,
leader_epoch,
Bytes::new(),
Vec::new(),
));
continue;
}
let (records, acquired) = if fetching {
acquire_share_records(
state,
&group_id,
&member_id,
&topic,
partition,
&mut budget,
record_limit,
)
} else {
(Bytes::new(), Vec::new())
};
any_acquired |= !acquired.is_empty();
if requested || !acquired.is_empty() {
entries.push((
topic_id,
partition,
ErrorCode::None,
ack_error,
leader,
leader_epoch,
records,
acquired,
));
}
}
if fetching && !poll.expired && !any_error && !any_acquired && req.max_wait_ms > 0 {
poll.share_acks = Some(ack_errors);
return Ok(Served::Wait(Duration::from_millis(req.max_wait_ms as u64)));
}
out.put_i32(state.throttle(ApiKey::ShareFetch));
write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?; out.put_i32(ACQUISITION_LOCK_TIMEOUT_MS);
let mut order: Vec<[u8; 16]> = Vec::new();
for entry in &entries {
if !order.contains(&entry.0) {
order.push(entry.0);
}
}
write_compact_array_len(out, order.len())?;
for topic_id in &order {
out.put_slice(topic_id);
let topic_entries: Vec<&ShareFetchEntry> =
entries.iter().filter(|e| e.0 == *topic_id).collect();
write_compact_array_len(out, topic_entries.len())?;
for (_, partition, error, ack_error, leader, epoch, records, acquired) in topic_entries {
write_share_fetch_partition(
out,
*partition,
*error,
*ack_error,
*leader,
*epoch,
if records.is_empty() {
None
} else {
Some(records)
},
acquired,
)?;
}
write_empty_tagged_fields(out)?; }
write_compact_array_len(out, 0)?; write_empty_tagged_fields(out)?;
Ok(Served::Done)
}
#[allow(clippy::too_many_arguments)]
fn write_share_fetch_partition(
out: &mut BytesMut,
partition: i32,
error: ErrorCode,
ack_error: ErrorCode,
leader_id: i32,
leader_epoch: i32,
records: Option<&Bytes>,
acquired: &[(i64, i64, i16)],
) -> Result<()> {
out.put_i32(partition);
write_error(out, error);
write_compact_nullable_string(out, None)?; write_error(out, ack_error);
write_compact_nullable_string(out, None)?; out.put_i32(leader_id);
out.put_i32(leader_epoch);
write_empty_tagged_fields(out)?; write_compact_nullable_bytes(out, records)?;
write_compact_array_len(out, acquired.len())?;
for &(first, last, delivery_count) in acquired {
out.put_i64(first);
out.put_i64(last);
out.put_i16(delivery_count);
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out) }
fn share_acknowledge(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = ShareAcknowledgeReq::read(body, api_version)?;
let Some((group_id, member_id)) = required_share_identity(&req.group_id, &req.member_id) else {
return write_share_acknowledge_error(
out,
api_version,
ErrorCode::InvalidRequest,
"ShareAcknowledge requires a group ID and member ID",
);
};
record_share_session_close(
state,
ApiKey::ShareAcknowledge,
node_id,
&group_id,
&member_id,
req.share_session_epoch,
);
if let Some(code) = check_share_session(
state,
ApiKey::ShareAcknowledge,
node_id,
&group_id,
&member_id,
req.share_session_epoch,
true,
&[],
&[],
) {
return write_share_acknowledge_error(out, api_version, code, &format!("{code:?}"));
}
out.put_i32(state.throttle(ApiKey::ShareAcknowledge));
write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?; if api_version >= 2 {
out.put_i32(ACQUISITION_LOCK_TIMEOUT_MS);
}
let mut order: Vec<[u8; 16]> = Vec::new();
let mut grouped: HashMap<[u8; 16], Vec<&ShareTopicPartitionAcks>> = HashMap::new();
for tp in &req.topics {
if !grouped.contains_key(&tp.topic_id) {
order.push(tp.topic_id);
}
grouped.entry(tp.topic_id).or_default().push(tp);
}
write_compact_array_len(out, order.len())?;
for topic_id in &order {
out.put_slice(topic_id);
let entries = grouped.get(topic_id).map_or(&[][..], Vec::as_slice);
write_compact_array_len(out, entries.len())?;
for entry in entries {
let (error, leader, epoch) = match topic_name_for_id(state, *topic_id) {
None => (ErrorCode::UnknownTopicId, -1, -1),
Some(topic) => match state.partition(&topic, entry.partition_index) {
None => (ErrorCode::UnknownTopicOrPartition, -1, -1),
Some(p) if p.leader != node_id => {
(ErrorCode::NotLeaderForPartition, p.leader, p.leader_epoch)
}
Some(p) => {
let (leader, epoch) = (p.leader, p.leader_epoch);
let code = apply_share_acks(
state,
api_version,
&group_id,
&member_id,
&topic,
entry.partition_index,
&entry.acknowledgement_batches,
);
(code, leader, epoch)
}
},
};
out.put_i32(entry.partition_index);
write_error(out, error);
write_compact_nullable_string(out, None)?; out.put_i32(leader);
out.put_i32(epoch);
write_empty_tagged_fields(out)?; write_empty_tagged_fields(out)?; }
write_empty_tagged_fields(out)?; }
if req.share_session_epoch == -1
&& let Some(group) = state.share_groups.get_mut(&group_id)
{
group.release_member(&member_id);
}
write_compact_array_len(out, 0)?; write_empty_tagged_fields(out)
}
fn share_partition_scoped(code: ErrorCode) -> bool {
matches!(
code,
ErrorCode::NotLeaderForPartition
| ErrorCode::UnknownTopicOrPartition
| ErrorCode::UnknownTopicId
| ErrorCode::FencedLeaderEpoch
| ErrorCode::UnknownLeaderEpoch
| ErrorCode::InvalidRecordState
)
}
fn write_share_partitions(
out: &mut BytesMut,
topics: &[ShareTopicPartitionAcks],
mut write_partition: impl FnMut(&mut BytesMut, i32) -> Result<()>,
) -> Result<()> {
let mut order: Vec<[u8; 16]> = Vec::new();
for tp in topics {
if !order.contains(&tp.topic_id) {
order.push(tp.topic_id);
}
}
write_compact_array_len(out, order.len())?;
for topic_id in &order {
out.put_slice(topic_id);
let partitions: Vec<i32> = topics
.iter()
.filter(|tp| tp.topic_id == *topic_id)
.map(|tp| tp.partition_index)
.collect();
write_compact_array_len(out, partitions.len())?;
for partition in partitions {
write_partition(out, partition)?;
}
write_empty_tagged_fields(out)?;
}
Ok(())
}
fn update_features(
body: &mut Bytes,
api_version: i16,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = UpdateFeaturesReq::read(body, api_version)?;
let write_response = |out: &mut BytesMut, code: ErrorCode, results: &[(String, ErrorCode)]| {
out.put_i32(0); write_error(out, code);
write_compact_nullable_string(out, None)?; if api_version < 2 {
write_compact_array_len(out, results.len())?;
for (feature, result) in results {
write_compact_string(out, feature)?;
write_error(out, *result);
write_compact_nullable_string(out, None)?;
write_empty_tagged_fields(out)?;
}
}
write_empty_tagged_fields(out)
};
if state.controller_id != node_id {
return write_response(out, ErrorCode::NotController, &[]);
}
let results: Vec<(String, ErrorCode)> = req
.feature_updates
.iter()
.map(|u| (u.feature.clone(), ErrorCode::None))
.collect();
if !req.validate_only && !req.feature_updates.is_empty() {
for update in &req.feature_updates {
if update.max_version_level == 0 {
state.finalized_features.remove(&update.feature);
} else {
state
.finalized_features
.insert(update.feature.clone(), update.max_version_level);
}
}
state.finalized_features_epoch += 1;
}
write_response(out, ErrorCode::None, &results)
}
fn streams_group_describe(
body: &mut Bytes,
node_id: i32,
state: &mut ClusterState,
out: &mut BytesMut,
) -> Result<()> {
let req = StreamsGroupDescribeReq::read(body)?;
out.put_i32(0); write_compact_array_len(out, req.group_ids.len())?;
for group_id in &req.group_ids {
if state.group_coordinator(group_id) != node_id {
write_error(out, ErrorCode::NotCoordinator);
write_compact_nullable_string(out, None)?;
write_compact_string(out, group_id)?;
write_compact_string(out, "")?; out.put_i32(0); out.put_i32(0); write_presence(out, false); write_compact_array_len(out, 0)?; out.put_i32(i32::MIN); write_empty_tagged_fields(out)?;
continue;
}
let Some(group) = state.streams_groups.get(group_id) else {
write_error(out, ErrorCode::GroupIdNotFound);
write_compact_nullable_string(out, Some("group not found"))?;
write_compact_string(out, group_id)?;
write_compact_string(out, "")?;
out.put_i32(0);
out.put_i32(0);
write_presence(out, false);
write_compact_array_len(out, 0)?;
out.put_i32(i32::MIN);
write_empty_tagged_fields(out)?;
continue;
};
write_error(out, ErrorCode::None);
write_compact_nullable_string(out, None)?; write_compact_string(out, group_id)?;
write_compact_string(out, &group.group_state)?;
out.put_i32(group.group_epoch);
out.put_i32(group.assignment_epoch);
match group.topology_epoch {
None => write_presence(out, false),
Some(epoch) => {
write_presence(out, true);
out.put_i32(epoch);
match &group.subtopologies {
None => crate::util::varint::encode_unsigned_varint(0, out),
Some(subs) => {
write_compact_array_len(out, subs.len())?;
for id in subs {
write_compact_string(out, id)?;
write_compact_array_len(out, 1)?; write_compact_string(out, "source-topic")?;
write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_empty_tagged_fields(out)?;
}
}
}
write_empty_tagged_fields(out)?; }
}
write_compact_array_len(out, group.members.len())?;
for member in &group.members {
write_compact_string(out, &member.member_id)?;
out.put_i32(member.member_epoch);
write_compact_nullable_string(out, None)?; write_compact_nullable_string(out, None)?; write_compact_string(out, "krafka-test")?; write_compact_string(out, "127.0.0.1")?; out.put_i32(member.topology_epoch);
write_compact_string(out, &member.process_id)?;
match &member.user_endpoint {
None => write_presence(out, false),
Some((host, port)) => {
write_presence(out, true);
write_compact_string(out, host)?;
out.put_u16(*port);
write_empty_tagged_fields(out)?;
}
}
write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_streams_assignment(out, &member.active_tasks)?;
write_streams_assignment(out, &member.target_active_tasks)?;
out.put_u8(0); write_empty_tagged_fields(out)?;
}
out.put_i32(if req.include_authorized_operations {
0
} else {
i32::MIN
});
write_empty_tagged_fields(out)?;
}
write_empty_tagged_fields(out)
}
fn write_streams_assignment(out: &mut BytesMut, active: &[(String, Vec<i32>)]) -> Result<()> {
write_compact_array_len(out, active.len())?;
for (subtopology_id, partitions) in active {
write_compact_string(out, subtopology_id)?;
write_compact_array_len(out, partitions.len())?;
for p in partitions {
out.put_i32(*p);
}
write_empty_tagged_fields(out)?;
}
write_compact_array_len(out, 0)?; write_compact_array_len(out, 0)?; write_empty_tagged_fields(out) }
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::protocol::{MetadataResponse, VersionedDecode};
use bytes::Buf;
#[test]
fn each_api_is_advertised_exactly_once() {
let versions = supported_versions();
let mut keys: Vec<i16> = versions.iter().map(|(k, _, _)| k.to_i16()).collect();
keys.sort_unstable();
let unique = {
let mut u = keys.clone();
u.dedup();
u
};
assert_eq!(keys, unique, "an API is advertised more than once");
assert!(
versions.iter().any(|(k, _, _)| *k == ApiKey::ApiVersions),
"ApiVersions must be advertised or no client can complete a handshake"
);
}
#[test]
fn metadata_response_round_trips_through_the_client_decoder() {
let mut state = ClusterState::new(2);
state.brokers[0].port = 9092;
state.brokers[1].port = 9093;
state.controller_id = 1;
state.create_topic("orders", 2);
let mut body = BytesMut::new();
write_compact_array_len(&mut body, 1).unwrap();
body.put_slice(&[0u8; 16]); write_compact_nullable_string(&mut body, Some("orders")).unwrap();
write_empty_tagged_fields(&mut body).unwrap();
body.put_u8(0); body.put_u8(0); write_empty_tagged_fields(&mut body).unwrap();
let mut body = body.freeze();
let mut out = BytesMut::new();
metadata(&mut body, &mut state, &mut out).unwrap();
let mut encoded = out.freeze();
let decoded = MetadataResponse::decode_versioned(12, &mut encoded).unwrap();
assert_eq!(encoded.remaining(), 0, "writer emitted trailing bytes");
assert_eq!(decoded.controller_id, 1);
assert_eq!(decoded.brokers.len(), 2);
assert_eq!(decoded.cluster_id.as_deref(), Some("krafka-fake-cluster"));
let topic = decoded.find_topic("orders").unwrap();
assert_eq!(topic.partitions.len(), 2);
assert_eq!(topic.error_code, ErrorCode::None);
assert!(
topic.topic_id.is_some_and(|id| id != [0u8; 16]),
"v12 must carry a real topic UUID"
);
}
#[test]
fn synthesized_metadata_error_still_decodes() {
let mut body = BytesMut::new();
write_compact_array_len(&mut body, 1).unwrap();
body.put_slice(&[0u8; 16]);
write_compact_nullable_string(&mut body, Some("orders")).unwrap();
write_empty_tagged_fields(&mut body).unwrap();
body.put_u8(0);
body.put_u8(0);
write_empty_tagged_fields(&mut body).unwrap();
let mut body = body.freeze();
let mut out = BytesMut::new();
dispatch_error(
ApiKey::Metadata,
12,
&mut body,
ErrorCode::NotController,
&mut out,
)
.unwrap();
let mut encoded = out.freeze();
let decoded = MetadataResponse::decode_versioned(12, &mut encoded).unwrap();
assert_eq!(encoded.remaining(), 0);
assert_eq!(decoded.topics[0].error_code, ErrorCode::NotController);
}
}