Skip to main content

mz_storage/
upsert_continual_feedback.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//! Implementation of feedback UPSERT operator and associated helpers. See
11//! [`upsert_inner`] for a description of how the operator works and why.
12
13use std::cmp::Reverse;
14use std::fmt::Debug;
15use std::sync::Arc;
16
17use differential_dataflow::hashable::Hashable;
18use differential_dataflow::{AsCollection, VecCollection};
19use indexmap::map::Entry;
20use itertools::Itertools;
21use mz_repr::{Diff, GlobalId, Row};
22use mz_storage_types::errors::{DataflowError, EnvelopeError};
23use mz_timely_util::builder_async::{
24    Event as AsyncEvent, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
25    sole_capability,
26};
27use std::convert::Infallible;
28use timely::container::CapacityContainerBuilder;
29use timely::dataflow::StreamVec;
30use timely::dataflow::channels::pact::Exchange;
31use timely::dataflow::operators::{Capability, CapabilitySet};
32use timely::order::{PartialOrder, TotalOrder};
33use timely::progress::timestamp::Refines;
34use timely::progress::{Antichain, Timestamp};
35
36use crate::healthcheck::HealthStatusUpdate;
37use crate::metrics::upsert::UpsertMetrics;
38use crate::upsert::UpsertConfig;
39use crate::upsert::UpsertErrorEmitter;
40use crate::upsert::UpsertKey;
41use crate::upsert::UpsertValue;
42use crate::upsert::types::UpsertValueAndSize;
43use crate::upsert::types::{self as upsert_types, ValueMetadata};
44use crate::upsert::types::{StateValue, UpsertState, UpsertStateBackend};
45
46/// An operator that transforms an input stream of upserts (updates to key-value
47/// pairs), which represents an imaginary key-value state, into a differential
48/// collection. It keeps an internal map-like state which keeps the latest value
49/// for each key, such that it can emit the retractions and additions implied by
50/// a new update for a given key.
51///
52/// This operator is intended to be used in an ingestion pipeline that reads
53/// from an external source, and the output of this operator is eventually
54/// written to persist.
55///
56/// The operator has two inputs: a) the source input, of upserts, and b) a
57/// persist input that feeds back the upsert state to the operator. Below, there
58/// is a section for each input that describes how and why we process updates
59/// from each input.
60///
61/// An important property of this operator is that it does _not_ update the
62/// map-like state that it keeps for translating the stream of upserts into a
63/// differential collection when it processes source input. It _only_ updates
64/// the map-like state based on updates from the persist (feedback) input. We do
65/// this because the operator is expected to be used in cases where there are
66/// multiple concurrent instances of the same ingestion pipeline, and the
67/// different instances might see different input because of concurrency and
68/// non-determinism. All instances of the upsert operator must produce output
69/// that is consistent with the current state of the output (that all instances
70/// produce "collaboratively"). This global state is what the operator
71/// continually learns about via updates from the persist input.
72///
73/// ## Processing the Source Input
74///
75/// Updates on the source input are stashed/staged until they can be processed.
76/// Whether or not an update can be processed depends both on the upper frontier
77/// of the source input and on the upper frontier of the persist input:
78///
79///  - Input updates are only processed once their timestamp is "done", that is
80///  the input upper is no longer `less_equal` their timestamp.
81///
82///  - Input updates are only processed once they are at the persist upper, that
83///  is we have emitted and written down updates for all previous times and we
84///  have updated our map-like state to the latest global state of the output of
85///  the ingestion pipeline. We know this is the case when the persist upper is
86///  no longer `less_than` their timestamp.
87///
88/// As an optimization, we allow processing input updates when they are right at
89/// the input frontier. This is called _partial emission_ because we are
90/// emitting updates that might be retracted when processing more updates from
91/// the same timestamp. In order to be able to process these updates we keep
92/// _provisional values_ in our upsert state. These will be overwritten when we
93/// get the final upsert values on the persist input.
94///
95/// ## Processing the Persist Input
96///
97/// We continually ingest updates from the persist input into our state using
98/// `UpsertState::consolidate_chunk`. We might be ingesting updates from the
99/// initial snapshot (when starting the operator) that are not consolidated or
100/// we might be ingesting updates from a partial emission (see above). In either
101/// case, our input might not be consolidated and `consolidate_chunk` is able to
102/// handle that.
103pub fn upsert_inner<'scope, T, FromTime, F, Fut, US>(
104    input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
105    key_indices: Vec<usize>,
106    resume_upper: Antichain<T>,
107    persist_ok: VecCollection<'scope, T, Row, Diff>,
108    persist_err: VecCollection<'scope, T, DataflowError, Diff>,
109    mut persist_token: Option<Vec<PressOnDropButton>>,
110    upsert_metrics: UpsertMetrics,
111    source_config: crate::source::SourceExportCreationConfig,
112    state_fn: F,
113    upsert_config: UpsertConfig,
114    prevent_snapshot_buffering: bool,
115    snapshot_buffering_max: Option<usize>,
116) -> (
117    VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
118    StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
119    StreamVec<'scope, T, Infallible>,
120    PressOnDropButton,
121)
122where
123    T: Timestamp + Refines<mz_repr::Timestamp> + TotalOrder + Sync,
124    F: FnOnce() -> Fut + 'static,
125    Fut: std::future::Future<Output = US>,
126    US: UpsertStateBackend<T, FromTime>,
127    FromTime: Debug + timely::ExchangeData + Clone + Ord + Sync,
128{
129    let mut builder = AsyncOperatorBuilder::new("Upsert".to_string(), input.scope());
130
131    let persist_input = crate::upsert::key_persist_feedback(persist_ok, persist_err, key_indices);
132    let (output_handle, output) = builder.new_output::<CapacityContainerBuilder<_>>();
133
134    // An output that just reports progress of the snapshot consolidation process upstream to the
135    // persist source to ensure that backpressure is applied
136    let (_snapshot_handle, snapshot_stream) =
137        builder.new_output::<CapacityContainerBuilder<Vec<Infallible>>>();
138
139    let (mut health_output, health_stream) = builder.new_output();
140    let mut input = builder.new_input_for(
141        input.inner,
142        Exchange::new(move |((key, _, _), _, _)| UpsertKey::hashed(key)),
143        &output_handle,
144    );
145
146    let mut persist_input = builder.new_disconnected_input(
147        persist_input.inner,
148        Exchange::new(|((key, _), _, _)| UpsertKey::hashed(key)),
149    );
150
151    let upsert_shared_metrics = Arc::clone(&upsert_metrics.shared);
152
153    let shutdown_button = builder.build(move |caps| async move {
154        let [output_cap, snapshot_cap, health_cap]: [_; 3] = caps.try_into().unwrap();
155        drop(output_cap);
156        let mut snapshot_cap = CapabilitySet::from_elem(snapshot_cap);
157
158        let mut state = UpsertState::<_, T, FromTime>::new(
159            state_fn().await,
160            upsert_shared_metrics,
161            &upsert_metrics,
162            source_config.source_statistics.clone(),
163            upsert_config.shrink_upsert_unused_buffers_by_ratio,
164        );
165
166        // True while we're still reading the initial "snapshot" (a whole bunch
167        // of updates, all at the same initial timestamp) from our persist
168        // input or while we're reading the initial snapshot from the upstream
169        // source.
170        let mut hydrating = true;
171
172        // A re-usable buffer of changes, per key. This is an `IndexMap`
173        // because it has to be `drain`-able and have a consistent iteration
174        // order.
175        let mut commands_state: indexmap::IndexMap<
176            _,
177            upsert_types::UpsertValueAndSize<T, FromTime>,
178        > = indexmap::IndexMap::new();
179        let mut multi_get_scratch = Vec::new();
180
181        // For stashing source input while it's not eligible for processing.
182        let mut stash = vec![];
183        // A capability suitable for emitting any updates based on stash. No capability is held
184        // when the stash is empty.
185        let mut stash_cap: Option<Capability<T>> = None;
186        let mut input_upper = Antichain::from_elem(Timestamp::minimum());
187        let mut partial_drain_time = None;
188
189        // For our persist/feedback input, both of these.
190        let mut persist_stash = vec![];
191        let mut persist_upper = Antichain::from_elem(Timestamp::minimum());
192
193        // We keep track of the largest timestamp seen on the persist input so
194        // that we can block processing source input while that timestamp is
195        // beyond the persist frontier. While ingesting updates of a timestamp,
196        // our upsert state is in a consolidating state, and trying to read it
197        // at that time would yield a panic.
198        //
199        // NOTE(aljoscha): You would think that it cannot happen that we even
200        // attempt to process source updates while the state is in a
201        // consolidating state, because we always wait until the persist
202        // frontier "catches up" with the timestamp of the source input. If
203        // there is only this here UPSERT operator and no concurrent instances,
204        // this is true. But with concurrent instances it can happen that an
205        // operator that is faster than us makes it so updates get written to
206        // persist. And we would then be ingesting them.
207        let mut largest_seen_persist_ts: Option<T> = None;
208
209        // A buffer for our output.
210        let mut output_updates = vec![];
211
212        let mut error_emitter = (&mut health_output, &health_cap);
213
214        loop {
215            tokio::select! {
216                _ = persist_input.ready() => {
217                    // Read away as much input as we can.
218                    while let Some(persist_event) = persist_input.next_sync() {
219                        match persist_event {
220                            AsyncEvent::Data(time, data) => {
221                                tracing::trace!(
222                                    worker_id = %source_config.worker_id,
223                                    source_id = %source_config.id,
224                                    time=?time,
225                                    updates=%data.len(),
226                                    "received persist data");
227
228                                persist_stash.extend(data.into_iter().map(
229                                    |((key, value), ts, diff)| {
230                                        largest_seen_persist_ts =
231                                            std::cmp::max(
232                                                largest_seen_persist_ts
233                                                    .clone(),
234                                                Some(ts.clone()),
235                                            );
236                                        (key, value, ts, diff)
237                                    },
238                                ));
239                            }
240                            AsyncEvent::Progress(upper) => {
241                                tracing::trace!(
242                                    worker_id = %source_config.worker_id,
243                                    source_id = %source_config.id,
244                                    ?upper,
245                                    "received persist progress");
246                                persist_upper = upper;
247                            }
248                        }
249                    }
250
251                    let last_rehydration_chunk =
252                        hydrating && PartialOrder::less_equal(&resume_upper, &persist_upper);
253
254                    tracing::debug!(
255                        worker_id = %source_config.worker_id,
256                        source_id = %source_config.id,
257                        persist_stash = %persist_stash.len(),
258                        %hydrating,
259                        %last_rehydration_chunk,
260                        ?resume_upper,
261                        ?persist_upper,
262                        "ingesting persist snapshot chunk");
263
264                    // Log any (key, ts) pairs in this batch that have a suspicious
265                    // net diff, to help diagnose how diff_sum corruption enters the
266                    // system.
267                    //
268                    // Consolidating by key alone is too noisy during hydration,
269                    // because a single batch can legitimately contain multiple
270                    // timestamps for the same key. The suspicious shape for this
271                    // bug is multiple net updates for the same key at one logical
272                    // timestamp.
273                    {
274                        let mut key_ts_diffs: Vec<(
275                            (UpsertKey, T),
276                            mz_repr::Diff
277                        )> = persist_stash
278                            .iter()
279                            .map(|(key, _val, ts, diff)| ((*key, ts.clone()), *diff))
280                            .collect();
281                        differential_dataflow::consolidation::consolidate(&mut key_ts_diffs);
282                        for ((key, ts), net_diff) in &key_ts_diffs {
283                            if net_diff.into_inner() > 1 || net_diff.into_inner() < -1 {
284                                tracing::warn!(
285                                    worker_id = %source_config.worker_id,
286                                    source_id = %source_config.id,
287                                    ?key,
288                                    ?ts,
289                                    net_diff = net_diff.into_inner(),
290                                    %hydrating,
291                                    ?persist_upper,
292                                    "persist feedback batch has (key, ts) with suspicious net diff \
293                                    (expected -1, 0, or 1 after per-(key, ts) consolidation)"
294                                );
295                            }
296                        }
297                    }
298
299                    let persist_stash_iter = persist_stash
300                        .drain(..)
301                        .map(|(key, val, _ts, diff)| (key, val, diff));
302
303                    match state
304                        .consolidate_chunk(
305                            persist_stash_iter,
306                            last_rehydration_chunk,
307                        )
308                        .await
309                    {
310                        Ok(_) => {}
311                        Err(e) => {
312                            // Make sure our persist source can shut down.
313                            persist_token.take();
314                            snapshot_cap.downgrade(&[]);
315                            UpsertErrorEmitter::<T>::emit(
316                                &mut error_emitter,
317                                "Failed to rehydrate state".to_string(),
318                                e,
319                            )
320                            .await;
321                        }
322                    }
323
324                    tracing::debug!(
325                        worker_id = %source_config.worker_id,
326                        source_id = %source_config.id,
327                        ?resume_upper,
328                        ?persist_upper,
329                        "downgrading snapshot cap",
330                    );
331
332                    // Only downgrade this _after_ ingesting the data, because
333                    // that can actually take quite some time, and we don't want
334                    // to announce that we're done ingesting the initial
335                    // snapshot too early.
336                    //
337                    // When we finish ingesting our initial persist snapshot,
338                    // during "re-hydration", we downgrade this to the empty
339                    // frontier, so we need to be lenient to this failing from
340                    // then on.
341                    let _ = snapshot_cap.try_downgrade(persist_upper.iter());
342
343
344
345                    if last_rehydration_chunk {
346                        hydrating = false;
347
348                        tracing::info!(
349                            worker_id = %source_config.worker_id,
350                            source_id = %source_config.id,
351                            "upsert source finished rehydration",
352                        );
353
354                        snapshot_cap.downgrade(&[]);
355                    }
356
357                }
358                _ = input.ready() => {
359                    let mut events_processed = 0;
360                    while let Some(event) = input.next_sync() {
361                        match event {
362                            AsyncEvent::Data(cap, mut data) => {
363                                let cap = sole_capability(&cap).clone();
364                                tracing::trace!(
365                                    worker_id = %source_config.worker_id,
366                                    source_id = %source_config.id,
367                                    time=?cap.time(),
368                                    updates=%data.len(),
369                                    "received data");
370
371                                let event_time = cap.time().clone();
372
373                                stage_input(
374                                    &mut stash,
375                                    &mut data,
376                                    &input_upper,
377                                    &resume_upper,
378                                );
379                                if !stash.is_empty() {
380                                    // Update the stashed capability to the minimum
381                                    stash_cap = match stash_cap {
382                                        Some(stash_cap) => {
383                                            if cap.time() < stash_cap.time() {
384                                                Some(cap)
385                                            } else {
386                                                Some(stash_cap)
387                                            }
388                                        }
389                                        None => Some(cap)
390                                    };
391                                }
392
393                                if prevent_snapshot_buffering
394                                    && input_upper.as_option()
395                                        == Some(&event_time)
396                                {
397                                    tracing::debug!(
398                                        worker_id = %source_config.worker_id,
399                                        source_id = %source_config.id,
400                                        ?event_time,
401                                        ?resume_upper,
402                                        ?input_upper,
403                                        "allowing partial drain");
404                                    partial_drain_time = Some(event_time.clone());
405                                } else {
406                                    tracing::debug!(
407                                        worker_id = %source_config.worker_id,
408                                        source_id = %source_config.id,
409                                        %prevent_snapshot_buffering,
410                                        ?event_time,
411                                        ?resume_upper,
412                                        ?input_upper,
413                                        "not allowing partial drain");
414                                }
415                            }
416                            AsyncEvent::Progress(upper) => {
417                                tracing::trace!(
418                                    worker_id = %source_config.worker_id,
419                                    source_id = %source_config.id,
420                                    ?upper,
421                                    "received progress");
422
423                                // Ignore progress updates before the `resume_upper`, which is our initial
424                                // capability post-snapshotting.
425                                if PartialOrder::less_than(&upper, &resume_upper) {
426                                    tracing::trace!(
427                                        worker_id = %source_config.worker_id,
428                                        source_id = %source_config.id,
429                                        ?upper,
430                                        ?resume_upper,
431                                        "ignoring progress updates before resume_upper");
432                                    continue;
433                                }
434
435                                // Disable partial drain, because this progress
436                                // update has moved the frontier. We might allow
437                                // it again once we receive data right at the
438                                // frontier again.
439                                partial_drain_time = None;
440                                input_upper = upper;
441                            }
442                        }
443
444                        events_processed += 1;
445                        if let Some(max) = snapshot_buffering_max {
446                            if events_processed >= max {
447                                break;
448                            }
449                        }
450                    }
451                }
452            };
453
454            // While we have partially ingested updates of a timestamp our state
455            // is in an inconsistent/consolidating state and accessing it would
456            // panic.
457            if let Some(largest_seen_persist_ts) = largest_seen_persist_ts.as_ref() {
458                let largest_seen_outer_persist_ts = largest_seen_persist_ts.clone().to_outer();
459                let outer_persist_upper = persist_upper.iter().map(|ts| ts.clone().to_outer());
460                let outer_persist_upper = Antichain::from_iter(outer_persist_upper);
461                if outer_persist_upper.less_equal(&largest_seen_outer_persist_ts) {
462                    continue;
463                }
464            }
465
466            // We try and drain from our stash every time we go through the
467            // loop. More of our stash can become eligible for draining both
468            // when the source-input frontier advances or when the persist
469            // frontier advances.
470            if !stash.is_empty() {
471                let cap = stash_cap
472                    .as_mut()
473                    .expect("missing capability for non-empty stash");
474
475                tracing::trace!(
476                    worker_id = %source_config.worker_id,
477                    source_id = %source_config.id,
478                    ?cap,
479                    ?stash,
480                    "stashed updates");
481
482                let mut min_remaining_time = drain_staged_input::<_, _, _, _>(
483                    &mut stash,
484                    &mut commands_state,
485                    &mut output_updates,
486                    &mut multi_get_scratch,
487                    DrainStyle::ToUpper {
488                        input_upper: &input_upper,
489                        persist_upper: &persist_upper,
490                    },
491                    &mut error_emitter,
492                    &mut state,
493                    &source_config,
494                )
495                .await;
496
497                tracing::trace!(
498                    worker_id = %source_config.worker_id,
499                    source_id = %source_config.id,
500                    output_updates = %output_updates.len(),
501                    "output updates for complete timestamp");
502
503                for (update, ts, diff) in output_updates.drain(..) {
504                    output_handle.give(cap, (update, ts, diff));
505                }
506
507                if !stash.is_empty() {
508                    let min_remaining_time = min_remaining_time
509                        .take()
510                        .expect("we still have updates left");
511                    cap.downgrade(&min_remaining_time);
512                } else {
513                    stash_cap = None;
514                }
515            }
516
517            if input_upper.is_empty() {
518                tracing::debug!(
519                    worker_id = %source_config.worker_id,
520                    source_id = %source_config.id,
521                    "input exhausted, shutting down");
522                break;
523            };
524
525            // If there were staged events that occurred at the capability time, drain
526            // them. This is safe because out-of-order updates to the same key that are
527            // drained in separate calls to `drain_staged_input` are correctly ordered by
528            // their `FromTime` in `drain_staged_input`.
529            //
530            // Note also that this may result in more updates in the output collection than
531            // the minimum. However, because the frontier only advances on `Progress` updates,
532            // the collection always accumulates correctly for all keys.
533            if let Some(partial_drain_time) = &partial_drain_time {
534                if !stash.is_empty() {
535                    let cap = stash_cap
536                        .as_mut()
537                        .expect("missing capability for non-empty stash");
538
539                    tracing::trace!(
540                        worker_id = %source_config.worker_id,
541                        source_id = %source_config.id,
542                        ?cap,
543                        ?stash,
544                        "stashed updates");
545
546                    let mut min_remaining_time = drain_staged_input::<_, _, _, _>(
547                        &mut stash,
548                        &mut commands_state,
549                        &mut output_updates,
550                        &mut multi_get_scratch,
551                        DrainStyle::AtTime {
552                            time: partial_drain_time.clone(),
553                            persist_upper: &persist_upper,
554                        },
555                        &mut error_emitter,
556                        &mut state,
557                        &source_config,
558                    )
559                    .await;
560
561                    tracing::trace!(
562                        worker_id = %source_config.worker_id,
563                        source_id = %source_config.id,
564                        output_updates = %output_updates.len(),
565                        "output updates for partial timestamp");
566
567                    for (update, ts, diff) in output_updates.drain(..) {
568                        output_handle.give(cap, (update, ts, diff));
569                    }
570
571                    if !stash.is_empty() {
572                        let min_remaining_time = min_remaining_time
573                            .take()
574                            .expect("we still have updates left");
575                        cap.downgrade(&min_remaining_time);
576                    } else {
577                        stash_cap = None;
578                    }
579                }
580            }
581        }
582    });
583
584    (
585        output
586            .as_collection()
587            .map(|result: UpsertValue| match result {
588                Ok(ok) => Ok(ok),
589                Err(err) => Err(DataflowError::from(EnvelopeError::Upsert(*err))),
590            }),
591        health_stream,
592        snapshot_stream,
593        shutdown_button.press_on_drop(),
594    )
595}
596
597/// Helper method for [`upsert_inner`] used to stage `data` updates
598/// from the input/source timely edge.
599#[allow(clippy::disallowed_types)]
600fn stage_input<T, FromTime>(
601    stash: &mut Vec<(T, UpsertKey, Reverse<FromTime>, Option<UpsertValue>)>,
602    data: &mut Vec<((UpsertKey, Option<UpsertValue>, FromTime), T, Diff)>,
603    input_upper: &Antichain<T>,
604    resume_upper: &Antichain<T>,
605) where
606    T: PartialOrder + timely::progress::Timestamp,
607    FromTime: Ord,
608{
609    if PartialOrder::less_equal(input_upper, resume_upper) {
610        data.retain(|(_, ts, _)| resume_upper.less_equal(ts));
611    }
612
613    stash.extend(data.drain(..).map(|((key, value, order), time, diff)| {
614        assert!(diff.is_positive(), "invalid upsert input");
615        (time, key, Reverse(order), value)
616    }));
617}
618
619/// The style of drain we are performing on the stash. `AtTime`-drains cannot
620/// assume that all values have been seen, and must leave tombstones behind for deleted values.
621#[derive(Debug)]
622enum DrainStyle<'a, T> {
623    ToUpper {
624        input_upper: &'a Antichain<T>,
625        persist_upper: &'a Antichain<T>,
626    },
627    // For partial draining when taking the source snapshot.
628    AtTime {
629        time: T,
630        persist_upper: &'a Antichain<T>,
631    },
632}
633
634/// Helper method for [`upsert_inner`] used to stage `data` updates
635/// from the input timely edge.
636///
637/// Returns the minimum observed time across the updates that remain in the
638/// stash or `None` if none are left.
639///
640/// ## Correctness
641///
642/// It is safe to call this function multiple times with the same `persist_upper` provided that the
643/// drain style is `AtTime`, which updates the state such that past actions are remembered and can
644/// be undone in subsequent calls.
645///
646/// It is *not* safe to call this function more than once with the same `persist_upper` and a
647/// `ToUpper` drain style. Doing so causes all calls except the first one to base their work on
648/// stale state, since in this drain style no modifications to the state are made.
649async fn drain_staged_input<S, T, FromTime, E>(
650    stash: &mut Vec<(T, UpsertKey, Reverse<FromTime>, Option<UpsertValue>)>,
651    commands_state: &mut indexmap::IndexMap<UpsertKey, UpsertValueAndSize<T, FromTime>>,
652    output_updates: &mut Vec<(UpsertValue, T, Diff)>,
653    multi_get_scratch: &mut Vec<UpsertKey>,
654    drain_style: DrainStyle<'_, T>,
655    error_emitter: &mut E,
656    state: &mut UpsertState<'_, S, T, FromTime>,
657    source_config: &crate::source::SourceExportCreationConfig,
658) -> Option<T>
659where
660    S: UpsertStateBackend<T, FromTime>,
661    T: Timestamp + TotalOrder + timely::ExchangeData + Clone + Debug + Ord + Sync,
662    FromTime: timely::ExchangeData + Clone + Ord + Sync,
663    E: UpsertErrorEmitter<T>,
664{
665    let mut min_remaining_time = Antichain::new();
666
667    let mut eligible_updates = stash
668        .extract_if(.., |(ts, _, _, _)| {
669            let eligible = match &drain_style {
670                DrainStyle::ToUpper {
671                    input_upper,
672                    persist_upper,
673                } => {
674                    // We make sure that a) we only process updates when we know their
675                    // timestamp is complete, that is there will be no more updates for
676                    // that timestamp, and b) that "previous" times in the persist
677                    // input are complete. The latter makes sure that we emit updates
678                    // for the next timestamp that are consistent with the global state
679                    // in the output persist shard, which also serves as a persistent
680                    // copy of our in-memory/on-disk upsert state.
681                    !input_upper.less_equal(ts) && !persist_upper.less_than(ts)
682                }
683                DrainStyle::AtTime {
684                    time,
685                    persist_upper,
686                } => {
687                    // Even when emitting partial updates, we still need to wait
688                    // until "previous" times in the persist input are complete.
689                    *ts <= *time && !persist_upper.less_than(ts)
690                }
691            };
692
693            if !eligible {
694                min_remaining_time.insert(ts.clone());
695            }
696
697            eligible
698        })
699        .filter(|(ts, _, _, _)| {
700            let persist_upper = match &drain_style {
701                DrainStyle::ToUpper {
702                    input_upper: _,
703                    persist_upper,
704                } => persist_upper,
705                DrainStyle::AtTime {
706                    time: _,
707                    persist_upper,
708                } => persist_upper,
709            };
710
711            // Any update that is "in the past" of the persist upper is not
712            // relevant anymore. We _can_ emit changes for it, but the
713            // downstream persist_sink would filter these updates out because
714            // the shard upper is already further ahead.
715            //
716            // Plus, our upsert state is up-to-date to the persist_upper, so we
717            // wouldn't be able to emit correct retractions for incoming
718            // commands whose `ts` is in the past of that.
719            let relevant = persist_upper.less_equal(ts);
720            relevant
721        })
722        .collect_vec();
723
724    tracing::debug!(
725        worker_id = %source_config.worker_id,
726        source_id = %source_config.id,
727        ?drain_style,
728        remaining = %stash.len(),
729        eligible = eligible_updates.len(),
730        "draining stash");
731
732    // Sort the eligible updates by (key, time, Reverse(from_time)) so that
733    // deduping by (key, time) gives the latest change for that key.
734    eligible_updates.sort_unstable_by(|a, b| {
735        let (ts1, key1, from_ts1, val1) = a;
736        let (ts2, key2, from_ts2, val2) = b;
737        Ord::cmp(&(ts1, key1, from_ts1, val1), &(ts2, key2, from_ts2, val2))
738    });
739
740    // Read the previous values _per key_ out of `state`, recording it
741    // along with the value with the _latest timestamp for that key_.
742    commands_state.clear();
743    for (_, key, _, _) in eligible_updates.iter() {
744        commands_state.entry(*key).or_default();
745    }
746
747    // These iterators iterate in the same order because `commands_state`
748    // is an `IndexMap`.
749    multi_get_scratch.clear();
750    multi_get_scratch.extend(commands_state.iter().map(|(k, _)| *k));
751    match state
752        .multi_get(multi_get_scratch.drain(..), commands_state.values_mut())
753        .await
754    {
755        Ok(_) => {}
756        Err(e) => {
757            error_emitter
758                .emit("Failed to fetch records from state".to_string(), e)
759                .await;
760        }
761    }
762
763    // From the prefix that can be emitted we can deduplicate based on (ts, key) in
764    // order to only process the command with the maximum order within the (ts,
765    // key) group. This is achieved by wrapping order in `Reverse(FromTime)` above.;
766    let mut commands = eligible_updates.into_iter().dedup_by(|a, b| {
767        let ((a_ts, a_key, _, _), (b_ts, b_key, _, _)) = (a, b);
768        a_ts == b_ts && a_key == b_key
769    });
770
771    let bincode_opts = upsert_types::upsert_bincode_opts();
772    // Upsert the values into `commands_state`, by recording the latest
773    // value (or deletion). These will be synced at the end to the `state`.
774    //
775    // Note that we are effectively doing "mini-upsert" here, using
776    // `command_state`. This "mini-upsert" is seeded with data from `state`, using
777    // a single `multi_get` above, and the final state is written out into
778    // `state` using a single `multi_put`. This simplifies `UpsertStateBackend`
779    // implementations, and reduces the number of reads and write we need to do.
780    //
781    // This "mini-upsert" technique is actually useful in `UpsertState`'s
782    // `consolidate_snapshot_read_write_inner` implementation, minimizing gets and puts on
783    // the `UpsertStateBackend` implementations. In some sense, its "upsert all the way down".
784    while let Some((ts, key, from_time, value)) = commands.next() {
785        let mut command_state = if let Entry::Occupied(command_state) = commands_state.entry(key) {
786            command_state
787        } else {
788            panic!("key missing from commands_state");
789        };
790
791        let existing_state_cell = &mut command_state.get_mut().value;
792
793        if let Some(cs) = existing_state_cell.as_mut() {
794            cs.ensure_decoded(bincode_opts, source_config.id, Some(&key));
795        }
796
797        // Skip this command if its order key is below the one in the upsert state.
798        // Note that the existing order key may be `None` if the existing value
799        // is from snapshotting, which always sorts below new values/deletes.
800        let existing_order = existing_state_cell
801            .as_ref()
802            .and_then(|cs| cs.provisional_order(&ts));
803        if existing_order >= Some(&from_time.0) {
804            // Skip this update. If no later updates adjust this key, then we just
805            // end up writing the same value back to state. If there
806            // is nothing in the state, `existing_order` is `None`, and this
807            // does not occur.
808            continue;
809        }
810
811        match value {
812            Some(value) => {
813                if let Some(old_value) = existing_state_cell.as_ref() {
814                    if let Some(old_value) = old_value.provisional_value_ref(&ts) {
815                        output_updates.push((old_value.clone(), ts.clone(), Diff::MINUS_ONE));
816                    }
817                }
818
819                match &drain_style {
820                    DrainStyle::AtTime { .. } => {
821                        let existing_value = existing_state_cell.take();
822
823                        let new_value = match existing_value {
824                            Some(existing_value) => existing_value.clone().into_provisional_value(
825                                value.clone(),
826                                ts.clone(),
827                                from_time.0.clone(),
828                            ),
829                            None => StateValue::new_provisional_value(
830                                value.clone(),
831                                ts.clone(),
832                                from_time.0.clone(),
833                            ),
834                        };
835
836                        existing_state_cell.replace(new_value);
837                    }
838                    DrainStyle::ToUpper { .. } => {
839                        // Not writing down provisional values, or anything.
840                    }
841                };
842
843                output_updates.push((value, ts, Diff::ONE));
844            }
845            None => {
846                if let Some(old_value) = existing_state_cell.as_ref() {
847                    if let Some(old_value) = old_value.provisional_value_ref(&ts) {
848                        output_updates.push((old_value.clone(), ts.clone(), Diff::MINUS_ONE));
849                    }
850                }
851
852                match &drain_style {
853                    DrainStyle::AtTime { .. } => {
854                        let existing_value = existing_state_cell.take();
855
856                        let new_value = match existing_value {
857                            Some(existing_value) => existing_value
858                                .into_provisional_tombstone(ts.clone(), from_time.0.clone()),
859                            None => StateValue::new_provisional_tombstone(
860                                ts.clone(),
861                                from_time.0.clone(),
862                            ),
863                        };
864
865                        existing_state_cell.replace(new_value);
866                    }
867                    DrainStyle::ToUpper { .. } => {
868                        // Not writing down provisional values, or anything.
869                    }
870                }
871            }
872        }
873    }
874
875    match &drain_style {
876        DrainStyle::AtTime { .. } => {
877            match state
878                .multi_put(
879                    // We don't want to update per-record stats, like size of
880                    // records indexed or count of records indexed.
881                    //
882                    // We only add provisional values and these will be
883                    // overwritten once we receive updates for state from the
884                    // persist input. And the merge functionality cannot know
885                    // what was in state before merging, so it cannot correctly
886                    // retract/update stats added here.
887                    //
888                    // Mostly, the merge functionality can't update those stats
889                    // because merging happens in a function that we pass to
890                    // rocksdb which doesn't have access to any external
891                    // context. And in general, with rocksdb we do blind writes
892                    // rather than inspect what was there before when
893                    // updating/inserting.
894                    false,
895                    commands_state.drain(..).map(|(k, cv)| {
896                        (
897                            k,
898                            upsert_types::PutValue {
899                                value: cv.value.map(|cv| cv.into_decoded()),
900                                previous_value_metadata: cv.metadata.map(|v| ValueMetadata {
901                                    size: v.size.try_into().expect("less than i64 size"),
902                                    is_tombstone: v.is_tombstone,
903                                }),
904                            },
905                        )
906                    }),
907                )
908                .await
909            {
910                Ok(_) => {}
911                Err(e) => {
912                    error_emitter
913                        .emit("Failed to update records in state".to_string(), e)
914                        .await;
915                }
916            }
917        }
918        style @ DrainStyle::ToUpper { .. } => {
919            tracing::trace!(
920                worker_id = %source_config.worker_id,
921                source_id = %source_config.id,
922                "not doing state update for drain style {:?}", style);
923        }
924    }
925
926    min_remaining_time.into_option()
927}
928
929#[cfg(test)]
930mod test {
931    //! No test drives errors through the persist feedback, so every harness below closes
932    //! the error input by dropping its handle.
933
934    use std::sync::mpsc;
935
936    use mz_ore::metrics::MetricsRegistry;
937    use mz_persist_types::ShardId;
938    use mz_repr::{Datum, Timestamp as MzTimestamp};
939    use mz_rocksdb::{RocksDBConfig, ValueIterator};
940    use mz_storage_operators::persist_source::Subtime;
941    use mz_storage_types::sources::SourceEnvelope;
942    use mz_storage_types::sources::envelope::{KeyEnvelope, UpsertEnvelope, UpsertStyle};
943    use rocksdb::Env;
944    use timely::dataflow::operators::capture::Extract;
945    use timely::dataflow::operators::{Capture, Input, Probe};
946    use timely::progress::Timestamp;
947
948    use crate::metrics::StorageMetrics;
949    use crate::metrics::upsert::UpsertMetricDefs;
950    use crate::source::SourceExportCreationConfig;
951    use crate::statistics::{SourceStatistics, SourceStatisticsMetricDefs};
952    use crate::upsert::memory::InMemoryHashMap;
953    use crate::upsert::types::{BincodeOpts, consolidating_merge_function, upsert_bincode_opts};
954
955    use super::*;
956
957    #[mz_ore::test]
958    #[cfg_attr(miri, ignore)]
959    fn gh_9160_repro() {
960        // Helper to wrap timestamps in the appropriate types
961        let new_ts = |ts| (MzTimestamp::new(ts), Subtime::minimum());
962
963        let output_handle = timely::execute_directly(move |worker| {
964            let (mut input_handle, mut persist_handle, output_handle) = worker
965                .dataflow::<MzTimestamp, _, _>(|scope| {
966                    // Enter a subscope since the upsert operator expects to work a backpressure
967                    // enabled scope.
968                    scope.scoped::<(MzTimestamp, Subtime), _, _>("upsert", |scope| {
969                        let (input_handle, input) = scope.new_input();
970                        let (persist_handle, persist_input) = scope.new_input();
971                        let (_persist_err_handle, persist_err_input) = scope.new_input();
972                        let upsert_config = UpsertConfig {
973                            shrink_upsert_unused_buffers_by_ratio: 0,
974                        };
975                        let source_id = GlobalId::User(0);
976                        let metrics_registry = MetricsRegistry::new();
977                        let upsert_metrics_defs =
978                            UpsertMetricDefs::register_with(&metrics_registry);
979                        let upsert_metrics =
980                            UpsertMetrics::new(&upsert_metrics_defs, source_id, 0, None);
981
982                        let metrics_registry = MetricsRegistry::new();
983                        let storage_metrics = StorageMetrics::register_with(&metrics_registry);
984
985                        let metrics_registry = MetricsRegistry::new();
986                        let source_statistics_defs =
987                            SourceStatisticsMetricDefs::register_with(&metrics_registry);
988                        let envelope = SourceEnvelope::Upsert(UpsertEnvelope {
989                            source_arity: 2,
990                            style: UpsertStyle::Default(KeyEnvelope::Flattened),
991                            key_indices: vec![0],
992                        });
993                        let source_statistics = SourceStatistics::new(
994                            source_id,
995                            0,
996                            &source_statistics_defs,
997                            source_id,
998                            &ShardId::new(),
999                            envelope,
1000                            Antichain::from_elem(Timestamp::minimum()),
1001                            Antichain::from_elem(Timestamp::minimum()),
1002                        );
1003
1004                        let source_config = SourceExportCreationConfig {
1005                            id: GlobalId::User(0),
1006                            worker_id: 0,
1007                            metrics: storage_metrics,
1008                            source_statistics,
1009                        };
1010
1011                        let (output, _, _, button) = upsert_inner(
1012                            input.as_collection(),
1013                            vec![0],
1014                            Antichain::from_elem(Timestamp::minimum()),
1015                            persist_input.as_collection(),
1016                            persist_err_input.as_collection(),
1017                            None,
1018                            upsert_metrics,
1019                            source_config,
1020                            || async { InMemoryHashMap::default() },
1021                            upsert_config,
1022                            true,
1023                            None,
1024                        );
1025                        std::mem::forget(button);
1026
1027                        (input_handle, persist_handle, output.inner.capture())
1028                    })
1029                });
1030
1031            // We work with a hypothetical schema of (key int, value int).
1032
1033            // The input will contain records for two keys, 0 and 1.
1034            let key0 = UpsertKey::from_key(Ok(&Row::pack_slice(&[Datum::Int64(0)])));
1035            let key1 = UpsertKey::from_key(Ok(&Row::pack_slice(&[Datum::Int64(1)])));
1036
1037            // We will assume that the kafka topic contains the following messages with their
1038            // associated reclocked timestamp:
1039            //  1. {offset=1, key=0, value=0}    @ mz_time = 0
1040            //  2. {offset=2, key=1, value=NULL} @ mz_time = 2  // <- deletion of unrelated key. Causes the operator
1041            //                                                  //    to maintain the associated cap to time 2
1042            //  3. {offset=3, key=0, value=1}    @ mz_time = 3
1043            //  4. {offset=4, key=0, value=2}    @ mz_time = 3  // <- messages 2 and 3 are reclocked to time 3
1044            let value1 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(0)]);
1045            let value3 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(1)]);
1046            let value4 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(2)]);
1047            let msg1 = (key0, Some(Ok(value1.clone())), 1);
1048            let msg2 = (key1, None, 2);
1049            let msg3 = (key0, Some(Ok(value3)), 3);
1050            let msg4 = (key0, Some(Ok(value4)), 4);
1051
1052            // The first message will initialize the upsert state such that key 0 has value 0 and
1053            // produce an output update to that effect.
1054            input_handle.send((msg1, new_ts(0), Diff::ONE));
1055            input_handle.advance_to(new_ts(2));
1056            worker.step();
1057
1058            // We assume this worker succesfully CAAs the update to the shard so we send it back
1059            // through the persist_input
1060            persist_handle.send((value1, new_ts(0), Diff::ONE));
1061            persist_handle.advance_to(new_ts(1));
1062            worker.step();
1063
1064            // Then, messages 2 and 3 are sent as one batch with capability = 2
1065            input_handle.send_batch(&mut vec![
1066                (msg2, new_ts(2), Diff::ONE),
1067                (msg3, new_ts(3), Diff::ONE),
1068            ]);
1069            // Advance our capability to 3
1070            input_handle.advance_to(new_ts(3));
1071            // Message 4 is sent with capability 3
1072            input_handle.send_batch(&mut vec![(msg4, new_ts(3), Diff::ONE)]);
1073            // Advance our capability to 4
1074            input_handle.advance_to(new_ts(4));
1075            // We now step the worker so that the pending data is received. This causes the
1076            // operator to store internally the following map from capabilities to updates:
1077            // cap=2 => [ msg2, msg3 ]
1078            // cap=3 => [ msg4 ]
1079            worker.step();
1080
1081            // We now assume that another replica raced us and processed msg1 at time 2, which in
1082            // this test is a no-op so the persist frontier advances to time 3 without new data.
1083            persist_handle.advance_to(new_ts(3));
1084            // We now step this worker again, which will notice that the persist upper is {3} and
1085            // wlil attempt to process msg3 and msg4 *separately*, causing it to produce a double
1086            // retraction.
1087            worker.step();
1088
1089            output_handle
1090        });
1091
1092        let mut actual_output = output_handle
1093            .extract()
1094            .into_iter()
1095            .flat_map(|(_cap, container)| container)
1096            .collect();
1097        differential_dataflow::consolidation::consolidate_updates(&mut actual_output);
1098
1099        // The expected consolidated output contains only updates for key 0 which has the value 0
1100        // at timestamp 0 and the value 2 at timestamp 3
1101        let value1 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(0)]);
1102        let value4 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(2)]);
1103        let expected_output: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1104            (Ok(value1.clone()), new_ts(0), Diff::ONE),
1105            (Ok(value1), new_ts(3), Diff::MINUS_ONE),
1106            (Ok(value4), new_ts(3), Diff::ONE),
1107        ];
1108        assert_eq!(actual_output, expected_output);
1109    }
1110
1111    #[mz_ore::test]
1112    #[cfg_attr(miri, ignore)]
1113    fn gh_9540_repro() {
1114        // Helper to wrap timestamps in the appropriate types
1115        let mz_ts = |ts| (MzTimestamp::new(ts), Subtime::minimum());
1116        let (tx, rx) = mpsc::channel::<std::thread::JoinHandle<()>>();
1117
1118        let rocksdb_dir = tempfile::tempdir().unwrap();
1119        let output_handle = timely::execute_directly(move |worker| {
1120            let tx = tx.clone();
1121            let (mut input_handle, mut persist_handle, output_probe, output_handle) =
1122                worker.dataflow::<MzTimestamp, _, _>(|scope| {
1123                    // Enter a subscope since the upsert operator expects to work a backpressure
1124                    // enabled scope.
1125                    scope.scoped::<(MzTimestamp, Subtime), _, _>("upsert", |scope| {
1126                        let (input_handle, input) = scope.new_input();
1127                        let (persist_handle, persist_input) = scope.new_input();
1128                        let (_persist_err_handle, persist_err_input) = scope.new_input();
1129                        let upsert_config = UpsertConfig {
1130                            shrink_upsert_unused_buffers_by_ratio: 0,
1131                        };
1132                        let source_id = GlobalId::User(0);
1133                        let metrics_registry = MetricsRegistry::new();
1134                        let upsert_metrics_defs =
1135                            UpsertMetricDefs::register_with(&metrics_registry);
1136                        let upsert_metrics =
1137                            UpsertMetrics::new(&upsert_metrics_defs, source_id, 0, None);
1138                        let rocksdb_shared_metrics = Arc::clone(&upsert_metrics.rocksdb_shared);
1139                        let rocksdb_instance_metrics =
1140                            Arc::clone(&upsert_metrics.rocksdb_instance_metrics);
1141
1142                        let metrics_registry = MetricsRegistry::new();
1143                        let storage_metrics = StorageMetrics::register_with(&metrics_registry);
1144
1145                        let metrics_registry = MetricsRegistry::new();
1146                        let source_statistics_defs =
1147                            SourceStatisticsMetricDefs::register_with(&metrics_registry);
1148                        let envelope = SourceEnvelope::Upsert(UpsertEnvelope {
1149                            source_arity: 2,
1150                            style: UpsertStyle::Default(KeyEnvelope::Flattened),
1151                            key_indices: vec![0],
1152                        });
1153                        let source_statistics = SourceStatistics::new(
1154                            source_id,
1155                            0,
1156                            &source_statistics_defs,
1157                            source_id,
1158                            &ShardId::new(),
1159                            envelope,
1160                            Antichain::from_elem(Timestamp::minimum()),
1161                            Antichain::from_elem(Timestamp::minimum()),
1162                        );
1163
1164                        let source_config = SourceExportCreationConfig {
1165                            id: GlobalId::User(0),
1166                            worker_id: 0,
1167                            metrics: storage_metrics,
1168                            source_statistics,
1169                        };
1170
1171                        // A closure that will initialize and return a configured RocksDB instance
1172                        let rocksdb_init_fn = move || async move {
1173                            let merge_operator = Some((
1174                                "upsert_state_snapshot_merge_v1".to_string(),
1175                                |a: &[u8],
1176                                 b: ValueIterator<
1177                                    BincodeOpts,
1178                                    StateValue<(MzTimestamp, Subtime), u64>,
1179                                >| {
1180                                    consolidating_merge_function::<(MzTimestamp, Subtime), u64>(
1181                                        a.into(),
1182                                        b,
1183                                    )
1184                                },
1185                            ));
1186                            let rocksdb_cleanup_tries = 5;
1187                            let tuning = RocksDBConfig::new(Default::default(), None);
1188                            let mut rocksdb_inst = mz_rocksdb::RocksDBInstance::new(
1189                                rocksdb_dir.path(),
1190                                mz_rocksdb::InstanceOptions::new(
1191                                    Env::mem_env().unwrap(),
1192                                    rocksdb_cleanup_tries,
1193                                    merge_operator,
1194                                    // For now, just use the same config as the one used for
1195                                    // merging snapshots.
1196                                    upsert_bincode_opts(),
1197                                ),
1198                                tuning,
1199                                rocksdb_shared_metrics,
1200                                rocksdb_instance_metrics,
1201                            )
1202                            .unwrap();
1203
1204                            let handle = rocksdb_inst.take_core_loop_handle().expect("join handle");
1205                            tx.send(handle).expect("sent joinhandle");
1206                            crate::upsert::rocksdb::RocksDB::new(rocksdb_inst)
1207                        };
1208
1209                        let (output, _, _, button) = upsert_inner(
1210                            input.as_collection(),
1211                            vec![0],
1212                            Antichain::from_elem(Timestamp::minimum()),
1213                            persist_input.as_collection(),
1214                            persist_err_input.as_collection(),
1215                            None,
1216                            upsert_metrics,
1217                            source_config,
1218                            rocksdb_init_fn,
1219                            upsert_config,
1220                            true,
1221                            None,
1222                        );
1223                        std::mem::forget(button);
1224
1225                        let (probe, stream) = output.inner.probe();
1226                        (input_handle, persist_handle, probe, stream.capture())
1227                    })
1228                });
1229
1230            // We work with a hypothetical schema of (key int, value int).
1231
1232            // The input will contain records for two keys, 0 and 1.
1233            let key0 = UpsertKey::from_key(Ok(&Row::pack_slice(&[Datum::Int64(0)])));
1234
1235            // We will assume that the kafka topic contains the following messages with their
1236            // associated reclocked timestamp:
1237            //  1. {offset=1, key=0, value=0}    @ mz_time = 0
1238            //  2. {offset=2, key=0, value=NULL} @ mz_time = 1
1239            //  3. {offset=3, key=0, value=0}    @ mz_time = 2
1240            //  4. {offset=4, key=0, value=NULL} @ mz_time = 2  // <- messages 3 and 4 are *BOTH* reclocked to time 2
1241            let value1 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(0)]);
1242            let msg1 = ((key0, Some(Ok(value1.clone())), 1), mz_ts(0), Diff::ONE);
1243            let msg2 = ((key0, None, 2), mz_ts(1), Diff::ONE);
1244            let msg3 = ((key0, Some(Ok(value1.clone())), 3), mz_ts(2), Diff::ONE);
1245            let msg4 = ((key0, None, 4), mz_ts(2), Diff::ONE);
1246
1247            // The first message will initialize the upsert state such that key 0 has value 0 and
1248            // produce an output update to that effect.
1249            input_handle.send(msg1);
1250            input_handle.advance_to(mz_ts(1));
1251            while output_probe.less_than(&mz_ts(1)) {
1252                worker.step_or_park(None);
1253            }
1254            // Feedback the produced output..
1255            persist_handle.send((value1.clone(), mz_ts(0), Diff::ONE));
1256            persist_handle.advance_to(mz_ts(1));
1257            // ..and send the next upsert command that deletes the key.
1258            input_handle.send(msg2);
1259            input_handle.advance_to(mz_ts(2));
1260            while output_probe.less_than(&mz_ts(2)) {
1261                worker.step_or_park(None);
1262            }
1263
1264            // Feedback the produced output..
1265            persist_handle.send((value1, mz_ts(1), Diff::MINUS_ONE));
1266            persist_handle.advance_to(mz_ts(2));
1267            // ..and send the next *out of order* upsert command that deletes the key. Here msg4
1268            // happens at offset 4 and the operator should rememeber that.
1269            input_handle.send(msg4);
1270            input_handle.flush();
1271            // Run the worker for enough steps to process these events. We can't guide the
1272            // execution with the probe here since the frontier does not advance, only provisional
1273            // updates are produced.
1274            for _ in 0..5 {
1275                worker.step();
1276            }
1277
1278            // Send the missing message that will now confuse the operator because it has lost
1279            // track that for key 0 it has already seen a command for offset 4, and therefore msg3
1280            // should be skipped.
1281            input_handle.send(msg3);
1282            input_handle.flush();
1283            input_handle.advance_to(mz_ts(3));
1284
1285            output_handle
1286        });
1287
1288        let mut actual_output = output_handle
1289            .extract()
1290            .into_iter()
1291            .flat_map(|(_cap, container)| container)
1292            .collect();
1293        differential_dataflow::consolidation::consolidate_updates(&mut actual_output);
1294
1295        // The expected consolidated output contains only updates for key 0 which has the value 0
1296        // at timestamp 0 and the value 2 at timestamp 3
1297        let value1 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(0)]);
1298        let expected_output: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1299            (Ok(value1.clone()), mz_ts(0), Diff::ONE),
1300            (Ok(value1), mz_ts(1), Diff::MINUS_ONE),
1301        ];
1302        assert_eq!(actual_output, expected_output);
1303
1304        while let Ok(handle) = rx.recv() {
1305            handle.join().expect("threads completed successfully");
1306        }
1307    }
1308
1309    /// v1 counterpart of v2's `lagging_replacement_below_upper_strands_data`,
1310    /// under the IDENTICAL setup: the external writer has advanced the feedback
1311    /// `persist_upper` to T = 10 with no operator output, and the lagging
1312    /// replacement then produces source data at ts BELOW that upper (5 and 7).
1313    ///
1314    /// v1's drain classifies any `ts` in the past of `persist_upper` as not
1315    /// relevant (`relevant = persist_upper.less_equal(ts)`) and DROPS it — the
1316    /// downstream persist_sink would filter it anyway since the shard upper is
1317    /// already further ahead. So v1 strands nothing and its output frontier
1318    /// advances cleanly past persist_upper (here to the input upper, 11),
1319    /// unlike v2, which without its drop-fix pins below.
1320    #[mz_ore::test]
1321    #[cfg_attr(miri, ignore)]
1322    fn lagging_replacement_below_upper_is_dropped() {
1323        let mz_ts = |ts| (MzTimestamp::new(ts), Subtime::minimum());
1324        let (tx, rx) = mpsc::channel::<std::thread::JoinHandle<()>>();
1325
1326        let rocksdb_dir = tempfile::tempdir().unwrap();
1327        let (frontier, capture) = timely::execute_directly(move |worker| {
1328            let tx = tx.clone();
1329            let (mut input, mut persist, probe, capture) =
1330                worker.dataflow::<MzTimestamp, _, _>(|scope| {
1331                    scope.scoped::<(MzTimestamp, Subtime), _, _>("upsert", |scope| {
1332                        let (input_handle, input) = scope.new_input();
1333                        let (persist_handle, persist_input) = scope.new_input();
1334                        let (_persist_err_handle, persist_err_input) = scope.new_input();
1335                        let upsert_config = UpsertConfig {
1336                            shrink_upsert_unused_buffers_by_ratio: 0,
1337                        };
1338                        let source_id = GlobalId::User(0);
1339                        let metrics_registry = MetricsRegistry::new();
1340                        let upsert_metrics_defs =
1341                            UpsertMetricDefs::register_with(&metrics_registry);
1342                        let upsert_metrics =
1343                            UpsertMetrics::new(&upsert_metrics_defs, source_id, 0, None);
1344                        let rocksdb_shared_metrics = Arc::clone(&upsert_metrics.rocksdb_shared);
1345                        let rocksdb_instance_metrics =
1346                            Arc::clone(&upsert_metrics.rocksdb_instance_metrics);
1347
1348                        let metrics_registry = MetricsRegistry::new();
1349                        let storage_metrics = StorageMetrics::register_with(&metrics_registry);
1350
1351                        let metrics_registry = MetricsRegistry::new();
1352                        let source_statistics_defs =
1353                            SourceStatisticsMetricDefs::register_with(&metrics_registry);
1354                        let envelope = SourceEnvelope::Upsert(UpsertEnvelope {
1355                            source_arity: 2,
1356                            style: UpsertStyle::Default(KeyEnvelope::Flattened),
1357                            key_indices: vec![0],
1358                        });
1359                        let source_statistics = SourceStatistics::new(
1360                            source_id,
1361                            0,
1362                            &source_statistics_defs,
1363                            source_id,
1364                            &ShardId::new(),
1365                            envelope,
1366                            Antichain::from_elem(Timestamp::minimum()),
1367                            Antichain::from_elem(Timestamp::minimum()),
1368                        );
1369
1370                        let source_config = SourceExportCreationConfig {
1371                            id: GlobalId::User(0),
1372                            worker_id: 0,
1373                            metrics: storage_metrics,
1374                            source_statistics,
1375                        };
1376
1377                        let rocksdb_init_fn = move || async move {
1378                            let merge_operator = Some((
1379                                "upsert_state_snapshot_merge_v1".to_string(),
1380                                |a: &[u8],
1381                                 b: ValueIterator<
1382                                    BincodeOpts,
1383                                    StateValue<(MzTimestamp, Subtime), u64>,
1384                                >| {
1385                                    consolidating_merge_function::<(MzTimestamp, Subtime), u64>(
1386                                        a.into(),
1387                                        b,
1388                                    )
1389                                },
1390                            ));
1391                            let rocksdb_cleanup_tries = 5;
1392                            let tuning = RocksDBConfig::new(Default::default(), None);
1393                            let mut rocksdb_inst = mz_rocksdb::RocksDBInstance::new(
1394                                rocksdb_dir.path(),
1395                                mz_rocksdb::InstanceOptions::new(
1396                                    Env::mem_env().unwrap(),
1397                                    rocksdb_cleanup_tries,
1398                                    merge_operator,
1399                                    upsert_bincode_opts(),
1400                                ),
1401                                tuning,
1402                                rocksdb_shared_metrics,
1403                                rocksdb_instance_metrics,
1404                            )
1405                            .unwrap();
1406
1407                            let handle = rocksdb_inst.take_core_loop_handle().expect("join handle");
1408                            tx.send(handle).expect("sent joinhandle");
1409                            crate::upsert::rocksdb::RocksDB::new(rocksdb_inst)
1410                        };
1411
1412                        let (output, _, _, button) = upsert_inner(
1413                            input.as_collection(),
1414                            vec![0],
1415                            Antichain::from_elem(Timestamp::minimum()),
1416                            persist_input.as_collection(),
1417                            persist_err_input.as_collection(),
1418                            None,
1419                            upsert_metrics,
1420                            source_config,
1421                            rocksdb_init_fn,
1422                            upsert_config,
1423                            true,
1424                            None,
1425                        );
1426                        std::mem::forget(button);
1427
1428                        let (probe, stream) = output.inner.probe();
1429                        (input_handle, persist_handle, probe, stream.capture())
1430                    })
1431                });
1432
1433            let key0 = UpsertKey::from_key(Ok(&Row::pack_slice(&[Datum::Int64(0)])));
1434            let key1 = UpsertKey::from_key(Ok(&Row::pack_slice(&[Datum::Int64(1)])));
1435            let value0 = Row::pack_slice(&[Datum::Int64(0), Datum::Int64(1)]);
1436            let value1 = Row::pack_slice(&[Datum::Int64(1), Datum::Int64(2)]);
1437
1438            // External writer has advanced the feedback persist_upper to T = 10
1439            // WITHOUT the operator emitting anything itself.
1440            persist.advance_to(mz_ts(10));
1441            for _ in 0..20 {
1442                worker.step();
1443            }
1444
1445            // Lagging replacement produces source data at ts BELOW the current
1446            // persist_upper (5 and 7 while persist_upper = 10).
1447            input.send(((key0, Some(Ok(value0)), 1), mz_ts(5), Diff::ONE));
1448            input.send(((key1, Some(Ok(value1)), 2), mz_ts(7), Diff::ONE));
1449            input.advance_to(mz_ts(11));
1450            for _ in 0..20 {
1451                worker.step();
1452            }
1453
1454            (probe.with_frontier(|f| f.to_vec()), capture)
1455        });
1456
1457        let mut emitted: Vec<_> = capture
1458            .extract()
1459            .into_iter()
1460            .flat_map(|(_cap, c)| c)
1461            .collect();
1462        differential_dataflow::consolidation::consolidate_updates(&mut emitted);
1463
1464        // v1 drops the below-upper data and lets its output frontier advance
1465        // freely (here to the input upper, 11): nothing is stranded, and the
1466        // frontier is never pinned below the shard upper (10).
1467        assert!(emitted.is_empty(), "v1 emitted {emitted:?}");
1468        assert_eq!(
1469            frontier,
1470            vec![mz_ts(11)],
1471            "v1 output frontier should advance to the input upper, not pin below \
1472             persist_upper as v2 does"
1473        );
1474        assert!(
1475            frontier[0] >= mz_ts(10),
1476            "v1 output frontier {frontier:?} should reach at least persist_upper (10)"
1477        );
1478
1479        while let Ok(handle) = rx.recv() {
1480            handle.join().expect("threads completed successfully");
1481        }
1482    }
1483}