1use std::collections::HashMap;
14use std::sync::{Arc, Mutex, Weak};
15use std::time::Duration;
16
17use arc_swap::ArcSwap;
18use kafka_conn::protocol::StrBytes;
19use kafka_conn::protocol::messages::metadata_request::MetadataRequestTopic;
20use kafka_conn::protocol::messages::{FindCoordinatorRequest, MetadataRequest, TopicName};
21use kafka_conn::{ApiKey, Connection, ConnectionConfig, Error, ErrorCode, Result, Rpc};
22
23use crate::pool::BrokerPool;
24use crate::retry::RetryPolicy;
25use crate::routing::{BrokerSelector, CoordinatorKind, Routing, routing};
26use crate::snapshot::{BrokerInfo, MetadataSnapshot, PartitionInfo, TopicId, TopicInfo};
27
28#[derive(Debug, Clone)]
30pub struct ClusterConfig {
31 pub connection: ConnectionConfig,
33 pub retry: RetryPolicy,
35 pub refresh_interval: Duration,
41 pub max_staleness: Duration,
43}
44
45impl Default for ClusterConfig {
46 fn default() -> Self {
47 Self {
48 connection: ConnectionConfig::default(),
49 retry: RetryPolicy::default(),
50 refresh_interval: Duration::from_secs(30),
51 max_staleness: Duration::from_secs(300),
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct Cluster {
59 inner: Arc<Inner>,
60}
61
62#[derive(Debug)]
63struct Inner {
64 pool: BrokerPool,
65 config: ClusterConfig,
66 snapshot: ArcSwap<MetadataSnapshot>,
67 coordinators: Mutex<HashMap<(CoordinatorKind, String), i32>>,
68}
69
70impl Cluster {
71 pub async fn connect(
73 bootstrap: impl IntoIterator<Item = impl Into<String>>,
74 config: ClusterConfig,
75 ) -> Result<Self> {
76 let pool = BrokerPool::new(bootstrap, config.connection.clone(), config.retry);
77 let cluster = Cluster {
78 inner: Arc::new(Inner {
79 pool,
80 config,
81 snapshot: ArcSwap::from_pointee(MetadataSnapshot::empty()),
82 coordinators: Mutex::new(HashMap::new()),
83 }),
84 };
85 cluster.refresh().await?;
86 cluster.spawn_refresh_task();
87 Ok(cluster)
88 }
89
90 pub fn snapshot(&self) -> Arc<MetadataSnapshot> {
92 self.inner.snapshot.load_full()
93 }
94
95 pub fn pool(&self) -> &BrokerPool {
97 &self.inner.pool
98 }
99
100 pub async fn negotiated_for<R: Rpc>(&self) -> Result<i16> {
107 self.inner.pool.any().await?.negotiated_for::<R>()
108 }
109
110 pub async fn refresh(&self) -> Result<Arc<MetadataSnapshot>> {
112 let connection = self.inner.pool.any().await?;
113 let response = connection.send(all_topics_request(&connection)).await?;
114 let snapshot = Arc::new(decode_metadata(response));
115 self.install(snapshot.clone());
116 Ok(snapshot)
117 }
118
119 pub async fn refresh_topics(&self, topics: &[&str]) -> Result<Arc<MetadataSnapshot>> {
125 if topics.is_empty() {
126 return Ok(self.snapshot());
127 }
128 let connection = self.inner.pool.any().await?;
129 let response = connection.send(topics_request(topics)).await?;
130 let fresh = decode_metadata(response);
131 let merged = Arc::new(self.snapshot().with_topics_merged(fresh.topics().to_vec()));
132 self.install(merged.clone());
133 Ok(merged)
134 }
135
136 pub async fn refresh_if_stale(&self) -> Result<Arc<MetadataSnapshot>> {
138 let snapshot = self.snapshot();
139 if snapshot.age() < self.inner.config.max_staleness && !snapshot.brokers().is_empty() {
140 return Ok(snapshot);
141 }
142 self.refresh().await
143 }
144
145 pub async fn leader_for(&self, topic: &str, partition: i32) -> Result<i32> {
147 if let Some(leader) = self.snapshot().leader_for(topic, partition) {
148 return Ok(leader);
149 }
150 let snapshot = self.refresh_topics(&[topic]).await?;
151 snapshot.leader_for(topic, partition).ok_or_else(|| {
152 match snapshot.topic(topic).and_then(|t| t.error) {
153 Some(code) => Error::from_code(code, Some(format!("topic {topic}"))),
154 None => Error::from_code(
155 ErrorCode::LeaderNotAvailable,
156 Some(format!("{topic}-{partition}")),
157 ),
158 }
159 })
160 }
161
162 pub async fn coordinator_for(&self, group: &str) -> Result<i32> {
164 self.coordinator(CoordinatorKind::Group, group).await
165 }
166
167 pub async fn coordinator(&self, kind: CoordinatorKind, key: &str) -> Result<i32> {
188 let policy = self.inner.config.retry;
189 let started = std::time::Instant::now();
190 let mut attempt = 1;
191 loop {
192 let delay = policy.delay(attempt);
193 if !delay.is_zero() {
194 tokio::time::sleep(delay).await;
195 }
196
197 let error = match self.coordinator_once(kind, key).await {
198 Ok(node) => return Ok(node),
199 Err(error) => error,
200 };
201
202 let budget_left = if error.needs_coordinator_refresh() {
203 started.elapsed() < policy.coordinator_timeout
204 } else {
205 policy.should_retry(attempt)
206 };
207
208 if !error.retriable() || !budget_left {
209 return Err(error);
210 }
211 tracing::debug!(?kind, key, attempt, %error, "retrying FindCoordinator");
212 attempt = attempt.saturating_add(1);
213 }
214 }
215
216 async fn coordinator_once(&self, kind: CoordinatorKind, key: &str) -> Result<i32> {
218 let cache_key = (kind, key.to_owned());
219 if let Some(node) = self
220 .inner
221 .coordinators
222 .lock()
223 .ok()
224 .and_then(|map| map.get(&cache_key).copied())
225 {
226 return Ok(node);
227 }
228
229 let connection = self.inner.pool.any().await?;
230 let version = connection.negotiated_for::<FindCoordinatorRequest>()?;
236 let request = FindCoordinatorRequest::default().with_key_type(kind.key_type());
237 let request = if version >= 4 {
238 request.with_coordinator_keys(vec![StrBytes::from_string(key.to_owned())])
239 } else {
240 request.with_key(StrBytes::from_string(key.to_owned()))
241 };
242 let response = connection.send(request).await?;
243
244 let (node_id, error_code, message) = match response.coordinators.first() {
249 Some(coordinator) => (
250 coordinator.node_id.0,
251 coordinator.error_code,
252 coordinator.error_message.as_ref().map(|m| m.to_string()),
253 ),
254 None => (
255 response.node_id.0,
256 response.error_code,
257 response.error_message.as_ref().map(|m| m.to_string()),
258 ),
259 };
260
261 if let Some(code) = ErrorCode::from_code(error_code) {
262 return Err(Error::from_code(code, message));
263 }
264 if node_id < 0 {
265 return Err(Error::from_code(
266 ErrorCode::CoordinatorNotAvailable,
267 Some(key.to_owned()),
268 ));
269 }
270
271 if let Ok(mut map) = self.inner.coordinators.lock() {
272 map.insert(cache_key, node_id);
273 }
274 Ok(node_id)
275 }
276
277 pub async fn controller(&self) -> Result<i32> {
279 if let Some(id) = self.snapshot().controller_id() {
280 return Ok(id);
281 }
282 self.refresh()
283 .await?
284 .controller_id()
285 .ok_or_else(|| Error::from_code(ErrorCode::NotController, None))
286 }
287
288 pub fn invalidate_coordinator(&self, kind: CoordinatorKind, key: &str) {
290 if let Ok(mut map) = self.inner.coordinators.lock() {
291 map.remove(&(kind, key.to_owned()));
292 }
293 }
294
295 pub fn invalidate(&self) {
297 self.install(Arc::new(MetadataSnapshot::empty()));
298 }
299
300 pub async fn send_any<R: Rpc + Clone>(&self, request: R) -> Result<R::Response> {
302 self.dispatch(Target::Any, request).await
303 }
304
305 pub async fn send_to_controller<R: Rpc + Clone>(&self, request: R) -> Result<R::Response> {
307 self.dispatch(Target::Controller, request).await
308 }
309
310 pub async fn send_to_node<R: Rpc + Clone>(
312 &self,
313 node_id: i32,
314 request: R,
315 ) -> Result<R::Response> {
316 self.dispatch(Target::Node(node_id), request).await
317 }
318
319 pub async fn send_to_coordinator<R: Rpc + Clone>(
321 &self,
322 kind: CoordinatorKind,
323 key: &str,
324 request: R,
325 ) -> Result<R::Response> {
326 self.dispatch(Target::Coordinator(kind, key.to_owned()), request)
327 .await
328 }
329
330 pub async fn send_to_leader<R: Rpc + Clone>(
332 &self,
333 topic: &str,
334 partition: i32,
335 request: R,
336 ) -> Result<R::Response> {
337 self.dispatch(Target::Leader(topic.to_owned(), partition), request)
338 .await
339 }
340
341 pub async fn send_routed<R: Rpc + Clone>(&self, request: R) -> Result<R::Response> {
347 match routing(R::API_KEY) {
348 Routing::Any => self.send_any(request).await,
349 Routing::Controller => self.send_to_controller(request).await,
350 Routing::Coordinator(kind) => Err(Error::InvalidRequest(format!(
351 "{} is routed to a {kind:?} coordinator; use send_to_coordinator",
352 R::API_KEY
353 ))),
354 Routing::Specific(BrokerSelector::Caller) => Err(Error::InvalidRequest(format!(
355 "{} is routed to one broker; use send_to_node",
356 R::API_KEY
357 ))),
358 Routing::Specific(BrokerSelector::PartitionLeader) => {
359 Err(Error::InvalidRequest(format!(
360 "{} is routed to a partition leader; use send_to_leader",
361 R::API_KEY
362 )))
363 }
364 }
365 }
366
367 async fn dispatch<R: Rpc + Clone>(&self, target: Target, request: R) -> Result<R::Response> {
369 let policy = self.inner.config.retry;
370 let started = std::time::Instant::now();
371 let mut attempt = 1;
372 loop {
373 let delay = policy.delay(attempt);
374 if !delay.is_zero() {
375 tokio::time::sleep(delay).await;
376 }
377
378 let outcome = self.attempt(&target, request.clone()).await;
379 let error = match outcome {
380 Ok(response) => return Ok(response),
381 Err(error) => error,
382 };
383
384 if error.needs_metadata_refresh() {
389 self.on_stale_metadata(&target).await;
390 }
391 let coordinator_moved =
392 error.needs_coordinator_refresh() && matches!(&target, Target::Coordinator(..));
393 if coordinator_moved && let Target::Coordinator(kind, key) = &target {
394 self.invalidate_coordinator(*kind, key);
395 }
396
397 let budget_left = if coordinator_moved {
404 started.elapsed() < policy.coordinator_timeout
405 } else {
406 policy.should_retry(attempt)
407 };
408
409 if !error.retriable() || !budget_left {
410 return Err(error);
411 }
412 tracing::debug!(api = %R::API_KEY, attempt, %error, "retrying");
413 attempt = attempt.saturating_add(1);
414 }
415 }
416
417 async fn attempt<R: Rpc + Clone>(&self, target: &Target, request: R) -> Result<R::Response> {
418 let connection = self.resolve(target).await?;
419 connection.send(request).await
420 }
421
422 async fn resolve(&self, target: &Target) -> Result<Connection> {
423 match target {
424 Target::Any => self.inner.pool.any().await,
425 Target::Node(node_id) => self.inner.pool.get(*node_id).await,
426 Target::Controller => {
427 let controller = self.controller().await?;
428 self.inner.pool.get(controller).await
429 }
430 Target::Coordinator(kind, key) => {
431 let node = self.coordinator(*kind, key).await?;
432 self.inner.pool.get(node).await
433 }
434 Target::Leader(topic, partition) => {
435 let leader = self.leader_for(topic, *partition).await?;
436 self.inner.pool.get(leader).await
437 }
438 }
439 }
440
441 async fn on_stale_metadata(&self, target: &Target) {
442 let refreshed = match target {
443 Target::Leader(topic, _) => self.refresh_topics(&[topic.as_str()]).await.map(|_| ()),
444 _ => self.refresh().await.map(|_| ()),
445 };
446 if let Err(error) = refreshed {
447 tracing::debug!(%error, "metadata refresh after a stale-view error failed");
448 }
449 }
450
451 fn install(&self, snapshot: Arc<MetadataSnapshot>) {
452 self.inner.pool.learn_addresses(
453 snapshot
454 .brokers()
455 .iter()
456 .map(|broker| (broker.node_id, broker.address())),
457 );
458 self.inner.snapshot.store(snapshot);
459 }
460
461 fn spawn_refresh_task(&self) {
463 let weak = Arc::downgrade(&self.inner);
464 let interval = self.inner.config.refresh_interval;
465 tokio::spawn(async move {
466 loop {
467 tokio::time::sleep(interval).await;
468 let Some(inner) = Weak::upgrade(&weak) else {
469 return;
470 };
471 let cluster = Cluster { inner };
472 if let Err(error) = cluster.refresh().await {
473 tracing::debug!(%error, "background metadata refresh failed");
474 }
475 }
476 });
477 }
478}
479
480#[derive(Debug, Clone)]
482enum Target {
483 Any,
484 Controller,
485 Node(i32),
486 Coordinator(CoordinatorKind, String),
487 Leader(String, i32),
488}
489
490fn all_topics_request(connection: &Connection) -> MetadataRequest {
497 let version = connection.negotiated_version(ApiKey::Metadata).unwrap_or(1);
498 let topics = if version >= 1 { None } else { Some(Vec::new()) };
499 base_metadata_request().with_topics(topics)
500}
501
502fn topics_request(topics: &[&str]) -> MetadataRequest {
503 base_metadata_request().with_topics(Some(
504 topics
505 .iter()
506 .map(|name| {
507 MetadataRequestTopic::default()
508 .with_name(Some(TopicName(StrBytes::from_string((*name).to_owned()))))
509 })
510 .collect(),
511 ))
512}
513
514fn base_metadata_request() -> MetadataRequest {
523 MetadataRequest::default().with_allow_auto_topic_creation(false)
524}
525
526fn decode_metadata(response: kafka_conn::protocol::messages::MetadataResponse) -> MetadataSnapshot {
528 let brokers = response
529 .brokers
530 .into_iter()
531 .map(|broker| BrokerInfo {
532 node_id: broker.node_id.0,
533 host: broker.host.to_string(),
534 port: broker.port,
535 rack: broker.rack.map(|r| r.to_string()),
536 })
537 .collect();
538
539 let topics = response
540 .topics
541 .into_iter()
542 .map(|topic| TopicInfo {
543 name: topic.name.map(|n| n.0.to_string()).unwrap_or_default(),
544 topic_id: TopicId::from_bytes(topic.topic_id.into_bytes()),
545 internal: topic.is_internal,
546 partitions: topic
547 .partitions
548 .into_iter()
549 .map(|partition| PartitionInfo {
550 partition: partition.partition_index,
551 leader: Some(partition.leader_id.0).filter(|id| *id >= 0),
554 leader_epoch: partition.leader_epoch,
555 replicas: partition.replica_nodes.iter().map(|id| id.0).collect(),
556 isr: partition.isr_nodes.iter().map(|id| id.0).collect(),
557 offline_replicas: partition.offline_replicas.iter().map(|id| id.0).collect(),
558 error: ErrorCode::from_code(partition.error_code),
559 })
560 .collect(),
561 error: ErrorCode::from_code(topic.error_code),
562 })
563 .collect();
564
565 MetadataSnapshot::new(
566 brokers,
567 topics,
568 Some(response.controller_id.0).filter(|id| *id >= 0),
569 response.cluster_id.map(|id| id.to_string()),
570 )
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
581 fn metadata_requests_never_allow_auto_topic_creation() {
582 assert!(!base_metadata_request().allow_auto_topic_creation);
583 assert!(!topics_request(&["orders"]).allow_auto_topic_creation);
584 }
585
586 #[test]
587 fn the_crates_default_is_the_dangerous_one() {
588 assert!(MetadataRequest::default().allow_auto_topic_creation);
591 }
592
593 #[test]
594 fn a_targeted_request_names_its_topics() {
595 let request = topics_request(&["orders", "events"]);
596 let names: Vec<String> = request
597 .topics
598 .unwrap_or_default()
599 .into_iter()
600 .filter_map(|t| t.name.map(|n| n.0.to_string()))
601 .collect();
602 assert_eq!(names, vec!["orders".to_owned(), "events".to_owned()]);
603 }
604
605 #[test]
606 fn send_routed_refuses_the_classes_it_cannot_resolve() {
607 assert_eq!(
610 routing(ApiKey::OffsetFetch),
611 Routing::Coordinator(CoordinatorKind::Group)
612 );
613 assert_eq!(
614 routing(ApiKey::DescribeLogDirs),
615 Routing::Specific(BrokerSelector::Caller)
616 );
617 }
618}