1use 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#[derive(Clone)]
116pub struct KafkaArgs {
117 pub enable: bool,
119 pub brokers: Vec<String>,
121 pub topic: String,
123 pub acks: i16,
125 pub tls_enable: bool,
127 pub tls_ca: String,
129 pub tls_client_cert: String,
131 pub tls_client_key: String,
133 pub sasl_enable: bool,
135 pub sasl_mechanism: String,
137 pub sasl_username: String,
139 pub sasl_password: String,
141 pub queue_dir: String,
143 pub queue_limit: u64,
145 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 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
298pub 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 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 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 match &err {
340 KafkaError::Config(_) => TargetError::Configuration(format!("{context}: {err}")),
341 _ => TargetError::Request(format!("{context}: {err}")),
342 }
343 }
344
345 #[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 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 if let Some(adapter) = &self.tls_adapter {
400 let producer: Arc<AsyncProducer> = (*adapter.current_material()).clone();
401
402 {
404 let mut guard = self.producer.lock().await;
405 *guard = Some(Arc::clone(&producer));
406 }
407 return Ok(producer);
408 }
409
410 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 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 fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
451 build_queued_payload(event)
452 }
453
454 #[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 with_serialized_kafka_delivery(
470 &self.delivery_lock,
471 &self.delivery_poisoned,
472 || self.get_or_build_producer(),
473 |producer| async move {
474 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 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 0,
593 )
594 }
595
596 fn record_final_failure(&self) {
597 self.delivery_counters.record_final_failure();
598 }
599}
600
601#[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 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 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 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}