use std::collections::HashMap;
use bytes::Bytes;
use super::wire;
use crate::protocol::ApiKey;
#[derive(Debug, Clone)]
pub struct BrokerNode {
pub node_id: i32,
pub host: String,
pub port: i32,
pub rack: Option<String>,
pub online: bool,
}
#[derive(Debug, Clone)]
pub struct PartitionState {
pub leader: i32,
pub leader_epoch: i32,
pub replicas: Vec<i32>,
pub isr: Vec<i32>,
pub log: Vec<Bytes>,
pub log_start_offset: i64,
pub next_offset: i64,
pub pending_txn_first_offset: Option<i64>,
pub aborted_transactions: Vec<(i64, i64, i64)>,
}
impl PartitionState {
fn new(leader: i32) -> Self {
Self {
leader,
leader_epoch: 0,
replicas: vec![leader],
isr: vec![leader],
log: Vec::new(),
log_start_offset: 0,
next_offset: 0,
pending_txn_first_offset: None,
aborted_transactions: Vec::new(),
}
}
pub(crate) fn last_stable_offset(&self) -> i64 {
self.pending_txn_first_offset.unwrap_or(self.next_offset)
}
pub(crate) fn append(&mut self, batch: &Bytes) -> i64 {
let base_offset = self.next_offset;
let count = wire::batch_record_count(batch).unwrap_or(0);
self.log
.push(wire::stamp_batch(batch, base_offset, self.leader_epoch));
self.next_offset += count;
base_offset
}
pub(crate) fn read_from(&self, fetch_offset: i64) -> Bytes {
self.read_range(fetch_offset, i64::MAX)
}
pub(crate) fn read_range(&self, fetch_offset: i64, limit: i64) -> Bytes {
let mut out = Vec::new();
for batch in &self.log {
let base = wire::batch_base_offset(batch).unwrap_or(0);
let count = wire::batch_record_count(batch).unwrap_or(0);
if base + count > fetch_offset && base + count <= limit {
out.extend_from_slice(batch);
}
}
Bytes::from(out)
}
pub(crate) fn aborted_transactions_from(&self, fetch_offset: i64) -> Vec<(i64, i64)> {
self.aborted_transactions
.iter()
.filter(|(_, _, marker_offset)| *marker_offset >= fetch_offset)
.map(|(producer_id, first_offset, _)| (*producer_id, *first_offset))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct TopicState {
pub topic_id: [u8; 16],
pub partitions: Vec<PartitionState>,
}
#[derive(Debug, Clone, Default)]
pub struct TransactionState {
pub producer_id: i64,
pub producer_epoch: i16,
pub open: bool,
pub partitions: Vec<(String, i32)>,
pub staged_offsets: HashMap<String, HashMap<(String, i32), CommittedOffset>>,
}
#[derive(Debug, Clone)]
pub struct GroupMember {
pub member_id: String,
pub group_instance_id: Option<String>,
pub metadata: Bytes,
}
#[derive(Debug, Clone)]
pub struct CommittedOffset {
pub offset: i64,
pub leader_epoch: i32,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct GroupState {
pub generation_id: i32,
pub protocol_type: String,
pub protocol_name: Option<String>,
pub leader: String,
pub members: Vec<GroupMember>,
pub assignments: HashMap<String, Bytes>,
pub offsets: HashMap<(String, i32), CommittedOffset>,
pub member_seq: u32,
pub consumer_members: HashMap<String, ConsumerGroupMemberState>,
pub group_epoch: i32,
}
#[derive(Debug, Clone, Default)]
pub struct ConsumerGroupMemberState {
pub member_epoch: i32,
pub instance_id: Option<String>,
pub subscribed_topics: Vec<String>,
pub assignment: HashMap<String, Vec<i32>>,
pub owned: HashMap<String, Vec<i32>>,
pub assignment_dirty: bool,
}
#[derive(Debug, Clone, Default)]
pub struct ShareGroupState {
pub group_epoch: i32,
pub members: HashMap<String, ShareMemberState>,
pub partitions: HashMap<(String, i32), SharePartitionState>,
}
#[derive(Debug, Clone, Default)]
pub struct ShareMemberState {
pub member_epoch: i32,
pub subscribed_topics: Vec<String>,
pub assignment: HashMap<String, Vec<i32>>,
pub assignment_dirty: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SharePartitionState {
pub start_offset: i64,
pub next_acquire: i64,
pub delivery_counts: HashMap<i64, i16>,
}
impl ShareGroupState {
pub(crate) fn release_in_flight(&mut self) {
for partition in self.partitions.values_mut() {
partition.next_acquire = partition.start_offset;
}
}
}
impl SharePartitionState {
pub(crate) fn acquire(&mut self, first: i64, last: i64) -> i16 {
let mut max = 1;
for offset in first..=last {
let count = self.delivery_counts.entry(offset).or_insert(0);
*count = count.saturating_add(1);
max = max.max(*count);
}
self.next_acquire = self.next_acquire.max(last + 1);
max
}
pub(crate) fn acknowledge(&mut self, first: i64, last: i64, acknowledge_type: i8) {
match acknowledge_type {
1 | 3 => {
self.start_offset = self.start_offset.max(last + 1);
for offset in first..=last {
self.delivery_counts.remove(&offset);
}
}
2 => {
self.next_acquire = self.next_acquire.min(first.max(self.start_offset));
}
_ => {}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StreamsGroupState {
pub group_state: String,
pub group_epoch: i32,
pub assignment_epoch: i32,
pub topology_epoch: Option<i32>,
pub subtopologies: Option<Vec<String>>,
pub members: Vec<StreamsMemberState>,
}
#[derive(Debug, Clone, Default)]
pub struct StreamsMemberState {
pub member_id: String,
pub member_epoch: i32,
pub topology_epoch: i32,
pub process_id: String,
pub user_endpoint: Option<(String, u16)>,
pub active_tasks: Vec<(String, Vec<i32>)>,
pub target_active_tasks: Vec<(String, Vec<i32>)>,
}
#[derive(Debug)]
pub struct ClusterState {
pub cluster_id: String,
pub brokers: Vec<BrokerNode>,
pub controller_id: i32,
pub topics: HashMap<String, TopicState>,
pub groups: HashMap<String, GroupState>,
pub share_groups: HashMap<String, ShareGroupState>,
pub streams_groups: HashMap<String, StreamsGroupState>,
pub group_coordinators: HashMap<String, i32>,
pub txn_coordinators: HashMap<String, i32>,
pub auto_create_topics: bool,
pub default_partitions: i32,
pub next_producer_id: i64,
pub producer_epochs: HashMap<i64, i16>,
pub transactions: HashMap<String, TransactionState>,
pub finalized_features: HashMap<String, i16>,
pub finalized_features_epoch: i64,
pub api_version_overrides: HashMap<ApiKey, (i16, i16)>,
topic_id_seq: u64,
}
impl ClusterState {
pub(crate) fn new(broker_count: usize) -> Self {
let brokers = (0..broker_count)
.map(|i| BrokerNode {
node_id: i as i32,
host: "127.0.0.1".to_string(),
port: 0,
rack: None,
online: true,
})
.collect();
Self {
cluster_id: "krafka-fake-cluster".to_string(),
brokers,
controller_id: 0,
topics: HashMap::new(),
groups: HashMap::new(),
share_groups: HashMap::new(),
streams_groups: HashMap::new(),
finalized_features: HashMap::new(),
finalized_features_epoch: 0,
api_version_overrides: HashMap::new(),
group_coordinators: HashMap::new(),
txn_coordinators: HashMap::new(),
auto_create_topics: true,
default_partitions: 1,
next_producer_id: 1000,
producer_epochs: HashMap::new(),
transactions: HashMap::new(),
topic_id_seq: 1,
}
}
pub fn default_coordinator(&self) -> i32 {
self.brokers
.iter()
.find(|b| b.online)
.map(|b| b.node_id)
.unwrap_or(-1)
}
pub fn group_coordinator(&self, group_id: &str) -> i32 {
self.group_coordinators
.get(group_id)
.copied()
.unwrap_or_else(|| self.default_coordinator())
}
pub fn txn_coordinator(&self, transactional_id: &str) -> i32 {
self.txn_coordinators
.get(transactional_id)
.copied()
.unwrap_or_else(|| self.default_coordinator())
}
pub fn broker(&self, node_id: i32) -> Option<&BrokerNode> {
self.brokers.iter().find(|b| b.node_id == node_id)
}
pub fn create_topic(&mut self, name: &str, partitions: i32) -> bool {
if self.topics.contains_key(name) {
return false;
}
let online: Vec<i32> = self
.brokers
.iter()
.filter(|b| b.online)
.map(|b| b.node_id)
.collect();
let partition_states = (0..partitions.max(1))
.map(|i| {
let leader = online
.get(i as usize % online.len().max(1))
.copied()
.unwrap_or(0);
PartitionState::new(leader)
})
.collect();
let mut topic_id = [0u8; 16];
topic_id[8..].copy_from_slice(&self.topic_id_seq.to_be_bytes());
self.topic_id_seq += 1;
self.topics.insert(
name.to_string(),
TopicState {
topic_id,
partitions: partition_states,
},
);
true
}
pub fn add_partitions(&mut self, name: &str, partitions: i32) -> usize {
let online: Vec<i32> = self
.brokers
.iter()
.filter(|b| b.online)
.map(|b| b.node_id)
.collect();
let Some(topic) = self.topics.get_mut(name) else {
return 0;
};
let existing = topic.partitions.len();
let target = partitions.max(0) as usize;
if target <= existing {
return 0;
}
for i in existing..target {
let leader = online.get(i % online.len().max(1)).copied().unwrap_or(0);
topic.partitions.push(PartitionState::new(leader));
}
target - existing
}
pub fn partition_mut(&mut self, topic: &str, partition: i32) -> Option<&mut PartitionState> {
self.topics
.get_mut(topic)
.and_then(|t| t.partitions.get_mut(usize::try_from(partition).ok()?))
}
pub fn partition(&self, topic: &str, partition: i32) -> Option<&PartitionState> {
self.topics
.get(topic)
.and_then(|t| t.partitions.get(usize::try_from(partition).ok()?))
}
pub fn allocate_producer_id(&mut self) -> (i64, i16) {
let id = self.next_producer_id;
self.next_producer_id += 1;
self.producer_epochs.insert(id, 0);
(id, 0)
}
pub fn next_member_id(&mut self, group_id: &str) -> String {
let group = self.groups.entry(group_id.to_string()).or_default();
group.member_seq += 1;
format!("krafka-fake-member-{}", group.member_seq)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::protocol::{Record, RecordBatch};
fn batch(values: &[&str]) -> Bytes {
let mut b = RecordBatch::new();
b.records = values
.iter()
.enumerate()
.map(|(i, v)| {
Record::new(None, Some(Bytes::copy_from_slice(v.as_bytes())))
.with_offset_delta(i as i32)
})
.collect();
b.encode().unwrap()
}
#[test]
fn appending_assigns_consecutive_offsets() {
let mut p = PartitionState::new(0);
assert_eq!(p.append(&batch(&["a", "b"])), 0);
assert_eq!(p.next_offset, 2);
assert_eq!(p.append(&batch(&["c"])), 2);
assert_eq!(p.next_offset, 3);
}
#[test]
fn reading_returns_whole_batches_that_span_the_fetch_offset() {
let mut p = PartitionState::new(0);
p.append(&batch(&["a", "b"])); p.append(&batch(&["c"]));
assert!(p.read_from(0).len() > p.read_from(2).len());
assert!(!p.read_from(1).is_empty(), "offset 1 sits inside batch one");
assert!(p.read_from(3).is_empty(), "nothing at or beyond the end");
}
#[test]
fn coordinators_default_to_the_lowest_online_broker_and_follow_overrides() {
let mut state = ClusterState::new(3);
assert_eq!(state.group_coordinator("g"), 0);
state.brokers[0].online = false;
assert_eq!(state.group_coordinator("g"), 1);
state.group_coordinators.insert("g".to_string(), 2);
assert_eq!(state.group_coordinator("g"), 2);
}
#[test]
fn topic_creation_spreads_leadership_over_online_brokers() {
let mut state = ClusterState::new(3);
assert!(state.create_topic("t", 3));
assert!(!state.create_topic("t", 3), "re-creation is a no-op");
let leaders: Vec<i32> = state.topics["t"]
.partitions
.iter()
.map(|p| p.leader)
.collect();
assert_eq!(leaders, vec![0, 1, 2]);
}
}