Skip to main content

risingwave_connector/sink/
kafka.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// See <https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md>
81/// for the detailed meaning of these librdkafka producer properties
82#[serde_as]
83#[derive(Debug, Clone, Deserialize, WithOptions)]
84pub struct RdKafkaPropertiesProducer {
85    /// Allow automatic topic creation on the broker when subscribing to or assigning non-existent topics.
86    #[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    /// Maximum number of messages allowed on the producer queue. This queue is shared by all
92    /// topics and partitions. A value of 0 disables this limit.
93    #[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    /// Maximum total message size sum allowed on the producer queue. This queue is shared by all
99    /// topics and partitions. This property has higher priority than queue.buffering.max.messages.
100    #[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    /// Delay in milliseconds to wait for messages in the producer queue to accumulate before
106    /// constructing message batches (`MessageSets`) to transmit to brokers. A higher value allows
107    /// larger and more effective (less overhead, improved compression) batches of messages to
108    /// accumulate at the expense of increased message delivery latency.
109    #[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    /// When set to true, the producer will ensure that messages are successfully produced exactly
115    /// once and in the original produce order. The following configuration properties are adjusted
116    /// automatically (if not modified by the user) when idempotence is enabled:
117    /// max.in.flight.requests.per.connection=5 (must be less than or equal to 5),
118    /// `retries=INT32_MAX` (must be greater than 0), acks=all, queuing.strategy=fifo. Producer
119    /// will fail if user-supplied configuration is incompatible.
120    #[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    /// How many times to retry sending a failing Message.
126    #[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    /// Maximum number of messages batched in one `MessageSet`
132    #[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    /// Maximum size (in bytes) of all messages batched in one `MessageSet`, including protocol
138    /// framing overhead. This limit is applied after the first message has been added to the
139    /// batch, regardless of the first message's size, this is to ensure that messages that exceed
140    /// batch.size are produced.
141    #[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    /// Compression codec to use for compressing message sets.
147    #[serde(rename = "properties.compression.codec")]
148    #[serde_as(as = "Option<DisplayFromStr>")]
149    compression_codec: Option<CompressionCodec>,
150
151    /// Produce message timeout.
152    /// This value is used to limits the time a produced message waits for
153    /// successful delivery (including retries).
154    #[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    /// The maximum number of unacknowledged requests the client will send on a single connection before blocking.
160    #[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    /// We have parsed the primary key for an upsert kafka sink into a `usize` vector representing
240    /// the indices of the pk columns in the frontend, so we simply store the primary key here
241    /// as a string.
242    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        // For non-append-only Kafka sink, the primary key must be defined.
378        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        // Check for formatter constructor error, before it is too late for error reporting.
385        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        // Try Kafka connection.
396        // There is no such interface for kafka producer to validate a connection
397        // use enumerator to validate broker reachability and existence of topic
398        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
421/// When the `DeliveryFuture` the current `future_delivery_buffer`
422/// is buffering is greater than `queue_buffering_max_messages` * `KAFKA_WRITER_MAX_QUEUE_SIZE_RATIO`,
423/// then enforcing commit once
424const KAFKA_WRITER_MAX_QUEUE_SIZE_RATIO: f32 = 1.2;
425/// The default queue size used to enforce a commit in kafka producer if `queue.buffering.max.messages` is not specified.
426/// This default value is determined based on the librdkafka default. See the following doc for more details:
427/// <https://github.com/confluentinc/librdkafka/blob/1cb80090dfc75f5a36eae3f4f8844b14885c045e/CONFIGURATION.md>
428const 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            // KafkaConfig configuration
460            config.connection.set_security_properties(&mut c);
461            config.set_client(&mut c);
462
463            // ClientConfig configuration
464            c.set("bootstrap.servers", &config.connection.brokers);
465
466            // Create the producer context, will be used to create the producer
467            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            // Generate the producer
478
479            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    /// The actual `send_result` function, will be called when the `KafkaSinkWriter` needs to sink
514    /// messages
515    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                // The enqueue buffer is full, `send_result` will immediately return
543                // We can retry for another round after sleeping for sometime
544                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            // In this case, after trying `max_retry_num`
570            // The enqueue buffer is still full
571            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        // Send the data but not wait it to finish sinking
591        // Will join all `DeliveryFuture` during commit
592        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            // Successfully sent the record
599            // Will return the partition and offset of the message (i32, i64)
600            // Note that `Vec<()>` won't cause memory allocation
601            Ok(Ok(_)) => Ok(()),
602            // If the message failed to be delivered. (i.e., flush)
603            // The error & the copy of the original message will be returned
604            // i.e., (KafkaError, OwnedMessage)
605            // We will just stop the loop, and return the error
606            // The sink executor will back to the latest checkpoint
607            Ok(Err((k_err, _msg))) => Err(k_err.into()),
608            // This represents the producer is dropped
609            // before the delivery status is received
610            // Return `KafkaError::Canceled`
611            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            // basic
642            // "connector".to_string() => "kafka".to_string(),
643            "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
644            "topic".to_owned() => "test".to_owned(),
645            // "type".to_string() => "append-only".to_string(),
646            // RdKafkaPropertiesCommon
647            "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            // RdKafkaPropertiesProducer
655            "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            // basic
719            "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(), // can only be 'true' or 'false'
725        };
726        assert!(KafkaConfig::from_btreemap(props).is_err());
727
728        let props: BTreeMap<String, String> = btreemap! {
729            // basic
730            "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(), // usize cannot be negative
735        };
736        assert!(KafkaConfig::from_btreemap(props).is_err());
737
738        let props: BTreeMap<String, String> = btreemap! {
739            // basic
740            "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(), // has to be a valid CompressionCodec
745        };
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            // "connector".to_string() => "kafka".to_string(),
753            "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
754            "topic".to_owned() => "test".to_owned(),
755            // "type".to_string() => "append-only".to_string(),
756            // "force_append_only".to_string() => "true".to_string(),
757            "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            // PrivateLink
764            "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        // PrivateLink fields
773        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        // Optional fields eliminated.
779        let properties: BTreeMap<String, String> = btreemap! {
780            // "connector".to_string() => "kafka".to_string(),
781            "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
782            "topic".to_owned() => "test".to_owned(),
783            // "type".to_string() => "upsert".to_string(),
784        };
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        // Invalid u32 input.
790        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(),  // error!
796        };
797        assert!(KafkaConfig::from_btreemap(properties).is_err());
798
799        // Invalid duration input.
800        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(),  // invalid duration
806        };
807        assert!(KafkaConfig::from_btreemap(properties).is_err());
808    }
809
810    /// Note: Please enable the kafka by running `./risedev configure` before commenting #[ignore]
811    /// to run the test, also remember to modify `risedev.yml`
812    #[ignore]
813    #[tokio::test]
814    async fn test_kafka_producer() -> Result<()> {
815        // Create a dummy kafka properties
816        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        // Create a table with two columns (| id : INT32 | v2 : VARCHAR |) here
825        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        // Create the actual sink writer to Kafka
839        let sink = KafkaSinkWriter::new(
840            kafka_config.clone(),
841            SinkFormatterImpl::AppendOnlyJson(AppendOnlyFormatter::new(
842                // We do not specify primary key for this schema
843                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}