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