use std::collections::HashMap;
use std::fmt;
use std::time::{Duration, Instant, SystemTime};
use kafka_conn::ErrorCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TopicId([u8; 16]);
impl TopicId {
pub const ZERO: TopicId = TopicId([0; 16]);
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn is_zero(&self) -> bool {
self.0 == [0; 16]
}
}
impl fmt::Display for TopicId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, byte) in self.0.iter().enumerate() {
if matches!(index, 4 | 6 | 8 | 10) {
f.write_str("-")?;
}
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokerInfo {
pub node_id: i32,
pub host: String,
pub port: i32,
pub rack: Option<String>,
}
impl BrokerInfo {
pub fn address(&self) -> String {
format!("{}:{}", self.host, self.port)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionInfo {
pub partition: i32,
pub leader: Option<i32>,
pub leader_epoch: i32,
pub replicas: Vec<i32>,
pub isr: Vec<i32>,
pub offline_replicas: Vec<i32>,
pub error: Option<ErrorCode>,
}
impl PartitionInfo {
pub fn under_replicated(&self) -> bool {
self.isr.len() < self.replicas.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopicInfo {
pub name: String,
pub topic_id: TopicId,
pub internal: bool,
pub partitions: Vec<PartitionInfo>,
pub error: Option<ErrorCode>,
}
impl TopicInfo {
pub fn partition(&self, index: i32) -> Option<&PartitionInfo> {
self.partitions.iter().find(|p| p.partition == index)
}
}
#[derive(Debug, Clone)]
pub struct MetadataSnapshot {
brokers: Vec<BrokerInfo>,
brokers_by_id: HashMap<i32, usize>,
topics: Vec<TopicInfo>,
topics_by_name: HashMap<String, usize>,
controller_id: Option<i32>,
cluster_id: Option<String>,
fetched_at: SystemTime,
fetched_instant: Instant,
}
impl MetadataSnapshot {
pub fn new(
brokers: Vec<BrokerInfo>,
topics: Vec<TopicInfo>,
controller_id: Option<i32>,
cluster_id: Option<String>,
) -> Self {
let brokers_by_id = brokers
.iter()
.enumerate()
.map(|(index, broker)| (broker.node_id, index))
.collect();
let topics_by_name = topics
.iter()
.enumerate()
.map(|(index, topic)| (topic.name.clone(), index))
.collect();
Self {
brokers,
brokers_by_id,
topics,
topics_by_name,
controller_id,
cluster_id,
fetched_at: SystemTime::now(),
fetched_instant: Instant::now(),
}
}
pub fn empty() -> Self {
Self::new(Vec::new(), Vec::new(), None, None)
}
pub fn brokers(&self) -> &[BrokerInfo] {
&self.brokers
}
pub fn broker(&self, node_id: i32) -> Option<&BrokerInfo> {
self.brokers_by_id
.get(&node_id)
.and_then(|index| self.brokers.get(*index))
}
pub fn topics(&self) -> &[TopicInfo] {
&self.topics
}
pub fn topic(&self, name: &str) -> Option<&TopicInfo> {
self.topics_by_name
.get(name)
.and_then(|index| self.topics.get(*index))
}
pub fn controller_id(&self) -> Option<i32> {
self.controller_id
}
pub fn cluster_id(&self) -> Option<&str> {
self.cluster_id.as_deref()
}
pub fn fetched_at(&self) -> SystemTime {
self.fetched_at
}
pub fn age(&self) -> Duration {
self.fetched_instant.elapsed()
}
pub fn leader_for(&self, topic: &str, partition: i32) -> Option<i32> {
self.topic(topic)?.partition(partition)?.leader
}
pub fn with_topics_merged(&self, updated: Vec<TopicInfo>) -> Self {
let mut topics = self.topics.clone();
for topic in updated {
match self.topics_by_name.get(&topic.name) {
Some(index) => {
if let Some(slot) = topics.get_mut(*index) {
*slot = topic;
}
}
None => topics.push(topic),
}
}
Self::new(
self.brokers.clone(),
topics,
self.controller_id,
self.cluster_id.clone(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn broker(id: i32) -> BrokerInfo {
BrokerInfo {
node_id: id,
host: format!("broker-{id}"),
port: 9092,
rack: None,
}
}
fn topic(name: &str, leaders: &[i32]) -> TopicInfo {
TopicInfo {
name: name.to_owned(),
topic_id: TopicId::ZERO,
internal: false,
partitions: leaders
.iter()
.enumerate()
.map(|(index, leader)| PartitionInfo {
partition: i32::try_from(index).unwrap_or(0),
leader: Some(*leader),
leader_epoch: 0,
replicas: vec![*leader],
isr: vec![*leader],
offline_replicas: Vec::new(),
error: None,
})
.collect(),
error: None,
}
}
#[test]
fn lookups_are_by_index_not_by_scan() {
let snapshot = MetadataSnapshot::new(
vec![broker(1), broker(2)],
vec![topic("orders", &[1, 2])],
Some(1),
Some("cluster".to_owned()),
);
assert_eq!(
snapshot.broker(2).map(|b| b.address()),
Some("broker-2:9092".to_owned())
);
assert!(snapshot.broker(99).is_none());
assert_eq!(snapshot.leader_for("orders", 1), Some(2));
assert_eq!(snapshot.leader_for("orders", 7), None);
assert_eq!(snapshot.leader_for("nope", 0), None);
}
#[test]
fn a_targeted_refresh_keeps_the_topics_it_did_not_ask_about() {
let snapshot = MetadataSnapshot::new(
vec![broker(1)],
vec![topic("orders", &[1]), topic("events", &[1])],
Some(1),
None,
);
let merged = snapshot.with_topics_merged(vec![topic("orders", &[1, 1, 1])]);
assert_eq!(merged.topics().len(), 2);
assert_eq!(merged.topic("orders").map(|t| t.partitions.len()), Some(3));
assert!(merged.topic("events").is_some());
}
#[test]
fn a_targeted_refresh_can_add_a_topic() {
let snapshot = MetadataSnapshot::new(vec![broker(1)], vec![], None, None);
let merged = snapshot.with_topics_merged(vec![topic("new", &[1])]);
assert!(merged.topic("new").is_some());
}
#[test]
fn a_missing_leader_is_none_not_minus_one() {
let mut info = topic("orders", &[1]);
if let Some(p) = info.partitions.get_mut(0) {
p.leader = None;
}
let snapshot = MetadataSnapshot::new(vec![broker(1)], vec![info], None, None);
assert_eq!(snapshot.leader_for("orders", 0), None);
}
#[test]
fn topic_ids_render_as_uuids() {
let id = TopicId::from_bytes([
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef,
]);
assert_eq!(id.to_string(), "01234567-89ab-cdef-0123-456789abcdef");
assert!(TopicId::ZERO.is_zero());
assert!(!id.is_zero());
}
#[test]
fn under_replicated_is_a_comparison_not_a_guess() {
let partition = PartitionInfo {
partition: 0,
leader: Some(1),
leader_epoch: 0,
replicas: vec![1, 2, 3],
isr: vec![1, 2],
offline_replicas: vec![3],
error: None,
};
assert!(partition.under_replicated());
}
}