1use std::fmt::Write as _;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use arrow_schema::SchemaRef;
8use async_trait::async_trait;
9use rdkafka::error::{KafkaError, RDKafkaErrorCode};
10use rdkafka::message::OwnedHeaders;
11use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord, Producer};
12use rdkafka::ClientConfig;
13use tracing::{debug, info, warn};
14
15use crate::config::{ConnectorConfig, ConnectorState};
16use crate::connector::{
17 ConnectorTaskOwner, ConnectorTaskTracker, CoordinatedCommitter, DeliveryGuarantee,
18 SinkConnector, SinkConsistency, SinkContract, SinkInputMode, SinkTopology, WriteResult,
19};
20use crate::error::ConnectorError;
21use crate::serde::RecordSerializer;
22
23use super::metadata_error::{fetch_error, invalid_response, topic_error};
24use super::partitioner::{
25 KafkaPartitioner, KeyHashPartitioner, RoundRobinPartitioner, StickyPartitioner,
26};
27use super::schema_registry::SchemaRegistryClient;
28use super::sink_config::{KafkaSinkConfig, PartitionStrategy, SinkEnvelope};
29use super::sink_metrics::KafkaSinkMetrics;
30
31mod commit_marker;
32mod exact_commit;
33mod failure;
34mod keys;
35mod lifecycle;
36mod serialization;
37mod staged_epoch;
38mod upsert;
39
40use exact_commit::KafkaExactCommitter;
41use failure::{
42 kafka_write_timeout, producer_creation_error, queue_retry_delay, record_failure,
43 unresolved_delivery_error, validate_payload_cardinality, KafkaFailure, KafkaFailureCertainty,
44 KafkaFailureScope, QUEUE_RETRY_TIMEOUT,
45};
46use keys::{KeyBuffer, KeyEncoder};
47use serialization::select_serializer;
48use staged_epoch::StagedEpoch;
49
50fn required_producer(producer: Option<&FutureProducer>) -> Result<&FutureProducer, ConnectorError> {
51 producer.ok_or_else(|| ConnectorError::InvalidState {
52 expected: "producer initialized".into(),
53 actual: "producer is None".into(),
54 })
55}
56
57pub struct KafkaSink {
73 producer: Option<FutureProducer>,
75 config: KafkaSinkConfig,
77 serializer: Box<dyn RecordSerializer>,
79 partitioner: Box<dyn KafkaPartitioner>,
81 state: ConnectorState,
83 dlq_producer: Option<FutureProducer>,
85 metrics: KafkaSinkMetrics,
87 schema: SchemaRef,
89 writer_schema: Option<SchemaRef>,
90 writer_projection: Vec<usize>,
91 schema_registry: Option<Arc<SchemaRegistryClient>>,
93 avro_schema_id: Arc<std::sync::atomic::AtomicU32>,
95 topic_partition_count: Option<i32>,
98 key_encoder: Option<KeyEncoder>,
100 json_keys: crate::serde::json::JsonSerializer,
102 unkeyed_partition: i32,
104 staged: Option<StagedEpoch>,
107 exact: Option<Arc<KafkaExactCommitter>>,
109 task_owner: ConnectorTaskOwner,
111 task_tracker: ConnectorTaskTracker,
113}
114
115impl KafkaSink {
116 #[must_use]
123 pub fn new(
124 schema: SchemaRef,
125 config: KafkaSinkConfig,
126 registry: Option<&prometheus::Registry>,
127 ) -> Self {
128 let avro_schema_id = Arc::new(std::sync::atomic::AtomicU32::new(0));
129 let serializer =
130 select_serializer(config.format, &schema, Arc::clone(&avro_schema_id), None)
131 .expect("format validated in KafkaSinkConfig::validate()");
132 let partitioner = select_partitioner(config.partitioner);
133 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
134 let (staged, exact) = exactly_once_parts(&config, &task_owner);
135
136 Self {
137 producer: None,
138 config,
139 serializer,
140 partitioner,
141 state: ConnectorState::Created,
142 dlq_producer: None,
143 metrics: KafkaSinkMetrics::new(registry),
144 schema,
145 writer_schema: None,
146 writer_projection: Vec::new(),
147 schema_registry: None,
148 avro_schema_id,
149 topic_partition_count: None,
150 key_encoder: None,
151 json_keys: crate::serde::json::JsonSerializer::new(),
152 unkeyed_partition: 0,
153 staged,
154 exact,
155 task_owner,
156 task_tracker,
157 }
158 }
159
160 #[must_use]
167 pub fn with_schema_registry(
168 schema: SchemaRef,
169 config: KafkaSinkConfig,
170 sr_client: SchemaRegistryClient,
171 ) -> Self {
172 let sr = Arc::new(sr_client);
173 let avro_schema_id = Arc::new(std::sync::atomic::AtomicU32::new(0));
174 let serializer = select_serializer(
175 config.format,
176 &schema,
177 Arc::clone(&avro_schema_id),
178 Some(Arc::clone(&sr)),
179 )
180 .expect("format validated in KafkaSinkConfig::validate()");
181 let partitioner = select_partitioner(config.partitioner);
182 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
183 let (staged, exact) = exactly_once_parts(&config, &task_owner);
184
185 Self {
186 producer: None,
187 config,
188 serializer,
189 partitioner,
190 state: ConnectorState::Created,
191 dlq_producer: None,
192 metrics: KafkaSinkMetrics::new(None),
193 schema,
194 writer_schema: None,
195 writer_projection: Vec::new(),
196 schema_registry: Some(sr),
197 avro_schema_id,
198 topic_partition_count: None,
199 key_encoder: None,
200 json_keys: crate::serde::json::JsonSerializer::new(),
201 unkeyed_partition: 0,
202 staged,
203 exact,
204 task_owner,
205 task_tracker,
206 }
207 }
208
209 #[must_use]
211 pub fn state(&self) -> ConnectorState {
212 self.state
213 }
214
215 #[must_use]
217 pub fn has_schema_registry(&self) -> bool {
218 self.schema_registry.is_some()
219 }
220
221 fn serialize_payloads(
222 &self,
223 batch: &arrow_array::RecordBatch,
224 ) -> Result<Vec<Vec<u8>>, ConnectorError> {
225 let projected;
226 let batch = if let Some(writer) = &self.writer_schema {
227 let columns = self
228 .writer_projection
229 .iter()
230 .map(|index| Arc::clone(batch.column(*index)))
231 .collect();
232 projected = arrow_array::RecordBatch::try_new(Arc::clone(writer), columns).map_err(
233 |error| ConnectorError::SchemaMismatch(format!("writer projection: {error}")),
234 )?;
235 &projected
236 } else {
237 batch
238 };
239 let payloads = self.serializer.serialize(batch).map_err(|error| {
240 self.metrics.record_serialization_error();
241 ConnectorError::Serde(error)
242 })?;
243 validate_payload_cardinality(batch.num_rows(), payloads.len())?;
244 Ok(payloads)
245 }
246
247 fn bind_keys(&mut self) -> Result<(), ConnectorError> {
249 let columns = self.config.key_columns();
250 self.key_encoder = if columns.is_empty() {
251 None
252 } else {
253 Some(KeyEncoder::bind(
254 &self.schema,
255 &columns,
256 self.config.key_format,
257 )?)
258 };
259 Ok(())
260 }
261
262 fn required_partition_count(&self) -> Result<i32, ConnectorError> {
263 self.topic_partition_count
264 .ok_or_else(|| ConnectorError::InvalidState {
265 expected: "broker topic metadata installed".into(),
266 actual: "partition count is unavailable".into(),
267 })
268 }
269
270 fn extract_keys(
272 &self,
273 batch: &arrow_array::RecordBatch,
274 ) -> Result<Option<KeyBuffer>, ConnectorError> {
275 self.key_encoder
276 .as_ref()
277 .map(|encoder| encoder.encode(batch, &self.json_keys))
278 .transpose()
279 }
280
281 fn route(&mut self, key: Option<&[u8]>, partition_count: i32) -> i32 {
284 self.partitioner
285 .partition(key, partition_count)
286 .unwrap_or(self.unkeyed_partition)
287 }
288
289 fn rotate_unkeyed_partition(&mut self, partition_count: i32) {
291 self.unkeyed_partition = (self.unkeyed_partition + 1) % partition_count;
292 }
293
294 fn route_batch(
296 &mut self,
297 batch: &arrow_array::RecordBatch,
298 rows: usize,
299 ) -> Result<(Option<KeyBuffer>, Vec<i32>), ConnectorError> {
300 let keys = self.extract_keys(batch)?;
301 let partition_count = self.required_partition_count()?;
302 let partitions = (0..rows)
303 .map(|row| {
304 self.route(
305 keys.as_ref().and_then(|keys| keys.key(row)),
306 partition_count,
307 )
308 })
309 .collect();
310 self.rotate_unkeyed_partition(partition_count);
311 Ok((keys, partitions))
312 }
313
314 async fn enqueue_dlq(
318 &self,
319 payload: &[u8],
320 key: Option<&[u8]>,
321 error_msg: &str,
322 queue_deadline: Instant,
323 ) -> Result<DeliveryFuture, KafkaFailure> {
324 let dlq_producer = self.dlq_producer.as_ref().ok_or_else(|| KafkaFailure {
325 certainty: KafkaFailureCertainty::DefinitelyNotPersisted,
326 scope: KafkaFailureScope::Connector,
327 detail: "DLQ topic or producer is not configured".into(),
328 retryable: false,
329 })?;
330 let dlq_topic = self.config.dlq_topic.as_ref().ok_or_else(|| KafkaFailure {
331 certainty: KafkaFailureCertainty::DefinitelyNotPersisted,
332 scope: KafkaFailureScope::Connector,
333 detail: "DLQ topic or producer is not configured".into(),
334 retryable: false,
335 })?;
336
337 let now = std::time::SystemTime::now()
338 .duration_since(std::time::UNIX_EPOCH)
339 .unwrap_or_else(|_| {
340 tracing::warn!("system clock before Unix epoch — using 0 for DLQ timestamp");
341 std::time::Duration::ZERO
342 })
343 .as_millis()
344 .to_string();
345 let headers = OwnedHeaders::new()
346 .insert(rdkafka::message::Header {
347 key: "__dlq.error",
348 value: Some(error_msg.as_bytes()),
349 })
350 .insert(rdkafka::message::Header {
351 key: "__dlq.topic",
352 value: Some(self.config.topic.as_bytes()),
353 })
354 .insert(rdkafka::message::Header {
355 key: "__dlq.timestamp",
356 value: Some(now.as_bytes()),
357 });
358
359 let mut record = FutureRecord::to(dlq_topic)
360 .payload(payload)
361 .headers(headers);
362
363 if let Some(k) = key {
364 record = record.key(k);
365 }
366
367 Self::enqueue_with_queue_retry(dlq_producer, record, queue_deadline)
368 .await
369 .map_err(|error| KafkaFailure::enqueue(&error, "Kafka DLQ"))
370 }
371
372 async fn enqueue_with_queue_retry(
377 producer: &FutureProducer,
378 mut record: FutureRecord<'_, [u8], [u8]>,
379 queue_deadline: Instant,
380 ) -> Result<DeliveryFuture, KafkaError> {
381 loop {
382 match producer.send_result(record) {
384 Ok(fut) => return Ok(fut),
385 Err((error @ KafkaError::MessageProduction(RDKafkaErrorCode::QueueFull), r)) => {
386 let Some(delay) = queue_retry_delay(queue_deadline, Instant::now()) else {
387 return Err(error);
388 };
389 record = r;
390 tokio::time::sleep(delay).await;
391 }
392 Err((error, _)) => return Err(error),
393 }
394 }
395 }
396}
397
398#[async_trait]
399impl SinkConnector for KafkaSink {
400 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
401 Some(self.task_tracker.clone())
402 }
403
404 fn contract(&self, config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
405 let cfg = if config.properties().is_empty() {
406 self.config.clone()
407 } else {
408 KafkaSinkConfig::from_config(config)?
409 };
410 let exactly_once = cfg.delivery == DeliveryGuarantee::ExactlyOnce;
411 let consistency = if exactly_once {
412 SinkConsistency::CheckpointCommittable
413 } else {
414 SinkConsistency::DurableAtLeastOnce
415 };
416 let topology = if cfg.envelope == SinkEnvelope::Upsert && !exactly_once {
420 SinkTopology::Singleton
421 } else {
422 SinkTopology::MultiWriter
423 };
424 let input_mode = if cfg.envelope == SinkEnvelope::Upsert {
425 SinkInputMode::FullChangelog
426 } else {
427 SinkInputMode::AppendOnly
428 };
429 Ok(SinkContract::new(consistency, topology, input_mode))
430 }
431
432 async fn resolve_schema(
433 &mut self,
434 config: &ConnectorConfig,
435 input: SchemaRef,
436 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
437 super::schema_resolution::resolve_sink(config, input).await
438 }
439
440 async fn prepare_schema(
441 &mut self,
442 config: &ConnectorConfig,
443 binding: &mut crate::schema::resolution::SchemaBinding,
444 ) -> Result<(), ConnectorError> {
445 super::schema_resolution::prepare_sink(config, binding).await
446 }
447
448 async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError> {
449 self.state = ConnectorState::Initializing;
450
451 if !config.properties().is_empty() {
452 let parsed = KafkaSinkConfig::from_config(config)?;
453 self.config = parsed;
454 self.serializer = select_serializer(
455 self.config.format,
456 &self.schema,
457 Arc::clone(&self.avro_schema_id),
458 self.schema_registry.clone(),
459 )?;
460 self.partitioner = select_partitioner(self.config.partitioner);
461 }
462 self.config.validate()?;
463 info!(
464 brokers = %self.config.bootstrap_servers,
465 topic = %self.config.topic,
466 format = %self.config.format,
467 "opening Kafka sink connector"
468 );
469
470 self.install_schema_contract(config).await?;
471 self.bind_keys()?;
472 info!(
473 settings = ?super::librdkafka::redacted_settings(&self.config.to_rdkafka_config()),
474 "Kafka producer effective configuration"
475 );
476
477 let rdkafka_config: ClientConfig = self.config.to_rdkafka_config();
478 let producer: FutureProducer = rdkafka_config
479 .create()
480 .map_err(|error| producer_creation_error("main", &error))?;
481 self.producer = Some(producer);
482
483 if self.config.dlq_topic.is_some() {
487 let dlq_config = self.config.to_dlq_rdkafka_config();
488 match dlq_config.create::<FutureProducer>() {
489 Ok(dlq_producer) => self.dlq_producer = Some(dlq_producer),
490 Err(error) => {
491 self.retire_producers();
492 return Err(producer_creation_error("DLQ", &error));
493 }
494 }
495 }
496
497 self.topic_partition_count = None;
500 let producer = self
501 .producer
502 .as_ref()
503 .expect("Kafka producer was installed above")
504 .clone();
505 let topic = self.config.topic.clone();
506 let metadata_guard = self
507 .task_owner
508 .track()
509 .expect("live Kafka sink must admit its metadata lookup");
510 let metadata = tokio::task::spawn_blocking(move || -> Result<i32, ConnectorError> {
511 let _metadata_guard = metadata_guard;
512 let metadata = producer
513 .client()
514 .fetch_metadata(Some(&topic), Duration::from_secs(5))
515 .map_err(|error| fetch_error(&topic, &error))?;
516 let topic_metadata = metadata
517 .topics()
518 .iter()
519 .find(|candidate| candidate.name() == topic.as_str())
520 .ok_or_else(|| invalid_response(&topic, "metadata response omitted the topic"))?;
521 if let Some(error) = topic_metadata.error() {
522 return Err(topic_error(&topic, error.into()));
523 }
524 i32::try_from(topic_metadata.partitions().len())
525 .ok()
526 .filter(|count| *count > 0)
527 .ok_or_else(|| invalid_response(&topic, "broker returned no partitions"))
528 })
529 .await;
530 match metadata {
531 Ok(Ok(count)) => {
532 let routed = match self.config.partition_count {
533 Some(pinned) if count < pinned => {
534 self.retire_producers();
535 return Err(ConnectorError::ConfigurationError(format!(
536 "Kafka topic '{}' has {count} partitions but partition.count is \
537 {pinned}; keyed records would route to partitions that do not exist",
538 self.config.topic
539 )));
540 }
541 Some(pinned) => pinned,
542 None => count,
543 };
544 self.topic_partition_count = Some(routed);
545 self.unkeyed_partition = 0;
546 info!(
547 topic = %self.config.topic,
548 partitions = count,
549 routed_partitions = routed,
550 "queried topic partition count from broker"
551 );
552 }
553 Ok(Err(error)) => {
554 self.retire_producers();
555 return Err(error);
556 }
557 Err(error) => {
558 self.retire_producers();
559 return Err(ConnectorError::Internal(format!(
560 "Kafka metadata worker for topic '{}' failed: {error}",
561 self.config.topic
562 )));
563 }
564 }
565
566 (self.staged, self.exact) = exactly_once_parts(&self.config, &self.task_owner);
567 self.state = ConnectorState::Running;
568 info!("Kafka sink connector opened successfully");
569 Ok(())
570 }
571
572 #[allow(clippy::cast_possible_truncation)]
573 #[allow(clippy::too_many_lines)]
578 async fn write_batch(
579 &mut self,
580 batch: &arrow_array::RecordBatch,
581 ) -> Result<WriteResult, ConnectorError> {
582 if self.state != ConnectorState::Running {
583 return Err(ConnectorError::InvalidState {
584 expected: "Running".into(),
585 actual: self.state.to_string(),
586 });
587 }
588
589 if self.config.envelope == SinkEnvelope::Upsert {
592 return self.write_upsert_batch(batch).await;
593 }
594
595 self.ensure_schema_ready(&batch.schema())?;
596
597 let payloads = self.serialize_payloads(batch)?;
598
599 let (keys, partitions) = self.route_batch(batch, payloads.len())?;
600 if let Some(stage) = self.staged.as_mut() {
601 let values = payloads.iter().map(|payload| Some(payload.as_slice()));
602 return stage_records(stage, &partitions, keys.as_ref(), values);
603 }
604 let producer = required_producer(self.producer.as_ref())?;
605
606 let mut delivery_futures = Vec::with_capacity(payloads.len());
611 let mut dlq_candidates = Vec::new();
612 let mut enqueue_failure: Option<(KafkaFailure, usize)> = None;
613 let main_queue_deadline = Instant::now() + QUEUE_RETRY_TIMEOUT;
614 for (i, payload) in payloads.iter().enumerate() {
615 let mut record = FutureRecord::to(&self.config.topic)
616 .payload(payload.as_slice())
617 .partition(partitions[i]);
618 if let Some(key) = keys.as_ref().and_then(|keys| keys.key(i)) {
619 record = record.key(key);
620 }
621
622 match Self::enqueue_with_queue_retry(producer, record, main_queue_deadline).await {
623 Ok(future) => delivery_futures.push((i, Instant::now(), future)),
624 Err(error) => {
625 self.metrics.record_error();
626 let mut failure = KafkaFailure::enqueue(&error, "Kafka");
627 if self.dlq_producer.is_some() && failure.dlq_eligible() {
628 dlq_candidates.push((i, failure.detail));
629 continue;
630 }
631
632 let affected = payloads.len() - i;
633 if affected > 1 {
634 let _ = write!(
635 failure.detail,
636 "; {} later record(s) were not attempted",
637 affected - 1
638 );
639 }
640 enqueue_failure = Some((failure, affected));
641 break;
642 }
643 }
644 }
645
646 let dlq_candidate_count = dlq_candidates.len();
651 let mut dlq_delivery_futures = Vec::with_capacity(dlq_candidate_count);
652 let mut dlq_enqueue_failure: Option<(KafkaFailure, usize)> = None;
653 let dlq_queue_deadline = Instant::now() + QUEUE_RETRY_TIMEOUT;
654 for (position, (row, original_error)) in dlq_candidates.into_iter().enumerate() {
655 let key = keys.as_ref().and_then(|buffer| buffer.key(row));
656 match self
657 .enqueue_dlq(&payloads[row], key, &original_error, dlq_queue_deadline)
658 .await
659 {
660 Ok(future) => dlq_delivery_futures.push((row, original_error, future)),
661 Err(mut failure) => {
662 let affected = dlq_candidate_count - position;
663 if affected > 1 {
664 let _ = write!(
665 failure.detail,
666 "; {} later DLQ record(s) were not attempted",
667 affected - 1
668 );
669 }
670 dlq_enqueue_failure = Some((failure, affected));
671 break;
672 }
673 }
674 }
675
676 let mut records_written: usize = 0;
677 let mut bytes_written: u64 = 0;
678 let mut definitely_not_persisted: usize = 0;
679 let mut ambiguous: usize = 0;
680 let mut dlq_records: usize = 0;
681 let mut dlq_bytes: u64 = 0;
682 let mut first_error: Option<String> = None;
683 let mut retryable = true;
684 for (row, send_time, future) in delivery_futures {
685 match future.await {
686 Ok(Ok(_)) => {
687 let latency_us = send_time.elapsed().as_micros() as u64;
688 self.metrics.record_produce_latency(latency_us);
689 records_written += 1;
690 bytes_written += payloads[row].len() as u64;
691 }
692 Ok(Err((error, _))) => {
693 self.metrics.record_error();
694 let failure = KafkaFailure::delivery(&error, "Kafka");
695 record_failure(
696 &failure,
697 1,
698 &mut definitely_not_persisted,
699 &mut ambiguous,
700 &mut first_error,
701 &mut retryable,
702 );
703 }
704 Err(_) => {
705 self.metrics.record_error();
706 let failure = KafkaFailure::canceled("Kafka");
707 record_failure(
708 &failure,
709 1,
710 &mut definitely_not_persisted,
711 &mut ambiguous,
712 &mut first_error,
713 &mut retryable,
714 );
715 }
716 }
717 }
718 for (row, original_error, future) in dlq_delivery_futures {
719 match future.await {
720 Ok(Ok(_)) => {
721 self.metrics.record_dlq();
722 dlq_records += 1;
723 dlq_bytes += payloads[row].len() as u64;
724 }
725 Ok(Err((error, _))) => {
726 self.metrics.record_error();
727 let mut failure = KafkaFailure::delivery(&error, "Kafka DLQ");
728 failure.detail = format!("original: {original_error}; {}", failure.detail);
729 warn!(
730 original_error = %original_error,
731 dlq_error = %failure.detail,
732 "failed to route definitely rejected Kafka record to DLQ"
733 );
734 record_failure(
735 &failure,
736 1,
737 &mut definitely_not_persisted,
738 &mut ambiguous,
739 &mut first_error,
740 &mut retryable,
741 );
742 }
743 Err(_) => {
744 self.metrics.record_error();
745 let mut failure = KafkaFailure::canceled("Kafka DLQ");
746 failure.detail = format!("original: {original_error}; {}", failure.detail);
747 record_failure(
748 &failure,
749 1,
750 &mut definitely_not_persisted,
751 &mut ambiguous,
752 &mut first_error,
753 &mut retryable,
754 );
755 }
756 }
757 }
758 if let Some((failure, affected)) = dlq_enqueue_failure {
759 record_failure(
760 &failure,
761 affected,
762 &mut definitely_not_persisted,
763 &mut ambiguous,
764 &mut first_error,
765 &mut retryable,
766 );
767 }
768 if let Some((failure, affected)) = enqueue_failure {
769 record_failure(
770 &failure,
771 affected,
772 &mut definitely_not_persisted,
773 &mut ambiguous,
774 &mut first_error,
775 &mut retryable,
776 );
777 }
778
779 self.metrics
780 .record_write(records_written as u64, bytes_written);
781
782 debug!(
783 records = records_written,
784 dlq_records,
785 bytes = bytes_written,
786 definitely_not_persisted,
787 ambiguous,
788 "wrote batch to Kafka"
789 );
790
791 let applied = records_written + dlq_records;
792 if definitely_not_persisted > 0 || ambiguous > 0 {
793 return Err(unresolved_delivery_error(
794 "produce",
795 payloads.len(),
796 applied,
797 definitely_not_persisted,
798 ambiguous,
799 first_error,
800 retryable,
801 ));
802 }
803
804 Ok(WriteResult::new(applied, bytes_written + dlq_bytes))
805 }
806
807 fn schema(&self) -> SchemaRef {
808 self.schema.clone()
809 }
810
811 async fn begin_epoch(&mut self, _epoch: u64) -> Result<(), ConnectorError> {
812 if self.staged.as_ref().is_some_and(|stage| !stage.is_empty()) {
813 return Err(ConnectorError::InvalidState {
814 expected: "the previous exactly-once epoch to be prepared or rolled back".into(),
815 actual: "staged Kafka output remains".into(),
816 });
817 }
818 Ok(())
819 }
820
821 async fn pre_commit(&mut self, _epoch: u64) -> Result<Option<Vec<u8>>, ConnectorError> {
822 let Some(stage) = self.staged.as_mut() else {
823 return Ok(None);
824 };
825 if stage.is_empty() {
826 return Ok(None);
827 }
828 let partition_count =
829 self.topic_partition_count
830 .ok_or_else(|| ConnectorError::InvalidState {
831 expected: "broker topic metadata installed".into(),
832 actual: "partition count is unavailable".into(),
833 })?;
834 let descriptor = stage.encode(&self.config.topic, partition_count)?;
835 stage.clear();
836 Ok(Some(descriptor))
837 }
838
839 async fn rollback_epoch(&mut self, _epoch: u64) -> Result<(), ConnectorError> {
840 if let Some(stage) = self.staged.as_mut() {
841 stage.clear();
842 }
843 Ok(())
844 }
845
846 fn as_coordinated_committer(&self) -> Option<&dyn CoordinatedCommitter> {
847 self.exact
848 .as_deref()
849 .map(|committer| committer as &dyn CoordinatedCommitter)
850 }
851
852 fn suggested_write_timeout(&self) -> Duration {
853 kafka_write_timeout(self.config.delivery_timeout)
857 }
858
859 async fn flush(&mut self) -> Result<(), ConnectorError> {
860 Ok(())
865 }
866
867 async fn close(&mut self) -> Result<(), ConnectorError> {
868 info!("closing Kafka sink connector");
869
870 self.retire_producers();
874 self.state = ConnectorState::Closed;
875 info!("Kafka sink connector closed");
876 Ok(())
877 }
878}
879
880impl Drop for KafkaSink {
881 fn drop(&mut self) {
882 self.retire_producers();
883 }
884}
885
886impl std::fmt::Debug for KafkaSink {
887 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
888 f.debug_struct("KafkaSink")
889 .field("state", &self.state)
890 .field("topic", &self.config.topic)
891 .field("format", &self.config.format)
892 .finish_non_exhaustive()
893 }
894}
895
896fn stage_records<'a>(
898 stage: &mut StagedEpoch,
899 partitions: &[i32],
900 keys: Option<&KeyBuffer>,
901 values: impl ExactSizeIterator<Item = Option<&'a [u8]>>,
902) -> Result<WriteResult, ConnectorError> {
903 let rows = values.len();
904 for (row, value) in values.enumerate() {
905 stage.push(partitions[row], keys.and_then(|keys| keys.key(row)), value)?;
906 }
907 Ok(WriteResult::new(rows, 0))
908}
909
910fn exactly_once_parts(
912 config: &KafkaSinkConfig,
913 task_owner: &ConnectorTaskOwner,
914) -> (Option<StagedEpoch>, Option<Arc<KafkaExactCommitter>>) {
915 match (config.delivery, config.commit_topic.as_ref()) {
916 (DeliveryGuarantee::ExactlyOnce, Some(commit_topic)) => (
917 Some(StagedEpoch::new(
918 config.epoch_max_bytes,
919 config.message_max_bytes().unwrap_or(usize::MAX),
920 )),
921 Some(Arc::new(KafkaExactCommitter::new(
922 config.clone(),
923 commit_topic.clone(),
924 task_owner.admission(),
925 ))),
926 ),
927 _ => (None, None),
928 }
929}
930
931fn select_partitioner(strategy: PartitionStrategy) -> Box<dyn KafkaPartitioner> {
933 match strategy {
934 PartitionStrategy::KeyHash => Box::new(KeyHashPartitioner::new()),
935 PartitionStrategy::RoundRobin => Box::new(RoundRobinPartitioner::new()),
936 PartitionStrategy::Sticky => Box::new(StickyPartitioner::new(100)),
937 }
938}
939
940#[cfg(test)]
941mod tests;