Skip to main content

rustfs_targets/target/
kafka.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::plugin::PluginEvent;
16use crate::{
17    StoreError, Target,
18    arn::TargetID,
19    error::TargetError,
20    runtime::tls::{
21        ReloadableTargetTls, TargetTlsInputSet, TlsReloadAdapter, config::ReloadApplyMode, fingerprint::TargetTlsGeneration,
22        validate::validate_tls_material,
23    },
24    store::{Key, Store},
25    target::{
26        ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
27        TargetTlsState, TargetType, build_queued_payload, build_target_tls_fingerprint, is_connectivity_error,
28        open_target_queue_store, persist_queued_payload_to_store,
29    },
30};
31use async_trait::async_trait;
32use rustfs_kafka_async::error::{ConnectionError, Error as KafkaError, KafkaCode};
33use rustfs_kafka_async::{AsyncProducer, AsyncProducerConfig, Record, RequiredAcks, SaslConfig, SecurityConfig};
34use rustfs_tls_runtime::{load_cert_bundle_der_bytes, load_private_key};
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::{fmt, future::Future, marker::PhantomData, sync::Arc, time::Duration};
37use tokio::sync::Mutex;
38use tracing::{debug, error, info, instrument, warn};
39
40pub(crate) const KAFKA_SASL_PLAIN: &str = "PLAIN";
41pub(crate) const KAFKA_SASL_SCRAM_SHA_256: &str = "SCRAM-SHA-256";
42pub(crate) const KAFKA_SASL_SCRAM_SHA_512: &str = "SCRAM-SHA-512";
43const KAFKA_DELIVERY_TIMEOUT: Duration = Duration::from_secs(30);
44
45struct KafkaDeliveryAttempt<'a> {
46    armed: bool,
47    poisoned: &'a AtomicBool,
48}
49
50impl KafkaDeliveryAttempt<'_> {
51    fn disarm(&mut self) {
52        self.armed = false;
53    }
54}
55
56impl Drop for KafkaDeliveryAttempt<'_> {
57    fn drop(&mut self) {
58        if self.armed {
59            self.poisoned.store(true, Ordering::Release);
60        }
61    }
62}
63
64fn kafka_delivery_timeout() -> TargetError {
65    TargetError::Timeout(format!("Kafka delivery timed out after {KAFKA_DELIVERY_TIMEOUT:?}"))
66}
67
68async fn with_serialized_kafka_delivery<P, T, Select, SelectFuture, Deliver, DeliveryFuture, Invalidate, InvalidateFuture>(
69    delivery_lock: &Mutex<()>,
70    delivery_poisoned: &AtomicBool,
71    select_producer: Select,
72    deliver: Deliver,
73    invalidate: Invalidate,
74) -> Result<T, TargetError>
75where
76    P: Send,
77    T: Send,
78    Select: FnOnce() -> SelectFuture + Send,
79    SelectFuture: Future<Output = Result<P, TargetError>> + Send,
80    Deliver: FnOnce(P) -> DeliveryFuture + Send,
81    DeliveryFuture: Future<Output = Result<T, TargetError>> + Send,
82    Invalidate: Fn() -> InvalidateFuture + Send,
83    InvalidateFuture: Future<Output = ()> + Send,
84{
85    let deadline = tokio::time::Instant::now() + KAFKA_DELIVERY_TIMEOUT;
86    let _delivery_guard = tokio::time::timeout_at(deadline, delivery_lock.lock())
87        .await
88        .map_err(|_| kafka_delivery_timeout())?;
89    let mut attempt = KafkaDeliveryAttempt {
90        armed: true,
91        poisoned: delivery_poisoned,
92    };
93
94    if delivery_poisoned.load(Ordering::Acquire) {
95        tokio::time::timeout_at(deadline, invalidate())
96            .await
97            .map_err(|_| kafka_delivery_timeout())?;
98        delivery_poisoned.store(false, Ordering::Release);
99    }
100
101    let result = tokio::time::timeout_at(deadline, async { deliver(select_producer().await?).await })
102        .await
103        .map_err(|_| kafka_delivery_timeout())?;
104    if result.as_ref().is_err_and(is_connectivity_error) {
105        tokio::time::timeout_at(deadline, invalidate())
106            .await
107            .map_err(|_| kafka_delivery_timeout())?;
108        delivery_poisoned.store(false, Ordering::Release);
109    }
110    attempt.disarm();
111    result
112}
113
114/// Arguments for configuring a Kafka target
115#[derive(Clone)]
116pub struct KafkaArgs {
117    /// Whether the target is enabled
118    pub enable: bool,
119    /// Comma-separated list of broker addresses (e.g. "localhost:9092,broker2:9092")
120    pub brokers: Vec<String>,
121    /// The topic to publish events to
122    pub topic: String,
123    /// Required acks: 0 = none, 1 = leader, -1 = all
124    pub acks: i16,
125    /// Whether to enable TLS for Kafka transport
126    pub tls_enable: bool,
127    /// Optional path to CA cert used for broker verification
128    pub tls_ca: String,
129    /// Optional path to client certificate for mTLS
130    pub tls_client_cert: String,
131    /// Optional path to client private key for mTLS
132    pub tls_client_key: String,
133    /// Whether to enable SASL authentication over the TLS transport
134    pub sasl_enable: bool,
135    /// SASL mechanism (PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512)
136    pub sasl_mechanism: String,
137    /// SASL username
138    pub sasl_username: String,
139    /// SASL password
140    pub sasl_password: String,
141    /// The directory to store events in case of failure
142    pub queue_dir: String,
143    /// The maximum number of events to store
144    pub queue_limit: u64,
145    /// The target type (audit or notify)
146    pub target_type: TargetType,
147}
148
149impl fmt::Debug for KafkaArgs {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.debug_struct("KafkaArgs")
152            .field("enable", &self.enable)
153            .field("brokers", &self.brokers)
154            .field("topic", &self.topic)
155            .field("acks", &self.acks)
156            .field("tls_enable", &self.tls_enable)
157            .field("tls_ca", &self.tls_ca)
158            .field("tls_client_cert", &self.tls_client_cert)
159            .field(
160                "tls_client_key",
161                if self.tls_client_key.is_empty() {
162                    &""
163                } else {
164                    &"***REDACTED***"
165                },
166            )
167            .field("sasl_enable", &self.sasl_enable)
168            .field("sasl_mechanism", &self.sasl_mechanism)
169            .field("sasl_username", &self.sasl_username)
170            .field(
171                "sasl_password",
172                if self.sasl_password.is_empty() {
173                    &""
174                } else {
175                    &"***REDACTED***"
176                },
177            )
178            .field("queue_dir", &self.queue_dir)
179            .field("queue_limit", &self.queue_limit)
180            .field("target_type", &self.target_type)
181            .finish()
182    }
183}
184
185fn normalize_kafka_sasl_mechanism(mechanism: &str) -> Result<&'static str, TargetError> {
186    let mechanism = mechanism.trim();
187    if mechanism.is_empty() || mechanism.eq_ignore_ascii_case(KAFKA_SASL_PLAIN) {
188        return Ok(KAFKA_SASL_PLAIN);
189    }
190    if mechanism.eq_ignore_ascii_case(KAFKA_SASL_SCRAM_SHA_256) {
191        return Ok(KAFKA_SASL_SCRAM_SHA_256);
192    }
193    if mechanism.eq_ignore_ascii_case(KAFKA_SASL_SCRAM_SHA_512) {
194        return Ok(KAFKA_SASL_SCRAM_SHA_512);
195    }
196    Err(TargetError::Configuration(
197        "kafka sasl_mechanism must be one of: PLAIN, SCRAM-SHA-256, SCRAM-SHA-512".to_string(),
198    ))
199}
200
201impl KafkaArgs {
202    /// Validates the KafkaArgs configuration
203    pub fn validate(&self) -> Result<(), TargetError> {
204        if !self.enable {
205            return Ok(());
206        }
207
208        if self.brokers.is_empty() {
209            return Err(TargetError::Configuration("kafka brokers cannot be empty".to_string()));
210        }
211
212        if self.topic.is_empty() {
213            return Err(TargetError::Configuration("kafka topic cannot be empty".to_string()));
214        }
215
216        if !matches!(self.acks, -1..=1) {
217            return Err(TargetError::Configuration("kafka acks must be one of: 0, 1, -1".to_string()));
218        }
219
220        if self.tls_client_cert.is_empty() != self.tls_client_key.is_empty() {
221            return Err(TargetError::Configuration(
222                "kafka tls_client_cert and tls_client_key must be specified together".to_string(),
223            ));
224        }
225
226        if self.sasl_enable {
227            if !self.tls_enable {
228                return Err(TargetError::Configuration(
229                    "kafka sasl_enable requires tls_enable for SASL_SSL".to_string(),
230                ));
231            }
232            normalize_kafka_sasl_mechanism(&self.sasl_mechanism)?;
233            if self.sasl_username.is_empty() || self.sasl_password.is_empty() {
234                return Err(TargetError::Configuration(
235                    "kafka sasl_username and sasl_password must be specified when sasl_enable is true".to_string(),
236                ));
237            }
238        } else if !self.sasl_mechanism.is_empty() || !self.sasl_username.is_empty() || !self.sasl_password.is_empty() {
239            return Err(TargetError::Configuration(
240                "kafka sasl_enable must be true when SASL fields are specified".to_string(),
241            ));
242        }
243
244        if !self.queue_dir.is_empty() {
245            let path = std::path::Path::new(&self.queue_dir);
246            if !path.is_absolute() {
247                return Err(TargetError::Configuration("kafka queueDir path should be absolute".to_string()));
248            }
249        }
250
251        Ok(())
252    }
253
254    pub(crate) fn security_config(&self, validate_tls_files: bool) -> Result<Option<SecurityConfig>, TargetError> {
255        if !self.tls_enable && !self.sasl_enable {
256            return Ok(None);
257        }
258
259        let mut security = SecurityConfig::new();
260        if !self.tls_ca.is_empty() {
261            if validate_tls_files {
262                let certs = load_cert_bundle_der_bytes(&self.tls_ca)
263                    .map_err(|e| TargetError::Configuration(format!("Failed to parse Kafka tls_ca: {e}")))?;
264                if certs.is_empty() {
265                    return Err(TargetError::Configuration(
266                        "Kafka tls_ca did not contain any parsable certificates".to_string(),
267                    ));
268                }
269            }
270            security = security.with_ca_cert(self.tls_ca.clone());
271        }
272        if !self.tls_client_cert.is_empty() && !self.tls_client_key.is_empty() {
273            if validate_tls_files {
274                let certs = load_cert_bundle_der_bytes(&self.tls_client_cert)
275                    .map_err(|e| TargetError::Configuration(format!("Failed to parse Kafka tls_client_cert: {e}")))?;
276                if certs.is_empty() {
277                    return Err(TargetError::Configuration(
278                        "Kafka tls_client_cert did not contain any parsable certificates".to_string(),
279                    ));
280                }
281                let _ = load_private_key(&self.tls_client_key)
282                    .map_err(|e| TargetError::Configuration(format!("Failed to parse Kafka tls_client_key: {e}")))?;
283            }
284            security = security.with_client_cert(self.tls_client_cert.clone(), self.tls_client_key.clone());
285        }
286        if self.sasl_enable {
287            security = security.with_sasl(SaslConfig::new(
288                normalize_kafka_sasl_mechanism(&self.sasl_mechanism)?.to_string(),
289                self.sasl_username.clone(),
290                self.sasl_password.clone(),
291            ));
292        }
293
294        Ok(Some(security))
295    }
296}
297
298/// A target that sends events to an Apache Kafka topic
299pub struct KafkaTarget<E>
300where
301    E: PluginEvent,
302{
303    id: TargetID,
304    args: KafkaArgs,
305    store: Option<Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>>,
306    producer: Arc<Mutex<Option<Arc<AsyncProducer>>>>,
307    delivery_lock: Arc<Mutex<()>>,
308    delivery_poisoned: Arc<AtomicBool>,
309    tls_state: Arc<Mutex<TargetTlsState>>,
310    /// Adapter that bridges this target to the TLS reload coordinator.
311    /// When `Some`, the target uses coordinator-managed material; when `None`,
312    /// it falls back to inline fingerprint-based change detection.
313    tls_adapter: Option<TlsReloadAdapter<Arc<AsyncProducer>>>,
314    delivery_counters: Arc<TargetDeliveryCounters>,
315    _phantom: PhantomData<E>,
316}
317
318impl<E> KafkaTarget<E>
319where
320    E: PluginEvent,
321{
322    fn map_kafka_error(err: KafkaError, context: &str) -> TargetError {
323        // Prefer the client's own retriable classification so transient broker
324        // states (leader election / NotLeaderForPartition, coordinator load,
325        // network blips, RequestTimedOut) are retried via store replay instead of
326        // being dropped as permanent failures (backlog#973).
327        if err.is_retriable() {
328            return match &err {
329                KafkaError::Connection(ConnectionError::Timeout(_)) | KafkaError::Kafka(KafkaCode::RequestTimedOut) => {
330                    TargetError::Timeout(format!("{context}: {err}"))
331                }
332                _ => TargetError::NotConnected,
333            };
334        }
335
336        // Non-retriable errors: configuration problems are permanent config
337        // errors; everything else (e.g. UnknownTopicOrPartition, authorization
338        // failures, oversize messages) is a permanent request-level failure.
339        match &err {
340            KafkaError::Config(_) => TargetError::Configuration(format!("{context}: {err}")),
341            _ => TargetError::Request(format!("{context}: {err}")),
342        }
343    }
344
345    /// Creates a new KafkaTarget
346    #[instrument(skip(args), fields(target_id = %id))]
347    pub fn new(id: String, args: KafkaArgs) -> Result<Self, TargetError> {
348        args.validate()?;
349
350        let target_id = TargetID::new(id, ChannelTargetType::Kafka.as_str().to_string());
351
352        let queue_store = open_target_queue_store(
353            &args.queue_dir,
354            args.queue_limit,
355            args.target_type,
356            ChannelTargetType::Kafka.as_str(),
357            &target_id,
358            "Failed to open store for Kafka target",
359        )?;
360
361        info!(target_id = %target_id.id, "Kafka target created");
362        Ok(KafkaTarget {
363            id: target_id,
364            args,
365            store: queue_store,
366            producer: Arc::new(Mutex::new(None)),
367            delivery_lock: Arc::new(Mutex::new(())),
368            delivery_poisoned: Arc::new(AtomicBool::new(false)),
369            tls_state: Arc::new(Mutex::new(TargetTlsState::default())),
370            tls_adapter: None,
371            delivery_counters: Arc::new(TargetDeliveryCounters::default()),
372            _phantom: PhantomData,
373        })
374    }
375
376    /// Builds a Kafka producer from the current args
377    async fn build_producer(&self) -> Result<AsyncProducer, TargetError> {
378        let acks = match self.args.acks {
379            0 => RequiredAcks::None,
380            1 => RequiredAcks::One,
381            _ => RequiredAcks::All,
382        };
383
384        let mut config = AsyncProducerConfig::new()
385            .with_ack_timeout(KAFKA_DELIVERY_TIMEOUT)
386            .with_required_acks(acks);
387
388        if let Some(security) = self.args.security_config(true)? {
389            config = config.with_security(security);
390        }
391
392        AsyncProducer::from_hosts_with_config(self.args.brokers.clone(), config)
393            .await
394            .map_err(|e| Self::map_kafka_error(e, "Failed to create Kafka producer"))
395    }
396
397    async fn get_or_build_producer(&self) -> Result<Arc<AsyncProducer>, TargetError> {
398        // Adapter-managed path: use the material directly from the TLS reload adapter.
399        if let Some(adapter) = &self.tls_adapter {
400            let producer: Arc<AsyncProducer> = (*adapter.current_material()).clone();
401
402            // Ensure the producer is also stored locally so that close() can drain it.
403            {
404                let mut guard = self.producer.lock().await;
405                *guard = Some(Arc::clone(&producer));
406            }
407            return Ok(producer);
408        }
409
410        // Inline fingerprint fallback path (no coordinator).
411        let next_fingerprint =
412            build_target_tls_fingerprint(&self.args.tls_ca, &self.args.tls_client_cert, &self.args.tls_client_key).await?;
413        let tls_changed = {
414            let tls_state_guard = self.tls_state.lock().await;
415            tls_state_guard.needs_update(&next_fingerprint)
416        };
417        if tls_changed {
418            let mut cached = self.producer.lock().await;
419            *cached = None;
420            self.tls_state.lock().await.refresh(next_fingerprint);
421        }
422
423        {
424            let cached = self.producer.lock().await;
425            if let Some(producer) = cached.as_ref() {
426                return Ok(Arc::clone(producer));
427            }
428        }
429
430        // Build the producer without holding the cache lock so a slow connect
431        // does not block other senders (which only need to read the cache).
432        // Re-check the cache after building in case another task raced us
433        // (backlog#983).
434        let producer = Arc::new(self.build_producer().await?);
435        let mut cached = self.producer.lock().await;
436        if let Some(existing) = cached.as_ref() {
437            return Ok(Arc::clone(existing));
438        }
439        *cached = Some(Arc::clone(&producer));
440        Ok(producer)
441    }
442
443    async fn invalidate_cached_producer(&self) {
444        let mut cached = self.producer.lock().await;
445        *cached = None;
446        self.tls_state.lock().await.reset();
447    }
448
449    /// Serializes the event and builds a QueuedPayload
450    fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
451        build_queued_payload(event)
452    }
453
454    /// Sends the raw body to Kafka
455    #[instrument(skip(self, body, meta), fields(target_id = %self.id))]
456    async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> {
457        debug!(
458            target = %self.id,
459            bucket = %meta.bucket_name,
460            object = %meta.object_name,
461            event = %meta.event_name,
462            payload_len = body.len(),
463            "Sending Kafka payload"
464        );
465
466        // rustfs-kafka-async does not validate response correlation IDs. Keep
467        // producer selection, send, and timeout invalidation serialized so a
468        // waiter cannot reuse a connection with an unread timed-out response.
469        with_serialized_kafka_delivery(
470            &self.delivery_lock,
471            &self.delivery_poisoned,
472            || self.get_or_build_producer(),
473            |producer| async move {
474                // Use "<bucket>/<object>" as the message key so all events for the same
475                // object hash to the same partition and preserve per-object ordering
476                // across multiple partitions (backlog#983).
477                let partition_key = format!("{}/{}", meta.bucket_name, meta.object_name);
478                producer
479                    .send(&Record::from_key_value(&self.args.topic, partition_key, body.as_slice()))
480                    .await
481                    .map_err(|err| Self::map_kafka_error(err, "Failed to send message to Kafka"))
482            },
483            || self.invalidate_cached_producer(),
484        )
485        .await?;
486
487        debug!(target_id = %self.id, topic = %self.args.topic, "Event published to Kafka topic");
488        self.delivery_counters.record_success();
489        Ok(())
490    }
491
492    /// Clones this target into a boxed trait object
493    pub fn clone_box(&self) -> Box<dyn Target<E> + Send + Sync> {
494        Box::new(KafkaTarget::<E> {
495            id: self.id.clone(),
496            args: self.args.clone(),
497            store: self.store.as_ref().map(|s| s.boxed_clone()),
498            producer: Arc::clone(&self.producer),
499            delivery_lock: Arc::clone(&self.delivery_lock),
500            delivery_poisoned: Arc::clone(&self.delivery_poisoned),
501            tls_state: Arc::clone(&self.tls_state),
502            tls_adapter: self.tls_adapter.clone(),
503            delivery_counters: Arc::clone(&self.delivery_counters),
504            _phantom: PhantomData,
505        })
506    }
507}
508
509#[async_trait]
510impl<E> Target<E> for KafkaTarget<E>
511where
512    E: PluginEvent,
513{
514    fn id(&self) -> TargetID {
515        self.id.clone()
516    }
517
518    async fn is_active(&self) -> Result<bool, TargetError> {
519        let _ = self.get_or_build_producer().await?;
520        Ok(true)
521    }
522
523    async fn save(&self, event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
524        let queued = match self.build_queued_payload(&event) {
525            Ok(queued) => queued,
526            Err(err) => {
527                self.delivery_counters.record_final_failure();
528                return Err(err);
529            }
530        };
531
532        if let Some(store) = &self.store {
533            if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
534                self.delivery_counters.record_final_failure();
535                return Err(e);
536            }
537            debug!("Event saved to store for Kafka target: {}", self.id);
538            Ok(())
539        } else {
540            if let Err(err) = self.send_body(queued.body, &queued.meta).await {
541                self.delivery_counters.record_final_failure();
542                return Err(err);
543            }
544            Ok(())
545        }
546    }
547
548    async fn send_raw_from_store(&self, key: Key, body: Vec<u8>, meta: QueuedPayloadMeta) -> Result<(), TargetError> {
549        debug!("Sending queued payload from store for Kafka target: {}, key: {}", self.id, key);
550
551        if let Err(e) = self.send_body(body, &meta).await {
552            if matches!(e, TargetError::NotConnected) {
553                warn!(target_id = %self.id, "Kafka not reachable, event remains in store.");
554                return Err(TargetError::NotConnected);
555            }
556            error!(target_id = %self.id, error = %e, "Failed to send event from store.");
557            return Err(e);
558        }
559
560        debug!("Event sent from store for Kafka target: {}", self.id);
561        Ok(())
562    }
563
564    async fn close(&self) -> Result<(), TargetError> {
565        {
566            let mut guard = self.producer.lock().await;
567            *guard = None;
568        }
569
570        self.tls_state.lock().await.reset();
571
572        info!("Kafka target closed: {}", self.id);
573        Ok(())
574    }
575
576    fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
577        self.store.as_deref()
578    }
579
580    fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
581        self.clone_box()
582    }
583
584    fn is_enabled(&self) -> bool {
585        self.args.enable
586    }
587
588    fn delivery_snapshot(&self) -> TargetDeliverySnapshot {
589        self.delivery_counters.snapshot(
590            self.store.as_deref().map_or(0, |store| store.len() as u64),
591            // Kafka targets record no terminal failures and keep no failed store.
592            0,
593        )
594    }
595
596    fn record_final_failure(&self) {
597        self.delivery_counters.record_final_failure();
598    }
599}
600
601/// Coordinated TLS hot-reload implementation for Kafka targets.
602///
603/// The coordinator calls these methods on a background poll loop to detect
604/// TLS file changes and rebuild the producer without restarting.
605#[async_trait]
606impl<E> ReloadableTargetTls for KafkaTarget<E>
607where
608    E: PluginEvent,
609{
610    type Material = Arc<AsyncProducer>;
611
612    fn tls_input_set(&self) -> TargetTlsInputSet {
613        TargetTlsInputSet {
614            ca_path: self.args.tls_ca.clone(),
615            client_cert_path: self.args.tls_client_cert.clone(),
616            client_key_path: self.args.tls_client_key.clone(),
617            target_label: format!("kafka:{}", self.id.id),
618        }
619    }
620
621    async fn build_tls_material(&self) -> Result<Self::Material, TargetError> {
622        let producer = self.build_producer().await?;
623        Ok(Arc::new(producer))
624    }
625
626    async fn apply_tls_material(
627        &self,
628        _generation: TargetTlsGeneration,
629        material: Arc<Self::Material>,
630        _mode: ReloadApplyMode,
631    ) -> Result<(), TargetError> {
632        let mut guard = self.producer.lock().await;
633        *guard = Some((*material).clone());
634        Ok(())
635    }
636
637    async fn validate_tls_files(&self) -> Result<(), TargetError> {
638        validate_tls_material(&self.args.tls_ca, &self.args.tls_client_cert, &self.args.tls_client_key)
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645    use std::sync::atomic::AtomicUsize;
646    use tokio::sync::Notify;
647
648    fn base_args() -> KafkaArgs {
649        KafkaArgs {
650            enable: true,
651            brokers: vec!["localhost:9092".to_string()],
652            topic: "rustfs-events".to_string(),
653            acks: 1,
654            tls_enable: false,
655            tls_ca: String::new(),
656            tls_client_cert: String::new(),
657            tls_client_key: String::new(),
658            sasl_enable: false,
659            sasl_mechanism: String::new(),
660            sasl_username: String::new(),
661            sasl_password: String::new(),
662            queue_dir: String::new(),
663            queue_limit: 0,
664            target_type: TargetType::NotifyEvent,
665        }
666    }
667
668    #[tokio::test(start_paused = true)]
669    async fn timeout_invalidates_before_the_next_delivery_selects_a_producer() {
670        let delivery_lock = Arc::new(Mutex::new(()));
671        let delivery_poisoned = Arc::new(AtomicBool::new(false));
672        let generation = Arc::new(AtomicUsize::new(1));
673        let first_entered = Arc::new(Notify::new());
674
675        let first = {
676            let delivery_lock = Arc::clone(&delivery_lock);
677            let delivery_poisoned = Arc::clone(&delivery_poisoned);
678            let generation = Arc::clone(&generation);
679            let first_entered = Arc::clone(&first_entered);
680            tokio::spawn(async move {
681                with_serialized_kafka_delivery(
682                    &delivery_lock,
683                    &delivery_poisoned,
684                    {
685                        let generation = Arc::clone(&generation);
686                        move || async move { Ok(generation.load(Ordering::SeqCst)) }
687                    },
688                    move |selected| async move {
689                        assert_eq!(selected, 1);
690                        first_entered.notify_one();
691                        std::future::pending::<Result<usize, TargetError>>().await
692                    },
693                    move || {
694                        let generation = Arc::clone(&generation);
695                        async move { generation.store(2, Ordering::SeqCst) }
696                    },
697                )
698                .await
699            })
700        };
701
702        first_entered.notified().await;
703        tokio::time::advance(Duration::from_secs(1)).await;
704        let second = {
705            let delivery_lock = Arc::clone(&delivery_lock);
706            let delivery_poisoned = Arc::clone(&delivery_poisoned);
707            let generation = Arc::clone(&generation);
708            tokio::spawn(async move {
709                with_serialized_kafka_delivery(
710                    &delivery_lock,
711                    &delivery_poisoned,
712                    {
713                        let generation = Arc::clone(&generation);
714                        move || async move { Ok(generation.load(Ordering::SeqCst)) }
715                    },
716                    |selected| async move { Ok(selected) },
717                    move || {
718                        let generation = Arc::clone(&generation);
719                        async move { generation.store(2, Ordering::SeqCst) }
720                    },
721                )
722                .await
723            })
724        };
725
726        assert!(matches!(
727            first.await.expect("first delivery task should not panic"),
728            Err(TargetError::Timeout(_))
729        ));
730        assert_eq!(
731            second
732                .await
733                .expect("second delivery task should not panic")
734                .expect("second delivery should succeed"),
735            2,
736            "the waiter must select a fresh producer generation after timeout invalidation"
737        );
738    }
739
740    #[tokio::test(start_paused = true)]
741    async fn delivery_deadline_includes_waiting_for_the_serialization_lock() {
742        let delivery_lock = Arc::new(Mutex::new(()));
743        let delivery_poisoned = AtomicBool::new(false);
744        let selected = Arc::new(AtomicBool::new(false));
745        let _held = delivery_lock.lock().await;
746
747        let error = with_serialized_kafka_delivery(
748            &delivery_lock,
749            &delivery_poisoned,
750            {
751                let selected = Arc::clone(&selected);
752                move || async move {
753                    selected.store(true, Ordering::SeqCst);
754                    Ok(())
755                }
756            },
757            |()| async { Ok(()) },
758            || async {},
759        )
760        .await
761        .expect_err("lock admission must share the absolute delivery deadline");
762
763        assert!(matches!(error, TargetError::Timeout(_)));
764        assert!(!selected.load(Ordering::SeqCst), "a timed-out waiter must not select a producer");
765        assert!(!delivery_poisoned.load(Ordering::SeqCst));
766    }
767
768    #[tokio::test]
769    async fn cancelled_delivery_poisons_the_connection_before_the_next_selection() {
770        let delivery_lock = Arc::new(Mutex::new(()));
771        let delivery_poisoned = Arc::new(AtomicBool::new(false));
772        let generation = Arc::new(AtomicUsize::new(1));
773        let first_entered = Arc::new(Notify::new());
774        let first = {
775            let delivery_lock = Arc::clone(&delivery_lock);
776            let delivery_poisoned = Arc::clone(&delivery_poisoned);
777            let first_entered = Arc::clone(&first_entered);
778            tokio::spawn(async move {
779                with_serialized_kafka_delivery(
780                    &delivery_lock,
781                    &delivery_poisoned,
782                    || async { Ok(1usize) },
783                    move |_| async move {
784                        first_entered.notify_one();
785                        std::future::pending::<Result<(), TargetError>>().await
786                    },
787                    || async {},
788                )
789                .await
790            })
791        };
792        first_entered.notified().await;
793        first.abort();
794        assert!(first.await.expect_err("first delivery should be cancelled").is_cancelled());
795        assert!(delivery_poisoned.load(Ordering::Acquire));
796
797        let selected = with_serialized_kafka_delivery(
798            &delivery_lock,
799            &delivery_poisoned,
800            {
801                let generation = Arc::clone(&generation);
802                move || async move { Ok(generation.load(Ordering::SeqCst)) }
803            },
804            |selected| async move { Ok(selected) },
805            {
806                let generation = Arc::clone(&generation);
807                move || {
808                    let generation = Arc::clone(&generation);
809                    async move { generation.store(2, Ordering::SeqCst) }
810                }
811            },
812        )
813        .await
814        .expect("the next delivery should recover from cancellation poisoning");
815
816        assert_eq!(selected, 2);
817        assert!(!delivery_poisoned.load(Ordering::Acquire));
818    }
819
820    #[test]
821    fn test_validate_empty_brokers() {
822        let args = KafkaArgs {
823            brokers: vec![],
824            ..base_args()
825        };
826        assert!(args.validate().is_err());
827    }
828
829    #[test]
830    fn test_validate_empty_topic() {
831        let args = KafkaArgs {
832            topic: String::new(),
833            ..base_args()
834        };
835        assert!(args.validate().is_err());
836    }
837
838    #[test]
839    fn test_validate_relative_queue_dir() {
840        let args = KafkaArgs {
841            queue_dir: "relative/path".to_string(),
842            ..base_args()
843        };
844        assert!(args.validate().is_err());
845    }
846
847    #[test]
848    fn test_validate_valid_args() {
849        assert!(base_args().validate().is_ok());
850    }
851
852    #[test]
853    fn test_validate_disabled_target_skips_validation() {
854        let args = KafkaArgs {
855            enable: false,
856            brokers: vec![],
857            topic: String::new(),
858            ..base_args()
859        };
860        assert!(args.validate().is_ok());
861    }
862
863    #[test]
864    fn test_validate_tls_client_cert_and_key_must_be_paired() {
865        let args = KafkaArgs {
866            tls_client_cert: "/tmp/client.crt".to_string(),
867            tls_client_key: String::new(),
868            ..base_args()
869        };
870        assert!(args.validate().is_err());
871    }
872
873    #[test]
874    fn test_validate_sasl_requires_tls() {
875        let args = KafkaArgs {
876            sasl_enable: true,
877            sasl_mechanism: KAFKA_SASL_SCRAM_SHA_512.to_string(),
878            sasl_username: "user".to_string(),
879            sasl_password: "secret".to_string(),
880            ..base_args()
881        };
882        let err = args.validate().expect_err("SASL without TLS should fail");
883        assert!(err.to_string().contains("requires tls_enable"));
884    }
885
886    #[test]
887    fn test_validate_sasl_requires_username_and_password() {
888        let args = KafkaArgs {
889            tls_enable: true,
890            sasl_enable: true,
891            sasl_mechanism: KAFKA_SASL_PLAIN.to_string(),
892            sasl_username: "user".to_string(),
893            sasl_password: String::new(),
894            ..base_args()
895        };
896        let err = args.validate().expect_err("SASL credentials should be paired");
897        assert!(err.to_string().contains("sasl_username and sasl_password"));
898    }
899
900    #[test]
901    fn test_validate_sasl_rejects_unsupported_mechanism() {
902        let args = KafkaArgs {
903            tls_enable: true,
904            sasl_enable: true,
905            sasl_mechanism: "OAUTHBEARER".to_string(),
906            sasl_username: "user".to_string(),
907            sasl_password: "secret".to_string(),
908            ..base_args()
909        };
910        let err = args.validate().expect_err("unsupported SASL mechanism should fail");
911        assert!(err.to_string().contains("sasl_mechanism must be one of"));
912    }
913
914    #[test]
915    fn test_security_config_includes_sasl() {
916        let args = KafkaArgs {
917            tls_enable: true,
918            sasl_enable: true,
919            sasl_mechanism: "scram-sha-512".to_string(),
920            sasl_username: "user".to_string(),
921            sasl_password: "secret".to_string(),
922            ..base_args()
923        };
924
925        let security = args
926            .security_config(false)
927            .expect("valid security config")
928            .expect("security should be configured");
929        let sasl = security.sasl_config().expect("SASL should be configured");
930
931        assert_eq!(sasl.mechanism(), KAFKA_SASL_SCRAM_SHA_512);
932        assert_eq!(sasl.username(), "user");
933        assert_eq!(sasl.password(), "secret");
934    }
935
936    #[test]
937    fn map_kafka_error_treats_transient_broker_states_as_retriable() {
938        // Leader election / metadata staleness must be retried, not dropped
939        // as a permanent failure (backlog#973).
940        assert!(matches!(
941            KafkaTarget::<serde_json::Value>::map_kafka_error(KafkaError::Kafka(KafkaCode::NotLeaderForPartition), "send"),
942            TargetError::NotConnected
943        ));
944        assert!(matches!(
945            KafkaTarget::<serde_json::Value>::map_kafka_error(KafkaError::Kafka(KafkaCode::LeaderNotAvailable), "send"),
946            TargetError::NotConnected
947        ));
948        // RequestTimedOut is retriable and surfaced as a timeout.
949        assert!(matches!(
950            KafkaTarget::<serde_json::Value>::map_kafka_error(KafkaError::Kafka(KafkaCode::RequestTimedOut), "send"),
951            TargetError::Timeout(_)
952        ));
953    }
954
955    #[test]
956    fn map_kafka_error_treats_permanent_broker_states_as_request_error() {
957        // A missing topic/partition is a permanent condition; retrying would
958        // storm the broker.
959        assert!(matches!(
960            KafkaTarget::<serde_json::Value>::map_kafka_error(KafkaError::Kafka(KafkaCode::UnknownTopicOrPartition), "send"),
961            TargetError::Request(_)
962        ));
963        assert!(matches!(
964            KafkaTarget::<serde_json::Value>::map_kafka_error(KafkaError::Config("bad".to_string()), "send"),
965            TargetError::Configuration(_)
966        ));
967    }
968
969    #[test]
970    fn test_debug_redacts_sasl_password_and_tls_key() {
971        let rendered = format!(
972            "{:?}",
973            KafkaArgs {
974                tls_client_key: "/tmp/client.key".to_string(),
975                sasl_enable: true,
976                sasl_password: "super-secret".to_string(),
977                ..base_args()
978            }
979        );
980
981        assert!(!rendered.contains("super-secret"));
982        assert!(!rendered.contains("/tmp/client.key"));
983        assert!(rendered.contains("***REDACTED***"));
984    }
985}