Skip to main content

mz_storage/
upsert.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::cell::RefCell;
11use std::cmp::Reverse;
12use std::convert::AsRef;
13use std::fmt::Debug;
14use std::hash::{Hash, Hasher};
15use std::path::PathBuf;
16use std::sync::Arc;
17
18use differential_dataflow::hashable::Hashable;
19use differential_dataflow::{AsCollection, VecCollection};
20use futures::StreamExt;
21use futures::future::FutureExt;
22use indexmap::map::Entry;
23use itertools::Itertools;
24use mz_ore::error::ErrorExt;
25use mz_repr::{Datum, DatumVec, Diff, GlobalId, Row};
26use mz_rocksdb::ValueIterator;
27use mz_sql_server_util::cdc::Lsn;
28use mz_storage_operators::metrics::BackpressureMetrics;
29use mz_storage_types::configuration::StorageConfiguration;
30use mz_storage_types::dyncfgs;
31use mz_storage_types::errors::{DataflowError, EnvelopeError, UpsertError};
32use mz_storage_types::sources::MzOffset;
33use mz_storage_types::sources::envelope::UpsertEnvelope;
34use mz_storage_types::sources::kafka::{KafkaTimestamp, RangeBound};
35use mz_storage_types::sources::mysql::GtidPartition;
36use mz_timely_util::builder_async::{
37    AsyncOutputHandle, Event as AsyncEvent, OperatorBuilder as AsyncOperatorBuilder,
38    PressOnDropButton,
39};
40use serde::{Deserialize, Serialize};
41use sha2::{Digest, Sha256};
42use timely::dataflow::channels::pact::Exchange;
43use timely::dataflow::operators::{Capability, InputCapability, Operator};
44use timely::dataflow::{Scope, StreamVec};
45use timely::order::{PartialOrder, TotalOrder};
46use timely::progress::timestamp::Refines;
47use timely::progress::{Antichain, Timestamp};
48
49use crate::healthcheck::HealthStatusUpdate;
50use crate::metrics::upsert::UpsertMetrics;
51use crate::storage_state::StorageInstanceContext;
52use crate::{upsert_continual_feedback, upsert_continual_feedback_v2};
53use types::{
54    BincodeOpts, StateValue, UpsertState, UpsertStateBackend, consolidating_merge_function,
55    upsert_bincode_opts,
56};
57
58#[cfg(any(test, feature = "fuzzing"))]
59pub mod memory;
60pub(crate) mod rocksdb;
61// TODO(aljoscha): Move next to upsert module, rename to upsert_types.
62pub(crate) mod types;
63
64pub type UpsertValue = Result<Row, Box<UpsertError>>;
65
66#[derive(
67    Copy,
68    Clone,
69    Hash,
70    PartialEq,
71    Eq,
72    PartialOrd,
73    Ord,
74    Serialize,
75    Deserialize,
76    bytemuck::AnyBitPattern,
77    bytemuck::NoUninit
78)]
79#[repr(transparent)]
80pub struct UpsertKey([u8; 32]);
81
82impl columnation::Columnation for UpsertKey {
83    type InnerRegion = columnation::CopyRegion<UpsertKey>;
84}
85
86/// Columnar (the `columnar` crate, distinct from `columnation`) support for
87/// `UpsertKey`, so the upsert-v2 source stash can use a paged columnar merge
88/// batcher keyed natively by `UpsertKey` (no `Row` packing). `UpsertKey` is a
89/// POD `[u8; 32]` newtype, so the container is a fixed-stride byte column.
90///
91/// This is hand-rolled rather than `#[derive(Columnar)]`d for one load-bearing
92/// reason: the reference type must be `&UpsertKey`. `&UpsertKey` is `Copy + Ord`
93/// (the lexicographic `[u8; 32]` order the persist-feedback trace is keyed on),
94/// which both satisfies the merge batcher's `Ref: Copy + Ord` requirement and —
95/// crucially — matches the read item of the feedback arrangement's
96/// `ColumnationStack<UpsertKey>` key container, so the paged `ValRow` builder
97/// can reconcile the `Column` input against the spine (its `BuilderInput` bound
98/// is `ReadItem: PartialEq<Ref<UpsertKey>>`). A derived impl would yield a
99/// generated `UpsertKeyReference` (and route `[u8; 32]` through the generic,
100/// non-fixed-stride array container), breaking that reconciliation.
101mod columnar_upsert_key {
102    use super::UpsertKey;
103    use columnar::Columnar;
104    use mz_ore::cast::CastFrom;
105    use std::ops::Range;
106
107    /// A newtype wrapper for a vector of `UpsertKey` values.
108    #[derive(Clone, Copy, Default, Debug)]
109    pub struct UpsertKeys<T>(T);
110    impl<D, T: columnar::Push<D>> columnar::Push<D> for UpsertKeys<T> {
111        #[inline(always)]
112        fn push(&mut self, item: D) {
113            self.0.push(item)
114        }
115    }
116    impl<T: columnar::Clear> columnar::Clear for UpsertKeys<T> {
117        #[inline(always)]
118        fn clear(&mut self) {
119            self.0.clear()
120        }
121    }
122    impl<T: columnar::Len> columnar::Len for UpsertKeys<T> {
123        #[inline(always)]
124        fn len(&self) -> usize {
125            self.0.len()
126        }
127    }
128    impl<'a> columnar::Index for UpsertKeys<&'a [UpsertKey]> {
129        type Ref = &'a UpsertKey;
130
131        #[inline(always)]
132        fn get(&self, index: usize) -> Self::Ref {
133            &self.0[index]
134        }
135    }
136
137    impl Columnar for UpsertKey {
138        #[inline(always)]
139        fn into_owned<'a>(other: columnar::Ref<'a, Self>) -> Self {
140            *other
141        }
142        type Container = UpsertKeys<Vec<UpsertKey>>;
143        #[inline(always)]
144        fn reborrow<'b, 'a: 'b>(thing: columnar::Ref<'a, Self>) -> columnar::Ref<'b, Self>
145        where
146            Self: 'a,
147        {
148            thing
149        }
150    }
151
152    impl columnar::Borrow for UpsertKeys<Vec<UpsertKey>> {
153        type Ref<'a> = &'a UpsertKey;
154        type Borrowed<'a>
155            = UpsertKeys<&'a [UpsertKey]>
156        where
157            Self: 'a;
158        #[inline(always)]
159        fn borrow<'a>(&'a self) -> Self::Borrowed<'a> {
160            UpsertKeys(self.0.as_slice())
161        }
162        #[inline(always)]
163        fn reborrow<'b, 'a: 'b>(item: Self::Borrowed<'a>) -> Self::Borrowed<'b>
164        where
165            Self: 'a,
166        {
167            UpsertKeys(item.0)
168        }
169        #[inline(always)]
170        fn reborrow_ref<'b, 'a: 'b>(item: Self::Ref<'a>) -> Self::Ref<'b>
171        where
172            Self: 'a,
173        {
174            item
175        }
176    }
177
178    impl columnar::Container for UpsertKeys<Vec<UpsertKey>> {
179        #[inline(always)]
180        fn extend_from_self(&mut self, other: Self::Borrowed<'_>, range: Range<usize>) {
181            self.0.extend_from_self(other.0, range)
182        }
183        #[inline(always)]
184        fn reserve_for<'a, I>(&mut self, selves: I)
185        where
186            Self: 'a,
187            I: Iterator<Item = Self::Borrowed<'a>> + Clone,
188        {
189            self.0.reserve_for(selves.map(|s| s.0));
190        }
191    }
192
193    impl<'a> columnar::AsBytes<'a> for UpsertKeys<&'a [UpsertKey]> {
194        const SLICE_COUNT: usize = 1;
195        #[inline(always)]
196        fn get_byte_slice(&self, index: usize) -> (u64, &'a [u8]) {
197            debug_assert!(index < Self::SLICE_COUNT);
198            (
199                u64::cast_from(align_of::<UpsertKey>()),
200                bytemuck::cast_slice(self.0),
201            )
202        }
203        #[inline(always)]
204        fn as_bytes(&self) -> impl Iterator<Item = (u64, &'a [u8])> {
205            std::iter::once((
206                u64::cast_from(align_of::<UpsertKey>()),
207                bytemuck::cast_slice(self.0),
208            ))
209        }
210    }
211    impl<'a> columnar::FromBytes<'a> for UpsertKeys<&'a [UpsertKey]> {
212        const SLICE_COUNT: usize = 1;
213        #[inline(always)]
214        fn from_bytes(bytes: &mut impl Iterator<Item = &'a [u8]>) -> Self {
215            UpsertKeys(bytemuck::cast_slice(
216                bytes.next().expect("Iterator exhausted prematurely"),
217            ))
218        }
219    }
220}
221
222/// Projects a source's native `FromTime` to a columnar, totally-ordered key
223/// used by the upsert source stash to keep the latest update per `(key, time)`.
224///
225/// The upsert stash is a paged columnar merge batcher; its diff carries this
226/// projection rather than the raw `FromTime`, so the only columnar type the
227/// stash needs is `Order` — never the (possibly structurally complex) source
228/// timestamp. This is what keeps the columnar requirement off the generic
229/// source-render path: that path only ever needs `FromTime: UpsertSourceTime`.
230/// Only the relative order matters; the value is never read back.
231///
232/// The upsert envelope is rendered for Kafka and the KEY VALUE load generator,
233/// so those source times (`KafkaTimestamp`, `MzOffset`) project to a real order
234/// key. The remaining source times implement the trait only for coherence on
235/// the generic render path — their sources never render upsert — so their
236/// projection is a panicking guard rather than a real key.
237pub trait UpsertSourceTime
238where
239    for<'a> columnar::Ref<'a, Self::Order>: Ord,
240{
241    /// Columnar order key. Must order consistently with the source time it is
242    /// projected from.
243    type Order: columnar::Columnar + Clone + Default + Ord + Send + Sync + 'static;
244    /// Project the source time onto its order key.
245    fn upsert_order(&self) -> Self::Order;
246}
247
248impl UpsertSourceTime for KafkaTimestamp {
249    /// Per-record Kafka source times are exact singletons (a single partition
250    /// at a single offset; see the source reader), and `KafkaTimestamp`'s
251    /// derived `Ord` is lexicographic on `(partition, offset)`, so this flat
252    /// projection is order-preserving. `RangeBound`'s infinities map to the
253    /// `i64` extrema to remain order-consistent for any non-singleton bound.
254    type Order = (i64, u64);
255    fn upsert_order(&self) -> (i64, u64) {
256        let partition = match self.interval().lower {
257            RangeBound::NegInfinity => i64::MIN,
258            RangeBound::Elem(p, _) => i64::from(p),
259            RangeBound::PosInfinity => i64::MAX,
260        };
261        (partition, self.timestamp().offset)
262    }
263}
264
265/// Load-generator (and Postgres) source time. The KEY VALUE load generator is
266/// the one non-Kafka source that renders the upsert envelope (see
267/// `apply_source_envelope_encoding` in the planner), so this projects to the
268/// record offset: offsets increase with each update, so "max order wins" is
269/// exactly "latest update wins" for dedup.
270impl UpsertSourceTime for MzOffset {
271    type Order = u64;
272    fn upsert_order(&self) -> u64 {
273        self.offset
274    }
275}
276
277/// Source times whose sources never render the upsert envelope (MySQL and SQL
278/// Server CDC). `Order = ()` keeps the generic render path free of any columnar
279/// requirement on the source time. The projection panics rather than returning:
280/// `()` would collapse every `from_time` to equal, silently breaking "latest
281/// offset wins" dedup, so if such a source ever reaches the upsert path we want
282/// a loud failure, not arbitrary per-key output.
283macro_rules! upsert_source_time_unit {
284    ($($ty:ty),+ $(,)?) => {$(
285        impl UpsertSourceTime for $ty {
286            type Order = ();
287            fn upsert_order(&self) {
288                unreachable!(
289                    "upsert source stash is not rendered for this source, but \
290                     {} reached the projection",
291                    std::any::type_name::<Self>(),
292                )
293            }
294        }
295    )+};
296}
297upsert_source_time_unit!(GtidPartition, Lsn);
298
299/// Pager for the upsert-v2 source stash.
300///
301/// This draws from the same process-wide [`TieredPolicy`] budget pool as the
302/// compute column-paged batcher — there is one budget and one underlying
303/// `mz_ore::pager` — but whether the stash *uses* it is gated by storage's own
304/// `enable_upsert_paged_spill` flag, independently of compute's
305/// `enable_column_paged_batcher_spill`. The shared pool's budget / backend /
306/// codec are configured by compute's `apply_tiered_config` (storage and compute
307/// run in the same `clusterd` process).
308///
309/// [`TieredPolicy`]: mz_timely_util::column_pager::policy::TieredPolicy
310pub mod upsert_stash_pager {
311    use std::sync::{LazyLock, RwLock};
312
313    use mz_timely_util::column_pager::{ColumnPager, shared_pager};
314
315    /// Active pager handed to upsert source-stash batchers. Defaults to
316    /// disabled (every chunk resident) until [`set_enabled`] turns it on.
317    static PAGER: LazyLock<RwLock<ColumnPager>> =
318        LazyLock::new(|| RwLock::new(ColumnPager::disabled()));
319
320    /// Enable or disable the stash's use of the shared column pager. When
321    /// enabled, the stash spills through the shared budget pool; when disabled
322    /// it keeps every chunk resident.
323    pub fn set_enabled(enabled: bool) {
324        *PAGER.write().expect("upsert stash pager poisoned") = shared_pager(enabled);
325    }
326
327    /// The current upsert-stash pager. Cheap: clones the inner `Arc`.
328    pub fn pager() -> ColumnPager {
329        PAGER.read().expect("upsert stash pager poisoned").clone()
330    }
331}
332
333impl Debug for UpsertKey {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        write!(f, "0x")?;
336        for byte in self.0 {
337            write!(f, "{:02x}", byte)?;
338        }
339        Ok(())
340    }
341}
342
343impl AsRef<[u8]> for UpsertKey {
344    #[inline(always)]
345    // Note we do 1 `multi_get` and 1 `multi_put` while processing a _batch of updates_. Within the
346    // batch, we effectively consolidate each key, before persisting that consolidated value.
347    // Easy!!
348    fn as_ref(&self) -> &[u8] {
349        &self.0
350    }
351}
352
353impl From<&[u8]> for UpsertKey {
354    fn from(bytes: &[u8]) -> Self {
355        UpsertKey(bytes.try_into().expect("invalid key length"))
356    }
357}
358
359/// The hash function used to map upsert keys. It is important that this hash is a cryptographic
360/// hash so that there is no risk of collisions. Collisions on SHA256 have a probability of 2^128
361/// which is many orders of magnitude smaller than many other events that we don't even think about
362/// (e.g bit flips). In short, we can safely assume that sha256(a) == sha256(b) iff a == b.
363type KeyHash = Sha256;
364
365impl UpsertKey {
366    pub fn from_key(key: Result<&Row, &UpsertError>) -> Self {
367        Self::from_iter(key.map(|r| r.iter()))
368    }
369
370    pub fn from_value(value: Result<&Row, &UpsertError>, key_indices: &[usize]) -> Self {
371        thread_local! {
372            /// A thread-local datum cache used to calculate hashes
373            static VALUE_DATUMS: RefCell<DatumVec> = RefCell::new(DatumVec::new());
374        }
375        VALUE_DATUMS.with(|value_datums| {
376            let mut value_datums = value_datums.borrow_mut();
377            let value = value.map(|v| value_datums.borrow_with(v));
378            let key = match value {
379                Ok(ref datums) => Ok(key_indices.iter().map(|&idx| datums[idx])),
380                Err(err) => Err(err),
381            };
382            Self::from_iter(key)
383        })
384    }
385
386    pub fn from_iter<'a, 'b>(
387        key: Result<impl Iterator<Item = Datum<'a>> + 'b, &UpsertError>,
388    ) -> Self {
389        thread_local! {
390            /// A thread-local datum cache used to calculate hashes
391            static KEY_DATUMS: RefCell<DatumVec> = RefCell::new(DatumVec::new());
392        }
393        KEY_DATUMS.with(|key_datums| {
394            let mut key_datums = key_datums.borrow_mut();
395            // Borrowing the DatumVec gives us a temporary buffer to store datums in that will be
396            // automatically cleared on Drop. See the DatumVec docs for more details.
397            let mut key_datums = key_datums.borrow();
398            let key: Result<&[Datum], Datum> = match key {
399                Ok(key) => {
400                    for datum in key {
401                        key_datums.push(datum);
402                    }
403                    Ok(&*key_datums)
404                }
405                Err(UpsertError::Value(err)) => {
406                    key_datums.extend(err.for_key.iter());
407                    Ok(&*key_datums)
408                }
409                Err(UpsertError::KeyDecode(err)) => Err(Datum::Bytes(&err.raw)),
410                Err(UpsertError::NullKey(_)) => Err(Datum::Null),
411            };
412            let mut hasher = DigestHasher(KeyHash::new());
413            key.hash(&mut hasher);
414            Self(hasher.0.finalize().into())
415        })
416    }
417}
418
419struct DigestHasher<H: Digest>(H);
420
421impl<H: Digest> Hasher for DigestHasher<H> {
422    fn write(&mut self, bytes: &[u8]) {
423        self.0.update(bytes);
424    }
425
426    fn finish(&self) -> u64 {
427        panic!("digest wrapper used to produce a hash");
428    }
429}
430
431use std::convert::Infallible;
432use timely::container::CapacityContainerBuilder;
433use timely::dataflow::channels::pact::Pipeline;
434
435use self::types::ValueMetadata;
436
437/// This leaf operator drops `token` after the input reaches the `resume_upper`.
438/// This is useful to take coordinated actions across all workers, after the `upsert`
439/// operator has rehydrated.
440pub fn rehydration_finished<'scope, T: Timestamp>(
441    scope: Scope<'scope, T>,
442    source_config: &crate::source::RawSourceCreationConfig,
443    // A token that we can drop to signal we are finished rehydrating.
444    token: impl std::any::Any + 'static,
445    resume_upper: Antichain<T>,
446    input: StreamVec<'scope, T, Infallible>,
447) {
448    let worker_id = source_config.worker_id;
449    let id = source_config.id;
450    let mut builder = AsyncOperatorBuilder::new(format!("rehydration_finished({id}"), scope);
451    let mut input = builder.new_disconnected_input(input, Pipeline);
452
453    builder.build(move |_capabilities| async move {
454        let mut input_upper = Antichain::from_elem(Timestamp::minimum());
455        // Ensure this operator finishes if the resume upper is `[0]`
456        while !PartialOrder::less_equal(&resume_upper, &input_upper) {
457            let Some(event) = input.next().await else {
458                break;
459            };
460            if let AsyncEvent::Progress(upper) = event {
461                input_upper = upper;
462            }
463        }
464        tracing::info!(
465            %worker_id,
466            source_id = %id,
467            "upsert source has downgraded past the resume upper ({resume_upper:?}) across all workers",
468        );
469        drop(token);
470    });
471}
472
473/// Resumes an upsert computation at `resume_upper` given as inputs a collection of upsert commands
474/// and the collection of the previous output of this operator.
475/// Returns a tuple of
476/// - A collection of the computed upsert operator and,
477/// - A health update stream to propagate errors
478pub(crate) fn upsert<'scope, T, FromTime>(
479    input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
480    upsert_envelope: UpsertEnvelope,
481    resume_upper: Antichain<T>,
482    previous: VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
483    previous_token: Option<Vec<PressOnDropButton>>,
484    source_config: crate::source::SourceExportCreationConfig,
485    instance_context: &StorageInstanceContext,
486    storage_configuration: &StorageConfiguration,
487    dataflow_paramters: &crate::internal_control::DataflowParameters,
488    backpressure_metrics: Option<BackpressureMetrics>,
489) -> (
490    VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
491    StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
492    StreamVec<'scope, T, Infallible>,
493    PressOnDropButton,
494)
495where
496    T: Timestamp + TotalOrder + Sync,
497    T: Refines<mz_repr::Timestamp> + TotalOrder + Sync,
498    FromTime: Timestamp + Clone + Sync,
499{
500    let upsert_metrics = source_config.metrics.get_upsert_metrics(
501        source_config.id,
502        source_config.worker_id,
503        backpressure_metrics,
504    );
505
506    let rocksdb_cleanup_tries =
507        dyncfgs::STORAGE_ROCKSDB_CLEANUP_TRIES.get(storage_configuration.config_set());
508
509    // Whether or not to partially drain the input buffer
510    // to prevent buffering of the _upstream_ snapshot.
511    let prevent_snapshot_buffering =
512        dyncfgs::STORAGE_UPSERT_PREVENT_SNAPSHOT_BUFFERING.get(storage_configuration.config_set());
513    // If the above is true, the number of timely batches to process at once.
514    let snapshot_buffering_max = dyncfgs::STORAGE_UPSERT_MAX_SNAPSHOT_BATCH_BUFFERING
515        .get(storage_configuration.config_set());
516
517    // Whether we should provide the upsert state merge operator to the RocksDB instance
518    // (for faster performance during snapshot hydration).
519    let rocksdb_use_native_merge_operator =
520        dyncfgs::STORAGE_ROCKSDB_USE_MERGE_OPERATOR.get(storage_configuration.config_set());
521
522    let upsert_config = UpsertConfig {
523        shrink_upsert_unused_buffers_by_ratio: storage_configuration
524            .parameters
525            .shrink_upsert_unused_buffers_by_ratio,
526    };
527
528    let thin_input = upsert_thinning(input);
529
530    let tuning = dataflow_paramters.upsert_rocksdb_tuning_config.clone();
531
532    // When running RocksDB in memory, the file system is emulated. However, we still need to
533    // pick a path that exists because RocksDB will attempt to create the working directory
534    // (see https://github.com/rust-rocksdb/rust-rocksdb/issues/1015) and write a lock file,
535    // so we need to ensure the directory is unique per worker.
536    let rocksdb_dir = instance_context
537        .scratch_directory
538        .clone()
539        .unwrap_or_else(|| PathBuf::from("/tmp"))
540        .join("storage")
541        .join("upsert")
542        .join(source_config.id.to_string())
543        .join(source_config.worker_id.to_string());
544
545    tracing::info!(
546        worker_id = %source_config.worker_id,
547        source_id = %source_config.id,
548        ?rocksdb_dir,
549        ?tuning,
550        ?rocksdb_use_native_merge_operator,
551        "rendering upsert source"
552    );
553
554    let rocksdb_shared_metrics = Arc::clone(&upsert_metrics.rocksdb_shared);
555    let rocksdb_instance_metrics = Arc::clone(&upsert_metrics.rocksdb_instance_metrics);
556
557    let env = instance_context
558        .rocksdb_env()
559        .expect("failed to create rocksdb env");
560
561    // A closure that will initialize and return a configured RocksDB instance
562    let rocksdb_init_fn = move || async move {
563        let merge_operator = if rocksdb_use_native_merge_operator {
564            Some((
565                "upsert_state_snapshot_merge_v1".to_string(),
566                |a: &[u8], b: ValueIterator<BincodeOpts, StateValue<T, FromTime>>| {
567                    consolidating_merge_function::<T, FromTime>(a.into(), b)
568                },
569            ))
570        } else {
571            None
572        };
573        rocksdb::RocksDB::new(
574            mz_rocksdb::RocksDBInstance::new(
575                &rocksdb_dir,
576                mz_rocksdb::InstanceOptions::new(
577                    env,
578                    rocksdb_cleanup_tries,
579                    merge_operator,
580                    // For now, just use the same config as the one used for
581                    // merging snapshots.
582                    upsert_bincode_opts(),
583                ),
584                tuning,
585                rocksdb_shared_metrics,
586                rocksdb_instance_metrics,
587            )
588            .unwrap(),
589        )
590    };
591
592    upsert_operator(
593        thin_input,
594        upsert_envelope.key_indices,
595        resume_upper,
596        previous,
597        previous_token,
598        upsert_metrics,
599        source_config,
600        rocksdb_init_fn,
601        upsert_config,
602        storage_configuration,
603        prevent_snapshot_buffering,
604        snapshot_buffering_max,
605    )
606}
607
608/// An experimental upsert implementation loosely described in this doc:
609/// [Upsert V2 Much Simpler Boogaloo](https://www.notion.so/materialize/Upsert-V2-Much-Simpler-Boogaloo-31913f48d37b807fa88bdeafc27c02d9?source=copy_link)
610///
611/// Instead of using rocksdb as a state backend, this implementation uses a differential dataflow collection to hold the key state,
612/// and performs consolidation of updates with matching keys and MZ timestamps, using max FromTime to choose winners,
613/// resulting in only one record per key per time.
614pub(crate) fn upsert_v2<'scope, T, FromTime>(
615    input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
616    upsert_envelope: UpsertEnvelope,
617    resume_upper: Antichain<T>,
618    previous: VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
619    previous_token: Option<Vec<PressOnDropButton>>,
620    source_config: crate::source::SourceExportCreationConfig,
621    backpressure_metrics: Option<BackpressureMetrics>,
622) -> (
623    VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
624    StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
625    StreamVec<'scope, T, Infallible>,
626    PressOnDropButton,
627)
628where
629    T: Timestamp + TotalOrder + Sync,
630    T: Refines<mz_repr::Timestamp> + differential_dataflow::lattice::Lattice,
631    T: columnation::Columnation,
632    T: columnar::Columnar + Default,
633    for<'a> columnar::Ref<'a, T>: Copy + Ord,
634    FromTime: Timestamp + Clone + Sync,
635    FromTime: UpsertSourceTime,
636{
637    let upsert_metrics = source_config.metrics.get_upsert_metrics(
638        source_config.id,
639        source_config.worker_id,
640        backpressure_metrics,
641    );
642
643    let thin_input = upsert_thinning(input);
644
645    tracing::info!(
646        worker_id = %source_config.worker_id,
647        source_id = %source_config.id,
648        "rendering upsert source (btreemap backend)"
649    );
650
651    upsert_continual_feedback_v2::upsert_inner(
652        thin_input,
653        upsert_envelope.key_indices,
654        resume_upper,
655        previous,
656        previous_token,
657        upsert_metrics,
658        source_config,
659    )
660}
661
662// A shim so we can dispatch based on the dyncfg that tells us which upsert
663// operator to use.
664fn upsert_operator<'scope, T, FromTime, F, Fut, US>(
665    input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
666    key_indices: Vec<usize>,
667    resume_upper: Antichain<T>,
668    persist_input: VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
669    persist_token: Option<Vec<PressOnDropButton>>,
670    upsert_metrics: UpsertMetrics,
671    source_config: crate::source::SourceExportCreationConfig,
672    state: F,
673    upsert_config: UpsertConfig,
674    _storage_configuration: &StorageConfiguration,
675    prevent_snapshot_buffering: bool,
676    snapshot_buffering_max: Option<usize>,
677) -> (
678    VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
679    StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
680    StreamVec<'scope, T, Infallible>,
681    PressOnDropButton,
682)
683where
684    T: Timestamp + TotalOrder + Sync,
685    T: Refines<mz_repr::Timestamp> + TotalOrder + Sync,
686    F: FnOnce() -> Fut + 'static,
687    Fut: std::future::Future<Output = US>,
688    US: UpsertStateBackend<T, FromTime>,
689    FromTime: Debug + timely::ExchangeData + Clone + Ord + Sync,
690{
691    // Hard-coded to true because classic UPSERT cannot be used safely with
692    // concurrent ingestions, which we need for both 0dt upgrades and
693    // multi-replica ingestions.
694    let use_continual_feedback_upsert = true;
695
696    tracing::info!(id = %source_config.id, %use_continual_feedback_upsert, "upsert operator implementation");
697
698    if use_continual_feedback_upsert {
699        upsert_continual_feedback::upsert_inner(
700            input,
701            key_indices,
702            resume_upper,
703            persist_input,
704            persist_token,
705            upsert_metrics,
706            source_config,
707            state,
708            upsert_config,
709            prevent_snapshot_buffering,
710            snapshot_buffering_max,
711        )
712    } else {
713        upsert_classic(
714            input,
715            key_indices,
716            resume_upper,
717            persist_input,
718            persist_token,
719            upsert_metrics,
720            source_config,
721            state,
722            upsert_config,
723            prevent_snapshot_buffering,
724            snapshot_buffering_max,
725        )
726    }
727}
728
729/// Renders an operator that discards updates that are known to not affect the outcome of upsert in
730/// a streaming fashion. For each distinct (key, time) in the input it emits the value with the
731/// highest from_time. Its purpose is to thin out data as much as possible before exchanging them
732/// across workers.
733fn upsert_thinning<'scope, T, K, V, FromTime>(
734    input: VecCollection<'scope, T, (K, V, FromTime), Diff>,
735) -> VecCollection<'scope, T, (K, V, FromTime), Diff>
736where
737    T: Timestamp + TotalOrder,
738    K: timely::ExchangeData + Clone + Eq + Ord,
739    V: timely::ExchangeData + Clone,
740    FromTime: Timestamp,
741{
742    input
743        .inner
744        .unary(Pipeline, "UpsertThinning", |_, _| {
745            // A capability suitable to emit all updates in `updates`, if any.
746            let mut capability: Option<InputCapability<T>> = None;
747            // A batch of received updates
748            let mut updates = Vec::new();
749            move |input, output| {
750                input.for_each(|cap, data| {
751                    assert!(
752                        data.iter().all(|(_, _, diff)| diff.is_positive()),
753                        "invalid upsert input"
754                    );
755                    updates.append(data);
756                    match capability.as_mut() {
757                        Some(capability) => {
758                            if cap.time() <= capability.time() {
759                                *capability = cap;
760                            }
761                        }
762                        None => capability = Some(cap),
763                    }
764                });
765                if let Some(capability) = capability.take() {
766                    // Sort by (key, time, Reverse(from_time)) so that deduping by (key, time) gives
767                    // the latest change for that key.
768                    updates.sort_unstable_by(|a, b| {
769                        let ((key1, _, from_time1), time1, _) = a;
770                        let ((key2, _, from_time2), time2, _) = b;
771                        Ord::cmp(
772                            &(key1, time1, Reverse(from_time1)),
773                            &(key2, time2, Reverse(from_time2)),
774                        )
775                    });
776                    let mut session = output.session(&capability);
777                    session.give_iterator(updates.drain(..).dedup_by(|a, b| {
778                        let ((key1, _, _), time1, _) = a;
779                        let ((key2, _, _), time2, _) = b;
780                        (key1, time1) == (key2, time2)
781                    }))
782                }
783            }
784        })
785        .as_collection()
786}
787
788/// Helper method for `upsert_classic` used to stage `data` updates
789/// from the input/source timely edge.
790fn stage_input<T, FromTime>(
791    stash: &mut Vec<(T, UpsertKey, Reverse<FromTime>, Option<UpsertValue>)>,
792    data: &mut Vec<((UpsertKey, Option<UpsertValue>, FromTime), T, Diff)>,
793    input_upper: &Antichain<T>,
794    resume_upper: &Antichain<T>,
795    storage_shrink_upsert_unused_buffers_by_ratio: usize,
796) where
797    T: PartialOrder,
798    FromTime: Ord,
799{
800    if PartialOrder::less_equal(input_upper, resume_upper) {
801        data.retain(|(_, ts, _)| resume_upper.less_equal(ts));
802    }
803
804    stash.extend(data.drain(..).map(|((key, value, order), time, diff)| {
805        assert!(diff.is_positive(), "invalid upsert input");
806        (time, key, Reverse(order), value)
807    }));
808
809    if storage_shrink_upsert_unused_buffers_by_ratio > 0 {
810        let reduced_capacity = stash.capacity() / storage_shrink_upsert_unused_buffers_by_ratio;
811        if reduced_capacity > stash.len() {
812            stash.shrink_to(reduced_capacity);
813        }
814    }
815}
816
817/// The style of drain we are performing on the stash. `AtTime`-drains cannot
818/// assume that all values have been seen, and must leave tombstones behind for deleted values.
819#[derive(Debug)]
820enum DrainStyle<'a, T> {
821    ToUpper(&'a Antichain<T>),
822    AtTime(T),
823}
824
825/// Helper method for `upsert_inner` used to stage `data` updates
826/// from the input timely edge.
827async fn drain_staged_input<S, T, FromTime, E>(
828    stash: &mut Vec<(T, UpsertKey, Reverse<FromTime>, Option<UpsertValue>)>,
829    commands_state: &mut indexmap::IndexMap<UpsertKey, types::UpsertValueAndSize<T, FromTime>>,
830    output_updates: &mut Vec<(UpsertValue, T, Diff)>,
831    multi_get_scratch: &mut Vec<UpsertKey>,
832    drain_style: DrainStyle<'_, T>,
833    error_emitter: &mut E,
834    state: &mut UpsertState<'_, S, T, FromTime>,
835    source_config: &crate::source::SourceExportCreationConfig,
836) where
837    S: UpsertStateBackend<T, FromTime>,
838    T: PartialOrder + Ord + Clone + Send + Sync + Serialize + Debug + 'static,
839    FromTime: timely::ExchangeData + Clone + Ord + Sync,
840    E: UpsertErrorEmitter<T>,
841{
842    stash.sort_unstable();
843
844    // Find the prefix that we can emit
845    let idx = stash.partition_point(|(ts, _, _, _)| match &drain_style {
846        DrainStyle::ToUpper(upper) => !upper.less_equal(ts),
847        DrainStyle::AtTime(time) => ts <= time,
848    });
849
850    tracing::trace!(?drain_style, updates = idx, "draining stash in upsert");
851
852    // Read the previous values _per key_ out of `state`, recording it
853    // along with the value with the _latest timestamp for that key_.
854    commands_state.clear();
855    for (_, key, _, _) in stash.iter().take(idx) {
856        commands_state.entry(*key).or_default();
857    }
858
859    // These iterators iterate in the same order because `commands_state`
860    // is an `IndexMap`.
861    multi_get_scratch.clear();
862    multi_get_scratch.extend(commands_state.iter().map(|(k, _)| *k));
863    match state
864        .multi_get(multi_get_scratch.drain(..), commands_state.values_mut())
865        .await
866    {
867        Ok(_) => {}
868        Err(e) => {
869            error_emitter
870                .emit("Failed to fetch records from state".to_string(), e)
871                .await;
872        }
873    }
874
875    // From the prefix that can be emitted we can deduplicate based on (ts, key) in
876    // order to only process the command with the maximum order within the (ts,
877    // key) group. This is achieved by wrapping order in `Reverse(FromTime)` above.;
878    let mut commands = stash.drain(..idx).dedup_by(|a, b| {
879        let ((a_ts, a_key, _, _), (b_ts, b_key, _, _)) = (a, b);
880        a_ts == b_ts && a_key == b_key
881    });
882
883    let bincode_opts = types::upsert_bincode_opts();
884    // Upsert the values into `commands_state`, by recording the latest
885    // value (or deletion). These will be synced at the end to the `state`.
886    //
887    // Note that we are effectively doing "mini-upsert" here, using
888    // `command_state`. This "mini-upsert" is seeded with data from `state`, using
889    // a single `multi_get` above, and the final state is written out into
890    // `state` using a single `multi_put`. This simplifies `UpsertStateBackend`
891    // implementations, and reduces the number of reads and write we need to do.
892    //
893    // This "mini-upsert" technique is actually useful in `UpsertState`'s
894    // `consolidate_snapshot_read_write_inner` implementation, minimizing gets and puts on
895    // the `UpsertStateBackend` implementations. In some sense, its "upsert all the way down".
896    while let Some((ts, key, from_time, value)) = commands.next() {
897        let mut command_state = if let Entry::Occupied(command_state) = commands_state.entry(key) {
898            command_state
899        } else {
900            panic!("key missing from commands_state");
901        };
902
903        let existing_value = &mut command_state.get_mut().value;
904
905        if let Some(cs) = existing_value.as_mut() {
906            cs.ensure_decoded(bincode_opts, source_config.id, Some(&key));
907        }
908
909        // Skip this command if its order key is below the one in the upsert state.
910        // Note that the existing order key may be `None` if the existing value
911        // is from snapshotting, which always sorts below new values/deletes.
912        let existing_order = existing_value
913            .as_ref()
914            .and_then(|cs| cs.provisional_order(&ts));
915        if existing_order >= Some(&from_time.0) {
916            // Skip this update. If no later updates adjust this key, then we just
917            // end up writing the same value back to state. If there
918            // is nothing in the state, `existing_order` is `None`, and this
919            // does not occur.
920            continue;
921        }
922
923        match value {
924            Some(value) => {
925                if let Some(old_value) =
926                    existing_value.replace(StateValue::finalized_value(value.clone()))
927                {
928                    if let Some(old_value) = old_value.into_decoded().finalized {
929                        output_updates.push((old_value, ts.clone(), Diff::MINUS_ONE));
930                    }
931                }
932                output_updates.push((value, ts, Diff::ONE));
933            }
934            None => {
935                if let Some(old_value) = existing_value.take() {
936                    if let Some(old_value) = old_value.into_decoded().finalized {
937                        output_updates.push((old_value, ts, Diff::MINUS_ONE));
938                    }
939                }
940
941                // Record a tombstone for deletes.
942                *existing_value = Some(StateValue::tombstone());
943            }
944        }
945    }
946
947    match state
948        .multi_put(
949            true, // Do update per-update stats.
950            commands_state.drain(..).map(|(k, cv)| {
951                (
952                    k,
953                    types::PutValue {
954                        value: cv.value.map(|cv| cv.into_decoded()),
955                        previous_value_metadata: cv.metadata.map(|v| ValueMetadata {
956                            size: v.size.try_into().expect("less than i64 size"),
957                            is_tombstone: v.is_tombstone,
958                        }),
959                    },
960                )
961            }),
962        )
963        .await
964    {
965        Ok(_) => {}
966        Err(e) => {
967            error_emitter
968                .emit("Failed to update records in state".to_string(), e)
969                .await;
970        }
971    }
972}
973
974/// A no-op-ish error emitter for the fuzzing hook. With the in-memory backend
975/// and the well-formed inputs the fuzzer builds, `multi_get`/`multi_put` never
976/// error, so reaching this is itself a finding.
977#[cfg(feature = "fuzzing")]
978struct PanicErrorEmitter;
979
980#[cfg(feature = "fuzzing")]
981#[async_trait::async_trait(?Send)]
982impl<T> UpsertErrorEmitter<T> for PanicErrorEmitter {
983    async fn emit(&mut self, context: String, e: anyhow::Error) {
984        panic!("unexpected upsert state error during fuzzing: {context}: {e}");
985    }
986}
987
988/// Fuzzing hook: run a single `drain_staged_input` over `commands` (each a
989/// `(timestamp, key, order, value)`, where `value == None` is a delete) against
990/// a fresh empty in-memory state, draining everything strictly below
991/// `drain_to`. Returns the emitted output updates and the final finalized value
992/// of each key in `all_keys`. Exposed only for fuzzing. Not a stable public
993/// API.
994#[cfg(feature = "fuzzing")]
995pub async fn fuzz_drain_staged_input(
996    parts: &types::FuzzUpsertParts,
997    source_config: &crate::source::SourceExportCreationConfig,
998    commands: Vec<(u64, UpsertKey, u64, Option<UpsertValue>)>,
999    drain_to: u64,
1000    all_keys: &[UpsertKey],
1001) -> (Vec<(UpsertValue, u64, Diff)>, Vec<Option<UpsertValue>>) {
1002    let mut state = parts.state();
1003    let mut stash: Vec<(u64, UpsertKey, Reverse<u64>, Option<UpsertValue>)> = commands
1004        .into_iter()
1005        .map(|(ts, key, order, value)| (ts, key, Reverse(order), value))
1006        .collect();
1007    let mut commands_state = indexmap::IndexMap::new();
1008    let mut output = Vec::new();
1009    let mut multi_get_scratch = Vec::new();
1010    let mut emitter = PanicErrorEmitter;
1011
1012    drain_staged_input(
1013        &mut stash,
1014        &mut commands_state,
1015        &mut output,
1016        &mut multi_get_scratch,
1017        DrainStyle::ToUpper(&Antichain::from_elem(drain_to)),
1018        &mut emitter,
1019        &mut state,
1020        source_config,
1021    )
1022    .await;
1023
1024    let bincode_opts = types::upsert_bincode_opts();
1025    let mut results = vec![types::UpsertValueAndSize::default(); all_keys.len()];
1026    state
1027        .multi_get(all_keys.iter().copied(), results.iter_mut())
1028        .await
1029        .expect("multi_get in fuzz hook should not error");
1030    let final_state = results
1031        .into_iter()
1032        .map(|r| match r.value {
1033            None => None,
1034            Some(mut sv) => {
1035                sv.ensure_decoded(bincode_opts, GlobalId::User(0), None);
1036                sv.into_decoded().finalized
1037            }
1038        })
1039        .collect();
1040
1041    (output, final_state)
1042}
1043
1044// Created a struct to hold the configs for upserts.
1045// So that new configs don't require a new method parameter.
1046pub(crate) struct UpsertConfig {
1047    pub shrink_upsert_unused_buffers_by_ratio: usize,
1048}
1049
1050fn upsert_classic<'scope, T, FromTime, F, Fut, US>(
1051    input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
1052    key_indices: Vec<usize>,
1053    resume_upper: Antichain<T>,
1054    previous: VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
1055    previous_token: Option<Vec<PressOnDropButton>>,
1056    upsert_metrics: UpsertMetrics,
1057    source_config: crate::source::SourceExportCreationConfig,
1058    state: F,
1059    upsert_config: UpsertConfig,
1060    prevent_snapshot_buffering: bool,
1061    snapshot_buffering_max: Option<usize>,
1062) -> (
1063    VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
1064    StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
1065    StreamVec<'scope, T, Infallible>,
1066    PressOnDropButton,
1067)
1068where
1069    T: Timestamp + TotalOrder + Sync,
1070    F: FnOnce() -> Fut + 'static,
1071    Fut: std::future::Future<Output = US>,
1072    US: UpsertStateBackend<T, FromTime>,
1073    FromTime: timely::ExchangeData + Clone + Ord + Sync,
1074{
1075    let mut builder = AsyncOperatorBuilder::new("Upsert".to_string(), input.scope());
1076
1077    // We only care about UpsertValueError since this is the only error that we can retract
1078    let previous = previous.flat_map(move |result| {
1079        let value = match result {
1080            Ok(ok) => Ok(ok),
1081            Err(DataflowError::EnvelopeError(err)) => match *err {
1082                EnvelopeError::Upsert(err) => Err(Box::new(err)),
1083                EnvelopeError::Flat(_) => return None,
1084            },
1085            Err(_) => return None,
1086        };
1087        let value_ref = match value {
1088            Ok(ref row) => Ok(row),
1089            Err(ref err) => Err(&**err),
1090        };
1091        Some((UpsertKey::from_value(value_ref, &key_indices), value))
1092    });
1093    let (output_handle, output) = builder.new_output();
1094
1095    // An output that just reports progress of the snapshot consolidation process upstream to the
1096    // persist source to ensure that backpressure is applied
1097    let (_snapshot_handle, snapshot_stream) =
1098        builder.new_output::<CapacityContainerBuilder<Vec<Infallible>>>();
1099
1100    let (mut health_output, health_stream) = builder.new_output();
1101    let mut input = builder.new_input_for(
1102        input.inner,
1103        Exchange::new(move |((key, _, _), _, _)| UpsertKey::hashed(key)),
1104        &output_handle,
1105    );
1106
1107    let mut previous = builder.new_input_for(
1108        previous.inner,
1109        Exchange::new(|((key, _), _, _)| UpsertKey::hashed(key)),
1110        &output_handle,
1111    );
1112
1113    let upsert_shared_metrics = Arc::clone(&upsert_metrics.shared);
1114    let shutdown_button = builder.build(move |caps| async move {
1115        let [mut output_cap, mut snapshot_cap, health_cap]: [_; 3] = caps.try_into().unwrap();
1116
1117        let mut state = UpsertState::<_, _, FromTime>::new(
1118            state().await,
1119            upsert_shared_metrics,
1120            &upsert_metrics,
1121            source_config.source_statistics.clone(),
1122            upsert_config.shrink_upsert_unused_buffers_by_ratio,
1123        );
1124        let mut events = vec![];
1125        let mut snapshot_upper = Antichain::from_elem(Timestamp::minimum());
1126
1127        let mut stash = vec![];
1128
1129        let mut error_emitter = (&mut health_output, &health_cap);
1130
1131        tracing::info!(
1132            ?resume_upper,
1133            ?snapshot_upper,
1134            "timely-{} upsert source {} starting rehydration",
1135            source_config.worker_id,
1136            source_config.id
1137        );
1138        // Read and consolidate the snapshot from the 'previous' input until it
1139        // reaches the `resume_upper`.
1140        while !PartialOrder::less_equal(&resume_upper, &snapshot_upper) {
1141            previous.ready().await;
1142            while let Some(event) = previous.next_sync() {
1143                match event {
1144                    AsyncEvent::Data(_cap, data) => {
1145                        events.extend(data.into_iter().filter_map(|((key, value), ts, diff)| {
1146                            if !resume_upper.less_equal(&ts) {
1147                                Some((key, value, diff))
1148                            } else {
1149                                None
1150                            }
1151                        }))
1152                    }
1153                    AsyncEvent::Progress(upper) => {
1154                        snapshot_upper = upper;
1155                    }
1156                };
1157            }
1158
1159            match state
1160                .consolidate_chunk(
1161                    events.drain(..),
1162                    PartialOrder::less_equal(&resume_upper, &snapshot_upper),
1163                )
1164                .await
1165            {
1166                Ok(_) => {
1167                    if let Some(ts) = snapshot_upper.clone().into_option() {
1168                        // As we shutdown, we could ostensibly get data from later than the
1169                        // `resume_upper`, which we ignore above. We don't want our output capability to make
1170                        // it further than the `resume_upper`.
1171                        if !resume_upper.less_equal(&ts) {
1172                            snapshot_cap.downgrade(&ts);
1173                            output_cap.downgrade(&ts);
1174                        }
1175                    }
1176                }
1177                Err(e) => {
1178                    UpsertErrorEmitter::<T>::emit(
1179                        &mut error_emitter,
1180                        "Failed to rehydrate state".to_string(),
1181                        e,
1182                    )
1183                    .await;
1184                }
1185            }
1186        }
1187
1188        drop(events);
1189        drop(previous_token);
1190        drop(snapshot_cap);
1191
1192        // Exchaust the previous input. It is expected to immediately reach the empty
1193        // antichain since we have dropped its token.
1194        //
1195        // Note that we do not need to also process the `input` during this, as the dropped token
1196        // will shutdown the `backpressure` operator
1197        while let Some(_event) = previous.next().await {}
1198
1199        // After snapshotting, our output frontier is exactly the `resume_upper`
1200        if let Some(ts) = resume_upper.as_option() {
1201            output_cap.downgrade(ts);
1202        }
1203
1204        tracing::info!(
1205            "timely-{} upsert source {} finished rehydration",
1206            source_config.worker_id,
1207            source_config.id
1208        );
1209
1210        // A re-usable buffer of changes, per key. This is an `IndexMap` because it has to be `drain`-able
1211        // and have a consistent iteration order.
1212        let mut commands_state: indexmap::IndexMap<_, types::UpsertValueAndSize<T, FromTime>> =
1213            indexmap::IndexMap::new();
1214        let mut multi_get_scratch = Vec::new();
1215
1216        // Now can can resume consuming the collection
1217        let mut output_updates = vec![];
1218        let mut input_upper = Antichain::from_elem(Timestamp::minimum());
1219
1220        while let Some(event) = input.next().await {
1221            // Buffer as many events as possible. This should be bounded, as new data can't be
1222            // produced in this worker until we yield to timely.
1223            let events = [event]
1224                .into_iter()
1225                .chain(std::iter::from_fn(|| input.next().now_or_never().flatten()))
1226                .enumerate();
1227
1228            let mut partial_drain_time = None;
1229            for (i, event) in events {
1230                match event {
1231                    AsyncEvent::Data(cap, mut data) => {
1232                        tracing::trace!(
1233                            time=?cap.time(),
1234                            updates=%data.len(),
1235                            "received data in upsert"
1236                        );
1237                        stage_input(
1238                            &mut stash,
1239                            &mut data,
1240                            &input_upper,
1241                            &resume_upper,
1242                            upsert_config.shrink_upsert_unused_buffers_by_ratio,
1243                        );
1244
1245                        let event_time = cap.time();
1246                        // If the data is at _exactly_ the output frontier, we can preemptively drain it into the state.
1247                        // Data within this set events strictly beyond this time are staged as
1248                        // normal.
1249                        //
1250                        // This is a load-bearing optimization, as it is required to avoid buffering
1251                        // the entire source snapshot in the `stash`.
1252                        if prevent_snapshot_buffering && output_cap.time() == event_time {
1253                            partial_drain_time = Some(event_time.clone());
1254                        }
1255                    }
1256                    AsyncEvent::Progress(upper) => {
1257                        tracing::trace!(?upper, "received progress in upsert");
1258                        // Ignore progress updates before the `resume_upper`, which is our initial
1259                        // capability post-snapshotting.
1260                        if PartialOrder::less_than(&upper, &resume_upper) {
1261                            continue;
1262                        }
1263
1264                        // Disable the partial drain as this progress event covers
1265                        // the `output_cap` time.
1266                        partial_drain_time = None;
1267                        drain_staged_input::<_, _, _, _>(
1268                            &mut stash,
1269                            &mut commands_state,
1270                            &mut output_updates,
1271                            &mut multi_get_scratch,
1272                            DrainStyle::ToUpper(&upper),
1273                            &mut error_emitter,
1274                            &mut state,
1275                            &source_config,
1276                        )
1277                        .await;
1278
1279                        output_handle.give_container(&output_cap, &mut output_updates);
1280
1281                        if let Some(ts) = upper.as_option() {
1282                            output_cap.downgrade(ts);
1283                        }
1284                        input_upper = upper;
1285                    }
1286                }
1287                let events_processed = i + 1;
1288                if let Some(max) = snapshot_buffering_max {
1289                    if events_processed >= max {
1290                        break;
1291                    }
1292                }
1293            }
1294
1295            // If there were staged events that occurred at the capability time, drain
1296            // them. This is safe because out-of-order updates to the same key that are
1297            // drained in separate calls to `drain_staged_input` are correctly ordered by
1298            // their `FromTime` in `drain_staged_input`.
1299            //
1300            // Note also that this may result in more updates in the output collection than
1301            // the minimum. However, because the frontier only advances on `Progress` updates,
1302            // the collection always accumulates correctly for all keys.
1303            if let Some(partial_drain_time) = partial_drain_time {
1304                drain_staged_input::<_, _, _, _>(
1305                    &mut stash,
1306                    &mut commands_state,
1307                    &mut output_updates,
1308                    &mut multi_get_scratch,
1309                    DrainStyle::AtTime(partial_drain_time),
1310                    &mut error_emitter,
1311                    &mut state,
1312                    &source_config,
1313                )
1314                .await;
1315
1316                output_handle.give_container(&output_cap, &mut output_updates);
1317            }
1318        }
1319    });
1320
1321    (
1322        output.as_collection().map(|result| match result {
1323            Ok(ok) => Ok(ok),
1324            Err(err) => Err(DataflowError::from(EnvelopeError::Upsert(*err))),
1325        }),
1326        health_stream,
1327        snapshot_stream,
1328        shutdown_button.press_on_drop(),
1329    )
1330}
1331
1332#[async_trait::async_trait(?Send)]
1333pub(crate) trait UpsertErrorEmitter<T> {
1334    async fn emit(&mut self, context: String, e: anyhow::Error);
1335}
1336
1337#[async_trait::async_trait(?Send)]
1338impl<T: Timestamp> UpsertErrorEmitter<T>
1339    for (
1340        &mut AsyncOutputHandle<
1341            T,
1342            CapacityContainerBuilder<Vec<(Option<GlobalId>, HealthStatusUpdate)>>,
1343        >,
1344        &Capability<T>,
1345    )
1346{
1347    async fn emit(&mut self, context: String, e: anyhow::Error) {
1348        process_upsert_state_error::<T>(context, e, self.0, self.1).await
1349    }
1350}
1351
1352/// Emit the given error, and stall till the dataflow is restarted.
1353async fn process_upsert_state_error<T: Timestamp>(
1354    context: String,
1355    e: anyhow::Error,
1356    health_output: &AsyncOutputHandle<
1357        T,
1358        CapacityContainerBuilder<Vec<(Option<GlobalId>, HealthStatusUpdate)>>,
1359    >,
1360    health_cap: &Capability<T>,
1361) {
1362    let update = HealthStatusUpdate::halting(e.context(context).to_string_with_causes(), None);
1363    health_output.give(health_cap, (None, update));
1364    std::future::pending::<()>().await;
1365    unreachable!("pending future never returns");
1366}