1use std::collections::HashMap;
16use std::sync::{Arc, LazyLock, Weak};
17use std::time::Duration;
18
19use anyhow::{Context, anyhow};
20use async_trait::async_trait;
21use moka::future::Cache as MokaCache;
22use moka::ops::compute::Op;
23use rdkafka::admin::{AdminClient, AdminOptions};
24use rdkafka::consumer::BaseConsumer;
25#[cfg(not(madsim))]
26use rdkafka::consumer::Consumer;
27use rdkafka::error::{KafkaError, KafkaResult};
28use rdkafka::types::RDKafkaErrorCode;
29use rdkafka::{ClientConfig, Offset, TopicPartitionList};
30use risingwave_common::bail;
31use risingwave_common::id::FragmentId;
32use risingwave_common::metrics::LabelGuardedIntGauge;
33use thiserror_ext::AsReport;
34
35use crate::connector_common::read_kafka_log_level;
36use crate::error::{ConnectorError, ConnectorResult};
37use crate::source::SourceEnumeratorContextRef;
38use crate::source::base::SplitEnumerator;
39use crate::source::kafka::split::KafkaSplit;
40use crate::source::kafka::{
41 KAFKA_ISOLATION_LEVEL, KafkaConnectionProps, KafkaContextCommon, KafkaProperties,
42 RwConsumerContext,
43};
44
45type KafkaConsumer = BaseConsumer<RwConsumerContext>;
46type KafkaAdmin = AdminClient<RwConsumerContext>;
47
48pub static SHARED_KAFKA_CONSUMER: LazyLock<MokaCache<KafkaConnectionProps, Weak<KafkaConsumer>>> =
50 LazyLock::new(|| moka::future::Cache::builder().build());
51pub static SHARED_KAFKA_ADMIN: LazyLock<MokaCache<KafkaConnectionProps, Arc<KafkaAdmin>>> =
53 LazyLock::new(|| {
54 moka::future::Cache::builder()
55 .time_to_idle(Duration::from_secs(5 * 60))
56 .build()
57 });
58
59#[derive(Debug, Copy, Clone, Eq, PartialEq)]
60pub enum KafkaEnumeratorOffset {
61 Earliest,
62 Latest,
63 Timestamp(i64),
64 None,
65}
66
67pub struct KafkaSplitEnumerator {
68 context: SourceEnumeratorContextRef,
69 broker_address: String,
70 topic: String,
71 client: Arc<KafkaConsumer>,
72 start_offset: KafkaEnumeratorOffset,
73
74 stop_offset: KafkaEnumeratorOffset,
76
77 sync_call_timeout: Duration,
78 high_watermark_metrics: HashMap<i32, LabelGuardedIntGauge>,
79
80 properties: KafkaProperties,
81 config: rdkafka::ClientConfig,
82}
83
84impl KafkaSplitEnumerator {
85 fn report_consumer_group_delete_failure(&self, group_id: &str) {
86 let source_id = self.context.info.source_id.to_string();
87 self.context
88 .metrics
89 .kafka_consumer_group_delete_failure_count
90 .with_label_values(&[&source_id, group_id])
91 .inc();
92 }
93
94 async fn drop_consumer_groups(&self, fragment_ids: Vec<FragmentId>) -> ConnectorResult<()> {
95 let admin = Box::pin(SHARED_KAFKA_ADMIN.try_get_with_by_ref(
96 &self.properties.connection,
97 async {
98 tracing::info!("build new kafka admin for {}", self.broker_address);
99 Ok(Arc::new(
100 build_kafka_admin(&self.config, &self.properties).await?,
101 ))
102 },
103 ))
104 .await?;
105
106 let group_ids = fragment_ids
107 .iter()
108 .map(|fragment_id| self.properties.group_id(*fragment_id))
109 .collect::<Vec<_>>();
110 let group_id_refs = group_ids
111 .iter()
112 .map(|group_id| group_id.as_str())
113 .collect::<Vec<_>>();
114
115 let res = match admin
116 .delete_groups(&group_id_refs, &AdminOptions::default())
117 .await
118 {
119 Ok(res) => res,
120 Err(err) => {
121 for group_id in &group_ids {
122 self.report_consumer_group_delete_failure(group_id);
123 }
124 tracing::warn!(
125 error = %err.as_report(),
126 topic = self.topic,
127 ?group_ids,
128 "failed to delete Kafka consumer groups"
129 );
130 return Err(err.into());
131 }
132 };
133
134 let mut failure_count = 0;
135 for result in &res {
136 if let Err((group_id, error_code)) = result {
137 failure_count += 1;
138 self.report_consumer_group_delete_failure(group_id);
139 tracing::warn!(
140 topic = self.topic,
141 group_id,
142 error = %error_code.as_report(),
143 "failed to delete Kafka consumer group"
144 );
145 }
146 }
147 tracing::debug!(
148 topic = self.topic,
149 ?fragment_ids,
150 ?res,
151 failure_count,
152 "delete groups result"
153 );
154 Ok(())
155 }
156}
157
158#[async_trait]
159impl SplitEnumerator for KafkaSplitEnumerator {
160 type Properties = KafkaProperties;
161 type Split = KafkaSplit;
162
163 async fn new(
164 properties: KafkaProperties,
165 context: SourceEnumeratorContextRef,
166 ) -> ConnectorResult<KafkaSplitEnumerator> {
167 let mut config = rdkafka::ClientConfig::new();
168 let common_props = &properties.common;
169
170 let broker_address = properties.connection.brokers.clone();
171 let topic = common_props.topic.clone();
172 config.set("bootstrap.servers", &broker_address);
173 config.set("isolation.level", KAFKA_ISOLATION_LEVEL);
174 if let Some(log_level) = read_kafka_log_level() {
175 config.set_log_level(log_level);
176 }
177 properties.connection.set_security_properties(&mut config);
178 properties.set_client(&mut config);
179 config.set("statistics.interval.ms", "0");
183 let mut scan_start_offset = match properties
184 .scan_startup_mode
185 .as_ref()
186 .map(|s| s.to_lowercase())
187 .as_deref()
188 {
189 Some("earliest") => KafkaEnumeratorOffset::Earliest,
190 Some("latest") => KafkaEnumeratorOffset::Latest,
191 None => KafkaEnumeratorOffset::Earliest,
192 _ => bail!(
193 "properties `scan_startup_mode` only supports earliest and latest or leaving it empty"
194 ),
195 };
196
197 if let Some(s) = &properties.time_offset {
198 let time_offset = s.parse::<i64>().map_err(|e| anyhow!(e))?;
199 scan_start_offset = KafkaEnumeratorOffset::Timestamp(time_offset)
200 }
201
202 let mut client: Option<Arc<KafkaConsumer>> = None;
203 SHARED_KAFKA_CONSUMER
204 .entry_by_ref(&properties.connection)
205 .and_try_compute_with::<_, _, ConnectorError>(|maybe_entry| async {
206 if let Some(entry) = maybe_entry {
207 let entry_value = entry.into_value();
208 if let Some(client_) = entry_value.upgrade() {
209 tracing::info!("reuse existing kafka client for {}", broker_address);
211 client = Some(client_);
212 return Ok(Op::Nop);
213 }
214 }
215 tracing::info!("build new kafka client for {}", broker_address);
216 client = Some(build_kafka_client(&config, &properties).await?);
217 Ok(Op::Put(Arc::downgrade(client.as_ref().unwrap())))
218 })
219 .await?;
220
221 Ok(Self {
222 context,
223 broker_address,
224 topic,
225 client: client.unwrap(),
226 start_offset: scan_start_offset,
227 stop_offset: KafkaEnumeratorOffset::None,
228 sync_call_timeout: properties.common.sync_call_timeout,
229 high_watermark_metrics: HashMap::new(),
230 properties,
231 config,
232 })
233 }
234
235 async fn list_splits(&mut self) -> ConnectorResult<Vec<KafkaSplit>> {
236 if let Some(Err(poll_err)) = {
245 #[cfg(not(madsim))]
246 {
247 self.client.poll(Duration::ZERO)
248 }
249 #[cfg(madsim)]
250 {
251 self.client.poll(Duration::ZERO).await
252 }
253 } && !is_expected_no_group_poll_error(&poll_err)
254 {
255 tracing::warn!(
256 error = %poll_err.as_report(),
257 topic = self.topic,
258 broker_address = self.broker_address,
259 "failed to poll kafka client");
260 }
261
262 let topic_partitions = self.fetch_topic_partition().await.with_context(|| {
263 format!(
264 "failed to fetch metadata from kafka ({})",
265 self.broker_address
266 )
267 })?;
268
269 let watermarks = self.get_watermarks(topic_partitions.as_ref()).await?;
270 let mut start_offsets = self
271 .fetch_start_offset(topic_partitions.as_ref(), &watermarks)
272 .await?;
273
274 let mut stop_offsets = self
275 .fetch_stop_offset(topic_partitions.as_ref(), &watermarks)
276 .await?;
277
278 let ret: Vec<_> = topic_partitions
279 .into_iter()
280 .map(|partition| KafkaSplit {
281 topic: self.topic.clone(),
282 partition,
283 start_offset: start_offsets.remove(&partition).unwrap(),
284 stop_offset: stop_offsets.remove(&partition).unwrap(),
285 })
286 .collect();
287
288 Ok(ret)
289 }
290
291 async fn on_drop_fragments(&mut self, fragment_ids: Vec<FragmentId>) -> ConnectorResult<()> {
292 self.drop_consumer_groups(fragment_ids).await
293 }
294
295 async fn on_finish_backfill(&mut self, fragment_ids: Vec<FragmentId>) -> ConnectorResult<()> {
296 self.drop_consumer_groups(fragment_ids).await
297 }
298}
299
300fn is_expected_no_group_poll_error(error: &KafkaError) -> bool {
301 matches!(
302 error,
303 KafkaError::MessageConsumption(RDKafkaErrorCode::UnknownGroup)
304 )
305}
306
307async fn build_kafka_client(
308 config: &ClientConfig,
309 properties: &KafkaProperties,
310) -> ConnectorResult<Arc<KafkaConsumer>> {
311 let ctx_common = KafkaContextCommon::new(
312 properties.privatelink_common.broker_rewrite_map.clone(),
313 None,
314 None,
315 properties.aws_auth_props.clone(),
316 properties.connection.is_aws_msk_iam(),
317 )
318 .await?;
319 let client_ctx = RwConsumerContext::new(ctx_common);
320 let client: KafkaConsumer = config.create_with_context(client_ctx).await?;
321
322 if properties.connection.is_aws_msk_iam() {
328 #[cfg(not(madsim))]
329 client.poll(Duration::from_secs(10)); #[cfg(madsim)]
331 client.poll(Duration::from_secs(10)).await;
332 }
333 Ok(Arc::new(client))
334}
335async fn build_kafka_admin(
336 config: &ClientConfig,
337 properties: &KafkaProperties,
338) -> ConnectorResult<KafkaAdmin> {
339 let ctx_common = KafkaContextCommon::new(
340 properties.privatelink_common.broker_rewrite_map.clone(),
341 None,
342 None,
343 properties.aws_auth_props.clone(),
344 properties.connection.is_aws_msk_iam(),
345 )
346 .await?;
347 let client_ctx = RwConsumerContext::new(ctx_common);
348 let client: KafkaAdmin = config.create_with_context(client_ctx).await?;
349 Ok(client)
351}
352
353impl KafkaSplitEnumerator {
354 async fn get_watermarks(
355 &mut self,
356 partitions: &[i32],
357 ) -> KafkaResult<HashMap<i32, (i64, i64)>> {
358 let mut map = HashMap::new();
359 for partition in partitions {
360 let (low, high) = self
361 .client
362 .fetch_watermarks(self.topic.as_str(), *partition, self.sync_call_timeout)
363 .await?;
364 self.report_high_watermark(*partition, high);
365 map.insert(*partition, (low, high));
366 }
367 tracing::debug!("fetch kafka watermarks: {map:?}");
368 Ok(map)
369 }
370
371 pub async fn list_splits_batch(
372 &mut self,
373 expect_start_timestamp_millis: Option<i64>,
374 expect_stop_timestamp_millis: Option<i64>,
375 ) -> ConnectorResult<Vec<KafkaSplit>> {
376 let topic_partitions = self.fetch_topic_partition().await.with_context(|| {
377 format!(
378 "failed to fetch metadata from kafka ({})",
379 self.broker_address
380 )
381 })?;
382
383 let mut watermarks = self.get_watermarks(topic_partitions.as_ref()).await?;
386
387 let mut expect_start_offset = if let Some(ts) = expect_start_timestamp_millis {
392 Some(
393 self.fetch_offset_for_time(topic_partitions.as_ref(), ts, &watermarks)
394 .await?,
395 )
396 } else {
397 None
398 };
399
400 let mut expect_stop_offset = if let Some(ts) = expect_stop_timestamp_millis {
401 Some(
402 self.fetch_offset_for_time(topic_partitions.as_ref(), ts, &watermarks)
403 .await?,
404 )
405 } else {
406 None
407 };
408
409 Ok(topic_partitions
410 .iter()
411 .map(|partition| {
412 let (low, high) = watermarks.remove(partition).unwrap();
413 let start_offset = {
414 let earliest_offset = low - 1;
415 let start = expect_start_offset
416 .as_mut()
417 .map(|m| m.remove(partition).flatten().unwrap_or(earliest_offset))
418 .unwrap_or(earliest_offset);
419 i64::max(start, earliest_offset)
420 };
421 let stop_offset = {
422 let stop = expect_stop_offset
423 .as_mut()
424 .map(|m| m.remove(partition).unwrap_or(Some(high)))
425 .unwrap_or(Some(high))
426 .unwrap_or(high);
427 i64::min(stop, high)
428 };
429
430 if start_offset > stop_offset {
431 tracing::warn!(
432 "Skipping topic {} partition {}: requested start offset {} is greater than stop offset {}",
433 self.topic,
434 partition,
435 start_offset,
436 stop_offset
437 );
438 }
439 KafkaSplit {
440 topic: self.topic.clone(),
441 partition: *partition,
442 start_offset: Some(start_offset),
443 stop_offset: Some(stop_offset),
444 }
445 })
446 .collect::<Vec<KafkaSplit>>())
447 }
448
449 async fn fetch_stop_offset(
450 &self,
451 partitions: &[i32],
452 watermarks: &HashMap<i32, (i64, i64)>,
453 ) -> KafkaResult<HashMap<i32, Option<i64>>> {
454 match self.stop_offset {
455 KafkaEnumeratorOffset::Earliest => unreachable!(),
456 KafkaEnumeratorOffset::Latest => {
457 let mut map = HashMap::new();
458 for partition in partitions {
459 let (_, high_watermark) = watermarks.get(partition).unwrap();
460 map.insert(*partition, Some(*high_watermark));
461 }
462 Ok(map)
463 }
464 KafkaEnumeratorOffset::Timestamp(time) => {
465 self.fetch_offset_for_time(partitions, time, watermarks)
466 .await
467 }
468 KafkaEnumeratorOffset::None => partitions
469 .iter()
470 .map(|partition| Ok((*partition, None)))
471 .collect(),
472 }
473 }
474
475 async fn fetch_start_offset(
476 &self,
477 partitions: &[i32],
478 watermarks: &HashMap<i32, (i64, i64)>,
479 ) -> KafkaResult<HashMap<i32, Option<i64>>> {
480 match self.start_offset {
481 KafkaEnumeratorOffset::Earliest | KafkaEnumeratorOffset::Latest => {
482 let mut map = HashMap::new();
483 for partition in partitions {
484 let (low_watermark, high_watermark) = watermarks.get(partition).unwrap();
485 let offset = match self.start_offset {
486 KafkaEnumeratorOffset::Earliest => low_watermark - 1,
487 KafkaEnumeratorOffset::Latest => high_watermark - 1,
488 _ => unreachable!(),
489 };
490 map.insert(*partition, Some(offset));
491 }
492 Ok(map)
493 }
494 KafkaEnumeratorOffset::Timestamp(time) => {
495 self.fetch_offset_for_time(partitions, time, watermarks)
496 .await
497 }
498 KafkaEnumeratorOffset::None => partitions
499 .iter()
500 .map(|partition| Ok((*partition, None)))
501 .collect(),
502 }
503 }
504
505 async fn fetch_offset_for_time(
506 &self,
507 partitions: &[i32],
508 time: i64,
509 watermarks: &HashMap<i32, (i64, i64)>,
510 ) -> KafkaResult<HashMap<i32, Option<i64>>> {
511 let mut tpl = TopicPartitionList::new();
512
513 for partition in partitions {
514 tpl.add_partition_offset(self.topic.as_str(), *partition, Offset::Offset(time))?;
515 }
516
517 let offsets = self
518 .client
519 .offsets_for_times(tpl, self.sync_call_timeout)
520 .await?;
521
522 let mut result = HashMap::with_capacity(partitions.len());
523
524 for elem in offsets.elements_for_topic(self.topic.as_str()) {
525 match elem.offset() {
526 Offset::Offset(offset) => {
527 result.insert(elem.partition(), Some(offset - 1));
529 }
530 Offset::End => {
531 let (_, high_watermark) = watermarks.get(&elem.partition()).unwrap();
532 tracing::info!(
533 source_id = %self.context.info.source_id,
534 "no message found before timestamp {} (ms) for partition {}, start from latest",
535 time,
536 elem.partition()
537 );
538 result.insert(elem.partition(), Some(high_watermark - 1)); }
540 Offset::Invalid => {
541 tracing::info!(
546 source_id = %self.context.info.source_id,
547 "got invalid offset for partition {} at timestamp {}, align to latest",
548 elem.partition(),
549 time
550 );
551 let (_, high_watermark) = watermarks.get(&elem.partition()).unwrap();
552 result.insert(elem.partition(), Some(high_watermark - 1)); }
554 Offset::Beginning => {
555 let (low, _) = watermarks.get(&elem.partition()).unwrap();
556 tracing::info!(
557 source_id = %self.context.info.source_id,
558 "all message in partition {} is after timestamp {} (ms), start from earliest",
559 elem.partition(),
560 time,
561 );
562 result.insert(elem.partition(), Some(low - 1)); }
564 err_offset @ Offset::Stored | err_offset @ Offset::OffsetTail(_) => {
565 tracing::error!(
566 source_id = %self.context.info.source_id,
567 "got invalid offset for partition {}: {err_offset:?}",
568 elem.partition(),
569 err_offset = err_offset,
570 );
571 return Err(KafkaError::OffsetFetch(RDKafkaErrorCode::NoOffset));
572 }
573 }
574 }
575
576 Ok(result)
577 }
578
579 #[inline]
580 fn report_high_watermark(&mut self, partition: i32, offset: i64) {
581 let high_watermark_metrics =
582 self.high_watermark_metrics
583 .entry(partition)
584 .or_insert_with(|| {
585 self.context
586 .metrics
587 .high_watermark
588 .with_guarded_label_values(&[
589 &self.context.info.source_id.to_string(),
590 &partition.to_string(),
591 ])
592 });
593 high_watermark_metrics.set(offset);
594 }
595
596 pub async fn check_reachability(&self) -> ConnectorResult<()> {
597 let _ = self
598 .client
599 .fetch_metadata(Some(self.topic.as_str()), self.sync_call_timeout)
600 .await?;
601 Ok(())
602 }
603
604 async fn fetch_topic_partition(&self) -> ConnectorResult<Vec<i32>> {
605 let metadata = self
607 .client
608 .fetch_metadata(Some(self.topic.as_str()), self.sync_call_timeout)
609 .await?;
610
611 let topic_meta = match metadata.topics() {
612 [meta] => meta,
613 _ => bail!("topic {} not found", self.topic),
614 };
615
616 if topic_meta.partitions().is_empty() {
617 bail!("topic {} not found", self.topic);
618 }
619
620 Ok(topic_meta
621 .partitions()
622 .iter()
623 .map(|partition| partition.id())
624 .collect())
625 }
626}