1use std::collections::BTreeMap;
16use std::fmt::Debug;
17use std::sync::{Arc, LazyLock};
18use std::time::Duration;
19
20use anyhow::anyhow;
21use futures::{Future, FutureExt, TryFuture};
22use rdkafka::ClientConfig;
23use rdkafka::error::KafkaError;
24use rdkafka::message::ToBytes;
25use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord};
26use rdkafka::types::RDKafkaErrorCode;
27use risingwave_common::array::StreamChunk;
28use risingwave_common::catalog::Schema;
29use risingwave_common::log::LogSuppressor;
30use serde::Deserialize;
31use serde_with::{DisplayFromStr, serde_as};
32use strum_macros::{Display, EnumString};
33use thiserror_ext::AsReport;
34use with_options::WithOptions;
35
36use super::catalog::{SinkFormat, SinkFormatDesc};
37use super::{Sink, SinkError, SinkParam};
38use crate::connector_common::{
39 AwsAuthProps, KafkaCommon, KafkaConnectionProps, KafkaPrivateLinkCommon,
40 RdKafkaPropertiesCommon, read_kafka_log_level,
41};
42use crate::enforce_secret::EnforceSecret;
43use crate::sink::formatter::SinkFormatterImpl;
44use crate::sink::log_store::DeliveryFutureManagerAddFuture;
45use crate::sink::writer::{
46 AsyncTruncateLogSinkerOf, AsyncTruncateSinkWriter, AsyncTruncateSinkWriterExt, FormattedSink,
47};
48use crate::sink::{Result, SinkWriterParam};
49use crate::source::kafka::{
50 KafkaContextCommon, KafkaProperties, KafkaSplitEnumerator, RwProducerContext,
51};
52use crate::source::{SourceEnumeratorContext, SplitEnumerator};
53use crate::{
54 deserialize_duration_from_string, deserialize_u32_from_string, dispatch_sink_formatter_impl,
55};
56
57pub const KAFKA_SINK: &str = "kafka";
58
59const fn _default_max_retries() -> u32 {
60 3
61}
62
63const fn _default_retry_backoff() -> Duration {
64 Duration::from_millis(100)
65}
66
67const fn _default_max_in_flight_requests_per_connection() -> usize {
68 5
69}
70
71#[derive(Debug, Clone, PartialEq, Display, Deserialize, EnumString)]
72#[strum(serialize_all = "snake_case")]
73pub enum CompressionCodec {
74 None,
75 Gzip,
76 Snappy,
77 Lz4,
78 Zstd,
79}
80
81#[serde_as]
84#[derive(Debug, Clone, Deserialize, WithOptions)]
85pub struct RdKafkaPropertiesProducer {
86 #[serde(rename = "properties.allow.auto.create.topics")]
88 #[serde_as(as = "Option<DisplayFromStr>")]
89 #[with_option(allow_alter_on_fly)]
90 pub allow_auto_create_topics: Option<bool>,
91
92 #[serde(rename = "properties.queue.buffering.max.messages")]
95 #[serde_as(as = "Option<DisplayFromStr>")]
96 #[with_option(allow_alter_on_fly)]
97 pub queue_buffering_max_messages: Option<usize>,
98
99 #[serde(rename = "properties.queue.buffering.max.kbytes")]
102 #[serde_as(as = "Option<DisplayFromStr>")]
103 #[with_option(allow_alter_on_fly)]
104 queue_buffering_max_kbytes: Option<usize>,
105
106 #[serde(rename = "properties.queue.buffering.max.ms")]
111 #[serde_as(as = "Option<DisplayFromStr>")]
112 #[with_option(allow_alter_on_fly)]
113 queue_buffering_max_ms: Option<f64>,
114
115 #[serde(rename = "properties.enable.idempotence")]
122 #[serde_as(as = "Option<DisplayFromStr>")]
123 #[with_option(allow_alter_on_fly)]
124 enable_idempotence: Option<bool>,
125
126 #[serde(rename = "properties.message.send.max.retries")]
128 #[serde_as(as = "Option<DisplayFromStr>")]
129 #[with_option(allow_alter_on_fly)]
130 message_send_max_retries: Option<usize>,
131
132 #[serde(rename = "properties.batch.num.messages")]
134 #[serde_as(as = "Option<DisplayFromStr>")]
135 #[with_option(allow_alter_on_fly)]
136 batch_num_messages: Option<usize>,
137
138 #[serde(rename = "properties.batch.size")]
143 #[serde_as(as = "Option<DisplayFromStr>")]
144 #[with_option(allow_alter_on_fly)]
145 batch_size: Option<usize>,
146
147 #[serde(rename = "properties.compression.codec")]
149 #[serde_as(as = "Option<DisplayFromStr>")]
150 compression_codec: Option<CompressionCodec>,
151
152 #[serde(rename = "properties.message.timeout.ms")]
156 #[serde_as(as = "Option<DisplayFromStr>")]
157 #[with_option(allow_alter_on_fly)]
158 message_timeout_ms: Option<usize>,
159
160 #[serde(
162 rename = "properties.max.in.flight.requests.per.connection",
163 default = "_default_max_in_flight_requests_per_connection"
164 )]
165 #[serde_as(as = "DisplayFromStr")]
166 #[with_option(allow_alter_on_fly)]
167 max_in_flight_requests_per_connection: usize,
168
169 #[serde(rename = "properties.request.required.acks")]
170 #[serde_as(as = "Option<DisplayFromStr>")]
171 #[with_option(allow_alter_on_fly)]
172 request_required_acks: Option<i32>,
173}
174
175impl RdKafkaPropertiesProducer {
176 pub(crate) fn set_client(&self, c: &mut rdkafka::ClientConfig) {
177 if let Some(v) = self.allow_auto_create_topics {
178 c.set("allow.auto.create.topics", v.to_string());
179 }
180 if let Some(v) = self.queue_buffering_max_messages {
181 c.set("queue.buffering.max.messages", v.to_string());
182 }
183 if let Some(v) = self.queue_buffering_max_kbytes {
184 c.set("queue.buffering.max.kbytes", v.to_string());
185 }
186 if let Some(v) = self.queue_buffering_max_ms {
187 c.set("queue.buffering.max.ms", v.to_string());
188 }
189 if let Some(v) = self.enable_idempotence {
190 c.set("enable.idempotence", v.to_string());
191 }
192 if let Some(v) = self.message_send_max_retries {
193 c.set("message.send.max.retries", v.to_string());
194 }
195 if let Some(v) = self.batch_num_messages {
196 c.set("batch.num.messages", v.to_string());
197 }
198 if let Some(v) = self.batch_size {
199 c.set("batch.size", v.to_string());
200 }
201 if let Some(v) = &self.compression_codec {
202 c.set("compression.codec", v.to_string());
203 }
204 if let Some(v) = self.request_required_acks {
205 c.set("request.required.acks", v.to_string());
206 }
207 if let Some(v) = self.message_timeout_ms {
208 c.set("message.timeout.ms", v.to_string());
209 }
210 c.set(
211 "max.in.flight.requests.per.connection",
212 self.max_in_flight_requests_per_connection.to_string(),
213 );
214 }
215}
216
217#[serde_as]
218#[derive(Debug, Clone, Deserialize, WithOptions)]
219pub struct KafkaConfig {
220 #[serde(flatten)]
221 pub common: KafkaCommon,
222
223 #[serde(flatten)]
224 pub connection: KafkaConnectionProps,
225
226 #[serde(
227 rename = "properties.retry.max",
228 default = "_default_max_retries",
229 deserialize_with = "deserialize_u32_from_string"
230 )]
231 pub max_retry_num: u32,
232
233 #[serde(
234 rename = "properties.retry.interval",
235 default = "_default_retry_backoff",
236 deserialize_with = "deserialize_duration_from_string"
237 )]
238 pub retry_interval: Duration,
239
240 pub primary_key: Option<String>,
244
245 #[serde(flatten)]
246 pub rdkafka_properties_common: RdKafkaPropertiesCommon,
247
248 #[serde(flatten)]
249 pub rdkafka_properties_producer: RdKafkaPropertiesProducer,
250
251 #[serde(flatten)]
252 pub privatelink_common: KafkaPrivateLinkCommon,
253
254 #[serde(flatten)]
255 pub aws_auth_props: AwsAuthProps,
256
257 #[serde(flatten)]
258 pub unknown_fields: std::collections::HashMap<String, String>,
259}
260
261crate::impl_sink_unknown_fields!(KafkaConfig);
262
263impl EnforceSecret for KafkaConfig {
264 fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
265 KafkaConnectionProps::enforce_one(prop)?;
266 AwsAuthProps::enforce_one(prop)?;
267 Ok(())
268 }
269}
270
271impl KafkaConfig {
272 pub fn from_btreemap(values: BTreeMap<String, String>) -> Result<Self> {
273 let config = serde_json::from_value::<KafkaConfig>(serde_json::to_value(values).unwrap())
274 .map_err(|e| SinkError::Config(anyhow!(e)))?;
275
276 Ok(config)
277 }
278
279 pub(crate) fn set_client(&self, c: &mut rdkafka::ClientConfig) {
280 self.rdkafka_properties_common.set_client(c);
281 self.rdkafka_properties_producer.set_client(c);
282 }
283}
284
285impl From<KafkaConfig> for KafkaProperties {
286 fn from(val: KafkaConfig) -> Self {
287 KafkaProperties {
288 bytes_per_second: None,
289 max_num_messages: None,
290 scan_startup_mode: None,
291 time_offset: None,
292 upsert: None,
293 common: val.common,
294 connection: val.connection,
295 rdkafka_properties_common: val.rdkafka_properties_common,
296 rdkafka_properties_consumer: Default::default(),
297 privatelink_common: val.privatelink_common,
298 aws_auth_props: val.aws_auth_props,
299 group_id_prefix: None,
300 unknown_fields: Default::default(),
301 }
302 }
303}
304
305#[derive(Debug)]
306pub struct KafkaSink {
307 pub config: KafkaConfig,
308 schema: Schema,
309 pk_indices: Vec<usize>,
310 format_desc: SinkFormatDesc,
311 db_name: String,
312 sink_from_name: String,
313}
314
315impl EnforceSecret for KafkaSink {
316 fn enforce_secret<'a>(
317 prop_iter: impl Iterator<Item = &'a str>,
318 ) -> crate::error::ConnectorResult<()> {
319 for prop in prop_iter {
320 KafkaConfig::enforce_one(prop)?;
321 }
322 Ok(())
323 }
324}
325
326impl TryFrom<SinkParam> for KafkaSink {
327 type Error = SinkError;
328
329 fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
330 let schema = param.schema();
331 let pk_indices = param.downstream_pk_or_empty();
332 let config = KafkaConfig::from_btreemap(param.properties)?;
333 Ok(Self {
334 config,
335 schema,
336 pk_indices,
337 format_desc: param
338 .format_desc
339 .ok_or_else(|| SinkError::Config(anyhow!("missing FORMAT ... ENCODE ...")))?,
340 db_name: param.db_name,
341 sink_from_name: param.sink_from_name,
342 })
343 }
344}
345
346impl Sink for KafkaSink {
347 type LogSinker = AsyncTruncateLogSinkerOf<KafkaSinkWriter>;
348
349 const SINK_NAME: &'static str = KAFKA_SINK;
350
351 crate::impl_validate_sink_unknown_fields!();
352
353 async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
354 let formatter = SinkFormatterImpl::new(
355 &self.format_desc,
356 self.schema.clone(),
357 self.pk_indices.clone(),
358 self.db_name.clone(),
359 self.sink_from_name.clone(),
360 &self.config.common.topic,
361 )
362 .await?;
363 let max_delivery_buffer_size = (self
364 .config
365 .rdkafka_properties_producer
366 .queue_buffering_max_messages
367 .as_ref()
368 .cloned()
369 .unwrap_or(KAFKA_WRITER_MAX_QUEUE_SIZE) as f32
370 * KAFKA_WRITER_MAX_QUEUE_SIZE_RATIO) as usize;
371
372 Ok(KafkaSinkWriter::new(self.config.clone(), formatter)
373 .await?
374 .into_log_sinker(max_delivery_buffer_size))
375 }
376
377 async fn validate(&self) -> Result<()> {
378 if self.format_desc.format != SinkFormat::AppendOnly && self.pk_indices.is_empty() {
380 return Err(SinkError::Config(anyhow!(
381 "primary key not defined for {:?} kafka sink (please define in `primary_key` field)",
382 self.format_desc.format
383 )));
384 }
385 SinkFormatterImpl::new(
387 &self.format_desc,
388 self.schema.clone(),
389 self.pk_indices.clone(),
390 self.db_name.clone(),
391 self.sink_from_name.clone(),
392 &self.config.common.topic,
393 )
394 .await?;
395
396 let check = KafkaSplitEnumerator::new(
400 KafkaProperties::from(self.config.clone()),
401 Arc::new(SourceEnumeratorContext::dummy()),
402 )
403 .await?;
404 if let Err(e) = check.check_reachability().await {
405 return Err(SinkError::Config(
406 anyhow!(
407 "cannot connect to kafka broker ({})",
408 self.config.connection.brokers,
409 )
410 .context(e),
411 ));
412 }
413 Ok(())
414 }
415
416 fn validate_alter_config(config: &BTreeMap<String, String>) -> Result<()> {
417 KafkaConfig::from_btreemap(config.clone())?;
418 Ok(())
419 }
420}
421
422const KAFKA_WRITER_MAX_QUEUE_SIZE_RATIO: f32 = 1.2;
426const KAFKA_WRITER_MAX_QUEUE_SIZE: usize = 100000;
430
431struct KafkaPayloadWriter<'a> {
432 inner: &'a FutureProducer<RwProducerContext>,
433 add_future: DeliveryFutureManagerAddFuture<'a, KafkaSinkDeliveryFuture>,
434 config: &'a KafkaConfig,
435}
436
437mod opaque_type {
438 use super::*;
439 pub type KafkaSinkDeliveryFuture = impl TryFuture<Ok = (), Error = SinkError> + Unpin + 'static;
440
441 #[define_opaque(KafkaSinkDeliveryFuture)]
442 pub(super) fn map_delivery_future(future: DeliveryFuture) -> KafkaSinkDeliveryFuture {
443 future.map(KafkaPayloadWriter::<'static>::map_future_result)
444 }
445}
446pub use opaque_type::KafkaSinkDeliveryFuture;
447use opaque_type::map_delivery_future;
448
449pub struct KafkaSinkWriter {
450 formatter: SinkFormatterImpl,
451 inner: FutureProducer<RwProducerContext>,
452 config: KafkaConfig,
453}
454
455impl KafkaSinkWriter {
456 async fn new(config: KafkaConfig, formatter: SinkFormatterImpl) -> Result<Self> {
457 let inner: FutureProducer<RwProducerContext> = {
458 let mut c = ClientConfig::new();
459
460 config.connection.set_security_properties(&mut c);
462 config.set_client(&mut c);
463
464 c.set("bootstrap.servers", &config.connection.brokers);
466
467 let broker_rewrite_map = config.privatelink_common.broker_rewrite_map.clone();
469 let ctx_common = KafkaContextCommon::new(
470 broker_rewrite_map,
471 None,
472 None,
473 config.aws_auth_props.clone(),
474 config.connection.is_aws_msk_iam(),
475 )
476 .await?;
477 let producer_ctx = RwProducerContext::new(ctx_common);
478 if let Some(log_level) = read_kafka_log_level() {
481 c.set_log_level(log_level);
482 }
483 c.create_with_context(producer_ctx).await?
484 };
485
486 Ok(KafkaSinkWriter {
487 formatter,
488 inner,
489 config: config.clone(),
490 })
491 }
492}
493
494impl AsyncTruncateSinkWriter for KafkaSinkWriter {
495 type DeliveryFuture = KafkaSinkDeliveryFuture;
496
497 async fn write_chunk<'a>(
498 &'a mut self,
499 chunk: StreamChunk,
500 add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
501 ) -> Result<()> {
502 let mut payload_writer = KafkaPayloadWriter {
503 inner: &mut self.inner,
504 add_future,
505 config: &self.config,
506 };
507 dispatch_sink_formatter_impl!(&self.formatter, formatter, {
508 payload_writer.write_chunk(chunk, formatter).await
509 })
510 }
511}
512
513impl KafkaPayloadWriter<'_> {
514 async fn send_result<'a, K, P>(&'a mut self, mut record: FutureRecord<'a, K, P>) -> Result<()>
517 where
518 K: ToBytes + ?Sized,
519 P: ToBytes + ?Sized,
520 {
521 let mut success_flag = false;
522
523 let mut ret = Ok(());
524
525 for i in 0..self.config.max_retry_num {
526 match self.inner.send_result(record) {
527 Ok(delivery_future) => {
528 let awaited = self
532 .add_future
533 .add_future_may_await(map_delivery_future(delivery_future))
534 .await?;
535 if awaited {
536 static LOG_SUPPRESSOR: LazyLock<LogSuppressor> =
537 LazyLock::new(LogSuppressor::default);
538 if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
539 tracing::warn!(
540 suppressed_count,
541 future_count = self.add_future.future_count(),
542 max_future_count = self.add_future.max_future_count(),
543 "delivery future buffer is full, awaited an outstanding delivery before enqueuing"
544 );
545 }
546 }
547 success_flag = true;
548 break;
549 }
550 Err((e, rec)) => match e {
551 KafkaError::MessageProduction(RDKafkaErrorCode::QueueFull) => {
552 record = rec;
553 static LOG_SUPPRESSOR: LazyLock<LogSuppressor> =
554 LazyLock::new(LogSuppressor::default);
555 if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
556 tracing::warn!(
557 suppressed_count,
558 future_count = self.add_future.future_count(),
559 retry = i,
560 "producer queue is full, awaiting an outstanding delivery before retry"
561 );
562 }
563 self.add_future.await_one_delivery().await?;
564 continue;
565 }
566 _ => {
567 tracing::warn!(
568 error = %e.as_report(),
569 "producing message (key {:?}) to topic {} failed",
570 rec.key.map(|k| k.to_bytes()),
571 rec.topic,
572 );
573 return Err(e.into());
574 }
575 },
576 }
577 }
578
579 if !success_flag {
580 ret = Err(KafkaError::MessageProduction(RDKafkaErrorCode::QueueFull).into());
583 }
584
585 ret
586 }
587
588 async fn write_inner(
589 &mut self,
590 event_key_object: Option<Vec<u8>>,
591 event_object: Option<Vec<u8>>,
592 ) -> Result<()> {
593 let topic = self.config.common.topic.clone();
594 let mut record = FutureRecord::<[u8], [u8]>::to(topic.as_str());
595 if let Some(key_str) = &event_key_object {
596 record = record.key(key_str);
597 }
598 if let Some(payload) = &event_object {
599 record = record.payload(payload);
600 }
601 self.send_result(record).await?;
604 Ok(())
605 }
606
607 fn map_future_result(delivery_future_result: <DeliveryFuture as Future>::Output) -> Result<()> {
608 match delivery_future_result {
609 Ok(Ok(_)) => Ok(()),
613 Ok(Err((k_err, _msg))) => Err(k_err.into()),
619 Err(_) => Err(KafkaError::Canceled.into()),
623 }
624 }
625}
626
627impl FormattedSink for KafkaPayloadWriter<'_> {
628 type K = Vec<u8>;
629 type V = Vec<u8>;
630
631 async fn write_one(&mut self, k: Option<Self::K>, v: Option<Self::V>) -> Result<()> {
632 self.write_inner(k, v).await
633 }
634}
635
636#[cfg(test)]
637mod test {
638 use maplit::btreemap;
639 use risingwave_common::catalog::Field;
640 use risingwave_common::types::DataType;
641
642 use super::*;
643 use crate::sink::encoder::{
644 DateHandlingMode, JsonEncoder, JsonbHandlingMode, TimeHandlingMode, TimestampHandlingMode,
645 TimestamptzHandlingMode,
646 };
647 use crate::sink::formatter::AppendOnlyFormatter;
648
649 #[test]
650 fn parse_rdkafka_props() {
651 let props: BTreeMap<String, String> = btreemap! {
652 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
655 "topic".to_owned() => "test".to_owned(),
656 "properties.message.max.bytes".to_owned() => "12345".to_owned(),
659 "properties.receive.message.max.bytes".to_owned() => "54321".to_owned(),
660 "properties.reconnect.backoff.ms".to_owned() => "1000".to_owned(),
661 "properties.reconnect.backoff.max.ms".to_owned() => "30000".to_owned(),
662 "properties.socket.connection.setup.timeout.ms".to_owned() => "60000".to_owned(),
663 "properties.retry.backoff.ms".to_owned() => "200".to_owned(),
664 "properties.retry.backoff.max.ms".to_owned() => "2000".to_owned(),
665 "properties.queue.buffering.max.messages".to_owned() => "114514".to_owned(),
667 "properties.queue.buffering.max.kbytes".to_owned() => "114514".to_owned(),
668 "properties.queue.buffering.max.ms".to_owned() => "114.514".to_owned(),
669 "properties.enable.idempotence".to_owned() => "false".to_owned(),
670 "properties.message.send.max.retries".to_owned() => "114514".to_owned(),
671 "properties.batch.num.messages".to_owned() => "114514".to_owned(),
672 "properties.batch.size".to_owned() => "114514".to_owned(),
673 "properties.compression.codec".to_owned() => "zstd".to_owned(),
674 "properties.message.timeout.ms".to_owned() => "114514".to_owned(),
675 "properties.max.in.flight.requests.per.connection".to_owned() => "114514".to_owned(),
676 "properties.request.required.acks".to_owned() => "-1".to_owned(),
677 };
678 let c = KafkaConfig::from_btreemap(props).unwrap();
679 assert_eq!(c.rdkafka_properties_common.message_max_bytes, Some(12345));
680 assert_eq!(
681 c.rdkafka_properties_common.receive_message_max_bytes,
682 Some(54321)
683 );
684 assert_eq!(c.rdkafka_properties_common.reconnect_backoff_ms, Some(1000));
685 assert_eq!(
686 c.rdkafka_properties_common.reconnect_backoff_max_ms,
687 Some(30000)
688 );
689 assert_eq!(
690 c.rdkafka_properties_common
691 .socket_connection_setup_timeout_ms,
692 Some(60000)
693 );
694 assert_eq!(c.rdkafka_properties_common.retry_backoff_ms, Some(200));
695 assert_eq!(c.rdkafka_properties_common.retry_backoff_max_ms, Some(2000));
696 let mut client_config = rdkafka::ClientConfig::new();
697 c.set_client(&mut client_config);
698 assert_eq!(client_config.get("reconnect.backoff.ms"), Some("1000"));
699 assert_eq!(client_config.get("reconnect.backoff.max.ms"), Some("30000"));
700 assert_eq!(
701 client_config.get("socket.connection.setup.timeout.ms"),
702 Some("60000")
703 );
704 assert_eq!(client_config.get("retry.backoff.ms"), Some("200"));
705 assert_eq!(client_config.get("retry.backoff.max.ms"), Some("2000"));
706 assert_eq!(
707 c.rdkafka_properties_producer.queue_buffering_max_ms,
708 Some(114.514f64)
709 );
710 assert_eq!(
711 c.rdkafka_properties_producer.compression_codec,
712 Some(CompressionCodec::Zstd)
713 );
714 assert_eq!(
715 c.rdkafka_properties_producer.message_timeout_ms,
716 Some(114514)
717 );
718 assert_eq!(
719 c.rdkafka_properties_producer
720 .max_in_flight_requests_per_connection,
721 114514
722 );
723 assert_eq!(
724 c.rdkafka_properties_producer.request_required_acks,
725 Some(-1)
726 );
727
728 let props: BTreeMap<String, String> = btreemap! {
729 "connector".to_owned() => "kafka".to_owned(),
731 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
732 "topic".to_owned() => "test".to_owned(),
733 "type".to_owned() => "append-only".to_owned(),
734
735 "properties.enable.idempotence".to_owned() => "True".to_owned(), };
737 assert!(KafkaConfig::from_btreemap(props).is_err());
738
739 let props: BTreeMap<String, String> = btreemap! {
740 "connector".to_owned() => "kafka".to_owned(),
742 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
743 "topic".to_owned() => "test".to_owned(),
744 "type".to_owned() => "append-only".to_owned(),
745 "properties.queue.buffering.max.kbytes".to_owned() => "-114514".to_owned(), };
747 assert!(KafkaConfig::from_btreemap(props).is_err());
748
749 let props: BTreeMap<String, String> = btreemap! {
750 "connector".to_owned() => "kafka".to_owned(),
752 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
753 "topic".to_owned() => "test".to_owned(),
754 "type".to_owned() => "append-only".to_owned(),
755 "properties.compression.codec".to_owned() => "notvalid".to_owned(), };
757 assert!(KafkaConfig::from_btreemap(props).is_err());
758 }
759
760 #[test]
761 fn parse_kafka_config() {
762 let properties: BTreeMap<String, String> = btreemap! {
763 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
765 "topic".to_owned() => "test".to_owned(),
766 "properties.security.protocol".to_owned() => "SASL".to_owned(),
769 "properties.sasl.mechanism".to_owned() => "SASL".to_owned(),
770 "properties.sasl.username".to_owned() => "test".to_owned(),
771 "properties.sasl.password".to_owned() => "test".to_owned(),
772 "properties.retry.max".to_owned() => "20".to_owned(),
773 "properties.retry.interval".to_owned() => "500ms".to_owned(),
774 "broker.rewrite.endpoints".to_owned() => "{\"broker1\": \"10.0.0.1:8001\"}".to_owned(),
776 };
777 let config = KafkaConfig::from_btreemap(properties).unwrap();
778 assert_eq!(config.connection.brokers, "localhost:9092");
779 assert_eq!(config.common.topic, "test");
780 assert_eq!(config.max_retry_num, 20);
781 assert_eq!(config.retry_interval, Duration::from_millis(500));
782
783 let btreemap: BTreeMap<String, String> = btreemap! {
785 "broker1".to_owned() => "10.0.0.1:8001".to_owned()
786 };
787 assert_eq!(config.privatelink_common.broker_rewrite_map, Some(btreemap));
788
789 let properties: BTreeMap<String, String> = btreemap! {
791 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
793 "topic".to_owned() => "test".to_owned(),
794 };
796 let config = KafkaConfig::from_btreemap(properties).unwrap();
797 assert_eq!(config.max_retry_num, 3);
798 assert_eq!(config.retry_interval, Duration::from_millis(100));
799
800 let properties: BTreeMap<String, String> = btreemap! {
802 "connector".to_owned() => "kafka".to_owned(),
803 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
804 "topic".to_owned() => "test".to_owned(),
805 "type".to_owned() => "upsert".to_owned(),
806 "properties.retry.max".to_owned() => "-20".to_owned(), };
808 assert!(KafkaConfig::from_btreemap(properties).is_err());
809
810 let properties: BTreeMap<String, String> = btreemap! {
812 "connector".to_owned() => "kafka".to_owned(),
813 "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
814 "topic".to_owned() => "test".to_owned(),
815 "type".to_owned() => "upsert".to_owned(),
816 "properties.retry.interval".to_owned() => "500miiinutes".to_owned(), };
818 assert!(KafkaConfig::from_btreemap(properties).is_err());
819 }
820
821 #[ignore]
824 #[tokio::test]
825 async fn test_kafka_producer() -> Result<()> {
826 let properties = btreemap! {
828 "connector".to_owned() => "kafka".to_owned(),
829 "properties.bootstrap.server".to_owned() => "localhost:29092".to_owned(),
830 "type".to_owned() => "append-only".to_owned(),
831 "topic".to_owned() => "test_topic".to_owned(),
832 "properties.compression.codec".to_owned() => "zstd".to_owned(),
833 };
834
835 let schema = Schema::new(vec![
837 Field {
838 data_type: DataType::Int32,
839 name: "id".into(),
840 },
841 Field {
842 data_type: DataType::Varchar,
843 name: "v2".into(),
844 },
845 ]);
846
847 let kafka_config = KafkaConfig::from_btreemap(properties)?;
848
849 let sink = KafkaSinkWriter::new(
851 kafka_config.clone(),
852 SinkFormatterImpl::AppendOnlyJson(AppendOnlyFormatter::new(
853 None,
855 JsonEncoder::new(
856 schema,
857 None,
858 DateHandlingMode::FromCe,
859 TimestampHandlingMode::Milli,
860 TimestamptzHandlingMode::UtcString,
861 TimeHandlingMode::Milli,
862 JsonbHandlingMode::String,
863 ),
864 )),
865 )
866 .await
867 .unwrap();
868
869 use crate::sink::log_store::DeliveryFutureManager;
870
871 let mut future_manager = DeliveryFutureManager::new(usize::MAX);
872
873 for i in 0..10 {
874 println!("epoch: {}", i);
875 for j in 0..100 {
876 let mut writer = KafkaPayloadWriter {
877 inner: &sink.inner,
878 add_future: future_manager.start_write_chunk(i, j),
879 config: &sink.config,
880 };
881 match writer
882 .send_result(
883 FutureRecord::to(kafka_config.common.topic.as_str())
884 .payload(format!("value-{}", j).as_bytes())
885 .key(format!("dummy_key_for_epoch-{}", i).as_bytes()),
886 )
887 .await
888 {
889 Ok(_) => {}
890 Err(e) => {
891 println!("{:?}", e);
892 break;
893 }
894 };
895 }
896 }
897
898 Ok(())
899 }
900}