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