Skip to main content

mz_compute/sink/
materialized_view.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//! A dataflow sink that writes input records to a persist shard.
11//!
12//! This implementation is both parallel and self-correcting.
13//!
14//!  * parallel: Multiple workers can participate in writing updates for the same times, letting
15//!    sink throughput scale with the number of workers allocated to the replica.
16//!  * self-correcting: The sink continually compares the contents of the persist shard with the
17//!    contents of the input collection and writes down the difference. If the persist shard ends
18//!    up with undesired contents for any reason, this is corrected the next time the sink manages
19//!    to append to the shard.
20//!
21//! ### Operators
22//!
23//! The persist sink consists of a graph of operators.
24//!
25//!    desired                    persist <---------------.
26//!       |                          |                    |
27//!       |                          |                    |
28//!       |---------------------.    |                    |
29//!       |                     |    |                    |
30//!       |                     |    |                    |
31//!       v                     v    v                    |
32//!   +--------+              +--------+              +--------+
33//!   |  mint  | --descs-.--> | write  | --batches--> | append |
34//!   +--------+          \   +--------+          .-> +--------+
35//!                        \_____________________/
36//!
37//!  * `mint` mints batch descriptions, i.e., `(lower, upper)` bounds of batches that should be
38//!    written. The persist API requires that all workers write batches with the same bounds, so
39//!    they can be appended as a single logical batch. To ensure this, the `mint` operator only
40//!    runs on a single worker that broadcasts minted descriptions to all workers. Batch bounds are
41//!    picked based on the frontiers of the `desired` stream and the output persist shard.
42//!  * `write` stages batch data in persist, based on the batch descriptions received from the
43//!    `mint` operator, but without appending it to the persist shard. This is a multi-worker
44//!    operator, with each worker writing batches of the data that arrives at its local inputs. To
45//!    do so it reads from the `desired` and `persist` streams and produces the difference between
46//!    them to write back out, ensuring that the final contents of the persist shard match
47//!    `desired`.
48//!  * `append` appends the batches minted by `mint` and written by `write` to the persist shard.
49//!    This is a multi-worker operator, where workers are responsible for different subsets of
50//!    batch descriptions. If a worker is responsible for a given batch description, it waits for
51//!    all workers to stage their batches for that batch description, then appends all the batches
52//!    together as a single logical batch.
53//!
54//! Note that while the above graph suggests that `mint` and `write` both receive copies of the
55//! `desired` stream, the actual implementation passes that stream through `mint` and lets `write`
56//! read the passed-through stream, to avoid cloning data.
57//!
58//! Also note that the `append` operator's implementation would perhaps be more natural as a
59//! single-worker implementation. The purpose of sharing the work between all workers is to avoid a
60//! work imbalance where one worker is overloaded (doing both appends and the consequent persist
61//! maintenance work) while others are comparatively idle.
62//!
63//! The persist sink is written to be robust to the presence of other conflicting instances (e.g.
64//! from other replicas) writing to the same persist shard. Each of the three operators needs to be
65//! able to handle conflicting writes that unexpectedly change the contents of the output persist
66//! shard.
67//!
68//! ### Frontiers
69//!
70//! The `desired` frontier tracks the progress of the upstream dataflow, but may be rounded up to
71//! the next refresh time for dataflows that follow a refresh schedule other than "on commit".
72//!
73//! The `persist` frontier tracks the `upper` frontier of the target persist shard, with one
74//! exception: When the `persist_source` that reads back the shard is rendered, it will start
75//! reading at its `since` frontier. So if the shard's `since` is initially greater than its
76//! `upper`, the `persist` frontier too will be in advance of the shard `upper`, until the `upper`
77//! has caught up. To avoid getting confused by this edge case, the `mint` operator does not use
78//! the `persist` stream to observe the shard frontier but keeps its own `WriteHandle` instead.
79//!
80//! The `descs` frontier communicates which `lower` bounds may still be emitted in batch
81//! descriptions. All future batch descriptions will have a `lower` that is greater or equal to the
82//! current `descs` frontier.
83//!
84//! The `batches` frontier communicates for which `lower` bounds batches may still be written. All
85//! batches for descriptions with `lower`s less than the current `batches` frontier have already
86//! been written.
87//!
88//! ### Invariants
89//!
90//! The implementation upholds several invariants that can be relied upon to simplify the
91//! implementation:
92//!
93//!  1. `lower`s in minted batch descriptions are unique and strictly increasing. That is, the
94//!     `mint` operator will never mint the same `lower` twice and a minted `lower` is always
95//!     greater than any previously minted ones.
96//!  2. `upper`s in minted batch descriptions are monotonically increasing.
97//!  3. From (1) follows that there is always at most one "valid" batch description in flight in
98//!     the operator graph. "Valid" here means that the described batch can be appended to the
99//!     persist shard.
100//!
101//! The main simplification these invariants allow is that operators only need to keep track of the
102//! most recent batch description and/or `lower`. Previous batch descriptions are not valid
103//! anymore, so there is no reason to hold any state or perform any work in support of them.
104//!
105//! ### Read-only Mode
106//!
107//! The persist sink can optionally be initialized in read-only mode. In this mode it is passive
108//! and avoids any writes to persist. Activating the `read_only_rx` transitions the sink into write
109//! mode, where it commences normal operation.
110//!
111//! Read-only mode is implemented by the `mint` operator. To disable writes, the `mint` operator
112//! simply avoids minting any batch descriptions. Since both the `write` and the `append` operator
113//! require batch descriptions to write/append batches, this suppresses any persist communication.
114//! At the same time, the `write` operator still observes changes to the `desired` and `persist`
115//! collections, allowing it to keep its correction buffer up-to-date.
116
117use std::any::Any;
118use std::cell::RefCell;
119use std::pin::pin;
120use std::rc::Rc;
121use std::sync::Arc;
122
123use differential_dataflow::{AsCollection, Hashable, VecCollection};
124use futures::StreamExt;
125use mz_compute_types::dyncfgs::{
126    MV_SINK_ADVANCE_PERSIST_FRONTIERS, MV_SINK_SHARED_BATCH_MAX_BYTES,
127};
128use mz_compute_types::sinks::{ComputeSinkDesc, MaterializedViewSinkConnection};
129use mz_dyncfg::ConfigSet;
130use mz_expr::MfpPlan;
131use mz_ore::cast::CastFrom;
132use mz_persist_client::batch::{Batch, ProtoBatch};
133use mz_persist_client::cache::PersistClientCache;
134use mz_persist_client::metrics::SinkMetrics;
135use mz_persist_client::operators::shard_source::{ErrorHandler, SnapshotMode};
136use mz_persist_client::write::WriteHandle;
137use mz_persist_client::{Diagnostics, PersistClient};
138use mz_persist_types::codec_impls::UnitSchema;
139use mz_repr::{Diff, GlobalId, Row, Timestamp};
140use mz_storage_operators::persist::{SharedBatchId, SharedBatches};
141use mz_storage_types::StorageDiff;
142use mz_storage_types::controller::CollectionMetadata;
143use mz_storage_types::sources::SourceData;
144use mz_timely_util::builder_async::PressOnDropButton;
145use mz_timely_util::builder_async::{Event, OperatorBuilder, sole_capability};
146use mz_timely_util::probe::{Handle, ProbeNotify};
147use serde::{Deserialize, Serialize};
148use timely::PartialOrder;
149use timely::container::CapacityContainerBuilder;
150use timely::dataflow::channels::pact::{Exchange, Pipeline};
151use timely::dataflow::operators::vec::Broadcast;
152use timely::dataflow::operators::{Capability, CapabilitySet, probe};
153use timely::dataflow::{Scope, StreamVec};
154use timely::progress::Antichain;
155use tokio::sync::watch;
156use tracing::trace;
157
158use crate::compute_state::ComputeState;
159use crate::render::StartSignal;
160use crate::render::errors::DataflowErrorSer;
161use crate::render::sinks::SinkRender;
162use crate::sink::correction::{ChannelLogging, Correction, CorrectionLogger};
163use crate::sink::materialized_view_v2;
164use crate::sink::refresh::apply_refresh;
165use crate::sink::shared_batch::SharedBatchWriter;
166
167impl<'scope> SinkRender<'scope> for MaterializedViewSinkConnection<CollectionMetadata> {
168    fn render_sink(
169        &self,
170        compute_state: &mut ComputeState,
171        sink: &ComputeSinkDesc<CollectionMetadata>,
172        sink_id: GlobalId,
173        as_of: Antichain<Timestamp>,
174        start_signal: StartSignal,
175        mut ok_collection: VecCollection<'scope, Timestamp, Row, Diff>,
176        mut err_collection: VecCollection<'scope, Timestamp, DataflowErrorSer, Diff>,
177        output_probe: &Handle<Timestamp>,
178    ) -> Option<Rc<dyn Any>> {
179        // Attach probes reporting the compute frontier.
180        // The `apply_refresh` operator can round up frontiers, making it impossible to accurately
181        // track the progress of the computation, so we need to attach probes before it.
182        let probe = probe::Handle::default();
183        ok_collection = ok_collection
184            .probe_with(&probe)
185            .inner
186            .probe_notify_with(vec![output_probe.clone()])
187            .as_collection();
188        let collection_state = compute_state.expect_collection_mut(sink_id);
189        collection_state.compute_probe = Some(probe);
190
191        // If a `RefreshSchedule` was specified, round up timestamps.
192        if let Some(refresh_schedule) = &sink.refresh_schedule {
193            ok_collection = apply_refresh(ok_collection, refresh_schedule.clone());
194            err_collection = apply_refresh(err_collection, refresh_schedule.clone());
195        }
196
197        if sink.up_to != Antichain::default() {
198            unimplemented!(
199                "UP TO is not supported for persist sinks yet, and shouldn't have been accepted during parsing/planning"
200            )
201        }
202
203        let read_only_rx = collection_state.read_only_rx.clone();
204
205        let token = persist_sink(
206            sink_id,
207            &self.storage_metadata,
208            ok_collection,
209            err_collection,
210            as_of,
211            compute_state,
212            start_signal,
213            read_only_rx,
214        );
215        Some(token)
216    }
217}
218
219/// Type of the `desired` stream, split into `Ok` and `Err` streams.
220pub(super) type DesiredStreams<'s> = OkErr<
221    StreamVec<'s, Timestamp, (Row, Timestamp, Diff)>,
222    StreamVec<'s, Timestamp, (DataflowErrorSer, Timestamp, Diff)>,
223>;
224
225/// Type of the `persist` stream, split into `Ok` and `Err` streams.
226pub(super) type PersistStreams<'s> = OkErr<
227    StreamVec<'s, Timestamp, (Row, Timestamp, Diff)>,
228    StreamVec<'s, Timestamp, (DataflowErrorSer, Timestamp, Diff)>,
229>;
230
231/// Type of the `descs` stream.
232pub(super) type DescsStream<'s> = StreamVec<'s, Timestamp, BatchDescription>;
233
234/// Type of the `batches` stream.
235pub(super) type BatchesStream<'s> = StreamVec<'s, Timestamp, (BatchDescription, ProtoBatch)>;
236
237/// Type of the shared sink write frontier.
238pub(super) type SharedSinkFrontier = Rc<RefCell<Antichain<Timestamp>>>;
239
240/// Renders an MV sink writing the given desired collection into the `target` persist collection.
241pub(super) fn persist_sink<'s>(
242    sink_id: GlobalId,
243    target: &CollectionMetadata,
244    ok_collection: VecCollection<'s, Timestamp, Row, Diff>,
245    err_collection: VecCollection<'s, Timestamp, DataflowErrorSer, Diff>,
246    as_of: Antichain<Timestamp>,
247    compute_state: &mut ComputeState,
248    start_signal: StartSignal,
249    read_only_rx: watch::Receiver<bool>,
250) -> Rc<dyn Any>
251where
252{
253    if mz_compute_types::dyncfgs::ENABLE_SYNC_MV_SINK.get(&compute_state.worker_config) {
254        return materialized_view_v2::persist_sink(
255            sink_id,
256            target,
257            ok_collection,
258            err_collection,
259            as_of,
260            compute_state,
261            start_signal,
262            read_only_rx,
263        );
264    }
265
266    let scope = ok_collection.scope();
267    let desired = OkErr::new(ok_collection.inner, err_collection.inner);
268
269    // Read back the persist shard.
270    let (persist, persist_token) =
271        persist_source(scope, sink_id, target.clone(), compute_state, start_signal);
272
273    let persist_api = PersistApi {
274        persist_clients: Arc::clone(&compute_state.persist_clients),
275        persist_batches: compute_state.persist_batches.clone(),
276        collection: target.clone(),
277        shard_name: sink_id.to_string(),
278        purpose: format!("MV sink {sink_id}"),
279    };
280
281    let (desired, descs, sink_frontier, mint_token) = mint::render(
282        sink_id,
283        persist_api.clone(),
284        as_of.clone(),
285        read_only_rx.clone(),
286        desired,
287    );
288
289    let (batches, write_token) = write::render(
290        sink_id,
291        persist_api.clone(),
292        as_of,
293        desired,
294        persist,
295        descs.clone(),
296        read_only_rx,
297        Rc::clone(&compute_state.worker_config),
298    );
299
300    let append_token = append::render(sink_id, persist_api, descs, batches);
301
302    // Report sink frontier updates to the `ComputeState`.
303    let collection = compute_state.expect_collection_mut(sink_id);
304    collection.sink_write_frontier = Some(sink_frontier);
305
306    Rc::new((persist_token, mint_token, write_token, append_token))
307}
308
309/// Generic wrapper around ok/err pairs (e.g. streams, frontiers), to simplify code dealing with
310/// such pairs.
311pub(super) struct OkErr<O, E> {
312    pub(super) ok: O,
313    pub(super) err: E,
314}
315
316impl<O, E> OkErr<O, E> {
317    pub(super) fn new(ok: O, err: E) -> Self {
318        Self { ok, err }
319    }
320}
321
322impl OkErr<Antichain<Timestamp>, Antichain<Timestamp>> {
323    pub(super) fn new_frontiers() -> Self {
324        Self {
325            ok: Antichain::from_elem(Timestamp::MIN),
326            err: Antichain::from_elem(Timestamp::MIN),
327        }
328    }
329
330    /// Return the overall frontier, i.e., the minimum of `ok` and `err`.
331    pub(super) fn frontier(&self) -> &Antichain<Timestamp> {
332        if PartialOrder::less_equal(&self.ok, &self.err) {
333            &self.ok
334        } else {
335            &self.err
336        }
337    }
338}
339
340/// Advance the given `frontier` to `new`, if the latter one is greater.
341///
342/// Returns whether `frontier` was advanced.
343pub(super) fn advance(
344    frontier: &mut Antichain<Timestamp>,
345    new: timely::progress::frontier::AntichainRef<'_, Timestamp>,
346) -> bool {
347    if PartialOrder::less_than(&frontier.borrow(), &new) {
348        frontier.clear();
349        frontier.extend(new.iter().cloned());
350        true
351    } else {
352        false
353    }
354}
355
356/// A persist API specialized to a single collection.
357#[derive(Clone)]
358pub(super) struct PersistApi {
359    pub(super) persist_clients: Arc<PersistClientCache>,
360    pub(super) persist_batches: SharedBatches,
361    pub(super) collection: CollectionMetadata,
362    pub(super) shard_name: String,
363    pub(super) purpose: String,
364}
365
366impl PersistApi {
367    pub(super) async fn open_client(&self) -> PersistClient {
368        self.persist_clients
369            .open(self.collection.persist_location.clone())
370            .await
371            .unwrap_or_else(|error| panic!("error opening persist client: {error}"))
372    }
373
374    pub(super) async fn open_writer(&self) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
375        self.open_client()
376            .await
377            .open_writer(
378                self.collection.data_shard,
379                Arc::new(self.collection.relation_desc.clone()),
380                Arc::new(UnitSchema),
381                Diagnostics {
382                    shard_name: self.shard_name.clone(),
383                    handle_purpose: self.purpose.clone(),
384                },
385            )
386            .await
387            .unwrap_or_else(|error| panic!("error opening persist writer: {error}"))
388    }
389
390    async fn open_metrics(&self) -> SinkMetrics {
391        let client = self.open_client().await;
392        client.metrics().sink.clone()
393    }
394}
395
396/// Instantiate a persist source reading back the `target` collection.
397pub(super) fn persist_source<'s>(
398    scope: Scope<'s, Timestamp>,
399    sink_id: GlobalId,
400    target: CollectionMetadata,
401    compute_state: &ComputeState,
402    start_signal: StartSignal,
403) -> (PersistStreams<'s>, Vec<PressOnDropButton>) {
404    // There is no guarantee that the sink as-of is beyond the persist shard's since. If it isn't,
405    // instantiating a `persist_source` with it would panic. So instead we leave it to
406    // `persist_source` to select an appropriate as-of. We only care about times beyond the current
407    // shard upper anyway.
408    //
409    // TODO(teskje): Ideally we would select the as-of as `join(sink_as_of, since, upper)`, to
410    // allow `persist_source` to omit as much historical detail as possible. However, we don't know
411    // the shard frontiers and we cannot get them here as that requires an `async` context. We
412    // should consider extending the `persist_source` API to allow as-of selection based on the
413    // shard's current frontiers.
414    let as_of = None;
415
416    let until = Antichain::new();
417    let map_filter_project: Option<&mut MfpPlan> = None;
418
419    let (ok_stream, err_stream, token) = mz_storage_operators::persist_source::persist_source::<
420        DataflowErrorSer,
421        mz_storage_operators::persist_source::RowVecBuilder<Timestamp>,
422        _,
423    >(
424        scope,
425        sink_id,
426        Arc::clone(&compute_state.persist_clients),
427        &compute_state.txns_ctx,
428        target,
429        None,
430        as_of,
431        SnapshotMode::Include,
432        until,
433        map_filter_project,
434        compute_state.dataflow_max_inflight_bytes(),
435        start_signal.into_send_future(),
436        ErrorHandler::Halt("compute persist sink"),
437    );
438
439    let streams = OkErr::new(ok_stream, err_stream);
440    (streams, token)
441}
442
443/// A description for a batch of updates to be written.
444///
445/// Batch descriptions are produced by the `mint` operator and consumed by the `write` and `append`
446/// operators, where they inform which batches should be written or appended, respectively.
447///
448/// Each batch description also contains the index of its "append worker", i.e. the worker that is
449/// responsible for appending the written batches to the output shard.
450#[derive(Clone, Serialize, Deserialize)]
451pub(super) struct BatchDescription {
452    pub(super) lower: Antichain<Timestamp>,
453    pub(super) upper: Antichain<Timestamp>,
454    pub(super) append_worker: usize,
455    /// Identifies the shared batch all workers building this description contribute to.
456    pub(super) shared_id: SharedBatchId,
457}
458
459impl BatchDescription {
460    pub(super) fn new(
461        lower: Antichain<Timestamp>,
462        upper: Antichain<Timestamp>,
463        append_worker: usize,
464    ) -> Self {
465        assert!(PartialOrder::less_than(&lower, &upper));
466        Self {
467            lower,
468            upper,
469            append_worker,
470            shared_id: SharedBatchId::new(),
471        }
472    }
473}
474
475impl std::fmt::Debug for BatchDescription {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        write!(
478            f,
479            "({:?}, {:?})@{} {:?}",
480            self.lower.elements(),
481            self.upper.elements(),
482            self.append_worker,
483            self.shared_id,
484        )
485    }
486}
487
488/// Construct a name for the given sub-operator.
489pub(super) fn operator_name(sink_id: GlobalId, sub_operator: &str) -> String {
490    format!("mv_sink({sink_id})::{sub_operator}")
491}
492
493/// Implementation of the `mint` operator.
494mod mint {
495    use super::*;
496
497    /// Render the `mint` operator.
498    ///
499    /// The parameters passed in are:
500    ///  * `sink_id`: The `GlobalId` of the sink export.
501    ///  * `persist_api`: An object providing access to the output persist shard.
502    ///  * `as_of`: The first time for which the sink may produce output.
503    ///  * `read_only_tx`: A receiver that reports the sink is in read-only mode.
504    ///  * `desired`: The ok/err streams that should be sinked to persist.
505    pub fn render<'s>(
506        sink_id: GlobalId,
507        persist_api: PersistApi,
508        as_of: Antichain<Timestamp>,
509        mut read_only_rx: watch::Receiver<bool>,
510        desired: DesiredStreams<'s>,
511    ) -> (
512        DesiredStreams<'s>,
513        DescsStream<'s>,
514        SharedSinkFrontier,
515        PressOnDropButton,
516    ) {
517        let scope = desired.ok.scope();
518        let worker_id = scope.index();
519        let worker_count = scope.peers();
520
521        // Determine the active worker for the mint operator.
522        let active_worker_id = usize::cast_from(sink_id.hashed()) % scope.peers();
523
524        let sink_frontier = Rc::new(RefCell::new(Antichain::from_elem(Timestamp::MIN)));
525        let shared_frontier = Rc::clone(&sink_frontier);
526
527        let name = operator_name(sink_id, "mint");
528        let mut op = OperatorBuilder::new(name, scope);
529
530        let (ok_output, ok_stream) = op.new_output::<CapacityContainerBuilder<_>>();
531        let (err_output, err_stream) = op.new_output::<CapacityContainerBuilder<_>>();
532        let desired_outputs = OkErr::new(ok_output, err_output);
533        let desired_output_streams = OkErr::new(ok_stream, err_stream);
534
535        let (desc_output, desc_output_stream) = op.new_output::<CapacityContainerBuilder<_>>();
536
537        let mut desired_inputs = OkErr {
538            ok: op.new_input_for(desired.ok, Pipeline, &desired_outputs.ok),
539            err: op.new_input_for(desired.err, Pipeline, &desired_outputs.err),
540        };
541
542        let button = op.build(move |capabilities| async move {
543            // Passing through the `desired` streams only requires data capabilities, so we can
544            // immediately drop their initial capabilities here.
545            let [_, _, desc_cap]: [_; 3] =
546                capabilities.try_into().expect("one capability per output");
547
548            // Non-active workers just pass the `desired` and `persist` data through.
549            if worker_id != active_worker_id {
550                drop(desc_cap);
551                shared_frontier.borrow_mut().clear();
552
553                loop {
554                    tokio::select! {
555                        Some(event) = desired_inputs.ok.next() => {
556                            if let Event::Data(cap, mut data) = event {
557                                desired_outputs.ok.give_container(&cap, &mut data);
558                            }
559                        }
560                        Some(event) = desired_inputs.err.next() => {
561                            if let Event::Data(cap, mut data) = event {
562                                desired_outputs.err.give_container(&cap, &mut data);
563                            }
564                        }
565                        // All inputs are exhausted, so we can shut down.
566                        else => return,
567                    }
568                }
569            }
570
571            let mut cap_set = CapabilitySet::from_elem(desc_cap);
572
573            let read_only = *read_only_rx.borrow_and_update();
574            let mut state = State::new(sink_id, worker_count, as_of, read_only);
575
576            // Create a stream that reports advancements of the target shard's frontier and updates
577            // the shared sink frontier.
578            //
579            // We collect the persist frontier from a write handle directly, rather than inspecting
580            // the `persist` stream, because the latter has two annoying glitches:
581            //  (a) It starts at the shard's read frontier, not its write frontier.
582            //  (b) It can lag behind if there are spikes in ingested data.
583            //
584            // The decoupling from the `persist` stream is load-bearing: that stream can fall
585            // arbitrarily behind the shard upper during snapshot replay or write spikes. Using it
586            // would delay both (1) the controller-visible sink frontier (`shared_frontier`),
587            // which previously caused a CrossJoin feature-bench regression where the controller
588            // held a finished MV dataflow open waiting for the empty frontier, and (2) the
589            // `state.persist_frontier` that gates batch-description minting, stalling mint until
590            // the read-back stream catches up. The goal isn't tick-granular descriptions per
591            // se — it's avoiding the stream-induced stall. See 5eab5ff896 for the original
592            // regression and rationale.
593            let mut persist_frontiers = pin!(async_stream::stream! {
594                let mut writer = persist_api.open_writer().await;
595                let mut frontier = Antichain::from_elem(Timestamp::MIN);
596                while !frontier.is_empty() {
597                    writer.wait_for_upper_past(&frontier).await;
598                    frontier = writer.upper().clone();
599                    shared_frontier.borrow_mut().clone_from(&frontier);
600                    yield frontier.clone();
601                }
602            });
603
604            loop {
605                // Read from the inputs, pass through all data to the respective outputs, and keep
606                // track of the input frontiers. When a frontier advances we might have to mint a
607                // new batch description.
608                let maybe_desc = tokio::select! {
609                    Some(event) = desired_inputs.ok.next() => {
610                        match event {
611                            Event::Data(cap, mut data) => {
612                                desired_outputs.ok.give_container(&cap, &mut data);
613                                None
614                            }
615                            Event::Progress(frontier) => {
616                                state.advance_desired_ok_frontier(frontier);
617                                state.maybe_mint_batch_description()
618                            }
619                        }
620                    }
621                    Some(event) = desired_inputs.err.next() => {
622                        match event {
623                            Event::Data(cap, mut data) => {
624                                desired_outputs.err.give_container(&cap, &mut data);
625                                None
626                            }
627                            Event::Progress(frontier) => {
628                                state.advance_desired_err_frontier(frontier);
629                                state.maybe_mint_batch_description()
630                            }
631                        }
632                    }
633                    Some(frontier) = persist_frontiers.next() => {
634                        state.advance_persist_frontier(frontier);
635                        state.maybe_mint_batch_description()
636                    }
637                    Ok(()) = read_only_rx.changed(), if read_only => {
638                        state.allow_writes();
639                        state.maybe_mint_batch_description()
640                    }
641                    // All inputs are exhausted, so we can shut down.
642                    else => return,
643                };
644
645                if let Some(desc) = maybe_desc {
646                    let lower_ts = *desc.lower.as_option().expect("not empty");
647                    let cap = cap_set.delayed(&lower_ts);
648                    desc_output.give(&cap, desc);
649
650                    // We only emit strictly increasing `lower`s, so we can let our output frontier
651                    // advance beyond the current `lower`.
652                    cap_set.downgrade([lower_ts.step_forward()]);
653                } else {
654                    // The next emitted `lower` will be at least the `persist` frontier, so we can
655                    // advance our output frontier as far.
656                    let _ = cap_set.try_downgrade(state.persist_frontier.iter());
657                }
658            }
659        });
660
661        (
662            desired_output_streams,
663            desc_output_stream,
664            sink_frontier,
665            button.press_on_drop(),
666        )
667    }
668
669    /// State maintained by the `mint` operator.
670    struct State {
671        sink_id: GlobalId,
672        /// The number of workers in the Timely cluster.
673        worker_count: usize,
674        /// The frontiers of the `desired` inputs.
675        desired_frontiers: OkErr<Antichain<Timestamp>, Antichain<Timestamp>>,
676        /// The frontier of the target persist shard.
677        persist_frontier: Antichain<Timestamp>,
678        /// The append worker for the next batch description, chosen in round-robin fashion.
679        next_append_worker: usize,
680        /// The last `lower` we have emitted in a batch description, if any. Whenever the
681        /// `persist_frontier` moves beyond this frontier, we need to mint a new description.
682        last_lower: Option<Antichain<Timestamp>>,
683        /// Whether we are operating in read-only mode.
684        ///
685        /// In read-only mode, minting of batch descriptions is disabled.
686        read_only: bool,
687    }
688
689    impl State {
690        fn new(
691            sink_id: GlobalId,
692            worker_count: usize,
693            as_of: Antichain<Timestamp>,
694            read_only: bool,
695        ) -> Self {
696            // Initializing `persist_frontier` to the `as_of` ensures that the first minted batch
697            // description will have a `lower` of `as_of` or beyond, and thus that we don't spend
698            // work needlessly writing batches at previous times.
699            let persist_frontier = as_of;
700
701            Self {
702                sink_id,
703                worker_count,
704                desired_frontiers: OkErr::new_frontiers(),
705                persist_frontier,
706                next_append_worker: 0,
707                last_lower: None,
708                read_only,
709            }
710        }
711
712        fn trace<S: AsRef<str>>(&self, message: S) {
713            let message = message.as_ref();
714            trace!(
715                sink_id = %self.sink_id,
716                desired_frontier = ?self.desired_frontiers.frontier().elements(),
717                persist_frontier = ?self.persist_frontier.elements(),
718                last_lower = ?self.last_lower.as_ref().map(|f| f.elements()),
719                message,
720            );
721        }
722
723        fn advance_desired_ok_frontier(&mut self, frontier: Antichain<Timestamp>) {
724            if advance(&mut self.desired_frontiers.ok, frontier.borrow()) {
725                self.trace("advanced `desired` ok frontier");
726            }
727        }
728
729        fn advance_desired_err_frontier(&mut self, frontier: Antichain<Timestamp>) {
730            if advance(&mut self.desired_frontiers.err, frontier.borrow()) {
731                self.trace("advanced `desired` err frontier");
732            }
733        }
734
735        fn advance_persist_frontier(&mut self, frontier: Antichain<Timestamp>) {
736            if advance(&mut self.persist_frontier, frontier.borrow()) {
737                self.trace("advanced `persist` frontier");
738            }
739        }
740
741        fn allow_writes(&mut self) {
742            if self.read_only {
743                self.read_only = false;
744                self.trace("disabled read-only mode");
745            }
746        }
747
748        fn maybe_mint_batch_description(&mut self) -> Option<BatchDescription> {
749            let desired_frontier = self.desired_frontiers.frontier();
750            let persist_frontier = &self.persist_frontier;
751
752            // We only mint new batch descriptions when:
753            //  1. We are _not_ in read-only mode.
754            //  2. The `desired` frontier is ahead of the `persist` frontier.
755            //  3. The `persist` frontier advanced since we last emitted a batch description.
756            let desired_ahead = PartialOrder::less_than(persist_frontier, desired_frontier);
757            let persist_advanced = self.last_lower.as_ref().map_or(true, |lower| {
758                PartialOrder::less_than(lower, persist_frontier)
759            });
760
761            if self.read_only || !desired_ahead || !persist_advanced {
762                return None;
763            }
764
765            let lower = persist_frontier.clone();
766            let upper = desired_frontier.clone();
767            let append_worker = self.next_append_worker;
768            let desc = BatchDescription::new(lower, upper, append_worker);
769
770            self.next_append_worker = (append_worker + 1) % self.worker_count;
771            self.last_lower = Some(desc.lower.clone());
772
773            self.trace(format!("minted batch description: {desc:?}"));
774            Some(desc)
775        }
776    }
777}
778
779/// Implementation of the `write` operator.
780mod write {
781    use super::*;
782
783    /// Render the `write` operator.
784    ///
785    /// The parameters passed in are:
786    ///  * `sink_id`: The `GlobalId` of the sink export.
787    ///  * `persist_api`: An object providing access to the output persist shard.
788    ///  * `as_of`: The first time for which the sink may produce output.
789    ///  * `desired`: The ok/err streams that should be sinked to persist.
790    ///  * `persist`: The ok/err streams read back from the output persist shard.
791    ///  * `descs`: The stream of batch descriptions produced by the `mint` operator.
792    pub fn render<'s>(
793        sink_id: GlobalId,
794        persist_api: PersistApi,
795        as_of: Antichain<Timestamp>,
796        desired: DesiredStreams<'s>,
797        persist: PersistStreams<'s>,
798        descs: DescsStream<'s>,
799        mut read_only_rx: watch::Receiver<bool>,
800        worker_config: Rc<ConfigSet>,
801    ) -> (BatchesStream<'s>, PressOnDropButton) {
802        let scope = desired.ok.scope();
803        let worker_id = scope.index();
804
805        let name = operator_name(sink_id, "write");
806        let mut op = OperatorBuilder::new(name, scope.clone());
807
808        let mut channel_logging = None;
809        let mut correction_logger = None;
810        if let (Some(compute_logger), Some(differential_logger)) = (
811            scope.worker().logger_for("materialize/compute"),
812            scope.worker().logger_for("differential/arrange"),
813        ) {
814            let operator_info = op.operator_info();
815            let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
816            channel_logging = Some(ChannelLogging::new(tx));
817            correction_logger = Some(CorrectionLogger::new(
818                compute_logger,
819                differential_logger.into(),
820                operator_info.global_id,
821                operator_info.address.to_vec(),
822                rx,
823            ));
824        }
825
826        let (batches_output, batches_output_stream) =
827            op.new_output::<CapacityContainerBuilder<_>>();
828
829        // It is important that we exchange the `desired` and `persist` data the same way, so
830        // updates that cancel each other out end up on the same worker.
831        let exchange_ok = |(d, _, _): &(Row, Timestamp, Diff)| d.hashed();
832        let exchange_err = |(d, _, _): &(DataflowErrorSer, Timestamp, Diff)| d.hashed();
833
834        let mut desired_inputs = OkErr::new(
835            op.new_disconnected_input(desired.ok, Exchange::new(exchange_ok)),
836            op.new_disconnected_input(desired.err, Exchange::new(exchange_err)),
837        );
838        let mut persist_inputs = OkErr::new(
839            op.new_disconnected_input(persist.ok, Exchange::new(exchange_ok)),
840            op.new_disconnected_input(persist.err, Exchange::new(exchange_err)),
841        );
842        let mut descs_input = op.new_input_for(descs.broadcast(), Pipeline, &batches_output);
843
844        let button = op.build(move |capabilities| async move {
845            // We will use the data capabilities from the `descs` input to produce output, so no
846            // need to hold onto the initial capabilities.
847            drop(capabilities);
848
849            let writer = persist_api.open_writer().await;
850            let shared_batch_writer = SharedBatchWriter::new(
851                &persist_api,
852                &writer,
853                MV_SINK_SHARED_BATCH_MAX_BYTES.handle(&worker_config),
854            )
855            .await;
856            // The write operator needs the writer only for its schema id.
857            drop(writer);
858            let sink_metrics = persist_api.open_metrics().await;
859            let read_only = *read_only_rx.borrow_and_update();
860            let mut state = State::new(
861                sink_id,
862                worker_id,
863                shared_batch_writer,
864                sink_metrics,
865                channel_logging,
866                as_of,
867                read_only,
868                &worker_config,
869            );
870            let mut correction_logger = correction_logger;
871
872            loop {
873                // Drain correction logging events from the channel.
874                if let Some(logger) = &mut correction_logger {
875                    logger.apply_events();
876                }
877
878                // Read from the inputs, extract `desired` updates as positive contributions to
879                // `correction` and `persist` updates as negative contributions. If either the
880                // `desired` or `persist` frontier advances, or if we receive a new batch description,
881                // we might have to write a new batch.
882                let maybe_batch = tokio::select! {
883                    Some(event) = desired_inputs.ok.next() => {
884                        match event {
885                            Event::Data(_cap, mut data) => {
886                                state.corrections.ok.insert(&mut data);
887                                None
888                            }
889                            Event::Progress(frontier) => {
890                                state.advance_desired_ok_frontier(frontier);
891                                state.maybe_write_batch().await
892                            }
893                        }
894                    }
895                    Some(event) = desired_inputs.err.next() => {
896                        match event {
897                            Event::Data(_cap, mut data) => {
898                                state.corrections.err.insert(&mut data);
899                                None
900                            }
901                            Event::Progress(frontier) => {
902                                state.advance_desired_err_frontier(frontier);
903                                state.maybe_write_batch().await
904                            }
905                        }
906                    }
907                    Some(event) = persist_inputs.ok.next() => {
908                        match event {
909                            Event::Data(_cap, mut data) => {
910                                state.corrections.ok.insert_negated(&mut data);
911                                None
912                            }
913                            Event::Progress(frontier) => {
914                                state.advance_persist_ok_frontier(frontier);
915                                state.maybe_write_batch().await
916                            }
917                        }
918                    }
919                    Some(event) = persist_inputs.err.next() => {
920                        match event {
921                            Event::Data(_cap, mut data) => {
922                                state.corrections.err.insert_negated(&mut data);
923                                None
924                            }
925                            Event::Progress(frontier) => {
926                                state.advance_persist_err_frontier(frontier);
927                                state.maybe_write_batch().await
928                            }
929                        }
930                    }
931                    Some(event) = descs_input.next() => {
932                        match event {
933                            Event::Data(cap, data) => {
934                                for desc in data {
935                                    let cap = sole_capability(&cap).clone();
936                                    state.absorb_batch_description(desc, cap);
937                                }
938                                state.maybe_write_batch().await
939                            }
940                            Event::Progress(_frontier) => None,
941                        }
942                    }
943                    // Track read-only mode so the forced consolidation stops re-arming once
944                    // writes are allowed and the batch-write path takes over.
945                    Ok(()) = read_only_rx.changed(), if state.read_only => {
946                        if !*read_only_rx.borrow_and_update() {
947                            state.set_read_only(false);
948                        }
949                        None
950                    }
951                    // All inputs are exhausted, so we can shut down.
952                    else => return,
953                };
954
955                if let Some((desc, batches, cap)) = maybe_batch {
956                    for batch in batches {
957                        batches_output.give(&cap, (desc.clone(), batch));
958                    }
959                }
960            }
961        });
962
963        (batches_output_stream, button.press_on_drop())
964    }
965
966    /// State maintained by the `write` operator.
967    struct State {
968        sink_id: GlobalId,
969        worker_id: usize,
970        /// Coalesces batches across the workers in this process.
971        shared_batch_writer: SharedBatchWriter,
972        /// Contains `desired - persist`, reflecting the updates we would like to commit to
973        /// `persist` in order to "correct" it to track `desired`. This collection is only modified
974        /// by updates received from either the `desired` or `persist` inputs.
975        corrections: OkErr<Correction<Row>, Correction<DataflowErrorSer>>,
976        /// The frontiers of the `desired` inputs.
977        desired_frontiers: OkErr<Antichain<Timestamp>, Antichain<Timestamp>>,
978        /// The frontiers of the `persist` inputs.
979        ///
980        /// Note that this is _not_ the same as the write frontier of the output persist shard! It
981        /// usually is, but during snapshot processing, these frontiers will start at the shard's
982        /// read frontier, so they can be beyond its write frontier. This is important as it means
983        /// we must not discard batch descriptions based on these persist frontiers: A batch
984        /// description might still be valid even if its `lower` is before the persist frontiers we
985        /// observe.
986        persist_frontiers: OkErr<Antichain<Timestamp>, Antichain<Timestamp>>,
987        /// The current valid batch description and associated output capability, if any.
988        batch_description: Option<(BatchDescription, Capability<Timestamp>)>,
989        /// A request to force a consolidation of `corrections` once both `desired_frontiers` and
990        /// `persist_frontiers` become greater than the given frontier.
991        ///
992        /// Normally we force a consolidation whenever we write a batch, but there are periods
993        /// (like read-only mode) when that doesn't happen, and we need to manually force
994        /// consolidation instead. In read-only mode this is re-armed after each firing so it
995        /// keeps sweeping forward as the frontiers advance (see `maybe_force_consolidation`).
996        force_consolidation_after: Option<Antichain<Timestamp>>,
997        /// Whether the sink is in read-only mode. While read-only the `write` operator mints no
998        /// batches, so the batch-write path never sweeps `consolidate_before(upper)` forward; the
999        /// forced consolidation stands in for it and is re-armed as long as this holds.
1000        read_only: bool,
1001    }
1002
1003    impl State {
1004        fn new(
1005            sink_id: GlobalId,
1006            worker_id: usize,
1007            shared_batch_writer: SharedBatchWriter,
1008            metrics: SinkMetrics,
1009            logging: Option<ChannelLogging>,
1010            as_of: Antichain<Timestamp>,
1011            read_only: bool,
1012            worker_config: &ConfigSet,
1013        ) -> Self {
1014            let worker_metrics = metrics.for_worker(worker_id);
1015
1016            // Force a consolidation of `corrections` after the snapshot updates have been fully
1017            // processed, to ensure we get rid of those as quickly as possible.
1018            let force_consolidation_after = Some(as_of.clone());
1019
1020            let mut state = Self {
1021                sink_id,
1022                worker_id,
1023                shared_batch_writer,
1024                corrections: OkErr::new(
1025                    Correction::new(
1026                        metrics.clone(),
1027                        worker_metrics.clone(),
1028                        logging.clone(),
1029                        worker_config,
1030                    ),
1031                    Correction::new(metrics, worker_metrics, logging, worker_config),
1032                ),
1033                desired_frontiers: OkErr::new_frontiers(),
1034                persist_frontiers: OkErr::new_frontiers(),
1035                batch_description: None,
1036                force_consolidation_after,
1037                read_only,
1038            };
1039
1040            // Immediately advance the persist frontier tracking to the `as_of`.
1041            // This is important to ensure the persist sink doesn't get stuck if the output shard's
1042            // initial frontier is less than the `as_of`. The `mint` operator first emits a batch
1043            // description with `lower = as_of`, and the `write` operator only emits a batch when
1044            // its observed persist frontier is >= the batch description's `lower`, which (assuming
1045            // no other writers) would be never if we didn't advance the observed persist frontier
1046            // to the `as_of`.
1047            if MV_SINK_ADVANCE_PERSIST_FRONTIERS.get(worker_config) {
1048                state.advance_persist_ok_frontier(as_of.clone());
1049                state.advance_persist_err_frontier(as_of);
1050            }
1051
1052            state
1053        }
1054
1055        fn trace<S: AsRef<str>>(&self, message: S) {
1056            let message = message.as_ref();
1057            trace!(
1058                sink_id = %self.sink_id,
1059                worker = %self.worker_id,
1060                desired_frontier = ?self.desired_frontiers.frontier().elements(),
1061                persist_frontier = ?self.persist_frontiers.frontier().elements(),
1062                batch_description = ?self.batch_description.as_ref().map(|(d, _)| d),
1063                message,
1064            );
1065        }
1066
1067        fn advance_desired_ok_frontier(&mut self, frontier: Antichain<Timestamp>) {
1068            if advance(&mut self.desired_frontiers.ok, frontier.borrow()) {
1069                self.apply_desired_frontier_advancement();
1070                self.trace("advanced `desired` ok frontier");
1071            }
1072        }
1073
1074        fn advance_desired_err_frontier(&mut self, frontier: Antichain<Timestamp>) {
1075            if advance(&mut self.desired_frontiers.err, frontier.borrow()) {
1076                self.apply_desired_frontier_advancement();
1077                self.trace("advanced `desired` err frontier");
1078            }
1079        }
1080
1081        fn advance_persist_ok_frontier(&mut self, frontier: Antichain<Timestamp>) {
1082            if advance(&mut self.persist_frontiers.ok, frontier.borrow()) {
1083                self.apply_persist_frontier_advancement();
1084                self.trace("advanced `persist` ok frontier");
1085            }
1086        }
1087
1088        fn advance_persist_err_frontier(&mut self, frontier: Antichain<Timestamp>) {
1089            if advance(&mut self.persist_frontiers.err, frontier.borrow()) {
1090                self.apply_persist_frontier_advancement();
1091                self.trace("advanced `persist` err frontier");
1092            }
1093        }
1094
1095        /// Apply the effects of a previous `desired` frontier advancement.
1096        fn apply_desired_frontier_advancement(&mut self) {
1097            self.maybe_force_consolidation();
1098        }
1099
1100        /// Apply the effects of a previous `persist` frontier advancement.
1101        fn apply_persist_frontier_advancement(&mut self) {
1102            let frontier = self.persist_frontiers.frontier();
1103
1104            // We will only emit times at or after the `persist` frontier, so now is a good time to
1105            // advance the times of stashed updates.
1106            self.corrections.ok.advance_since(frontier.clone());
1107            self.corrections.err.advance_since(frontier.clone());
1108
1109            self.maybe_force_consolidation();
1110        }
1111
1112        /// If the current consolidation request has become applicable, apply it.
1113        fn maybe_force_consolidation(&mut self) {
1114            let Some(request) = &self.force_consolidation_after else {
1115                return;
1116            };
1117
1118            let desired_frontier = self.desired_frontiers.frontier();
1119            let persist_frontier = self.persist_frontiers.frontier();
1120            if PartialOrder::less_than(request, desired_frontier)
1121                && PartialOrder::less_than(request, persist_frontier)
1122            {
1123                self.trace("forcing correction consolidation");
1124                self.corrections.ok.consolidate_at_since();
1125                self.corrections.err.consolidate_at_since();
1126
1127                // In read-only mode the sink mints no batches, so the batch-write path never
1128                // sweeps `consolidate_before(upper)` forward and a single forced consolidation
1129                // only covers the band below the `since` at the moment it fires, leaving the
1130                // forward region uncancelled for the whole read-only window (CLU-131). Re-arm at
1131                // the current persist frontier so the forced consolidation keeps sweeping forward
1132                // as `desired`/`persist` advance. Once writes are allowed, the batch-write path
1133                // takes over and we disarm.
1134                self.force_consolidation_after = if self.read_only {
1135                    Some(persist_frontier.to_owned())
1136                } else {
1137                    None
1138                };
1139            }
1140        }
1141
1142        /// Update read-only mode. While read-only the forced consolidation re-arms itself; once
1143        /// writes are allowed the still-armed request fires one final time and then disarms, after
1144        /// which the batch-write path drives consolidation.
1145        fn set_read_only(&mut self, read_only: bool) {
1146            self.read_only = read_only;
1147        }
1148
1149        fn absorb_batch_description(&mut self, desc: BatchDescription, cap: Capability<Timestamp>) {
1150            // The incoming batch description is outdated if we already have a batch description
1151            // with a greater `lower`.
1152            //
1153            // Note that we cannot assume a description is outdated based on the comparison of its
1154            // `lower` with the `persist_frontier`. The persist frontier observed by the `write`
1155            // operator is initialized with the shard's read frontier, so it can be greater than
1156            // the shard's write frontier.
1157            if let Some((prev, _)) = &self.batch_description {
1158                if PartialOrder::less_than(&desc.lower, &prev.lower) {
1159                    self.trace(format!("skipping outdated batch description: {desc:?}"));
1160                    return;
1161                }
1162            }
1163
1164            self.batch_description = Some((desc, cap));
1165            self.trace("set batch description");
1166        }
1167
1168        async fn maybe_write_batch(
1169            &mut self,
1170        ) -> Option<(BatchDescription, Vec<ProtoBatch>, Capability<Timestamp>)> {
1171            let (desc, _cap) = self.batch_description.as_ref()?;
1172
1173            // We can write a new batch if we have seen all `persist` updates before `lower` and
1174            // all `desired` updates up to `upper`.
1175            let persist_complete =
1176                PartialOrder::less_equal(&desc.lower, self.persist_frontiers.frontier());
1177            let desired_complete =
1178                PartialOrder::less_equal(&desc.upper, self.desired_frontiers.frontier());
1179            if !persist_complete || !desired_complete {
1180                return None;
1181            }
1182
1183            let (desc, cap) = self.batch_description.take()?;
1184
1185            let ok_updates = self.corrections.ok.updates_before(&desc.upper);
1186            let err_updates = self.corrections.err.updates_before(&desc.upper);
1187
1188            let oks = ok_updates.map(|(d, t, r)| ((SourceData(Ok(d)), ()), t, r.into_inner()));
1189            let errs = err_updates
1190                .map(|(d, t, r)| ((SourceData(Err(d.deserialize())), ()), t, r.into_inner()));
1191            let mut updates = oks.chain(errs).peekable();
1192
1193            // Don't write empty batches.
1194            if updates.peek().is_none() {
1195                drop(updates);
1196                self.trace("skipping empty batch");
1197                return None;
1198            }
1199
1200            let mut write = self.shared_batch_writer.start(&desc);
1201            for (kv, t, d) in updates {
1202                write.push(&kv, t, d).await;
1203            }
1204            let batches = write.finish().await;
1205            if batches.is_empty() {
1206                self.trace("contributed to a shared batch");
1207                return None;
1208            }
1209
1210            self.trace(format!("wrote {} batches", batches.len()));
1211            Some((desc, batches, cap))
1212        }
1213    }
1214}
1215
1216/// Implementation of the `append` operator.
1217mod append {
1218    use super::*;
1219
1220    /// Render the `append` operator.
1221    ///
1222    /// The parameters passed in are:
1223    ///  * `sink_id`: The `GlobalId` of the sink export.
1224    ///  * `persist_api`: An object providing access to the output persist shard.
1225    ///  * `descs`: The stream of batch descriptions produced by the `mint` operator.
1226    ///  * `batches`: The stream of written batches produced by the `write` operator.
1227    pub fn render<'s>(
1228        sink_id: GlobalId,
1229        persist_api: PersistApi,
1230        descs: DescsStream<'s>,
1231        batches: BatchesStream<'s>,
1232    ) -> PressOnDropButton {
1233        let scope = descs.scope();
1234        let worker_id = scope.index();
1235
1236        let name = operator_name(sink_id, "append");
1237        let mut op = OperatorBuilder::new(name, scope);
1238
1239        // Broadcast batch descriptions to all workers, regardless of whether or not they are
1240        // responsible for the append, to give them a chance to clean up any outdated state they
1241        // might still hold.
1242        let mut descs_input = op.new_disconnected_input(descs.broadcast(), Pipeline);
1243        let mut batches_input = op.new_disconnected_input(
1244            batches,
1245            Exchange::new(move |(desc, _): &(BatchDescription, _)| {
1246                u64::cast_from(desc.append_worker)
1247            }),
1248        );
1249
1250        let button = op.build(move |_capabilities| async move {
1251            let writer = persist_api.open_writer().await;
1252            let mut state = State::new(sink_id, worker_id, writer);
1253
1254            loop {
1255                // Read from the inputs, absorb batch descriptions and batches. If the `batches`
1256                // frontier advances, or if we receive a new batch description, we might have to
1257                // append a new batch.
1258                tokio::select! {
1259                    Some(event) = descs_input.next() => {
1260                        if let Event::Data(_cap, data) = event {
1261                            for desc in data {
1262                                state.absorb_batch_description(desc).await;
1263                                state.maybe_append_batches().await;
1264                            }
1265                        }
1266                    }
1267                    Some(event) = batches_input.next() => {
1268                        match event {
1269                            Event::Data(_cap, data) => {
1270                                // The batch description is only used for routing and we ignore it
1271                                // here since we already get one from `descs_input`.
1272                                for (_desc, batch) in data {
1273                                    state.absorb_batch(batch).await;
1274                                }
1275                            }
1276                            Event::Progress(frontier) => {
1277                                state.advance_batches_frontier(frontier);
1278                                state.maybe_append_batches().await;
1279                            }
1280                        }
1281                    }
1282                    // All inputs are exhausted, so we can shut down.
1283                    else => return,
1284                }
1285            }
1286        });
1287
1288        button.press_on_drop()
1289    }
1290
1291    /// State maintained by the `append` operator.
1292    struct State {
1293        sink_id: GlobalId,
1294        worker_id: usize,
1295        persist_writer: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
1296        /// The current input frontier of `batches`.
1297        batches_frontier: Antichain<Timestamp>,
1298        /// The greatest observed `lower` from both `descs` and `batches`.
1299        lower: Antichain<Timestamp>,
1300        /// The batch description for `lower`, if any.
1301        batch_description: Option<BatchDescription>,
1302        /// Batches received for `lower`.
1303        batches: Vec<Batch<SourceData, (), Timestamp, StorageDiff>>,
1304    }
1305
1306    impl State {
1307        fn new(
1308            sink_id: GlobalId,
1309            worker_id: usize,
1310            persist_writer: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
1311        ) -> Self {
1312            Self {
1313                sink_id,
1314                worker_id,
1315                persist_writer,
1316                batches_frontier: Antichain::from_elem(Timestamp::MIN),
1317                lower: Antichain::from_elem(Timestamp::MIN),
1318                batch_description: None,
1319                batches: Default::default(),
1320            }
1321        }
1322
1323        fn trace<S: AsRef<str>>(&self, message: S) {
1324            let message = message.as_ref();
1325            trace!(
1326                sink_id = %self.sink_id,
1327                worker = %self.worker_id,
1328                batches_frontier = ?self.batches_frontier.elements(),
1329                lower = ?self.lower.elements(),
1330                batch_description = ?self.batch_description,
1331                message,
1332            );
1333        }
1334
1335        fn advance_batches_frontier(&mut self, frontier: Antichain<Timestamp>) {
1336            if advance(&mut self.batches_frontier, frontier.borrow()) {
1337                self.trace("advanced `batches` frontier");
1338            }
1339        }
1340
1341        /// Advance the current `lower`.
1342        ///
1343        /// Discards all currently stashed batches and batch descriptions, assuming that they are
1344        /// now invalid.
1345        async fn advance_lower(&mut self, frontier: Antichain<Timestamp>) {
1346            assert!(PartialOrder::less_than(&self.lower, &frontier));
1347
1348            self.lower = frontier;
1349            self.batch_description = None;
1350
1351            // Remove stashed batches, cleaning up those we didn't append.
1352            for batch in self.batches.drain(..) {
1353                batch.delete().await;
1354            }
1355
1356            self.trace("advanced `lower`");
1357        }
1358
1359        /// Absorb the given batch description into the state, provided it is not outdated.
1360        async fn absorb_batch_description(&mut self, desc: BatchDescription) {
1361            if PartialOrder::less_than(&self.lower, &desc.lower) {
1362                self.advance_lower(desc.lower.clone()).await;
1363            } else if &self.lower != &desc.lower {
1364                self.trace(format!("skipping outdated batch description: {desc:?}"));
1365                return;
1366            }
1367
1368            if desc.append_worker == self.worker_id {
1369                self.batch_description = Some(desc);
1370                self.trace("set batch description");
1371            }
1372        }
1373
1374        /// Absorb the given batch into the state, provided it is not outdated.
1375        async fn absorb_batch(&mut self, batch: ProtoBatch) {
1376            let batch = self.persist_writer.batch_from_transmittable_batch(batch);
1377            if PartialOrder::less_than(&self.lower, batch.lower()) {
1378                self.advance_lower(batch.lower().clone()).await;
1379            } else if &self.lower != batch.lower() {
1380                self.trace(format!(
1381                    "skipping outdated batch: ({:?}, {:?})",
1382                    batch.lower().elements(),
1383                    batch.upper().elements(),
1384                ));
1385
1386                // Ensure the batch's data gets properly cleaned up before dropping it.
1387                batch.delete().await;
1388                return;
1389            }
1390
1391            self.batches.push(batch);
1392            self.trace("absorbed a batch");
1393        }
1394
1395        async fn maybe_append_batches(&mut self) {
1396            let batches_complete = PartialOrder::less_than(&self.lower, &self.batches_frontier);
1397            if !batches_complete {
1398                return;
1399            }
1400
1401            let Some(desc) = self.batch_description.take() else {
1402                return;
1403            };
1404
1405            let new_lower = match self.append_batches(desc).await {
1406                Ok(shard_upper) => {
1407                    self.trace("appended a batch");
1408                    shard_upper
1409                }
1410                Err(shard_upper) => {
1411                    // Failing the append is expected in the presence of concurrent replicas. There
1412                    // is nothing special to do here: The self-correcting feedback mechanism
1413                    // ensures that we observe the concurrent changes, compute their consequences,
1414                    // and append them at a future time.
1415                    self.trace(format!(
1416                        "append failed due to `lower` mismatch: {:?}",
1417                        shard_upper.elements(),
1418                    ));
1419                    shard_upper
1420                }
1421            };
1422
1423            self.advance_lower(new_lower).await;
1424        }
1425
1426        /// Append the current `batches` to the output shard.
1427        ///
1428        /// Returns whether the append was successful or not, and the current shard upper in either
1429        /// case.
1430        ///
1431        /// This method advances the shard upper to the batch `lower` if necessary. This is the
1432        /// mechanism that brings the shard upper to the sink as-of when appending the initial
1433        /// batch.
1434        ///
1435        /// An alternative mechanism for bringing the shard upper to the sink as-of would be making
1436        /// a single append at operator startup. The reason we are doing it here instead is that it
1437        /// simplifies the implementation of read-only mode. In read-only mode we have to defer any
1438        /// persist writes, including the initial upper bump. Having only a single place that
1439        /// performs writes makes it easy to ensure we are doing that correctly.
1440        async fn append_batches(
1441            &mut self,
1442            desc: BatchDescription,
1443        ) -> Result<Antichain<Timestamp>, Antichain<Timestamp>> {
1444            let (lower, upper) = (desc.lower, desc.upper);
1445            let mut to_append: Vec<_> = self.batches.iter_mut().collect();
1446
1447            loop {
1448                let result = self
1449                    .persist_writer
1450                    .compare_and_append_batch(&mut to_append, lower.clone(), upper.clone(), true)
1451                    .await
1452                    .expect("valid usage");
1453
1454                match result {
1455                    Ok(()) => return Ok(upper),
1456                    Err(mismatch) if PartialOrder::less_than(&mismatch.current, &lower) => {
1457                        advance_shard_upper(&mut self.persist_writer, lower.clone()).await;
1458
1459                        // At this point the shard's since and upper are likely the same, a state
1460                        // that is likely to hit edge-cases in logic reasoning about frontiers.
1461                        fail::fail_point!("mv_advanced_upper");
1462                    }
1463                    Err(mismatch) => return Err(mismatch.current),
1464                }
1465            }
1466        }
1467    }
1468
1469    /// Advance the frontier of the given writer's shard to at least the given `upper`.
1470    async fn advance_shard_upper(
1471        persist_writer: &mut WriteHandle<SourceData, (), Timestamp, StorageDiff>,
1472        upper: Antichain<Timestamp>,
1473    ) {
1474        let empty_updates: &[((SourceData, ()), Timestamp, StorageDiff)] = &[];
1475        let lower = Antichain::from_elem(Timestamp::MIN);
1476        persist_writer
1477            .append(empty_updates, lower, upper)
1478            .await
1479            .expect("valid usage")
1480            .expect("should always succeed");
1481    }
1482}