Skip to main content

mz_storage/sink/
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
10//! Code to render the sink dataflow of a [`KafkaSinkConnection`]. The dataflow consists
11//! of two operators in order to take advantage of all the available workers.
12//!
13//! ```text
14//!        ┏━━━━━━━━━━━━━━┓
15//!        ┃   persist    ┃
16//!        ┃    source    ┃
17//!        ┗━━━━━━┯━━━━━━━┛
18//!               │ stream of arrangement batches (trace reader dropped)
19//!               │
20//!        ┏━━━━━━v━━━━━━┓
21//!        ┃    row      ┃ walks each batch's cursor and emits one
22//!        ┃   encoder   ┃ encoded `KafkaMessage` per DiffPair
23//!        ┗━━━━━━┯━━━━━━┛
24//!               │ encoded data
25//!               │
26//!        ┏━━━━━━v━━━━━━┓
27//!        ┃    kafka    ┃ (single worker)
28//!        ┃    sink     ┃
29//!        ┗━━┯━━━━━━━━┯━┛
30//!   records │        │ uppers
31//!      ╭────v──╮ ╭───v──────╮
32//!      │ data  │ │ progress │  <- records and uppers are produced
33//!      │ topic │ │  topic   │     transactionally to both topics
34//!      ╰───────╯ ╰──────────╯
35//! ```
36//!
37//! # Encoding
38//!
39//! One part of the dataflow deals with encoding the rows that we read from persist. The encoder
40//! walks the input arrangement's batches via
41//! [`mz_interchange::envelopes::for_each_diff_pair`], producing one encoded `KafkaMessage` per
42//! `DiffPair` observed at each `(key, timestamp)`. An initialization step first ensures that the
43//! schemas are published to the Schema Registry.
44//!
45//! # Sinking
46//!
47//! The other part of the dataflow, and what this module mostly deals with, is interacting with the
48//! Kafka cluster in order to transactionally commit batches (sets of records associated with a
49//! frontier). All the processing happens in a single worker and so all previously encoded records
50//! go through an exchange in order to arrive at the chosen worker. We may be able to improve this
51//! in the future by committing disjoint partitions of the key space for independent workers but
52//! for now we do the simple thing.
53//!
54//! ## Retries
55//!
56//! All of the retry logic heavy lifting is offloaded to `librdkafka` since it already implements
57//! the required behavior[1]. In particular we only ever enqueue records to its send queue and
58//! eventually call `commit_transaction` which will ensure that all queued messages are
59//! successfully delivered before the transaction is reported as committed.
60//!
61//! The only error that is possible during sending is that the queue is full. We are purposefully
62//! NOT handling this error and simply configure `librdkafka` with a very large queue. The reason
63//! for this choice is that the only choice for hanlding such an error ourselves would be to queue
64//! it, and there isn't a good argument about two small queues being better than one big one. If we
65//! reach the queue limit we simply error out the entire sink dataflow and start over.
66//!
67//! # Error handling
68//!
69//! Both the encoding operator and the sinking operator can produce a transient error that is wired
70//! up with our health monitoring and will trigger a restart of the sink dataflow.
71//!
72//! [1]: https://github.com/confluentinc/librdkafka/blob/master/INTRODUCTION.md#message-reliability
73
74use std::cell::RefCell;
75use std::cmp::Ordering;
76use std::collections::BTreeMap;
77use std::future::Future;
78use std::rc::Rc;
79use std::sync::atomic::AtomicU64;
80use std::sync::{Arc, Weak};
81use std::time::Duration;
82
83use anyhow::{Context, anyhow, bail};
84use differential_dataflow::{AsCollection, Hashable, VecCollection};
85use futures::StreamExt;
86use maplit::btreemap;
87use mz_expr::{Eval, MirScalarExpr};
88use mz_interchange::avro::{AvroEncoder, AvroSchemaId};
89use mz_interchange::encode::Encode;
90use mz_interchange::envelopes::{dbz_format, for_each_diff_pair};
91use mz_interchange::json::JsonEncoder;
92use mz_interchange::text_binary::{BinaryEncoder, TextEncoder};
93use mz_kafka_util::admin::EnsureTopicConfig;
94use mz_kafka_util::client::{
95    DEFAULT_FETCH_METADATA_TIMEOUT, GetPartitionsError, MzClientContext, TimeoutConfig,
96    TunnelingClientContext,
97};
98use mz_ore::cast::CastFrom;
99use mz_ore::collections::CollectionExt;
100use mz_ore::error::ErrorExt;
101use mz_ore::future::InTask;
102use mz_ore::soft_assert_or_log;
103use mz_ore::task::{self, AbortOnDropHandle};
104use mz_persist_client::Diagnostics;
105use mz_persist_client::write::WriteHandle;
106use mz_persist_types::codec_impls::UnitSchema;
107use mz_repr::{Datum, DatumVec, Diff, GlobalId, RelationDesc, Row, RowArena, Timestamp};
108use mz_storage_client::sink::progress_key::ProgressKey;
109use mz_storage_types::StorageDiff;
110use mz_storage_types::configuration::StorageConfiguration;
111use mz_storage_types::controller::CollectionMetadata;
112use mz_storage_types::dyncfgs::{
113    KAFKA_BUFFERED_EVENT_RESIZE_THRESHOLD_ELEMENTS, KAFKA_SINK_BATCH_NUM_MESSAGES,
114    KAFKA_SINK_BATCH_SIZE, KAFKA_SINK_EMIT_SINK_ID_HEADER, KAFKA_SINK_MESSAGE_MAX_BYTES,
115    SINK_ENSURE_TOPIC_CONFIG, SINK_PROGRESS_SEARCH,
116};
117use mz_storage_types::errors::{ContextCreationError, ContextCreationErrorExt, DataflowError};
118use mz_storage_types::sinks::{
119    KafkaSinkConnection, KafkaSinkFormatType, SinkEnvelope, StorageSinkDesc,
120};
121use mz_storage_types::sources::SourceData;
122use mz_storage_types::wire_format::WireFormat;
123use mz_timely_util::antichain::AntichainExt;
124use mz_timely_util::builder_async::{
125    Event, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
126};
127use rdkafka::consumer::{BaseConsumer, Consumer, ConsumerContext};
128use rdkafka::error::KafkaError;
129use rdkafka::message::{Header, OwnedHeaders, ToBytes};
130use rdkafka::producer::{BaseRecord, Producer, ThreadedProducer};
131use rdkafka::types::RDKafkaErrorCode;
132use rdkafka::{Message, Offset, Statistics, TopicPartitionList};
133use serde::{Deserialize, Deserializer, Serialize, Serializer};
134use timely::PartialOrder;
135use timely::container::CapacityContainerBuilder;
136use timely::dataflow::StreamVec;
137use timely::dataflow::channels::pact::{Exchange, Pipeline};
138use timely::dataflow::operators::vec::{Map, ToStream};
139use timely::dataflow::operators::{CapabilitySet, Concatenate};
140use timely::progress::{Antichain, Timestamp as _};
141use tokio::sync::watch;
142use tokio::time::{self, MissedTickBehavior};
143use tracing::{debug, error, info, warn};
144
145use crate::healthcheck::{HealthStatusMessage, HealthStatusUpdate, StatusNamespace};
146use crate::metrics::sink::kafka::KafkaSinkMetrics;
147use crate::render::sinks::{PkViolationWarner, SinkBatchStream, SinkRender};
148use crate::statistics::SinkStatistics;
149use crate::storage_state::StorageState;
150
151impl<'scope> SinkRender<'scope> for KafkaSinkConnection {
152    fn get_key_indices(&self) -> Option<&[usize]> {
153        self.key_desc_and_indices
154            .as_ref()
155            .map(|(_desc, indices)| indices.as_slice())
156    }
157
158    fn get_relation_key_indices(&self) -> Option<&[usize]> {
159        self.relation_key_indices.as_deref()
160    }
161
162    fn render_sink(
163        &self,
164        storage_state: &mut StorageState,
165        sink: &StorageSinkDesc<CollectionMetadata, Timestamp>,
166        sink_id: GlobalId,
167        batches: SinkBatchStream<'scope>,
168        key_is_synthetic: bool,
169        // TODO(benesch): errors should stream out through the sink,
170        // if we figure out a protocol for that.
171        _err_collection: VecCollection<'scope, Timestamp, DataflowError, Diff>,
172    ) -> (
173        StreamVec<'scope, Timestamp, HealthStatusMessage>,
174        Vec<PressOnDropButton>,
175    ) {
176        let scope = batches.scope();
177
178        let write_handle = {
179            let persist = Arc::clone(&storage_state.persist_clients);
180            let shard_meta = sink.to_storage_metadata.clone();
181            async move {
182                let client = persist.open(shard_meta.persist_location).await?;
183                let handle = client
184                    .open_writer(
185                        shard_meta.data_shard,
186                        Arc::new(shard_meta.relation_desc),
187                        Arc::new(UnitSchema),
188                        Diagnostics::from_purpose("sink handle"),
189                    )
190                    .await?;
191                Ok(handle)
192            }
193        };
194
195        let write_frontier = Rc::new(RefCell::new(Antichain::from_elem(Timestamp::minimum())));
196        storage_state
197            .sink_write_frontiers
198            .insert(sink_id, Rc::clone(&write_frontier));
199
200        let (encoded, encode_status, encode_token) = encode_collection(
201            format!("kafka-{sink_id}-{}-encode", self.format.get_format_name()),
202            batches,
203            sink.envelope,
204            self.clone(),
205            storage_state.storage_configuration.clone(),
206            sink_id,
207            sink.from,
208            key_is_synthetic,
209        );
210
211        let metrics = storage_state.metrics.get_kafka_sink_metrics(sink_id);
212        let statistics = storage_state
213            .aggregated_statistics
214            .get_sink(&sink_id)
215            .expect("statistics initialized")
216            .clone();
217
218        let (sink_status, sink_token) = sink_collection(
219            format!("kafka-{sink_id}-sink"),
220            encoded,
221            sink_id,
222            self.clone(),
223            storage_state.storage_configuration.clone(),
224            sink,
225            metrics,
226            statistics,
227            write_handle,
228            write_frontier,
229        );
230
231        let running_status = Some(HealthStatusMessage {
232            id: None,
233            update: HealthStatusUpdate::Running,
234            namespace: StatusNamespace::Kafka,
235        })
236        .to_stream(scope);
237
238        let status = scope.concatenate([running_status, encode_status, sink_status]);
239
240        (status, vec![encode_token, sink_token])
241    }
242}
243
244struct TransactionalProducer {
245    /// The task name used for any blocking calls spawned onto the tokio threadpool.
246    task_name: String,
247    /// The topic where all the updates go.
248    data_topic: String,
249    /// The topic where all the upper frontiers go.
250    progress_topic: String,
251    /// The key each progress record is associated with.
252    progress_key: ProgressKey,
253    /// The version of this sink, used to fence out previous versions from writing.
254    sink_version: u64,
255    /// The `materialize-sink-id` header value, when the header is enabled.
256    sink_id_header: Option<String>,
257    /// The number of partitions in the target topic.
258    partition_count: Arc<AtomicU64>,
259    /// A task to periodically refresh the partition count.
260    _partition_count_task: AbortOnDropHandle<()>,
261    /// The underlying Kafka producer.
262    producer: ThreadedProducer<TunnelingClientContext<MzClientContext>>,
263    /// A handle to the metrics associated with this sink.
264    statistics: SinkStatistics,
265    /// The number of messages staged for the currently open transactions. It is reset to zero
266    /// every time a transaction commits.
267    staged_messages: u64,
268    /// The total number bytes staged for the currently open transactions. It is reset to zero
269    /// every time a transaction commits.
270    staged_bytes: u64,
271    /// The timeout to use for network operations.
272    socket_timeout: Duration,
273    /// The timeout to use for committing transactions.
274    transaction_timeout: Duration,
275}
276
277impl TransactionalProducer {
278    /// Initializes a transcational producer for the sink identified by `sink_id`. After this call
279    /// returns it is guranteed that all previous `TransactionalProducer` instances for the same
280    /// sink have been fenced out (i.e `init_transations()` has been called successfully).
281    async fn new(
282        sink_id: GlobalId,
283        connection: &KafkaSinkConnection,
284        storage_configuration: &StorageConfiguration,
285        metrics: Arc<KafkaSinkMetrics>,
286        statistics: SinkStatistics,
287        sink_version: u64,
288    ) -> Result<(Self, Antichain<mz_repr::Timestamp>), ContextCreationError> {
289        let client_id = connection.client_id(
290            storage_configuration.config_set(),
291            &storage_configuration.connection_context,
292            sink_id,
293        );
294        let transactional_id =
295            connection.transactional_id(&storage_configuration.connection_context, sink_id);
296
297        let timeout_config = &storage_configuration.parameters.kafka_timeout_config;
298        let mut options = BTreeMap::new();
299        // Ensure that messages are sinked in order and without duplicates. Note that this only
300        // applies to a single instance of a producer - in the case of restarts, all bets are off
301        // and full exactly once support is required.
302        options.insert("enable.idempotence", "true".into());
303        // Use the compression type requested by the user.
304        options.insert(
305            "compression.type",
306            connection.compression_type.to_librdkafka_option().into(),
307        );
308        // Set the maximum buffer size limit. We don't want to impose anything lower than the max
309        // here as the operator has nothing better to do with the data than to buffer them.
310        options.insert("queue.buffering.max.kbytes", "2147483647".into());
311        // Disable the default buffer limit of 100k messages. We don't want to impose any limit
312        // here as the operator has nothing better to do with the data than to buffer them.
313        options.insert("queue.buffering.max.messages", "0".into());
314        // Make the Kafka producer wait at least 10 ms before sending out MessageSets
315        options.insert("queue.buffering.max.ms", format!("{}", 10));
316        // Time out transactions after `transaction_timeout`.
317        options.insert(
318            "transaction.timeout.ms",
319            format!("{}", timeout_config.transaction_timeout.as_millis()),
320        );
321        // Use the transactional ID requested by the user.
322        options.insert("transactional.id", transactional_id);
323        // Allow Kafka monitoring tools to identify this producer.
324        options.insert("client.id", client_id);
325        // We want to be notified regularly with statistics
326        options.insert("statistics.interval.ms", "1000".into());
327        // Maximum size of a single produced message, controlled by dyncfg so
328        // operators can raise the limit when messages exceed librdkafka's 1MB
329        // default.
330        options.insert(
331            "message.max.bytes",
332            format!(
333                "{}",
334                KAFKA_SINK_MESSAGE_MAX_BYTES.get(storage_configuration.config_set())
335            ),
336        );
337        // Maximum size (bytes) of a produced MessageSet.
338        options.insert(
339            "batch.size",
340            format!(
341                "{}",
342                KAFKA_SINK_BATCH_SIZE.get(storage_configuration.config_set())
343            ),
344        );
345        // Maximum number of messages batched in one MessageSet.
346        options.insert(
347            "batch.num.messages",
348            format!(
349                "{}",
350                KAFKA_SINK_BATCH_NUM_MESSAGES.get(storage_configuration.config_set())
351            ),
352        );
353
354        let ctx = MzClientContext::default();
355
356        let stats_receiver = ctx.subscribe_statistics();
357        let task_name = format!("kafka_sink_metrics_collector:{sink_id}");
358        task::spawn(
359            || &task_name,
360            collect_statistics(stats_receiver, Arc::clone(&metrics)),
361        );
362
363        let producer: ThreadedProducer<_> = connection
364            .connection
365            .create_with_context(storage_configuration, ctx, &options, InTask::Yes)
366            .await?;
367
368        // The partition count is fixed up after we ensure the topic exists.
369        let partition_count = Arc::new(AtomicU64::new(0));
370        let update_partition_count = {
371            let partition_count = Arc::clone(&partition_count);
372            let metrics = Arc::clone(&metrics);
373            Arc::new(move |pc| {
374                partition_count.store(pc, std::sync::atomic::Ordering::SeqCst);
375                metrics.partition_count.set(pc);
376            })
377        };
378
379        // Start a task that will keep the partition count up to date in the
380        // background.
381        let partition_count_task = task::spawn(
382            || format!("kafka_sink_producer_fetch_metadata_loop:{sink_id}"),
383            fetch_partition_count_loop(
384                producer.clone(),
385                sink_id,
386                connection.topic.clone(),
387                connection.topic_metadata_refresh_interval,
388                Arc::clone(&update_partition_count),
389            ),
390        );
391
392        let task_name = format!("kafka_sink_producer:{sink_id}");
393        let progress_key = ProgressKey::new(sink_id);
394
395        let producer = Self {
396            task_name,
397            data_topic: connection.topic.clone(),
398            partition_count,
399            _partition_count_task: partition_count_task.abort_on_drop(),
400            progress_topic: connection
401                .progress_topic(&storage_configuration.connection_context)
402                .into_owned(),
403            progress_key,
404            sink_version,
405            sink_id_header: KAFKA_SINK_EMIT_SINK_ID_HEADER
406                .get(storage_configuration.config_set())
407                .then(|| sink_id.to_string()),
408            producer,
409            statistics,
410            staged_messages: 0,
411            staged_bytes: 0,
412            socket_timeout: timeout_config.socket_timeout,
413            transaction_timeout: timeout_config.transaction_timeout,
414        };
415
416        let timeout = timeout_config.socket_timeout;
417        producer
418            .spawn_blocking(move |p| p.init_transactions(timeout))
419            .await?;
420
421        // We have just called init_transactions, which means that we have fenced out all previous
422        // transactional producers, making it safe to determine the resume upper.
423        let progress = determine_sink_progress(
424            sink_id,
425            connection,
426            storage_configuration,
427            Arc::clone(&metrics),
428        )
429        .await?;
430
431        let resume_upper = match progress {
432            Some(progress) => {
433                if sink_version < progress.version {
434                    return Err(ContextCreationError::Other(anyhow!(
435                        "Fenced off by newer version of the sink. ours={} theirs={}",
436                        sink_version,
437                        progress.version
438                    )));
439                }
440                progress.frontier
441            }
442            None => {
443                mz_storage_client::sink::ensure_kafka_topic(
444                    connection,
445                    storage_configuration,
446                    &connection.topic,
447                    &connection.topic_options,
448                    EnsureTopicConfig::Skip,
449                )
450                .await?;
451                Antichain::from_elem(Timestamp::minimum())
452            }
453        };
454
455        // At this point the topic must exist and so we can query for its
456        // partition count. Even though we have a background task to fetch the
457        // partition count, we do this synchronously to ensure we don't attempt
458        // to produce any messages with our initial partition count of 0.
459        let partition_count =
460            fetch_partition_count(&producer.producer, sink_id, &connection.topic).await?;
461        update_partition_count(partition_count);
462
463        Ok((producer, resume_upper))
464    }
465
466    /// Runs the blocking operation `f` on the producer in the tokio threadpool and checks for SSH
467    /// status in case of failure.
468    async fn spawn_blocking<F, R>(&self, f: F) -> Result<R, ContextCreationError>
469    where
470        F: FnOnce(
471                ThreadedProducer<TunnelingClientContext<MzClientContext>>,
472            ) -> Result<R, KafkaError>
473            + Send
474            + 'static,
475        R: Send + 'static,
476    {
477        let producer = self.producer.clone();
478        task::spawn_blocking(|| &self.task_name, move || f(producer))
479            .await
480            .check_ssh_status(self.producer.context())
481    }
482
483    async fn begin_transaction(&self) -> Result<(), ContextCreationError> {
484        self.spawn_blocking(|p| p.begin_transaction()).await
485    }
486
487    /// Synchronously puts the provided message to librdkafka's send queue. This method only
488    /// returns an error if the queue is full. Handling this error by buffering the message and
489    /// retrying is equivalent to adjusting the maximum number of queued items in rdkafka so it is
490    /// adviced that callers only handle this error in order to apply backpressure to the rest of
491    /// the system.
492    fn send(
493        &mut self,
494        message: &KafkaMessage,
495        time: Timestamp,
496        diff: Diff,
497    ) -> Result<(), KafkaError> {
498        assert_eq!(diff, Diff::ONE, "invalid sink update");
499
500        let mut headers = OwnedHeaders::new().insert(Header {
501            key: "materialize-timestamp",
502            value: Some(time.to_string().as_bytes()),
503        });
504        if let Some(sink_id) = &self.sink_id_header {
505            headers = headers.insert(Header {
506                key: "materialize-sink-id",
507                value: Some(sink_id.as_bytes()),
508            });
509        }
510        for header in &message.headers {
511            // Headers that start with `materialize-` are reserved for our
512            // internal use, so we silently drop any such user-specified
513            // headers. While this behavior is documented, it'd be a nicer UX to
514            // send a warning or error somewhere. Unfortunately sinks don't have
515            // anywhere user-visible to send errors. See database-issues#5148.
516            if header.key.starts_with("materialize-") {
517                continue;
518            }
519
520            headers = headers.insert(Header {
521                key: header.key.as_str(),
522                value: header.value.as_ref(),
523            });
524        }
525
526        let pc = self
527            .partition_count
528            .load(std::sync::atomic::Ordering::SeqCst);
529        let partition = Some(i32::try_from(message.hash % pc).unwrap());
530
531        let record = BaseRecord {
532            topic: &self.data_topic,
533            key: message.key.as_ref(),
534            payload: message.value.as_ref(),
535            headers: Some(headers),
536            partition,
537            timestamp: None,
538            delivery_opaque: (),
539        };
540        let key_size = message.key.as_ref().map(|k| k.len()).unwrap_or(0);
541        let value_size = message.value.as_ref().map(|k| k.len()).unwrap_or(0);
542        let headers_size = message
543            .headers
544            .iter()
545            .map(|h| h.key.len() + h.value.as_ref().map(|v| v.len()).unwrap_or(0))
546            .sum::<usize>();
547        let record_size = u64::cast_from(key_size + value_size + headers_size);
548        self.statistics.inc_messages_staged_by(1);
549        self.staged_messages += 1;
550        self.statistics.inc_bytes_staged_by(record_size);
551        self.staged_bytes += record_size;
552        self.producer.send(record).map_err(|(e, _)| e)
553    }
554
555    /// Commits all the staged updates of the currently open transaction plus a progress record
556    /// describing `upper` to the progress topic.
557    async fn commit_transaction(
558        &mut self,
559        upper: Antichain<Timestamp>,
560    ) -> Result<(), ContextCreationError> {
561        let progress = ProgressRecord {
562            frontier: upper,
563            version: self.sink_version,
564        };
565        let payload = serde_json::to_vec(&progress).expect("infallible");
566        let record = BaseRecord::to(&self.progress_topic)
567            .payload(&payload)
568            .key(&self.progress_key);
569        self.producer.send(record).map_err(|(e, _)| e)?;
570
571        fail::fail_point!("kafka_sink_commit_transaction");
572
573        let timeout = self.transaction_timeout;
574        match self
575            .spawn_blocking(move |p| p.commit_transaction(timeout))
576            .await
577        {
578            Ok(()) => {
579                self.statistics
580                    .inc_messages_committed_by(self.staged_messages);
581                self.statistics.inc_bytes_committed_by(self.staged_bytes);
582                self.staged_messages = 0;
583                self.staged_bytes = 0;
584                Ok(())
585            }
586            Err(ContextCreationError::KafkaError(KafkaError::Transaction(err))) => {
587                // Make one attempt at aborting the transaction before letting the error percolate
588                // up and the process exit. Aborting allows the consumers of the topic to skip over
589                // any messages we've written in the transaction, so it's polite to do... but if it
590                // fails, the transaction will be aborted either when fenced out by a future
591                // version of this producer or by the broker-side timeout.
592                if err.txn_requires_abort() {
593                    let timeout = self.socket_timeout;
594                    self.spawn_blocking(move |p| p.abort_transaction(timeout))
595                        .await?;
596                }
597                Err(ContextCreationError::KafkaError(KafkaError::Transaction(
598                    err,
599                )))
600            }
601            Err(err) => Err(err),
602        }
603    }
604}
605
606/// Listens for statistics updates from librdkafka and updates our Prometheus metrics.
607async fn collect_statistics(
608    mut receiver: watch::Receiver<Statistics>,
609    metrics: Arc<KafkaSinkMetrics>,
610) {
611    while receiver.changed().await.is_ok() {
612        // The librdkafka per-broker statistics are either running totals (counters)
613        // or point-in-time values (gauges). In both cases we aggregate across brokers
614        // by re-summing from scratch each interval, so these accumulators are reset
615        // every time statistics are fetched.
616        //
617        // see <https://docs.confluent.io/platform/current/clients/librdkafka/html/md_STATISTICS.html#autotoc_md121>
618        let mut outbuf_cnt: i64 = 0;
619        let mut outbuf_msg_cnt: i64 = 0;
620        let mut waitresp_cnt: i64 = 0;
621        let mut waitresp_msg_cnt: i64 = 0;
622        let mut txerrs: u64 = 0;
623        let mut txretries: u64 = 0;
624        let mut req_timeouts: u64 = 0;
625        let mut connects: i64 = 0;
626        let mut disconnects: i64 = 0;
627
628        let stats = receiver.borrow();
629        for broker in stats.brokers.values() {
630            outbuf_cnt += broker.outbuf_cnt;
631            outbuf_msg_cnt += broker.outbuf_msg_cnt;
632            waitresp_cnt += broker.waitresp_cnt;
633            waitresp_msg_cnt += broker.waitresp_msg_cnt;
634            txerrs += broker.txerrs;
635            txretries += broker.txretries;
636            req_timeouts += broker.req_timeouts;
637            connects += broker.connects.unwrap_or(0);
638            disconnects += broker.disconnects.unwrap_or(0);
639        }
640        metrics.rdkafka_msg_cnt.set(stats.msg_cnt);
641        metrics.rdkafka_msg_size.set(stats.msg_size);
642        metrics.rdkafka_txmsgs.set(stats.txmsgs);
643        metrics.rdkafka_txmsg_bytes.set(stats.txmsg_bytes);
644        metrics.rdkafka_tx.set(stats.tx);
645        metrics.rdkafka_tx_bytes.set(stats.tx_bytes);
646        metrics.rdkafka_outbuf_cnt.set(outbuf_cnt);
647        metrics.rdkafka_outbuf_msg_cnt.set(outbuf_msg_cnt);
648        metrics.rdkafka_waitresp_cnt.set(waitresp_cnt);
649        metrics.rdkafka_waitresp_msg_cnt.set(waitresp_msg_cnt);
650        metrics.rdkafka_txerrs.set(txerrs);
651        metrics.rdkafka_txretries.set(txretries);
652        metrics.rdkafka_req_timeouts.set(req_timeouts);
653        metrics.rdkafka_connects.set(connects);
654        metrics.rdkafka_disconnects.set(disconnects);
655    }
656}
657
658/// A message to produce to Kafka.
659#[derive(Debug, Clone, Serialize, Deserialize)]
660struct KafkaMessage {
661    /// A hash of the key that can be used for partitioning.
662    hash: u64,
663    /// The message key.
664    key: Option<Vec<u8>>,
665    /// The message value.
666    value: Option<Vec<u8>>,
667    /// Message headers.
668    headers: Vec<KafkaHeader>,
669}
670
671/// A header to attach to a Kafka message.
672#[derive(Debug, Clone, Serialize, Deserialize)]
673struct KafkaHeader {
674    /// The header key.
675    key: String,
676    /// The header value.
677    value: Option<Vec<u8>>,
678}
679
680/// Sinks a collection of encoded rows to Kafka.
681///
682/// This operator exchanges all updates to a single worker by hashing on the given sink `id`.
683///
684/// Updates are sent in ascending timestamp order.
685fn sink_collection<'scope>(
686    name: String,
687    input: VecCollection<'scope, Timestamp, KafkaMessage, Diff>,
688    sink_id: GlobalId,
689    connection: KafkaSinkConnection,
690    storage_configuration: StorageConfiguration,
691    sink: &StorageSinkDesc<CollectionMetadata, Timestamp>,
692    metrics: KafkaSinkMetrics,
693    statistics: SinkStatistics,
694    write_handle: impl Future<
695        Output = anyhow::Result<WriteHandle<SourceData, (), Timestamp, StorageDiff>>,
696    > + 'static,
697    write_frontier: Rc<RefCell<Antichain<Timestamp>>>,
698) -> (
699    StreamVec<'scope, Timestamp, HealthStatusMessage>,
700    PressOnDropButton,
701) {
702    let scope = input.scope();
703    let mut builder = AsyncOperatorBuilder::new(name.clone(), input.inner.scope());
704
705    // We want exactly one worker to send all the data to the sink topic.
706    let hashed_id = sink_id.hashed();
707    let is_active_worker = usize::cast_from(hashed_id) % scope.peers() == scope.index();
708    let buffer_min_capacity =
709        KAFKA_BUFFERED_EVENT_RESIZE_THRESHOLD_ELEMENTS.handle(storage_configuration.config_set());
710
711    let mut input = builder.new_disconnected_input(input.inner, Exchange::new(move |_| hashed_id));
712
713    let as_of = sink.as_of.clone();
714    let sink_version = sink.version;
715    let (button, errors) = builder.build_fallible(move |_caps| {
716        Box::pin(async move {
717            if !is_active_worker {
718                write_frontier.borrow_mut().clear();
719                return Ok(());
720            }
721
722            fail::fail_point!("kafka_sink_creation_error", |_| Err(
723                ContextCreationError::Other(anyhow::anyhow!("synthetic error"))
724            ));
725
726            let mut write_handle = write_handle.await?;
727
728            let metrics = Arc::new(metrics);
729
730            let (mut producer, resume_upper) = TransactionalProducer::new(
731                sink_id,
732                &connection,
733                &storage_configuration,
734                Arc::clone(&metrics),
735                statistics,
736                sink_version,
737            )
738            .await?;
739
740            // The input has overcompacted if
741            let overcompacted =
742                // ..we have made some progress in the past
743                *resume_upper != [Timestamp::minimum()] &&
744                // ..but the since frontier is now beyond that
745                !PartialOrder::less_equal(&as_of, &resume_upper);
746            if overcompacted {
747                let err =
748                    super::overcompacted_error(&name, &as_of, &resume_upper, "recreate the sink");
749                // This would normally be an assertion but because it can happen after a
750                // Materialize backup/restore we log an error so that it appears on Sentry but
751                // leaves the rest of the objects in the cluster unaffected.
752                error!("{err}");
753                return Err(anyhow!("{err}").into());
754            }
755
756            info!(
757                "{name}: as_of: {}, resume upper: {}",
758                as_of.pretty(),
759                resume_upper.pretty()
760            );
761
762            // The section below relies on TotalOrder for correctness so we'll work with timestamps
763            // directly to make sure this doesn't compile if someone attempts to make this operator
764            // generic over partial orders in the future.
765            let Some(mut upper) = resume_upper.clone().into_option() else {
766                write_frontier.borrow_mut().clear();
767                return Ok(());
768            };
769
770            let mut deferred_updates = vec![];
771            let mut extra_updates = vec![];
772            // We must wait until we have data to commit before starting a transaction because
773            // Kafka doesn't have a heartbeating mechanism to keep a transaction open indefinitely.
774            // This flag tracks whether we have started the transaction.
775            let mut transaction_begun = false;
776            while let Some(event) = input.next().await {
777                match event {
778                    Event::Data(_cap, batch) => {
779                        for (message, time, diff) in batch {
780                            // We want to publish updates in time order and we know that we have
781                            // already committed all times not beyond `upper`. Therefore, if this
782                            // update happens *exactly* at upper then it is the minimum pending
783                            // time and so emitting it now will not violate the timestamp publish
784                            // order. This optimization is load bearing because it is the mechanism
785                            // by which we incrementally stream the initial snapshot out to Kafka
786                            // instead of buffering it all in memory first. This argument doesn't
787                            // hold for partially ordered time because many different timestamps
788                            // can be *exactly* at upper but we can't know ahead of time which one
789                            // will be advanced in the next progress message.
790                            match upper.cmp(&time) {
791                                Ordering::Less => deferred_updates.push((message, time, diff)),
792                                Ordering::Equal => {
793                                    if !transaction_begun {
794                                        producer.begin_transaction().await?;
795                                        transaction_begun = true;
796                                    }
797                                    producer.send(&message, time, diff)?;
798                                }
799                                Ordering::Greater => continue,
800                            }
801                        }
802                    }
803                    Event::Progress(progress) => {
804                        // Ignore progress updates before our resumption frontier
805                        if !PartialOrder::less_equal(&resume_upper, &progress) {
806                            continue;
807                        }
808                        // Also ignore progress updates until we are past the as_of frontier. This
809                        // is to avoid the following pathological scenario:
810                        // 1. Sink gets instantiated with an as_of = {10}, resume_upper = {0}.
811                        //    `progress` initially jumps at {10}, then the snapshot appears at time
812                        //    10.
813                        // 2. `progress` would normally advance to say {11} and we would commit the
814                        //    snapshot but clusterd crashes instead.
815                        // 3. A new cluster restarts the sink with an earlier as_of, say {5}. This
816                        //    is valid, the earlier as_of has strictly more information. The
817                        //    snapshot now appears at time 5.
818                        //
819                        // If we were to commit an empty transaction in step 1 and advanced the
820                        // resume_upper to {10} then in step 3 we would ignore the snapshot that
821                        // now appears at 5 completely. So it is important to only start committing
822                        // transactions after we're strictly beyond the as_of.
823                        // TODO(petrosagg): is this logic an indication of us holding something
824                        // wrong elsewhere? Investigate.
825                        // Note: !PartialOrder::less_than(as_of, progress) would not be equivalent
826                        // nor correct for partially ordered times.
827                        if !as_of.iter().all(|t| !progress.less_equal(t)) {
828                            continue;
829                        }
830                        if !transaction_begun {
831                            producer.begin_transaction().await?;
832                        }
833
834                        extra_updates.extend(
835                            deferred_updates
836                                .extract_if(.., |(_, time, _)| !progress.less_equal(time)),
837                        );
838                        // Shrink after draining items out, so the call actually
839                        // reduces capacity in the oversized-buffer scenario
840                        // (e.g. progress topic was deleted and resume upper is 0).
841                        deferred_updates.shrink_to(buffer_min_capacity.get());
842                        extra_updates.sort_unstable_by(|a, b| a.1.cmp(&b.1));
843
844                        for (message, time, diff) in extra_updates.drain(..) {
845                            producer.send(&message, time, diff)?;
846                        }
847                        extra_updates.shrink_to(buffer_min_capacity.get());
848
849                        debug!("{name}: committing transaction for {}", progress.pretty());
850                        producer.commit_transaction(progress.clone()).await?;
851                        transaction_begun = false;
852                        let mut expect_upper = write_handle.shared_upper();
853                        loop {
854                            if PartialOrder::less_equal(&progress, &expect_upper) {
855                                // The frontier has already been advanced as far as necessary.
856                                break;
857                            }
858                            // TODO(sinks): include the high water mark in the output topic for
859                            // the messages we've published, if and when we allow reads to the sink
860                            // directly, to allow monitoring the progress of the sink in terms of
861                            // the output system.
862                            const EMPTY: &[((SourceData, ()), Timestamp, StorageDiff)] = &[];
863                            match write_handle
864                                .compare_and_append(EMPTY, expect_upper, progress.clone())
865                                .await
866                                .expect("valid usage")
867                            {
868                                Ok(()) => break,
869                                Err(mismatch) => {
870                                    expect_upper = mismatch.current;
871                                }
872                            }
873                        }
874                        write_frontier.borrow_mut().clone_from(&progress);
875                        match progress.into_option() {
876                            Some(new_upper) => upper = new_upper,
877                            None => break,
878                        }
879                    }
880                }
881            }
882            Ok(())
883        })
884    });
885
886    let statuses = errors.map(|error: Rc<ContextCreationError>| {
887        let hint = match *error {
888            ContextCreationError::KafkaError(KafkaError::Transaction(ref e)) => {
889                if e.is_retriable() && e.code() == RDKafkaErrorCode::OperationTimedOut {
890                    let hint = "If you're running a single Kafka broker, ensure that the configs \
891                        transaction.state.log.replication.factor, transaction.state.log.min.isr, \
892                        and offsets.topic.replication.factor are set to 1 on the broker";
893                    Some(hint.to_owned())
894                } else {
895                    None
896                }
897            }
898            _ => None,
899        };
900
901        HealthStatusMessage {
902            id: None,
903            update: HealthStatusUpdate::halting(format!("{}", error.display_with_causes()), hint),
904            namespace: if matches!(*error, ContextCreationError::Ssh(_)) {
905                StatusNamespace::Ssh
906            } else {
907                StatusNamespace::Kafka
908            },
909        }
910    });
911
912    (statuses, button.press_on_drop())
913}
914
915/// Determines the latest progress record from the specified topic for the given
916/// progress key.
917///
918/// IMPORTANT: to achieve exactly once guarantees, the producer that will resume
919/// production at the returned timestamp *must* have called `init_transactions`
920/// prior to calling this method.
921async fn determine_sink_progress(
922    sink_id: GlobalId,
923    connection: &KafkaSinkConnection,
924    storage_configuration: &StorageConfiguration,
925    metrics: Arc<KafkaSinkMetrics>,
926) -> Result<Option<ProgressRecord>, ContextCreationError> {
927    // ****************************** WARNING ******************************
928    // Be VERY careful when editing the code in this function. It is very easy
929    // to accidentally introduce a correctness or liveness bug when refactoring
930    // this code.
931    // ****************************** WARNING ******************************
932
933    let TimeoutConfig {
934        fetch_metadata_timeout,
935        progress_record_fetch_timeout,
936        ..
937    } = storage_configuration.parameters.kafka_timeout_config;
938
939    let client_id = connection.client_id(
940        storage_configuration.config_set(),
941        &storage_configuration.connection_context,
942        sink_id,
943    );
944    let group_id = connection.progress_group_id(&storage_configuration.connection_context, sink_id);
945    let progress_topic = connection
946        .progress_topic(&storage_configuration.connection_context)
947        .into_owned();
948    let progress_topic_options = &connection.connection.progress_topic_options;
949    let progress_key = ProgressKey::new(sink_id);
950
951    let common_options = btreemap! {
952        // Consumer group ID, which may have been overridden by the user. librdkafka requires this,
953        // even though we'd prefer to disable the consumer group protocol entirely.
954        "group.id" => group_id,
955        // Allow Kafka monitoring tools to identify this consumer.
956        "client.id" => client_id,
957        "enable.auto.commit" => "false".into(),
958        "auto.offset.reset" => "earliest".into(),
959        // The fetch loop below needs EOF notifications to reliably detect that we have reached the
960        // high watermark.
961        "enable.partition.eof" => "true".into(),
962    };
963
964    // Construct two cliens in read committed and read uncommitted isolations respectively. See
965    // comment below for an explanation on why we need it.
966    let progress_client_read_committed: BaseConsumer<_> = {
967        let mut opts = common_options.clone();
968        opts.insert("isolation.level", "read_committed".into());
969        let ctx = MzClientContext::default();
970        connection
971            .connection
972            .create_with_context(storage_configuration, ctx, &opts, InTask::Yes)
973            .await?
974    };
975
976    let progress_client_read_uncommitted: BaseConsumer<_> = {
977        let mut opts = common_options;
978        opts.insert("isolation.level", "read_uncommitted".into());
979        let ctx = MzClientContext::default();
980        connection
981            .connection
982            .create_with_context(storage_configuration, ctx, &opts, InTask::Yes)
983            .await?
984    };
985
986    let ctx = Arc::clone(progress_client_read_committed.client().context());
987
988    // Ensure the progress topic exists.
989    let ensure_topic_config =
990        match &*SINK_ENSURE_TOPIC_CONFIG.get(storage_configuration.config_set()) {
991            "skip" => EnsureTopicConfig::Skip,
992            "check" => EnsureTopicConfig::Check,
993            "alter" => EnsureTopicConfig::Alter,
994            _ => {
995                tracing::warn!(
996                    topic = progress_topic,
997                    "unexpected value for ensure-topic-config; skipping checks"
998                );
999                EnsureTopicConfig::Skip
1000            }
1001        };
1002    mz_storage_client::sink::ensure_kafka_topic(
1003        connection,
1004        storage_configuration,
1005        &progress_topic,
1006        progress_topic_options,
1007        ensure_topic_config,
1008    )
1009    .await
1010    .add_context("error registering kafka progress topic for sink")?;
1011
1012    // We are about to spawn a blocking task that cannot be aborted by simply calling .abort() on
1013    // its handle but we must be able to cancel it prompty so as to not leave long running
1014    // operations around when interest to this task is lost. To accomplish this we create a shared
1015    // token of which a weak reference is given to the task and a strong reference is held by the
1016    // parent task. The task periodically checks if its weak reference is still valid before
1017    // continuing its work.
1018    let parent_token = Arc::new(());
1019    let child_token = Arc::downgrade(&parent_token);
1020    let task_name = format!("get_latest_ts:{sink_id}");
1021    let sink_progress_search = SINK_PROGRESS_SEARCH.get(storage_configuration.config_set());
1022    let result = task::spawn_blocking(|| task_name, move || {
1023        let progress_topic = progress_topic.as_ref();
1024        // Ensure the progress topic has exactly one partition. Kafka only
1025        // guarantees ordering within a single partition, and we need a strict
1026        // order on the progress messages we read and write.
1027        let partitions = match mz_kafka_util::client::get_partitions(
1028            progress_client_read_committed.client(),
1029            progress_topic,
1030            fetch_metadata_timeout,
1031        ) {
1032            Ok(partitions) => partitions,
1033            Err(GetPartitionsError::TopicDoesNotExist) => {
1034                // The progress topic doesn't exist, which indicates there is
1035                // no committed timestamp.
1036                return Ok(None);
1037            }
1038            e => e.with_context(|| {
1039                format!(
1040                    "Unable to fetch metadata about progress topic {}",
1041                    progress_topic
1042                )
1043            })?,
1044        };
1045        if partitions.len() != 1 {
1046            bail!(
1047                    "Progress topic {} should contain a single partition, but instead contains {} partitions",
1048                    progress_topic, partitions.len(),
1049                );
1050        }
1051        let partition = partitions.into_element();
1052
1053        // We scan from the beginning and see if we can find a progress record. We have
1054        // to do it like this because Kafka Control Batches mess with offsets. We
1055        // therefore cannot simply take the last offset from the back and expect a
1056        // progress message there. With a transactional producer, the OffsetTail(1) will
1057        // not point to an progress message but a control message. With aborted
1058        // transactions, there might even be a lot of garbage at the end of the
1059        // topic or in between.
1060
1061        metrics.consumed_progress_records.set(0);
1062
1063        // First, determine the current high water mark for the progress topic.
1064        // This is the position our `progress_client` consumer *must* reach
1065        // before we can conclude that we've seen the latest progress record for
1066        // the specified `progress_key`. A safety argument:
1067        //
1068        //   * Our caller has initialized transactions before calling this
1069        //     method, which prevents the prior incarnation of this sink from
1070        //     committing any further progress records.
1071        //
1072        //   * We use `read_uncommitted` isolation to ensure that we fetch the
1073        //     true high water mark for the topic, even if there are pending
1074        //     transactions in the topic. If we used the `read_committed`
1075        //     isolation level, we'd instead get the "last stable offset" (LSO),
1076        //     which is the offset of the first message in an open transaction,
1077        //     which might not include the last progress message committed for
1078        //     this sink! (While the caller of this function has fenced out
1079        //     older producers for this sink, *other* sinks writing using the
1080        //     same progress topic might have long-running transactions that
1081        //     hold back the LSO.)
1082        //
1083        //   * If another sink spins up and fences out the producer for this
1084        //     incarnation of the sink, we may not see the latest progress
1085        //     record... but since the producer has been fenced out, it will be
1086        //     unable to act on our stale information.
1087        //
1088        let (lo, hi) = progress_client_read_uncommitted
1089            .fetch_watermarks(progress_topic, partition, fetch_metadata_timeout)
1090            .map_err(|e| {
1091                anyhow!(
1092                    "Failed to fetch metadata while reading from progress topic: {}",
1093                    e
1094                )
1095            })?;
1096
1097        // This topic might be long, but the desired offset will usually be right near the end.
1098        // Instead of always scanning through the entire topic, we scan through exponentially-growing
1099        // suffixes of it. (Because writes are ordered, the largest progress record in any suffix,
1100        // if present, is the global max.) If we find it in one of our suffixes, we've saved at least
1101        // an order of magnitude of work; if we don't, we've added at most a constant factor.
1102        let mut start_indices = vec![lo];
1103        if sink_progress_search {
1104            let mut lookback = hi.saturating_sub(lo) / 10;
1105            while lookback >= 20_000 {
1106                start_indices.push(hi - lookback);
1107                lookback /= 10;
1108            }
1109        }
1110        for lo in start_indices.into_iter().rev() {
1111            if let Some(found) = progress_search(
1112                &progress_client_read_committed,
1113                progress_record_fetch_timeout,
1114                progress_topic,
1115                partition,
1116                lo,
1117                hi,
1118                progress_key.clone(),
1119                Weak::clone(&child_token),
1120                Arc::clone(&metrics)
1121            )? {
1122                return Ok(Some(found));
1123            }
1124        }
1125        Ok(None)
1126    }).await.check_ssh_status(&ctx);
1127    // Express interest to the computation until after we've received its result
1128    drop(parent_token);
1129    result
1130}
1131
1132fn progress_search<C: ConsumerContext + 'static>(
1133    progress_client_read_committed: &BaseConsumer<C>,
1134    progress_record_fetch_timeout: Duration,
1135    progress_topic: &str,
1136    partition: i32,
1137    lo: i64,
1138    hi: i64,
1139    progress_key: ProgressKey,
1140    child_token: Weak<()>,
1141    metrics: Arc<KafkaSinkMetrics>,
1142) -> anyhow::Result<Option<ProgressRecord>> {
1143    // Seek to the beginning of the given range in the progress topic.
1144    let mut tps = TopicPartitionList::new();
1145    tps.add_partition(progress_topic, partition);
1146    tps.set_partition_offset(progress_topic, partition, Offset::Offset(lo))?;
1147    progress_client_read_committed
1148        .assign(&tps)
1149        .with_context(|| {
1150            format!(
1151                "Error seeking in progress topic {}:{}",
1152                progress_topic, partition
1153            )
1154        })?;
1155
1156    // Helper to get the progress consumer's current position.
1157    let get_position = || {
1158        if child_token.strong_count() == 0 {
1159            bail!("operation cancelled");
1160        }
1161        let position = progress_client_read_committed
1162            .position()?
1163            .find_partition(progress_topic, partition)
1164            .ok_or_else(|| {
1165                anyhow!(
1166                    "No position info found for progress topic {}",
1167                    progress_topic
1168                )
1169            })?
1170            .offset();
1171        let position = match position {
1172            Offset::Offset(position) => position,
1173            // An invalid offset indicates the consumer has not yet read a
1174            // message. Since we assigned the consumer to the beginning of
1175            // the topic, it's safe to return the low water mark here, which
1176            // indicates the position before the first possible message.
1177            //
1178            // Note that it's important to return the low water mark and not
1179            // the minimum possible offset (i.e., zero) in order to break
1180            // out of the loop if the topic is empty but the low water mark
1181            // is greater than zero.
1182            Offset::Invalid => lo,
1183            _ => bail!(
1184                "Consumer::position returned offset of wrong type: {:?}",
1185                position
1186            ),
1187        };
1188        // Record the outstanding number of progress records that remain to be processed
1189        let outstanding = u64::try_from(std::cmp::max(0, hi - position)).unwrap();
1190        metrics.outstanding_progress_records.set(outstanding);
1191        Ok(position)
1192    };
1193
1194    info!("fetching latest progress record for {progress_key}, lo/hi: {lo}/{hi}");
1195
1196    // Read messages until the consumer is positioned at or beyond the high
1197    // water mark.
1198    //
1199    // We use `read_committed` isolation to ensure we don't see progress
1200    // records for transactions that did not commit. This means we have to
1201    // wait for the LSO to progress to the high water mark `hi`, which means
1202    // waiting for any open transactions for other sinks using the same
1203    // progress topic to complete. We set a short transaction timeout (10s)
1204    // to ensure we never need to wait more than 10s.
1205    //
1206    // Note that the stall time on the progress topic is not a function of
1207    // transaction size. We've designed our transactions so that the
1208    // progress record is always written last, after all the data has been
1209    // written, and so the window of time in which the progress topic has an
1210    // open transaction is quite small. The only vulnerability is if another
1211    // sink using the same progress topic crashes in that small window
1212    // between writing the progress record and committing the transaction,
1213    // in which case we have to wait out the transaction timeout.
1214    //
1215    // Important invariant: we only exit this loop successfully (i.e., not
1216    // returning an error) if we have positive proof of a position at or
1217    // beyond the high water mark. To make this invariant easy to check, do
1218    // not use `break` in the body of the loop.
1219    let mut last_progress: Option<ProgressRecord> = None;
1220    loop {
1221        let current_position = get_position()?;
1222
1223        if current_position >= hi {
1224            // consumer is at or beyond the high water mark and has read enough messages
1225            break;
1226        }
1227
1228        let message = match progress_client_read_committed.poll(progress_record_fetch_timeout) {
1229            Some(Ok(message)) => message,
1230            Some(Err(KafkaError::PartitionEOF(_))) => {
1231                // No message, but the consumer's position may have advanced
1232                // past a transaction control message that positions us at
1233                // or beyond the high water mark. Go around the loop again
1234                // to check.
1235                continue;
1236            }
1237            Some(Err(e)) => bail!("failed to fetch progress message {e}"),
1238            None => {
1239                bail!(
1240                    "timed out while waiting to reach high water mark of non-empty \
1241                        topic {progress_topic}:{partition}, lo/hi: {lo}/{hi}, current position: {current_position}"
1242                );
1243            }
1244        };
1245
1246        if message.key() != Some(progress_key.to_bytes()) {
1247            // This is a progress message for a different sink.
1248            continue;
1249        }
1250
1251        metrics.consumed_progress_records.inc();
1252
1253        let Some(payload) = message.payload() else {
1254            continue;
1255        };
1256        let progress = parse_progress_record(payload)?;
1257
1258        match last_progress {
1259            Some(last_progress)
1260                if !PartialOrder::less_equal(&last_progress.frontier, &progress.frontier) =>
1261            {
1262                bail!(
1263                    "upper regressed in topic {progress_topic}:{partition} from {:?} to {:?}",
1264                    last_progress.frontier,
1265                    progress.frontier,
1266                );
1267            }
1268            _ => last_progress = Some(progress),
1269        }
1270    }
1271
1272    // If we get here, we are assured that we've read all messages up to
1273    // the high water mark, and therefore `last_timestamp` contains the
1274    // most recent timestamp for the sink under consideration.
1275    Ok(last_progress)
1276}
1277
1278/// This is the legacy struct that used to be emitted as part of a transactional produce and
1279/// contains the largest timestamp within the batch committed. Since it is just a timestamp it
1280/// cannot encode the fact that a sink has finished and deviates from upper frontier semantics.
1281/// Materialize no longer produces this record but it's possible that we encounter this in topics
1282/// written by older versions. In those cases we convert it into upper semantics by stepping the
1283/// timestamp forward.
1284#[derive(Debug, PartialEq, Serialize, Deserialize)]
1285pub struct LegacyProgressRecord {
1286    // Double Option to tell apart an omitted field from one set to null explicitly
1287    // https://github.com/serde-rs/serde/issues/984
1288    #[serde(default, deserialize_with = "deserialize_some")]
1289    pub timestamp: Option<Option<Timestamp>>,
1290}
1291
1292// Any value that is present is considered Some value, including null.
1293fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
1294where
1295    T: Deserialize<'de>,
1296    D: Deserializer<'de>,
1297{
1298    Deserialize::deserialize(deserializer).map(Some)
1299}
1300
1301/// This struct is emitted as part of a transactional produce, and contains the upper frontier of
1302/// the batch committed. It is used to recover the frontier a sink needs to resume at.
1303#[derive(Debug, PartialEq, Serialize, Deserialize)]
1304pub struct ProgressRecord {
1305    #[serde(
1306        deserialize_with = "deserialize_frontier",
1307        serialize_with = "serialize_frontier"
1308    )]
1309    pub frontier: Antichain<Timestamp>,
1310    #[serde(default)]
1311    pub version: u64,
1312}
1313fn serialize_frontier<S>(frontier: &Antichain<Timestamp>, serializer: S) -> Result<S::Ok, S::Error>
1314where
1315    S: Serializer,
1316{
1317    Serialize::serialize(frontier.elements(), serializer)
1318}
1319
1320fn deserialize_frontier<'de, D>(deserializer: D) -> Result<Antichain<Timestamp>, D::Error>
1321where
1322    D: Deserializer<'de>,
1323{
1324    let times: Vec<Timestamp> = Deserialize::deserialize(deserializer)?;
1325    Ok(Antichain::from(times))
1326}
1327
1328fn parse_progress_record(payload: &[u8]) -> Result<ProgressRecord, anyhow::Error> {
1329    Ok(match serde_json::from_slice::<ProgressRecord>(payload) {
1330        Ok(progress) => progress,
1331        // If we fail to deserialize we might be reading a legacy progress record
1332        Err(_) => match serde_json::from_slice::<LegacyProgressRecord>(payload) {
1333            Ok(LegacyProgressRecord {
1334                timestamp: Some(Some(time)),
1335            }) => ProgressRecord {
1336                frontier: Antichain::from_elem(time.step_forward()),
1337                version: 0,
1338            },
1339            Ok(LegacyProgressRecord {
1340                timestamp: Some(None),
1341            }) => ProgressRecord {
1342                frontier: Antichain::new(),
1343                version: 0,
1344            },
1345            _ => match std::str::from_utf8(payload) {
1346                Ok(payload) => bail!("invalid progress record: {payload}"),
1347                Err(_) => bail!("invalid progress record bytes: {payload:?}"),
1348            },
1349        },
1350    })
1351}
1352
1353/// Fetches the partition count for the identified topic.
1354async fn fetch_partition_count(
1355    producer: &ThreadedProducer<TunnelingClientContext<MzClientContext>>,
1356    sink_id: GlobalId,
1357    topic_name: &str,
1358) -> Result<u64, anyhow::Error> {
1359    let meta = task::spawn_blocking(|| format!("kafka_sink_fetch_partition_count:{sink_id}"), {
1360        let producer = producer.clone();
1361        move || {
1362            producer
1363                .client()
1364                .fetch_metadata(None, DEFAULT_FETCH_METADATA_TIMEOUT)
1365        }
1366    })
1367    .await
1368    .check_ssh_status(producer.context())?;
1369
1370    match meta.topics().iter().find(|t| t.name() == topic_name) {
1371        Some(topic) => {
1372            let partition_count = u64::cast_from(topic.partitions().len());
1373            if partition_count == 0 {
1374                bail!("topic {topic_name} has an impossible partition count of zero");
1375            }
1376            Ok(partition_count)
1377        }
1378        None => bail!("topic {topic_name} does not exist"),
1379    }
1380}
1381
1382/// Fetches the partition count for the identified topic at the specified
1383/// interval.
1384///
1385/// When an updated partition count is discovered, invokes
1386/// `update_partition_count` with the new partition count.
1387async fn fetch_partition_count_loop<F>(
1388    producer: ThreadedProducer<TunnelingClientContext<MzClientContext>>,
1389    sink_id: GlobalId,
1390    topic_name: String,
1391    interval: Duration,
1392    update_partition_count: Arc<F>,
1393) where
1394    F: Fn(u64),
1395{
1396    let mut interval = time::interval(interval);
1397    interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
1398    loop {
1399        interval.tick().await;
1400        match fetch_partition_count(&producer, sink_id, &topic_name).await {
1401            Ok(pc) => update_partition_count(pc),
1402            Err(e) => {
1403                warn!(%sink_id, "failed updating partition count: {e}");
1404                continue;
1405            }
1406        };
1407    }
1408}
1409
1410/// Register an Avro `schema` with the sink's schema registry and build an
1411/// [`AvroEncoder`] framing records for that registry.
1412///
1413/// `subject` is the registry name for the schema, either `{topic}-key` or
1414/// `{topic}-value`. It is the Confluent subject or, for Glue, the schema name.
1415/// Registration happens here in the render cycle, so schemas are re-published
1416/// each time the sink is rendered. The `WireFormat` must carry a registry:
1417/// sinks are never built without one (see the sink planner), so a missing
1418/// registry is unreachable.
1419async fn build_avro_encoder(
1420    desc: RelationDesc,
1421    debezium: bool,
1422    schema: String,
1423    compatibility_level: Option<mz_ccsr::CompatibilityLevel>,
1424    wire_format: WireFormat,
1425    subject: String,
1426    storage_configuration: &StorageConfiguration,
1427) -> Result<AvroEncoder, anyhow::Error> {
1428    let schema_id = match wire_format {
1429        WireFormat::Confluent {
1430            registry: Some(csr),
1431        } => {
1432            let ccsr = csr.connect(storage_configuration, InTask::Yes).await?;
1433            let schema_id = mz_storage_client::sink::publish_kafka_schema(
1434                ccsr,
1435                subject,
1436                schema.clone(),
1437                mz_ccsr::SchemaType::Avro,
1438                compatibility_level,
1439            )
1440            .await
1441            .context("error publishing kafka schemas for sink")?;
1442            AvroSchemaId::Confluent(schema_id)
1443        }
1444        WireFormat::Glue {
1445            registry: Some(glue),
1446        } => {
1447            let enforce_external_addresses = mz_storage_types::dyncfgs::ENFORCE_EXTERNAL_ADDRESSES
1448                .get(storage_configuration.config_set());
1449            let sdk_config = glue
1450                .aws_connection
1451                .connection
1452                .load_sdk_config(
1453                    &storage_configuration.connection_context,
1454                    glue.aws_connection.connection_id,
1455                    InTask::Yes,
1456                    enforce_external_addresses,
1457                )
1458                .await?;
1459            let client = mz_aws_glue_schema_registry::ClientConfig::new(sdk_config).build();
1460            let compatibility =
1461                compatibility_level.map(mz_storage_client::sink::glue_compatibility_from_csr);
1462            // `subject` is used verbatim as the Glue schema name. When it is
1463            // topic-derived (`{topic}-key` or `{topic}-value`) no mangling is
1464            // needed: Kafka topic chars `[a-zA-Z0-9._-]` are a subset of Glue's
1465            // `[a-zA-Z0-9-_$#.]`, and the longest suffix `-value` (6 chars) plus
1466            // the max Kafka topic length (249) lands exactly on Glue's max
1467            // schema name length (255).
1468            //
1469            // A user-supplied schema name carries no such guarantee. As with
1470            // CSR subjects, it is not validated at plan time, so an out-of-range
1471            // or ill-formed name is rejected by Glue here at registration.
1472            //
1473            // Kafka topic name rules:
1474            // https://github.com/a0x8o/kafka/blob/master/core/src/main/scala/kafka/common/Topic.scala
1475            // Glue schema name rules:
1476            // https://docs.aws.amazon.com/glue/latest/webapi/API_CreateSchema.html
1477            let schema_version_id = mz_storage_client::sink::publish_glue_schema(
1478                client,
1479                glue.registry_name,
1480                subject,
1481                schema.clone(),
1482                compatibility,
1483            )
1484            .await
1485            .context("error publishing glue schemas for sink")?;
1486            AvroSchemaId::Glue(schema_version_id)
1487        }
1488        other => unreachable!(
1489            "sink Avro wire_format must carry a registry, got {:?}",
1490            other
1491        ),
1492    };
1493    Ok(AvroEncoder::new(desc, debezium, &schema, schema_id))
1494}
1495
1496/// Walks each arrangement batch and emits encoded Kafka messages, one per
1497/// `DiffPair` observed at each `(key, timestamp)`.
1498///
1499/// When `key_is_synthetic`, the batch keys are per-row hashes used only for
1500/// worker distribution; the emitted `KafkaMessage` uses no key in that case.
1501fn encode_collection<'scope>(
1502    name: String,
1503    batches: SinkBatchStream<'scope>,
1504    envelope: SinkEnvelope,
1505    connection: KafkaSinkConnection,
1506    storage_configuration: StorageConfiguration,
1507    sink_id: GlobalId,
1508    from_id: GlobalId,
1509    key_is_synthetic: bool,
1510) -> (
1511    VecCollection<'scope, Timestamp, KafkaMessage, Diff>,
1512    StreamVec<'scope, Timestamp, HealthStatusMessage>,
1513    PressOnDropButton,
1514) {
1515    let mut builder = AsyncOperatorBuilder::new(name, batches.scope());
1516
1517    let (output, stream) = builder.new_output::<CapacityContainerBuilder<Vec<_>>>();
1518    let mut input = builder.new_input_for(batches, Pipeline, &output);
1519
1520    let (button, errors) = builder.build_fallible(move |caps| {
1521        Box::pin(async move {
1522            let [capset]: &mut [_; 1] = caps.try_into().unwrap();
1523            let key_desc = connection
1524                .key_desc_and_indices
1525                .as_ref()
1526                .map(|(desc, _indices)| desc.clone());
1527            let value_desc = connection.value_desc;
1528
1529            let key_encoder: Option<Box<dyn Encode>> =
1530                match (key_desc, connection.format.key_format) {
1531                    (Some(desc), Some(KafkaSinkFormatType::Bytes)) => {
1532                        Some(Box::new(BinaryEncoder::new(desc, false)))
1533                    }
1534                    (Some(desc), Some(KafkaSinkFormatType::Text)) => {
1535                        Some(Box::new(TextEncoder::new(desc, false)))
1536                    }
1537                    (Some(desc), Some(KafkaSinkFormatType::Json)) => {
1538                        Some(Box::new(JsonEncoder::new(desc, false)))
1539                    }
1540                    (Some(desc), Some(KafkaSinkFormatType::Avro {
1541                        schema,
1542                        compatibility_level,
1543                        schema_name,
1544                        wire_format,
1545                    })) => {
1546                        let subject = schema_name
1547                            .unwrap_or_else(|| format!("{}-key", connection.topic));
1548                        let encoder = build_avro_encoder(
1549                            desc,
1550                            false,
1551                            schema,
1552                            compatibility_level,
1553                            wire_format,
1554                            subject,
1555                            &storage_configuration,
1556                        )
1557                        .await?;
1558                        Some(Box::new(encoder))
1559                    }
1560                    (None, None) => None,
1561                    (desc, format) => {
1562                        return Err(anyhow!(
1563                            "key_desc and key_format must be both set or both unset, but key_desc: {:?}, key_format: {:?}",
1564                            desc,
1565                            format
1566                        ))
1567                    }
1568                };
1569
1570            // whether to apply the debezium envelope to the value encoding
1571            let debezium = matches!(envelope, SinkEnvelope::Debezium);
1572
1573            let value_encoder: Box<dyn Encode> = match connection.format.value_format {
1574                KafkaSinkFormatType::Bytes => Box::new(BinaryEncoder::new(value_desc, debezium)),
1575                KafkaSinkFormatType::Text => Box::new(TextEncoder::new(value_desc, debezium)),
1576                KafkaSinkFormatType::Json => Box::new(JsonEncoder::new(value_desc, debezium)),
1577                KafkaSinkFormatType::Avro {
1578                    schema,
1579                    compatibility_level,
1580                    schema_name,
1581                    wire_format,
1582                } => {
1583                    let subject = schema_name
1584                        .unwrap_or_else(|| format!("{}-value", connection.topic));
1585                    let encoder = build_avro_encoder(
1586                        value_desc,
1587                        debezium,
1588                        schema,
1589                        compatibility_level,
1590                        wire_format,
1591                        subject,
1592                        &storage_configuration,
1593                    )
1594                    .await?;
1595                    Box::new(encoder)
1596                }
1597            };
1598
1599            // !IMPORTANT!
1600            // Correctness of this operator relies on no fallible operations happening after this
1601            // point. This is a temporary workaround of build_fallible's bad interaction of owned
1602            // capabilities and errors.
1603            // TODO(petrosagg): Make the fallible async operator safe
1604            *capset = CapabilitySet::new();
1605
1606            let mut row_buf = Row::default();
1607            let mut datums = DatumVec::new();
1608            let mut pk_warner =
1609                (!key_is_synthetic).then(|| PkViolationWarner::new(sink_id, from_id));
1610
1611            while let Some(event) = input.next().await {
1612                if let Event::Data(cap, mut batches) = event {
1613                    for span in batches.drain(..) {
1614                        // A span with no updates carries no batch to encode.
1615                        let Some(batch) = &span.inner else {
1616                            continue;
1617                        };
1618                        for_each_diff_pair(batch, |key, time, value| {
1619                            if let Some(warner) = pk_warner.as_mut() {
1620                                warner.observe(key, time);
1621                            }
1622                            // Only emit the arrangement key when the user configured one; relation-key
1623                            // and synthetic-hash arrangements exist purely for grouping / worker
1624                            // distribution and have no corresponding key encoder.
1625                            let key_for_message = if key_encoder.is_some() { key } else { &None };
1626
1627                            let mut hash = None;
1628                            let mut headers = vec![];
1629                            if connection.headers_index.is_some()
1630                                || connection.partition_by.is_some()
1631                            {
1632                                // Header values and partition by values are derived from the row
1633                                // that produces an event. But it is ambiguous whether to use the
1634                                // `before` or `after` from the event. The rule applied here is
1635                                // simple: use `after` if it exists (insertions and updates),
1636                                // otherwise fall back to `before` (deletions).
1637                                //
1638                                // It is up to the SQL planner to ensure this produces sensible
1639                                // results. (When using the upsert envelope and both `before` and
1640                                // `after` are present, it's always unambiguous to use `after`
1641                                // because that's all that will be present in the Kafka message;
1642                                // when using the Debezium envelope, it's okay to refer to columns
1643                                // in the key because those are guaranteed to be the same in both
1644                                // `before` and `after`.)
1645                                let row = value
1646                                    .after
1647                                    .as_ref()
1648                                    .or(value.before.as_ref())
1649                                    .expect("one of before or after must be set");
1650                                let row = datums.borrow_with(row);
1651
1652                                if let Some(i) = connection.headers_index {
1653                                    headers = encode_headers(row[i]);
1654                                }
1655
1656                                if let Some(partition_by) = &connection.partition_by {
1657                                    hash = Some(evaluate_partition_by(partition_by, &row));
1658                                }
1659                            }
1660                            let (encoded_key, hash) = match key_for_message {
1661                                Some(key) => {
1662                                    let key_encoder =
1663                                        key_encoder.as_ref().expect("key present");
1664                                    let encoded = key_encoder.encode_unchecked(key.clone());
1665                                    let hash =
1666                                        hash.unwrap_or_else(|| key_encoder.hash(&encoded));
1667                                    (Some(encoded), hash)
1668                                }
1669                                None => (None, hash.unwrap_or(0)),
1670                            };
1671                            let value = match envelope {
1672                                SinkEnvelope::Upsert => value.after,
1673                                SinkEnvelope::Debezium => {
1674                                    dbz_format(&mut row_buf.packer(), value);
1675                                    Some(row_buf.clone())
1676                                }
1677                                SinkEnvelope::Append => {
1678                                    unreachable!("Append envelope is not valid for Kafka sinks")
1679                                }
1680                            };
1681                            let value = value.map(|value| value_encoder.encode_unchecked(value));
1682                            let message = KafkaMessage {
1683                                hash,
1684                                key: encoded_key,
1685                                value,
1686                                headers,
1687                            };
1688                            output.give(&cap, (message, time, Diff::ONE));
1689                        });
1690                        // Flush after each batch so the final `(key, time)` group of the walk is
1691                        // resolved immediately — a PK violation in the last group is otherwise
1692                        // held until more data arrives or the operator shuts down.
1693                        if let Some(warner) = pk_warner.as_mut() {
1694                            warner.flush();
1695                        }
1696                    }
1697                }
1698            }
1699
1700            Ok::<(), anyhow::Error>(())
1701        })
1702    });
1703
1704    let statuses = errors.map(|error| HealthStatusMessage {
1705        id: None,
1706        update: HealthStatusUpdate::halting(format!("{}", error.display_with_causes()), None),
1707        namespace: StatusNamespace::Kafka,
1708    });
1709
1710    (stream.as_collection(), statuses, button.press_on_drop())
1711}
1712
1713fn encode_headers(datum: Datum) -> Vec<KafkaHeader> {
1714    let mut out = vec![];
1715    if datum.is_null() {
1716        return out;
1717    }
1718    for (key, value) in datum.unwrap_map().iter() {
1719        out.push(KafkaHeader {
1720            key: key.into(),
1721            value: match value {
1722                Datum::Null => None,
1723                Datum::String(s) => Some(s.as_bytes().to_vec()),
1724                Datum::Bytes(b) => Some(b.to_vec()),
1725                _ => panic!("encode_headers called with unexpected header value {value:?}"),
1726            },
1727        })
1728    }
1729    out
1730}
1731
1732/// Evaluates a partition by expression on the given row, returning the hash
1733/// value to use for partition assignment.
1734///
1735/// The provided expression must have type `Int32`, `Int64`, `UInt32`, or
1736/// `UInt64`. If the expression produces an error when evaluated, or if the
1737/// expression is of a signed integer type and produces a negative value, this
1738/// function returns 0.
1739fn evaluate_partition_by(partition_by: &MirScalarExpr, row: &[Datum]) -> u64 {
1740    // NOTE(benesch): The way this function converts errors and invalid values
1741    // to 0 is somewhat surpising. Ideally, we would put the sink in a
1742    // permanently errored state if the partition by expression produces an
1743    // error or invalid value. But we don't presently have a way for sinks to
1744    // report errors (see materialize#17688), so the current behavior was determined to be
1745    // the best available option. The behavior is clearly documented in the
1746    // user-facing `CREATE SINK` docs.
1747    let temp_storage = RowArena::new();
1748    match partition_by.eval(row, &temp_storage) {
1749        Ok(Datum::UInt64(u)) => u,
1750        Ok(datum) => {
1751            // If we are here the only valid type that we should be seeing is
1752            // null. Anything else is a bug in the planner.
1753            soft_assert_or_log!(datum.is_null(), "unexpected partition_by result: {datum:?}");
1754            // We treat nulls the same as we treat errors: map them to partition 0.
1755            0
1756        }
1757        Err(_) => 0,
1758    }
1759}
1760
1761#[cfg(test)]
1762mod test {
1763    use mz_ore::assert_err;
1764
1765    use super::*;
1766
1767    #[mz_ore::test]
1768    fn progress_record_migration() {
1769        assert_err!(parse_progress_record(b"{}"));
1770
1771        assert_eq!(
1772            parse_progress_record(b"{\"timestamp\":1}").unwrap(),
1773            ProgressRecord {
1774                frontier: Antichain::from_elem(2.into()),
1775                version: 0,
1776            }
1777        );
1778
1779        assert_eq!(
1780            parse_progress_record(b"{\"timestamp\":null}").unwrap(),
1781            ProgressRecord {
1782                frontier: Antichain::new(),
1783                version: 0,
1784            }
1785        );
1786
1787        assert_eq!(
1788            parse_progress_record(b"{\"frontier\":[1]}").unwrap(),
1789            ProgressRecord {
1790                frontier: Antichain::from_elem(1.into()),
1791                version: 0,
1792            }
1793        );
1794
1795        assert_eq!(
1796            parse_progress_record(b"{\"frontier\":[]}").unwrap(),
1797            ProgressRecord {
1798                frontier: Antichain::new(),
1799                version: 0,
1800            }
1801        );
1802
1803        assert_eq!(
1804            parse_progress_record(b"{\"frontier\":[], \"version\": 42}").unwrap(),
1805            ProgressRecord {
1806                frontier: Antichain::new(),
1807                version: 42,
1808            }
1809        );
1810
1811        assert_err!(parse_progress_record(b"{\"frontier\":null}"));
1812    }
1813}