1use 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
46const 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 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 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 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 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 let pulsar = self
291 .config
292 .common
293 .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 .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 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 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 let iter = {
441 dispatch_sink_formatter_str_key_impl!(
442 &self.formatter,
443 formatter,
444 {
445 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 auto_enums::auto_enum(Iterator)
456 )
457 };
458
459 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}