mz_storage/upsert/
types.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//! # State-management for UPSERT.
11//!
12//! This module and provide structures for use within an UPSERT
13//! operator implementation.
14//!
15//! UPSERT is a effectively a process which transforms a `Stream<(Key, Option<Data>)>`
16//! into a differential collection, by indexing the data based on the key.
17//!
18//! _This module does not implement this transformation, instead exposing APIs designed
19//! for use within an UPSERT operator. There is one exception to this: `consolidate_chunk`
20//! implements an efficient upsert-like transformation to re-index a collection using the
21//! _output collection_ of an upsert transformation. More on this below.
22//!
23//! ## `UpsertState`
24//!
25//! Its primary export is `UpsertState`, which wraps an `UpsertStateBackend` and provides 3 APIs:
26//!
27//! ### `multi_get`
28//! `multi_get` returns the current value for a (unique) set of keys. To keep implementations
29//! efficient, the set of keys is an iterator, and results are written back into another parallel
30//! iterator. In addition to returning the current values, implementations must also return the
31//! _size_ of those values _as they are stored within the implementation_. Implementations are
32//! required to chunk large iterators if they need to operate over smaller batches.
33//!
34//! `multi_get` is implemented directly with `UpsertStateBackend::multi_get`.
35//!
36//! ### `multi_put`
37//! Update or delete values for a set of keys. To keep implementations efficient, the set
38//! of updates is an iterator. Implementations are also required to return the difference
39//! in values and total size after processing the updates. To simplify this (and because
40//! in the `upsert` usecase we have this data readily available), the updates are input
41//! with the size of the current value (if any) that was returned from a previous `multi_get`.
42//! Implementations are required to chunk large iterators if they need to operate over smaller
43//! batches.
44//!
45//! `multi_put` is implemented directly with `UpsertStateBackend::multi_put`.
46//!
47//! ### `consolidate_chunk`
48//!
49//! `consolidate_chunk` re-indexes an UPSERT collection based on its _output collection_ (as
50//! opposed to its _input `Stream`_. Please see the docs on `consolidate_chunk` and `StateValue`
51//! for more information.
52//!
53//! `consolidate_chunk` is implemented with both `UpsertStateBackend::multi_put` and
54//! `UpsertStateBackend::multi_get`
55//!
56//! ## Order Keys
57//!
58//! In practice, the input stream for UPSERT collections includes an _order key_. This is used to
59//! sort data with the same key occurring in the same timestamp. This module provides support
60//! for serializing and deserializing order keys with their associated data. Being able to ingest
61//! data on non-frontier boundaries requires this support.
62//!
63//! A consequence of this is that tombstones with an order key can be stored within the state.
64//! There is currently no support for cleaning these tombstones up, as they are considered rare and
65//! small enough.
66//!
67//! Because `consolidate_chunk` handles data that consolidates correctly, it does not handle
68//! order keys.
69//!
70//!
71//! ## A note on state size
72//!
73//! The `UpsertStateBackend` trait requires implementations report _relatively accurate_ information about
74//! how the state size changes over time. Note that it does NOT ask the implementations to give
75//! accurate information about actual resource consumption (like disk space including space
76//! amplification), and instead is just asking about the size of the values, after they have been
77//! encoded. For implementations like `RocksDB`, these may be highly accurate (it literally
78//! reports the encoded size as written to the RocksDB API, and for others like the
79//! `InMemoryHashMap`, they may be rough estimates of actual memory usage. See
80//! `StateValue::memory_size` for more information.
81//!
82//! Note also that after consolidation, additional space may be used if `StateValue` is
83//! used.
84//!
85
86use std::fmt;
87use std::num::Wrapping;
88use std::sync::Arc;
89use std::time::Instant;
90
91use bincode::Options;
92use itertools::Itertools;
93use mz_ore::error::ErrorExt;
94use mz_repr::{Diff, GlobalId};
95use serde::{Serialize, de::DeserializeOwned};
96
97use crate::metrics::upsert::{UpsertMetrics, UpsertSharedMetrics};
98use crate::statistics::SourceStatistics;
99use crate::upsert::{UpsertKey, UpsertValue};
100
101/// The default set of `bincode` options used for consolidating
102/// upsert updates (and writing values to RocksDB).
103pub type BincodeOpts = bincode::config::DefaultOptions;
104
105/// Build the default `BincodeOpts`.
106pub fn upsert_bincode_opts() -> BincodeOpts {
107    // We don't allow trailing bytes, for now,
108    // and use varint encoding for space saving.
109    bincode::DefaultOptions::new()
110}
111
112/// The result type for `multi_get`.
113/// The value and size are stored in individual `Option`s so callees
114/// can reuse this value as they overwrite this value, keeping
115/// track of the previous metadata. Additionally, values
116/// may be `None` for tombstones.
117#[derive(Clone)]
118pub struct UpsertValueAndSize<T, O> {
119    /// The value, if there was one.
120    pub value: Option<StateValue<T, O>>,
121    /// The size of original`value` as persisted,
122    /// Useful for users keeping track of statistics.
123    pub metadata: Option<ValueMetadata<u64>>,
124}
125
126impl<T, O> std::fmt::Debug for UpsertValueAndSize<T, O> {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        f.debug_struct("UpsertValueAndSize")
129            .field("value", &self.value)
130            .field("metadata", &self.metadata)
131            .finish()
132    }
133}
134
135impl<T, O> Default for UpsertValueAndSize<T, O> {
136    fn default() -> Self {
137        Self {
138            value: None,
139            metadata: None,
140        }
141    }
142}
143
144/// Metadata about an existing value in the upsert state backend, as returned
145/// by `multi_get`.
146#[derive(Copy, Clone, Debug)]
147pub struct ValueMetadata<S> {
148    /// The size of the value.
149    pub size: S,
150    /// If the value is a tombstone.
151    pub is_tombstone: bool,
152}
153
154/// A value to put in with `multi_put`.
155#[derive(Clone, Debug)]
156pub struct PutValue<V> {
157    /// The new value, or a `None` to indicate a delete.
158    pub value: Option<V>,
159    /// The value of the previous value for this key, if known.
160    /// Passed into efficiently calculate statistics.
161    pub previous_value_metadata: Option<ValueMetadata<i64>>,
162}
163
164/// A value to put in with a `multi_merge`.
165pub struct MergeValue<V> {
166    /// The value of the merge operand to write to the backend.
167    pub value: V,
168    /// The 'diff' of this merge operand value, used to estimate the overall size diff
169    /// of the working set after this merge operand is merged by the backend.
170    pub diff: Diff,
171}
172
173/// `UpsertState` has 2 modes:
174/// - Normal operation
175/// - Consolidation.
176///
177/// This struct and its substructs are helpers to simplify the logic that
178/// individual `UpsertState` implementations need to do to manage these 2 modes.
179///
180/// Normal operation is simple, we just store an ordinary `UpsertValue`, and allow the implementer
181/// to store it any way they want. During consolidation, the logic is more complex.
182/// See the docs on `StateValue::merge_update` for more information.
183///
184/// Note also that this type is designed to support _partial updates_. All values are
185/// associated with an _order key_ `O` that can be used to determine if a value existing in the
186/// `UpsertStateBackend` occurred before or after a value being considered for insertion.
187///
188/// `O` typically required to be `: Default`, with the default value sorting below all others.
189/// During consolidation, values consolidate correctly (as they are actual
190/// differential updates with diffs), so order keys are not required.
191#[derive(Clone, serde::Serialize, serde::Deserialize)]
192pub enum StateValue<T, O> {
193    Consolidating(Consolidating),
194    Value(Value<T, O>),
195}
196
197impl<T, O> std::fmt::Debug for StateValue<T, O> {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            StateValue::Consolidating(c) => std::fmt::Display::fmt(c, f),
201            StateValue::Value(_) => write!(f, "Value"),
202        }
203    }
204}
205
206/// A totally consolidated value stored within the `UpsertStateBackend`.
207///
208/// This type contains support for _tombstones_, that contain an _order key_,
209/// and provisional values.
210///
211/// What is considered finalized and provisional depends on the implementation
212/// of the UPSERT operator: it might consider everything that it writes to its
213/// state finalized, and assume that what it emits will be written down in the
214/// output exactly as presented. Or it might consider everything it writes down
215/// provisional, and only consider updates that it _knows_ to be persisted as
216/// finalized.
217///
218/// Provisional values should only be considered while still "working off"
219/// updates with the same timestamp at which the provisional update was
220/// recorded.
221#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
222pub struct Value<T, O> {
223    /// The finalized value of a key is the value we know to be correct for the last complete
224    /// timestamp that got processed. A finalized value of None means that the key has been deleted
225    /// and acts as a tombstone.
226    pub finalized: Option<UpsertValue>,
227    /// When `Some(_)` it contains the upsert value has been processed for a yet incomplete
228    /// timestamp. When None, no provisional update has been emitted yet.
229    pub provisional: Option<ProvisionalValue<T, O>>,
230}
231
232/// A provisional value emitted for a timestamp. This struct contains enough information to
233#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
234pub struct ProvisionalValue<T, O> {
235    /// The timestamp at which this provisional value occured at
236    pub timestamp: T,
237    /// The order of this upsert command *within* the timestamp. Commands that happen at the same
238    /// timestamp with lower order get ignored. Commands with higher order override this one. If
239    /// there a case of equal order then the value itself is used as a tie breaker.
240    pub order: O,
241    /// The provisional value. A provisional value of None means that the key has been deleted and
242    /// acts as a tombstone.
243    pub value: Option<UpsertValue>,
244}
245
246/// A value as produced during consolidation.
247#[derive(Clone, Default, serde::Serialize, serde::Deserialize, Debug)]
248pub struct Consolidating {
249    #[serde(with = "serde_bytes")]
250    value_xor: Vec<u8>,
251    len_sum: Wrapping<i64>,
252    checksum_sum: Wrapping<i64>,
253    diff_sum: Wrapping<i64>,
254}
255
256impl fmt::Display for Consolidating {
257    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
258        f.debug_struct("Consolidating")
259            .field("len_sum", &self.len_sum)
260            .field("checksum_sum", &self.checksum_sum)
261            .field("diff_sum", &self.diff_sum)
262            .finish_non_exhaustive()
263    }
264}
265
266impl<T, O> StateValue<T, O> {
267    /// A finalized, that is (assumed) persistent, value.
268    pub fn finalized_value(value: UpsertValue) -> Self {
269        Self::Value(Value {
270            finalized: Some(value),
271            provisional: None,
272        })
273    }
274
275    #[allow(unused)]
276    /// A tombstoned value.
277    pub fn tombstone() -> Self {
278        Self::Value(Value {
279            finalized: None,
280            provisional: None,
281        })
282    }
283
284    /// Whether the value is a tombstone.
285    pub fn is_tombstone(&self) -> bool {
286        match self {
287            Self::Value(value) => value.finalized.is_none(),
288            _ => false,
289        }
290    }
291
292    /// Pull out the `Value` value for a `StateValue`, after `ensure_decoded` has been called.
293    pub fn into_decoded(self) -> Value<T, O> {
294        match self {
295            Self::Value(value) => value,
296            _ => panic!("called `into_decoded without calling `ensure_decoded`"),
297        }
298    }
299
300    /// The size of a `StateValue`, in memory. This is:
301    /// 1. only used in the `InMemoryHashMap` implementation.
302    /// 2. An estimate (it only looks at value sizes, and not errors)
303    ///
304    /// Other implementations may use more accurate accounting.
305    #[cfg(test)]
306    pub fn memory_size(&self) -> usize {
307        use mz_repr::Row;
308        use std::mem::size_of;
309
310        let heap_size = match self {
311            Self::Consolidating(Consolidating { value_xor, .. }) => value_xor.len(),
312            Self::Value(value) => {
313                let finalized_heap_size = match value.finalized {
314                    Some(Ok(ref row)) => {
315                        // `Row::byte_len` includes the size of `Row`, which is also in `Self`, so we
316                        // subtract it.
317                        row.byte_len() - size_of::<Row>()
318                    }
319                    // Assume errors are rare enough to not move the needle.
320                    _ => 0,
321                };
322                let provisional_heap_size = match value.provisional {
323                    Some(ref provisional) => match provisional.value {
324                        Some(Ok(ref row)) => {
325                            // `Row::byte_len` includes the size of `Row`, which is also in `Self`, so we
326                            // subtract it.
327                            row.byte_len() - size_of::<Row>()
328                        }
329                        // Assume errors are rare enough to not move the needle.
330                        _ => 0,
331                    },
332                    None => 0,
333                };
334                finalized_heap_size + provisional_heap_size
335            }
336        };
337        heap_size + size_of::<Self>()
338    }
339}
340
341impl<T: Eq, O> StateValue<T, O> {
342    /// Creates a new provisional value, occurring at some order key, observed
343    /// at the given timestamp.
344    pub fn new_provisional_value(value: UpsertValue, timestamp: T, order: O) -> Self {
345        Self::Value(Value {
346            finalized: None,
347            provisional: Some(ProvisionalValue {
348                value: Some(value),
349                timestamp,
350                order,
351            }),
352        })
353    }
354
355    /// Creates a provisional value, that retains the finalized value in this `StateValue`.
356    pub fn into_provisional_value(self, value: UpsertValue, timestamp: T, order: O) -> Self {
357        match self {
358            StateValue::Value(finalized) => Self::Value(Value {
359                finalized: finalized.finalized,
360                provisional: Some(ProvisionalValue {
361                    value: Some(value),
362                    timestamp,
363                    order,
364                }),
365            }),
366            StateValue::Consolidating(_) => {
367                panic!("called `into_provisional_value` without calling `ensure_decoded`")
368            }
369        }
370    }
371
372    /// Creates a new provisional tombstone occurring at some order key,
373    /// observed at the given timestamp.
374    pub fn new_provisional_tombstone(timestamp: T, order: O) -> Self {
375        Self::Value(Value {
376            finalized: None,
377            provisional: Some(ProvisionalValue {
378                value: None,
379                timestamp,
380                order,
381            }),
382        })
383    }
384
385    /// Creates a provisional tombstone, that retains the finalized value in this `StateValue`.
386    ///
387    /// We record the current finalized value, so that we can present it when
388    /// needed or when trying to read a provisional value at a different
389    /// timestamp.
390    pub fn into_provisional_tombstone(self, timestamp: T, order: O) -> Self {
391        match self {
392            StateValue::Value(finalized) => Self::Value(Value {
393                finalized: finalized.finalized,
394                provisional: Some(ProvisionalValue {
395                    value: None,
396                    timestamp,
397                    order,
398                }),
399            }),
400            StateValue::Consolidating(_) => {
401                panic!("called `into_provisional_tombstone` without calling `ensure_decoded`")
402            }
403        }
404    }
405
406    /// Returns the order of a provisional value at the given timestamp, if any.
407    pub fn provisional_order(&self, ts: &T) -> Option<&O> {
408        match self {
409            Self::Value(value) => match &value.provisional {
410                Some(p) if &p.timestamp == ts => Some(&p.order),
411                _ => None,
412            },
413            Self::Consolidating(_) => {
414                panic!("called `provisional_order` without calling `ensure_decoded`")
415            }
416        }
417    }
418
419    /// Returns the provisional value, if one is present at the given timestamp.
420    /// Falls back to the finalized value, or `None` if there is neither.
421    pub fn provisional_value_ref(&self, ts: &T) -> Option<&UpsertValue> {
422        match self {
423            Self::Value(value) => match &value.provisional {
424                Some(p) if &p.timestamp == ts => p.value.as_ref(),
425                _ => value.finalized.as_ref(),
426            },
427            Self::Consolidating(_) => {
428                panic!("called `provisional_value_ref` without calling `ensure_decoded`")
429            }
430        }
431    }
432
433    /// Returns the the finalized value, if one is present.
434    pub fn into_finalized_value(self) -> Option<UpsertValue> {
435        match self {
436            Self::Value(v) => v.finalized,
437            _ => panic!("called `into_finalized_value` without calling `ensure_decoded`"),
438        }
439    }
440}
441
442impl<T: Eq, O> StateValue<T, O> {
443    /// We use a XOR trick in order to accumulate the values without having to store the full
444    /// unconsolidated history in memory. For all (value, diff) updates of a key we track:
445    /// - diff_sum = SUM(diff)
446    /// - checksum_sum = SUM(checksum(bincode(value)) * diff)
447    /// - len_sum = SUM(len(bincode(value)) * diff)
448    /// - value_xor = XOR(bincode(value))
449    ///
450    /// ## Return value
451    /// Returns a `bool` indicating whether or not the current merged value is able to be deleted.
452    ///
453    /// ## Correctness
454    ///
455    /// The method is correct because a well formed upsert collection at a given
456    /// timestamp will have for each key:
457    /// - Zero or one updates of the form (cur_value, +1)
458    /// - Zero or more pairs of updates of the form (prev_value, +1), (prev_value, -1)
459    ///
460    /// We are interested in extracting the cur_value of each key and discard all prev_values
461    /// that might be included in the stream. Since the history of prev_values always comes in
462    /// pairs, computing the XOR of those is always going to cancel their effects out. Also,
463    /// since XOR is commutative this property is true independent of the order. The same is
464    /// true for the summations of the length and checksum since the sum will contain the
465    /// unrelated values zero times.
466    ///
467    /// Therefore the accumulators will end up precisely in one of two states:
468    /// 1. diff == 0, checksum == 0, value == [0..] => the key is not present
469    /// 2. diff == 1, checksum == checksum(cur_value) value == cur_value => the key is present
470    ///
471    /// ## Robustness
472    ///
473    /// In the absense of bugs, accumulating the diff and checksum is not required since we know
474    /// that a well formed collection always satisfies XOR(bincode(values)) == bincode(cur_value).
475    /// However bugs may happen and so storing 16 more bytes per key to have a very high
476    /// guarantee that we're not decoding garbage is more than worth it.
477    /// The main key->value used to store previous values.
478    #[allow(clippy::as_conversions)]
479    pub fn merge_update(
480        &mut self,
481        value: UpsertValue,
482        diff: mz_repr::Diff,
483        bincode_opts: BincodeOpts,
484        bincode_buffer: &mut Vec<u8>,
485    ) -> bool {
486        match self {
487            Self::Consolidating(Consolidating {
488                value_xor,
489                len_sum,
490                checksum_sum,
491                diff_sum,
492            }) => {
493                bincode_buffer.clear();
494                bincode_opts
495                    .serialize_into(&mut *bincode_buffer, &value)
496                    .unwrap();
497                let len = i64::try_from(bincode_buffer.len()).unwrap();
498
499                *diff_sum += diff.into_inner();
500                *len_sum += len.wrapping_mul(diff.into_inner());
501                // Truncation is fine (using `as`) as this is just a checksum
502                *checksum_sum +=
503                    (seahash::hash(&*bincode_buffer) as i64).wrapping_mul(diff.into_inner());
504
505                // XOR of even diffs cancel out, so we only do it if diff is odd
506                if diff.abs() % Diff::from(2) == Diff::ONE {
507                    if value_xor.len() < bincode_buffer.len() {
508                        value_xor.resize(bincode_buffer.len(), 0);
509                    }
510                    // Note that if the new value is _smaller_ than the `value_xor`, and
511                    // the values at the end are zeroed out, we can shrink the buffer. This
512                    // is extremely sensitive code, so we don't (yet) do that.
513                    for (acc, val) in value_xor[0..bincode_buffer.len()]
514                        .iter_mut()
515                        .zip_eq(bincode_buffer.drain(..))
516                    {
517                        *acc ^= val;
518                    }
519                }
520
521                // Returns whether or not the value can be deleted. This allows
522                // us to delete values in `UpsertState::consolidate_chunk` (even
523                // if they come back later), to minimize space usage.
524                diff_sum.0 == 0 && checksum_sum.0 == 0 && value_xor.iter().all(|&x| x == 0)
525            }
526            StateValue::Value(_value) => {
527                // We can turn a Value back into a Consolidating state:
528                // `std::mem::take` will leave behind a default value, which
529                // happens to be a default `Consolidating` `StateValue`.
530                let this = std::mem::take(self);
531
532                let finalized_value = this.into_finalized_value();
533                if let Some(finalized_value) = finalized_value {
534                    // If we had a value before, merge it into the
535                    // now-consolidating state first.
536                    let _ =
537                        self.merge_update(finalized_value, Diff::ONE, bincode_opts, bincode_buffer);
538
539                    // Then merge the new value in.
540                    self.merge_update(value, diff, bincode_opts, bincode_buffer)
541                } else {
542                    // We didn't have a value before, might have been a
543                    // tombstone. So just merge in the new value.
544                    self.merge_update(value, diff, bincode_opts, bincode_buffer)
545                }
546            }
547        }
548    }
549
550    /// Merge an existing StateValue into this one, using the same method described in `merge_update`.
551    /// See the docstring above for more information on correctness and robustness.
552    pub fn merge_update_state(&mut self, other: &Self) {
553        match (self, other) {
554            (
555                Self::Consolidating(Consolidating {
556                    value_xor,
557                    len_sum,
558                    checksum_sum,
559                    diff_sum,
560                }),
561                Self::Consolidating(other_consolidating),
562            ) => {
563                *diff_sum += other_consolidating.diff_sum;
564                *len_sum += other_consolidating.len_sum;
565                *checksum_sum += other_consolidating.checksum_sum;
566                if other_consolidating.value_xor.len() > value_xor.len() {
567                    value_xor.resize(other_consolidating.value_xor.len(), 0);
568                }
569                for (acc, val) in value_xor[0..other_consolidating.value_xor.len()]
570                    .iter_mut()
571                    .zip_eq(other_consolidating.value_xor.iter())
572                {
573                    *acc ^= val;
574                }
575            }
576            _ => panic!("`merge_update_state` called with non-consolidating state"),
577        }
578    }
579
580    /// During and after consolidation, we assume that values in the `UpsertStateBackend` implementation
581    /// can be `Self::Consolidating`, with a `diff_sum` of 1 (or 0, if they have been deleted).
582    /// Afterwards, if we need to retract one of these values, we need to assert that its in this correct state,
583    /// then mutate it to its `Value` state, so the `upsert` operator can use it.
584    #[allow(clippy::as_conversions)]
585    pub fn ensure_decoded(&mut self, bincode_opts: BincodeOpts, source_id: GlobalId) {
586        match self {
587            StateValue::Consolidating(consolidating) => {
588                match consolidating.diff_sum.0 {
589                    1 => {
590                        let len = usize::try_from(consolidating.len_sum.0)
591                            .map_err(|_| {
592                                format!(
593                                    "len_sum can't be made into a usize, state: {}, {}",
594                                    consolidating, source_id,
595                                )
596                            })
597                            .expect("invalid upsert state");
598                        let value = &consolidating
599                            .value_xor
600                            .get(..len)
601                            .ok_or_else(|| {
602                                format!(
603                                    "value_xor is not the same length ({}) as len ({}), state: {}, {}",
604                                    consolidating.value_xor.len(),
605                                    len,
606                                    consolidating,
607                                    source_id,
608                                )
609                            })
610                            .expect("invalid upsert state");
611                        // Truncation is fine (using `as`) as this is just a checksum
612                        assert_eq!(
613                            consolidating.checksum_sum.0,
614                            // Hash the value, not the full buffer, which may have extra 0's
615                            seahash::hash(value) as i64,
616                            "invalid upsert state: checksum_sum does not match, state: {}, {}",
617                            consolidating,
618                            source_id,
619                        );
620                        *self = Self::finalized_value(bincode_opts.deserialize(value).unwrap());
621                    }
622                    0 => {
623                        assert_eq!(
624                            consolidating.len_sum.0, 0,
625                            "invalid upsert state: len_sum is non-0, state: {}, {}",
626                            consolidating, source_id,
627                        );
628                        assert_eq!(
629                            consolidating.checksum_sum.0, 0,
630                            "invalid upsert state: checksum_sum is non-0, state: {}, {}",
631                            consolidating, source_id,
632                        );
633                        assert!(
634                            consolidating.value_xor.iter().all(|&x| x == 0),
635                            "invalid upsert state: value_xor not all 0s with 0 diff. \
636                            Non-zero positions: {:?}, state: {}, {}",
637                            consolidating
638                                .value_xor
639                                .iter()
640                                .positions(|&x| x != 0)
641                                .collect::<Vec<_>>(),
642                            consolidating,
643                            source_id,
644                        );
645                        *self = Self::tombstone();
646                    }
647                    other => panic!(
648                        "invalid upsert state: non 0/1 diff_sum: {}, state: {}, {}",
649                        other, consolidating, source_id
650                    ),
651                }
652            }
653            _ => {}
654        }
655    }
656}
657
658impl<T, O> Default for StateValue<T, O> {
659    fn default() -> Self {
660        Self::Consolidating(Consolidating::default())
661    }
662}
663
664/// Statistics for a single call to `consolidate_chunk`.
665#[derive(Clone, Default, Debug)]
666pub struct SnapshotStats {
667    /// The number of updates processed.
668    pub updates: u64,
669    /// The aggregated number of values inserted or deleted into `state`.
670    pub values_diff: Diff,
671    /// The total aggregated size of values inserted, deleted, or updated in `state`.
672    /// If the current call to `consolidate_chunk` deletes a lot of values,
673    /// or updates values to smaller ones, this can be negative!
674    pub size_diff: i64,
675    /// The number of inserts i.e. +1 diff
676    pub inserts: u64,
677    /// The number of deletes i.e. -1 diffs
678    pub deletes: u64,
679}
680
681impl std::ops::AddAssign for SnapshotStats {
682    fn add_assign(&mut self, rhs: Self) {
683        self.updates += rhs.updates;
684        self.values_diff += rhs.values_diff;
685        self.size_diff += rhs.size_diff;
686        self.inserts += rhs.inserts;
687        self.deletes += rhs.deletes;
688    }
689}
690
691/// Statistics for a single call to `multi_merge`.
692#[derive(Clone, Default, Debug)]
693pub struct MergeStats {
694    /// The number of updates written as merge operands to the backend, for the backend
695    /// to process async in the `consolidating_merge_function`.
696    /// Should be equal to number of inserts + deletes
697    pub written_merge_operands: u64,
698    /// The total size of values provided to `multi_merge`. The backend will write these
699    /// down and then later merge them in the `consolidating_merge_function`.
700    pub size_written: u64,
701    /// The estimated diff of the total size of the working set after the merge operands
702    /// are merged by the backend. This is an estimate since it can't account for the
703    /// size overhead of `StateValue` for values that consolidate to 0 (tombstoned-values).
704    pub size_diff: i64,
705}
706
707/// Statistics for a single call to `multi_put`.
708#[derive(Clone, Default, Debug)]
709pub struct PutStats {
710    /// The number of puts/deletes processed
711    /// Should be equal to number of inserts + updates + deletes
712    pub processed_puts: u64,
713    /// The aggregated number of non-tombstone values inserted or deleted into `state`.
714    pub values_diff: i64,
715    /// The aggregated number of tombstones inserted or deleted into `state`
716    pub tombstones_diff: i64,
717    /// The total aggregated size of values inserted, deleted, or updated in `state`.
718    /// If the current call to `multi_put` deletes a lot of values,
719    /// or updates values to smaller ones, this can be negative!
720    pub size_diff: i64,
721    /// The number of inserts
722    pub inserts: u64,
723    /// The number of updates
724    pub updates: u64,
725    /// The number of deletes
726    pub deletes: u64,
727}
728
729impl PutStats {
730    /// Adjust the `PutStats` based on the new value and the previous metadata.
731    ///
732    /// The size parameter is separate as its value is backend-dependent. Its optional
733    /// as some backends increase the total size after an entire batch is processed.
734    ///
735    /// This method is provided for implementors of `UpsertStateBackend::multi_put`.
736    pub fn adjust<T, O>(
737        &mut self,
738        new_value: Option<&StateValue<T, O>>,
739        new_size: Option<i64>,
740        previous_metdata: &Option<ValueMetadata<i64>>,
741    ) {
742        self.adjust_size(new_value, new_size, previous_metdata);
743        self.adjust_values(new_value, previous_metdata);
744        self.adjust_tombstone(new_value, previous_metdata);
745    }
746
747    fn adjust_size<T, O>(
748        &mut self,
749        new_value: Option<&StateValue<T, O>>,
750        new_size: Option<i64>,
751        previous_metdata: &Option<ValueMetadata<i64>>,
752    ) {
753        match (&new_value, previous_metdata.as_ref()) {
754            (Some(_), Some(ps)) => {
755                self.size_diff -= ps.size;
756                if let Some(new_size) = new_size {
757                    self.size_diff += new_size;
758                }
759            }
760            (None, Some(ps)) => {
761                self.size_diff -= ps.size;
762            }
763            (Some(_), None) => {
764                if let Some(new_size) = new_size {
765                    self.size_diff += new_size;
766                }
767            }
768            (None, None) => {}
769        }
770    }
771
772    fn adjust_values<T, O>(
773        &mut self,
774        new_value: Option<&StateValue<T, O>>,
775        previous_metdata: &Option<ValueMetadata<i64>>,
776    ) {
777        let truly_new_value = new_value.map_or(false, |v| !v.is_tombstone());
778        let truly_old_value = previous_metdata.map_or(false, |v| !v.is_tombstone);
779
780        match (truly_new_value, truly_old_value) {
781            (false, true) => {
782                self.values_diff -= 1;
783            }
784            (true, false) => {
785                self.values_diff += 1;
786            }
787            _ => {}
788        }
789    }
790
791    fn adjust_tombstone<T, O>(
792        &mut self,
793        new_value: Option<&StateValue<T, O>>,
794        previous_metdata: &Option<ValueMetadata<i64>>,
795    ) {
796        let new_tombstone = new_value.map_or(false, |v| v.is_tombstone());
797        let old_tombstone = previous_metdata.map_or(false, |v| v.is_tombstone);
798
799        match (new_tombstone, old_tombstone) {
800            (false, true) => {
801                self.tombstones_diff -= 1;
802            }
803            (true, false) => {
804                self.tombstones_diff += 1;
805            }
806            _ => {}
807        }
808    }
809}
810
811/// Statistics for a single call to `multi_get`.
812#[derive(Clone, Default, Debug)]
813pub struct GetStats {
814    /// The number of gets processed
815    pub processed_gets: u64,
816    /// The total size in bytes returned
817    pub processed_gets_size: u64,
818    /// The number of non-empty records returned
819    pub returned_gets: u64,
820}
821
822/// A trait that defines the fundamental primitives required by a state-backing of
823/// `UpsertState`.
824///
825/// Implementors of this trait are blind maps that associate keys and values. They need
826/// not understand the semantics of `StateValue`, tombstones, or anything else related
827/// to a correct `upsert` implementation. The singular exception to this is that they
828/// **must** produce accurate `PutStats` and `GetStats`. The reasoning for this is two-fold:
829/// - efficiency: this avoids additional buffer allocation.
830/// - value sizes: only the backend implementation understands the size of values as recorded
831///
832/// This **must** is not a correctness requirement (we won't panic when emitting statistics), but
833/// rather a requirement to ensure the upsert operator is introspectable.
834#[async_trait::async_trait(?Send)]
835pub trait UpsertStateBackend<T, O>
836where
837    T: 'static,
838    O: 'static,
839{
840    /// Whether this backend supports the `multi_merge` operation.
841    fn supports_merge(&self) -> bool;
842
843    /// Insert or delete for all `puts` keys, prioritizing the last value for
844    /// repeated keys.
845    ///
846    /// The `PutValue` is _guaranteed_ to have an accurate and up-to-date
847    /// record of the metadata for existing value for the given key (if one existed),
848    /// as reported by a previous call to `multi_get`.
849    ///
850    /// `PutStats` **must** be populated correctly, according to these semantics:
851    /// - `values_diff` must record the difference in number of new non-tombstone values being
852    /// inserted into the backend.
853    /// - `tombstones_diff` must record the difference in number of tombstone values being
854    /// inserted into the backend.
855    /// - `size_diff` must record the change in size for the values being inserted/deleted/updated
856    /// in the backend, regardless of whether the values are tombstones or not.
857    async fn multi_put<P>(&mut self, puts: P) -> Result<PutStats, anyhow::Error>
858    where
859        P: IntoIterator<Item = (UpsertKey, PutValue<StateValue<T, O>>)>;
860
861    /// Get the `gets` keys, which must be unique, placing the results in `results_out`.
862    ///
863    /// Panics if `gets` and `results_out` are not the same length.
864    async fn multi_get<'r, G, R>(
865        &mut self,
866        gets: G,
867        results_out: R,
868    ) -> Result<GetStats, anyhow::Error>
869    where
870        G: IntoIterator<Item = UpsertKey>,
871        R: IntoIterator<Item = &'r mut UpsertValueAndSize<T, O>>;
872
873    /// For each key in `merges` writes a 'merge operand' to the backend. The backend stores these
874    /// merge operands and periodically calls the `consolidating_merge_function` to merge them into
875    /// any existing value for each key. The backend will merge the merge operands in the order
876    /// they are provided, and the merge function will always be run for a given key when a `get`
877    /// operation is performed on that key, or when the backend decides to run the merge based
878    /// on its own internal logic.
879    /// This allows avoiding the read-modify-write method of updating many values to
880    /// improve performance.
881    ///
882    /// The `MergeValue` should include a `diff` field that represents the update diff for the
883    /// value. This is used to estimate the overall size diff of the working set
884    /// after the merge operands are merged by the backend `sum[merges: m](m.diff * m.size)`.
885    ///
886    /// `MergeStats` **must** be populated correctly, according to these semantics:
887    /// - `written_merge_operands` must record the number of merge operands written to the backend.
888    /// - `size_written` must record the total size of values written to the backend.
889    ///     Note that the size of the post-merge values are not known, so this is the size of the
890    ///     values written to the backend as merge operands.
891    /// - `size_diff` must record the estimated diff of the total size of the working set after the
892    ///    merge operands are merged by the backend.
893    async fn multi_merge<P>(&mut self, merges: P) -> Result<MergeStats, anyhow::Error>
894    where
895        P: IntoIterator<Item = (UpsertKey, MergeValue<StateValue<T, O>>)>;
896}
897
898/// A function that merges a set of updates for a key into the existing value
899/// for the key. This is called by the backend implementation when it has
900/// accumulated a set of updates for a key, and needs to merge them into the
901/// existing value for the key.
902///
903/// The function is called with the following arguments:
904/// - The key for which the merge is being performed.
905/// - An iterator over any current value and merge operands queued for the key.
906///
907/// The function should return the new value for the key after merging all the updates.
908pub(crate) fn consolidating_merge_function<T: Eq, O>(
909    _key: UpsertKey,
910    updates: impl Iterator<Item = StateValue<T, O>>,
911) -> StateValue<T, O> {
912    let mut current: StateValue<T, O> = Default::default();
913
914    let mut bincode_buf = Vec::new();
915    for update in updates {
916        match update {
917            StateValue::Consolidating(_) => {
918                current.merge_update_state(&update);
919            }
920            StateValue::Value(_) => {
921                // This branch is more expensive, but we hopefully rarely hit
922                // it.
923                if let Some(finalized_value) = update.into_finalized_value() {
924                    let mut update = StateValue::default();
925                    update.merge_update(
926                        finalized_value,
927                        Diff::ONE,
928                        upsert_bincode_opts(),
929                        &mut bincode_buf,
930                    );
931                    current.merge_update_state(&update);
932                }
933            }
934        }
935    }
936
937    current
938}
939
940/// An `UpsertStateBackend` wrapper that supports consolidating merging, and
941/// reports basic metrics about the usage of the `UpsertStateBackend`.
942pub struct UpsertState<'metrics, S, T, O> {
943    inner: S,
944
945    // The status, start time, and stats about calls to `consolidate_chunk`.
946    pub snapshot_start: Instant,
947    snapshot_stats: SnapshotStats,
948    snapshot_completed: bool,
949
950    // Metrics shared across all workers running the `upsert` operator.
951    metrics: Arc<UpsertSharedMetrics>,
952    // Metrics for a specific worker.
953    worker_metrics: &'metrics UpsertMetrics,
954    // User-facing statistics.
955    stats: SourceStatistics,
956
957    // Bincode options and buffer used in `consolidate_chunk`.
958    bincode_opts: BincodeOpts,
959    bincode_buffer: Vec<u8>,
960
961    // We need to iterate over `updates` in `consolidate_chunk` twice, so we
962    // have a scratch vector for this.
963    consolidate_scratch: Vec<(UpsertKey, UpsertValue, mz_repr::Diff)>,
964    // "mini-upsert" map used in `consolidate_chunk`
965    consolidate_upsert_scratch: indexmap::IndexMap<UpsertKey, UpsertValueAndSize<T, O>>,
966    // a scratch vector for calling `multi_get`
967    multi_get_scratch: Vec<UpsertKey>,
968    shrink_upsert_unused_buffers_by_ratio: usize,
969}
970
971impl<'metrics, S, T, O> UpsertState<'metrics, S, T, O> {
972    pub(crate) fn new(
973        inner: S,
974        metrics: Arc<UpsertSharedMetrics>,
975        worker_metrics: &'metrics UpsertMetrics,
976        stats: SourceStatistics,
977        shrink_upsert_unused_buffers_by_ratio: usize,
978    ) -> Self {
979        Self {
980            inner,
981            snapshot_start: Instant::now(),
982            snapshot_stats: SnapshotStats::default(),
983            snapshot_completed: false,
984            metrics,
985            worker_metrics,
986            stats,
987            bincode_opts: upsert_bincode_opts(),
988            bincode_buffer: Vec::new(),
989            consolidate_scratch: Vec::new(),
990            consolidate_upsert_scratch: indexmap::IndexMap::new(),
991            multi_get_scratch: Vec::new(),
992            shrink_upsert_unused_buffers_by_ratio,
993        }
994    }
995}
996
997impl<S, T, O> UpsertState<'_, S, T, O>
998where
999    S: UpsertStateBackend<T, O>,
1000    T: Eq + Clone + Send + Sync + Serialize + 'static,
1001    O: Clone + Send + Sync + Serialize + DeserializeOwned + 'static,
1002{
1003    /// Consolidate the following differential updates into the state. Updates
1004    /// provided to this method can be assumed to consolidate into a single
1005    /// value per-key, after all chunks of updates for a given timestamp have
1006    /// been processed,
1007    ///
1008    /// Therefore, after all updates of a given timestamp have been
1009    /// `consolidated`, all values must be in the correct state (as determined
1010    /// by `StateValue::ensure_decoded`).
1011    ///
1012    /// The `completed` boolean communicates whether or not this is the final
1013    /// chunk of updates for the initial "snapshot" from persist.
1014    ///
1015    /// If the backend supports it, this method will use `multi_merge` to
1016    /// consolidate the updates to avoid having to read the existing value for
1017    /// each key first. On some backends (like RocksDB), this can be
1018    /// significantly faster than the read-then-write consolidation strategy.
1019    ///
1020    /// Also note that we use `self.inner.multi_*`, not `self.multi_*`. This is
1021    /// to avoid erroneously changing metric and stats values.
1022    pub async fn consolidate_chunk<U>(
1023        &mut self,
1024        updates: U,
1025        completed: bool,
1026    ) -> Result<(), anyhow::Error>
1027    where
1028        U: IntoIterator<Item = (UpsertKey, UpsertValue, mz_repr::Diff)> + ExactSizeIterator,
1029    {
1030        fail::fail_point!("fail_consolidate_chunk", |_| {
1031            Err(anyhow::anyhow!("Error consolidating values"))
1032        });
1033
1034        if completed && self.snapshot_completed {
1035            panic!("attempted completion of already completed upsert snapshot")
1036        }
1037
1038        let now = Instant::now();
1039        let batch_size = updates.len();
1040
1041        self.consolidate_scratch.clear();
1042        self.consolidate_upsert_scratch.clear();
1043        self.multi_get_scratch.clear();
1044
1045        // Shrinking the scratch vectors if the capacity is significantly more than batch size
1046        if self.shrink_upsert_unused_buffers_by_ratio > 0 {
1047            let reduced_capacity =
1048                self.consolidate_scratch.capacity() / self.shrink_upsert_unused_buffers_by_ratio;
1049            if reduced_capacity > batch_size {
1050                // These vectors have already been cleared above and should be empty here
1051                self.consolidate_scratch.shrink_to(reduced_capacity);
1052                self.consolidate_upsert_scratch.shrink_to(reduced_capacity);
1053                self.multi_get_scratch.shrink_to(reduced_capacity);
1054            }
1055        }
1056
1057        // Depending on if the backend supports multi_merge, call the appropriate method.
1058        let stats = if self.inner.supports_merge() {
1059            self.consolidate_merge_inner(updates).await?
1060        } else {
1061            self.consolidate_read_write_inner(updates).await?
1062        };
1063
1064        // NOTE: These metrics use the term `merge` to refer to the consolidation of values.
1065        // This is because they were introduced before we the `multi_merge` operation was added.
1066        self.metrics
1067            .merge_snapshot_latency
1068            .observe(now.elapsed().as_secs_f64());
1069        self.worker_metrics
1070            .merge_snapshot_updates
1071            .inc_by(stats.updates);
1072        self.worker_metrics
1073            .merge_snapshot_inserts
1074            .inc_by(stats.inserts);
1075        self.worker_metrics
1076            .merge_snapshot_deletes
1077            .inc_by(stats.deletes);
1078
1079        self.stats.update_bytes_indexed_by(stats.size_diff);
1080        self.stats
1081            .update_records_indexed_by(stats.values_diff.into_inner());
1082
1083        self.snapshot_stats += stats;
1084
1085        if !self.snapshot_completed {
1086            // Updating the metrics
1087            self.worker_metrics.rehydration_total.set(
1088                self.snapshot_stats
1089                    .values_diff
1090                    .into_inner()
1091                    .try_into()
1092                    .unwrap_or_else(|e: std::num::TryFromIntError| {
1093                        tracing::warn!(
1094                            "rehydration_total metric overflowed or is negative \
1095                        and is innacurate: {}. Defaulting to 0",
1096                            e.display_with_causes(),
1097                        );
1098
1099                        0
1100                    }),
1101            );
1102            self.worker_metrics
1103                .rehydration_updates
1104                .set(self.snapshot_stats.updates);
1105        }
1106
1107        if completed {
1108            if self.shrink_upsert_unused_buffers_by_ratio > 0 {
1109                // After rehydration is done, these scratch buffers should now be empty
1110                // shrinking them entirely
1111                self.consolidate_scratch.shrink_to_fit();
1112                self.consolidate_upsert_scratch.shrink_to_fit();
1113                self.multi_get_scratch.shrink_to_fit();
1114            }
1115
1116            self.worker_metrics
1117                .rehydration_latency
1118                .set(self.snapshot_start.elapsed().as_secs_f64());
1119
1120            self.snapshot_completed = true;
1121        }
1122        Ok(())
1123    }
1124
1125    /// Consolidate the updates into the state. This method requires the backend
1126    /// has support for the `multi_merge` operation, and will panic if
1127    /// `self.inner.supports_merge()` was not checked before calling this
1128    /// method. `multi_merge` will write the updates as 'merge operands' to the
1129    /// backend, and then the backend will consolidate those updates with any
1130    /// existing state using the `consolidating_merge_function`.
1131    ///
1132    /// This method can have significant performance benefits over the
1133    /// read-then-write method of `consolidate_read_write_inner`.
1134    async fn consolidate_merge_inner<U>(
1135        &mut self,
1136        updates: U,
1137    ) -> Result<SnapshotStats, anyhow::Error>
1138    where
1139        U: IntoIterator<Item = (UpsertKey, UpsertValue, mz_repr::Diff)> + ExactSizeIterator,
1140    {
1141        let mut updates = updates.into_iter().peekable();
1142
1143        let mut stats = SnapshotStats::default();
1144
1145        if updates.peek().is_some() {
1146            let m_stats = self
1147                .inner
1148                .multi_merge(updates.map(|(k, v, diff)| {
1149                    // Transform into a `StateValue<O>` that can be used by the
1150                    // `consolidating_merge_function` to merge with any existing
1151                    // value for the key.
1152                    let mut val: StateValue<T, O> = Default::default();
1153                    val.merge_update(v, diff, self.bincode_opts, &mut self.bincode_buffer);
1154
1155                    stats.updates += 1;
1156                    if diff.is_positive() {
1157                        stats.inserts += 1;
1158                    } else if diff.is_negative() {
1159                        stats.deletes += 1;
1160                    }
1161
1162                    // To keep track of the overall `values_diff` we can use the sum of diffs which
1163                    // should be equal to the number of non-tombstoned values in the backend.
1164                    // This is a bit misleading as this represents the eventual state after the
1165                    // `consolidating_merge_function` has been called to merge all the updates,
1166                    // and not the state after this `multi_merge` call.
1167                    //
1168                    // This does not accurately report values that have been consolidated to diff == 0, as tracking that
1169                    // per-key is extremely difficult.
1170                    stats.values_diff += diff;
1171
1172                    (k, MergeValue { value: val, diff })
1173                }))
1174                .await?;
1175
1176            stats.size_diff = m_stats.size_diff;
1177        }
1178
1179        Ok(stats)
1180    }
1181
1182    /// Consolidates the updates into the state. This method reads the existing
1183    /// values for each key, consolidates the updates, and writes the new values
1184    /// back to the state.
1185    async fn consolidate_read_write_inner<U>(
1186        &mut self,
1187        updates: U,
1188    ) -> Result<SnapshotStats, anyhow::Error>
1189    where
1190        U: IntoIterator<Item = (UpsertKey, UpsertValue, mz_repr::Diff)> + ExactSizeIterator,
1191    {
1192        let mut updates = updates.into_iter().peekable();
1193
1194        let mut stats = SnapshotStats::default();
1195
1196        if updates.peek().is_some() {
1197            self.consolidate_scratch.extend(updates);
1198            self.consolidate_upsert_scratch.extend(
1199                self.consolidate_scratch
1200                    .iter()
1201                    .map(|(k, _, _)| (*k, UpsertValueAndSize::default())),
1202            );
1203            self.multi_get_scratch
1204                .extend(self.consolidate_upsert_scratch.iter().map(|(k, _)| *k));
1205            self.inner
1206                .multi_get(
1207                    self.multi_get_scratch.drain(..),
1208                    self.consolidate_upsert_scratch.iter_mut().map(|(_, v)| v),
1209                )
1210                .await?;
1211
1212            for (key, value, diff) in self.consolidate_scratch.drain(..) {
1213                stats.updates += 1;
1214                if diff.is_positive() {
1215                    stats.inserts += 1;
1216                } else if diff.is_negative() {
1217                    stats.deletes += 1;
1218                }
1219
1220                // We rely on the diffs in our input instead of the result of
1221                // multi_put below. This makes sure we report the same stats as
1222                // `consolidate_merge_inner`, regardless of what values
1223                // there were in state before.
1224                stats.values_diff += diff;
1225
1226                let entry = self.consolidate_upsert_scratch.get_mut(&key).unwrap();
1227                let val = entry.value.get_or_insert_with(Default::default);
1228
1229                if val.merge_update(value, diff, self.bincode_opts, &mut self.bincode_buffer) {
1230                    entry.value = None;
1231                }
1232            }
1233
1234            // Note we do 1 `multi_get` and 1 `multi_put` while processing a _batch of updates_.
1235            // Within the batch, we effectively consolidate each key, before persisting that
1236            // consolidated value. Easy!!
1237            let p_stats = self
1238                .inner
1239                .multi_put(self.consolidate_upsert_scratch.drain(..).map(|(k, v)| {
1240                    (
1241                        k,
1242                        PutValue {
1243                            value: v.value,
1244                            previous_value_metadata: v.metadata.map(|v| ValueMetadata {
1245                                size: v.size.try_into().expect("less than i64 size"),
1246                                is_tombstone: v.is_tombstone,
1247                            }),
1248                        },
1249                    )
1250                }))
1251                .await?;
1252
1253            stats.size_diff = p_stats.size_diff;
1254        }
1255
1256        Ok(stats)
1257    }
1258
1259    /// Insert or delete for all `puts` keys, prioritizing the last value for
1260    /// repeated keys.
1261    pub async fn multi_put<P>(
1262        &mut self,
1263        update_per_record_stats: bool,
1264        puts: P,
1265    ) -> Result<(), anyhow::Error>
1266    where
1267        P: IntoIterator<Item = (UpsertKey, PutValue<Value<T, O>>)>,
1268    {
1269        fail::fail_point!("fail_state_multi_put", |_| {
1270            Err(anyhow::anyhow!("Error putting values into state"))
1271        });
1272        let now = Instant::now();
1273        let stats = self
1274            .inner
1275            .multi_put(puts.into_iter().map(|(k, pv)| {
1276                (
1277                    k,
1278                    PutValue {
1279                        value: pv.value.map(StateValue::Value),
1280                        previous_value_metadata: pv.previous_value_metadata,
1281                    },
1282                )
1283            }))
1284            .await?;
1285
1286        self.metrics
1287            .multi_put_latency
1288            .observe(now.elapsed().as_secs_f64());
1289        self.worker_metrics
1290            .multi_put_size
1291            .inc_by(stats.processed_puts);
1292
1293        if update_per_record_stats {
1294            self.worker_metrics.upsert_inserts.inc_by(stats.inserts);
1295            self.worker_metrics.upsert_updates.inc_by(stats.updates);
1296            self.worker_metrics.upsert_deletes.inc_by(stats.deletes);
1297
1298            self.stats.update_bytes_indexed_by(stats.size_diff);
1299            self.stats.update_records_indexed_by(stats.values_diff);
1300            self.stats
1301                .update_envelope_state_tombstones_by(stats.tombstones_diff);
1302        }
1303
1304        Ok(())
1305    }
1306
1307    /// Get the `gets` keys, which must be unique, placing the results in `results_out`.
1308    ///
1309    /// Panics if `gets` and `results_out` are not the same length.
1310    pub async fn multi_get<'r, G, R>(
1311        &mut self,
1312        gets: G,
1313        results_out: R,
1314    ) -> Result<(), anyhow::Error>
1315    where
1316        G: IntoIterator<Item = UpsertKey>,
1317        R: IntoIterator<Item = &'r mut UpsertValueAndSize<T, O>>,
1318        O: 'r,
1319    {
1320        fail::fail_point!("fail_state_multi_get", |_| {
1321            Err(anyhow::anyhow!("Error getting values from state"))
1322        });
1323        let now = Instant::now();
1324        let stats = self.inner.multi_get(gets, results_out).await?;
1325
1326        self.metrics
1327            .multi_get_latency
1328            .observe(now.elapsed().as_secs_f64());
1329        self.worker_metrics
1330            .multi_get_size
1331            .inc_by(stats.processed_gets);
1332        self.worker_metrics
1333            .multi_get_result_count
1334            .inc_by(stats.returned_gets);
1335        self.worker_metrics
1336            .multi_get_result_bytes
1337            .inc_by(stats.processed_gets_size);
1338
1339        Ok(())
1340    }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345    use mz_repr::Row;
1346
1347    use super::*;
1348    #[mz_ore::test]
1349    fn test_merge_update() {
1350        let mut buf = Vec::new();
1351        let opts = upsert_bincode_opts();
1352
1353        let mut s = StateValue::<(), ()>::Consolidating(Consolidating::default());
1354
1355        let small_row = Ok(Row::default());
1356        let longer_row = Ok(Row::pack([mz_repr::Datum::Null]));
1357        s.merge_update(small_row, Diff::ONE, opts, &mut buf);
1358        s.merge_update(longer_row.clone(), Diff::MINUS_ONE, opts, &mut buf);
1359        // This clears the retraction of the `longer_row`, but the
1360        // `value_xor` is the length of the `longer_row`. This tests
1361        // that we are tracking checksums correctly.
1362        s.merge_update(longer_row, Diff::ONE, opts, &mut buf);
1363
1364        // Assert that the `Consolidating` value is fully merged.
1365        s.ensure_decoded(opts, GlobalId::User(1));
1366    }
1367
1368    // We guard some of our assumptions. Increasing in-memory size of StateValue
1369    // has a direct impact on memory usage of in-memory UPSERT sources.
1370    #[mz_ore::test]
1371    fn test_memory_size() {
1372        let finalized_value: StateValue<(), ()> = StateValue::finalized_value(Ok(Row::default()));
1373        assert!(
1374            finalized_value.memory_size() <= 64,
1375            "memory size is {}",
1376            finalized_value.memory_size(),
1377        );
1378
1379        let provisional_value_with_finalized_value: StateValue<(), ()> =
1380            finalized_value.into_provisional_value(Ok(Row::default()), (), ());
1381        assert!(
1382            provisional_value_with_finalized_value.memory_size() <= 64,
1383            "memory size is {}",
1384            provisional_value_with_finalized_value.memory_size(),
1385        );
1386
1387        let provisional_value_without_finalized_value: StateValue<(), ()> =
1388            StateValue::new_provisional_value(Ok(Row::default()), (), ());
1389        assert!(
1390            provisional_value_without_finalized_value.memory_size() <= 64,
1391            "memory size is {}",
1392            provisional_value_without_finalized_value.memory_size(),
1393        );
1394
1395        let mut consolidating_value: StateValue<(), ()> = StateValue::default();
1396        consolidating_value.merge_update(
1397            Ok(Row::default()),
1398            Diff::ONE,
1399            upsert_bincode_opts(),
1400            &mut Vec::new(),
1401        );
1402        assert!(
1403            consolidating_value.memory_size() <= 66,
1404            "memory size is {}",
1405            consolidating_value.memory_size(),
1406        );
1407    }
1408
1409    #[mz_ore::test]
1410    #[should_panic(
1411        expected = "invalid upsert state: len_sum is non-0, state: Consolidating { len_sum: 1"
1412    )]
1413    fn test_merge_update_len_0_assert() {
1414        let mut buf = Vec::new();
1415        let opts = upsert_bincode_opts();
1416
1417        let mut s = StateValue::<(), ()>::Consolidating(Consolidating::default());
1418
1419        let small_row = Ok(mz_repr::Row::default());
1420        let longer_row = Ok(mz_repr::Row::pack([mz_repr::Datum::Null]));
1421        s.merge_update(longer_row.clone(), Diff::ONE, opts, &mut buf);
1422        s.merge_update(small_row.clone(), Diff::MINUS_ONE, opts, &mut buf);
1423
1424        s.ensure_decoded(opts, GlobalId::User(1));
1425    }
1426
1427    #[mz_ore::test]
1428    #[should_panic(
1429        expected = "invalid upsert state: \"value_xor is not the same length (3) as len (4), state: Consolidating { len_sum: 4"
1430    )]
1431    fn test_merge_update_len_to_long_assert() {
1432        let mut buf = Vec::new();
1433        let opts = upsert_bincode_opts();
1434
1435        let mut s = StateValue::<(), ()>::Consolidating(Consolidating::default());
1436
1437        let small_row = Ok(mz_repr::Row::default());
1438        let longer_row = Ok(mz_repr::Row::pack([mz_repr::Datum::Null]));
1439        s.merge_update(longer_row.clone(), Diff::ONE, opts, &mut buf);
1440        s.merge_update(small_row.clone(), Diff::MINUS_ONE, opts, &mut buf);
1441        s.merge_update(longer_row.clone(), Diff::ONE, opts, &mut buf);
1442
1443        s.ensure_decoded(opts, GlobalId::User(1));
1444    }
1445
1446    #[mz_ore::test]
1447    #[should_panic(expected = "invalid upsert state: checksum_sum does not match")]
1448    fn test_merge_update_checksum_doesnt_match() {
1449        let mut buf = Vec::new();
1450        let opts = upsert_bincode_opts();
1451
1452        let mut s = StateValue::<(), ()>::Consolidating(Consolidating::default());
1453
1454        let small_row = Ok(mz_repr::Row::pack([mz_repr::Datum::Int64(2)]));
1455        let longer_row = Ok(mz_repr::Row::pack([mz_repr::Datum::Int64(1)]));
1456        s.merge_update(longer_row.clone(), Diff::ONE, opts, &mut buf);
1457        s.merge_update(small_row.clone(), Diff::MINUS_ONE, opts, &mut buf);
1458        s.merge_update(longer_row.clone(), Diff::ONE, opts, &mut buf);
1459
1460        s.ensure_decoded(opts, GlobalId::User(1));
1461    }
1462}