1use std::collections::BTreeMap;
11use std::collections::btree_map::Entry;
12use std::ops::ControlFlow;
13use std::rc::Rc;
14use std::str::{self};
15use std::sync::Arc;
16use std::thread;
17use std::time::Duration;
18
19use anyhow::anyhow;
20use chrono::{DateTime, NaiveDateTime};
21use differential_dataflow::{AsCollection, Hashable};
22use futures::StreamExt;
23use itertools::Itertools;
24use maplit::btreemap;
25use mz_kafka_util::client::{
26 GetPartitionsError, MzClientContext, PartitionId, TunnelingClientContext, get_partitions,
27};
28use mz_ore::assert_none;
29use mz_ore::cast::CastFrom;
30use mz_ore::error::ErrorExt;
31use mz_ore::future::InTask;
32use mz_ore::iter::IteratorExt;
33use mz_repr::adt::timestamp::CheckedTimestamp;
34use mz_repr::{Datum, Diff, GlobalId, Row, adt::jsonb::Jsonb};
35use mz_ssh_util::tunnel::SshTunnelStatus;
36use mz_storage_types::connections::KafkaConnection;
37use mz_storage_types::errors::{
38 ContextCreationError, DataflowError, SourceError, SourceErrorDetails,
39};
40use mz_storage_types::sources::kafka::{
41 KafkaMetadataKind, KafkaSourceConnection, KafkaTimestamp, RangeBound,
42};
43use mz_storage_types::sources::{MzOffset, SourceExport, SourceExportDetails, SourceTimestamp};
44use mz_timely_util::antichain::AntichainExt;
45use mz_timely_util::builder_async::{
46 AsyncOutputHandle, Event, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
47};
48use mz_timely_util::containers::stack::FueledBuilder;
49use mz_timely_util::order::Partitioned;
50use rdkafka::consumer::base_consumer::PartitionQueue;
51use rdkafka::consumer::{BaseConsumer, Consumer, ConsumerContext};
52use rdkafka::error::KafkaError;
53use rdkafka::message::{BorrowedMessage, Headers};
54use rdkafka::statistics::Statistics;
55use rdkafka::topic_partition_list::Offset;
56use rdkafka::{ClientContext, Message, TopicPartitionList};
57use serde::{Deserialize, Serialize};
58use timely::PartialOrder;
59use timely::container::CapacityContainerBuilder;
60use timely::dataflow::channels::pact::Pipeline;
61use timely::dataflow::operators::Capability;
62use timely::dataflow::operators::core::Partition;
63use timely::dataflow::operators::vec::Broadcast;
64use timely::dataflow::{Scope, StreamVec};
65use timely::progress::Antichain;
66use timely::progress::Timestamp;
67use tokio::sync::{Notify, OnceCell, mpsc};
68use tracing::{error, info, trace};
69
70use crate::healthcheck::{HealthStatusMessage, HealthStatusUpdate, StatusNamespace};
71use crate::metrics::source::kafka::KafkaSourceMetrics;
72use crate::source::types::{
73 FuelSize, Probe, ResumeUppers, SignaledFuture, SourceRender, StackedCollection,
74};
75use crate::source::{RawSourceCreationConfig, SourceMessage, probe};
76
77#[derive(
78 Clone,
79 Debug,
80 Default,
81 PartialEq,
82 Eq,
83 PartialOrd,
84 Ord,
85 Serialize,
86 Deserialize
87)]
88struct HealthStatus {
89 kafka: Option<HealthStatusUpdate>,
90 ssh: Option<HealthStatusUpdate>,
91}
92
93impl HealthStatus {
94 fn kafka(update: HealthStatusUpdate) -> Self {
95 Self {
96 kafka: Some(update),
97 ssh: None,
98 }
99 }
100
101 fn ssh(update: HealthStatusUpdate) -> Self {
102 Self {
103 kafka: None,
104 ssh: Some(update),
105 }
106 }
107}
108
109pub struct KafkaSourceReader {
111 topic_name: String,
113 source_name: String,
115 id: GlobalId,
117 consumer: Rc<OnceCell<Arc<ReaderConsumer>>>,
119 partition_consumers: Vec<PartitionConsumer>,
121 worker_id: usize,
123 worker_count: usize,
125 last_offsets: BTreeMap<usize, BTreeMap<PartitionId, i64>>,
129 start_offsets: BTreeMap<PartitionId, u64>,
131 stats_rx: crossbeam_channel::Receiver<Jsonb>,
133 partition_metrics: KafkaSourceMetrics,
135 partition_capabilities: BTreeMap<PartitionId, PartitionCapability>,
137}
138
139type ReaderConsumer = BaseConsumer<TunnelingClientContext<GlueConsumerContext>>;
140
141struct PartitionCapability {
142 data: Capability<KafkaTimestamp>,
144}
145
146type PartitionWatermark = u64;
151
152pub struct KafkaResumeUpperProcessor {
155 config: RawSourceCreationConfig,
156 topic_name: String,
157 consumer: Rc<OnceCell<Arc<ReaderConsumer>>>,
159 committed_offsets: Vec<(PartitionId, MzOffset)>,
162}
163
164fn responsible_for_pid(config: &RawSourceCreationConfig, pid: i32) -> bool {
168 let pid = usize::try_from(pid).expect("positive pid");
169 ((config.responsible_worker(config.id) + pid) % config.worker_count) == config.worker_id
170}
171
172struct SourceOutputInfo {
173 id: GlobalId,
174 output_index: usize,
175 resume_upper: Antichain<KafkaTimestamp>,
176 metadata_columns: Vec<KafkaMetadataKind>,
177}
178
179impl SourceRender for KafkaSourceConnection {
180 type Time = KafkaTimestamp;
185
186 const STATUS_NAMESPACE: StatusNamespace = StatusNamespace::Kafka;
187
188 fn render<'scope>(
189 self,
190 scope: Scope<'scope, KafkaTimestamp>,
191 config: &RawSourceCreationConfig,
192 resume_uppers: impl futures::Stream<Item = ResumeUppers<KafkaTimestamp>> + 'static,
193 start_signal: impl std::future::Future<Output = ()> + 'static,
194 ) -> (
195 BTreeMap<
196 GlobalId,
197 StackedCollection<'scope, KafkaTimestamp, Result<SourceMessage, DataflowError>>,
198 >,
199 StreamVec<'scope, KafkaTimestamp, HealthStatusMessage>,
200 StreamVec<'scope, KafkaTimestamp, Probe<KafkaTimestamp>>,
201 Vec<PressOnDropButton>,
202 ) {
203 let (metadata, probes, metadata_token) =
204 render_metadata_fetcher(scope, self.clone(), config.clone());
205 let (data, health, reader_token) = render_reader(
206 scope,
207 self,
208 config.clone(),
209 resume_uppers,
210 metadata,
211 start_signal,
212 );
213
214 let partition_count = u64::cast_from(config.source_exports.len());
215 let data_streams: Vec<_> = data.inner.partition::<CapacityContainerBuilder<_>, _, _>(
216 partition_count,
217 |((output, data), time, diff)| {
218 let output = u64::cast_from(output);
219 (output, (data, time, diff))
220 },
221 );
222 let mut data_collections = BTreeMap::new();
223 for (id, data_stream) in config.source_exports.keys().zip_eq(data_streams) {
224 data_collections.insert(*id, data_stream.as_collection());
225 }
226
227 (
228 data_collections,
229 health,
230 probes,
231 vec![metadata_token, reader_token],
232 )
233 }
234}
235
236fn render_reader<'scope>(
241 scope: Scope<'scope, KafkaTimestamp>,
242 connection: KafkaSourceConnection,
243 config: RawSourceCreationConfig,
244 resume_uppers: impl futures::Stream<Item = ResumeUppers<KafkaTimestamp>> + 'static,
245 metadata_stream: StreamVec<'scope, KafkaTimestamp, (mz_repr::Timestamp, MetadataUpdate)>,
246 start_signal: impl std::future::Future<Output = ()> + 'static,
247) -> (
248 StackedCollection<'scope, KafkaTimestamp, (usize, Result<SourceMessage, DataflowError>)>,
249 StreamVec<'scope, KafkaTimestamp, HealthStatusMessage>,
250 PressOnDropButton,
251) {
252 let name = format!("KafkaReader({})", config.id);
253 let mut builder = AsyncOperatorBuilder::new(name, scope.clone());
254
255 let (data_output, stream) = builder.new_output::<FueledBuilder<_>>();
256 let (health_output, health_stream) = builder.new_output::<CapacityContainerBuilder<Vec<_>>>();
257
258 let mut metadata_input = builder.new_disconnected_input(metadata_stream.broadcast(), Pipeline);
259
260 let mut outputs = vec![];
261
262 let mut all_export_stats = vec![];
264 let mut snapshot_export_stats = vec![];
265 for (idx, (id, export)) in config.source_exports.iter().enumerate() {
266 let SourceExport {
267 details,
268 storage_metadata: _,
269 data_config: _,
270 } = export;
271 let resume_upper = Antichain::from_iter(
272 config
273 .source_resume_uppers
274 .get(id)
275 .expect("all source exports must be present in source resume uppers")
276 .iter()
277 .map(Partitioned::<RangeBound<PartitionId>, MzOffset>::decode_row),
278 );
279
280 let metadata_columns = match details {
281 SourceExportDetails::Kafka(details) => details
282 .metadata_columns
283 .iter()
284 .map(|(_name, kind)| kind.clone())
285 .collect::<Vec<_>>(),
286 _ => panic!("unexpected source export details: {:?}", details),
287 };
288
289 let statistics = config
290 .statistics
291 .get(id)
292 .expect("statistics have been initialized")
293 .clone();
294 if resume_upper.as_ref() == &[Partitioned::minimum()] {
296 snapshot_export_stats.push(statistics.clone());
297 }
298 all_export_stats.push(statistics);
299
300 let output = SourceOutputInfo {
301 id: *id,
302 resume_upper,
303 output_index: idx,
304 metadata_columns,
305 };
306 outputs.push(output);
307 }
308
309 let busy_signal = Arc::clone(&config.busy_signal);
310 let button = builder.build(move |caps| {
311 SignaledFuture::new(busy_signal, async move {
312 let [mut data_cap, health_cap] = caps.try_into().unwrap();
313
314 let client_id = connection.client_id(
315 config.config.config_set(),
316 &config.config.connection_context,
317 config.id,
318 );
319 let group_id = connection.group_id(&config.config.connection_context, config.id);
320 let KafkaSourceConnection {
321 connection,
322 topic,
323 topic_metadata_refresh_interval,
324 start_offsets,
325 metadata_columns: _,
326 connection_id: _, group_id_prefix: _, } = connection;
331
332 info!(
333 source_id = config.id.to_string(),
334 worker_id = config.worker_id,
335 num_workers = config.worker_count,
336 "instantiating Kafka source reader at offsets {start_offsets:?}"
337 );
338
339 let (stats_tx, stats_rx) = crossbeam_channel::unbounded();
340 let notificator = Arc::new(Notify::new());
341
342 let consumer_setup = ReaderConsumerSetup {
343 config: config.clone(),
344 connection,
345 topic: topic.clone(),
346 topic_metadata_refresh_interval,
347 group_id,
348 client_id,
349 notificator: Arc::clone(¬ificator),
350 stats_tx,
351 outputs: outputs.iter().map(|o| (o.id, o.output_index)).collect(),
352 data_output: data_output.clone(),
353 health_output: health_output.clone(),
354 health_cap: health_cap.clone(),
355 };
356 let consumer: Rc<OnceCell<Arc<ReaderConsumer>>> = Rc::default();
357
358 let mut start_offsets: BTreeMap<_, u64> = start_offsets
360 .clone()
361 .into_iter()
362 .filter(|(pid, _offset)| responsible_for_pid(&config, *pid))
363 .map(|(pid, offset)| (pid, u64::try_from(offset).expect("start offsets must be non-negative and fit into u64")))
364 .collect();
365
366 let mut partition_capabilities = BTreeMap::new();
367 let mut max_pid = None;
368 let resume_upper = Antichain::from_iter(
369 outputs
370 .iter()
371 .map(|output| output.resume_upper.clone())
372 .flatten(),
373 );
374
375 tracing::info!(
376 source_id = config.id.to_string(),
377 worker_id = config.worker_id,
378 num_workers = config.worker_count,
379 "Kafka source reader starting rehydration with resume upper: {resume_upper:?} and start offsets: {start_offsets:?}"
380 );
381
382 for ts in resume_upper.elements() {
383 if let Some(pid) = ts.interval().singleton() {
384 let pid = pid.unwrap_exact();
385 max_pid = std::cmp::max(max_pid, Some(*pid));
386
387 if responsible_for_pid(&config, *pid) {
388 let restored_offset = ts.timestamp().offset;
389 if let Some(start_offset) = start_offsets.get_mut(pid) {
390 *start_offset = std::cmp::max(restored_offset, *start_offset);
391 } else {
392 start_offsets.insert(*pid, restored_offset);
393 }
394
395 let part_ts = Partitioned::new_singleton(
396 RangeBound::exact(*pid),
397 ts.timestamp().clone(),
398 );
399 let part_cap = PartitionCapability {
400 data: data_cap.delayed(&part_ts),
401 };
402 partition_capabilities.insert(*pid, part_cap);
403 }
404 }
405 }
406 let lower = max_pid
407 .map(RangeBound::after)
408 .unwrap_or(RangeBound::NegInfinity);
409 let future_ts =
410 Partitioned::new_range(lower, RangeBound::PosInfinity, MzOffset::from(0));
411 data_cap.downgrade(&future_ts);
412
413 if !start_offsets.is_empty()
417 && consumer_setup
418 .get_or_create(&consumer, &mut start_offsets, &data_cap)
419 .await
420 .is_break()
421 {
422 return;
423 }
424
425 start_signal.await;
429 info!(
430 source_id = config.id.to_string(),
431 worker_id = config.worker_id,
432 num_workers = config.worker_count,
433 "kafka worker noticed rehydration is finished, starting partition queues..."
434 );
435
436 let partition_ids = start_offsets.keys().copied().collect();
437 let offset_commit_metrics = config.metrics.get_offset_commit_metrics(config.id);
438
439 let mut reader = KafkaSourceReader {
440 topic_name: topic.clone(),
441 source_name: config.name.clone(),
442 id: config.id,
443 partition_consumers: Vec::new(),
444 consumer: Rc::clone(&consumer),
445 worker_id: config.worker_id,
446 worker_count: config.worker_count,
447 last_offsets: outputs
448 .iter()
449 .map(|output| (output.output_index, BTreeMap::new()))
450 .collect(),
451 start_offsets,
452 stats_rx,
453 partition_metrics: config.metrics.get_kafka_source_metrics(
454 partition_ids,
455 topic.clone(),
456 config.id,
457 ),
458 partition_capabilities,
459 };
460
461 let mut offset_committer = KafkaResumeUpperProcessor {
462 config: config.clone(),
463 topic_name: topic.clone(),
464 consumer: Rc::clone(&consumer),
465 committed_offsets: Vec::new(),
466 };
467
468 if !snapshot_export_stats.is_empty() {
470 let seed = ResumeUppers {
471 source: Some(resume_upper.clone()),
472 exports: outputs
473 .iter()
474 .map(|output| (output.id, output.resume_upper.clone()))
475 .collect(),
476 };
477 if let Err(e) = offset_committer.process_frontier(&seed).await {
478 offset_commit_metrics.offset_commit_failures.inc();
479 tracing::warn!(
480 %e,
481 "timely-{worker_id} source({source_id}) failed to commit offsets: resume_upper={upper}",
482 worker_id = config.worker_id,
483 source_id = config.id,
484 upper = resume_upper.pretty()
485 );
486 }
487 for statistics in config.statistics.values() {
491 statistics.set_snapshot_records_known(0);
492 statistics.set_snapshot_records_staged(0);
493 }
494 }
495
496 let refresh_interval = mz_storage_types::dyncfgs::KAFKA_OFFSET_COMMIT_REFRESH_INTERVAL
497 .get(config.config.config_set());
498 let resume_uppers_process_loop = async move {
499 tokio::pin!(resume_uppers);
500 let mut refresh =
503 tokio::time::interval(refresh_interval.max(Duration::from_millis(1)));
504 refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
506 let report = |e: anyhow::Error, what: &dyn std::fmt::Display| {
507 offset_commit_metrics.offset_commit_failures.inc();
508 tracing::warn!(
509 %e,
510 "timely-{worker_id} source({source_id}) failed to commit offsets: {what}",
511 worker_id = config.worker_id,
512 source_id = config.id,
513 );
514 };
515 loop {
516 tokio::select! {
517 uppers = resume_uppers.next() => match uppers {
518 Some(uppers) => {
519 if let Err(e) = offset_committer.process_frontier(&uppers).await {
520 report(e, &uppers);
521 }
522 }
523 None => break,
524 },
525 _ = refresh.tick(), if !refresh_interval.is_zero() => {
526 if let Err(e) = offset_committer.refresh().await {
527 report(e, &"refresh");
528 }
529 }
530 }
531 }
532 std::future::pending::<()>().await;
537 };
538 tokio::pin!(resume_uppers_process_loop);
539
540 let mut metadata_update: Option<MetadataUpdate> = None;
541 let mut snapshot_total = None;
542
543 let max_wait_time =
544 mz_storage_types::dyncfgs::KAFKA_POLL_MAX_WAIT.get(config.config.config_set());
545 loop {
546 tokio::select! {
549 _ = tokio::time::timeout(max_wait_time, notificator.notified()) => {},
552
553 _ = metadata_input.ready() => {
554 let mut updates = Vec::new();
556 while let Some(event) = metadata_input.next_sync() {
557 if let Event::Data(_, mut data) = event {
558 updates.append(&mut data);
559 }
560 }
561 metadata_update = updates
562 .into_iter()
563 .max_by_key(|(ts, _)| *ts)
564 .map(|(_, update)| update);
565 }
566
567 _ = resume_uppers_process_loop.as_mut() => {},
571 }
572
573 match metadata_update.take() {
574 Some(MetadataUpdate::Partitions(partitions)) => {
575 let max_pid = partitions.keys().last().cloned();
576 let lower = max_pid
577 .map(RangeBound::after)
578 .unwrap_or(RangeBound::NegInfinity);
579 let future_ts = Partitioned::new_range(
580 lower,
581 RangeBound::PosInfinity,
582 MzOffset::from(0),
583 );
584
585 let mut offset_known = 0;
586 for (&pid, &high_watermark) in &partitions {
587 if responsible_for_pid(&config, pid) {
588 offset_known += high_watermark;
589 let kafka_consumer = match consumer_setup
590 .get_or_create(&consumer, &mut reader.start_offsets, &data_cap)
591 .await
592 {
593 ControlFlow::Continue(kafka_consumer) => {
594 Arc::clone(kafka_consumer)
595 }
596 ControlFlow::Break(()) => return,
597 };
598 reader.ensure_partition(&kafka_consumer, pid);
599 if let Entry::Vacant(entry) =
600 reader.partition_capabilities.entry(pid)
601 {
602 let start_offset =
603 reader.start_offsets.get(&pid).copied().unwrap_or(0);
604 let part_since_ts = Partitioned::new_singleton(
605 RangeBound::exact(pid),
606 MzOffset::from(start_offset),
607 );
608
609 entry.insert(PartitionCapability {
610 data: data_cap.delayed(&part_since_ts),
611 });
612 }
613 }
614 }
615
616 if !snapshot_export_stats.is_empty() && snapshot_total.is_none() {
619 snapshot_total = Some(offset_known);
623 }
624
625 for output in &outputs {
629 for namespace in [StatusNamespace::Kafka, StatusNamespace::Ssh] {
630 health_output.give(
631 &health_cap,
632 HealthStatusMessage {
633 id: Some(output.id),
634 namespace,
635 update: HealthStatusUpdate::running(),
636 },
637 );
638 }
639 }
640 for namespace in [StatusNamespace::Kafka, StatusNamespace::Ssh] {
641 health_output.give(
642 &health_cap,
643 HealthStatusMessage {
644 id: None,
645 namespace,
646 update: HealthStatusUpdate::running(),
647 },
648 );
649 }
650
651 for export_stat in all_export_stats.iter() {
652 export_stat.set_offset_known(offset_known);
653 }
654
655 data_cap.downgrade(&future_ts);
656 }
657 Some(MetadataUpdate::TransientError(status)) => {
658 if let Some(update) = status.kafka {
659 health_output.give(
660 &health_cap,
661 HealthStatusMessage {
662 id: None,
663 namespace: StatusNamespace::Kafka,
664 update: update.clone(),
665 },
666 );
667 for (output, update) in outputs.iter().repeat_clone(update) {
668 health_output.give(
669 &health_cap,
670 HealthStatusMessage {
671 id: Some(output.id),
672 namespace: StatusNamespace::Kafka,
673 update,
674 },
675 );
676 }
677 }
678 if let Some(update) = status.ssh {
679 health_output.give(
680 &health_cap,
681 HealthStatusMessage {
682 id: None,
683 namespace: StatusNamespace::Ssh,
684 update: update.clone(),
685 },
686 );
687 for (output, update) in outputs.iter().repeat_clone(update) {
688 health_output.give(
689 &health_cap,
690 HealthStatusMessage {
691 id: Some(output.id),
692 namespace: StatusNamespace::Ssh,
693 update,
694 },
695 );
696 }
697 }
698 }
699 Some(MetadataUpdate::DefiniteError(error)) => {
700 health_output.give(
701 &health_cap,
702 HealthStatusMessage {
703 id: None,
704 namespace: StatusNamespace::Kafka,
705 update: HealthStatusUpdate::stalled(
706 error.to_string(),
707 None,
708 ),
709 },
710 );
711 let error = Err(error.into());
712 let time = data_cap.time().clone();
713 for (output, error) in
714 outputs.iter().map(|o| o.output_index).repeat_clone(error)
715 {
716 let update = ((output, error), time, Diff::ONE);
717 let size = update.fuel_size();
718 data_output
719 .give_fueled(&data_cap, update, size)
720 .await;
721 }
722
723 return;
724 }
725 None => {}
726 }
727
728 let polled_consumer = reader.consumer.get().map(Arc::clone);
736 while let Some(result) = polled_consumer
737 .as_ref()
738 .and_then(|consumer| consumer.poll(Duration::from_secs(0)))
739 {
740 match result {
741 Err(e) => {
742 let error = format!(
743 "kafka error when polling consumer for source: {} topic: {} : {}",
744 reader.source_name, reader.topic_name, e
745 );
746 let status = HealthStatusUpdate::stalled(error, None);
747 health_output.give(
748 &health_cap,
749 HealthStatusMessage {
750 id: None,
751 namespace: StatusNamespace::Kafka,
752 update: status.clone(),
753 },
754 );
755 for (output, status) in outputs.iter().repeat_clone(status) {
756 health_output.give(
757 &health_cap,
758 HealthStatusMessage {
759 id: Some(output.id),
760 namespace: StatusNamespace::Kafka,
761 update: status,
762 },
763 );
764 }
765 }
766 Ok(message) => {
767 let output_messages = outputs
768 .iter()
769 .map(|output| {
770 let (message, ts) = construct_source_message(
771 &message,
772 &output.metadata_columns,
773 );
774 (output.output_index, message, ts)
775 })
776 .collect::<Vec<_>>();
780 for (output_index, message, ts) in output_messages {
781 if let Some((msg, time, diff)) =
782 reader.handle_message(message, ts, &output_index)
783 {
784 let pid = time.interval().singleton().unwrap().unwrap_exact();
785 let part_cap = &reader.partition_capabilities[pid].data;
786 let msg = msg.map_err(|e| {
787 DataflowError::SourceError(Box::new(SourceError {
788 error: SourceErrorDetails::Other(e.to_string().into()),
789 hint: None,
790 }))
791 });
792 let update = ((output_index, msg), time, diff);
793 let size = update.fuel_size();
794 data_output
795 .give_fueled(part_cap, update, size)
796 .await;
797 }
798 }
799 }
800 }
801 }
802
803 reader.update_stats();
804
805 let mut consumers = std::mem::take(&mut reader.partition_consumers);
807 for consumer in consumers.iter_mut() {
808 let pid = consumer.pid();
809 let mut partition_exhausted = false;
815 for _ in 0..10_000 {
816 let Some(message) = consumer.get_next_message().transpose() else {
817 partition_exhausted = true;
818 break;
819 };
820
821 for output in outputs.iter() {
822 let message = match &message {
823 Ok((msg, pid)) => {
824 let (msg, ts) =
825 construct_source_message(msg, &output.metadata_columns);
826 assert_eq!(*pid, ts.0);
827 Ok(reader.handle_message(msg, ts, &output.output_index))
828 }
829 Err(err) => Err(err),
830 };
831 match message {
832 Ok(Some((msg, time, diff))) => {
833 let pid = time.interval().singleton().unwrap().unwrap_exact();
834 let part_cap = &reader.partition_capabilities[pid].data;
835 let msg = msg.map_err(|e| {
836 DataflowError::SourceError(Box::new(SourceError {
837 error: SourceErrorDetails::Other(e.to_string().into()),
838 hint: None,
839 }))
840 });
841 let update =
842 ((output.output_index, msg), time, diff);
843 let size = update.fuel_size();
844 data_output
845 .give_fueled(part_cap, update, size)
846 .await;
847 }
848 Ok(None) => continue,
850 Err(err) => {
851 let last_offset = reader
852 .last_offsets
853 .get(&output.output_index)
854 .expect("output known to be installed")
855 .get(&pid)
856 .expect("partition known to be installed");
857
858 let status = HealthStatusUpdate::stalled(
859 format!(
860 "error consuming from source: {} topic: {topic}:\
861 partition: {pid} last processed offset:\
862 {last_offset} : {err}",
863 config.name
864 ),
865 None,
866 );
867 health_output.give(
868 &health_cap,
869 HealthStatusMessage {
870 id: None,
871 namespace: StatusNamespace::Kafka,
872 update: status.clone(),
873 },
874 );
875 health_output.give(
876 &health_cap,
877 HealthStatusMessage {
878 id: Some(output.id),
879 namespace: StatusNamespace::Kafka,
880 update: status,
881 },
882 );
883 }
884 }
885 }
886 }
887 if !partition_exhausted {
888 notificator.notify_one();
889 }
890 }
891 assert!(reader.partition_consumers.is_empty());
893 reader.partition_consumers = consumers;
894
895 let positions = match reader.consumer.get() {
896 Some(consumer) => consumer.position().unwrap(),
897 None => TopicPartitionList::new(),
898 };
899 let topic_positions = positions.elements_for_topic(&reader.topic_name);
900 let mut snapshot_staged = 0;
901
902 for position in topic_positions {
903 if let Offset::Offset(offset) = position.offset() {
906 let pid = position.partition();
907 let upper_offset = MzOffset::from(u64::try_from(offset).unwrap());
908 let upper =
909 Partitioned::new_singleton(RangeBound::exact(pid), upper_offset);
910
911 let part_cap = reader.partition_capabilities.get_mut(&pid).unwrap();
912 match part_cap.data.try_downgrade(&upper) {
913 Ok(()) => {
914 if !snapshot_export_stats.is_empty() {
915 snapshot_staged += offset.try_into().unwrap_or(0u64);
918 if let Some(snapshot_total) = snapshot_total {
920 snapshot_staged =
923 std::cmp::min(snapshot_staged, snapshot_total);
924 }
925 }
926 }
927 Err(_) => {
928 info!(
931 source_id = config.id.to_string(),
932 worker_id = config.worker_id,
933 num_workers = config.worker_count,
934 "kafka source frontier downgrade skipped due to already \
935 seen offset: {:?}",
936 upper
937 );
938 }
939 };
940
941 }
942 }
943
944 if let (Some(snapshot_total), true) =
945 (snapshot_total, !snapshot_export_stats.is_empty())
946 {
947 for export_stat in snapshot_export_stats.iter() {
948 export_stat.set_snapshot_records_known(snapshot_total);
949 export_stat.set_snapshot_records_staged(snapshot_staged);
950 }
951 if snapshot_total == snapshot_staged {
952 snapshot_export_stats.clear();
953 }
954 }
955 }
956 })
957 });
958
959 (
960 stream.as_collection(),
961 health_stream,
962 button.press_on_drop(),
963 )
964}
965
966impl KafkaResumeUpperProcessor {
967 async fn process_frontier(
970 &mut self,
971 uppers: &ResumeUppers<KafkaTimestamp>,
972 ) -> Result<(), anyhow::Error> {
973 for (id, frontier) in &uppers.exports {
974 if let Some(stat) = self.config.statistics.get(id) {
975 let offset_committed = self
980 .responsible_offsets(frontier)
981 .map(|(_, offset)| offset.offset)
982 .sum();
983 stat.set_offset_committed(offset_committed);
984 }
985 }
986
987 let Some(frontier) = &uppers.source else {
988 return Ok(());
989 };
990 let offsets: Vec<_> = self.responsible_offsets(frontier).collect();
991 if offsets.is_empty() || offsets == self.committed_offsets {
994 return Ok(());
995 }
996 let Some(consumer) = self.consumer.get() else {
998 return Ok(());
999 };
1000 self.commit(consumer, &offsets).await?;
1001 self.committed_offsets = offsets;
1002 Ok(())
1003 }
1004
1005 async fn refresh(&self) -> Result<(), anyhow::Error> {
1009 let Some(consumer) = self.consumer.get() else {
1011 return Ok(());
1012 };
1013 if self.committed_offsets.is_empty() {
1014 return Ok(());
1015 }
1016 self.commit(consumer, &self.committed_offsets).await
1017 }
1018
1019 async fn commit(
1020 &self,
1021 consumer: &Arc<ReaderConsumer>,
1022 offsets: &[(PartitionId, MzOffset)],
1023 ) -> Result<(), anyhow::Error> {
1024 use rdkafka::consumer::CommitMode;
1025
1026 let mut tpl = TopicPartitionList::new();
1027 for (pid, offset) in offsets {
1028 let offset_to_commit =
1029 Offset::Offset(offset.offset.try_into().expect("offset to be vald i64"));
1030 tpl.add_partition_offset(&self.topic_name, *pid, offset_to_commit)
1031 .expect("offset known to be valid");
1032 }
1033 let consumer = Arc::clone(consumer);
1034 mz_ore::task::spawn_blocking(
1035 || format!("source({}) kafka offset commit", self.config.id),
1036 move || consumer.commit(&tpl, CommitMode::Sync),
1037 )
1038 .await?;
1039 Ok(())
1040 }
1041
1042 fn responsible_offsets<'a>(
1044 &'a self,
1045 frontier: &'a Antichain<KafkaTimestamp>,
1046 ) -> impl Iterator<Item = (PartitionId, MzOffset)> + 'a {
1047 frontier.iter().filter_map(|ts| {
1048 let pid = *ts.interval().singleton()?.unwrap_exact();
1049 responsible_for_pid(&self.config, pid).then(|| (pid, *ts.timestamp()))
1050 })
1051 }
1052}
1053
1054impl KafkaSourceReader {
1055 fn ensure_partition(&mut self, consumer: &Arc<ReaderConsumer>, pid: PartitionId) {
1057 if self.last_offsets.is_empty() {
1058 tracing::info!(
1059 source_id = %self.id,
1060 worker_id = %self.worker_id,
1061 "kafka source does not have any outputs, not creating partition queue");
1062
1063 return;
1064 }
1065 for last_offsets in self.last_offsets.values() {
1066 if last_offsets.contains_key(&pid) {
1068 return;
1069 }
1070 }
1071
1072 let start_offset = self.start_offsets.get(&pid).copied().unwrap_or(0);
1073 let start_offset = i64::try_from(start_offset).expect("start offsets must fit into i64");
1074 self.create_partition_queue(consumer, pid, Offset::Offset(start_offset));
1075
1076 for last_offsets in self.last_offsets.values_mut() {
1077 let prev = last_offsets.insert(pid, start_offset - 1);
1078 assert_none!(prev);
1079 }
1080 }
1081
1082 fn create_partition_queue(
1084 &mut self,
1085 consumer: &Arc<ReaderConsumer>,
1086 partition_id: PartitionId,
1087 initial_offset: Offset,
1088 ) {
1089 info!(
1090 source_id = self.id.to_string(),
1091 worker_id = self.worker_id,
1092 num_workers = self.worker_count,
1093 "activating Kafka queue for topic {}, partition {}",
1094 self.topic_name,
1095 partition_id,
1096 );
1097
1098 let tpl = consumer.assignment().unwrap();
1100 let mut partition_list = TopicPartitionList::new();
1102 for partition in tpl.elements_for_topic(&self.topic_name) {
1103 partition_list
1104 .add_partition_offset(partition.topic(), partition.partition(), partition.offset())
1105 .expect("offset known to be valid");
1106 }
1107 partition_list
1109 .add_partition_offset(&self.topic_name, partition_id, initial_offset)
1110 .expect("offset known to be valid");
1111 consumer
1112 .assign(&partition_list)
1113 .expect("assignment known to be valid");
1114
1115 let context = Arc::clone(consumer.context());
1118 for pc in &mut self.partition_consumers {
1119 pc.partition_queue = consumer
1120 .split_partition_queue(&self.topic_name, pc.pid)
1121 .expect("partition known to be valid");
1122 pc.partition_queue.set_nonempty_callback({
1123 let context = Arc::clone(&context);
1124 move || context.inner().activate()
1125 });
1126 }
1127
1128 let mut partition_queue = consumer
1129 .split_partition_queue(&self.topic_name, partition_id)
1130 .expect("partition known to be valid");
1131 partition_queue.set_nonempty_callback(move || context.inner().activate());
1132 self.partition_consumers
1133 .push(PartitionConsumer::new(partition_id, partition_queue));
1134 assert_eq!(
1135 consumer
1136 .assignment()
1137 .unwrap()
1138 .elements_for_topic(&self.topic_name)
1139 .len(),
1140 self.partition_consumers.len()
1141 );
1142 }
1143
1144 fn update_stats(&mut self) {
1146 while let Ok(stats) = self.stats_rx.try_recv() {
1147 match serde_json::from_str::<Statistics>(&stats.to_string()) {
1148 Ok(statistics) => {
1149 let topic = statistics.topics.get(&self.topic_name);
1150 match topic {
1151 Some(topic) => {
1152 for (id, partition) in &topic.partitions {
1153 self.partition_metrics
1154 .set_offset_max(*id, partition.hi_offset);
1155 }
1156 }
1157 None => error!("No stats found for topic: {}", &self.topic_name),
1158 }
1159 }
1160 Err(e) => {
1161 error!("failed decoding librdkafka statistics JSON: {}", e);
1162 }
1163 }
1164 }
1165 }
1166
1167 fn handle_message(
1170 &mut self,
1171 message: Result<SourceMessage, KafkaHeaderParseError>,
1172 (partition, offset): (PartitionId, MzOffset),
1173 output_index: &usize,
1174 ) -> Option<(
1175 Result<SourceMessage, KafkaHeaderParseError>,
1176 KafkaTimestamp,
1177 Diff,
1178 )> {
1179 assert!(
1191 self.last_offsets
1192 .get(output_index)
1193 .unwrap()
1194 .contains_key(&partition)
1195 );
1196
1197 let last_offset_ref = self
1198 .last_offsets
1199 .get_mut(output_index)
1200 .expect("output known to be installed")
1201 .get_mut(&partition)
1202 .expect("partition known to be installed");
1203
1204 let last_offset = *last_offset_ref;
1205 let offset_as_i64: i64 = offset.offset.try_into().expect("offset to be < i64::MAX");
1206 if offset_as_i64 <= last_offset {
1207 info!(
1208 source_id = self.id.to_string(),
1209 worker_id = self.worker_id,
1210 num_workers = self.worker_count,
1211 "kafka message before expected offset: \
1212 source {} (reading topic {}, partition {}, output {}) \
1213 received offset {} expected offset {:?}",
1214 self.source_name,
1215 self.topic_name,
1216 partition,
1217 output_index,
1218 offset.offset,
1219 last_offset + 1,
1220 );
1221 None
1223 } else {
1224 *last_offset_ref = offset_as_i64;
1225
1226 let ts = Partitioned::new_singleton(RangeBound::exact(partition), offset);
1227 Some((message, ts, Diff::ONE))
1228 }
1229 }
1230}
1231
1232fn construct_source_message(
1233 msg: &BorrowedMessage<'_>,
1234 metadata_columns: &[KafkaMetadataKind],
1235) -> (
1236 Result<SourceMessage, KafkaHeaderParseError>,
1237 (PartitionId, MzOffset),
1238) {
1239 let pid = msg.partition();
1240 let Ok(offset) = u64::try_from(msg.offset()) else {
1241 panic!(
1242 "got negative offset ({}) from otherwise non-error'd kafka message",
1243 msg.offset()
1244 );
1245 };
1246
1247 let mut metadata = Row::default();
1248 let mut packer = metadata.packer();
1249 for kind in metadata_columns {
1250 match kind {
1251 KafkaMetadataKind::Partition => packer.push(Datum::from(pid)),
1252 KafkaMetadataKind::Offset => packer.push(Datum::UInt64(offset)),
1253 KafkaMetadataKind::Timestamp => {
1254 let ts = msg
1255 .timestamp()
1256 .to_millis()
1257 .expect("kafka sources always have upstream_time");
1258
1259 let d: Datum = DateTime::from_timestamp_millis(ts)
1260 .and_then(|dt| {
1261 let ct: Option<CheckedTimestamp<NaiveDateTime>> =
1262 dt.naive_utc().try_into().ok();
1263 ct
1264 })
1265 .into();
1266 packer.push(d)
1267 }
1268 KafkaMetadataKind::Header { key, use_bytes } => {
1269 match msg.headers() {
1270 Some(headers) => {
1271 let d = headers
1272 .iter()
1273 .filter(|header| header.key == key)
1274 .last()
1275 .map(|header| match header.value {
1276 Some(v) => {
1277 if *use_bytes {
1278 Ok(Datum::Bytes(v))
1279 } else {
1280 match str::from_utf8(v) {
1281 Ok(str) => Ok(Datum::String(str)),
1282 Err(_) => Err(KafkaHeaderParseError::Utf8Error {
1283 key: key.clone(),
1284 raw: v.to_vec(),
1285 }),
1286 }
1287 }
1288 }
1289 None => Ok(Datum::Null),
1290 })
1291 .unwrap_or_else(|| {
1292 Err(KafkaHeaderParseError::KeyNotFound { key: key.clone() })
1293 });
1294 match d {
1295 Ok(d) => packer.push(d),
1296 Err(err) => return (Err(err), (pid, offset.into())),
1298 }
1299 }
1300 None => packer.push(Datum::Null),
1301 }
1302 }
1303 KafkaMetadataKind::Headers => {
1304 packer.push_list_with(|r| {
1305 if let Some(headers) = msg.headers() {
1306 for header in headers.iter() {
1307 match header.value {
1308 Some(v) => r.push_list_with(|record_row| {
1309 record_row.push(Datum::String(header.key));
1310 record_row.push(Datum::Bytes(v));
1311 }),
1312 None => r.push_list_with(|record_row| {
1313 record_row.push(Datum::String(header.key));
1314 record_row.push(Datum::Null);
1315 }),
1316 }
1317 }
1318 }
1319 });
1320 }
1321 }
1322 }
1323
1324 let key = match msg.key() {
1325 Some(bytes) => Row::pack([Datum::Bytes(bytes)]),
1326 None => Row::pack([Datum::Null]),
1327 };
1328 let value = match msg.payload() {
1329 Some(bytes) => Row::pack([Datum::Bytes(bytes)]),
1330 None => Row::pack([Datum::Null]),
1331 };
1332 (
1333 Ok(SourceMessage {
1334 key,
1335 value,
1336 metadata,
1337 }),
1338 (pid, offset.into()),
1339 )
1340}
1341
1342struct PartitionConsumer {
1344 pid: PartitionId,
1346 partition_queue: PartitionQueue<TunnelingClientContext<GlueConsumerContext>>,
1348}
1349
1350impl PartitionConsumer {
1351 fn new(
1353 pid: PartitionId,
1354 partition_queue: PartitionQueue<TunnelingClientContext<GlueConsumerContext>>,
1355 ) -> Self {
1356 PartitionConsumer {
1357 pid,
1358 partition_queue,
1359 }
1360 }
1361
1362 fn get_next_message(&self) -> Result<Option<(BorrowedMessage<'_>, PartitionId)>, KafkaError> {
1369 match self.partition_queue.poll(Duration::from_millis(0)) {
1370 Some(Ok(msg)) => Ok(Some((msg, self.pid))),
1371 Some(Err(err)) => Err(err),
1372 _ => Ok(None),
1373 }
1374 }
1375
1376 fn pid(&self) -> PartitionId {
1378 self.pid
1379 }
1380}
1381
1382struct GlueConsumerContext {
1385 notificator: Arc<Notify>,
1386 stats_tx: crossbeam_channel::Sender<Jsonb>,
1387 inner: MzClientContext,
1388}
1389
1390impl ClientContext for GlueConsumerContext {
1391 fn stats_raw(&self, statistics: &[u8]) {
1392 match Jsonb::from_slice(statistics) {
1393 Ok(statistics) => {
1394 self.stats_tx
1395 .send(statistics)
1396 .expect("timely operator hung up while Kafka source active");
1397 self.activate();
1398 }
1399 Err(e) => error!("failed decoding librdkafka statistics JSON: {}", e),
1400 };
1401 }
1402
1403 fn log(&self, level: rdkafka::config::RDKafkaLogLevel, fac: &str, log_message: &str) {
1406 self.inner.log(level, fac, log_message)
1407 }
1408 fn error(&self, error: rdkafka::error::KafkaError, reason: &str) {
1409 self.inner.error(error, reason)
1410 }
1411}
1412
1413impl GlueConsumerContext {
1414 fn activate(&self) {
1415 self.notificator.notify_one();
1416 }
1417}
1418
1419impl ConsumerContext for GlueConsumerContext {}
1420
1421type DataOutput = AsyncOutputHandle<
1422 KafkaTimestamp,
1423 FueledBuilder<
1424 CapacityContainerBuilder<
1425 Vec<(
1426 (usize, Result<SourceMessage, DataflowError>),
1427 KafkaTimestamp,
1428 Diff,
1429 )>,
1430 >,
1431 >,
1432>;
1433type HealthOutput =
1434 AsyncOutputHandle<KafkaTimestamp, CapacityContainerBuilder<Vec<HealthStatusMessage>>>;
1435
1436struct ReaderConsumerSetup {
1439 config: RawSourceCreationConfig,
1440 connection: KafkaConnection,
1441 topic: String,
1442 topic_metadata_refresh_interval: Duration,
1443 group_id: String,
1444 client_id: String,
1445 notificator: Arc<Notify>,
1446 stats_tx: crossbeam_channel::Sender<Jsonb>,
1447 outputs: Vec<(GlobalId, usize)>,
1449 data_output: DataOutput,
1450 health_output: HealthOutput,
1451 health_cap: Capability<KafkaTimestamp>,
1452}
1453
1454impl ReaderConsumerSetup {
1455 async fn get_or_create<'a>(
1463 &self,
1464 slot: &'a OnceCell<Arc<ReaderConsumer>>,
1465 start_offsets: &mut BTreeMap<PartitionId, u64>,
1466 data_cap: &Capability<KafkaTimestamp>,
1467 ) -> ControlFlow<(), &'a Arc<ReaderConsumer>> {
1468 if let Some(consumer) = slot.get() {
1469 return ControlFlow::Continue(consumer);
1470 }
1471 let consumer = match slot.get_or_try_init(|| self.create()).await {
1472 Ok(consumer) => consumer,
1473 Err(e) => {
1474 self.report_creation_failure(&e);
1475 std::future::pending::<()>().await;
1479 unreachable!("pending future never returns");
1480 }
1481 };
1482 if mz_storage_types::dyncfgs::KAFKA_LOW_WATERMARK_CHECK.get(self.config.config.config_set())
1483 {
1484 self.check_low_watermarks(consumer, start_offsets, data_cap)
1485 .await?;
1486 }
1487 ControlFlow::Continue(consumer)
1488 }
1489
1490 async fn create(&self) -> Result<Arc<ReaderConsumer>, ContextCreationError> {
1491 let consumer: ReaderConsumer = self
1492 .connection
1493 .create_with_context(
1494 &self.config.config,
1495 GlueConsumerContext {
1496 notificator: Arc::clone(&self.notificator),
1497 stats_tx: self.stats_tx.clone(),
1498 inner: MzClientContext::default(),
1499 },
1500 &btreemap! {
1501 "enable.auto.commit" => "false".into(),
1506 "auto.offset.reset" => "earliest".into(),
1509 "topic.metadata.refresh.interval.ms" =>
1512 self.topic_metadata_refresh_interval
1513 .as_millis()
1514 .to_string(),
1515 "fetch.message.max.bytes" => "134217728".into(),
1517 "group.id" => self.group_id.clone(),
1523 "client.id" => self.client_id.clone(),
1526 },
1527 InTask::Yes,
1528 )
1529 .await?;
1530 Ok(Arc::new(consumer))
1531 }
1532
1533 fn report_creation_failure(&self, e: &ContextCreationError) {
1534 let update = HealthStatusUpdate::halting(
1535 format!(
1536 "failed creating kafka reader consumer: {}",
1537 e.display_with_causes()
1538 ),
1539 None,
1540 );
1541 let namespace = if matches!(e, ContextCreationError::Ssh(_)) {
1542 StatusNamespace::Ssh
1543 } else {
1544 StatusNamespace::Kafka
1545 };
1546 self.report_health(namespace, update);
1547 }
1548
1549 fn report_health(&self, namespace: StatusNamespace, update: HealthStatusUpdate) {
1551 let ids = std::iter::once(None).chain(self.outputs.iter().map(|(id, _)| Some(*id)));
1552 for (id, update) in ids.repeat_clone(update) {
1553 self.health_output.give(
1554 &self.health_cap,
1555 HealthStatusMessage {
1556 id,
1557 namespace,
1558 update,
1559 },
1560 );
1561 }
1562 }
1563
1564 async fn emit_definite_error(
1566 &self,
1567 data_cap: &Capability<KafkaTimestamp>,
1568 error: SourceErrorDetails,
1569 ) {
1570 let error: Result<SourceMessage, DataflowError> =
1571 Err(SourceError { error, hint: None }.into());
1572 let time = data_cap.time().clone();
1573 for (output, error) in self
1574 .outputs
1575 .iter()
1576 .map(|(_, output_index)| *output_index)
1577 .repeat_clone(error)
1578 {
1579 let update = ((output, error), time.clone(), Diff::ONE);
1580 let size = update.fuel_size();
1581 self.data_output.give_fueled(data_cap, update, size).await;
1582 }
1583 }
1584
1585 async fn check_low_watermarks(
1586 &self,
1587 consumer: &ReaderConsumer,
1588 start_offsets: &mut BTreeMap<PartitionId, u64>,
1589 data_cap: &Capability<KafkaTimestamp>,
1590 ) -> ControlFlow<()> {
1591 let config = &self.config;
1592 let topic = &self.topic;
1593 let low_watermarks = match fetch_partition_info(
1594 consumer,
1595 topic,
1596 config
1597 .config
1598 .parameters
1599 .kafka_timeout_config
1600 .fetch_metadata_timeout,
1601 Offset::Beginning, ) {
1603 Ok(low_watermarks) => low_watermarks,
1604 Err(e) => {
1605 tracing::warn!(
1606 source_id = config.id.to_string(),
1607 worker_id = config.worker_id,
1608 num_workers = config.worker_count,
1609 "Failed to fetch watermarks for topic {topic}: {e}"
1610 );
1611 self.report_health(
1612 StatusNamespace::Kafka,
1613 HealthStatusUpdate::stalled(
1614 format!("Failed to fetch watermarks for topic {topic}: {e}"),
1615 None,
1616 ),
1617 );
1618 let ssh_update = match consumer.client().context().tunnel_status() {
1619 SshTunnelStatus::Running => HealthStatusUpdate::running(),
1620 SshTunnelStatus::Errored(e) => HealthStatusUpdate::stalled(e, None),
1621 };
1622 self.report_health(StatusNamespace::Ssh, ssh_update);
1623 if let GetPartitionsError::TopicDoesNotExist = e {
1624 self.emit_definite_error(
1626 data_cap,
1627 SourceErrorDetails::Initialization(e.to_string().into()),
1628 )
1629 .await;
1630 }
1631 return ControlFlow::Continue(());
1632 }
1633 };
1634 for (pid, lwm) in &low_watermarks {
1635 if !responsible_for_pid(config, *pid) {
1636 continue;
1637 }
1638 if let Some(start_offset) = start_offsets.get_mut(pid) {
1645 tracing::info!(
1646 source_id = config.id.to_string(),
1647 worker_id = config.worker_id,
1648 num_workers = config.worker_count,
1649 "restored offset {start_offset} for topic {topic} partition {pid} with low watermark {lwm}"
1650 );
1651 if lwm > start_offset {
1652 tracing::error!(
1653 source_id = config.id.to_string(),
1654 worker_id = config.worker_id,
1655 num_workers = config.worker_count,
1656 "start offset and resume upper {start_offset} for topic {topic} \
1657 partition {pid} is behind the low watermark {lwm}. This likely \
1658 means that the offsets have been compacted away by Kafka."
1659 );
1660 let err_str = format!(
1661 "Low watermark {lwm} of kafka topic {topic} partition {pid} \
1662 is past the start offset/resume upper: {start_offset} \
1663 This likely means that the offsets have been compacted away \
1664 by Kafka. Please consider setting a higher start offset or \
1665 adjusting your retention policies to prevent this.",
1666 );
1667 self.health_output.give(
1668 &self.health_cap,
1669 HealthStatusMessage {
1670 id: None,
1671 namespace: StatusNamespace::Kafka,
1672 update: HealthStatusUpdate::stalled(err_str.clone(), None),
1673 },
1674 );
1675 self.emit_definite_error(
1676 data_cap,
1677 SourceErrorDetails::Initialization(err_str.into()),
1678 )
1679 .await;
1680 return ControlFlow::Break(());
1681 }
1682 } else {
1683 tracing::warn!(
1684 source_id = config.id.to_string(),
1685 worker_id = config.worker_id,
1686 num_workers = config.worker_count,
1687 "partition {pid} has a non-zero low watermark {lwm}, but no start offset or \
1688 resume upper was found for this partition. Setting start offset to low watermark"
1689 );
1690 start_offsets.insert(*pid, *lwm);
1691 }
1692 }
1693 ControlFlow::Continue(())
1694 }
1695}
1696
1697#[cfg(test)]
1698mod tests {
1699 use std::sync::Arc;
1700 use std::time::Duration;
1701
1702 use mz_kafka_util::client::create_new_client_config_simple;
1703 use rdkafka::consumer::{BaseConsumer, Consumer};
1704 use rdkafka::{Message, Offset, TopicPartitionList};
1705 use uuid::Uuid;
1706
1707 #[mz_ore::test]
1719 #[ignore]
1720 fn demonstrate_kafka_queue_race_condition() -> Result<(), anyhow::Error> {
1721 let topic_name = "queue-test";
1722 let pid = 0;
1723
1724 let mut kafka_config = create_new_client_config_simple();
1725 kafka_config.set("bootstrap.servers", "localhost:9092".to_string());
1726 kafka_config.set("enable.auto.commit", "false");
1727 kafka_config.set("group.id", Uuid::new_v4().to_string());
1728 kafka_config.set("fetch.message.max.bytes", "100");
1729 let consumer: BaseConsumer<_> = mz_kafka_util::client::create(&kafka_config)?;
1730
1731 let consumer = Arc::new(consumer);
1732
1733 let mut partition_list = TopicPartitionList::new();
1734 partition_list.add_partition_offset(topic_name, pid, Offset::Offset(0))?;
1737
1738 consumer.assign(&partition_list)?;
1739
1740 let partition_queue = consumer
1741 .split_partition_queue(topic_name, pid)
1742 .expect("missing partition queue");
1743
1744 let expected_messages = 1_000;
1745
1746 let mut common_queue_count = 0;
1747 let mut partition_queue_count = 0;
1748
1749 loop {
1750 if let Some(msg) = consumer.poll(Duration::from_millis(0)) {
1751 match msg {
1752 Ok(msg) => {
1753 let _payload =
1754 std::str::from_utf8(msg.payload().expect("missing payload"))?;
1755 if partition_queue_count > 0 {
1756 anyhow::bail!(
1757 "Got message from common queue after we internally switched to partition queue."
1758 );
1759 }
1760
1761 common_queue_count += 1;
1762 }
1763 Err(err) => anyhow::bail!("{}", err),
1764 }
1765 }
1766
1767 match partition_queue.poll(Duration::from_millis(0)) {
1768 Some(Ok(msg)) => {
1769 let _payload = std::str::from_utf8(msg.payload().expect("missing payload"))?;
1770 partition_queue_count += 1;
1771 }
1772 Some(Err(err)) => anyhow::bail!("{}", err),
1773 _ => (),
1774 }
1775
1776 if (common_queue_count + partition_queue_count) == expected_messages {
1777 break;
1778 }
1779 }
1780
1781 assert!(
1782 common_queue_count == 0,
1783 "Got {} out of {} messages from common queue. Partition queue: {}",
1784 common_queue_count,
1785 expected_messages,
1786 partition_queue_count
1787 );
1788
1789 Ok(())
1790 }
1791}
1792
1793fn fetch_partition_info<C: ConsumerContext>(
1795 consumer: &BaseConsumer<C>,
1796 topic: &str,
1797 fetch_timeout: Duration,
1798 offset_requested: Offset,
1799) -> Result<BTreeMap<PartitionId, PartitionWatermark>, GetPartitionsError> {
1800 let pids = get_partitions(consumer.client(), topic, fetch_timeout)?;
1801
1802 let mut offset_requests = TopicPartitionList::with_capacity(pids.len());
1803 for pid in pids {
1804 offset_requests.add_partition_offset(topic, pid, offset_requested)?;
1805 }
1806
1807 let offset_responses = consumer.offsets_for_times(offset_requests, fetch_timeout)?;
1808
1809 let mut result = BTreeMap::new();
1810 for entry in offset_responses.elements() {
1811 let offset = match entry.offset() {
1812 Offset::Offset(offset) => offset,
1813 offset => Err(anyhow!("unexpected high watermark offset: {offset:?}"))?,
1814 };
1815
1816 let pid = entry.partition();
1817 let watermark = offset.try_into().expect("invalid negative offset");
1818 result.insert(pid, watermark);
1819 }
1820
1821 Ok(result)
1822}
1823
1824#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1826enum MetadataUpdate {
1827 Partitions(BTreeMap<PartitionId, PartitionWatermark>),
1829 TransientError(HealthStatus),
1833 DefiniteError(SourceError),
1837}
1838
1839impl MetadataUpdate {
1840 fn upstream_frontier(&self) -> Option<Antichain<KafkaTimestamp>> {
1842 match self {
1843 Self::Partitions(partitions) => {
1844 let max_pid = partitions.keys().last().copied();
1845 let lower = max_pid
1846 .map(RangeBound::after)
1847 .unwrap_or(RangeBound::NegInfinity);
1848 let future_ts =
1849 Partitioned::new_range(lower, RangeBound::PosInfinity, MzOffset::from(0));
1850
1851 let mut frontier = Antichain::from_elem(future_ts);
1852 for (pid, high_watermark) in partitions {
1853 frontier.insert(Partitioned::new_singleton(
1854 RangeBound::exact(*pid),
1855 MzOffset::from(*high_watermark),
1856 ));
1857 }
1858
1859 Some(frontier)
1860 }
1861 Self::DefiniteError(_) => Some(Antichain::new()),
1862 Self::TransientError(_) => None,
1863 }
1864 }
1865}
1866
1867#[derive(Debug, thiserror::Error)]
1868pub enum KafkaHeaderParseError {
1869 #[error("A header with key '{key}' was not found in the message headers")]
1870 KeyNotFound { key: String },
1871 #[error(
1872 "Found ill-formed byte sequence in header '{key}' that cannot be decoded as valid utf-8 (original bytes: {raw:x?})"
1873 )]
1874 Utf8Error { key: String, raw: Vec<u8> },
1875}
1876
1877fn render_metadata_fetcher<'scope>(
1883 scope: Scope<'scope, KafkaTimestamp>,
1884 connection: KafkaSourceConnection,
1885 config: RawSourceCreationConfig,
1886) -> (
1887 StreamVec<'scope, KafkaTimestamp, (mz_repr::Timestamp, MetadataUpdate)>,
1888 StreamVec<'scope, KafkaTimestamp, Probe<KafkaTimestamp>>,
1889 PressOnDropButton,
1890) {
1891 let active_worker_id = usize::cast_from(config.id.hashed());
1892 let is_active_worker = active_worker_id % scope.peers() == scope.index();
1893
1894 let resume_upper = Antichain::from_iter(
1895 config
1896 .source_resume_uppers
1897 .values()
1898 .map(|uppers| uppers.iter().map(KafkaTimestamp::decode_row))
1899 .flatten(),
1900 );
1901
1902 let name = format!("KafkaMetadataFetcher({})", config.id);
1903 let mut builder = AsyncOperatorBuilder::new(name, scope.clone());
1904
1905 let (metadata_output, metadata_stream) =
1906 builder.new_output::<CapacityContainerBuilder<Vec<_>>>();
1907 let (probe_output, probe_stream) = builder.new_output::<CapacityContainerBuilder<Vec<_>>>();
1908
1909 let button = builder.build(move |caps| async move {
1910 if !is_active_worker {
1911 return;
1912 }
1913
1914 let [metadata_cap, probe_cap] = caps.try_into().unwrap();
1915
1916 let client_id = connection.client_id(
1917 config.config.config_set(),
1918 &config.config.connection_context,
1919 config.id,
1920 );
1921 let KafkaSourceConnection {
1922 connection,
1923 topic,
1924 topic_metadata_refresh_interval,
1925 ..
1926 } = connection;
1927
1928 let consumer: Result<BaseConsumer<_>, _> = connection
1929 .create_with_context(
1930 &config.config,
1931 MzClientContext::default(),
1932 &btreemap! {
1933 "topic.metadata.refresh.interval.ms" =>
1936 topic_metadata_refresh_interval
1937 .as_millis()
1938 .to_string(),
1939 "client.id" => format!("{client_id}-metadata"),
1942 },
1943 InTask::Yes,
1944 )
1945 .await;
1946
1947 let consumer = match consumer {
1948 Ok(consumer) => consumer,
1949 Err(e) => {
1950 let msg = format!(
1951 "failed creating kafka metadata consumer: {}",
1952 e.display_with_causes()
1953 );
1954 let status_update = HealthStatusUpdate::halting(msg, None);
1955 let status = match e {
1956 ContextCreationError::Ssh(_) => HealthStatus::ssh(status_update),
1957 _ => HealthStatus::kafka(status_update),
1958 };
1959 let error = MetadataUpdate::TransientError(status);
1960 let timestamp = (config.now_fn)().into();
1961 metadata_output.give(&metadata_cap, (timestamp, error));
1962
1963 std::future::pending::<()>().await;
1967 unreachable!("pending future never returns");
1968 }
1969 };
1970
1971 let (tx, mut rx) = mpsc::unbounded_channel();
1972 spawn_metadata_thread(config, consumer, topic, tx);
1973
1974 let mut prev_upstream_frontier = resume_upper;
1975
1976 while let Some((timestamp, mut update)) = rx.recv().await {
1977 if prev_upstream_frontier.is_empty() {
1978 return;
1979 }
1980
1981 if let Some(upstream_frontier) = update.upstream_frontier() {
1982 if !PartialOrder::less_equal(&prev_upstream_frontier, &upstream_frontier) {
1992 let error = SourceError {
1993 error: SourceErrorDetails::Other("topic was recreated".into()),
1994 hint: None,
1995 };
1996 update = MetadataUpdate::DefiniteError(error);
1997 }
1998 }
1999
2000 if let Some(upstream_frontier) = update.upstream_frontier() {
2001 prev_upstream_frontier = upstream_frontier.clone();
2002
2003 let probe = Probe {
2004 probe_ts: timestamp,
2005 upstream_frontier,
2006 };
2007 probe_output.give(&probe_cap, probe);
2008 }
2009
2010 metadata_output.give(&metadata_cap, (timestamp, update));
2011 }
2012 });
2013
2014 (metadata_stream, probe_stream, button.press_on_drop())
2015}
2016
2017fn spawn_metadata_thread<C: ConsumerContext>(
2018 config: RawSourceCreationConfig,
2019 consumer: BaseConsumer<TunnelingClientContext<C>>,
2020 topic: String,
2021 tx: mpsc::UnboundedSender<(mz_repr::Timestamp, MetadataUpdate)>,
2022) {
2023 thread::Builder::new()
2025 .name(format!("kfk-mtdt-{}", config.id))
2026 .spawn(move || {
2027 trace!(
2028 source_id = config.id.to_string(),
2029 worker_id = config.worker_id,
2030 num_workers = config.worker_count,
2031 "kafka metadata thread: starting..."
2032 );
2033
2034 let timestamp_interval = config.timestamp_interval;
2035 let mut ticker = probe::Ticker::new(move || timestamp_interval, config.now_fn);
2036
2037 loop {
2038 let probe_ts = ticker.tick_blocking();
2039 let result = fetch_partition_info(
2040 &consumer,
2041 &topic,
2042 config
2043 .config
2044 .parameters
2045 .kafka_timeout_config
2046 .fetch_metadata_timeout,
2047 Offset::End,
2048 );
2049 trace!(
2050 source_id = config.id.to_string(),
2051 worker_id = config.worker_id,
2052 num_workers = config.worker_count,
2053 "kafka metadata thread: metadata fetch result: {:?}",
2054 result
2055 );
2056 let update = match result {
2057 Ok(partitions) => {
2058 trace!(
2059 source_id = config.id.to_string(),
2060 worker_id = config.worker_id,
2061 num_workers = config.worker_count,
2062 "kafka metadata thread: fetched partition metadata info",
2063 );
2064
2065 MetadataUpdate::Partitions(partitions)
2066 }
2067 Err(GetPartitionsError::TopicDoesNotExist) => {
2068 let error = SourceError {
2069 error: SourceErrorDetails::Other("topic was deleted".into()),
2070 hint: None,
2071 };
2072 MetadataUpdate::DefiniteError(error)
2073 }
2074 Err(e) => {
2075 let kafka_status = Some(HealthStatusUpdate::stalled(
2076 format!("{}", e.display_with_causes()),
2077 None,
2078 ));
2079
2080 let ssh_status = consumer.client().context().tunnel_status();
2081 let ssh_status = match ssh_status {
2082 SshTunnelStatus::Running => Some(HealthStatusUpdate::running()),
2083 SshTunnelStatus::Errored(e) => {
2084 Some(HealthStatusUpdate::stalled(e, None))
2085 }
2086 };
2087
2088 MetadataUpdate::TransientError(HealthStatus {
2089 kafka: kafka_status,
2090 ssh: ssh_status,
2091 })
2092 }
2093 };
2094
2095 if tx.send((probe_ts, update)).is_err() {
2096 break;
2097 }
2098 }
2099
2100 info!(
2101 source_id = config.id.to_string(),
2102 worker_id = config.worker_id,
2103 num_workers = config.worker_count,
2104 "kafka metadata thread: receiver has gone away; shutting down."
2105 )
2106 })
2107 .unwrap();
2108}