Skip to main content

mz_storage/source/
kafka.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use 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
109/// Contains all information necessary to ingest data from Kafka
110pub struct KafkaSourceReader {
111    /// Name of the topic on which this source is backed on
112    topic_name: String,
113    /// Name of the source (will have format kafka-source-id)
114    source_name: String,
115    /// Source global ID
116    id: GlobalId,
117    /// Kafka consumer for this source, created once this worker is responsible for a partition.
118    consumer: Rc<OnceCell<Arc<ReaderConsumer>>>,
119    /// List of consumers. A consumer should be assigned per partition to guarantee fairness
120    partition_consumers: Vec<PartitionConsumer>,
121    /// Worker ID
122    worker_id: usize,
123    /// Total count of workers
124    worker_count: usize,
125    /// The most recently read offset for each partition known to this source
126    /// reader by output-index. An offset of -1 indicates that no prior message
127    /// has been read for the given partition.
128    last_offsets: BTreeMap<usize, BTreeMap<PartitionId, i64>>,
129    /// The offset to start reading from for each partition.
130    start_offsets: BTreeMap<PartitionId, u64>,
131    /// Channel to receive Kafka statistics JSON blobs from the stats callback.
132    stats_rx: crossbeam_channel::Receiver<Jsonb>,
133    /// A handle to the partition specific metrics
134    partition_metrics: KafkaSourceMetrics,
135    /// Per partition capabilities used to produce messages
136    partition_capabilities: BTreeMap<PartitionId, PartitionCapability>,
137}
138
139type ReaderConsumer = BaseConsumer<TunnelingClientContext<GlueConsumerContext>>;
140
141struct PartitionCapability {
142    /// The capability of the data produced
143    data: Capability<KafkaTimestamp>,
144}
145
146/// The high/low watermark offsets of a Kafka partition.
147///
148/// This is the offset of either the first available or the latest message in the topic/partition
149///  available for consumption + 1.
150type PartitionWatermark = u64;
151
152/// Processes `resume_uppers` stream updates, committing them upstream and
153/// reporting each export's `offset_committed`.
154pub struct KafkaResumeUpperProcessor {
155    config: RawSourceCreationConfig,
156    topic_name: String,
157    /// Shared with [`KafkaSourceReader::consumer`].
158    consumer: Rc<OnceCell<Arc<ReaderConsumer>>>,
159    /// The offsets this worker last committed upstream. Only a successful commit updates it, so a
160    /// failed commit is retried on the next resume upper.
161    committed_offsets: Vec<(PartitionId, MzOffset)>,
162}
163
164/// Computes whether this worker is responsible for consuming a partition. It assigns partitions to
165/// workers in a round-robin fashion, starting at an arbitrary worker based on the hash of the
166/// source id.
167fn 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    // TODO(petrosagg): The type used for the partition (RangeBound<PartitionId>) doesn't need to
181    // be so complicated and we could instead use `Partitioned<PartitionId, Option<u64>>` where all
182    // ranges are inclusive and a time of `None` signifies that a particular partition is not
183    // present. This requires an shard migration of the remap shard.
184    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
236/// Render the reader of a Kafka source.
237///
238/// The reader is responsible for polling the Kafka topic partitions for new messages, and
239/// transforming them into a `SourceMessage` collection.
240fn 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    // Contains the `SourceStatistics` entries for exports that require a snapshot.
263    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        // export requires snapshot
295        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                // Exhaustive match protects against forgetting to apply an
327                // option. Ignored fields are justified below.
328                connection_id: _,   // not needed here
329                group_id_prefix: _, // used above via `connection.group_id`
330            } = 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(&notificator),
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            // Start offsets is a map from partition to the next offset to read from.
359            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            // Each consumer owns librdkafka threads, one per broker plus internal ones, so only
414            // workers responsible for a partition create one. A worker that knows of no such
415            // partition yet creates its consumer when a metadata update assigns it one.
416            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            // Note that we wait for this AFTER we downgrade to the source `resume_upper`. This
426            // allows downstream operators (namely, the `reclock_operator`) to downgrade to the
427            // `resume_upper`, which is necessary for this basic form of backpressure to work.
428            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            // Seed the progress metrics from the resume uppers if we are snapshotting.
469            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                // Reset snapshot statistics for any exports that are not involved
488                // in this round of snapshotting. Those that are snapshotting this round will
489                // see updates as the snapshot commences.
490                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                // Zero disables the refresh. `interval` panics on a zero period, so the tick arm
501                // below is what honors it.
502                let mut refresh =
503                    tokio::time::interval(refresh_interval.max(Duration::from_millis(1)));
504                // A commit that outlasts a period must not be followed by a burst of recommits.
505                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                // During dataflow shutdown this loop can end due to the general chaos caused by
533                // dropping tokens as a means to shutdown. This call ensures this future never ends
534                // and we instead rely on this operator being dropped altogether when *its* token
535                // is dropped.
536                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                // Wait for data or metadata events while also making progress with offset
547                // committing.
548                tokio::select! {
549                    // TODO(petrosagg): remove the timeout and rely purely on librdkafka waking us
550                    // up
551                    _ = tokio::time::timeout(max_wait_time, notificator.notified()) => {},
552
553                    _ = metadata_input.ready() => {
554                        // Collect all pending updates, then only keep the most recent one.
555                        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                    // This future is not cancel safe but we are only passing a reference to it in
568                    // the select! loop so the future stays on the stack and never gets cancelled
569                    // until the end of the function.
570                    _ = 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 we are snapshotting, record our first set of partitions as the snapshot
617                        // size.
618                        if !snapshot_export_stats.is_empty() && snapshot_total.is_none() {
619                            // Note that we want to represent the _number of offsets_, which
620                            // means the watermark's frontier semantics is correct, without
621                            // subtracting (Kafka offsets start at 0).
622                            snapshot_total = Some(offset_known);
623                        }
624
625                        // Clear all the health namespaces we know about.
626                        // Note that many kafka sources's don't have an ssh tunnel, but the
627                        // `health_operator` handles this fine.
628                        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                // Poll the consumer once. We split the consumer's partitions out into separate
729                // queues and poll those individually, but it's still necessary to drive logic that
730                // consumes from rdkafka's internal event queue, such as statistics callbacks.
731                //
732                // Additionally, assigning topics and splitting them off into separate queues is
733                // not atomic, so we expect to see at least some messages to show up when polling
734                // the consumer directly.
735                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                                // This vec allocation is required to allow obtaining a `&mut`
777                                // on `reader` for the `reader.handle_message` call in the
778                                // loop below since  `message` is borrowed from `reader`.
779                                .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                // Take the consumers temporarily to get around borrow checker errors
806                let mut consumers = std::mem::take(&mut reader.partition_consumers);
807                for consumer in consumers.iter_mut() {
808                    let pid = consumer.pid();
809                    // We want to make sure the rest of the actions in the outer loops get
810                    // a chance to run. If rdkafka keeps pumping data at us we might find
811                    // ourselves in a situation where we keep dumping data into the
812                    // dataflow without signaling progress. For this reason we consume at most
813                    // 10k messages from each partition and go around the loop.
814                    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                                // The message was from an offset we've already seen.
849                                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                // We can now put them back
892                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                    // The offset begins in the `Offset::Invalid` state in which case we simply
904                    // skip this partition.
905                    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                                    // The `.position()` of the consumer represents what offset we have
916                                    // read up to.
917                                    snapshot_staged += offset.try_into().unwrap_or(0u64);
918                                    // This will always be `Some` at this point.
919                                    if let Some(snapshot_total) = snapshot_total {
920                                        // We will eventually read past the snapshot total, so we need
921                                        // to bound it here.
922                                        snapshot_staged =
923                                            std::cmp::min(snapshot_staged, snapshot_total);
924                                    }
925                                }
926                            }
927                            Err(_) => {
928                                // If we can't downgrade, it means we have already seen this offset.
929                                // This is expected and we can safely ignore it.
930                                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    /// Reports each export's `offset_committed` and commits the source upper's offsets upstream
968    /// when they differ from the last successful commit.
969    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                // Note that we do not subtract 1 from the frontier. Imagine
976                // that frontier is 2 for this pid. That means we have
977                // full processed offset 0 and offset 1, which means we have
978                // processed _2_ offsets.
979                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        // `uppers` changes whenever any export's upper moves, usually leaving this worker's
992        // offsets unchanged, and each commit is a synchronous round trip to the group coordinator.
993        if offsets.is_empty() || offsets == self.committed_offsets {
994            return Ok(());
995        }
996        // Without a consumer the offsets stay uncommitted, and a later upper retries them.
997        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    /// Recommits the offsets of the last successful commit. The consumer only ever `assign`s, so
1006    /// the broker treats its group as standalone and expires a partition's offset
1007    /// `offsets.retention.minutes` after that partition's last commit, current or not.
1008    async fn refresh(&self) -> Result<(), anyhow::Error> {
1009        // Offsets are only ever committed through a consumer.
1010        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    /// The offsets of `frontier` in the partitions this worker is responsible for.
1043    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    /// Ensures that a partition queue for `pid` exists.
1056    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            // early exit if we've already inserted this partition
1067            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    /// Creates a new partition queue for `partition_id`.
1083    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        // Collect old partition assignments
1099        let tpl = consumer.assignment().unwrap();
1100        // Create list from assignments
1101        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        // Add new partition
1108        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        // Since librdkafka v1.6.0, we need to recreate all partition queues
1116        // after every call to `consumer.assign`.
1117        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    /// Read any statistics JSON blobs generated via the rdkafka statistics callback.
1145    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    /// Checks if the given message is viable for emission. This checks if the message offset is
1168    /// past the expected offset and returns None if it is not.
1169    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        // Offsets are guaranteed to be 1) monotonically increasing *unless* there is
1180        // a network issue or a new partition added, at which point the consumer may
1181        // start processing the topic from the beginning, or we may see duplicate offsets
1182        // At all times, the guarantee : if we see offset x, we have seen all offsets [0,x-1]
1183        // that we are ever going to see holds.
1184        // Offsets are guaranteed to be contiguous when compaction is disabled. If compaction
1185        // is enabled, there may be gaps in the sequence.
1186        // If we see an "old" offset, we skip that message.
1187
1188        // Given the explicit consumer to partition assignment, we should never receive a message
1189        // for a partition for which we have no metadata
1190        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            // We explicitly should not consume the message as we have already processed it.
1222            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                            //abort with a definite error when the header is not found or cannot be parsed correctly
1297                            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
1342/// Wrapper around a partition containing the underlying consumer
1343struct PartitionConsumer {
1344    /// the partition id with which this consumer is associated
1345    pid: PartitionId,
1346    /// The underlying Kafka partition queue
1347    partition_queue: PartitionQueue<TunnelingClientContext<GlueConsumerContext>>,
1348}
1349
1350impl PartitionConsumer {
1351    /// Creates a new partition consumer from underlying Kafka consumer
1352    fn new(
1353        pid: PartitionId,
1354        partition_queue: PartitionQueue<TunnelingClientContext<GlueConsumerContext>>,
1355    ) -> Self {
1356        PartitionConsumer {
1357            pid,
1358            partition_queue,
1359        }
1360    }
1361
1362    /// Returns the next message to process for this partition (if any).
1363    ///
1364    /// The outer `Result` represents irrecoverable failures, the inner one can and will
1365    /// be transformed into empty values.
1366    ///
1367    /// The inner `Option` represents if there is a message to process.
1368    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    /// Return the partition id for this PartitionConsumer
1377    fn pid(&self) -> PartitionId {
1378        self.pid
1379    }
1380}
1381
1382/// An implementation of [`ConsumerContext`] that forwards statistics to the
1383/// worker
1384struct 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    // The shape of the rdkafka *Context traits require us to forward to the `MzClientContext`
1404    // implementation.
1405    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
1436/// Creates a reader worker's consumer, possibly long after the operator started, and reports
1437/// failures to the operator's outputs.
1438struct 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    /// `(id, output_index)` of each source export.
1448    outputs: Vec<(GlobalId, usize)>,
1449    data_output: DataOutput,
1450    health_output: HealthOutput,
1451    health_cap: Capability<KafkaTimestamp>,
1452}
1453
1454impl ReaderConsumerSetup {
1455    /// Returns the consumer in `slot`, creating it first if needed.
1456    ///
1457    /// On creation, checks `start_offsets`, the start offsets of the partitions this worker is
1458    /// responsible for, against the topic's low watermarks, and adds the low watermark for
1459    /// responsible partitions without a start offset. Returns `Break` after emitting a definite
1460    /// error when a start offset has been compacted away, after which the reader must stop. Never
1461    /// returns if the consumer cannot be created.
1462    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                // IMPORTANT: wedge forever until the `SuspendAndRestart` is processed.
1476                // Returning would incorrectly present to the remap operator as progress to the
1477                // empty frontier which would be incorrectly recorded to the remap shard.
1478                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                    // Disable Kafka auto commit. We manually commit offsets
1502                    // to Kafka once we have reclocked those offsets, so
1503                    // that users can use standard Kafka tools for progress
1504                    // tracking.
1505                    "enable.auto.commit" => "false".into(),
1506                    // Always begin ingest at 0 when restarted, even if Kafka
1507                    // contains committed consumer read offsets
1508                    "auto.offset.reset" => "earliest".into(),
1509                    // Use the user-configured topic metadata refresh
1510                    // interval.
1511                    "topic.metadata.refresh.interval.ms" =>
1512                        self.topic_metadata_refresh_interval
1513                        .as_millis()
1514                        .to_string(),
1515                    // TODO: document the rationale for this.
1516                    "fetch.message.max.bytes" => "134217728".into(),
1517                    // Consumer group ID, which may have been overridden by
1518                    // the user. librdkafka requires this, and we use offset
1519                    // committing to provide a way for users to monitor
1520                    // ingest progress, though we do not rely on the
1521                    // committed offsets for any functionality.
1522                    "group.id" => self.group_id.clone(),
1523                    // Allow Kafka monitoring tools to identify this
1524                    // consumer.
1525                    "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    /// Gives `update` for the source and each of its exports.
1550    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    /// Gives `error` to each export at the time of `data_cap`.
1565    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, // fetch the low watermark
1602        ) {
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                    // If the topic doesn't exist, that is a definite error
1625                    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 a start offset exists for this partition, then either the user specified it
1639            // or we restored it from the resume upper. In either case, if the low watermark is
1640            // greater than the start offset, we know for certain that the offset we need to
1641            // start at has been compacted away or dropped from retention by Kafka. If there
1642            // is no start offset, then we set it to the low watermark and start consuming from
1643            // there, assuming the user doesn't care about the messages that have been compacted away.
1644            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    // Splitting off a partition queue with an `Offset` that is not `Offset::Beginning` seems to
1708    // lead to a race condition where sometimes we receive messages from polling the main consumer
1709    // instead of on the partition queue. This can be surfaced by running the test in a loop (in
1710    // the dataflow directory) using:
1711    //
1712    // cargo stress --lib --release source::kafka::tests::reproduce_kafka_queue_issue
1713    //
1714    // cargo-stress can be installed via `cargo install cargo-stress`
1715    //
1716    // You need to set up a topic "queue-test" with 1000 "hello" messages in it. Obviously, running
1717    // this test requires a running Kafka instance at localhost:9092.
1718    #[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        // Using Offset:Beginning here will work fine, only Offset:Offset(0) leads to the race
1735        // condition.
1736        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
1793/// Fetches the list of partitions and their corresponding high watermark.
1794fn 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/// An update produced by the metadata fetcher.
1825#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1826enum MetadataUpdate {
1827    /// The current IDs and high watermarks of all topic partitions.
1828    Partitions(BTreeMap<PartitionId, PartitionWatermark>),
1829    /// A transient error.
1830    ///
1831    /// Transient errors stall the source until their cause has been resolved.
1832    TransientError(HealthStatus),
1833    /// A definite error.
1834    ///
1835    /// Definite errors cannot be recovered from. They poison the source until the end of time.
1836    DefiniteError(SourceError),
1837}
1838
1839impl MetadataUpdate {
1840    /// Return the upstream frontier resulting from the metadata update, if any.
1841    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
1877/// Render the metadata fetcher of a Kafka source.
1878///
1879/// The metadata fetcher is a single-worker operator that is responsible for periodically fetching
1880/// the Kafka topic metadata (partition IDs and high watermarks) and making it available as a
1881/// Timely stream.
1882fn 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                    // Use the user-configured topic metadata refresh
1934                    // interval.
1935                    "topic.metadata.refresh.interval.ms" =>
1936                        topic_metadata_refresh_interval
1937                        .as_millis()
1938                        .to_string(),
1939                    // Allow Kafka monitoring tools to identify this
1940                    // consumer.
1941                    "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                // IMPORTANT: wedge forever until the `SuspendAndRestart` is processed.
1964                // Returning would incorrectly present to the remap operator as progress to the
1965                // empty frontier which would be incorrectly recorded to the remap shard.
1966                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                // Topics are identified by name but it's possible that a user recreates a topic
1983                // with the same name. Ideally we'd want to catch all of these cases and
1984                // immediately error out the source, since the data is effectively gone.
1985                // Unfortunately this is not possible without something like KIP-516.
1986                //
1987                // The best we can do is check whether the upstream frontier regressed. This tells
1988                // us that the topic was recreated and now contains fewer offsets and/or fewer
1989                // partitions. Note that we are not able to detect topic recreation if neither of
1990                // the two are true.
1991                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    // Linux thread names are limited to 15 characters. Use a truncated ID to fit the name.
2024    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}