Skip to main content

risingwave_connector/sink/
pulsar.rs

1// Copyright 2023 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::time::Duration;
18
19use anyhow::anyhow;
20use futures::{FutureExt, TryFuture, TryFutureExt};
21use pulsar::producer::{Message, SendFuture};
22use pulsar::routing_policy::RoutingPolicy;
23use pulsar::{Producer, ProducerOptions, Pulsar, TokioExecutor};
24use risingwave_common::array::StreamChunk;
25use risingwave_common::catalog::Schema;
26use serde::Deserialize;
27use serde_with::{DisplayFromStr, serde_as};
28use strum_macros::{Display, EnumString};
29use with_options::WithOptions;
30
31use super::catalog::{SinkFormat, SinkFormatDesc};
32use super::{Sink, SinkError, SinkParam, SinkWriterParam};
33use crate::connector_common::{AwsAuthProps, PulsarCommon, PulsarOauthCommon};
34use crate::enforce_secret::EnforceSecret;
35use crate::sink::Result;
36use crate::sink::encoder::SerTo;
37use crate::sink::formatter::{SinkFormatter, SinkFormatterImpl};
38use crate::sink::log_store::DeliveryFutureManagerAddFuture;
39use crate::sink::writer::{
40    AsyncTruncateLogSinkerOf, AsyncTruncateSinkWriter, AsyncTruncateSinkWriterExt, FormattedSink,
41};
42use crate::{deserialize_duration_from_string, dispatch_sink_formatter_str_key_impl};
43
44pub const PULSAR_SINK: &str = "pulsar";
45
46/// The delivery buffer queue size
47/// When the `SendFuture` the current `send_future_buffer`
48/// is buffering is greater than this size, then enforcing commit once
49const PULSAR_SEND_FUTURE_BUFFER_MAX_SIZE: usize = 65536;
50
51const fn _default_max_retries() -> u32 {
52    3
53}
54
55const fn _default_retry_backoff() -> Duration {
56    Duration::from_millis(100)
57}
58
59const fn _default_batch_size() -> u32 {
60    10000
61}
62
63const fn _default_batch_byte_size() -> usize {
64    1 << 20
65}
66
67fn pulsar_to_sink_err(e: pulsar::Error) -> SinkError {
68    SinkError::Pulsar(anyhow!(e))
69}
70
71async fn build_pulsar_producer(
72    pulsar: &Pulsar<TokioExecutor>,
73    config: &PulsarConfig,
74) -> Result<Producer<TokioExecutor>> {
75    // Reduce async state machine size (see `clippy::large_futures`).
76    Box::pin(
77        pulsar
78            .producer()
79            .with_options(ProducerOptions {
80                batch_size: Some(config.producer_properties.batch_size),
81                batch_byte_size: Some(config.producer_properties.batch_byte_size),
82                routing_policy: pulsar_producer_routing_policy(
83                    config.producer_properties.routing_mode,
84                ),
85                ..Default::default()
86            })
87            .with_topic(&config.common.topic)
88            .build()
89            .map_err(pulsar_to_sink_err),
90    )
91    .await
92}
93
94fn pulsar_producer_routing_policy(
95    routing_mode: Option<PulsarRoutingMode>,
96) -> Option<RoutingPolicy> {
97    routing_mode.map(Into::into)
98}
99
100#[derive(Debug, Copy, Clone, Display, Deserialize, EnumString)]
101#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
102pub enum PulsarRoutingMode {
103    #[strum(
104        serialize = "round_robin",
105        serialize = "roundrobin",
106        serialize = "roundrobinpartition",
107        serialize = "round_robin_partition",
108        serialize = "round-robin",
109        serialize = "round-robin-partition",
110        serialize = "RoundRobinPartition"
111    )]
112    RoundRobin,
113    #[strum(
114        serialize = "single",
115        serialize = "singlepartition",
116        serialize = "single_partition",
117        serialize = "single-partition",
118        serialize = "SinglePartition"
119    )]
120    Single,
121}
122
123impl From<PulsarRoutingMode> for RoutingPolicy {
124    fn from(mode: PulsarRoutingMode) -> Self {
125        match mode {
126            PulsarRoutingMode::RoundRobin => RoutingPolicy::RoundRobin,
127            PulsarRoutingMode::Single => RoutingPolicy::Single,
128        }
129    }
130}
131
132#[serde_as]
133#[derive(Debug, Clone, Deserialize, WithOptions)]
134pub struct PulsarPropertiesProducer {
135    #[serde(rename = "properties.batch.size", default = "_default_batch_size")]
136    #[serde_as(as = "DisplayFromStr")]
137    batch_size: u32,
138
139    #[serde(
140        rename = "properties.batch.byte.size",
141        default = "_default_batch_byte_size"
142    )]
143    #[serde_as(as = "DisplayFromStr")]
144    batch_byte_size: usize,
145
146    #[serde(
147        rename = "properties.routing.mode",
148        alias = "properties.routing_mode",
149        alias = "routing_mode",
150        alias = "pulsar.routing_mode",
151        alias = "pulsar.routing.mode",
152        alias = "pulsar.properties.routing.mode",
153        alias = "pulsar.properties.routing_mode"
154    )]
155    #[serde_as(as = "Option<DisplayFromStr>")]
156    #[with_option(allow_alter_on_fly)]
157    routing_mode: Option<PulsarRoutingMode>,
158}
159
160#[serde_as]
161#[derive(Debug, Clone, Deserialize, WithOptions)]
162pub struct PulsarConfig {
163    #[serde(rename = "properties.retry.max", default = "_default_max_retries")]
164    #[serde_as(as = "DisplayFromStr")]
165    pub max_retry_num: u32,
166
167    #[serde(
168        rename = "properties.retry.interval",
169        default = "_default_retry_backoff",
170        deserialize_with = "deserialize_duration_from_string"
171    )]
172    pub retry_interval: Duration,
173
174    #[serde(flatten)]
175    pub common: PulsarCommon,
176
177    #[serde(flatten)]
178    pub oauth: Option<PulsarOauthCommon>,
179
180    #[serde(flatten)]
181    pub aws_auth_props: AwsAuthProps,
182
183    #[serde(flatten)]
184    pub producer_properties: PulsarPropertiesProducer,
185
186    #[serde(flatten)]
187    pub unknown_fields: std::collections::HashMap<String, String>,
188}
189
190crate::impl_sink_unknown_fields!(PulsarConfig);
191
192impl EnforceSecret for PulsarConfig {
193    fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
194        PulsarCommon::enforce_one(prop)?;
195        AwsAuthProps::enforce_one(prop)?;
196        Ok(())
197    }
198}
199impl PulsarConfig {
200    pub fn from_btreemap(values: BTreeMap<String, String>) -> Result<Self> {
201        let config = serde_json::from_value::<PulsarConfig>(serde_json::to_value(values).unwrap())
202            .map_err(|e| SinkError::Config(anyhow!(e)))?;
203
204        Ok(config)
205    }
206}
207
208#[derive(Debug)]
209pub struct PulsarSink {
210    pub config: PulsarConfig,
211    schema: Schema,
212    downstream_pk: Vec<usize>,
213    format_desc: SinkFormatDesc,
214    db_name: String,
215    sink_from_name: String,
216}
217
218impl EnforceSecret for PulsarSink {
219    fn enforce_secret<'a>(
220        prop_iter: impl Iterator<Item = &'a str>,
221    ) -> crate::error::ConnectorResult<()> {
222        for prop in prop_iter {
223            PulsarConfig::enforce_one(prop)?;
224        }
225        Ok(())
226    }
227}
228
229impl TryFrom<SinkParam> for PulsarSink {
230    type Error = SinkError;
231
232    fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
233        let schema = param.schema();
234        let downstream_pk = param.downstream_pk_or_empty();
235        let config = PulsarConfig::from_btreemap(param.properties)?;
236        Ok(Self {
237            config,
238            schema,
239            downstream_pk,
240            format_desc: param
241                .format_desc
242                .ok_or_else(|| SinkError::Config(anyhow!("missing FORMAT ... ENCODE ...")))?,
243            db_name: param.db_name,
244            sink_from_name: param.sink_from_name,
245        })
246    }
247}
248
249impl Sink for PulsarSink {
250    type LogSinker = AsyncTruncateLogSinkerOf<PulsarSinkWriter>;
251
252    const SINK_NAME: &'static str = PULSAR_SINK;
253
254    crate::impl_validate_sink_unknown_fields!();
255
256    async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
257        // Reduce async state machine size (see `clippy::large_futures`).
258        let writer = Box::pin(PulsarSinkWriter::new(
259            self.config.clone(),
260            self.schema.clone(),
261            self.downstream_pk.clone(),
262            &self.format_desc,
263            self.db_name.clone(),
264            self.sink_from_name.clone(),
265        ))
266        .await?;
267        Ok(writer.into_log_sinker(PULSAR_SEND_FUTURE_BUFFER_MAX_SIZE))
268    }
269
270    async fn validate(&self) -> Result<()> {
271        // For non-append-only Pulsar sink, the primary key must be defined.
272        if self.format_desc.format != SinkFormat::AppendOnly && self.downstream_pk.is_empty() {
273            return Err(SinkError::Config(anyhow!(
274                "primary key not defined for {:?} pulsar sink (please define in `primary_key` field)",
275                self.format_desc.format
276            )));
277        }
278        // Check for formatter constructor error, before it is too late for error reporting.
279        SinkFormatterImpl::new(
280            &self.format_desc,
281            self.schema.clone(),
282            self.downstream_pk.clone(),
283            self.db_name.clone(),
284            self.sink_from_name.clone(),
285            &self.config.common.topic,
286        )
287        .await?;
288
289        // Validate pulsar connection.
290        let pulsar = self
291            .config
292            .common
293            // Source-side Pulsar client retry overrides are intentionally not applied to sinks.
294            .build_client(&self.config.oauth, &self.config.aws_auth_props, None)
295            .await?;
296        build_pulsar_producer(&pulsar, &self.config).await?;
297
298        Ok(())
299    }
300}
301
302pub struct PulsarSinkWriter {
303    formatter: SinkFormatterImpl,
304    #[expect(dead_code)]
305    pulsar: Pulsar<TokioExecutor>,
306    producer: Producer<TokioExecutor>,
307    config: PulsarConfig,
308}
309
310struct PulsarPayloadWriter<'w> {
311    producer: &'w mut Producer<TokioExecutor>,
312    config: &'w PulsarConfig,
313    add_future: DeliveryFutureManagerAddFuture<'w, PulsarDeliveryFuture>,
314}
315
316mod opaque_type {
317    use super::*;
318    pub type PulsarDeliveryFuture = impl TryFuture<Ok = (), Error = SinkError> + Unpin + 'static;
319
320    #[define_opaque(PulsarDeliveryFuture)]
321    pub(super) fn may_delivery_future(future: SendFuture) -> PulsarDeliveryFuture {
322        future.map(|result| {
323            result
324                .map(|_| ())
325                .map_err(|e: pulsar::Error| SinkError::Pulsar(anyhow!(e)))
326        })
327    }
328}
329pub use opaque_type::PulsarDeliveryFuture;
330use opaque_type::may_delivery_future;
331
332impl PulsarSinkWriter {
333    pub async fn new(
334        config: PulsarConfig,
335        schema: Schema,
336        downstream_pk: Vec<usize>,
337        format_desc: &SinkFormatDesc,
338        db_name: String,
339        sink_from_name: String,
340    ) -> Result<Self> {
341        let formatter = SinkFormatterImpl::new(
342            format_desc,
343            schema,
344            downstream_pk,
345            db_name,
346            sink_from_name,
347            &config.common.topic,
348        )
349        .await?;
350        let pulsar = config
351            .common
352            // Source-side Pulsar client retry overrides are intentionally not applied to sinks.
353            .build_client(&config.oauth, &config.aws_auth_props, None)
354            .await?;
355        let producer = build_pulsar_producer(&pulsar, &config).await?;
356        Ok(Self {
357            formatter,
358            pulsar,
359            producer,
360            config,
361        })
362    }
363}
364
365impl PulsarPayloadWriter<'_> {
366    async fn send_message(&mut self, message: Message) -> Result<()> {
367        let mut success_flag = false;
368        let mut connection_err = None;
369
370        for retry_num in 0..self.config.max_retry_num {
371            if retry_num > 0 {
372                tracing::warn!("Failed to send message, at retry no. {retry_num}");
373            }
374            match Box::pin(self.producer.send_non_blocking(message.clone())).await {
375                // If the message is sent successfully,
376                // a SendFuture holding the message receipt
377                // or error after sending is returned
378                Ok(send_future) => {
379                    self.add_future
380                        .add_future_may_await(may_delivery_future(send_future))
381                        .await?;
382                    success_flag = true;
383                    break;
384                }
385                // error upon sending
386                Err(e) => match e {
387                    pulsar::Error::Connection(_)
388                    | pulsar::Error::Producer(_)
389                    | pulsar::Error::Consumer(_) => {
390                        connection_err = Some(e);
391                        tokio::time::sleep(self.config.retry_interval).await;
392                        continue;
393                    }
394                    _ => return Err(SinkError::Pulsar(anyhow!(e))),
395                },
396            }
397        }
398
399        if !success_flag {
400            Err(SinkError::Pulsar(anyhow!(connection_err.unwrap())))
401        } else {
402            Ok(())
403        }
404    }
405
406    async fn write_inner(
407        &mut self,
408        event_key_object: Option<String>,
409        event_object: Option<Vec<u8>>,
410    ) -> Result<()> {
411        let message = Message {
412            partition_key: event_key_object,
413            payload: event_object.unwrap_or_default(),
414            ..Default::default()
415        };
416
417        self.send_message(message).await?;
418        Ok(())
419    }
420}
421
422impl FormattedSink for PulsarPayloadWriter<'_> {
423    type K = String;
424    type V = Vec<u8>;
425
426    async fn write_one(&mut self, k: Option<Self::K>, v: Option<Self::V>) -> Result<()> {
427        self.write_inner(k, v).await
428    }
429}
430
431impl AsyncTruncateSinkWriter for PulsarSinkWriter {
432    type DeliveryFuture = PulsarDeliveryFuture;
433
434    async fn write_chunk<'a>(
435        &'a mut self,
436        chunk: StreamChunk,
437        add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
438    ) -> Result<()> {
439        // Structured to avoid `clippy::large_stack_frames` and `large_futures`
440        let iter = {
441            dispatch_sink_formatter_str_key_impl!(
442                &self.formatter,
443                formatter,
444                {
445                    // Convert items to owned, concrete types before any `.await`,
446                    // so the future doesn't capture formatter/iterator generics.
447                    formatter.format_chunk(&chunk).map(|r| {
448                        let (key, value) = r?;
449                        let key: Option<String> = key.map(SerTo::ser_to).transpose()?;
450                        let value: Option<Vec<u8>> = value.map(SerTo::ser_to).transpose()?;
451                        Ok((key, value)) as Result<_>
452                    })
453                },
454                // Produce a single iterator type for all formatter variants.
455                auto_enums::auto_enum(Iterator)
456            )
457        };
458
459        // Only concrete state is held across `.await`, keeping the future small.
460        let mut payload_writer = PulsarPayloadWriter {
461            producer: &mut self.producer,
462            add_future,
463            config: &self.config,
464        };
465
466        for r in iter {
467            let (key, value): (Option<String>, Option<Vec<u8>>) = r?;
468            payload_writer.write_inner(key, value).await?;
469        }
470
471        Ok(())
472    }
473
474    async fn barrier(&mut self, is_checkpoint: bool) -> Result<()> {
475        if is_checkpoint {
476            self.producer
477                .send_batch()
478                .map_err(pulsar_to_sink_err)
479                .await?;
480        }
481
482        Ok(())
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use std::collections::BTreeMap;
489
490    use super::*;
491
492    fn base_properties() -> BTreeMap<String, String> {
493        BTreeMap::from([
494            (
495                "service.url".to_owned(),
496                "pulsar://localhost:6650".to_owned(),
497            ),
498            ("topic".to_owned(), "test-topic".to_owned()),
499        ])
500    }
501
502    fn parse_config_with(
503        extra: impl IntoIterator<Item = (&'static str, &'static str)>,
504    ) -> PulsarConfig {
505        let mut props = base_properties();
506        props.extend(
507            extra
508                .into_iter()
509                .map(|(key, value)| (key.to_owned(), value.to_owned())),
510        );
511        PulsarConfig::from_btreemap(props).unwrap()
512    }
513
514    #[test]
515    fn test_pulsar_routing_mode_default_is_unset() {
516        let config = parse_config_with([]);
517
518        assert!(config.producer_properties.routing_mode.is_none());
519    }
520
521    #[test]
522    fn test_pulsar_producer_routing_policy_preserves_unset_default() {
523        let config = parse_config_with([]);
524
525        assert!(pulsar_producer_routing_policy(config.producer_properties.routing_mode).is_none());
526    }
527
528    #[test]
529    fn test_pulsar_producer_routing_policy_preserves_configured_mode() {
530        let config = parse_config_with([("properties.routing.mode", "SinglePartition")]);
531
532        assert!(matches!(
533            pulsar_producer_routing_policy(config.producer_properties.routing_mode),
534            Some(RoutingPolicy::Single)
535        ));
536    }
537
538    #[test]
539    fn test_parse_pulsar_routing_mode() {
540        let config = parse_config_with([("properties.routing.mode", "round_robin")]);
541        assert!(matches!(
542            config.producer_properties.routing_mode,
543            Some(PulsarRoutingMode::RoundRobin)
544        ));
545
546        let config = parse_config_with([("properties.routing.mode", "SinglePartition")]);
547        assert!(matches!(
548            config.producer_properties.routing_mode,
549            Some(PulsarRoutingMode::Single)
550        ));
551    }
552
553    #[test]
554    fn test_parse_pulsar_routing_mode_alias() {
555        let config = parse_config_with([("routing_mode", "single")]);
556
557        assert!(matches!(
558            config.producer_properties.routing_mode,
559            Some(PulsarRoutingMode::Single)
560        ));
561    }
562}