1use std::fmt::Write as _;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use arrow_array::{Array, StringArray};
8use arrow_schema::SchemaRef;
9use async_trait::async_trait;
10use rdkafka::error::{KafkaError, RDKafkaErrorCode};
11use rdkafka::message::OwnedHeaders;
12use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord, Producer};
13use rdkafka::ClientConfig;
14use tracing::{debug, info, warn};
15
16use crate::config::{ConnectorConfig, ConnectorState};
17use crate::connector::{
18 ConnectorTaskOwner, ConnectorTaskTracker, SinkConnector, SinkConsistency, SinkContract,
19 SinkInputMode, SinkTopology, WriteResult,
20};
21use crate::error::ConnectorError;
22use crate::serde::{Format, RecordSerializer};
23
24use super::metadata_error::{fetch_error, invalid_response, topic_error};
25use super::partitioner::{
26 KafkaPartitioner, KeyHashPartitioner, RoundRobinPartitioner, StickyPartitioner,
27};
28use super::schema_registry::SchemaRegistryClient;
29use super::sink_config::{KafkaSinkConfig, PartitionStrategy, SinkEnvelope};
30use super::sink_metrics::KafkaSinkMetrics;
31
32mod failure;
33mod keys;
34mod lifecycle;
35mod serialization;
36mod upsert;
37
38use failure::{
39 kafka_write_timeout, producer_creation_error, queue_retry_delay, record_failure,
40 unresolved_delivery_error, validate_payload_cardinality, KafkaFailure, KafkaFailureCertainty,
41 KafkaFailureScope, QUEUE_RETRY_TIMEOUT,
42};
43use keys::KeyBuffer;
44use serialization::select_serializer;
45use upsert::{project_upsert_values, validate_upsert_keys};
46
47fn required_producer(producer: Option<&FutureProducer>) -> Result<&FutureProducer, ConnectorError> {
48 producer.ok_or_else(|| ConnectorError::InvalidState {
49 expected: "producer initialized".into(),
50 actual: "producer is None".into(),
51 })
52}
53
54pub struct KafkaSink {
70 producer: Option<FutureProducer>,
72 config: KafkaSinkConfig,
74 serializer: Box<dyn RecordSerializer>,
76 partitioner: Box<dyn KafkaPartitioner>,
78 state: ConnectorState,
80 dlq_producer: Option<FutureProducer>,
82 metrics: KafkaSinkMetrics,
84 schema: SchemaRef,
86 schema_registry: Option<Arc<SchemaRegistryClient>>,
88 avro_schema_id: Arc<std::sync::atomic::AtomicU32>,
90 topic_partition_count: Option<i32>,
92 task_owner: ConnectorTaskOwner,
94 task_tracker: ConnectorTaskTracker,
96}
97
98impl KafkaSink {
99 #[must_use]
106 pub fn new(
107 schema: SchemaRef,
108 config: KafkaSinkConfig,
109 registry: Option<&prometheus::Registry>,
110 ) -> Self {
111 let avro_schema_id = Arc::new(std::sync::atomic::AtomicU32::new(0));
112 let serializer =
113 select_serializer(config.format, &schema, Arc::clone(&avro_schema_id), None)
114 .expect("format validated in KafkaSinkConfig::validate()");
115 let partitioner = select_partitioner(config.partitioner);
116 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
117
118 Self {
119 producer: None,
120 config,
121 serializer,
122 partitioner,
123 state: ConnectorState::Created,
124 dlq_producer: None,
125 metrics: KafkaSinkMetrics::new(registry),
126 schema,
127 schema_registry: None,
128 avro_schema_id,
129 topic_partition_count: None,
130 task_owner,
131 task_tracker,
132 }
133 }
134
135 #[must_use]
142 pub fn with_schema_registry(
143 schema: SchemaRef,
144 config: KafkaSinkConfig,
145 sr_client: SchemaRegistryClient,
146 ) -> Self {
147 let sr = Arc::new(sr_client);
148 let avro_schema_id = Arc::new(std::sync::atomic::AtomicU32::new(0));
149 let serializer = select_serializer(
150 config.format,
151 &schema,
152 Arc::clone(&avro_schema_id),
153 Some(Arc::clone(&sr)),
154 )
155 .expect("format validated in KafkaSinkConfig::validate()");
156 let partitioner = select_partitioner(config.partitioner);
157 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
158
159 Self {
160 producer: None,
161 config,
162 serializer,
163 partitioner,
164 state: ConnectorState::Created,
165 dlq_producer: None,
166 metrics: KafkaSinkMetrics::new(None),
167 schema,
168 schema_registry: Some(sr),
169 avro_schema_id,
170 topic_partition_count: None,
171 task_owner,
172 task_tracker,
173 }
174 }
175
176 #[must_use]
178 pub fn state(&self) -> ConnectorState {
179 self.state
180 }
181
182 #[must_use]
184 pub fn has_schema_registry(&self) -> bool {
185 self.schema_registry.is_some()
186 }
187
188 fn serialize_payloads(
189 &self,
190 batch: &arrow_array::RecordBatch,
191 ) -> Result<Vec<Vec<u8>>, ConnectorError> {
192 let payloads = self.serializer.serialize(batch).map_err(|error| {
193 self.metrics.record_serialization_error();
194 ConnectorError::Serde(error)
195 })?;
196 validate_payload_cardinality(batch.num_rows(), payloads.len())?;
197 Ok(payloads)
198 }
199
200 fn required_partition_count(&self) -> Result<i32, ConnectorError> {
201 self.topic_partition_count
202 .ok_or_else(|| ConnectorError::InvalidState {
203 expected: "broker topic metadata installed".into(),
204 actual: "partition count is unavailable".into(),
205 })
206 }
207
208 fn extract_keys(
212 &self,
213 batch: &arrow_array::RecordBatch,
214 ) -> Result<Option<KeyBuffer>, ConnectorError> {
215 let Some(key_col) = &self.config.key_column else {
216 return Ok(None);
217 };
218
219 let col_idx = batch.schema().index_of(key_col).map_err(|_| {
220 ConnectorError::ConfigurationError(format!(
221 "key column '{key_col}' not found in schema"
222 ))
223 })?;
224
225 let array = batch.column(col_idx);
226 let num_rows = batch.num_rows();
227 let mut buf = KeyBuffer::with_capacity(num_rows, 32);
228
229 if let Some(str_array) = array.as_any().downcast_ref::<StringArray>() {
231 for i in 0..num_rows {
232 if str_array.is_null(i) {
233 buf.push_empty();
234 } else {
235 buf.push(str_array.value(i).as_bytes());
236 }
237 }
238 } else {
239 use std::fmt::Write;
240 let formatter = arrow_cast::display::ArrayFormatter::try_new(
241 array,
242 &arrow_cast::display::FormatOptions::default(),
243 )
244 .map_err(|e| {
245 ConnectorError::Internal(format!(
246 "failed to create array formatter for key column: {e}"
247 ))
248 })?;
249 let mut fmt_buf = String::with_capacity(64);
250 for i in 0..num_rows {
251 if array.is_null(i) {
252 buf.push_empty();
253 } else {
254 fmt_buf.clear();
255 let _ = write!(fmt_buf, "{}", formatter.value(i));
256 buf.push(fmt_buf.as_bytes());
257 }
258 }
259 }
260
261 Ok(Some(buf))
262 }
263
264 async fn enqueue_dlq(
268 &self,
269 payload: &[u8],
270 key: Option<&[u8]>,
271 error_msg: &str,
272 queue_deadline: Instant,
273 ) -> Result<DeliveryFuture, KafkaFailure> {
274 let dlq_producer = self.dlq_producer.as_ref().ok_or_else(|| KafkaFailure {
275 certainty: KafkaFailureCertainty::DefinitelyNotPersisted,
276 scope: KafkaFailureScope::Connector,
277 detail: "DLQ topic or producer is not configured".into(),
278 retryable: false,
279 })?;
280 let dlq_topic = self.config.dlq_topic.as_ref().ok_or_else(|| KafkaFailure {
281 certainty: KafkaFailureCertainty::DefinitelyNotPersisted,
282 scope: KafkaFailureScope::Connector,
283 detail: "DLQ topic or producer is not configured".into(),
284 retryable: false,
285 })?;
286
287 let now = std::time::SystemTime::now()
288 .duration_since(std::time::UNIX_EPOCH)
289 .unwrap_or_else(|_| {
290 tracing::warn!("system clock before Unix epoch — using 0 for DLQ timestamp");
291 std::time::Duration::ZERO
292 })
293 .as_millis()
294 .to_string();
295 let headers = OwnedHeaders::new()
296 .insert(rdkafka::message::Header {
297 key: "__dlq.error",
298 value: Some(error_msg.as_bytes()),
299 })
300 .insert(rdkafka::message::Header {
301 key: "__dlq.topic",
302 value: Some(self.config.topic.as_bytes()),
303 })
304 .insert(rdkafka::message::Header {
305 key: "__dlq.timestamp",
306 value: Some(now.as_bytes()),
307 });
308
309 let mut record = FutureRecord::to(dlq_topic)
310 .payload(payload)
311 .headers(headers);
312
313 if let Some(k) = key {
314 record = record.key(k);
315 }
316
317 Self::enqueue_with_queue_retry(dlq_producer, record, queue_deadline)
318 .await
319 .map_err(|error| KafkaFailure::enqueue(&error, "Kafka DLQ"))
320 }
321
322 async fn enqueue_with_queue_retry(
327 producer: &FutureProducer,
328 mut record: FutureRecord<'_, [u8], [u8]>,
329 queue_deadline: Instant,
330 ) -> Result<DeliveryFuture, KafkaError> {
331 loop {
332 match producer.send_result(record) {
334 Ok(fut) => return Ok(fut),
335 Err((error @ KafkaError::MessageProduction(RDKafkaErrorCode::QueueFull), r)) => {
336 let Some(delay) = queue_retry_delay(queue_deadline, Instant::now()) else {
337 return Err(error);
338 };
339 record = r;
340 tokio::time::sleep(delay).await;
341 }
342 Err((error, _)) => return Err(error),
343 }
344 }
345 }
346
347 #[allow(clippy::cast_possible_truncation)]
352 async fn write_upsert_batch(
353 &mut self,
354 batch: &arrow_array::RecordBatch,
355 ) -> Result<WriteResult, ConnectorError> {
356 let collapsed = self.collapse_upsert_changelog(batch)?;
357 let rows = collapsed.num_rows();
358 if rows == 0 {
359 return Ok(WriteResult::new(0, 0));
360 }
361 let (value_batch, ops) = project_upsert_values(&collapsed)?;
362
363 self.ensure_schema_ready(&value_batch.schema()).await?;
364 let payloads = self.serialize_payloads(&value_batch)?;
365 let keys = self.extract_keys(&collapsed)?;
366 validate_upsert_keys(keys.as_ref(), payloads.len())?;
369
370 let partition_count = self.required_partition_count()?;
371 let producer = required_producer(self.producer.as_ref())?;
372
373 let mut delivery_futures = Vec::with_capacity(rows);
374 let mut enqueue_failure: Option<(KafkaFailure, usize)> = None;
375 let queue_deadline = Instant::now() + QUEUE_RETRY_TIMEOUT;
376 for (i, payload) in payloads.iter().enumerate() {
377 let key: Option<&[u8]> = keys.as_ref().map(|kb| kb.key(i));
378 let is_delete = ops.value(i) == "D";
379 let partition = self.partitioner.partition(key, partition_count);
380 let mut record: FutureRecord<'_, [u8], [u8]> = FutureRecord::to(&self.config.topic);
382 if let Some(k) = key {
383 record = record.key(k);
384 }
385 if !is_delete {
386 record = record.payload(payload.as_slice());
387 }
388 if let Some(p) = partition {
389 record = record.partition(p);
390 }
391 match Self::enqueue_with_queue_retry(producer, record, queue_deadline).await {
392 Ok(future) => delivery_futures.push((Instant::now(), future, is_delete, i)),
393 Err(error) => {
394 let mut failure = KafkaFailure::enqueue(&error, "Kafka upsert");
395 let affected = rows - i;
396 if affected > 1 {
397 let _ = write!(
398 failure.detail,
399 "; {} later record(s) were not attempted",
400 affected - 1
401 );
402 }
403 enqueue_failure = Some((failure, affected));
404 break;
405 }
406 }
407 }
408
409 let mut records_written: usize = 0;
410 let mut bytes_written: u64 = 0;
411 let mut definitely_not_persisted: usize = 0;
412 let mut ambiguous: usize = 0;
413 let mut first_error: Option<String> = None;
414 let mut retryable = true;
415 for (send_time, future, is_delete, i) in delivery_futures {
416 match future.await {
417 Ok(Ok(_)) => {
418 self.metrics
419 .record_produce_latency(send_time.elapsed().as_micros() as u64);
420 records_written += 1;
421 if !is_delete {
422 bytes_written += payloads[i].len() as u64;
423 }
424 }
425 Ok(Err((err, _))) => {
426 let failure = KafkaFailure::delivery(&err, "Kafka upsert");
427 record_failure(
428 &failure,
429 1,
430 &mut definitely_not_persisted,
431 &mut ambiguous,
432 &mut first_error,
433 &mut retryable,
434 );
435 }
436 Err(_canceled) => {
437 let failure = KafkaFailure::canceled("Kafka upsert");
438 record_failure(
439 &failure,
440 1,
441 &mut definitely_not_persisted,
442 &mut ambiguous,
443 &mut first_error,
444 &mut retryable,
445 );
446 }
447 }
448 }
449 if let Some((failure, affected)) = enqueue_failure {
450 record_failure(
451 &failure,
452 affected,
453 &mut definitely_not_persisted,
454 &mut ambiguous,
455 &mut first_error,
456 &mut retryable,
457 );
458 }
459 self.metrics
460 .record_write(records_written as u64, bytes_written);
461 if definitely_not_persisted > 0 || ambiguous > 0 {
462 self.metrics.record_error();
463 return Err(unresolved_delivery_error(
464 "upsert produce",
465 rows,
466 records_written,
467 definitely_not_persisted,
468 ambiguous,
469 first_error,
470 retryable,
471 ));
472 }
473 Ok(WriteResult::new(records_written, bytes_written))
474 }
475}
476
477#[async_trait]
478impl SinkConnector for KafkaSink {
479 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
480 Some(self.task_tracker.clone())
481 }
482
483 fn contract(&self, config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
484 let cfg = if config.properties().is_empty() {
485 self.config.clone()
486 } else {
487 KafkaSinkConfig::from_config(config)?
488 };
489 let consistency = SinkConsistency::DurableAtLeastOnce;
492 let topology = if cfg.envelope == SinkEnvelope::Upsert {
496 SinkTopology::Singleton
497 } else {
498 SinkTopology::MultiWriter
499 };
500 let input_mode = if cfg.envelope == SinkEnvelope::Upsert {
501 SinkInputMode::FullChangelog
502 } else {
503 SinkInputMode::AppendOnly
504 };
505 Ok(SinkContract::new(consistency, topology, input_mode))
506 }
507
508 async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError> {
509 self.state = ConnectorState::Initializing;
510
511 if !config.properties().is_empty() {
512 let parsed = KafkaSinkConfig::from_config(config)?;
513 self.config = parsed;
514 self.serializer = select_serializer(
515 self.config.format,
516 &self.schema,
517 Arc::clone(&self.avro_schema_id),
518 self.schema_registry.clone(),
519 )?;
520 self.partitioner = select_partitioner(self.config.partitioner);
521 }
522 self.config.validate()?;
523 info!(
524 brokers = %self.config.bootstrap_servers,
525 topic = %self.config.topic,
526 format = %self.config.format,
527 "opening Kafka sink connector"
528 );
529
530 if let Some(ref url) = self.config.schema_registry_url {
531 if self.schema_registry.is_none() {
532 let sr = if let Some(ref ca_path) = self.config.schema_registry_ssl_ca_location {
533 SchemaRegistryClient::with_tls(
534 url,
535 self.config.schema_registry_auth.clone(),
536 ca_path,
537 )?
538 } else {
539 SchemaRegistryClient::new(url, self.config.schema_registry_auth.clone())?
540 };
541 self.schema_registry = Some(Arc::new(sr));
542 }
543 }
544
545 if self.config.format == Format::Avro {
549 if let Some(ref sr) = self.schema_registry {
550 if let Some(ref compat) = self.config.schema_compatibility {
551 let subject = format!("{}-value", self.config.topic);
552 sr.set_compatibility_level(&subject, *compat).await?;
553 }
554 }
555 }
556
557 let rdkafka_config: ClientConfig = self.config.to_rdkafka_config();
558 let producer: FutureProducer = rdkafka_config
559 .create()
560 .map_err(|error| producer_creation_error("main", &error))?;
561 self.producer = Some(producer);
562
563 if self.config.dlq_topic.is_some() {
567 let dlq_config = self.config.to_dlq_rdkafka_config();
568 match dlq_config.create::<FutureProducer>() {
569 Ok(dlq_producer) => self.dlq_producer = Some(dlq_producer),
570 Err(error) => {
571 self.retire_producers();
572 return Err(producer_creation_error("DLQ", &error));
573 }
574 }
575 }
576
577 self.topic_partition_count = None;
580 let producer = self
581 .producer
582 .as_ref()
583 .expect("Kafka producer was installed above")
584 .clone();
585 let topic = self.config.topic.clone();
586 let metadata_guard = self
587 .task_owner
588 .track()
589 .expect("live Kafka sink must admit its metadata lookup");
590 let metadata = tokio::task::spawn_blocking(move || -> Result<i32, ConnectorError> {
591 let _metadata_guard = metadata_guard;
592 let metadata = producer
593 .client()
594 .fetch_metadata(Some(&topic), Duration::from_secs(5))
595 .map_err(|error| fetch_error(&topic, &error))?;
596 let topic_metadata = metadata
597 .topics()
598 .iter()
599 .find(|candidate| candidate.name() == topic.as_str())
600 .ok_or_else(|| invalid_response(&topic, "metadata response omitted the topic"))?;
601 if let Some(error) = topic_metadata.error() {
602 return Err(topic_error(&topic, error.into()));
603 }
604 i32::try_from(topic_metadata.partitions().len())
605 .ok()
606 .filter(|count| *count > 0)
607 .ok_or_else(|| invalid_response(&topic, "broker returned no partitions"))
608 })
609 .await;
610 match metadata {
611 Ok(Ok(count)) => {
612 self.topic_partition_count = Some(count);
613 info!(
614 topic = %self.config.topic,
615 partitions = count,
616 "queried topic partition count from broker"
617 );
618 }
619 Ok(Err(error)) => {
620 self.retire_producers();
621 return Err(error);
622 }
623 Err(error) => {
624 self.retire_producers();
625 return Err(ConnectorError::Internal(format!(
626 "Kafka metadata worker for topic '{}' failed: {error}",
627 self.config.topic
628 )));
629 }
630 }
631
632 self.state = ConnectorState::Running;
633 info!("Kafka sink connector opened successfully");
634 Ok(())
635 }
636
637 #[allow(clippy::cast_possible_truncation)]
638 #[allow(clippy::too_many_lines)]
643 async fn write_batch(
644 &mut self,
645 batch: &arrow_array::RecordBatch,
646 ) -> Result<WriteResult, ConnectorError> {
647 if self.state != ConnectorState::Running {
648 return Err(ConnectorError::InvalidState {
649 expected: "Running".into(),
650 actual: self.state.to_string(),
651 });
652 }
653
654 if self.config.envelope == SinkEnvelope::Upsert {
657 return self.write_upsert_batch(batch).await;
658 }
659
660 self.ensure_schema_ready(&batch.schema()).await?;
661
662 let payloads = self.serialize_payloads(batch)?;
663
664 let keys = self.extract_keys(batch)?;
665 let partition_count = self.required_partition_count()?;
666 let producer = required_producer(self.producer.as_ref())?;
667
668 let mut delivery_futures = Vec::with_capacity(payloads.len());
673 let mut dlq_candidates = Vec::new();
674 let mut enqueue_failure: Option<(KafkaFailure, usize)> = None;
675 let main_queue_deadline = Instant::now() + QUEUE_RETRY_TIMEOUT;
676 for (i, payload) in payloads.iter().enumerate() {
677 let key: Option<&[u8]> = keys.as_ref().map(|kb| kb.key(i)).filter(|k| !k.is_empty());
678 let partition = self.partitioner.partition(key, partition_count);
679
680 let mut record = FutureRecord::to(&self.config.topic).payload(payload.as_slice());
681 if let Some(k) = key {
682 record = record.key(k);
683 }
684 if let Some(p) = partition {
685 record = record.partition(p);
686 }
687
688 match Self::enqueue_with_queue_retry(producer, record, main_queue_deadline).await {
689 Ok(future) => delivery_futures.push((i, Instant::now(), future)),
690 Err(error) => {
691 self.metrics.record_error();
692 let mut failure = KafkaFailure::enqueue(&error, "Kafka");
693 if self.dlq_producer.is_some() && failure.dlq_eligible() {
694 dlq_candidates.push((i, failure.detail));
695 continue;
696 }
697
698 let affected = payloads.len() - i;
699 if affected > 1 {
700 let _ = write!(
701 failure.detail,
702 "; {} later record(s) were not attempted",
703 affected - 1
704 );
705 }
706 enqueue_failure = Some((failure, affected));
707 break;
708 }
709 }
710 }
711
712 let dlq_candidate_count = dlq_candidates.len();
717 let mut dlq_delivery_futures = Vec::with_capacity(dlq_candidate_count);
718 let mut dlq_enqueue_failure: Option<(KafkaFailure, usize)> = None;
719 let dlq_queue_deadline = Instant::now() + QUEUE_RETRY_TIMEOUT;
720 for (position, (row, original_error)) in dlq_candidates.into_iter().enumerate() {
721 let key = keys
722 .as_ref()
723 .map(|buffer| buffer.key(row))
724 .filter(|key| !key.is_empty());
725 match self
726 .enqueue_dlq(&payloads[row], key, &original_error, dlq_queue_deadline)
727 .await
728 {
729 Ok(future) => dlq_delivery_futures.push((row, original_error, future)),
730 Err(mut failure) => {
731 let affected = dlq_candidate_count - position;
732 if affected > 1 {
733 let _ = write!(
734 failure.detail,
735 "; {} later DLQ record(s) were not attempted",
736 affected - 1
737 );
738 }
739 dlq_enqueue_failure = Some((failure, affected));
740 break;
741 }
742 }
743 }
744
745 let mut records_written: usize = 0;
746 let mut bytes_written: u64 = 0;
747 let mut definitely_not_persisted: usize = 0;
748 let mut ambiguous: usize = 0;
749 let mut dlq_records: usize = 0;
750 let mut dlq_bytes: u64 = 0;
751 let mut first_error: Option<String> = None;
752 let mut retryable = true;
753 for (row, send_time, future) in delivery_futures {
754 match future.await {
755 Ok(Ok(_)) => {
756 let latency_us = send_time.elapsed().as_micros() as u64;
757 self.metrics.record_produce_latency(latency_us);
758 records_written += 1;
759 bytes_written += payloads[row].len() as u64;
760 }
761 Ok(Err((error, _))) => {
762 self.metrics.record_error();
763 let failure = KafkaFailure::delivery(&error, "Kafka");
764 record_failure(
765 &failure,
766 1,
767 &mut definitely_not_persisted,
768 &mut ambiguous,
769 &mut first_error,
770 &mut retryable,
771 );
772 }
773 Err(_) => {
774 self.metrics.record_error();
775 let failure = KafkaFailure::canceled("Kafka");
776 record_failure(
777 &failure,
778 1,
779 &mut definitely_not_persisted,
780 &mut ambiguous,
781 &mut first_error,
782 &mut retryable,
783 );
784 }
785 }
786 }
787 for (row, original_error, future) in dlq_delivery_futures {
788 match future.await {
789 Ok(Ok(_)) => {
790 self.metrics.record_dlq();
791 dlq_records += 1;
792 dlq_bytes += payloads[row].len() as u64;
793 }
794 Ok(Err((error, _))) => {
795 self.metrics.record_error();
796 let mut failure = KafkaFailure::delivery(&error, "Kafka DLQ");
797 failure.detail = format!("original: {original_error}; {}", failure.detail);
798 warn!(
799 original_error = %original_error,
800 dlq_error = %failure.detail,
801 "failed to route definitely rejected Kafka record to DLQ"
802 );
803 record_failure(
804 &failure,
805 1,
806 &mut definitely_not_persisted,
807 &mut ambiguous,
808 &mut first_error,
809 &mut retryable,
810 );
811 }
812 Err(_) => {
813 self.metrics.record_error();
814 let mut failure = KafkaFailure::canceled("Kafka DLQ");
815 failure.detail = format!("original: {original_error}; {}", failure.detail);
816 record_failure(
817 &failure,
818 1,
819 &mut definitely_not_persisted,
820 &mut ambiguous,
821 &mut first_error,
822 &mut retryable,
823 );
824 }
825 }
826 }
827 if let Some((failure, affected)) = dlq_enqueue_failure {
828 record_failure(
829 &failure,
830 affected,
831 &mut definitely_not_persisted,
832 &mut ambiguous,
833 &mut first_error,
834 &mut retryable,
835 );
836 }
837 if let Some((failure, affected)) = enqueue_failure {
838 record_failure(
839 &failure,
840 affected,
841 &mut definitely_not_persisted,
842 &mut ambiguous,
843 &mut first_error,
844 &mut retryable,
845 );
846 }
847
848 self.metrics
849 .record_write(records_written as u64, bytes_written);
850
851 debug!(
852 records = records_written,
853 dlq_records,
854 bytes = bytes_written,
855 definitely_not_persisted,
856 ambiguous,
857 "wrote batch to Kafka"
858 );
859
860 let applied = records_written + dlq_records;
861 if definitely_not_persisted > 0 || ambiguous > 0 {
862 return Err(unresolved_delivery_error(
863 "produce",
864 payloads.len(),
865 applied,
866 definitely_not_persisted,
867 ambiguous,
868 first_error,
869 retryable,
870 ));
871 }
872
873 Ok(WriteResult::new(applied, bytes_written + dlq_bytes))
874 }
875
876 fn schema(&self) -> SchemaRef {
877 self.schema.clone()
878 }
879
880 fn suggested_write_timeout(&self) -> Duration {
881 kafka_write_timeout(self.config.delivery_timeout)
885 }
886
887 async fn flush(&mut self) -> Result<(), ConnectorError> {
888 Ok(())
893 }
894
895 async fn close(&mut self) -> Result<(), ConnectorError> {
896 info!("closing Kafka sink connector");
897
898 self.retire_producers();
902 self.state = ConnectorState::Closed;
903 info!("Kafka sink connector closed");
904 Ok(())
905 }
906}
907
908impl Drop for KafkaSink {
909 fn drop(&mut self) {
910 self.retire_producers();
911 }
912}
913
914impl std::fmt::Debug for KafkaSink {
915 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
916 f.debug_struct("KafkaSink")
917 .field("state", &self.state)
918 .field("topic", &self.config.topic)
919 .field("format", &self.config.format)
920 .finish_non_exhaustive()
921 }
922}
923
924fn select_partitioner(strategy: PartitionStrategy) -> Box<dyn KafkaPartitioner> {
926 match strategy {
927 PartitionStrategy::KeyHash => Box::new(KeyHashPartitioner::new()),
928 PartitionStrategy::RoundRobin => Box::new(RoundRobinPartitioner::new()),
929 PartitionStrategy::Sticky => Box::new(StickyPartitioner::new(100)),
930 }
931}
932
933#[cfg(test)]
934mod tests;