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, 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/// See <https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md>
82/// for the detailed meaning of these librdkafka producer properties
83#[serde_as]
84#[derive(Debug, Clone, Deserialize, WithOptions)]
85pub struct RdKafkaPropertiesProducer {
86    /// Allow automatic topic creation on the broker when subscribing to or assigning non-existent topics.
87    #[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    /// Maximum number of messages allowed on the producer queue. This queue is shared by all
93    /// topics and partitions. A value of 0 disables this limit.
94    #[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    /// Maximum total message size sum allowed on the producer queue. This queue is shared by all
100    /// topics and partitions. This property has higher priority than queue.buffering.max.messages.
101    #[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    /// Delay in milliseconds to wait for messages in the producer queue to accumulate before
107    /// constructing message batches (`MessageSets`) to transmit to brokers. A higher value allows
108    /// larger and more effective (less overhead, improved compression) batches of messages to
109    /// accumulate at the expense of increased message delivery latency.
110    #[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    /// When set to true, the producer will ensure that messages are successfully produced exactly
116    /// once and in the original produce order. The following configuration properties are adjusted
117    /// automatically (if not modified by the user) when idempotence is enabled:
118    /// max.in.flight.requests.per.connection=5 (must be less than or equal to 5),
119    /// `retries=INT32_MAX` (must be greater than 0), acks=all, queuing.strategy=fifo. Producer
120    /// will fail if user-supplied configuration is incompatible.
121    #[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    /// How many times to retry sending a failing Message.
127    #[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    /// Maximum number of messages batched in one `MessageSet`
133    #[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    /// Maximum size (in bytes) of all messages batched in one `MessageSet`, including protocol
139    /// framing overhead. This limit is applied after the first message has been added to the
140    /// batch, regardless of the first message's size, this is to ensure that messages that exceed
141    /// batch.size are produced.
142    #[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    /// Compression codec to use for compressing message sets.
148    #[serde(rename = "properties.compression.codec")]
149    #[serde_as(as = "Option<DisplayFromStr>")]
150    compression_codec: Option<CompressionCodec>,
151
152    /// Produce message timeout.
153    /// This value is used to limits the time a produced message waits for
154    /// successful delivery (including retries).
155    #[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    /// The maximum number of unacknowledged requests the client will send on a single connection before blocking.
161    #[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    /// We have parsed the primary key for an upsert kafka sink into a `usize` vector representing
241    /// the indices of the pk columns in the frontend, so we simply store the primary key here
242    /// as a string.
243    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        // For non-append-only Kafka sink, the primary key must be defined.
379        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        // Check for formatter constructor error, before it is too late for error reporting.
386        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        // Try Kafka connection.
397        // There is no such interface for kafka producer to validate a connection
398        // use enumerator to validate broker reachability and existence of topic
399        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
422/// When the `DeliveryFuture` the current `future_delivery_buffer`
423/// is buffering is greater than `queue_buffering_max_messages` * `KAFKA_WRITER_MAX_QUEUE_SIZE_RATIO`,
424/// then enforcing commit once
425const KAFKA_WRITER_MAX_QUEUE_SIZE_RATIO: f32 = 1.2;
426/// The default queue size used to enforce a commit in kafka producer if `queue.buffering.max.messages` is not specified.
427/// This default value is determined based on the librdkafka default. See the following doc for more details:
428/// <https://github.com/confluentinc/librdkafka/blob/1cb80090dfc75f5a36eae3f4f8844b14885c045e/CONFIGURATION.md>
429const 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            // KafkaConfig configuration
461            config.connection.set_security_properties(&mut c);
462            config.set_client(&mut c);
463
464            // ClientConfig configuration
465            c.set("bootstrap.servers", &config.connection.brokers);
466
467            // Create the producer context, will be used to create the producer
468            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            // Generate the producer
479
480            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    /// The actual `send_result` function, will be called when the `KafkaSinkWriter` needs to sink
515    /// messages
516    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                    // The buffer holds delivery futures that we have not polled yet, including
529                    // those of records already acked by the broker, so reaching the cap is
530                    // ordinary backpressure rather than an anomaly.
531                    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            // In this case, after trying `max_retry_num`
581            // The enqueue buffer is still full
582            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        // Send the data but not wait it to finish sinking
602        // Will join all `DeliveryFuture` during commit
603        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            // Successfully sent the record
610            // Will return the partition and offset of the message (i32, i64)
611            // Note that `Vec<()>` won't cause memory allocation
612            Ok(Ok(_)) => Ok(()),
613            // If the message failed to be delivered. (i.e., flush)
614            // The error & the copy of the original message will be returned
615            // i.e., (KafkaError, OwnedMessage)
616            // We will just stop the loop, and return the error
617            // The sink executor will back to the latest checkpoint
618            Ok(Err((k_err, _msg))) => Err(k_err.into()),
619            // This represents the producer is dropped
620            // before the delivery status is received
621            // Return `KafkaError::Canceled`
622            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            // basic
653            // "connector".to_string() => "kafka".to_string(),
654            "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
655            "topic".to_owned() => "test".to_owned(),
656            // "type".to_string() => "append-only".to_string(),
657            // RdKafkaPropertiesCommon
658            "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            // RdKafkaPropertiesProducer
666            "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            // 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
735            "properties.enable.idempotence".to_owned() => "True".to_owned(), // can only be 'true' or 'false'
736        };
737        assert!(KafkaConfig::from_btreemap(props).is_err());
738
739        let props: BTreeMap<String, String> = btreemap! {
740            // basic
741            "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(), // usize cannot be negative
746        };
747        assert!(KafkaConfig::from_btreemap(props).is_err());
748
749        let props: BTreeMap<String, String> = btreemap! {
750            // basic
751            "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(), // has to be a valid CompressionCodec
756        };
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            // "connector".to_string() => "kafka".to_string(),
764            "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
765            "topic".to_owned() => "test".to_owned(),
766            // "type".to_string() => "append-only".to_string(),
767            // "force_append_only".to_string() => "true".to_string(),
768            "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            // PrivateLink
775            "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        // PrivateLink fields
784        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        // Optional fields eliminated.
790        let properties: BTreeMap<String, String> = btreemap! {
791            // "connector".to_string() => "kafka".to_string(),
792            "properties.bootstrap.server".to_owned() => "localhost:9092".to_owned(),
793            "topic".to_owned() => "test".to_owned(),
794            // "type".to_string() => "upsert".to_string(),
795        };
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        // Invalid u32 input.
801        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(),  // error!
807        };
808        assert!(KafkaConfig::from_btreemap(properties).is_err());
809
810        // Invalid duration input.
811        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(),  // invalid duration
817        };
818        assert!(KafkaConfig::from_btreemap(properties).is_err());
819    }
820
821    /// Note: Please enable the kafka by running `./risedev configure` before commenting #[ignore]
822    /// to run the test, also remember to modify `risedev.yml`
823    #[ignore]
824    #[tokio::test]
825    async fn test_kafka_producer() -> Result<()> {
826        // Create a dummy kafka properties
827        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        // Create a table with two columns (| id : INT32 | v2 : VARCHAR |) here
836        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        // Create the actual sink writer to Kafka
850        let sink = KafkaSinkWriter::new(
851            kafka_config.clone(),
852            SinkFormatterImpl::AppendOnlyJson(AppendOnlyFormatter::new(
853                // We do not specify primary key for this schema
854                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}