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