1use std::fmt::Debug;
92
93use differential_dataflow::difference::{IsZero, Semigroup};
94use differential_dataflow::hashable::Hashable;
95use differential_dataflow::lattice::Lattice;
96use differential_dataflow::logging::Logger;
97use differential_dataflow::operators::arrange::agent::TraceAgent;
98use differential_dataflow::operators::arrange::arrangement::{Arranged, arrange_core};
99use differential_dataflow::trace::chunk::{ChunkBatcher, ChunkBuilder, ChunkSpine};
100use differential_dataflow::trace::{Batcher, Cursor, Description, TraceReader};
101use differential_dataflow::{AsCollection, VecCollection};
102use mz_dyncfg::ConfigSet;
103use mz_repr::{Datum, Diff, GlobalId, Row};
104#[cfg(feature = "fuzzing")]
106use mz_row_spine::DatumSeq;
107use mz_row_spine::{ValRowColPagedBuilder, ValRowSpine};
108use mz_storage_types::dyncfgs::ENABLE_UPSERT_CHUNKED_STASH;
109use mz_storage_types::errors::{DataflowError, EnvelopeError, UpsertError};
110use mz_timely_util::builder_async::{
111 AsyncOutputHandle, Event as AsyncEvent, OperatorBuilder as AsyncOperatorBuilder,
112 PressOnDropButton,
113};
114use mz_timely_util::columnar::batcher::ColumnChunker;
115use mz_timely_util::columnar::builder::ColumnBuilder;
116use mz_timely_util::columnar::chunk::{ChunkChunker, ColumnChunk};
117use mz_timely_util::columnar::merge_batcher::ColumnMergeBatcher;
118use mz_timely_util::columnar::unload::UnloadBatch;
119use mz_timely_util::columnar::{Col2ValPagedBatcher, Column};
120use mz_timely_util::containers::stack::FueledBuilder;
121use std::convert::Infallible;
122use timely::container::{CapacityContainerBuilder, PushInto};
123use timely::dataflow::channels::pact::{Exchange, Pipeline};
124use timely::dataflow::operators::generic::Operator;
125use timely::dataflow::operators::{Capability, CapabilitySet, Exchange as _};
126use timely::dataflow::{Stream, StreamVec};
127use timely::order::{PartialOrder, TotalOrder};
128use timely::progress::frontier::AntichainRef;
129use timely::progress::timestamp::Refines;
130use timely::progress::{Antichain, Timestamp};
131
132use crate::healthcheck::HealthStatusUpdate;
133use crate::metrics::upsert::UpsertMetrics;
134use crate::statistics::SourceStatistics;
135use crate::upsert::UpsertKey;
136use crate::upsert::UpsertSourceTime;
137use crate::upsert::UpsertValue;
138
139#[derive(Clone, Copy, Debug)]
144pub enum UpsertStashFlavor {
145 Paged,
149 Chunked,
152}
153
154impl UpsertStashFlavor {
155 pub fn from_config(config: &ConfigSet) -> Self {
159 if ENABLE_UPSERT_CHUNKED_STASH.get(config) {
160 Self::Chunked
161 } else {
162 Self::Paged
163 }
164 }
165}
166
167struct UpsertFeedbackBatcher<T: columnar::Columnar>(Col2ValPagedBatcher<UpsertKey, Row, T, Diff>);
179
180impl<T> Batcher for UpsertFeedbackBatcher<T>
181where
182 T: Timestamp + columnar::Columnar + Default + PartialOrder,
183 for<'a> columnar::Ref<'a, T>: Copy + Ord,
184{
185 type Output = Column<((UpsertKey, Row), T, Diff)>;
186 type Time = T;
187
188 fn new(logger: Option<Logger>, operator_id: usize) -> Self {
189 let mut batcher =
190 <Col2ValPagedBatcher<UpsertKey, Row, T, Diff> as Batcher>::new(logger, operator_id);
191 batcher.set_pager(crate::upsert::upsert_stash_pager::pager());
192 Self(batcher)
193 }
194
195 fn seal(&mut self, upper: Antichain<T>) -> (Vec<Self::Output>, Description<T>) {
196 self.0.seal(upper)
197 }
198
199 fn frontier(&mut self) -> AntichainRef<'_, T> {
200 self.0.frontier()
201 }
202}
203
204impl<T> PushInto<Column<((UpsertKey, Row), T, Diff)>> for UpsertFeedbackBatcher<T>
205where
206 T: Timestamp + columnar::Columnar + Default + PartialOrder,
207 for<'a> columnar::Ref<'a, T>: Copy + Ord,
208{
209 fn push_into(&mut self, chunk: Column<((UpsertKey, Row), T, Diff)>) {
210 self.0.push_into(chunk)
211 }
212}
213
214type FeedbackUpdate<T> = ((UpsertKey, Row), T, Diff);
217
218type FeedbackChunk<T> = ColumnChunk<(UpsertKey, Row), T, Diff>;
225
226type FeedbackSpine<T> = ChunkSpine<FeedbackChunk<T>>;
228
229#[derive(Clone, Debug, Default, columnar::Columnar)]
247#[columnar(derive(PartialEq, Eq, PartialOrd, Ord))]
248struct UpsertDiff<O> {
249 from_time: O,
250 value: Option<Row>,
251}
252
253impl<O> IsZero for UpsertDiff<O> {
254 fn is_zero(&self) -> bool {
255 false
256 }
257}
258
259impl<O: Ord + Clone> Semigroup for UpsertDiff<O> {
260 fn plus_equals(&mut self, rhs: &Self) {
261 if rhs.from_time > self.from_time {
262 *self = rhs.clone();
263 }
264 }
265}
266
267impl<'a, O> Semigroup<columnar::Ref<'a, UpsertDiff<O>>> for UpsertDiff<O>
274where
275 O: columnar::Columnar + Ord + Clone,
276{
277 fn plus_equals(&mut self, rhs: &columnar::Ref<'a, UpsertDiff<O>>) {
278 let rhs_from_time = <O as columnar::Columnar>::into_owned(rhs.from_time);
279 if rhs_from_time > self.from_time {
280 self.from_time = rhs_from_time;
281 self.value = <Option<Row> as columnar::Columnar>::into_owned(rhs.value);
282 }
283 }
284}
285
286type UpsertUpdate<T, O> = (UpsertKey, T, UpsertDiff<O>);
290
291type UpsertChunk<T, O> = ColumnChunk<UpsertKey, T, UpsertDiff<O>>;
294
295type UpsertChunkBatcher<T, O> = ChunkBatcher<UpsertChunk<T, O>>;
303
304type UpsertPagedBatcher<T, O> = ColumnMergeBatcher<UpsertKey, T, UpsertDiff<O>>;
309
310type UpsertChunker<T, O> = ColumnChunker<UpsertUpdate<T, O>>;
313
314type UpsertOutputHandle<T> =
318 AsyncOutputHandle<T, FueledBuilder<CapacityContainerBuilder<Vec<(UpsertValue, T, Diff)>>>>;
319
320#[doc(hidden)]
333pub fn upsert_value_to_row(value: &UpsertValue) -> Row {
334 let mut row = Row::default();
335 let mut packer = row.packer();
336 match value {
337 Ok(ok) => {
338 packer.push(Datum::UInt8(0));
339 packer.extend(ok.iter());
340 }
341 Err(err) => {
342 packer.push(Datum::UInt8(1));
343 let bytes =
344 bincode::serialize(err.as_ref()).expect("UpsertError is serializable via bincode");
345 packer.push(Datum::Bytes(&bytes));
346 }
347 }
348 row
349}
350
351fn upsert_value_byte_len(value: &UpsertValue) -> usize {
354 match value {
355 Ok(row) => row.byte_len(),
356 Err(err) => std::mem::size_of_val(err.as_ref()),
357 }
358}
359
360#[cfg(feature = "fuzzing")]
366#[doc(hidden)]
367pub fn datum_seq_to_upsert_value(seq: DatumSeq<'_>) -> UpsertValue {
368 decode_upsert_value(seq)
369}
370
371fn decode_upsert_value<'a>(mut iter: impl Iterator<Item = Datum<'a>>) -> UpsertValue {
374 let tag = match iter.next() {
375 Some(Datum::UInt8(tag)) => tag,
376 other => panic!("upsert value missing UInt8 tag, got {:?}", other),
377 };
378 match tag {
379 0 => {
380 let mut row = Row::default();
381 row.packer().extend(iter);
382 Ok(row)
383 }
384 1 => {
385 let bytes = match iter.next() {
386 Some(Datum::Bytes(b)) => b,
387 other => panic!("upsert error tag missing Bytes payload, got {:?}", other),
388 };
389 let err: UpsertError =
390 bincode::deserialize(bytes).expect("UpsertError bincode round-trip");
391 Err(Box::new(err))
392 }
393 tag => panic!("unknown upsert value tag {tag}"),
394 }
395}
396
397#[allow(clippy::disallowed_methods)]
412pub fn upsert_inner<'scope, T, FromTime>(
413 flavor: UpsertStashFlavor,
414 input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
415 key_indices: Vec<usize>,
416 resume_upper: Antichain<T>,
417 persist_input: VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
418 persist_token: Option<Vec<PressOnDropButton>>,
419 upsert_metrics: UpsertMetrics,
420 source_config: crate::source::SourceExportCreationConfig,
421) -> (
422 VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
423 StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
424 StreamVec<'scope, T, Infallible>,
425 PressOnDropButton,
426)
427where
428 T: Timestamp + TotalOrder + Sync,
429 T: Refines<mz_repr::Timestamp> + differential_dataflow::lattice::Lattice,
430 T: columnation::Columnation,
431 T: columnar::Columnar + Default,
432 for<'a> columnar::Ref<'a, T>: Copy + Ord,
433 FromTime: Debug + timely::ExchangeData + Clone + Ord + Sync,
434 FromTime: UpsertSourceTime,
435{
436 let encoded = encode_feedback(
440 persist_input,
441 key_indices,
442 source_config.source_statistics.clone(),
443 );
444 match flavor {
445 UpsertStashFlavor::Chunked => {
446 let persist_arranged = arrange_core::<
450 _,
451 _,
452 ChunkChunker<(UpsertKey, Row), T, Diff>,
453 ChunkBatcher<FeedbackChunk<T>>,
454 ChunkBuilder<FeedbackChunk<T>>,
455 FeedbackSpine<T>,
456 >(encoded, Pipeline, "Persist feedback");
457 build_upsert_operator::<ChunkedArm, _, _>(
458 input,
459 resume_upper,
460 persist_arranged,
461 persist_token,
462 upsert_metrics,
463 source_config,
464 )
465 }
466 UpsertStashFlavor::Paged => {
467 let persist_arranged = arrange_core::<
473 _,
474 _,
475 ColumnChunker<((UpsertKey, Row), T, Diff)>,
476 UpsertFeedbackBatcher<T>,
477 ValRowColPagedBuilder<UpsertKey, T, Diff>,
478 ValRowSpine<UpsertKey, T, Diff>,
479 >(encoded, Pipeline, "Persist feedback");
480 build_upsert_operator::<PagedArm, _, _>(
481 input,
482 resume_upper,
483 persist_arranged,
484 persist_token,
485 upsert_metrics,
486 source_config,
487 )
488 }
489 }
490}
491
492fn encode_feedback<'scope, T>(
498 persist_input: VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
499 key_indices: Vec<usize>,
500 source_statistics: SourceStatistics,
501) -> Stream<'scope, T, Column<((UpsertKey, Row), T, Diff)>>
502where
503 T: Timestamp + TotalOrder + Sync,
504 T: Refines<mz_repr::Timestamp> + differential_dataflow::lattice::Lattice,
505 T: columnation::Columnation,
506 T: columnar::Columnar + Default,
507 for<'a> columnar::Ref<'a, T>: Copy + Ord,
508{
509 let persist_keyed = persist_input.flat_map(move |result| {
511 let value = match result {
512 Ok(ok) => Ok(ok),
513 Err(DataflowError::EnvelopeError(err)) => match *err {
514 EnvelopeError::Upsert(err) => Err(Box::new(err)),
515 EnvelopeError::Flat(_) => return None,
516 },
517 Err(_) => return None,
518 };
519 let value_ref = match value {
520 Ok(ref row) => Ok(row),
521 Err(ref err) => Err(&**err),
522 };
523 Some((UpsertKey::from_value(value_ref, &key_indices), value))
524 });
525 let persist_keyed = persist_keyed
526 .inner
527 .exchange(move |((key, _), _, _)| UpsertKey::hashed(key))
530 .as_collection()
531 .inspect(move |((_, row), _, diff)| {
532 source_statistics.update_records_indexed_by(diff.into_inner());
533 source_statistics.update_bytes_indexed_by(
534 row.as_ref().map_or(0, |r| r.byte_len().try_into().unwrap()) * diff.into_inner(),
535 );
536 });
537 persist_keyed
538 .inner
539 .unary::<ColumnBuilder<((UpsertKey, Row), T, Diff)>, _, _, _>(
540 Pipeline,
541 "Persist feedback encode",
542 |_, _| {
543 move |input, output| {
544 input.for_each(|time, data| {
545 let mut session = output.session_with_builder(&time);
546 for ((key, value), ts, diff) in data.drain(..) {
547 let row = upsert_value_to_row(&value);
548 session.give(((&key, &row), &ts, &diff));
549 }
550 });
551 }
552 },
553 )
554}
555
556#[allow(clippy::disallowed_methods)]
564fn build_upsert_operator<'scope, A, T, FromTime>(
565 input: VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff>,
566 resume_upper: Antichain<T>,
567 persist_arranged: Arranged<'scope, TraceAgent<A::Spine>>,
568 persist_token: Option<Vec<PressOnDropButton>>,
569 upsert_metrics: UpsertMetrics,
570 source_config: crate::source::SourceExportCreationConfig,
571) -> (
572 VecCollection<'scope, T, Result<Row, DataflowError>, Diff>,
573 StreamVec<'scope, T, (Option<GlobalId>, HealthStatusUpdate)>,
574 StreamVec<'scope, T, Infallible>,
575 PressOnDropButton,
576)
577where
578 A: UpsertStashArm<T, FromTime::Order>,
579 T: Timestamp + TotalOrder + Sync,
580 T: Refines<mz_repr::Timestamp> + differential_dataflow::lattice::Lattice,
581 T: columnation::Columnation,
582 T: columnar::Columnar + Default,
583 for<'a> columnar::Ref<'a, T>: Copy + Ord,
584 FromTime: Debug + timely::ExchangeData + Clone + Ord + Sync,
585 FromTime: UpsertSourceTime,
586{
587 let mut persist_trace = persist_arranged.trace.clone();
588
589 use timely::dataflow::operators::Probe;
593 let (persist_probe, _persist_probe_stream) = persist_arranged.stream.probe();
594
595 let mut builder = AsyncOperatorBuilder::new("Upsert V2".to_string(), input.scope());
597
598 let (output_handle, output) = builder
599 .new_output::<FueledBuilder<CapacityContainerBuilder<Vec<(UpsertValue, T, Diff)>>>>();
600 let (_snapshot_handle, snapshot_stream) =
601 builder.new_output::<CapacityContainerBuilder<Vec<Infallible>>>();
602 let (_health_output, health_stream) = builder
603 .new_output::<CapacityContainerBuilder<Vec<(Option<GlobalId>, HealthStatusUpdate)>>>();
604
605 let mut input = builder.new_input_for(
606 input.inner,
607 Exchange::new(move |((key, _, _), _, _)| UpsertKey::hashed(key)),
608 &output_handle,
609 );
610
611 let mut persist_wakeup = builder.new_disconnected_input(_persist_probe_stream, Pipeline);
615
616 let shutdown_button = builder.build(move |caps| async move {
617 let _persist_token = persist_token;
620
621 let [output_cap, snapshot_cap, _health_cap]: [_; 3] = caps.try_into().unwrap();
622 drop(output_cap);
623 let mut snapshot_cap = CapabilitySet::from_elem(snapshot_cap);
624
625 let mut hydrating = true;
626
627 let mut batcher = A::new_batcher();
633 let mut chunker: UpsertChunker<T, FromTime::Order> = Default::default();
636 let mut push_buffer: Vec<UpsertUpdate<T, FromTime::Order>> = Vec::new();
639
640 let mut stash_cap: Option<Capability<T>> = None;
644 let mut input_upper = Antichain::from_elem(Timestamp::minimum());
645
646 let snapshot_start = std::time::Instant::now();
647 let mut prev_persist_upper = Antichain::from_elem(Timestamp::minimum());
648
649 let mut rehydration_total: u64 = 0;
651 let mut rehydration_updates: u64 = 0;
652
653 loop {
659 tokio::select! {
661 _ = input.ready() => {}
662 _ = persist_wakeup.ready() => {
663 while persist_wakeup.next_sync().is_some() {}
664 }
665 }
666
667 while let Some(event) = input.next_sync() {
672 match event {
673 AsyncEvent::Data(cap, data) => {
674 let mut pushed_any = false;
675 for ((key, value, from_time), ts, diff) in data {
676 assert!(diff.is_positive(), "invalid upsert input");
677 if PartialOrder::less_equal(&input_upper, &resume_upper)
678 && !resume_upper.less_equal(&ts)
679 {
680 continue;
681 }
682 let value = value.as_ref().map(upsert_value_to_row);
683 let from_time = from_time.upsert_order();
684 push_buffer.push((key, ts, UpsertDiff { from_time, value }));
685 pushed_any = true;
686 }
687 if pushed_any {
690 stash_cap = Some(match stash_cap {
691 Some(prev) if cap.time() < prev.time() => cap,
692 Some(prev) => prev,
693 None => cap,
694 });
695 }
696 }
697 AsyncEvent::Progress(upper) => {
698 if PartialOrder::less_than(&upper, &resume_upper) {
699 continue;
700 }
701 input_upper = upper;
702 }
703 }
704 }
705
706 A::flush(&mut push_buffer, &mut chunker, &mut batcher);
710
711 let persist_upper = persist_probe.with_frontier(|f| f.to_owned());
720
721 if persist_upper != prev_persist_upper {
722 let last_rehydration_chunk =
723 hydrating && PartialOrder::less_equal(&resume_upper, &persist_upper);
724
725 if last_rehydration_chunk {
726 hydrating = false;
727 upsert_metrics
728 .rehydration_latency
729 .set(snapshot_start.elapsed().as_secs_f64());
730 upsert_metrics.rehydration_total.set(rehydration_total);
731 upsert_metrics.rehydration_updates.set(rehydration_updates);
732 tracing::info!(
733 worker_id = %source_config.worker_id,
734 source_id = %source_config.id,
735 "upsert finished rehydration",
736 );
737 snapshot_cap.downgrade(&[]);
738 }
739
740 let _ = snapshot_cap.try_downgrade(persist_upper.iter());
741
742 persist_trace.set_logical_compaction(persist_upper.borrow());
744 persist_trace.set_physical_compaction(persist_upper.borrow());
745
746 prev_persist_upper = persist_upper.clone();
747 }
748
749 if let Some(cap) = stash_cap.as_mut()
786 && !persist_upper.less_than(cap.time())
787 && PartialOrder::less_than(&persist_upper, &input_upper)
788 {
789 let (sealed, _description) = batcher.seal(input_upper.clone());
793 let remaining_frontier = batcher.frontier().to_owned();
795
796 let mut ineligible = Vec::new();
797 let drain_stats = A::drain(
801 sealed,
802 &mut ineligible,
803 &output_handle,
804 &*cap,
805 &persist_upper,
806 &mut persist_trace,
807 source_config.worker_id,
808 source_config.id,
809 )
810 .await;
811
812 upsert_metrics.multi_get_size.inc_by(drain_stats.eligible);
813 upsert_metrics
814 .multi_get_result_count
815 .inc_by(drain_stats.result_count);
816 upsert_metrics
817 .multi_put_size
818 .inc_by(drain_stats.output_count);
819 upsert_metrics.upsert_inserts.inc_by(drain_stats.inserts);
820 upsert_metrics.upsert_updates.inc_by(drain_stats.updates);
821 upsert_metrics.upsert_deletes.inc_by(drain_stats.deletes);
822
823 if hydrating {
824 rehydration_total += drain_stats.inserts;
825 rehydration_updates += drain_stats.eligible;
826 }
827
828 let min_ineligible_ts = ineligible.iter().map(|(_, ts, _)| ts).min().cloned();
833 A::flush(&mut ineligible, &mut chunker, &mut batcher);
834
835 let min_ts = remaining_frontier
838 .elements()
839 .first()
840 .into_iter()
841 .chain(min_ineligible_ts.as_ref())
842 .min();
843 match min_ts {
844 Some(min_ts) => cap.downgrade(min_ts),
845 None => stash_cap = None,
848 }
849 }
850
851 if input_upper.is_empty() {
852 break;
853 }
854 }
855 });
856
857 (
858 output
859 .as_collection()
860 .map(|result: UpsertValue| match result {
861 Ok(ok) => Ok(ok),
862 Err(err) => Err(DataflowError::from(EnvelopeError::Upsert(*err))),
863 }),
864 health_stream,
865 snapshot_stream,
866 shutdown_button.press_on_drop(),
867 )
868}
869
870trait UpsertStashArm<T, O>
875where
876 T: Timestamp + Lattice + columnar::Columnar + Default,
877 for<'a> columnar::Ref<'a, T>: Copy + Ord,
878 O: columnar::Columnar + Default + Ord + Clone + Send + Sync + 'static,
879 for<'a> columnar::Ref<'a, O>: Ord + Copy,
880{
881 type Spine: TraceReader<Time = T> + 'static;
884 type Batcher: Batcher<Time = T> + 'static;
887
888 fn new_batcher() -> Self::Batcher;
890
891 fn push_chunk(batcher: &mut Self::Batcher, chunk: Column<UpsertUpdate<T, O>>);
894
895 fn flush(
900 updates: &mut Vec<UpsertUpdate<T, O>>,
901 chunker: &mut UpsertChunker<T, O>,
902 batcher: &mut Self::Batcher,
903 ) {
904 use timely::container::{ContainerBuilder as _, PushInto as _};
905 if updates.is_empty() {
906 return;
907 }
908 let mut raw: Column<UpsertUpdate<T, O>> = Default::default();
909 for update in updates.drain(..) {
910 raw.push_into(&update);
911 }
912 chunker.push_into(&mut raw);
913 while let Some(chunk) = chunker.extract() {
914 Self::push_chunk(batcher, std::mem::take(chunk));
915 }
916 }
917
918 async fn drain(
921 sealed: Vec<<Self::Batcher as Batcher>::Output>,
922 ineligible: &mut Vec<UpsertUpdate<T, O>>,
923 output_handle: &UpsertOutputHandle<T>,
924 output_cap: &Capability<T>,
925 persist_upper: &Antichain<T>,
926 trace: &mut TraceAgent<Self::Spine>,
927 worker_id: usize,
928 source_id: GlobalId,
929 ) -> DrainStats;
930}
931
932struct ChunkedArm;
944
945impl<T, O> UpsertStashArm<T, O> for ChunkedArm
946where
947 T: Timestamp + TotalOrder + Lattice + Sync,
948 T: columnation::Columnation + columnar::Columnar + Default,
949 for<'a> columnar::Ref<'a, T>: Copy + Ord,
950 O: columnar::Columnar + Default + Ord + Clone + Send + Sync + 'static,
951 for<'a> columnar::Ref<'a, O>: Ord + Copy,
952{
953 type Spine = FeedbackSpine<T>;
954 type Batcher = UpsertChunkBatcher<T, O>;
955
956 fn new_batcher() -> Self::Batcher {
957 Batcher::new(None, 0)
958 }
959
960 fn push_chunk(batcher: &mut Self::Batcher, chunk: Column<UpsertUpdate<T, O>>) {
961 batcher.push_into(ColumnChunk::from_column(chunk));
962 }
963
964 async fn drain(
965 sealed: Vec<UpsertChunk<T, O>>,
966 ineligible: &mut Vec<UpsertUpdate<T, O>>,
967 output_handle: &UpsertOutputHandle<T>,
968 output_cap: &Capability<T>,
969 persist_upper: &Antichain<T>,
970 trace: &mut TraceAgent<Self::Spine>,
971 worker_id: usize,
972 source_id: GlobalId,
973 ) -> DrainStats {
974 drain_sealed_input_chunked(
975 sealed.into_iter().map(ColumnChunk::into_column),
976 ineligible,
977 output_handle,
978 output_cap,
979 persist_upper,
980 trace,
981 worker_id,
982 source_id,
983 )
984 .await
985 }
986}
987
988struct PagedArm;
992
993impl<T, O> UpsertStashArm<T, O> for PagedArm
994where
995 T: Timestamp + TotalOrder + Lattice + Sync,
996 T: columnation::Columnation + columnar::Columnar + Default,
997 for<'a> columnar::Ref<'a, T>: Copy + Ord,
998 O: columnar::Columnar + Default + Ord + Clone + Send + Sync + 'static,
999 for<'a> columnar::Ref<'a, O>: Ord + Copy,
1000{
1001 type Spine = ValRowSpine<UpsertKey, T, Diff>;
1002 type Batcher = UpsertPagedBatcher<T, O>;
1003
1004 fn new_batcher() -> Self::Batcher {
1005 let mut batcher: UpsertPagedBatcher<T, O> = Batcher::new(None, 0);
1006 batcher.set_pager(crate::upsert::upsert_stash_pager::pager());
1007 batcher
1008 }
1009
1010 fn push_chunk(batcher: &mut Self::Batcher, chunk: Column<UpsertUpdate<T, O>>) {
1011 batcher.push_into(chunk);
1012 }
1013
1014 async fn drain(
1015 sealed: Vec<Column<UpsertUpdate<T, O>>>,
1016 ineligible: &mut Vec<UpsertUpdate<T, O>>,
1017 output_handle: &UpsertOutputHandle<T>,
1018 output_cap: &Capability<T>,
1019 persist_upper: &Antichain<T>,
1020 trace: &mut TraceAgent<Self::Spine>,
1021 worker_id: usize,
1022 source_id: GlobalId,
1023 ) -> DrainStats {
1024 drain_sealed_input_paged(
1025 sealed,
1026 ineligible,
1027 output_handle,
1028 output_cap,
1029 persist_upper,
1030 trace,
1031 worker_id,
1032 source_id,
1033 )
1034 .await
1035 }
1036}
1037
1038enum TimeClass {
1040 AlreadyPersisted,
1042 Eligible,
1044 Ineligible,
1046}
1047
1048fn classify_time<T: PartialOrder>(persist_upper: &Antichain<T>, ts: &T) -> TimeClass {
1055 if !persist_upper.less_equal(ts) {
1056 TimeClass::AlreadyPersisted
1057 } else if persist_upper.less_than(ts) {
1058 TimeClass::Ineligible
1059 } else {
1060 TimeClass::Eligible
1061 }
1062}
1063
1064struct DrainStats {
1067 eligible: u64,
1069 result_count: u64,
1071 inserts: u64,
1073 updates: u64,
1075 deletes: u64,
1077 output_count: u64,
1079}
1080
1081async fn drain_sealed_input_chunked<T, O>(
1107 sealed: impl Iterator<Item = Column<UpsertUpdate<T, O>>>,
1108 ineligible: &mut Vec<UpsertUpdate<T, O>>,
1109 output_handle: &UpsertOutputHandle<T>,
1110 output_cap: &Capability<T>,
1111 persist_upper: &Antichain<T>,
1112 trace: &mut TraceAgent<FeedbackSpine<T>>,
1113 worker_id: usize,
1114 source_id: GlobalId,
1115) -> DrainStats
1116where
1117 T: Timestamp + TotalOrder + Lattice + Sync,
1118 T: columnation::Columnation + columnar::Columnar + Default,
1119 for<'a> columnar::Ref<'a, T>: Copy + Ord,
1120 O: columnar::Columnar,
1121{
1122 let mut eligible_count: u64 = 0;
1123 let mut result_count: u64 = 0;
1124 let mut output_count: u64 = 0;
1125 let mut inserts: u64 = 0;
1126 let mut updates: u64 = 0;
1127 let mut deletes: u64 = 0;
1128
1129 let batches = trace
1133 .batches_through(Antichain::new().borrow())
1134 .expect("complete batch set for the feedback trace; is it closed?");
1135
1136 const PROBE_WINDOW: usize = 1024;
1141
1142 for chunk in sealed {
1143 use columnar::{Index, Len};
1144 let view = chunk.borrow();
1145 let total = view.len();
1146 let mut start = 0;
1147 while start < total {
1148 let mut probe_col = <UpsertKey as columnar::Columnar>::Container::default();
1154 let mut probe_count = 0usize;
1155 let mut end = total;
1156 {
1157 use columnar::Push;
1158 let mut last_probe: Option<&UpsertKey> = None;
1159 for index in start..total {
1160 let (key, ts, _diff) = view.get(index);
1161 let ts = <T as columnar::Columnar>::into_owned(ts);
1162 if matches!(classify_time(persist_upper, &ts), TimeClass::Eligible) {
1163 if last_probe != Some(key) {
1164 if probe_count == PROBE_WINDOW {
1165 end = index;
1166 break;
1167 }
1168 probe_col.push(key);
1169 probe_count += 1;
1170 last_probe = Some(key);
1171 }
1172 }
1173 }
1174 }
1175
1176 let mut old_values: std::collections::BTreeMap<UpsertKey, UpsertValue> =
1182 std::collections::BTreeMap::new();
1183 if probe_count > 0 {
1184 use columnar::Borrow;
1185 let mut staging = <FeedbackUpdate<T> as columnar::Columnar>::Container::default();
1186 for batch in &batches {
1187 batch.extract_into(probe_col.borrow(), &mut staging);
1188 }
1189 let staged = staging.borrow();
1190 let mut hits: Vec<_> = (0..staged.len())
1191 .map(|i| {
1192 let ((key, val), _time, diff) = staged.get(i);
1193 (key, val, <Diff as columnar::Columnar>::into_owned(diff))
1194 })
1195 .collect();
1196 hits.sort_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
1197 let mut i = 0;
1198 while i < hits.len() {
1199 let (key, val, _) = hits[i];
1200 let mut count = Diff::ZERO;
1201 let mut j = i;
1202 while j < hits.len() && hits[j].0 == key && hits[j].1 == val {
1203 count += hits[j].2;
1204 j += 1;
1205 }
1206 if count.is_positive() {
1207 assert!(
1208 count == 1.into(),
1209 "unexpected multiple entries for the same key in persist trace"
1210 );
1211 let prev = old_values.insert(*key, decode_upsert_value(val.iter()));
1212 assert!(
1213 prev.is_none(),
1214 "unexpected multiple values for the same key in persist trace"
1215 );
1216 }
1217 i = j;
1218 }
1219 }
1220
1221 for index in start..end {
1223 let (key, ts, diff) = view.get(index);
1224 let ts = <T as columnar::Columnar>::into_owned(ts);
1225 match classify_time(persist_upper, &ts) {
1226 TimeClass::AlreadyPersisted => continue,
1227 TimeClass::Ineligible => {
1228 ineligible.push((
1230 *key,
1231 ts,
1232 <UpsertDiff<O> as columnar::Columnar>::into_owned(diff),
1233 ));
1234 continue;
1235 }
1236 TimeClass::Eligible => {}
1237 }
1238
1239 eligible_count += 1;
1243 let old_value = old_values.remove(key);
1244
1245 if old_value.is_some() {
1246 result_count += 1;
1247 }
1248
1249 match diff.value {
1250 Some(row) => {
1251 if let Some(old_val) = old_value {
1252 let size = upsert_value_byte_len(&old_val);
1253 output_handle
1254 .give_fueled(
1255 output_cap,
1256 (old_val, ts.clone(), Diff::MINUS_ONE),
1257 size,
1258 )
1259 .await;
1260 output_count += 1;
1261 updates += 1;
1262 } else {
1263 inserts += 1;
1264 }
1265 let new_val = decode_upsert_value(row.iter());
1266 let size = upsert_value_byte_len(&new_val);
1267 output_handle
1268 .give_fueled(output_cap, (new_val, ts, Diff::ONE), size)
1269 .await;
1270 output_count += 1;
1271 }
1272 None => {
1273 if let Some(old_val) = old_value {
1274 let size = upsert_value_byte_len(&old_val);
1275 output_handle
1276 .give_fueled(output_cap, (old_val, ts, Diff::MINUS_ONE), size)
1277 .await;
1278 output_count += 1;
1279 deletes += 1;
1280 }
1281 }
1282 }
1283 }
1284 start = end;
1285 }
1286 }
1287
1288 tracing::debug!(
1289 worker_id = %worker_id,
1290 source_id = %source_id,
1291 ineligible = ineligible.len(),
1292 eligible = eligible_count,
1293 "drained stash",
1294 );
1295
1296 DrainStats {
1297 eligible: eligible_count,
1298 result_count,
1299 inserts,
1300 updates,
1301 deletes,
1302 output_count,
1303 }
1304}
1305
1306async fn drain_sealed_input_paged<T, O>(
1317 sealed: Vec<Column<UpsertUpdate<T, O>>>,
1318 ineligible: &mut Vec<UpsertUpdate<T, O>>,
1319 output_handle: &UpsertOutputHandle<T>,
1320 output_cap: &Capability<T>,
1321 persist_upper: &Antichain<T>,
1322 trace: &mut TraceAgent<ValRowSpine<UpsertKey, T, Diff>>,
1323 worker_id: usize,
1324 source_id: GlobalId,
1325) -> DrainStats
1326where
1327 T: Timestamp + TotalOrder + Lattice + Sync,
1328 T: columnation::Columnation + columnar::Columnar,
1329 O: columnar::Columnar,
1330{
1331 use columnar::Index as _;
1332
1333 let mut eligible_count: u64 = 0;
1348 let mut result_count: u64 = 0;
1349 let mut output_count: u64 = 0;
1350 let mut inserts: u64 = 0;
1351 let mut updates: u64 = 0;
1352 let mut deletes: u64 = 0;
1353
1354 let (mut cursor, storage) = trace.cursor();
1355
1356 for chunk in &sealed {
1357 for (key, ts, diff) in chunk.borrow().into_index_iter() {
1358 let ts = <T as columnar::Columnar>::into_owned(ts);
1359 match classify_time(persist_upper, &ts) {
1360 TimeClass::AlreadyPersisted => continue,
1361 TimeClass::Ineligible => {
1362 ineligible.push((
1364 *key,
1365 ts,
1366 <UpsertDiff<O> as columnar::Columnar>::into_owned(diff),
1367 ));
1368 continue;
1369 }
1370 TimeClass::Eligible => {}
1371 }
1372
1373 eligible_count += 1;
1378 cursor.seek_key(&storage, key);
1379 let old_value = match cursor.get_key(&storage) {
1380 Some(found) if found == key => {
1381 let mut result = None;
1382 while let Some(val) = cursor.get_val(&storage) {
1383 let mut count = Diff::ZERO;
1384 cursor.map_times(&storage, |_time, d| {
1385 count += d.clone();
1386 });
1387 if count.is_positive() {
1388 assert!(
1389 count == 1.into(),
1390 "unexpected multiple entries for the same key in persist trace"
1391 );
1392 assert!(
1393 result.is_none(),
1394 "unexpected multiple values for the same key in persist trace"
1395 );
1396 result = Some(decode_upsert_value(val));
1397 }
1398 cursor.step_val(&storage);
1399 }
1400 result
1401 }
1402 _ => None,
1403 };
1404
1405 if old_value.is_some() {
1406 result_count += 1;
1407 }
1408
1409 match diff.value {
1410 Some(row) => {
1411 if let Some(old_val) = old_value {
1412 let size = upsert_value_byte_len(&old_val);
1413 output_handle
1414 .give_fueled(output_cap, (old_val, ts.clone(), Diff::MINUS_ONE), size)
1415 .await;
1416 output_count += 1;
1417 updates += 1;
1418 } else {
1419 inserts += 1;
1420 }
1421 let new_val = decode_upsert_value(row.iter());
1422 let size = upsert_value_byte_len(&new_val);
1423 output_handle
1424 .give_fueled(output_cap, (new_val, ts, Diff::ONE), size)
1425 .await;
1426 output_count += 1;
1427 }
1428 None => {
1429 if let Some(old_val) = old_value {
1430 let size = upsert_value_byte_len(&old_val);
1431 output_handle
1432 .give_fueled(output_cap, (old_val, ts, Diff::MINUS_ONE), size)
1433 .await;
1434 output_count += 1;
1435 deletes += 1;
1436 }
1437 }
1438 }
1439 }
1440 }
1441
1442 tracing::debug!(
1443 worker_id = %worker_id,
1444 source_id = %source_id,
1445 ineligible = ineligible.len(),
1446 eligible = eligible_count,
1447 "drained stash",
1448 );
1449
1450 DrainStats {
1451 eligible: eligible_count,
1452 result_count,
1453 inserts,
1454 updates,
1455 deletes,
1456 output_count,
1457 }
1458}
1459
1460#[cfg(test)]
1461mod test {
1462 use mz_ore::metrics::MetricsRegistry;
1463 use mz_persist_types::ShardId;
1464 use mz_repr::{Datum, Timestamp as MzTimestamp};
1465 use mz_storage_operators::persist_source::Subtime;
1466 use mz_storage_types::sources::SourceEnvelope;
1467 use mz_storage_types::sources::envelope::{KeyEnvelope, UpsertEnvelope, UpsertStyle};
1468 use timely::dataflow::operators::capture::Extract;
1469 use timely::dataflow::operators::{Capture, Input};
1470 use timely::progress::Timestamp;
1471
1472 use crate::metrics::StorageMetrics;
1473 use crate::metrics::upsert::UpsertMetricDefs;
1474 use crate::source::SourceExportCreationConfig;
1475 use crate::statistics::{SourceStatistics, SourceStatisticsMetricDefs};
1476
1477 use super::*;
1478
1479 impl UpsertSourceTime for i32 {
1482 type Order = i32;
1483 fn upsert_order(&self) -> i32 {
1484 *self
1485 }
1486 }
1487
1488 type Ts = (MzTimestamp, Subtime);
1489
1490 fn new_ts(ts: u64) -> Ts {
1491 (MzTimestamp::new(ts), Subtime::minimum())
1492 }
1493
1494 fn key(k: i64) -> UpsertKey {
1495 UpsertKey::from_key(Ok(&Row::pack_slice(&[Datum::Int64(k)])))
1496 }
1497
1498 fn row(k: i64, v: i64) -> Row {
1499 Row::pack_slice(&[Datum::Int64(k), Datum::Int64(v)])
1500 }
1501
1502 macro_rules! upsert_test {
1507 (|$input:ident, $persist:ident, $worker:ident| $body:block) => {{
1508 let run = |flavor: UpsertStashFlavor| {
1509 let output_handle = timely::execute_directly(move |$worker| {
1510 let (mut $input, mut $persist, output_handle) = $worker
1511 .dataflow::<MzTimestamp, _, _>(|scope| {
1512 scope.scoped::<Ts, _, _>("upsert", |scope| {
1513 let (input_handle, input) = scope.new_input();
1514 let (persist_handle, persist_input) = scope.new_input();
1515 let source_id = GlobalId::User(0);
1516
1517 let reg = MetricsRegistry::new();
1518 let upsert_defs = UpsertMetricDefs::register_with(®);
1519 let upsert_metrics =
1520 UpsertMetrics::new(&upsert_defs, source_id, 0, None);
1521
1522 let reg2 = MetricsRegistry::new();
1523 let storage_metrics = StorageMetrics::register_with(®2);
1524
1525 let reg3 = MetricsRegistry::new();
1526 let stats_defs =
1527 SourceStatisticsMetricDefs::register_with(®3);
1528 let envelope = SourceEnvelope::Upsert(UpsertEnvelope {
1529 source_arity: 2,
1530 style: UpsertStyle::Default(KeyEnvelope::Flattened),
1531 key_indices: vec![0],
1532 });
1533 let source_statistics = SourceStatistics::new(
1534 source_id, 0, &stats_defs, source_id, &ShardId::new(),
1535 envelope, Antichain::from_elem(Timestamp::minimum()),
1536 );
1537 let source_config = SourceExportCreationConfig {
1538 id: source_id,
1539 worker_id: 0,
1540 metrics: storage_metrics,
1541 source_statistics,
1542 };
1543
1544 let (output, _, _, button) = upsert_inner(
1545 flavor,
1546 input.as_collection(),
1547 vec![0],
1548 Antichain::from_elem(Timestamp::minimum()),
1549 persist_input.as_collection(),
1550 None,
1551 upsert_metrics,
1552 source_config,
1553 );
1554 std::mem::forget(button);
1555 (input_handle, persist_handle, output.inner.capture())
1556 })
1557 });
1558
1559 $body
1560
1561 output_handle
1562 });
1563
1564 let mut actual: Vec<_> = output_handle
1565 .extract()
1566 .into_iter()
1567 .flat_map(|(_cap, container)| container)
1568 .collect();
1569 differential_dataflow::consolidation::consolidate_updates(&mut actual);
1570 actual
1571 };
1572
1573 let paged = run(UpsertStashFlavor::Paged);
1574 let chunked = run(UpsertStashFlavor::Chunked);
1575 assert_eq!(paged, chunked, "stash flavors must produce equal output");
1576 chunked
1577 }};
1578 }
1579
1580 #[mz_ore::test]
1581 #[cfg_attr(miri, ignore)]
1582 fn gh_9160_repro() {
1583 let actual = upsert_test!(|input, persist, worker| {
1584 let key0 = key(0);
1585 let key1 = key(1);
1586 let value1 = row(0, 0);
1587 let value3 = row(0, 1);
1588 let value4 = row(0, 2);
1589
1590 input.send(((key0, Some(Ok(value1.clone())), 1), new_ts(0), Diff::ONE));
1591 input.advance_to(new_ts(2));
1592 worker.step();
1593
1594 persist.send((Ok(value1), new_ts(0), Diff::ONE));
1595 persist.advance_to(new_ts(1));
1596 worker.step();
1597
1598 input.send_batch(&mut vec![
1599 ((key1, None, 2), new_ts(2), Diff::ONE),
1600 ((key0, Some(Ok(value3)), 3), new_ts(3), Diff::ONE),
1601 ]);
1602 input.advance_to(new_ts(3));
1603 input.send_batch(&mut vec![(
1604 (key0, Some(Ok(value4)), 4),
1605 new_ts(3),
1606 Diff::ONE,
1607 )]);
1608 input.advance_to(new_ts(4));
1609 worker.step();
1610
1611 persist.advance_to(new_ts(3));
1612 worker.step();
1613 });
1614
1615 let value1 = row(0, 0);
1616 let value4 = row(0, 2);
1617 let expected: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1618 (Ok(value1.clone()), new_ts(0), Diff::ONE),
1619 (Ok(value1), new_ts(3), Diff::MINUS_ONE),
1620 (Ok(value4), new_ts(3), Diff::ONE),
1621 ];
1622 assert_eq!(actual, expected);
1623 }
1624
1625 #[mz_ore::test]
1626 #[cfg_attr(miri, ignore)]
1627 fn out_of_order_keys_across_timestamps() {
1628 let actual = upsert_test!(|input, persist, worker| {
1629 let key_high = key(99);
1630 let key_low = key(1);
1631 let val_a = row(99, 1);
1632 let val_b = row(1, 2);
1633
1634 input.send(((key_high, Some(Ok(val_a.clone())), 1), new_ts(0), Diff::ONE));
1635 input.advance_to(new_ts(1));
1636 worker.step();
1637 persist.send((Ok(val_a.clone()), new_ts(0), Diff::ONE));
1638 persist.advance_to(new_ts(1));
1639 worker.step();
1640
1641 input.send(((key_low, Some(Ok(val_b.clone())), 2), new_ts(1), Diff::ONE));
1642 input.advance_to(new_ts(2));
1643 worker.step();
1644 persist.send((Ok(val_b.clone()), new_ts(1), Diff::ONE));
1645 persist.advance_to(new_ts(2));
1646 worker.step();
1647
1648 let val_a2 = row(99, 10);
1649 let val_b2 = row(1, 20);
1650 input.send_batch(&mut vec![
1651 (
1652 (key_high, Some(Ok(val_a2.clone())), 3),
1653 new_ts(2),
1654 Diff::ONE,
1655 ),
1656 ((key_low, Some(Ok(val_b2.clone())), 4), new_ts(2), Diff::ONE),
1657 ]);
1658 input.advance_to(new_ts(3));
1659 worker.step();
1660 persist.advance_to(new_ts(3));
1661 worker.step();
1662 });
1663
1664 let val_a = row(99, 1);
1665 let val_b = row(1, 2);
1666 let val_a2 = row(99, 10);
1667 let val_b2 = row(1, 20);
1668 let expected: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1669 (Ok(val_b.clone()), new_ts(1), Diff::ONE),
1670 (Ok(val_b), new_ts(2), Diff::MINUS_ONE),
1671 (Ok(val_b2), new_ts(2), Diff::ONE),
1672 (Ok(val_a.clone()), new_ts(0), Diff::ONE),
1673 (Ok(val_a), new_ts(2), Diff::MINUS_ONE),
1674 (Ok(val_a2), new_ts(2), Diff::ONE),
1675 ];
1676 let mut actual_sorted = actual;
1677 let mut expected_sorted = expected;
1678 actual_sorted.sort();
1679 expected_sorted.sort();
1680 assert_eq!(actual_sorted, expected_sorted);
1681 }
1682
1683 #[mz_ore::test]
1684 #[cfg_attr(miri, ignore)]
1685 fn rehydration_then_update() {
1686 let actual = upsert_test!(|input, persist, worker| {
1687 let k = key(42);
1688 let old_val = row(42, 100);
1689 let new_val = row(42, 200);
1690
1691 persist.send((Ok(old_val), new_ts(0), Diff::ONE));
1692 persist.advance_to(new_ts(1));
1693 worker.step();
1694
1695 input.send(((k, Some(Ok(new_val)), 1), new_ts(1), Diff::ONE));
1696 input.advance_to(new_ts(2));
1697 worker.step();
1698 persist.advance_to(new_ts(2));
1699 worker.step();
1700 });
1701
1702 let old_val = row(42, 100);
1703 let new_val = row(42, 200);
1704 let expected: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1705 (Ok(old_val), new_ts(1), Diff::MINUS_ONE),
1706 (Ok(new_val), new_ts(1), Diff::ONE),
1707 ];
1708 assert_eq!(actual, expected);
1709 }
1710
1711 #[mz_ore::test]
1712 #[cfg_attr(miri, ignore)]
1713 fn drain_crosses_probe_window() {
1714 const KEYS: i64 = 1500;
1719 let actual = upsert_test!(|input, persist, worker| {
1720 for k in 0..KEYS {
1721 persist.send((Ok(row(k, k)), new_ts(0), Diff::ONE));
1722 }
1723 persist.advance_to(new_ts(1));
1724 worker.step();
1725
1726 for k in 0..KEYS {
1727 input.send(((key(k), Some(Ok(row(k, k + 1))), 1), new_ts(1), Diff::ONE));
1728 }
1729 input.advance_to(new_ts(2));
1730 worker.step();
1731 persist.advance_to(new_ts(2));
1732 worker.step();
1733 });
1734
1735 let mut expected: Vec<(Result<Row, DataflowError>, _, _)> = Vec::new();
1736 for k in 0..KEYS {
1737 expected.push((Ok(row(k, k)), new_ts(1), Diff::MINUS_ONE));
1738 expected.push((Ok(row(k, k + 1)), new_ts(1), Diff::ONE));
1739 }
1740 let mut actual_sorted = actual;
1741 actual_sorted.sort();
1742 expected.sort();
1743 assert_eq!(actual_sorted, expected);
1744 }
1745
1746 #[mz_ore::test]
1755 #[cfg_attr(miri, ignore)]
1756 fn drain_reads_spilled_chunks() {
1757 use mz_ore::pool::Pool;
1758 use mz_timely_util::columnar::chunk::set_spill_override;
1759
1760 let pool = Pool::new().expect("pool creation");
1761 set_spill_override(Some(pool.clone()));
1762
1763 const KEYS: i64 = 1500;
1764 let actual = upsert_test!(|input, persist, worker| {
1765 for k in 0..KEYS {
1766 persist.send((Ok(row(k, k)), new_ts(0), Diff::ONE));
1767 }
1768 persist.advance_to(new_ts(1));
1769 worker.step();
1770
1771 for k in 0..KEYS {
1772 input.send(((key(k), Some(Ok(row(k, k + 1))), 1), new_ts(1), Diff::ONE));
1773 }
1774 input.advance_to(new_ts(2));
1775 worker.step();
1776 persist.advance_to(new_ts(2));
1777 worker.step();
1778 });
1779
1780 set_spill_override(None);
1781 assert!(
1782 pool.stats().inserts > 0,
1783 "chunks should have spilled through the pool"
1784 );
1785
1786 let mut expected: Vec<(Result<Row, DataflowError>, _, _)> = Vec::new();
1787 for k in 0..KEYS {
1788 expected.push((Ok(row(k, k)), new_ts(1), Diff::MINUS_ONE));
1789 expected.push((Ok(row(k, k + 1)), new_ts(1), Diff::ONE));
1790 }
1791 let mut actual_sorted = actual;
1792 actual_sorted.sort();
1793 expected.sort();
1794 assert_eq!(actual_sorted, expected);
1795 }
1796
1797 #[mz_ore::test]
1798 #[cfg_attr(miri, ignore)]
1799 fn delete_existing_key() {
1800 let actual = upsert_test!(|input, persist, worker| {
1801 let k = key(7);
1802 let val = row(7, 77);
1803
1804 input.send(((k, Some(Ok(val.clone())), 1), new_ts(0), Diff::ONE));
1805 input.advance_to(new_ts(1));
1806 worker.step();
1807 persist.send((Ok(val), new_ts(0), Diff::ONE));
1808 persist.advance_to(new_ts(1));
1809 worker.step();
1810
1811 input.send(((k, None, 2), new_ts(1), Diff::ONE));
1812 input.advance_to(new_ts(2));
1813 worker.step();
1814 persist.advance_to(new_ts(2));
1815 worker.step();
1816 });
1817
1818 let val = row(7, 77);
1819 let expected: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1820 (Ok(val.clone()), new_ts(0), Diff::ONE),
1821 (Ok(val), new_ts(1), Diff::MINUS_ONE),
1822 ];
1823 assert_eq!(actual, expected);
1824 }
1825
1826 #[mz_ore::test]
1827 #[cfg_attr(miri, ignore)]
1828 fn multi_batch_rehydration() {
1829 let actual = upsert_test!(|input, persist, worker| {
1830 let k = key(5);
1831 let old_val = row(5, 10);
1832 let new_val = row(5, 20);
1833 let updated_val = row(5, 30);
1834
1835 persist.send((Ok(old_val.clone()), new_ts(0), Diff::ONE));
1836 persist.send((Ok(old_val), new_ts(0), Diff::MINUS_ONE));
1837 persist.send((Ok(new_val), new_ts(0), Diff::ONE));
1838 persist.advance_to(new_ts(1));
1839 worker.step();
1840
1841 input.send(((k, Some(Ok(updated_val)), 1), new_ts(1), Diff::ONE));
1842 input.advance_to(new_ts(2));
1843 worker.step();
1844 persist.advance_to(new_ts(2));
1845 worker.step();
1846 });
1847
1848 let new_val = row(5, 20);
1849 let updated_val = row(5, 30);
1850 let expected: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1851 (Ok(new_val), new_ts(1), Diff::MINUS_ONE),
1852 (Ok(updated_val), new_ts(1), Diff::ONE),
1853 ];
1854 assert_eq!(actual, expected);
1855 }
1856
1857 #[mz_ore::test]
1858 #[cfg_attr(miri, ignore)]
1859 fn delete_nonexistent_key() {
1860 let actual = upsert_test!(|input, persist, worker| {
1861 let k = key(99);
1862
1863 persist.advance_to(new_ts(1));
1864 worker.step();
1865
1866 input.send(((k, None, 1), new_ts(1), Diff::ONE));
1867 input.advance_to(new_ts(2));
1868 worker.step();
1869 persist.advance_to(new_ts(2));
1870 worker.step();
1871 });
1872
1873 assert!(actual.is_empty(), "expected empty output, got: {actual:?}");
1874 }
1875
1876 #[mz_ore::test]
1877 #[cfg_attr(miri, ignore)]
1878 fn reinsert_after_delete() {
1879 let actual = upsert_test!(|input, persist, worker| {
1880 let k = key(3);
1881 let val_a = row(3, 10);
1882 let val_b = row(3, 20);
1883
1884 input.send(((k, Some(Ok(val_a.clone())), 1), new_ts(0), Diff::ONE));
1885 input.advance_to(new_ts(1));
1886 worker.step();
1887 persist.send((Ok(val_a.clone()), new_ts(0), Diff::ONE));
1888 persist.advance_to(new_ts(1));
1889 worker.step();
1890
1891 input.send(((k, None, 2), new_ts(1), Diff::ONE));
1892 input.advance_to(new_ts(2));
1893 worker.step();
1894 persist.send((Ok(val_a), new_ts(1), Diff::MINUS_ONE));
1895 persist.advance_to(new_ts(2));
1896 worker.step();
1897
1898 input.send(((k, Some(Ok(val_b.clone())), 3), new_ts(2), Diff::ONE));
1899 input.advance_to(new_ts(3));
1900 worker.step();
1901 persist.advance_to(new_ts(3));
1902 worker.step();
1903 });
1904
1905 let val_a = row(3, 10);
1906 let val_b = row(3, 20);
1907 let mut expected: Vec<(Result<Row, DataflowError>, _, _)> = vec![
1908 (Ok(val_a.clone()), new_ts(0), Diff::ONE),
1909 (Ok(val_a), new_ts(1), Diff::MINUS_ONE),
1910 (Ok(val_b), new_ts(2), Diff::ONE),
1911 ];
1912 expected.sort();
1913 let mut actual = actual;
1914 actual.sort();
1915 assert_eq!(actual, expected);
1916 }
1917
1918 #[mz_ore::test]
1919 #[cfg_attr(miri, ignore)]
1920 fn idempotent_update() {
1921 let actual = upsert_test!(|input, persist, worker| {
1922 let k = key(11);
1923 let val = row(11, 50);
1924
1925 input.send(((k, Some(Ok(val.clone())), 1), new_ts(0), Diff::ONE));
1926 input.advance_to(new_ts(1));
1927 worker.step();
1928 persist.send((Ok(val.clone()), new_ts(0), Diff::ONE));
1929 persist.advance_to(new_ts(1));
1930 worker.step();
1931
1932 input.send(((k, Some(Ok(val.clone())), 2), new_ts(1), Diff::ONE));
1933 input.advance_to(new_ts(2));
1934 worker.step();
1935 persist.advance_to(new_ts(2));
1936 worker.step();
1937 });
1938
1939 let val = row(11, 50);
1940 let expected: Vec<(Result<Row, DataflowError>, _, _)> =
1941 vec![(Ok(val), new_ts(0), Diff::ONE)];
1942 assert_eq!(actual, expected);
1943 }
1944
1945 #[mz_ore::test]
1962 #[cfg_attr(miri, ignore)]
1963 fn lagging_replacement_below_upper_strands_data() {
1964 for flavor in [UpsertStashFlavor::Paged, UpsertStashFlavor::Chunked] {
1965 let (frontier, emitted) = run_below_upper_scenario_v2(flavor);
1966
1967 assert!(
1971 emitted.is_empty(),
1972 "below-upper data should be dropped, not emitted; got {emitted:?} ({flavor:?})"
1973 );
1974 assert_eq!(
1975 frontier,
1976 vec![new_ts(11)],
1977 "v2 output frontier should advance to the input upper, not pin below \
1978 persist_upper ({flavor:?})"
1979 );
1980 assert!(
1981 frontier[0] >= new_ts(10),
1982 "v2 output frontier {frontier:?} should reach at least persist_upper (10) \
1983 ({flavor:?})"
1984 );
1985 }
1986 }
1987
1988 fn run_below_upper_scenario_v2(
1991 flavor: UpsertStashFlavor,
1992 ) -> (Vec<Ts>, Vec<(Result<Row, DataflowError>, Ts, Diff)>) {
1993 use timely::dataflow::operators::Probe;
1994
1995 let (frontier, capture) = timely::execute_directly(move |worker| {
1996 let (mut input, mut persist, probe, capture) =
1997 worker.dataflow::<MzTimestamp, _, _>(|scope| {
1998 scope.scoped::<Ts, _, _>("upsert", |scope| {
1999 let (input_handle, input) = scope.new_input();
2000 let (persist_handle, persist_input) = scope.new_input();
2001 let source_id = GlobalId::User(0);
2002
2003 let reg = MetricsRegistry::new();
2004 let upsert_defs = UpsertMetricDefs::register_with(®);
2005 let upsert_metrics = UpsertMetrics::new(&upsert_defs, source_id, 0, None);
2006
2007 let reg2 = MetricsRegistry::new();
2008 let storage_metrics = StorageMetrics::register_with(®2);
2009
2010 let reg3 = MetricsRegistry::new();
2011 let stats_defs = SourceStatisticsMetricDefs::register_with(®3);
2012 let envelope = SourceEnvelope::Upsert(UpsertEnvelope {
2013 source_arity: 2,
2014 style: UpsertStyle::Default(KeyEnvelope::Flattened),
2015 key_indices: vec![0],
2016 });
2017 let source_statistics = SourceStatistics::new(
2018 source_id,
2019 0,
2020 &stats_defs,
2021 source_id,
2022 &ShardId::new(),
2023 envelope,
2024 Antichain::from_elem(Timestamp::minimum()),
2025 );
2026 let source_config = SourceExportCreationConfig {
2027 id: source_id,
2028 worker_id: 0,
2029 metrics: storage_metrics,
2030 source_statistics,
2031 };
2032
2033 let (output, _, _, button) = upsert_inner(
2034 flavor,
2035 input.as_collection(),
2036 vec![0],
2037 Antichain::from_elem(Timestamp::minimum()),
2038 persist_input.as_collection(),
2039 None,
2040 upsert_metrics,
2041 source_config,
2042 );
2043 std::mem::forget(button);
2044 let (probe, stream) = output.inner.probe();
2045 (input_handle, persist_handle, probe, stream.capture())
2046 })
2047 });
2048
2049 persist.advance_to(new_ts(10));
2052 for _ in 0..20 {
2053 worker.step();
2054 }
2055
2056 input.send(((key(0), Some(Ok(row(0, 1))), 1), new_ts(5), Diff::ONE));
2059 input.send(((key(1), Some(Ok(row(1, 2))), 2), new_ts(7), Diff::ONE));
2060 input.advance_to(new_ts(11));
2061 for _ in 0..20 {
2062 worker.step();
2063 }
2064
2065 (probe.with_frontier(|f| f.to_vec()), capture)
2066 });
2067
2068 let mut emitted: Vec<_> = capture
2069 .extract()
2070 .into_iter()
2071 .flat_map(|(_cap, c)| c)
2072 .collect();
2073 differential_dataflow::consolidation::consolidate_updates(&mut emitted);
2074 (frontier, emitted)
2075 }
2076}