1use std::sync::{Arc, Mutex, PoisonError};
6
7use krabka_protocol::owned::{metadata_request, metadata_response};
8use krabka_units::{Time, convert::TimeExt as _, minutes};
9use metadata_request::MetadataRequest;
10use refined_type::rule::GreaterEqualI64;
11use tokio::time::Instant;
12use tracing::field;
13
14use crate::{
15 bootstrap,
16 connection::{
17 ClientDnsTimeout, ClientFrameMax, Connection, ConnectionDispatchQueueCapacity,
18 ConnectionOptions,
19 },
20 error::ClientError,
21 fetch,
22 metadata_topics::{MetadataScope, MetadataTopics},
23 offset_for_leader_epoch,
24 pool::{BrokerInfo, BrokerPool},
25 request::ProtocolRequest,
26 security::ClientSecurity,
27 telemetry::{
28 ClientMetrics, ClientTelemetry, ClientTelemetryConfig, NetworkMetrics,
29 TelemetryConnections, TelemetryTiming, client_instance_id,
30 },
31};
32
33#[derive(Clone)]
40pub struct Client {
41 bootstrap: String,
42 pool: Arc<BrokerPool>,
43 options: ConnectionOptions,
44 metadata_recovery: Arc<MetadataRecovery>,
45 metadata_topics: Arc<MetadataTopics>,
46 metrics: Option<ClientMetrics>,
48 telemetry: Option<Arc<ClientTelemetry>>,
51}
52
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
56pub enum MetadataRecoveryStrategy {
57 None,
60 #[default]
63 Rebootstrap,
64}
65
66pub const DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER: Time = minutes(5);
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Into)]
72pub struct MetadataRecoveryRebootstrapTrigger(i64);
73
74impl MetadataRecoveryRebootstrapTrigger {
75 pub fn new(value: Time) -> Result<Self, String> {
82 let milliseconds = GreaterEqualI64::<0>::new(value.millis_i64())
83 .map_err(|error| format!("metadata recovery rebootstrap trigger: {error}"))?
84 .into_value();
85 if !value.secs_f64().is_finite() || Time::from_millis(milliseconds) != value {
86 return Err(
87 "metadata recovery rebootstrap trigger must be a whole number of milliseconds"
88 .to_owned(),
89 );
90 }
91 Ok(Self(milliseconds))
92 }
93
94 #[must_use]
96 pub fn time(self) -> Time {
97 Time::from_millis(self.0)
98 }
99}
100
101impl Default for MetadataRecoveryRebootstrapTrigger {
102 fn default() -> Self {
103 Self(DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER.millis_i64())
104 }
105}
106
107#[derive(Debug)]
108struct MetadataRecovery {
109 strategy: MetadataRecoveryStrategy,
110 trigger: MetadataRecoveryRebootstrapTrigger,
111 first_attempt: Mutex<Option<Instant>>,
112}
113
114impl MetadataRecovery {
115 fn begin_attempt(&self) {
116 if self.strategy == MetadataRecoveryStrategy::None {
117 return;
118 }
119 let mut first_attempt = self
120 .first_attempt
121 .lock()
122 .unwrap_or_else(PoisonError::into_inner);
123 first_attempt.get_or_insert_with(Instant::now);
124 }
125
126 fn complete_attempt(&self) {
127 *self
128 .first_attempt
129 .lock()
130 .unwrap_or_else(PoisonError::into_inner) = None;
131 }
132
133 fn timed_out(&self) -> bool {
134 if self.strategy == MetadataRecoveryStrategy::None {
135 return false;
136 }
137 self.first_attempt
138 .lock()
139 .unwrap_or_else(PoisonError::into_inner)
140 .is_some_and(|started| started.elapsed() >= self.trigger.time().to_std())
141 }
142}
143
144const REBOOTSTRAP_REQUIRED: i16 = 129;
145
146#[bon::bon]
147impl Client {
148 #[builder(start_fn = builder, finish_fn = build)]
150 #[tracing::instrument(
151 level = "info",
152 skip_all,
153 fields(bootstrap = %bootstrap, client_id = %client_id),
154 err(level = "info"),
155 )]
156 pub async fn start(
159 #[builder(into)] bootstrap: String,
160 #[builder(into, default = "krabka".to_owned())] client_id: String,
161 #[builder(default = crate::DEFAULT_CLIENT_DNS_TIMEOUT)] dns_timeout: Time,
162 #[builder(default = crate::DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT)]
163 socket_connection_setup_timeout: Time,
164 #[builder(default = crate::DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX)]
165 socket_connection_setup_timeout_max: Time,
166 #[builder(default = crate::DEFAULT_RECONNECT_BACKOFF)] reconnect_backoff: Time,
167 #[builder(default = crate::DEFAULT_RECONNECT_BACKOFF_MAX)] reconnect_backoff_max: Time,
168 #[builder(default = crate::DEFAULT_CONNECTIONS_MAX_IDLE)] connections_max_idle: Time,
169 #[builder(default)] client_dns_lookup: crate::ClientDnsLookup,
170 #[builder(required, default = Some(crate::DEFAULT_SEND_BUFFER))] send_buffer: Option<
171 krabka_units::ByteSize,
172 >,
173 #[builder(required, default = Some(crate::DEFAULT_RECEIVE_BUFFER))] receive_buffer: Option<
174 krabka_units::ByteSize,
175 >,
176 #[builder(default = crate::DEFAULT_CLIENT_REQUEST_TIMEOUT)] request_timeout: Time,
177 #[builder(default = crate::DEFAULT_CONNECTION_DISPATCH_QUEUE_CAPACITY)]
178 dispatch_queue_capacity: usize,
179 #[builder(default = crate::DEFAULT_CLIENT_FRAME_MAX)] frame_max: krabka_units::ByteSize,
180 #[builder(default)] metadata_recovery_strategy: MetadataRecoveryStrategy,
181 #[builder(default = crate::DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER)]
182 metadata_recovery_rebootstrap_trigger: Time,
183 security: Option<ClientSecurity>,
184 #[builder(default)]
187 metadata_scope: MetadataScope,
188 telemetry: Option<ClientTelemetryConfig>,
193 ) -> Result<Self, ClientError> {
194 let dns_timeout = ClientDnsTimeout::new(dns_timeout).map_err(ClientError::InvalidConfig)?;
195 let dispatch_queue_capacity = ConnectionDispatchQueueCapacity::new(dispatch_queue_capacity)
196 .map_err(ClientError::InvalidConfig)?;
197 let frame_max = ClientFrameMax::try_from(frame_max).map_err(ClientError::InvalidConfig)?;
198 let metadata_recovery_rebootstrap_trigger =
199 MetadataRecoveryRebootstrapTrigger::new(metadata_recovery_rebootstrap_trigger)
200 .map_err(ClientError::InvalidConfig)?;
201 if let Some(tls) = security.as_ref().and_then(|security| security.tls.as_ref()) {
205 tls.connector()
206 .map_err(|error| ClientError::InvalidConfig(error.to_string()))?;
207 }
208 let options = ConnectionOptions {
209 client_id,
210 dns_timeout,
211 socket_connection_setup_timeout,
212 socket_connection_setup_timeout_max,
213 reconnect_backoff,
214 reconnect_backoff_max,
215 connections_max_idle,
216 client_dns_lookup,
217 send_buffer,
218 receive_buffer,
219 request_timeout,
220 dispatch_queue_capacity,
221 frame_max,
222 security: security.map(Box::new),
223 network_metrics: None,
225 };
226 options.validate().map_err(ClientError::InvalidConfig)?;
227 Self::start_with_options(
228 bootstrap,
229 options,
230 (
231 metadata_recovery_strategy,
232 metadata_recovery_rebootstrap_trigger,
233 ),
234 metadata_scope,
235 telemetry,
236 )
237 .await
238 }
239}
240
241impl Client {
242 #[tracing::instrument(
243 level = "info",
244 skip_all,
245 fields(bootstrap = %bootstrap, client_id = %options.client_id),
246 err(level = "info"),
247 )]
248 async fn start_with_options(
249 bootstrap: String,
250 mut options: ConnectionOptions,
251 (metadata_recovery_strategy, metadata_recovery_rebootstrap_trigger): (
252 MetadataRecoveryStrategy,
253 MetadataRecoveryRebootstrapTrigger,
254 ),
255 metadata_scope: MetadataScope,
256 telemetry: Option<ClientTelemetryConfig>,
257 ) -> Result<Self, ClientError> {
258 let addrs = bootstrap::resolve_with_server_names(
259 &bootstrap,
260 options.dns_timeout,
261 options.client_dns_lookup,
262 )
263 .await?;
264 let metrics = telemetry.as_ref().map(|_| ClientMetrics::new());
265 options.network_metrics =
266 telemetry
267 .as_ref()
268 .zip(metrics.as_ref())
269 .map(|(config, metrics)| {
270 NetworkMetrics::register(metrics, config.client_type.metric_group())
271 });
272 let pool = Arc::new(BrokerPool::new_with_server_names(addrs, options.clone()));
273 let client = Self {
274 bootstrap,
275 pool,
276 options,
277 metadata_recovery: Arc::new(MetadataRecovery {
278 strategy: metadata_recovery_strategy,
279 trigger: metadata_recovery_rebootstrap_trigger,
280 first_attempt: Mutex::new(None),
281 }),
282 metadata_topics: Arc::new(MetadataTopics::new(metadata_scope)),
283 metrics,
284 telemetry: None,
285 };
286 let telemetry = telemetry
287 .zip(client.metrics.clone())
288 .map(|(config, metrics)| {
289 let timing = TelemetryTiming {
290 request_timeout: client.options.request_timeout.to_std(),
291 reconnect_backoff: client.options.reconnect_backoff.to_std(),
292 };
293 Arc::new(ClientTelemetry::start(
294 client.clone(),
295 &config,
296 metrics,
297 timing,
298 ))
299 });
300 Ok(Self {
301 telemetry,
302 ..client
303 })
304 }
305
306 #[must_use]
310 pub fn connection_options(&self) -> &ConnectionOptions {
311 &self.options
312 }
313
314 #[must_use]
317 pub fn metrics(&self) -> Option<&ClientMetrics> {
318 self.metrics.as_ref()
319 }
320
321 pub async fn close_telemetry(&self) {
326 if let Some(telemetry) = &self.telemetry {
327 telemetry.close().await;
328 }
329 }
330
331 pub async fn client_instance_id(
341 &self,
342 timeout: Time,
343 ) -> Result<Option<uuid::Uuid>, ClientError> {
344 client_instance_id(self.telemetry.as_deref(), timeout).await
345 }
346
347 #[tracing::instrument(level = "debug", skip_all, fields(api_key = R::API_KEY), err(level = "debug"))]
353 pub async fn send<R: ProtocolRequest>(&self, req: R) -> Result<R::Response, ClientError> {
356 let conn = self.pool.least_loaded().await?;
357 conn.send(req).await
358 }
359
360 pub async fn send_at_least<R: ProtocolRequest>(
367 &self,
368 req: R,
369 min_version: i16,
370 ) -> Result<R::Response, ClientError> {
371 let conn = self.pool.least_loaded().await?;
372 let (broker_min, broker_max) = conn.advertised_api_range(R::API_KEY).unwrap_or((0, 0));
373 let client_min = R::MIN_VERSION.max(min_version);
374 let chosen = R::LATEST_STABLE_VERSION.min(broker_max);
375 if chosen < client_min || chosen < broker_min {
376 return Err(ClientError::IncompatibleVersion {
377 api_key: R::API_KEY,
378 broker_min,
379 broker_max,
380 client_min,
381 client_max: R::LATEST_STABLE_VERSION,
382 });
383 }
384 conn.send(req).await
385 }
386
387 pub async fn send_to_connection<R: ProtocolRequest>(
399 &self,
400 broker_id: Option<i32>,
401 req: R,
402 ) -> Result<R::Response, ClientError> {
403 let connection = match broker_id {
404 Some(id) => self.pool.get(id).await?,
405 None => self.pool.bootstrap_connection().await?,
406 };
407 connection.send(req).await
408 }
409
410 pub async fn send_to_connection_no_response<R: ProtocolRequest>(
417 &self,
418 broker_id: Option<i32>,
419 req: R,
420 ) -> Result<(), ClientError> {
421 let connection = match broker_id {
422 Some(id) => self.pool.get(id).await?,
423 None => self.pool.bootstrap_connection().await?,
424 };
425 connection.send_no_response(req).await
426 }
427
428 pub async fn send_no_response<R: ProtocolRequest>(&self, req: R) -> Result<(), ClientError> {
437 let conn = self.pool.least_loaded().await?;
438 conn.send_no_response(req).await
439 }
440
441 #[tracing::instrument(level = "debug", skip_all, fields(bootstrap = %self.bootstrap))]
447 pub async fn reconnect_bootstrap(&self) {
448 self.pool.evict_bootstrap();
449 self.pool.prefer_bootstrap();
452 if let Ok(addrs) = bootstrap::resolve_with_server_names(
453 &self.bootstrap,
454 self.options.dns_timeout,
455 self.options.client_dns_lookup,
456 )
457 .await
458 {
459 self.pool.replace_bootstrap_with_server_names(addrs);
460 }
461 }
462
463 async fn rebootstrap_metadata(&self) -> Result<(), ClientError> {
464 let addrs = bootstrap::resolve_with_server_names(
465 &self.bootstrap,
466 self.options.dns_timeout,
467 self.options.client_dns_lookup,
468 )
469 .await?;
470 self.pool.rebootstrap_with_server_names(addrs);
471 self.metadata_recovery.complete_attempt();
472 Ok(())
473 }
474
475 async fn request_metadata_from_current_cluster(
476 &self,
477 request: &MetadataRequest,
478 ) -> Result<metadata_response::MetadataResponse, ClientError> {
479 let broker_ids = self.pool.broker_ids_by_load();
480 if broker_ids.is_empty() {
481 let connection = self.pool.bootstrap_connection().await?;
482 return connection.send(request.clone()).await;
483 }
484
485 let mut last_error = None;
486 let mut empty_response = None;
487 for broker_id in broker_ids {
488 let connection = match self.pool.get(broker_id).await {
489 Ok(connection) => connection,
490 Err(
491 error @ (ClientError::Connect { .. }
492 | ClientError::Tls { .. }
493 | ClientError::Sasl { .. }
494 | ClientError::Timeout(_)
495 | ClientError::Disconnected
496 | ClientError::Io(_)),
497 ) => {
498 self.pool.evict(broker_id);
499 last_error = Some(error);
500 continue;
501 }
502 Err(error) => return Err(error),
503 };
504 match connection.send(request.clone()).await {
505 Ok(response)
506 if response.error_code == REBOOTSTRAP_REQUIRED
507 || !response.brokers.is_empty() =>
508 {
509 return Ok(response);
510 }
511 Ok(response) => empty_response = Some(response),
512 Err(
513 error @ (ClientError::Connect { .. }
514 | ClientError::Timeout(_)
515 | ClientError::Disconnected
516 | ClientError::Io(_)),
517 ) => {
518 self.pool.evict(broker_id);
519 last_error = Some(error);
520 }
521 Err(error) => return Err(error),
522 }
523 }
524
525 empty_response.map_or_else(|| Err(last_error.unwrap_or(ClientError::Disconnected)), Ok)
526 }
527
528 #[must_use]
534 pub fn finalized_features(&self) -> crate::FinalizedFeatures {
535 self.pool.finalized_features()
536 }
537
538 #[must_use]
547 #[cfg_attr(test, mutants::skip)]
548 pub fn knows_broker(&self, broker_id: i32) -> bool {
549 self.pool.knows_broker(broker_id)
550 }
551
552 #[must_use]
558 pub fn broker(&self, broker_id: i32) -> BrokerHandle<'_> {
559 BrokerHandle {
560 client: self,
561 broker_id,
562 }
563 }
564
565 #[cfg_attr(test, mutants::skip)]
572 pub fn evict_broker(&self, broker_id: i32) {
573 self.pool.evict(broker_id);
574 }
575
576 #[must_use]
579 pub fn metadata_topics(&self) -> &MetadataTopics {
580 &self.metadata_topics
581 }
582
583 pub async fn refresh_metadata(
596 &self,
597 ) -> Result<metadata_response::MetadataResponse, ClientError> {
598 let response = self
599 .refresh_metadata_with(self.metadata_topics.request())
600 .await?;
601 self.metadata_topics.mark_refreshed();
602 Ok(response)
603 }
604
605 #[cfg_attr(test, mutants::skip)]
619 #[tracing::instrument(
620 level = "info",
621 skip_all,
622 fields(brokers = field::Empty),
623 err(level = "info"),
624 )]
625 pub async fn refresh_metadata_with(
626 &self,
627 request: MetadataRequest,
628 ) -> Result<metadata_response::MetadataResponse, ClientError> {
629 self.metadata_recovery.begin_attempt();
630 let first = self.request_metadata_from_current_cluster(&request).await;
631 let resp = match first {
632 Ok(resp)
633 if resp.error_code == REBOOTSTRAP_REQUIRED
634 && self.metadata_recovery.strategy == MetadataRecoveryStrategy::Rebootstrap =>
635 {
636 self.rebootstrap_metadata().await?;
637 self.metadata_recovery.begin_attempt();
638 self.request_metadata_from_current_cluster(&request).await?
639 }
640 Ok(resp) if resp.error_code == REBOOTSTRAP_REQUIRED => {
641 return Err(ClientError::Server {
642 error_code: REBOOTSTRAP_REQUIRED,
643 });
644 }
645 Ok(resp) if resp.brokers.is_empty() && self.metadata_recovery.timed_out() => {
646 self.rebootstrap_metadata().await?;
647 self.metadata_recovery.begin_attempt();
648 self.request_metadata_from_current_cluster(&request).await?
649 }
650 Ok(resp) => resp,
651 Err(
657 error @ (ClientError::Connect { .. }
658 | ClientError::Tls { .. }
659 | ClientError::Sasl { .. }
660 | ClientError::Timeout(_)
661 | ClientError::Disconnected
662 | ClientError::Io(_)),
663 ) => {
664 if self.metadata_recovery.strategy == MetadataRecoveryStrategy::None {
665 return Err(error);
666 }
667 self.rebootstrap_metadata().await?;
668 self.metadata_recovery.begin_attempt();
669 self.request_metadata_from_current_cluster(&request).await?
670 }
671 Err(error) => return Err(error),
672 };
673 if resp.error_code == REBOOTSTRAP_REQUIRED {
674 return Err(ClientError::Server {
675 error_code: REBOOTSTRAP_REQUIRED,
676 });
677 }
678 if !resp.brokers.is_empty() {
679 self.metadata_recovery.complete_attempt();
680 }
681 let brokers: Vec<BrokerInfo> = resp
682 .brokers
683 .iter()
684 .map(|b| BrokerInfo {
685 id: b.node_id,
686 host: b.host.clone(),
687 port: b.port,
688 rack: b.rack.clone(),
689 })
690 .collect();
691 tracing::Span::current().record("brokers", brokers.len());
692 self.pool.refresh_brokers(&brokers).await;
693 Ok(resp)
694 }
695
696 #[tracing::instrument(
709 level = "debug",
710 skip_all,
711 fields(topic = %topic, partition, current_leader_epoch, leader_epoch),
712 err(level = "debug"),
713 )]
714 pub async fn offset_for_leader_epoch(
715 &self,
716 topic: &str,
717 partition: i32,
718 current_leader_epoch: i32,
719 leader_epoch: i32,
720 ) -> Result<offset_for_leader_epoch::EpochEndOffset, ClientError> {
721 let conn = match self.pool.bootstrap_connection().await {
722 Ok(conn) => conn,
723 Err(
724 ClientError::Connect { .. }
725 | ClientError::Tls { .. }
726 | ClientError::Sasl { .. }
727 | ClientError::Timeout(_)
728 | ClientError::Disconnected
729 | ClientError::Io(_),
730 ) => {
731 self.reconnect_bootstrap().await;
732 self.pool.bootstrap_connection().await?
733 }
734 Err(e) => return Err(e),
735 };
736 offset_for_leader_epoch::offset_for_leader_epoch(
737 &conn,
738 topic,
739 partition,
740 current_leader_epoch,
741 leader_epoch,
742 )
743 .await
744 }
745
746 #[tracing::instrument(
764 level = "debug",
765 skip_all,
766 fields(broker_id, topic = %topic, partition, current_leader_epoch, leader_epoch),
767 err(level = "debug"),
768 )]
769 pub async fn offset_for_leader_epoch_on(
770 &self,
771 broker_id: i32,
772 topic: &str,
773 partition: i32,
774 current_leader_epoch: i32,
775 leader_epoch: i32,
776 ) -> Result<offset_for_leader_epoch::EpochEndOffset, ClientError> {
777 let conn = match self.pool.get(broker_id).await {
778 Ok(conn) => conn,
779 Err(
780 ClientError::Connect { .. }
781 | ClientError::Tls { .. }
782 | ClientError::Sasl { .. }
783 | ClientError::Timeout(_)
784 | ClientError::Io(_),
785 ) if self.pool.knows_broker(broker_id) => {
786 self.pool.evict(broker_id);
787 self.refresh_metadata().await?;
788 self.pool.get(broker_id).await?
789 }
790 Err(e) => return Err(e),
791 };
792 offset_for_leader_epoch::offset_for_leader_epoch(
793 &conn,
794 topic,
795 partition,
796 current_leader_epoch,
797 leader_epoch,
798 )
799 .await
800 }
801
802 #[tracing::instrument(
812 level = "debug",
813 skip_all,
814 fields(
815 broker_id,
816 topic = %fetch.topic,
817 partition = fetch.partition,
818 fetch_offset = fetch.fetch_offset,
819 isolation_level = fetch.isolation_level,
820 ),
821 err(level = "debug"),
822 )]
823 pub async fn fetch_partition_with_isolation_on(
824 &self,
825 broker_id: i32,
826 fetch: fetch::IsolatedFetch<'_>,
827 ) -> Result<Vec<fetch::FetchedRecord>, ClientError> {
828 let conn = match self.pool.get(broker_id).await {
829 Ok(conn) => conn,
830 Err(ClientError::Disconnected) if !self.pool.knows_broker(broker_id) => {
831 self.pool.bootstrap_connection().await?
832 }
833 Err(
834 ClientError::Connect { .. }
835 | ClientError::Tls { .. }
836 | ClientError::Sasl { .. }
837 | ClientError::Timeout(_)
838 | ClientError::Io(_),
839 ) if self.pool.knows_broker(broker_id) => {
840 self.pool.evict(broker_id);
841 self.refresh_metadata().await?;
842 match self.pool.get(broker_id).await {
843 Ok(conn) => conn,
844 Err(ClientError::Disconnected) if !self.pool.knows_broker(broker_id) => {
845 self.pool.bootstrap_connection().await?
846 }
847 Err(error) => return Err(error),
848 }
849 }
850 Err(error) => return Err(error),
851 };
852 fetch::fetch_partition_with_isolation(&conn, fetch).await
853 }
854
855 #[cfg_attr(test, mutants::skip)]
858 pub fn close(self) {
859 if let Some(pool) = Arc::into_inner(self.pool) {
860 pool.close_all();
861 }
862 }
863}
864
865impl TelemetryConnections for Client {
866 fn open_connection(&self, preferred: Option<i32>) -> Option<(i32, Arc<Connection>)> {
867 self.pool.open_connection(preferred)
868 }
869}
870
871pub struct BrokerHandle<'a> {
875 client: &'a Client,
876 broker_id: i32,
877}
878
879impl BrokerHandle<'_> {
880 #[cfg_attr(test, mutants::skip)]
895 #[tracing::instrument(
896 level = "debug",
897 skip_all,
898 fields(broker_id = self.broker_id, api_key = R::API_KEY),
899 err(level = "debug"),
900 )]
901 pub async fn send<R: ProtocolRequest>(&self, req: R) -> Result<R::Response, ClientError> {
904 let conn = match self.client.pool.get(self.broker_id).await {
905 Ok(conn) => conn,
906 Err(ClientError::Disconnected) if !self.client.pool.knows_broker(self.broker_id) => {
907 self.client.pool.bootstrap_connection().await?
908 }
909 Err(
910 ClientError::Connect { .. }
911 | ClientError::Tls { .. }
912 | ClientError::Sasl { .. }
913 | ClientError::Timeout(_)
914 | ClientError::Io(_),
915 ) if self.client.pool.knows_broker(self.broker_id) => {
916 self.client.pool.evict(self.broker_id);
917 self.client.refresh_metadata().await?;
918 match self.client.pool.get(self.broker_id).await {
919 Ok(conn) => conn,
920 Err(ClientError::Disconnected)
921 if !self.client.pool.knows_broker(self.broker_id) =>
922 {
923 self.client.pool.bootstrap_connection().await?
924 }
925 Err(e) => return Err(e),
926 }
927 }
928 Err(e) => return Err(e),
929 };
930 conn.send(req).await
931 }
932
933 pub async fn send_no_response<R: ProtocolRequest>(&self, req: R) -> Result<(), ClientError> {
940 let conn = match self.client.pool.get(self.broker_id).await {
941 Ok(conn) => conn,
942 Err(ClientError::Disconnected) if !self.client.pool.knows_broker(self.broker_id) => {
943 self.client.pool.bootstrap_connection().await?
944 }
945 Err(
946 ClientError::Connect { .. }
947 | ClientError::Tls { .. }
948 | ClientError::Sasl { .. }
949 | ClientError::Timeout(_)
950 | ClientError::Io(_),
951 ) if self.client.pool.knows_broker(self.broker_id) => {
952 self.client.pool.evict(self.broker_id);
953 self.client.refresh_metadata().await?;
954 match self.client.pool.get(self.broker_id).await {
955 Ok(conn) => conn,
956 Err(ClientError::Disconnected)
957 if !self.client.pool.knows_broker(self.broker_id) =>
958 {
959 self.client.pool.bootstrap_connection().await?
960 }
961 Err(error) => return Err(error),
962 }
963 }
964 Err(error) => return Err(error),
965 };
966 conn.send_no_response(req).await
967 }
968}
969
970#[cfg(test)]
971mod bootstrap_failover_tests {
972 use std::{
973 net::SocketAddr,
974 sync::{
975 Arc,
976 atomic::{AtomicU16, AtomicUsize, Ordering},
977 },
978 time::Duration,
979 };
980
981 use bytes::BytesMut;
982 use krabka_protocol::{
983 Decode, Encode,
984 owned::{
985 api_versions_request,
986 api_versions_response::{ApiVersion, ApiVersionsResponse},
987 metadata_request::{self, MetadataRequestTopic},
988 metadata_response::{
989 FLEXIBLE_MIN as META_FLEXIBLE_MIN, MetadataResponse, MetadataResponseBroker,
990 },
991 },
992 };
993 use krabka_units::{bytes, mebibytes, millis};
994 use tokio::{
995 net::TcpStream,
996 task::yield_now,
997 time::{sleep, timeout},
998 };
999
1000 use super::*;
1001 use crate::mock::MockBroker;
1002
1003 #[tokio::test]
1004 async fn zero_dns_timeout_is_rejected_before_resolution() {
1005 let result = Client::builder()
1006 .bootstrap("unused.invalid:9092")
1007 .dns_timeout(millis(0))
1008 .build()
1009 .await;
1010 assert2::assert!(matches!(result, Err(ClientError::InvalidConfig(_))));
1011 }
1012
1013 #[tokio::test]
1014 async fn invalid_connection_resource_policy_fails_before_resolution() {
1015 let queue_result = Client::builder()
1016 .bootstrap("unused.invalid:9092")
1017 .dispatch_queue_capacity(0)
1018 .build()
1019 .await;
1020 let Err(queue_error) = queue_result else {
1021 panic!("zero queue capacity must fail");
1022 };
1023 assert2::assert!(queue_error.to_string().contains("dispatch queue capacity"));
1024
1025 let frame_result = Client::builder()
1026 .bootstrap("unused.invalid:9092")
1027 .frame_max(mebibytes(100) + bytes(1))
1028 .build()
1029 .await;
1030 let Err(frame_error) = frame_result else {
1031 panic!("frame limit above fixed ceiling must fail");
1032 };
1033 assert2::assert!(frame_error.to_string().contains("client frame max"));
1034
1035 assert2::assert!(
1036 MetadataRecoveryRebootstrapTrigger::new(Time::ZERO)
1037 .expect("zero is an immediate KIP-1102 trigger")
1038 .time()
1039 == Time::ZERO
1040 );
1041 assert2::assert!(
1042 MetadataRecoveryRebootstrapTrigger::default()
1043 == MetadataRecoveryRebootstrapTrigger::new(
1044 DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER
1045 )
1046 .unwrap()
1047 );
1048 let recovery_result = Client::builder()
1049 .bootstrap("unused.invalid:9092")
1050 .metadata_recovery_rebootstrap_trigger(Time::from_millis(-1))
1051 .build()
1052 .await;
1053 let Err(recovery_error) = recovery_result else {
1054 panic!("negative metadata recovery trigger must fail");
1055 };
1056 assert2::assert!(recovery_error.to_string().contains("metadata recovery"));
1057 }
1058
1059 fn api_versions_v0() -> Vec<u8> {
1060 let resp = ApiVersionsResponse {
1061 error_code: 0,
1062 api_keys: vec![
1063 ApiVersion {
1064 api_key: api_versions_request::API_KEY,
1065 min_version: 0,
1066 max_version: 3,
1067 ..Default::default()
1068 },
1069 ApiVersion {
1070 api_key: metadata_request::API_KEY,
1071 min_version: 0,
1072 max_version: 13,
1073 ..Default::default()
1074 },
1075 ],
1076 ..Default::default()
1077 };
1078 let mut buf = BytesMut::new();
1079 resp.encode(&mut buf, 0).unwrap();
1080 buf.to_vec()
1081 }
1082
1083 fn metadata_v(version: i16, node_id: i32) -> Vec<u8> {
1084 metadata_v_with_port(version, node_id, 9092)
1085 }
1086
1087 fn metadata_v_with_port(version: i16, node_id: i32, port: u16) -> Vec<u8> {
1088 let resp = MetadataResponse {
1089 brokers: vec![MetadataResponseBroker {
1090 node_id,
1091 host: "127.0.0.1".into(),
1092 port: i32::from(port),
1093 ..Default::default()
1094 }],
1095 ..Default::default()
1096 };
1097 let mut buf = BytesMut::new();
1098 if version >= META_FLEXIBLE_MIN {
1099 buf.extend_from_slice(&[0x00u8]); }
1101 resp.encode(&mut buf, version).unwrap();
1102 buf.to_vec()
1103 }
1104
1105 fn empty_metadata_v(version: i16, error_code: i16) -> Vec<u8> {
1106 let resp = MetadataResponse {
1107 error_code,
1108 ..Default::default()
1109 };
1110 let mut buf = BytesMut::new();
1111 if version >= META_FLEXIBLE_MIN {
1112 buf.extend_from_slice(&[0x00u8]);
1113 }
1114 resp.encode(&mut buf, version).unwrap();
1115 buf.to_vec()
1116 }
1117
1118 fn handler(
1119 node_id: i32,
1120 ) -> impl FnMut(i16, i16, i32, &[u8]) -> Option<Vec<u8>> + Send + 'static {
1121 move |api_key, version, _corr, _body| {
1122 if api_key == api_versions_request::API_KEY {
1123 return Some(api_versions_v0());
1124 }
1125 if api_key == metadata_request::API_KEY {
1126 return Some(metadata_v(version, node_id));
1127 }
1128 None
1129 }
1130 }
1131
1132 fn dynamic_port_handler(
1133 node_id: i32,
1134 port: Arc<AtomicU16>,
1135 ) -> impl FnMut(i16, i16, i32, &[u8]) -> Option<Vec<u8>> + Send + 'static {
1136 move |api_key, version, _corr, _body| {
1137 if api_key == api_versions_request::API_KEY {
1138 return Some(api_versions_v0());
1139 }
1140 if api_key == metadata_request::API_KEY {
1141 return Some(metadata_v_with_port(
1142 version,
1143 node_id,
1144 port.load(Ordering::SeqCst),
1145 ));
1146 }
1147 None
1148 }
1149 }
1150
1151 async fn wait_for_listener_closed(addr: SocketAddr) {
1160 timeout(Duration::from_secs(10), async {
1161 while TcpStream::connect(addr).await.is_ok() {
1162 yield_now().await;
1163 }
1164 })
1165 .await
1166 .expect("broker listener must close after stop()");
1167 }
1168
1169 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1170 async fn refresh_metadata_fails_over_when_bootstrap_broker_dies() {
1171 let a = MockBroker::start(handler(0)).await.unwrap();
1178 let b = MockBroker::start(handler(1)).await.unwrap();
1179 let bootstrap = format!("{},{}", a.addr, b.addr);
1180 let client = Client::builder()
1181 .bootstrap(bootstrap)
1182 .request_timeout(millis(500))
1183 .build()
1184 .await
1185 .expect("client connects to bootstrap A");
1186
1187 client
1189 .refresh_metadata()
1190 .await
1191 .expect("first refresh succeeds via A");
1192
1193 let a_addr = a.addr;
1196 a.stop();
1197 wait_for_listener_closed(a_addr).await;
1198
1199 let md = client
1202 .refresh_metadata()
1203 .await
1204 .expect("refresh must fail over to live bootstrap B after A dies");
1205 assert2::assert!(!md.brokers.is_empty());
1206 }
1207
1208 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1211 async fn refresh_metadata_with_sends_the_request_and_learns_the_brokers() {
1212 let orders = MetadataRequest {
1213 topics: Some(vec![MetadataRequestTopic {
1214 name: Some("orders".into()),
1215 ..Default::default()
1216 }]),
1217 allow_auto_topic_creation: true,
1218 ..Default::default()
1219 };
1220 for (name, request) in [
1221 ("all topics", crate::all_topics_request()),
1222 ("the topic orders", orders),
1223 ] {
1224 let seen = Arc::new(Mutex::new(Vec::new()));
1225 let handler_seen = Arc::clone(&seen);
1226 let broker = MockBroker::start(move |api_key, version, _corr, body| {
1227 if api_key == api_versions_request::API_KEY {
1228 return Some(api_versions_v0());
1229 }
1230 if api_key == metadata_request::API_KEY {
1231 let header_len =
1232 2 + "krabka".len() + usize::from(version >= metadata_request::FLEXIBLE_MIN);
1233 let mut request_body = &body[header_len..];
1234 let decoded = MetadataRequest::decode(&mut request_body, version)
1235 .expect("decode Metadata");
1236 handler_seen.lock().expect("seen").push(decoded);
1237 return Some(metadata_v(version, 7));
1238 }
1239 None
1240 })
1241 .await
1242 .unwrap();
1243 let client = Client::builder()
1244 .bootstrap(broker.addr.to_string())
1245 .request_timeout(millis(500))
1246 .build()
1247 .await
1248 .expect("client connects");
1249
1250 let refreshed = client.refresh_metadata_with(request.clone()).await.is_ok();
1251 let observed = (
1252 refreshed,
1253 seen.lock().expect("seen").clone(),
1254 client.knows_broker(7),
1255 );
1256 broker.stop();
1257 assert2::assert!(observed == (true, vec![request], true), "{name}");
1258 }
1259 }
1260
1261 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1262 async fn refresh_metadata_uses_a_learned_broker_after_the_only_seed_retires() {
1263 let healthy_port = Arc::new(AtomicU16::new(0));
1264 let healthy_calls = Arc::new(AtomicUsize::new(0));
1265 let h_port = Arc::clone(&healthy_port);
1266 let h_calls = Arc::clone(&healthy_calls);
1267 let healthy = MockBroker::start(move |api_key, version, _corr, _body| {
1268 if api_key == api_versions_request::API_KEY {
1269 return Some(api_versions_v0());
1270 }
1271 if api_key == metadata_request::API_KEY {
1272 h_calls.fetch_add(1, Ordering::SeqCst);
1273 return Some(metadata_v_with_port(
1274 version,
1275 7,
1276 h_port.load(Ordering::SeqCst),
1277 ));
1278 }
1279 None
1280 })
1281 .await
1282 .unwrap();
1283 healthy_port.store(healthy.addr.port(), Ordering::SeqCst);
1284
1285 let seed_port = Arc::clone(&healthy_port);
1286 let seed = MockBroker::start(dynamic_port_handler(7, seed_port))
1287 .await
1288 .unwrap();
1289 let client = Client::builder()
1290 .bootstrap(seed.addr.to_string())
1291 .request_timeout(millis(500))
1292 .build()
1293 .await
1294 .expect("client resolves its only seed");
1295 client
1296 .refresh_metadata()
1297 .await
1298 .expect("seed supplies the current broker metadata");
1299
1300 let seed_addr = seed.addr;
1301 seed.stop();
1302 wait_for_listener_closed(seed_addr).await;
1303
1304 let metadata = client
1305 .refresh_metadata()
1306 .await
1307 .expect("learned broker remains usable after seed retirement");
1308 assert2::assert!(metadata.brokers[0].node_id == 7);
1309 assert2::assert!(healthy_calls.load(Ordering::SeqCst) == 1);
1310 }
1311
1312 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1316 async fn send_goes_to_a_known_broker_after_the_first_metadata() {
1317 let counting_handler = |calls: Arc<AtomicUsize>, port: Arc<AtomicU16>| {
1318 move |api_key, version, _corr, _body: &[u8]| {
1319 if api_key == api_versions_request::API_KEY {
1320 return Some(api_versions_v0());
1321 }
1322 if api_key == metadata_request::API_KEY {
1323 calls.fetch_add(1, Ordering::SeqCst);
1324 return Some(metadata_v_with_port(
1325 version,
1326 7,
1327 port.load(Ordering::SeqCst),
1328 ));
1329 }
1330 None
1331 }
1332 };
1333 let port = Arc::new(AtomicU16::new(0));
1334 let seed_calls = Arc::new(AtomicUsize::new(0));
1335 let broker_calls = Arc::new(AtomicUsize::new(0));
1336 let broker = MockBroker::start(counting_handler(
1337 Arc::clone(&broker_calls),
1338 Arc::clone(&port),
1339 ))
1340 .await
1341 .unwrap();
1342 port.store(broker.addr.port(), Ordering::SeqCst);
1343 let seed = MockBroker::start(counting_handler(Arc::clone(&seed_calls), Arc::clone(&port)))
1344 .await
1345 .unwrap();
1346 let client = Client::builder()
1347 .bootstrap(seed.addr.to_string())
1348 .request_timeout(millis(500))
1349 .build()
1350 .await
1351 .expect("client builds");
1352
1353 client.send(MetadataRequest::default()).await.unwrap();
1354 client.refresh_metadata().await.unwrap();
1355 let before = (
1356 seed_calls.load(Ordering::SeqCst),
1357 broker_calls.load(Ordering::SeqCst),
1358 );
1359 for _ in 0..3 {
1360 client.send(MetadataRequest::default()).await.unwrap();
1361 }
1362 let after = (
1363 seed_calls.load(Ordering::SeqCst),
1364 broker_calls.load(Ordering::SeqCst),
1365 );
1366 seed.stop();
1367 broker.stop();
1368 assert2::assert!(before == (2, 0));
1369 assert2::assert!(after == (2, 3));
1370 }
1371
1372 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1373 async fn reconnect_bootstrap_forces_next_send_to_redial() {
1374 let a = MockBroker::start(handler(0)).await.unwrap();
1375 let b = MockBroker::start(handler(1)).await.unwrap();
1376 let bootstrap = format!("{},{}", a.addr, b.addr);
1377 let client = Client::builder()
1378 .bootstrap(bootstrap)
1379 .request_timeout(millis(500))
1380 .build()
1381 .await
1382 .expect("client builds");
1383
1384 let _ = client
1385 .send(metadata_request::MetadataRequest::default())
1386 .await
1387 .expect("first send succeeds via A");
1388
1389 let a_addr = a.addr;
1390 a.stop();
1391 wait_for_listener_closed(a_addr).await;
1392 client.reconnect_bootstrap().await;
1393
1394 let md = client
1395 .send(metadata_request::MetadataRequest::default())
1396 .await
1397 .expect("send after reconnect_bootstrap reaches B");
1398 assert2::assert!(!md.brokers.is_empty());
1399 }
1400
1401 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1402 async fn broker_send_refreshes_metadata_when_learned_address_is_stale() {
1403 let a_port = Arc::new(AtomicU16::new(0));
1404 let b_port = Arc::new(AtomicU16::new(0));
1405 let a = MockBroker::start(dynamic_port_handler(1, Arc::clone(&a_port)))
1406 .await
1407 .unwrap();
1408 a_port.store(a.addr.port(), Ordering::SeqCst);
1409 let b = MockBroker::start(dynamic_port_handler(1, Arc::clone(&b_port)))
1410 .await
1411 .unwrap();
1412 b_port.store(b.addr.port(), Ordering::SeqCst);
1413
1414 let bootstrap = format!("{},{}", a.addr, b.addr);
1415 let client = Client::builder()
1416 .bootstrap(bootstrap)
1417 .socket_connection_setup_timeout(millis(500))
1418 .request_timeout(millis(500))
1419 .build()
1420 .await
1421 .expect("client builds");
1422
1423 client
1424 .refresh_metadata()
1425 .await
1426 .expect("first metadata refresh learns broker 1 at A");
1427 let a_addr = a.addr;
1428 a.stop();
1429 wait_for_listener_closed(a_addr).await;
1430
1431 let md = client
1432 .broker(1)
1433 .send(metadata_request::MetadataRequest::default())
1434 .await
1435 .expect("broker send should refresh metadata and redial broker 1 at B");
1436 assert2::assert!(!md.brokers.is_empty());
1437 }
1438
1439 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1440 async fn metadata_error_129_rebootstraps_immediately() {
1441 let handshakes = Arc::new(AtomicUsize::new(0));
1442 let metadata_calls = Arc::new(AtomicUsize::new(0));
1443 let broker_port = Arc::new(AtomicU16::new(0));
1444 let h_handshakes = Arc::clone(&handshakes);
1445 let h_metadata_calls = Arc::clone(&metadata_calls);
1446 let h_broker_port = Arc::clone(&broker_port);
1447 let broker = MockBroker::start(move |api_key, version, _corr, _body| {
1448 if api_key == api_versions_request::API_KEY {
1449 h_handshakes.fetch_add(1, Ordering::SeqCst);
1450 return Some(api_versions_v0());
1451 }
1452 if api_key == metadata_request::API_KEY {
1453 let call = h_metadata_calls.fetch_add(1, Ordering::SeqCst);
1454 return Some(if call == 0 {
1455 empty_metadata_v(version, REBOOTSTRAP_REQUIRED)
1456 } else {
1457 metadata_v_with_port(version, 7, h_broker_port.load(Ordering::SeqCst))
1458 });
1459 }
1460 None
1461 })
1462 .await
1463 .unwrap();
1464 broker_port.store(broker.addr.port(), Ordering::SeqCst);
1465 let client = Client::builder()
1466 .bootstrap(broker.addr.to_string())
1467 .build()
1468 .await
1469 .expect("client builds");
1470
1471 let metadata = client
1472 .refresh_metadata()
1473 .await
1474 .expect("error 129 triggers one rebootstrap and retry");
1475
1476 assert2::assert!(metadata.brokers[0].node_id == 7);
1477 assert2::assert!(metadata_calls.load(Ordering::SeqCst) == 2);
1478 assert2::assert!(handshakes.load(Ordering::SeqCst) == 2);
1479 }
1480
1481 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1482 async fn stale_metadata_timeout_rebootstraps_after_first_bad_attempt() {
1483 let handshakes = Arc::new(AtomicUsize::new(0));
1484 let metadata_calls = Arc::new(AtomicUsize::new(0));
1485 let broker_port = Arc::new(AtomicU16::new(0));
1486 let h_handshakes = Arc::clone(&handshakes);
1487 let h_metadata_calls = Arc::clone(&metadata_calls);
1488 let h_broker_port = Arc::clone(&broker_port);
1489 let broker = MockBroker::start(move |api_key, version, _corr, _body| {
1490 if api_key == api_versions_request::API_KEY {
1491 h_handshakes.fetch_add(1, Ordering::SeqCst);
1492 return Some(api_versions_v0());
1493 }
1494 if api_key == metadata_request::API_KEY {
1495 let call = h_metadata_calls.fetch_add(1, Ordering::SeqCst);
1496 return Some(if call < 2 {
1497 empty_metadata_v(version, 0)
1498 } else {
1499 metadata_v_with_port(version, 8, h_broker_port.load(Ordering::SeqCst))
1500 });
1501 }
1502 None
1503 })
1504 .await
1505 .unwrap();
1506 broker_port.store(broker.addr.port(), Ordering::SeqCst);
1507 let client = Client::builder()
1508 .bootstrap(broker.addr.to_string())
1509 .metadata_recovery_rebootstrap_trigger(millis(20))
1510 .build()
1511 .await
1512 .expect("client builds");
1513
1514 let first = client
1515 .refresh_metadata()
1516 .await
1517 .expect("first empty metadata response remains observable");
1518 assert2::assert!(first.brokers.is_empty());
1519 sleep(Duration::from_millis(25)).await;
1520 let recovered = client
1521 .refresh_metadata()
1522 .await
1523 .expect("expired metadata attempt triggers rebootstrap");
1524
1525 assert2::assert!(recovered.brokers[0].node_id == 8);
1526 assert2::assert!(metadata_calls.load(Ordering::SeqCst) == 3);
1527 assert2::assert!(handshakes.load(Ordering::SeqCst) == 2);
1528 }
1529
1530 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1531 async fn metadata_recovery_none_surfaces_error_129_without_retry() {
1532 let metadata_calls = Arc::new(AtomicUsize::new(0));
1533 let h_metadata_calls = Arc::clone(&metadata_calls);
1534 let broker = MockBroker::start(move |api_key, version, _corr, _body| {
1535 if api_key == api_versions_request::API_KEY {
1536 return Some(api_versions_v0());
1537 }
1538 if api_key == metadata_request::API_KEY {
1539 h_metadata_calls.fetch_add(1, Ordering::SeqCst);
1540 return Some(empty_metadata_v(version, REBOOTSTRAP_REQUIRED));
1541 }
1542 None
1543 })
1544 .await
1545 .unwrap();
1546 let client = Client::builder()
1547 .bootstrap(broker.addr.to_string())
1548 .metadata_recovery_strategy(MetadataRecoveryStrategy::None)
1549 .build()
1550 .await
1551 .expect("client builds");
1552
1553 let error = client
1554 .refresh_metadata()
1555 .await
1556 .expect_err("disabled recovery surfaces the proxy request");
1557
1558 assert2::assert!(matches!(
1559 error,
1560 ClientError::Server {
1561 error_code: REBOOTSTRAP_REQUIRED
1562 }
1563 ));
1564 assert2::assert!(metadata_calls.load(Ordering::SeqCst) == 1);
1565 }
1566}
1567
1568#[cfg(test)]
1569mod authentication_failure_tests {
1570 use std::sync::{
1571 Arc,
1572 atomic::{AtomicU16, AtomicUsize, Ordering},
1573 };
1574
1575 use bytes::BytesMut;
1576 use krabka_protocol::{
1577 Encode,
1578 owned::{
1579 api_versions_request,
1580 api_versions_response::{ApiVersion, ApiVersionsResponse},
1581 metadata_request::{self, MetadataRequest},
1582 metadata_response::{MetadataResponse, MetadataResponseBroker},
1583 sasl_handshake_request,
1584 },
1585 };
1586 use krabka_security::ListenerProtocol;
1587
1588 use super::*;
1589 use crate::{
1590 OutboundSaslError, SaslAuthenticationError,
1591 error::AuthenticationError,
1592 mock::{MockBroker, MockReply, MockSaslAnswer},
1593 security::{ClientSecurity, SaslCredentials},
1594 };
1595
1596 const UNSUPPORTED_SASL_MECHANISM: i16 = 33;
1597 const ILLEGAL_SASL_STATE: i16 = 34;
1598 const UNSUPPORTED_VERSION: i16 = 35;
1599 const SASL_AUTHENTICATION_FAILED: i16 = 58;
1600 const BROKER_ID: i32 = 1;
1601
1602 #[derive(Clone, Copy, Debug)]
1604 enum Route {
1605 Bootstrap,
1607 Broker,
1610 }
1611
1612 #[derive(Debug, PartialEq, Eq)]
1614 enum Outcome {
1615 Ok,
1616 Rejected(SaslAuthenticationError),
1617 SaslServerError(i16),
1618 Other(String),
1619 }
1620
1621 #[derive(Debug, PartialEq, Eq)]
1622 struct Observed {
1623 outcome: Outcome,
1624 handshakes: usize,
1625 }
1626
1627 fn classify<T>(result: &Result<T, ClientError>) -> Outcome {
1628 match result {
1629 Ok(_) => Outcome::Ok,
1630 Err(ClientError::Authentication {
1631 source: AuthenticationError::Sasl(error),
1632 ..
1633 }) => Outcome::Rejected(error.clone()),
1634 Err(ClientError::Sasl {
1635 source: OutboundSaslError::Server { error_code, .. },
1636 ..
1637 }) => Outcome::SaslServerError(*error_code),
1638 Err(error) => Outcome::Other(error.to_string()),
1639 }
1640 }
1641
1642 fn encode(response: &impl Encode, version: i16) -> Vec<u8> {
1643 let mut body = BytesMut::new();
1644 response.encode(&mut body, version).unwrap();
1645 body.to_vec()
1646 }
1647
1648 async fn sasl_broker(script: Vec<MockSaslAnswer>, handshakes: Arc<AtomicUsize>) -> MockBroker {
1652 let port = Arc::new(AtomicU16::new(0));
1653 let handler_port = Arc::clone(&port);
1654 let mut answer = *script.last().unwrap();
1655 let broker = MockBroker::start_with_replies(move |api_key, version, _corr, _body| {
1656 if api_key == sasl_handshake_request::API_KEY {
1657 let connection = handshakes.fetch_add(1, Ordering::SeqCst);
1658 answer = script
1659 .get(connection)
1660 .copied()
1661 .unwrap_or_else(|| *script.last().unwrap());
1662 }
1663 if let Some(reply) = answer.reply(api_key, version).unwrap() {
1664 return reply;
1665 }
1666 match api_key {
1667 api_versions_request::API_KEY => MockReply::Respond(encode(
1668 &ApiVersionsResponse {
1669 api_keys: vec![
1670 ApiVersion {
1671 api_key: api_versions_request::API_KEY,
1672 min_version: 0,
1673 max_version: 3,
1674 ..Default::default()
1675 },
1676 ApiVersion {
1677 api_key: metadata_request::API_KEY,
1678 min_version: 0,
1679 max_version: 8,
1680 ..Default::default()
1681 },
1682 ],
1683 ..Default::default()
1684 },
1685 0,
1686 )),
1687 metadata_request::API_KEY => MockReply::Respond(encode(
1688 &MetadataResponse {
1689 brokers: vec![MetadataResponseBroker {
1690 node_id: BROKER_ID,
1691 host: "127.0.0.1".into(),
1692 port: i32::from(handler_port.load(Ordering::SeqCst)),
1693 ..Default::default()
1694 }],
1695 ..Default::default()
1696 },
1697 version,
1698 )),
1699 _ => MockReply::Silent,
1700 }
1701 })
1702 .await
1703 .unwrap();
1704 port.store(broker.addr.port(), Ordering::SeqCst);
1705 broker
1706 }
1707
1708 fn plain_security() -> ClientSecurity {
1709 ClientSecurity {
1710 protocol: ListenerProtocol::SaslPlaintext,
1711 tls: None,
1712 sasl: Some(SaslCredentials::Plain {
1713 username: "alice".into(),
1714 password: "secret".into(),
1715 }),
1716 sasl_host: None,
1717 }
1718 }
1719
1720 fn authenticate_failed() -> SaslAuthenticationError {
1721 SaslAuthenticationError::Failed(
1722 "SaslAuthenticate(PLAIN) error_code=58 error_message=Some(\"rejected by mock broker\")"
1723 .into(),
1724 )
1725 }
1726
1727 fn unsupported_mechanism() -> SaslAuthenticationError {
1728 SaslAuthenticationError::UnsupportedMechanism(
1729 "client SASL mechanism 'PLAIN' not enabled in the server, enabled mechanisms are \
1730 [\"PLAIN\"]"
1731 .into(),
1732 )
1733 }
1734
1735 fn illegal_state() -> SaslAuthenticationError {
1736 SaslAuthenticationError::IllegalState(
1737 "unexpected handshake request with client mechanism PLAIN, enabled mechanisms are \
1738 [\"PLAIN\"]"
1739 .into(),
1740 )
1741 }
1742
1743 #[tokio::test]
1748 async fn sasl_rejection_is_raised_without_a_retry() {
1749 use MockSaslAnswer::{Accept, AuthenticateError, CloseAfterHandshake, HandshakeError};
1750
1751 let cases = [
1752 (
1753 "bootstrap: SaslAuthenticate 58",
1754 Route::Bootstrap,
1755 vec![AuthenticateError(SASL_AUTHENTICATION_FAILED)],
1756 Observed {
1757 outcome: Outcome::Rejected(authenticate_failed()),
1758 handshakes: 1,
1759 },
1760 ),
1761 (
1762 "bootstrap: SaslHandshake 33",
1763 Route::Bootstrap,
1764 vec![HandshakeError(UNSUPPORTED_SASL_MECHANISM)],
1765 Observed {
1766 outcome: Outcome::Rejected(unsupported_mechanism()),
1767 handshakes: 1,
1768 },
1769 ),
1770 (
1771 "bootstrap: SaslHandshake 34",
1772 Route::Bootstrap,
1773 vec![HandshakeError(ILLEGAL_SASL_STATE)],
1774 Observed {
1775 outcome: Outcome::Rejected(illegal_state()),
1776 handshakes: 1,
1777 },
1778 ),
1779 (
1780 "bootstrap: SaslAuthenticate code that is not an authentication error",
1781 Route::Bootstrap,
1782 vec![AuthenticateError(UNSUPPORTED_VERSION)],
1783 Observed {
1784 outcome: Outcome::SaslServerError(UNSUPPORTED_VERSION),
1785 handshakes: 2,
1786 },
1787 ),
1788 (
1789 "bootstrap: close after SaslHandshake, then accept",
1790 Route::Bootstrap,
1791 vec![CloseAfterHandshake, Accept],
1792 Observed {
1793 outcome: Outcome::Ok,
1794 handshakes: 2,
1795 },
1796 ),
1797 (
1798 "broker: SaslAuthenticate 58",
1799 Route::Broker,
1800 vec![Accept, AuthenticateError(SASL_AUTHENTICATION_FAILED)],
1801 Observed {
1802 outcome: Outcome::Rejected(authenticate_failed()),
1803 handshakes: 2,
1804 },
1805 ),
1806 (
1807 "broker: SaslHandshake 33",
1808 Route::Broker,
1809 vec![Accept, HandshakeError(UNSUPPORTED_SASL_MECHANISM)],
1810 Observed {
1811 outcome: Outcome::Rejected(unsupported_mechanism()),
1812 handshakes: 2,
1813 },
1814 ),
1815 (
1816 "broker: close after SaslHandshake, then accept",
1817 Route::Broker,
1818 vec![Accept, CloseAfterHandshake, Accept],
1819 Observed {
1820 outcome: Outcome::Ok,
1821 handshakes: 3,
1822 },
1823 ),
1824 ];
1825
1826 for (name, route, script, expected) in cases {
1827 let handshakes = Arc::new(AtomicUsize::new(0));
1828 let broker = sasl_broker(script, Arc::clone(&handshakes)).await;
1829 let client = Client::builder()
1830 .bootstrap(broker.addr.to_string())
1831 .security(plain_security())
1832 .build()
1833 .await
1834 .unwrap();
1835
1836 let outcome = match route {
1837 Route::Bootstrap => classify(&client.refresh_metadata().await),
1838 Route::Broker => {
1839 client.refresh_metadata().await.unwrap();
1840 classify(
1841 &client
1842 .broker(BROKER_ID)
1843 .send(MetadataRequest::default())
1844 .await,
1845 )
1846 }
1847 };
1848 let observed = Observed {
1849 outcome,
1850 handshakes: handshakes.load(Ordering::SeqCst),
1851 };
1852
1853 broker.stop();
1854 assert2::check!(observed == expected, "case {name}");
1855 }
1856 }
1857}