1use anyhow::bail;
13use aws_config::SdkConfig;
14use fancy_regex::Regex;
15use std::collections::{BTreeMap, btree_map};
16use std::error::Error;
17use std::io;
18use std::net::{SocketAddr, ToSocketAddrs};
19use std::str::FromStr;
20use std::sync::Arc;
21use std::sync::Mutex;
22use std::time::Duration;
23use tokio::sync::watch;
24
25use anyhow::{Context, anyhow};
26use crossbeam::channel::{Receiver, Sender, unbounded};
27use mz_ore::collections::CollectionExt;
28use mz_ore::error::ErrorExt;
29use mz_ore::future::InTask;
30use mz_ore::netio::{DnsResolutionError, resolve_address};
31use mz_ssh_util::tunnel::{SshTimeoutConfig, SshTunnelConfig, SshTunnelStatus};
32use mz_ssh_util::tunnel_manager::{ManagedSshTunnelHandle, SshTunnelManager};
33use rdkafka::client::{Client, NativeClient, OAuthToken};
34use rdkafka::config::{
35 ClientConfig, FromClientConfig, FromClientConfigAndContext, RDKafkaLogLevel,
36};
37use rdkafka::consumer::{ConsumerContext, Rebalance};
38use rdkafka::error::{KafkaError, KafkaResult, RDKafkaErrorCode};
39use rdkafka::producer::{DefaultProducerContext, DeliveryResult, ProducerContext};
40use rdkafka::types::RDKafkaRespErr;
41use rdkafka::util::Timeout;
42use rdkafka::{ClientContext, Statistics, TopicPartitionList};
43use serde::{Deserialize, Serialize};
44use tokio::runtime::Handle;
45use tracing::{Level, debug, info, trace, warn};
46
47use crate::aws;
48
49pub const DEFAULT_TOPIC_METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
55
56pub struct MzClientContext {
63 error_tx: Sender<MzKafkaError>,
65 statistics_tx: watch::Sender<Statistics>,
68}
69
70impl Default for MzClientContext {
71 fn default() -> Self {
72 Self::with_errors().0
73 }
74}
75
76impl MzClientContext {
77 pub fn with_errors() -> (Self, Receiver<MzKafkaError>) {
82 let (error_tx, error_rx) = unbounded();
83 let (statistics_tx, _) = watch::channel(Default::default());
84 let ctx = Self {
85 error_tx,
86 statistics_tx,
87 };
88 (ctx, error_rx)
89 }
90
91 pub fn subscribe_statistics(&self) -> watch::Receiver<Statistics> {
94 self.statistics_tx.subscribe()
95 }
96
97 fn record_error(&self, msg: &str) {
98 let err = match MzKafkaError::from_str(msg) {
99 Ok(err) => err,
100 Err(()) => {
101 warn!(original_error = msg, "failed to parse kafka error");
102 MzKafkaError::Internal(msg.to_owned())
103 }
104 };
105 let _ = self.error_tx.send(err);
107 }
108}
109
110const PRIVATE_BROKER_ADDRESS_MARKER: &str = "Broker address resolved to a private IP";
114
115#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
117pub enum MzKafkaError {
118 #[error("Invalid username or password")]
120 InvalidCredentials,
121 #[error("Invalid CA certificate")]
123 InvalidCACertificate,
124 #[error("Disconnected during handshake; broker might require SSL encryption")]
126 SSLEncryptionMaybeRequired,
127 #[error("Broker does not support SSL connections")]
129 SSLUnsupported,
130 #[error("Broker did not provide a certificate")]
132 BrokerCertificateMissing,
133 #[error("Failed to verify broker certificate")]
135 InvalidBrokerCertificate,
136 #[error("Connection reset: {0}")]
138 ConnectionReset(String),
139 #[error("Connection timeout")]
141 ConnectionTimeout,
142 #[error("Failed to resolve hostname")]
144 HostnameResolutionFailed,
145 #[error("Broker address resolved to a private IP")]
147 PrivateBrokerAddress,
148 #[error("Unsupported SASL mechanism")]
150 UnsupportedSASLMechanism,
151 #[error("Unsupported broker version")]
153 UnsupportedBrokerVersion,
154 #[error("Broker transport failure")]
156 BrokerTransportFailure,
157 #[error("All brokers down")]
159 AllBrokersDown,
160 #[error("SASL authentication required")]
162 SaslAuthenticationRequired,
163 #[error("SASL authentication failed")]
165 SaslAuthenticationFailed,
166 #[error("SSL authentication required")]
168 SslAuthenticationRequired,
169 #[error("Unknown topic or partition")]
171 UnknownTopicOrPartition,
172 #[error("Internal kafka error: {0}")]
174 Internal(String),
175}
176
177impl FromStr for MzKafkaError {
178 type Err = ();
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 if s.contains("Authentication failed: Invalid username or password") {
182 Ok(Self::InvalidCredentials)
183 } else if s.contains("broker certificate could not be verified") {
184 Ok(Self::InvalidCACertificate)
185 } else if s.contains("connecting to a SSL listener?") {
186 Ok(Self::SSLEncryptionMaybeRequired)
187 } else if s.contains("client SSL authentication might be required") {
188 Ok(Self::SslAuthenticationRequired)
189 } else if s.contains("connecting to a PLAINTEXT broker listener") {
190 Ok(Self::SSLUnsupported)
191 } else if s.contains("Broker did not provide a certificate") {
192 Ok(Self::BrokerCertificateMissing)
193 } else if s.contains("Failed to verify broker certificate: ") {
194 Ok(Self::InvalidBrokerCertificate)
195 } else if let Some((_prefix, inner)) = s.split_once("Send failed: ") {
196 Ok(Self::ConnectionReset(inner.to_owned()))
197 } else if let Some((_prefix, inner)) = s.split_once("Receive failed: ") {
198 Ok(Self::ConnectionReset(inner.to_owned()))
199 } else if s.contains("request(s) timed out: disconnect") {
200 Ok(Self::ConnectionTimeout)
201 } else if s.contains(PRIVATE_BROKER_ADDRESS_MARKER) {
202 Ok(Self::PrivateBrokerAddress)
203 } else if s.contains("Failed to resolve") {
204 Ok(Self::HostnameResolutionFailed)
205 } else if s.contains("mechanism handshake failed:") {
206 Ok(Self::UnsupportedSASLMechanism)
207 } else if s.contains(
208 "verify that security.protocol is correctly configured, \
209 broker might require SASL authentication",
210 ) {
211 Ok(Self::SaslAuthenticationRequired)
212 } else if s.contains("SASL authentication error: Authentication failed") {
213 Ok(Self::SaslAuthenticationFailed)
214 } else if s
215 .contains("incorrect security.protocol configuration (connecting to a SSL listener?)")
216 {
217 Ok(Self::SslAuthenticationRequired)
218 } else if s.contains("probably due to broker version < 0.10") {
219 Ok(Self::UnsupportedBrokerVersion)
220 } else if s.contains("Disconnected while requesting ApiVersion")
221 || s.contains("Broker transport failure")
222 || s.contains("Connection refused")
223 {
224 Ok(Self::BrokerTransportFailure)
225 } else if Regex::new(r"\d+/\d+ brokers are down")
226 .unwrap()
227 .is_match(s)
228 .unwrap_or_default()
229 {
230 Ok(Self::AllBrokersDown)
232 } else if s.contains("Unknown topic or partition") || s.contains("Unknown partition") {
233 Ok(Self::UnknownTopicOrPartition)
234 } else {
235 Err(())
236 }
237 }
238}
239
240impl ClientContext for MzClientContext {
241 fn log(&self, level: rdkafka::config::RDKafkaLogLevel, fac: &str, log_message: &str) {
242 use rdkafka::config::RDKafkaLogLevel::*;
243
244 if matches!(level, Emerg | Alert | Critical | Error) || fac == "FAIL" {
252 self.record_error(log_message);
253 }
254
255 match level {
258 Emerg | Alert | Critical | Error => {
259 warn!(target: "librdkafka", "error: {} {}", fac, log_message);
264 }
265 Warning => warn!(target: "librdkafka", "warning: {} {}", fac, log_message),
266 Notice => info!(target: "librdkafka", "{} {}", fac, log_message),
267 Info => info!(target: "librdkafka", "{} {}", fac, log_message),
268 Debug => debug!(target: "librdkafka", "{} {}", fac, log_message),
269 }
270 }
271
272 fn stats(&self, statistics: Statistics) {
273 self.statistics_tx.send_replace(statistics);
274 }
275
276 fn error(&self, error: KafkaError, reason: &str) {
277 self.record_error(reason);
278 warn!(target: "librdkafka", "error: {}: {}", error, reason);
280 }
281}
282
283impl ConsumerContext for MzClientContext {}
284
285impl ProducerContext for MzClientContext {
286 type DeliveryOpaque = <DefaultProducerContext as ProducerContext>::DeliveryOpaque;
287 fn delivery(
288 &self,
289 delivery_result: &DeliveryResult<'_>,
290 delivery_opaque: Self::DeliveryOpaque,
291 ) {
292 DefaultProducerContext.delivery(delivery_result, delivery_opaque);
293 }
294}
295
296#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
298pub struct BrokerAddr {
299 pub host: String,
301 pub port: u16,
303}
304
305impl BrokerAddr {
306 pub fn to_socket_addrs(&self) -> Result<Vec<SocketAddr>, io::Error> {
308 Ok((self.host.as_str(), self.port).to_socket_addrs()?.collect())
309 }
310}
311
312#[derive(Debug, Clone)]
316pub struct BrokerRewrite {
317 pub host: String,
319 pub port: Option<u16>,
323}
324
325impl BrokerRewrite {
326 pub fn rewrite(&self, address: &BrokerAddr) -> BrokerAddr {
328 BrokerAddr {
329 host: self.host.clone(),
330 port: self.port.unwrap_or(address.port),
331 }
332 }
333}
334
335#[derive(Clone)]
336enum BrokerRewriteHandle {
337 Simple(BrokerRewrite),
338 SshTunnel(
339 ManagedSshTunnelHandle,
341 ),
342 FailedDefaultSshTunnel(String),
345}
346
347#[derive(Clone)]
348pub struct ConnectionRulePattern {
350 pub prefix_wildcard: bool,
352 pub literal_match: String,
354 pub suffix_wildcard: bool,
356}
357
358impl std::fmt::Display for ConnectionRulePattern {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 if self.prefix_wildcard {
361 f.write_str("*")?;
362 }
363 f.write_str(&self.literal_match)?;
364 if self.suffix_wildcard {
365 f.write_str("*")?;
366 }
367 Ok(())
368 }
369}
370
371impl ConnectionRulePattern {
372 pub fn matches(&self, address: &str) -> bool {
374 if self.prefix_wildcard {
375 if self.suffix_wildcard {
376 address.contains(&self.literal_match)
377 } else {
378 address.ends_with(&self.literal_match)
379 }
380 } else if self.suffix_wildcard {
381 address.starts_with(&self.literal_match)
382 } else {
383 address == self.literal_match
384 }
385 }
386}
387
388#[derive(Clone)]
389pub struct HostMappingRules {
391 pub rules: Vec<(ConnectionRulePattern, BrokerRewrite)>,
393}
394
395impl HostMappingRules {
396 pub fn rewrite(&self, src: &BrokerAddr) -> Option<BrokerAddr> {
399 let address = format!("{}:{}", src.host, src.port);
400 for (pattern, dst) in &self.rules {
401 if pattern.matches(&address) {
402 let result = dst.rewrite(src);
403 info!(
404 "HostMappingRules: broker {}:{} matched pattern '{}' -> rewriting to {}:{}",
405 src.host, src.port, pattern, result.host, result.port,
406 );
407 return Some(result);
408 }
409 }
410
411 warn!(
412 "HostMappingRules: broker {}:{} matched no rules, using original address",
413 src.host, src.port,
414 );
415 None
416 }
417}
418
419#[derive(Clone)]
422pub enum TunnelConfig {
423 Ssh(SshTunnelConfig),
425 StaticHost(String),
427 Rules(HostMappingRules),
429 None,
431}
432
433#[derive(Clone)]
435pub struct TunnelingClientContext<C> {
436 inner: C,
437 rewrites: Arc<Mutex<BTreeMap<BrokerAddr, BrokerRewriteHandle>>>,
438 default_tunnel: TunnelConfig,
439 in_task: InTask,
440 ssh_tunnel_manager: SshTunnelManager,
441 ssh_timeout_config: SshTimeoutConfig,
442 aws_config: Option<SdkConfig>,
443 runtime: Handle,
444 enforce_external_addresses: bool,
445}
446
447impl<C> TunnelingClientContext<C> {
448 pub fn new(
450 inner: C,
451 runtime: Handle,
452 ssh_tunnel_manager: SshTunnelManager,
453 ssh_timeout_config: SshTimeoutConfig,
454 aws_config: Option<SdkConfig>,
455 in_task: InTask,
456 enforce_external_addresses: bool,
457 ) -> TunnelingClientContext<C> {
458 TunnelingClientContext {
459 inner,
460 rewrites: Arc::new(Mutex::new(BTreeMap::new())),
461 default_tunnel: TunnelConfig::None,
462 in_task,
463 ssh_tunnel_manager,
464 ssh_timeout_config,
465 aws_config,
466 runtime,
467 enforce_external_addresses,
468 }
469 }
470
471 pub fn set_default_tunnel(&mut self, tunnel: TunnelConfig) {
476 self.default_tunnel = tunnel;
477 }
478
479 pub async fn add_ssh_tunnel(
486 &self,
487 broker: BrokerAddr,
488 tunnel: SshTunnelConfig,
489 ) -> Result<(), anyhow::Error> {
490 let ssh_tunnel = self
491 .ssh_tunnel_manager
492 .connect(
493 tunnel,
494 &broker.host,
495 broker.port,
496 self.ssh_timeout_config,
497 self.in_task,
498 )
499 .await
500 .context("creating ssh tunnel")?;
501
502 let mut rewrites = self.rewrites.lock().expect("poisoned");
503 rewrites.insert(broker, BrokerRewriteHandle::SshTunnel(ssh_tunnel));
504 Ok(())
505 }
506
507 pub fn add_broker_rewrite(&self, broker: BrokerAddr, rewrite: BrokerRewrite) {
513 let mut rewrites = self.rewrites.lock().expect("poisoned");
514 rewrites.insert(broker, BrokerRewriteHandle::Simple(rewrite));
515 }
516
517 pub fn inner(&self) -> &C {
519 &self.inner
520 }
521
522 pub fn tunnel_status(&self) -> SshTunnelStatus {
525 self.rewrites
526 .lock()
527 .expect("poisoned")
528 .values()
529 .map(|handle| match handle {
530 BrokerRewriteHandle::SshTunnel(s) => s.check_status(),
531 BrokerRewriteHandle::FailedDefaultSshTunnel(e) => {
532 SshTunnelStatus::Errored(e.clone())
533 }
534 BrokerRewriteHandle::Simple(_) => SshTunnelStatus::Running,
535 })
536 .fold(SshTunnelStatus::Running, |acc, status| {
537 match (acc, status) {
538 (SshTunnelStatus::Running, SshTunnelStatus::Errored(e))
539 | (SshTunnelStatus::Errored(e), SshTunnelStatus::Running) => {
540 SshTunnelStatus::Errored(e)
541 }
542 (SshTunnelStatus::Errored(err), SshTunnelStatus::Errored(e)) => {
543 SshTunnelStatus::Errored(format!("{}, {}", err, e))
544 }
545 (SshTunnelStatus::Running, SshTunnelStatus::Running) => {
546 SshTunnelStatus::Running
547 }
548 }
549 })
550 }
551}
552
553impl<C: ClientContext> TunnelingClientContext<C> {
554 fn resolve_direct(&self, addr: &BrokerAddr) -> Result<Vec<SocketAddr>, io::Error> {
562 if !self.enforce_external_addresses {
563 return addr.to_socket_addrs();
564 }
565 match self.runtime.block_on(resolve_address(&addr.host, true)) {
566 Ok(ips) => Ok(ips
567 .into_iter()
568 .map(|ip| SocketAddr::new(ip, addr.port))
569 .collect()),
570 Err(e) => {
571 if let DnsResolutionError::PrivateAddress = e {
574 self.inner.log(
575 RDKafkaLogLevel::Error,
576 "FAIL",
577 &format!(
578 "{PRIVATE_BROKER_ADDRESS_MARKER}: {}:{} is not routable on the public internet",
579 addr.host, addr.port
580 ),
581 );
582 }
583 Err(io::Error::other(e))
584 }
585 }
586 }
587}
588
589impl<C> ClientContext for TunnelingClientContext<C>
590where
591 C: ClientContext,
592{
593 const ENABLE_REFRESH_OAUTH_TOKEN: bool = true;
594
595 fn generate_oauth_token(
596 &self,
597 _oauthbearer_config: Option<&str>,
598 ) -> Result<OAuthToken, Box<dyn Error>> {
599 info!(target: "librdkafka", "generating OAuth token");
610
611 let generate = || {
612 let Some(sdk_config) = &self.aws_config else {
613 bail!("internal error: AWS configuration missing");
614 };
615
616 self.runtime.block_on(aws::generate_auth_token(sdk_config))
617 };
618
619 match generate() {
620 Ok((token, lifetime_ms)) => {
621 info!(target: "librdkafka", %lifetime_ms, "successfully generated OAuth token");
622 trace!(target: "librdkafka", %token);
623 Ok(OAuthToken {
624 token,
625 lifetime_ms,
626 principal_name: "".to_string(),
627 })
628 }
629 Err(e) => {
630 warn!(target: "librdkafka", "failed to generate OAuth token: {e:#}");
631 Err(e.into())
632 }
633 }
634 }
635
636 fn resolve_broker_addr(&self, host: &str, port: u16) -> Result<Vec<SocketAddr>, io::Error> {
641 info!("kafka: resolve_broker_addr called for {}:{}", host, port);
642 let return_rewrite = |rewrite: &BrokerRewriteHandle| -> Result<Vec<SocketAddr>, io::Error> {
643 let rewrite = match rewrite {
644 BrokerRewriteHandle::Simple(rewrite) => rewrite.clone(),
645 BrokerRewriteHandle::SshTunnel(ssh_tunnel) => {
646 let addr = ssh_tunnel.local_addr();
649 BrokerRewrite {
650 host: addr.ip().to_string(),
651 port: Some(addr.port()),
652 }
653 }
654 BrokerRewriteHandle::FailedDefaultSshTunnel(_) => {
655 unreachable!()
656 }
657 };
658 let rewrite_port = rewrite.port.unwrap_or(port);
659
660 info!(
661 "rewriting broker {}:{} to {}:{}",
662 host, port, rewrite.host, rewrite_port
663 );
664
665 (rewrite.host, rewrite_port)
666 .to_socket_addrs()
667 .map(|addrs| addrs.collect())
668 };
669
670 let addr = BrokerAddr {
671 host: host.into(),
672 port,
673 };
674 let rewrite = self.rewrites.lock().expect("poisoned").get(&addr).cloned();
675
676 match rewrite {
677 None | Some(BrokerRewriteHandle::FailedDefaultSshTunnel(_)) => {
679 match &self.default_tunnel {
681 TunnelConfig::Ssh(default_tunnel) => {
684 let ssh_tunnel = self.runtime.block_on(async {
688 self.ssh_tunnel_manager
689 .connect(
690 default_tunnel.clone(),
691 host,
692 port,
693 self.ssh_timeout_config,
694 self.in_task,
695 )
696 .await
697 });
698 match ssh_tunnel {
699 Ok(ssh_tunnel) => {
701 let mut rewrites = self.rewrites.lock().expect("poisoned");
702 let rewrite = match rewrites.entry(addr.clone()) {
703 btree_map::Entry::Occupied(mut o)
704 if matches!(
705 o.get(),
706 BrokerRewriteHandle::FailedDefaultSshTunnel(_)
707 ) =>
708 {
709 o.insert(BrokerRewriteHandle::SshTunnel(
710 ssh_tunnel.clone(),
711 ));
712 o.into_mut()
713 }
714 btree_map::Entry::Occupied(o) => o.into_mut(),
715 btree_map::Entry::Vacant(v) => {
716 v.insert(BrokerRewriteHandle::SshTunnel(ssh_tunnel.clone()))
717 }
718 };
719
720 return_rewrite(rewrite)
721 }
722 Err(e) => {
724 warn!(
725 "failed to create ssh tunnel for {:?}: {}",
726 addr,
727 e.display_with_causes()
728 );
729
730 let mut rewrites = self.rewrites.lock().expect("poisoned");
732 rewrites.entry(addr.clone()).or_insert_with(|| {
733 BrokerRewriteHandle::FailedDefaultSshTunnel(
734 e.to_string_with_causes(),
735 )
736 });
737
738 Err(io::Error::new(
739 io::ErrorKind::Other,
740 "creating SSH tunnel failed",
741 ))
742 }
743 }
744 }
745 TunnelConfig::StaticHost(host) => (host.as_str(), port)
747 .to_socket_addrs()
748 .map(|addrs| addrs.collect()),
749 TunnelConfig::Rules(rules) => {
751 let (resolved, result) = match rules.rewrite(&addr) {
753 Some(rewritten) => {
754 let result = rewritten.to_socket_addrs();
755 (rewritten, result)
756 }
757 None => (addr.clone(), self.resolve_direct(&addr)),
758 };
759 match result {
760 Ok(addrs) => {
761 info!(
762 "kafka: resolve_broker_addr {}:{} -> {}:{} resolved to {:?}",
763 host, port, resolved.host, resolved.port, addrs,
764 );
765 Ok(addrs)
766 }
767 Err(e) => {
768 warn!(
769 "kafka: resolve_broker_addr {}:{} -> {}:{} DNS resolution FAILED: {e}",
770 host, port, resolved.host, resolved.port,
771 );
772 Err(e)
773 }
774 }
775 }
776 TunnelConfig::None => self.resolve_direct(&addr),
778 }
779 }
780 Some(rewrite) => {
782 info!(
783 "kafka: resolve_broker_addr {}:{} using cached rewrite",
784 host, port
785 );
786 return_rewrite(&rewrite)
787 }
788 }
789 }
790
791 fn log(&self, level: RDKafkaLogLevel, fac: &str, log_message: &str) {
792 self.inner.log(level, fac, log_message)
793 }
794
795 fn error(&self, error: KafkaError, reason: &str) {
796 self.inner.error(error, reason)
797 }
798
799 fn stats(&self, statistics: Statistics) {
800 self.inner.stats(statistics)
801 }
802
803 fn stats_raw(&self, statistics: &[u8]) {
804 self.inner.stats_raw(statistics)
805 }
806}
807
808impl<C> ConsumerContext for TunnelingClientContext<C>
809where
810 C: ConsumerContext,
811{
812 fn rebalance(
813 &self,
814 native_client: &NativeClient,
815 err: RDKafkaRespErr,
816 tpl: &mut TopicPartitionList,
817 ) {
818 self.inner.rebalance(native_client, err, tpl)
819 }
820
821 fn pre_rebalance<'a>(&self, rebalance: &Rebalance<'a>) {
822 self.inner.pre_rebalance(rebalance)
823 }
824
825 fn post_rebalance<'a>(&self, rebalance: &Rebalance<'a>) {
826 self.inner.post_rebalance(rebalance)
827 }
828
829 fn commit_callback(&self, result: KafkaResult<()>, offsets: &TopicPartitionList) {
830 self.inner.commit_callback(result, offsets)
831 }
832
833 fn main_queue_min_poll_interval(&self) -> Timeout {
834 self.inner.main_queue_min_poll_interval()
835 }
836}
837
838impl<C> ProducerContext for TunnelingClientContext<C>
839where
840 C: ProducerContext,
841{
842 type DeliveryOpaque = C::DeliveryOpaque;
843
844 fn delivery(
845 &self,
846 delivery_result: &DeliveryResult<'_>,
847 delivery_opaque: Self::DeliveryOpaque,
848 ) {
849 self.inner.delivery(delivery_result, delivery_opaque)
850 }
851}
852
853pub type PartitionId = i32;
855
856#[derive(Debug, thiserror::Error)]
858pub enum GetPartitionsError {
859 #[error("Topic does not exist")]
861 TopicDoesNotExist,
862 #[error(transparent)]
864 Kafka(#[from] KafkaError),
865 #[error(transparent)]
867 Other(#[from] anyhow::Error),
868}
869
870pub fn get_partitions<C: ClientContext>(
872 client: &Client<C>,
873 topic: &str,
874 timeout: Duration,
875) -> Result<Vec<PartitionId>, GetPartitionsError> {
876 let meta = client.fetch_metadata(Some(topic), timeout)?;
877 if meta.topics().len() != 1 {
878 Err(anyhow!(
879 "topic {} has {} metadata entries; expected 1",
880 topic,
881 meta.topics().len()
882 ))?;
883 }
884
885 fn check_err(err: Option<RDKafkaRespErr>) -> Result<(), GetPartitionsError> {
886 match err.map(RDKafkaErrorCode::from) {
887 Some(RDKafkaErrorCode::UnknownTopic | RDKafkaErrorCode::UnknownTopicOrPartition) => {
888 Err(GetPartitionsError::TopicDoesNotExist)
889 }
890 Some(code) => Err(anyhow!(code))?,
891 None => Ok(()),
892 }
893 }
894
895 let meta_topic = meta.topics().into_element();
896 check_err(meta_topic.error())?;
897
898 if meta_topic.name() != topic {
899 Err(anyhow!(
900 "got results for wrong topic {} (expected {})",
901 meta_topic.name(),
902 topic
903 ))?;
904 }
905
906 let mut partition_ids = Vec::with_capacity(meta_topic.partitions().len());
907 for partition_meta in meta_topic.partitions() {
908 check_err(partition_meta.error())?;
909
910 partition_ids.push(partition_meta.id());
911 }
912
913 if partition_ids.len() == 0 {
914 Err(GetPartitionsError::TopicDoesNotExist)?;
915 }
916
917 Ok(partition_ids)
918}
919
920pub const DEFAULT_KEEPALIVE: bool = true;
922pub const DEFAULT_SOCKET_TIMEOUT: Duration = Duration::from_secs(60);
925pub const DEFAULT_TRANSACTION_TIMEOUT: Duration = Duration::from_secs(600);
928pub const DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT: Duration = Duration::from_secs(30);
931pub const DEFAULT_FETCH_METADATA_TIMEOUT: Duration = Duration::from_secs(10);
933pub const DEFAULT_PROGRESS_RECORD_FETCH_TIMEOUT: Duration = Duration::from_secs(90);
936
937#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
939pub struct TimeoutConfig {
940 pub keepalive: bool,
942 pub socket_timeout: Duration,
945 pub transaction_timeout: Duration,
947 pub socket_connection_setup_timeout: Duration,
949 pub fetch_metadata_timeout: Duration,
951 pub progress_record_fetch_timeout: Duration,
953}
954
955impl Default for TimeoutConfig {
956 fn default() -> Self {
957 TimeoutConfig {
958 keepalive: DEFAULT_KEEPALIVE,
959 socket_timeout: DEFAULT_SOCKET_TIMEOUT,
960 transaction_timeout: DEFAULT_TRANSACTION_TIMEOUT,
961 socket_connection_setup_timeout: DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT,
962 fetch_metadata_timeout: DEFAULT_FETCH_METADATA_TIMEOUT,
963 progress_record_fetch_timeout: DEFAULT_PROGRESS_RECORD_FETCH_TIMEOUT,
964 }
965 }
966}
967
968impl TimeoutConfig {
969 pub fn build(
972 keepalive: bool,
973 socket_timeout: Option<Duration>,
974 transaction_timeout: Duration,
975 socket_connection_setup_timeout: Duration,
976 fetch_metadata_timeout: Duration,
977 progress_record_fetch_timeout: Option<Duration>,
978 ) -> TimeoutConfig {
979 let transaction_timeout = if transaction_timeout.as_millis() > i32::MAX.try_into().unwrap()
986 {
987 warn!(
988 "transaction_timeout ({transaction_timeout:?}) greater than max \
989 of {}, defaulting to the default of {DEFAULT_TRANSACTION_TIMEOUT:?}",
990 i32::MAX
991 );
992 DEFAULT_TRANSACTION_TIMEOUT
993 } else if transaction_timeout.as_millis() < 1000 {
994 warn!(
995 "transaction_timeout ({transaction_timeout:?}) less than max \
996 of 1000ms, defaulting to the default of {DEFAULT_TRANSACTION_TIMEOUT:?}"
997 );
998 DEFAULT_TRANSACTION_TIMEOUT
999 } else {
1000 transaction_timeout
1001 };
1002
1003 let progress_record_fetch_timeout_derived_default =
1004 std::cmp::max(transaction_timeout, DEFAULT_PROGRESS_RECORD_FETCH_TIMEOUT);
1005 let progress_record_fetch_timeout =
1006 progress_record_fetch_timeout.unwrap_or(progress_record_fetch_timeout_derived_default);
1007 let progress_record_fetch_timeout = if progress_record_fetch_timeout < transaction_timeout {
1008 warn!(
1009 "progress record fetch ({progress_record_fetch_timeout:?}) less than transaction \
1010 timeout ({transaction_timeout:?}), defaulting to transaction timeout {transaction_timeout:?}",
1011 );
1012 transaction_timeout
1013 } else {
1014 progress_record_fetch_timeout
1015 };
1016
1017 let max_socket_timeout = std::cmp::min(
1020 transaction_timeout + Duration::from_millis(100),
1021 Duration::from_secs(300),
1022 );
1023 let socket_timeout_derived_default =
1024 std::cmp::min(max_socket_timeout, DEFAULT_SOCKET_TIMEOUT);
1025 let socket_timeout = socket_timeout.unwrap_or(socket_timeout_derived_default);
1026 let socket_timeout = if socket_timeout > max_socket_timeout {
1027 warn!(
1028 "socket_timeout ({socket_timeout:?}) greater than max \
1029 of min(30000, transaction.timeout.ms + 100 ({})), \
1030 defaulting to the maximum of {max_socket_timeout:?}",
1031 transaction_timeout.as_millis() + 100
1032 );
1033 max_socket_timeout
1034 } else if socket_timeout.as_millis() < 10 {
1035 warn!(
1036 "socket_timeout ({socket_timeout:?}) less than min \
1037 of 10ms, defaulting to the default of {socket_timeout_derived_default:?}"
1038 );
1039 socket_timeout_derived_default
1040 } else {
1041 socket_timeout
1042 };
1043
1044 let socket_connection_setup_timeout =
1045 if socket_connection_setup_timeout.as_millis() > i32::MAX.try_into().unwrap() {
1046 warn!(
1047 "socket_connection_setup_timeout ({socket_connection_setup_timeout:?}) \
1048 greater than max of {}ms, defaulting to the default \
1049 of {DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT:?}",
1050 i32::MAX,
1051 );
1052 DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT
1053 } else if socket_connection_setup_timeout.as_millis() < 10 {
1054 warn!(
1055 "socket_connection_setup_timeout ({socket_connection_setup_timeout:?}) \
1056 less than max of 10ms, defaulting to the default of \
1057 {DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT:?}"
1058 );
1059 DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT
1060 } else {
1061 socket_connection_setup_timeout
1062 };
1063
1064 TimeoutConfig {
1065 keepalive,
1066 socket_timeout,
1067 transaction_timeout,
1068 socket_connection_setup_timeout,
1069 fetch_metadata_timeout,
1070 progress_record_fetch_timeout,
1071 }
1072 }
1073}
1074
1075pub fn create_new_client_config_simple() -> ClientConfig {
1078 create_new_client_config(tracing::Level::INFO, Default::default())
1079}
1080
1081pub fn create_new_client_config(
1085 tracing_level: Level,
1086 timeout_config: TimeoutConfig,
1087) -> ClientConfig {
1088 #[allow(clippy::disallowed_methods)]
1089 let mut config = ClientConfig::new();
1090
1091 let level = if tracing_level >= Level::DEBUG {
1092 RDKafkaLogLevel::Debug
1093 } else if tracing_level >= Level::INFO {
1094 RDKafkaLogLevel::Info
1095 } else if tracing_level >= Level::WARN {
1096 RDKafkaLogLevel::Warning
1097 } else {
1098 RDKafkaLogLevel::Error
1099 };
1100 tracing::debug!(target: "librdkafka", level = ?level, "Determined log level for librdkafka");
1117 config.set_log_level(level);
1118
1119 if tracing_level >= Level::DEBUG {
1124 tracing::debug!(target: "librdkafka", "Enabling debug logs for rdkafka");
1125 config.set("debug", "all");
1126 }
1127
1128 if timeout_config.keepalive {
1129 config.set("socket.keepalive.enable", "true");
1130 }
1131
1132 config.set(
1133 "socket.timeout.ms",
1134 timeout_config.socket_timeout.as_millis().to_string(),
1135 );
1136 config.set(
1137 "socket.connection.setup.timeout.ms",
1138 timeout_config
1139 .socket_connection_setup_timeout
1140 .as_millis()
1141 .to_string(),
1142 );
1143
1144 config
1145}
1146
1147pub fn create<T: FromClientConfig>(config: &ClientConfig) -> KafkaResult<T> {
1152 #[allow(clippy::disallowed_methods)]
1153 let client = config.create();
1154 drop(openssl::error::ErrorStack::get());
1155 client
1156}
1157
1158pub fn create_with_context<C, T>(config: &ClientConfig, context: C) -> KafkaResult<T>
1162where
1163 C: ClientContext,
1164 T: FromClientConfigAndContext<C>,
1165{
1166 #[allow(clippy::disallowed_methods)]
1167 let client = config.create_with_context(context);
1168 drop(openssl::error::ErrorStack::get());
1177 client
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 use openssl::asn1::Asn1Time;
1185 use openssl::hash::MessageDigest;
1186 use openssl::pkey::PKey;
1187 use openssl::rsa::Rsa;
1188 use openssl::x509::{X509, X509NameBuilder};
1189 use rdkafka::consumer::BaseConsumer;
1190
1191 fn self_signed_cert_pem() -> String {
1192 let key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
1193 let mut name = X509NameBuilder::new().unwrap();
1194 name.append_entry_by_text("CN", "test").unwrap();
1195 let name = name.build();
1196 let mut cert = X509::builder().unwrap();
1197 cert.set_version(2).unwrap();
1198 cert.set_subject_name(&name).unwrap();
1199 cert.set_issuer_name(&name).unwrap();
1200 cert.set_pubkey(&key).unwrap();
1201 cert.set_not_before(&Asn1Time::days_from_now(0).unwrap())
1202 .unwrap();
1203 cert.set_not_after(&Asn1Time::days_from_now(1).unwrap())
1204 .unwrap();
1205 cert.sign(&key, MessageDigest::sha256()).unwrap();
1206 String::from_utf8(cert.build().to_pem().unwrap()).unwrap()
1207 }
1208
1209 #[mz_ore::test]
1210 fn all_brokers_down_parses_regardless_of_counts() {
1211 for msg in ["1/1 brokers are down", "0/2 brokers are down"] {
1212 assert!(
1213 matches!(
1214 MzKafkaError::from_str(msg),
1215 Ok(MzKafkaError::AllBrokersDown)
1216 ),
1217 "{msg}"
1218 );
1219 }
1220 }
1221
1222 #[mz_ore::test]
1223 #[cfg_attr(miri, ignore)] fn resolve_direct_broker_enforces_external_addresses() {
1225 let runtime = tokio::runtime::Runtime::new().unwrap();
1226 let loopback = BrokerAddr {
1227 host: "127.0.0.1".into(),
1228 port: 9092,
1229 };
1230 let no_match = HostMappingRules {
1231 rules: vec![(
1232 ConnectionRulePattern {
1233 prefix_wildcard: false,
1234 literal_match: "other:9092".into(),
1235 suffix_wildcard: false,
1236 },
1237 BrokerRewrite {
1238 host: "rewritten".into(),
1239 port: None,
1240 },
1241 )],
1242 };
1243 for tunnel in [TunnelConfig::None, TunnelConfig::Rules(no_match)] {
1244 for enforce in [false, true] {
1245 let (inner, errors) = MzClientContext::with_errors();
1246 let mut cx = TunnelingClientContext::new(
1247 inner,
1248 runtime.handle().clone(),
1249 SshTunnelManager::default(),
1250 SshTimeoutConfig::default(),
1251 None,
1252 InTask::No,
1253 enforce,
1254 );
1255 cx.set_default_tunnel(tunnel.clone());
1256 let result = cx.resolve_broker_addr(&loopback.host, loopback.port);
1257 if enforce {
1258 assert!(result.is_err(), "loopback broker should be rejected");
1259 assert_eq!(
1260 errors.try_iter().collect::<Vec<_>>(),
1261 vec![MzKafkaError::PrivateBrokerAddress],
1262 );
1263 } else {
1264 assert_eq!(
1265 result.expect("loopback broker should resolve"),
1266 loopback.to_socket_addrs().unwrap(),
1267 );
1268 }
1269 }
1270 }
1271 }
1272
1273 #[mz_ore::test]
1274 #[cfg_attr(miri, ignore)] fn test_create_with_context_clears_openssl_error_queue() {
1276 let mut config = create_new_client_config_simple();
1277 config.set("bootstrap.servers", "localhost:1");
1279 config.set("security.protocol", "ssl");
1280 config.set("ssl.ca.pem", self_signed_cert_pem());
1281 let _consumer: BaseConsumer =
1282 create_with_context(&config, rdkafka::consumer::DefaultConsumerContext).unwrap();
1283 let errors = openssl::error::ErrorStack::get();
1284 assert!(
1285 errors.errors().is_empty(),
1286 "stale OpenSSL errors after client creation: {errors}"
1287 );
1288 }
1289
1290 #[mz_ore::test]
1291 fn test_connection_rule_pattern_matches() {
1292 let p = ConnectionRulePattern {
1293 prefix_wildcard: false,
1294 literal_match: "broker:9092".to_string(),
1295 suffix_wildcard: false,
1296 };
1297 assert!(p.matches("broker:9092"));
1298 assert!(!p.matches("other:9092"));
1299
1300 let p = ConnectionRulePattern {
1301 prefix_wildcard: true,
1302 literal_match: ":9092".to_string(),
1303 suffix_wildcard: false,
1304 };
1305 assert!(p.matches("any-host:9092"));
1306 assert!(!p.matches("broker:9093"));
1307
1308 let p = ConnectionRulePattern {
1309 prefix_wildcard: false,
1310 literal_match: "broker:".to_string(),
1311 suffix_wildcard: true,
1312 };
1313 assert!(p.matches("broker:9092"));
1314 assert!(!p.matches("other:9092"));
1315
1316 let p = ConnectionRulePattern {
1317 prefix_wildcard: true,
1318 literal_match: "broker".to_string(),
1319 suffix_wildcard: true,
1320 };
1321 assert!(p.matches("my-broker-host:1234"));
1322 assert!(!p.matches("other:9092"));
1323 }
1324}