1use std::collections::HashMap;
14use std::fmt;
15use std::time::{Duration, Instant, SystemTime};
16
17use kafka_conn::ErrorCode;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct TopicId([u8; 16]);
26
27impl TopicId {
28 pub const ZERO: TopicId = TopicId([0; 16]);
30
31 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
33 Self(bytes)
34 }
35
36 pub const fn as_bytes(&self) -> &[u8; 16] {
38 &self.0
39 }
40
41 pub fn is_zero(&self) -> bool {
43 self.0 == [0; 16]
44 }
45}
46
47impl fmt::Display for TopicId {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 for (index, byte) in self.0.iter().enumerate() {
51 if matches!(index, 4 | 6 | 8 | 10) {
52 f.write_str("-")?;
53 }
54 write!(f, "{byte:02x}")?;
55 }
56 Ok(())
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct BrokerInfo {
63 pub node_id: i32,
65 pub host: String,
67 pub port: i32,
69 pub rack: Option<String>,
71}
72
73impl BrokerInfo {
74 pub fn address(&self) -> String {
76 format!("{}:{}", self.host, self.port)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PartitionInfo {
83 pub partition: i32,
85 pub leader: Option<i32>,
90 pub leader_epoch: i32,
92 pub replicas: Vec<i32>,
94 pub isr: Vec<i32>,
96 pub offline_replicas: Vec<i32>,
98 pub error: Option<ErrorCode>,
100}
101
102impl PartitionInfo {
103 pub fn under_replicated(&self) -> bool {
105 self.isr.len() < self.replicas.len()
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct TopicInfo {
112 pub name: String,
114 pub topic_id: TopicId,
116 pub internal: bool,
118 pub partitions: Vec<PartitionInfo>,
120 pub error: Option<ErrorCode>,
123}
124
125impl TopicInfo {
126 pub fn partition(&self, index: i32) -> Option<&PartitionInfo> {
128 self.partitions.iter().find(|p| p.partition == index)
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct MetadataSnapshot {
135 brokers: Vec<BrokerInfo>,
136 brokers_by_id: HashMap<i32, usize>,
137 topics: Vec<TopicInfo>,
138 topics_by_name: HashMap<String, usize>,
139 controller_id: Option<i32>,
140 cluster_id: Option<String>,
141 fetched_at: SystemTime,
142 fetched_instant: Instant,
143}
144
145impl MetadataSnapshot {
146 pub fn new(
148 brokers: Vec<BrokerInfo>,
149 topics: Vec<TopicInfo>,
150 controller_id: Option<i32>,
151 cluster_id: Option<String>,
152 ) -> Self {
153 let brokers_by_id = brokers
154 .iter()
155 .enumerate()
156 .map(|(index, broker)| (broker.node_id, index))
157 .collect();
158 let topics_by_name = topics
159 .iter()
160 .enumerate()
161 .map(|(index, topic)| (topic.name.clone(), index))
162 .collect();
163 Self {
164 brokers,
165 brokers_by_id,
166 topics,
167 topics_by_name,
168 controller_id,
169 cluster_id,
170 fetched_at: SystemTime::now(),
171 fetched_instant: Instant::now(),
172 }
173 }
174
175 pub fn empty() -> Self {
177 Self::new(Vec::new(), Vec::new(), None, None)
178 }
179
180 pub fn brokers(&self) -> &[BrokerInfo] {
182 &self.brokers
183 }
184
185 pub fn broker(&self, node_id: i32) -> Option<&BrokerInfo> {
187 self.brokers_by_id
188 .get(&node_id)
189 .and_then(|index| self.brokers.get(*index))
190 }
191
192 pub fn topics(&self) -> &[TopicInfo] {
194 &self.topics
195 }
196
197 pub fn topic(&self, name: &str) -> Option<&TopicInfo> {
199 self.topics_by_name
200 .get(name)
201 .and_then(|index| self.topics.get(*index))
202 }
203
204 pub fn controller_id(&self) -> Option<i32> {
206 self.controller_id
207 }
208
209 pub fn cluster_id(&self) -> Option<&str> {
211 self.cluster_id.as_deref()
212 }
213
214 pub fn fetched_at(&self) -> SystemTime {
219 self.fetched_at
220 }
221
222 pub fn age(&self) -> Duration {
224 self.fetched_instant.elapsed()
225 }
226
227 pub fn leader_for(&self, topic: &str, partition: i32) -> Option<i32> {
229 self.topic(topic)?.partition(partition)?.leader
230 }
231
232 pub fn with_topics_merged(&self, updated: Vec<TopicInfo>) -> Self {
237 let mut topics = self.topics.clone();
238 for topic in updated {
239 match self.topics_by_name.get(&topic.name) {
240 Some(index) => {
241 if let Some(slot) = topics.get_mut(*index) {
242 *slot = topic;
243 }
244 }
245 None => topics.push(topic),
246 }
247 }
248 Self::new(
249 self.brokers.clone(),
250 topics,
251 self.controller_id,
252 self.cluster_id.clone(),
253 )
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 fn broker(id: i32) -> BrokerInfo {
262 BrokerInfo {
263 node_id: id,
264 host: format!("broker-{id}"),
265 port: 9092,
266 rack: None,
267 }
268 }
269
270 fn topic(name: &str, leaders: &[i32]) -> TopicInfo {
271 TopicInfo {
272 name: name.to_owned(),
273 topic_id: TopicId::ZERO,
274 internal: false,
275 partitions: leaders
276 .iter()
277 .enumerate()
278 .map(|(index, leader)| PartitionInfo {
279 partition: i32::try_from(index).unwrap_or(0),
280 leader: Some(*leader),
281 leader_epoch: 0,
282 replicas: vec![*leader],
283 isr: vec![*leader],
284 offline_replicas: Vec::new(),
285 error: None,
286 })
287 .collect(),
288 error: None,
289 }
290 }
291
292 #[test]
293 fn lookups_are_by_index_not_by_scan() {
294 let snapshot = MetadataSnapshot::new(
295 vec![broker(1), broker(2)],
296 vec![topic("orders", &[1, 2])],
297 Some(1),
298 Some("cluster".to_owned()),
299 );
300 assert_eq!(
301 snapshot.broker(2).map(|b| b.address()),
302 Some("broker-2:9092".to_owned())
303 );
304 assert!(snapshot.broker(99).is_none());
305 assert_eq!(snapshot.leader_for("orders", 1), Some(2));
306 assert_eq!(snapshot.leader_for("orders", 7), None);
307 assert_eq!(snapshot.leader_for("nope", 0), None);
308 }
309
310 #[test]
311 fn a_targeted_refresh_keeps_the_topics_it_did_not_ask_about() {
312 let snapshot = MetadataSnapshot::new(
313 vec![broker(1)],
314 vec![topic("orders", &[1]), topic("events", &[1])],
315 Some(1),
316 None,
317 );
318 let merged = snapshot.with_topics_merged(vec![topic("orders", &[1, 1, 1])]);
319 assert_eq!(merged.topics().len(), 2);
320 assert_eq!(merged.topic("orders").map(|t| t.partitions.len()), Some(3));
321 assert!(merged.topic("events").is_some());
322 }
323
324 #[test]
325 fn a_targeted_refresh_can_add_a_topic() {
326 let snapshot = MetadataSnapshot::new(vec![broker(1)], vec![], None, None);
327 let merged = snapshot.with_topics_merged(vec![topic("new", &[1])]);
328 assert!(merged.topic("new").is_some());
329 }
330
331 #[test]
332 fn a_missing_leader_is_none_not_minus_one() {
333 let mut info = topic("orders", &[1]);
336 if let Some(p) = info.partitions.get_mut(0) {
337 p.leader = None;
338 }
339 let snapshot = MetadataSnapshot::new(vec![broker(1)], vec![info], None, None);
340 assert_eq!(snapshot.leader_for("orders", 0), None);
341 }
342
343 #[test]
344 fn topic_ids_render_as_uuids() {
345 let id = TopicId::from_bytes([
346 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
347 0xcd, 0xef,
348 ]);
349 assert_eq!(id.to_string(), "01234567-89ab-cdef-0123-456789abcdef");
350 assert!(TopicId::ZERO.is_zero());
351 assert!(!id.is_zero());
352 }
353
354 #[test]
355 fn under_replicated_is_a_comparison_not_a_guess() {
356 let partition = PartitionInfo {
357 partition: 0,
358 leader: Some(1),
359 leader_epoch: 0,
360 replicas: vec![1, 2, 3],
361 isr: vec![1, 2],
362 offline_replicas: vec![3],
363 error: None,
364 };
365 assert!(partition.under_replicated());
366 }
367}