Skip to main content

mz_persist_client/internal/
state_diff.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::cmp::Ordering;
11use std::collections::BTreeMap;
12use std::fmt::Debug;
13use std::sync::Arc;
14
15use bytes::{Bytes, BytesMut};
16use differential_dataflow::lattice::Lattice;
17use differential_dataflow::trace::Description;
18use mz_ore::assert_none;
19use mz_ore::cast::CastFrom;
20use mz_persist::location::{SeqNo, VersionedData};
21use mz_persist_types::Codec64;
22use mz_persist_types::schema::SchemaId;
23use mz_proto::TryFromProtoError;
24use timely::PartialOrder;
25use timely::progress::{Antichain, Timestamp};
26use tracing::debug;
27
28use crate::critical::CriticalReaderId;
29use crate::internal::paths::PartialRollupKey;
30use crate::internal::state::{
31    CriticalReaderState, EncodedSchemas, HollowBatch, HollowBlobRef, HollowRollup,
32    LeasedReaderState, ProtoStateField, ProtoStateFieldDiffType, ProtoStateFieldDiffs, RunPart,
33    State, StateCollections, WriterState,
34};
35use crate::internal::trace::CompactionInput;
36use crate::internal::trace::{FueledMergeRes, SpineId, ThinMerge, ThinSpineBatch, Trace};
37use crate::read::LeasedReaderId;
38use crate::write::WriterId;
39use crate::{Metrics, PersistConfig, ShardId};
40
41use StateFieldValDiff::*;
42
43use super::state::{ActiveGc, ActiveRollup};
44
45#[derive(Clone, Debug)]
46#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
47pub enum StateFieldValDiff<V> {
48    Insert(V),
49    Update(V, V),
50    Delete(V),
51}
52
53#[derive(Clone)]
54#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
55pub struct StateFieldDiff<K, V> {
56    pub key: K,
57    pub val: StateFieldValDiff<V>,
58}
59
60impl<K: Debug, V: Debug> std::fmt::Debug for StateFieldDiff<K, V> {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("StateFieldDiff")
63            // In the cases we've seen in the wild, it's been more useful to
64            // have the val printed first.
65            .field("val", &self.val)
66            .field("key", &self.key)
67            .finish()
68    }
69}
70
71#[derive(Debug)]
72#[cfg_attr(any(test, debug_assertions), derive(Clone, PartialEq))]
73pub struct StateDiff<T> {
74    pub(crate) applier_version: semver::Version,
75    pub(crate) seqno_from: SeqNo,
76    pub(crate) seqno_to: SeqNo,
77    pub(crate) walltime_ms: u64,
78    pub(crate) latest_rollup_key: PartialRollupKey,
79    pub(crate) rollups: Vec<StateFieldDiff<SeqNo, HollowRollup>>,
80    pub(crate) active_rollup: Vec<StateFieldDiff<(), ActiveRollup>>,
81    pub(crate) active_gc: Vec<StateFieldDiff<(), ActiveGc>>,
82    pub(crate) hostname: Vec<StateFieldDiff<(), String>>,
83    pub(crate) last_gc_req: Vec<StateFieldDiff<(), SeqNo>>,
84    pub(crate) leased_readers: Vec<StateFieldDiff<LeasedReaderId, LeasedReaderState<T>>>,
85    pub(crate) critical_readers: Vec<StateFieldDiff<CriticalReaderId, CriticalReaderState<T>>>,
86    pub(crate) writers: Vec<StateFieldDiff<WriterId, WriterState<T>>>,
87    pub(crate) schemas: Vec<StateFieldDiff<SchemaId, EncodedSchemas>>,
88    pub(crate) since: Vec<StateFieldDiff<(), Antichain<T>>>,
89    pub(crate) legacy_batches: Vec<StateFieldDiff<HollowBatch<T>, ()>>,
90    pub(crate) hollow_batches: Vec<StateFieldDiff<SpineId, Arc<HollowBatch<T>>>>,
91    pub(crate) spine_batches: Vec<StateFieldDiff<SpineId, ThinSpineBatch<T>>>,
92    pub(crate) merges: Vec<StateFieldDiff<SpineId, ThinMerge<T>>>,
93}
94
95impl<T: Timestamp + Codec64> StateDiff<T> {
96    pub fn new(
97        applier_version: semver::Version,
98        seqno_from: SeqNo,
99        seqno_to: SeqNo,
100        walltime_ms: u64,
101        latest_rollup_key: PartialRollupKey,
102    ) -> Self {
103        StateDiff {
104            applier_version,
105            seqno_from,
106            seqno_to,
107            walltime_ms,
108            latest_rollup_key,
109            rollups: Vec::default(),
110            active_rollup: Vec::default(),
111            active_gc: Vec::default(),
112            hostname: Vec::default(),
113            last_gc_req: Vec::default(),
114            leased_readers: Vec::default(),
115            critical_readers: Vec::default(),
116            writers: Vec::default(),
117            schemas: Vec::default(),
118            since: Vec::default(),
119            legacy_batches: Vec::default(),
120            hollow_batches: Vec::default(),
121            spine_batches: Vec::default(),
122            merges: Vec::default(),
123        }
124    }
125
126    pub fn referenced_batches(&self) -> impl Iterator<Item = StateFieldValDiff<&HollowBatch<T>>> {
127        let legacy_batches = self
128            .legacy_batches
129            .iter()
130            .filter_map(|diff| match diff.val {
131                Insert(()) => Some(Insert(&diff.key)),
132                Update((), ()) => None, // Ignoring a noop diff.
133                Delete(()) => Some(Delete(&diff.key)),
134            });
135        let hollow_batches = self.hollow_batches.iter().map(|diff| match &diff.val {
136            Insert(batch) => Insert(&**batch),
137            Update(before, after) => Update(&**before, &**after),
138            Delete(batch) => Delete(&**batch),
139        });
140        legacy_batches.chain(hollow_batches)
141    }
142}
143
144impl<T: Timestamp + Lattice + Codec64> StateDiff<T> {
145    pub fn from_diff(from: &State<T>, to: &State<T>) -> Self {
146        // Deconstruct from and to so we get a compile failure if new
147        // fields are added.
148        let State {
149            shard_id: from_shard_id,
150            seqno: from_seqno,
151            hostname: from_hostname,
152            walltime_ms: _, // Intentionally unused
153            collections:
154                StateCollections {
155                    version: _,
156                    last_gc_req: from_last_gc_req,
157                    rollups: from_rollups,
158                    active_rollup: from_active_rollup,
159                    active_gc: from_active_gc,
160                    leased_readers: from_leased_readers,
161                    critical_readers: from_critical_readers,
162                    writers: from_writers,
163                    schemas: from_schemas,
164                    trace: from_trace,
165                },
166        } = from;
167        let State {
168            shard_id: to_shard_id,
169            seqno: to_seqno,
170            walltime_ms: to_walltime_ms,
171            hostname: to_hostname,
172            collections:
173                StateCollections {
174                    version: to_applier_version,
175                    last_gc_req: to_last_gc_req,
176                    rollups: to_rollups,
177                    active_rollup: to_active_rollup,
178                    active_gc: to_active_gc,
179                    leased_readers: to_leased_readers,
180                    critical_readers: to_critical_readers,
181                    writers: to_writers,
182                    schemas: to_schemas,
183                    trace: to_trace,
184                },
185        } = to;
186        assert_eq!(from_shard_id, to_shard_id);
187
188        let (_, latest_rollup) = to.latest_rollup();
189        let mut diffs = Self::new(
190            to_applier_version.clone(),
191            *from_seqno,
192            *to_seqno,
193            *to_walltime_ms,
194            latest_rollup.key.clone(),
195        );
196        diff_field_single(from_hostname, to_hostname, &mut diffs.hostname);
197        diff_field_single(from_last_gc_req, to_last_gc_req, &mut diffs.last_gc_req);
198        diff_field_sorted_iter(
199            from_active_rollup.iter().map(|r| (&(), r)),
200            to_active_rollup.iter().map(|r| (&(), r)),
201            &mut diffs.active_rollup,
202        );
203        diff_field_sorted_iter(
204            from_active_gc.iter().map(|g| (&(), g)),
205            to_active_gc.iter().map(|g| (&(), g)),
206            &mut diffs.active_gc,
207        );
208        diff_field_sorted_iter(from_rollups.iter(), to_rollups, &mut diffs.rollups);
209        diff_field_sorted_iter(
210            from_leased_readers.iter(),
211            to_leased_readers,
212            &mut diffs.leased_readers,
213        );
214        diff_field_sorted_iter(
215            from_critical_readers.iter(),
216            to_critical_readers,
217            &mut diffs.critical_readers,
218        );
219        diff_field_sorted_iter(from_writers.iter(), to_writers, &mut diffs.writers);
220        diff_field_sorted_iter(from_schemas.iter(), to_schemas, &mut diffs.schemas);
221        diff_field_single(from_trace.since(), to_trace.since(), &mut diffs.since);
222
223        let from_flat = from_trace.flatten();
224        let to_flat = to_trace.flatten();
225        diff_field_sorted_iter(
226            from_flat.legacy_batches.iter().map(|(k, v)| (&**k, v)),
227            to_flat.legacy_batches.iter().map(|(k, v)| (&**k, v)),
228            &mut diffs.legacy_batches,
229        );
230        diff_field_sorted_iter(
231            from_flat.hollow_batches.iter(),
232            to_flat.hollow_batches.iter(),
233            &mut diffs.hollow_batches,
234        );
235        diff_field_sorted_iter(
236            from_flat.spine_batches.iter(),
237            to_flat.spine_batches.iter(),
238            &mut diffs.spine_batches,
239        );
240        diff_field_sorted_iter(
241            from_flat.merges.iter(),
242            to_flat.merges.iter(),
243            &mut diffs.merges,
244        );
245        diffs
246    }
247
248    pub(crate) fn blob_inserts(&self) -> impl Iterator<Item = HollowBlobRef<'_, T>> {
249        let batches = self
250            .referenced_batches()
251            .filter_map(|spine_diff| match spine_diff {
252                Insert(b) | Update(_, b) => Some(HollowBlobRef::Batch(b)),
253                Delete(_) => None, // No-op
254            });
255        let rollups = self
256            .rollups
257            .iter()
258            .filter_map(|rollups_diff| match &rollups_diff.val {
259                StateFieldValDiff::Insert(x) | StateFieldValDiff::Update(_, x) => {
260                    Some(HollowBlobRef::Rollup(x))
261                }
262                StateFieldValDiff::Delete(_) => None, // No-op
263            });
264        batches.chain(rollups)
265    }
266
267    pub(crate) fn part_deletes(&self) -> impl Iterator<Item = &RunPart<T>> {
268        // With the introduction of incremental compaction, we
269        // need to be more careful about what we consider "deleted".
270        // If there is a HollowBatch that we replace 2 out of the 4 runs of,
271        // we need to ensure that we only delete the runs that are actually
272        // no longer referenced.
273        let removed = self
274            .referenced_batches()
275            .filter_map(|spine_diff| match spine_diff {
276                Insert(_) => None,
277                Update(a, _) | Delete(a) => Some(a.parts.iter().collect::<Vec<_>>()),
278            });
279
280        let added: std::collections::BTreeSet<_> = self
281            .referenced_batches()
282            .filter_map(|spine_diff| match spine_diff {
283                Insert(a) | Update(_, a) => Some(a.parts.iter().collect::<Vec<_>>()),
284                Delete(_) => None,
285            })
286            .flatten()
287            .collect();
288
289        removed
290            .into_iter()
291            .flat_map(|x| x)
292            .filter(move |part| !added.contains(part))
293    }
294
295    pub(crate) fn rollup_deletes(&self) -> impl Iterator<Item = &HollowRollup> {
296        self.rollups
297            .iter()
298            .filter_map(|rollups_diff| match &rollups_diff.val {
299                Insert(_) => None,
300                Update(a, _) | Delete(a) => Some(a),
301            })
302    }
303
304    #[cfg(any(test, debug_assertions))]
305    #[allow(dead_code)]
306    pub fn validate_roundtrip<K, V, D>(
307        metrics: &Metrics,
308        from_state: &crate::internal::state::TypedState<K, V, T, D>,
309        diff: &Self,
310        to_state: &crate::internal::state::TypedState<K, V, T, D>,
311    ) -> Result<(), String>
312    where
313        K: mz_persist_types::Codec + std::fmt::Debug,
314        V: mz_persist_types::Codec + std::fmt::Debug,
315        D: differential_dataflow::difference::Monoid + Codec64,
316    {
317        use mz_proto::RustType;
318        use prost::Message;
319
320        use crate::internal::state::ProtoStateDiff;
321
322        let mut roundtrip_state = from_state.clone(from_state.hostname.clone());
323        roundtrip_state.apply_diff(metrics, diff.clone())?;
324
325        if &roundtrip_state != to_state {
326            // The weird spacing in this format string is so they all line up
327            // when printed out.
328            return Err(format!(
329                "state didn't roundtrip\n  from_state {:?}\n  to_state   {:?}\n  rt_state   {:?}\n  diff       {:?}\n",
330                from_state, to_state, roundtrip_state, diff
331            ));
332        }
333
334        let encoded_diff = diff.into_proto().encode_to_vec();
335        let roundtrip_diff = Self::from_proto(
336            ProtoStateDiff::decode(encoded_diff.as_slice()).map_err(|err| err.to_string())?,
337        )
338        .map_err(|err| err.to_string())?;
339
340        if &roundtrip_diff != diff {
341            // The weird spacing in this format string is so they all line up
342            // when printed out.
343            return Err(format!(
344                "diff didn't roundtrip\n  diff    {:?}\n  rt_diff {:?}",
345                diff, roundtrip_diff
346            ));
347        }
348
349        Ok(())
350    }
351}
352
353impl<T: Timestamp + Lattice + Codec64> State<T> {
354    pub fn apply_encoded_diffs<'a, I: IntoIterator<Item = &'a VersionedData>>(
355        &mut self,
356        cfg: &PersistConfig,
357        metrics: &Metrics,
358        diffs: I,
359    ) {
360        let mut state_seqno = self.seqno;
361        let diffs = diffs.into_iter().filter_map(move |x| {
362            if x.seqno != state_seqno.next() {
363                // No-op.
364                return None;
365            }
366            let data = x.data.clone();
367            let diff = metrics
368                .codecs
369                .state_diff
370                // Note: `x.data` is a `Bytes`, so cloning just increments a ref count
371                .decode(|| StateDiff::decode(&cfg.build_version, x.data.clone()));
372            assert_eq!(diff.seqno_from, state_seqno);
373            state_seqno = diff.seqno_to;
374            Some((diff, data))
375        });
376        self.apply_diffs(metrics, diffs);
377    }
378}
379
380impl<T: Timestamp + Lattice + Codec64> State<T> {
381    pub fn apply_diffs<I: IntoIterator<Item = (StateDiff<T>, Bytes)>>(
382        &mut self,
383        metrics: &Metrics,
384        diffs: I,
385    ) {
386        for (diff, data) in diffs {
387            // TODO: This could special-case batch apply for diffs where it's
388            // more efficient (in particular, spine batches that hit the slow
389            // path).
390            match self.apply_diff(metrics, diff) {
391                Ok(()) => {}
392                Err(err) => {
393                    // Having the full diff in the error message is critical for debugging any
394                    // issues that may arise from diff application. We pass along the original
395                    // Bytes it decoded from just so we can decode in this error path, while
396                    // avoiding any extraneous clones in the expected Ok path.
397                    // FIXME: this passes the state version but the method requires the build version.
398                    let diff = StateDiff::<T>::decode(&self.collections.version, data);
399                    panic!(
400                        "state diff should apply cleanly: {} diff {:?} state {:?}",
401                        err, diff, self
402                    )
403                }
404            }
405        }
406    }
407
408    // Intentionally not even pub(crate) because all callers should use
409    // [Self::apply_diffs].
410    pub(super) fn apply_diff(
411        &mut self,
412        metrics: &Metrics,
413        diff: StateDiff<T>,
414    ) -> Result<(), String> {
415        // Deconstruct diff so we get a compile failure if new fields are added.
416        let StateDiff {
417            applier_version: diff_applier_version,
418            seqno_from: diff_seqno_from,
419            seqno_to: diff_seqno_to,
420            walltime_ms: diff_walltime_ms,
421            latest_rollup_key: _,
422            rollups: diff_rollups,
423            active_rollup: diff_active_rollup,
424            active_gc: diff_active_gc,
425            hostname: diff_hostname,
426            last_gc_req: diff_last_gc_req,
427            leased_readers: diff_leased_readers,
428            critical_readers: diff_critical_readers,
429            writers: diff_writers,
430            schemas: diff_schemas,
431            since: diff_since,
432            legacy_batches: diff_legacy_batches,
433            hollow_batches: diff_hollow_batches,
434            spine_batches: diff_spine_batches,
435            merges: diff_merges,
436        } = diff;
437        if self.seqno == diff_seqno_to {
438            return Ok(());
439        }
440        if self.seqno != diff_seqno_from {
441            return Err(format!(
442                "could not apply diff {} -> {} to state {}",
443                diff_seqno_from, diff_seqno_to, self.seqno
444            ));
445        }
446        self.seqno = diff_seqno_to;
447        self.walltime_ms = diff_walltime_ms;
448        force_apply_diffs_single(
449            &self.shard_id,
450            diff_seqno_to,
451            "hostname",
452            diff_hostname,
453            &mut self.hostname,
454            metrics,
455        )?;
456
457        // Deconstruct collections so we get a compile failure if new fields are
458        // added.
459        let StateCollections {
460            version,
461            last_gc_req,
462            rollups,
463            active_rollup,
464            active_gc,
465            leased_readers,
466            critical_readers,
467            writers,
468            schemas,
469            trace,
470        } = &mut self.collections;
471
472        *version = diff_applier_version;
473        apply_diffs_map("rollups", diff_rollups, rollups)?;
474        apply_diffs_single("last_gc_req", diff_last_gc_req, last_gc_req)?;
475        apply_diffs_single_option("active_rollup", diff_active_rollup, active_rollup)?;
476        apply_diffs_single_option("active_gc", diff_active_gc, active_gc)?;
477        apply_diffs_map("leased_readers", diff_leased_readers, leased_readers)?;
478        apply_diffs_map("critical_readers", diff_critical_readers, critical_readers)?;
479        apply_diffs_map("writers", diff_writers, writers)?;
480        apply_diffs_map("schemas", diff_schemas, schemas)?;
481
482        let structure_unchanged = diff_hollow_batches.is_empty()
483            && diff_spine_batches.is_empty()
484            && diff_merges.is_empty();
485        let spine_unchanged =
486            diff_since.is_empty() && diff_legacy_batches.is_empty() && structure_unchanged;
487
488        if spine_unchanged {
489            return Ok(());
490        }
491
492        let mut flat = if trace.roundtrip_structure {
493            metrics.state.apply_spine_flattened.inc();
494            let mut flat = trace.flatten();
495            apply_diffs_single("since", diff_since, &mut flat.since)?;
496            apply_diffs_map(
497                "legacy_batches",
498                diff_legacy_batches
499                    .into_iter()
500                    .map(|StateFieldDiff { key, val }| StateFieldDiff {
501                        key: Arc::new(key),
502                        val,
503                    }),
504                &mut flat.legacy_batches,
505            )?;
506            Some(flat)
507        } else {
508            for x in diff_since {
509                match x.val {
510                    Update(from, to) => {
511                        if trace.since() != &from {
512                            return Err(format!(
513                                "since update didn't match: {:?} vs {:?}",
514                                self.collections.trace.since(),
515                                from
516                            ));
517                        }
518                        trace.downgrade_since(&to);
519                    }
520                    Insert(_) => return Err("cannot insert since field".to_string()),
521                    Delete(_) => return Err("cannot delete since field".to_string()),
522                }
523            }
524            if !diff_legacy_batches.is_empty() {
525                apply_diffs_spine(metrics, diff_legacy_batches, trace)?;
526                debug_assert_eq!(trace.validate(), Ok(()), "{:?}", trace);
527            }
528            None
529        };
530
531        if !structure_unchanged {
532            let flat = flat.get_or_insert_with(|| trace.flatten());
533            apply_diffs_map(
534                "hollow_batches",
535                diff_hollow_batches,
536                &mut flat.hollow_batches,
537            )?;
538            apply_diffs_map("spine_batches", diff_spine_batches, &mut flat.spine_batches)?;
539            apply_diffs_map("merges", diff_merges, &mut flat.merges)?;
540        }
541
542        if let Some(flat) = flat {
543            *trace = Trace::unflatten(flat)?;
544        }
545
546        // There's various sanity checks that this method could run (e.g. since,
547        // upper, seqno_since, etc don't regress or that diff.latest_rollup ==
548        // state.rollups.last()), are they a good idea? On one hand, I like
549        // sanity checks, other the other, one of the goals here is to keep
550        // apply logic as straightforward and unchanging as possible.
551        Ok(())
552    }
553}
554
555fn diff_field_single<T: PartialEq + Clone>(
556    from: &T,
557    to: &T,
558    diffs: &mut Vec<StateFieldDiff<(), T>>,
559) {
560    // This could use the `diff_field_sorted_iter(once(from), once(to), diffs)`
561    // general impl, but we just do the obvious thing.
562    if from != to {
563        diffs.push(StateFieldDiff {
564            key: (),
565            val: Update(from.clone(), to.clone()),
566        })
567    }
568}
569
570fn apply_diffs_single_option<X: PartialEq + Debug>(
571    name: &str,
572    diffs: Vec<StateFieldDiff<(), X>>,
573    single: &mut Option<X>,
574) -> Result<(), String> {
575    for diff in diffs {
576        apply_diff_single_option(name, diff, single)?;
577    }
578    Ok(())
579}
580
581fn apply_diff_single_option<X: PartialEq + Debug>(
582    name: &str,
583    diff: StateFieldDiff<(), X>,
584    single: &mut Option<X>,
585) -> Result<(), String> {
586    match diff.val {
587        Update(from, to) => {
588            if single.as_ref() != Some(&from) {
589                return Err(format!(
590                    "{} update didn't match: {:?} vs {:?}",
591                    name, single, from
592                ));
593            }
594            *single = Some(to)
595        }
596        Insert(to) => {
597            if single.is_some() {
598                return Err(format!("{} insert found existing value", name));
599            }
600            *single = Some(to)
601        }
602        Delete(from) => {
603            if single.as_ref() != Some(&from) {
604                return Err(format!(
605                    "{} delete didn't match: {:?} vs {:?}",
606                    name, single, from
607                ));
608            }
609            *single = None
610        }
611    }
612    Ok(())
613}
614
615fn apply_diffs_single<X: PartialEq + Debug>(
616    name: &str,
617    diffs: Vec<StateFieldDiff<(), X>>,
618    single: &mut X,
619) -> Result<(), String> {
620    for diff in diffs {
621        apply_diff_single(name, diff, single)?;
622    }
623    Ok(())
624}
625
626fn apply_diff_single<X: PartialEq + Debug>(
627    name: &str,
628    diff: StateFieldDiff<(), X>,
629    single: &mut X,
630) -> Result<(), String> {
631    match diff.val {
632        Update(from, to) => {
633            if single != &from {
634                return Err(format!(
635                    "{} update didn't match: {:?} vs {:?}",
636                    name, single, from
637                ));
638            }
639            *single = to
640        }
641        Insert(_) => return Err(format!("cannot insert {} field", name)),
642        Delete(_) => return Err(format!("cannot delete {} field", name)),
643    }
644    Ok(())
645}
646
647// A hack to force apply a diff, making `single` equal to
648// the Update `to` value, ignoring a mismatch on `from`.
649// Used to migrate forward after writing down incorrect
650// diffs.
651//
652// TODO: delete this once `hostname` has zero mismatches
653fn force_apply_diffs_single<X: PartialEq + Debug>(
654    shard_id: &ShardId,
655    seqno: SeqNo,
656    name: &str,
657    diffs: Vec<StateFieldDiff<(), X>>,
658    single: &mut X,
659    metrics: &Metrics,
660) -> Result<(), String> {
661    for diff in diffs {
662        force_apply_diff_single(shard_id, seqno, name, diff, single, metrics)?;
663    }
664    Ok(())
665}
666
667fn force_apply_diff_single<X: PartialEq + Debug>(
668    shard_id: &ShardId,
669    seqno: SeqNo,
670    name: &str,
671    diff: StateFieldDiff<(), X>,
672    single: &mut X,
673    metrics: &Metrics,
674) -> Result<(), String> {
675    match diff.val {
676        Update(from, to) => {
677            if single != &from {
678                debug!(
679                    "{}: update didn't match: {:?} vs {:?}, continuing to force apply diff to {:?} for shard {} and seqno {}",
680                    name, single, &from, &to, shard_id, seqno
681                );
682                metrics.state.force_apply_hostname.inc();
683            }
684            *single = to
685        }
686        Insert(_) => return Err(format!("cannot insert {} field", name)),
687        Delete(_) => return Err(format!("cannot delete {} field", name)),
688    }
689    Ok(())
690}
691
692fn diff_field_sorted_iter<'a, K, V, IF, IT>(from: IF, to: IT, diffs: &mut Vec<StateFieldDiff<K, V>>)
693where
694    K: Ord + Clone + 'a,
695    V: PartialEq + Clone + 'a,
696    IF: IntoIterator<Item = (&'a K, &'a V)>,
697    IT: IntoIterator<Item = (&'a K, &'a V)>,
698{
699    let (mut from, mut to) = (from.into_iter(), to.into_iter());
700    let (mut f, mut t) = (from.next(), to.next());
701    loop {
702        match (f, t) {
703            (None, None) => break,
704            (Some((fk, fv)), Some((tk, tv))) => match fk.cmp(tk) {
705                Ordering::Less => {
706                    diffs.push(StateFieldDiff {
707                        key: fk.clone(),
708                        val: Delete(fv.clone()),
709                    });
710                    let f_next = from.next();
711                    mz_ore::soft_assert_no_log!(
712                        f_next.as_ref().map_or(true, |(fk_next, _)| fk_next > &fk)
713                    );
714                    f = f_next;
715                }
716                Ordering::Greater => {
717                    diffs.push(StateFieldDiff {
718                        key: tk.clone(),
719                        val: Insert(tv.clone()),
720                    });
721                    let t_next = to.next();
722                    mz_ore::soft_assert_no_log!(
723                        t_next.as_ref().map_or(true, |(tk_next, _)| tk_next > &tk)
724                    );
725                    t = t_next;
726                }
727                Ordering::Equal => {
728                    // TODO: regression test for this if, I missed it in the
729                    // original impl :)
730                    if fv != tv {
731                        diffs.push(StateFieldDiff {
732                            key: fk.clone(),
733                            val: Update(fv.clone(), tv.clone()),
734                        });
735                    }
736                    let f_next = from.next();
737                    mz_ore::soft_assert_no_log!(
738                        f_next.as_ref().map_or(true, |(fk_next, _)| fk_next > &fk)
739                    );
740                    f = f_next;
741                    let t_next = to.next();
742                    mz_ore::soft_assert_no_log!(
743                        t_next.as_ref().map_or(true, |(tk_next, _)| tk_next > &tk)
744                    );
745                    t = t_next;
746                }
747            },
748            (None, Some((tk, tv))) => {
749                diffs.push(StateFieldDiff {
750                    key: tk.clone(),
751                    val: Insert(tv.clone()),
752                });
753                let t_next = to.next();
754                mz_ore::soft_assert_no_log!(
755                    t_next.as_ref().map_or(true, |(tk_next, _)| tk_next > &tk)
756                );
757                t = t_next;
758            }
759            (Some((fk, fv)), None) => {
760                diffs.push(StateFieldDiff {
761                    key: fk.clone(),
762                    val: Delete(fv.clone()),
763                });
764                let f_next = from.next();
765                mz_ore::soft_assert_no_log!(
766                    f_next.as_ref().map_or(true, |(fk_next, _)| fk_next > &fk)
767                );
768                f = f_next;
769            }
770        }
771    }
772}
773
774fn apply_diffs_map<K: Ord, V: PartialEq + Debug>(
775    name: &str,
776    diffs: impl IntoIterator<Item = StateFieldDiff<K, V>>,
777    map: &mut BTreeMap<K, V>,
778) -> Result<(), String> {
779    for diff in diffs {
780        apply_diff_map(name, diff, map)?;
781    }
782    Ok(())
783}
784
785// This might leave state in an invalid (umm) state when returning an error. The
786// caller ultimately ends up panic'ing on error, but if that changes, we might
787// want to revisit this.
788fn apply_diff_map<K: Ord, V: PartialEq + Debug>(
789    name: &str,
790    diff: StateFieldDiff<K, V>,
791    map: &mut BTreeMap<K, V>,
792) -> Result<(), String> {
793    match diff.val {
794        Insert(to) => {
795            let prev = map.insert(diff.key, to);
796            if prev != None {
797                return Err(format!("{} insert found existing value: {:?}", name, prev));
798            }
799        }
800        Update(from, to) => {
801            let prev = map.insert(diff.key, to);
802            if prev.as_ref() != Some(&from) {
803                return Err(format!(
804                    "{} update didn't match: {:?} vs {:?}",
805                    name,
806                    prev,
807                    Some(from),
808                ));
809            }
810        }
811        Delete(from) => {
812            let prev = map.remove(&diff.key);
813            if prev.as_ref() != Some(&from) {
814                return Err(format!(
815                    "{} delete didn't match: {:?} vs {:?}",
816                    name,
817                    prev,
818                    Some(from),
819                ));
820            }
821        }
822    };
823    Ok(())
824}
825
826// This might leave state in an invalid (umm) state when returning an error. The
827// caller ultimately ends up panic'ing on error, but if that changes, we might
828// want to revisit this.
829fn apply_diffs_spine<T: Timestamp + Lattice + Codec64>(
830    metrics: &Metrics,
831    mut diffs: Vec<StateFieldDiff<HollowBatch<T>, ()>>,
832    trace: &mut Trace<T>,
833) -> Result<(), String> {
834    // Another special case: sniff out a newly inserted batch (one whose lower
835    // lines up with the current upper) and handle that now. Then fall through
836    // to the rest of the handling on whatever is left.
837    if let Some(insert) = sniff_insert(&mut diffs, trace.upper()) {
838        // Ignore merge_reqs because whichever process generated this diff is
839        // assigned the work.
840        let () = trace.push_batch_no_merge_reqs(insert);
841        // If this insert was the only thing in diffs, then return now instead
842        // of falling through to the "no diffs" case in the match so we can inc
843        // the apply_spine_fast_path metric.
844        if diffs.is_empty() {
845            metrics.state.apply_spine_fast_path.inc();
846            return Ok(());
847        }
848    }
849
850    match &diffs[..] {
851        // Fast-path: no diffs.
852        [] => return Ok(()),
853
854        // Fast-path: batch insert with both new and most recent batch empty.
855        // Spine will happily merge these empty batches together without a call
856        // out to compaction.
857        [
858            StateFieldDiff {
859                key: del,
860                val: StateFieldValDiff::Delete(()),
861            },
862            StateFieldDiff {
863                key: ins,
864                val: StateFieldValDiff::Insert(()),
865            },
866        ] => {
867            if del.is_empty()
868                && ins.is_empty()
869                && del.desc.lower() == ins.desc.lower()
870                && PartialOrder::less_than(del.desc.upper(), ins.desc.upper())
871            {
872                // Ignore merge_reqs because whichever process generated this diff is
873                // assigned the work.
874                let () = trace.push_batch_no_merge_reqs(HollowBatch::empty(Description::new(
875                    del.desc.upper().clone(),
876                    ins.desc.upper().clone(),
877                    // `keys.len() == 0` for both `del` and `ins` means we
878                    // don't have to think about what the compaction
879                    // frontier is for these batches (nothing in them, so nothing could have been compacted.
880                    Antichain::from_elem(T::minimum()),
881                )));
882                metrics.state.apply_spine_fast_path.inc();
883                return Ok(());
884            }
885        }
886        // Fall-through
887        _ => {}
888    }
889
890    // Fast-path: compaction
891    if let Some((_inputs, output)) = sniff_compaction(&diffs) {
892        let res = FueledMergeRes {
893            output,
894            input: CompactionInput::Legacy,
895            new_active_compaction: None,
896        };
897        // We can't predict how spine will arrange the batches when it's
898        // hydrated. This means that something that is maintaining a Spine
899        // starting at some seqno may not exactly match something else
900        // maintaining the same spine starting at a different seqno. (Plus,
901        // maybe these aren't even on the same version of the code and we've
902        // changed the spine logic.) Because apply_merge_res is strict,
903        // we're not _guaranteed_ that we can apply a compaction response
904        // that was generated elsewhere. Most of the time we can, though, so
905        // count the good ones and fall back to the slow path below when we
906        // can't.
907        if trace.apply_merge_res_unchecked(&res).applied() {
908            // Maybe return the replaced batches from apply_merge_res and verify
909            // that they match _inputs?
910            metrics.state.apply_spine_fast_path.inc();
911            return Ok(());
912        }
913
914        // Otherwise, try our lenient application of a compaction result.
915        let mut batches = Vec::new();
916        trace.map_batches(|b| batches.push(b.clone()));
917
918        match apply_compaction_lenient(metrics, batches, &res.output) {
919            Ok(batches) => {
920                let mut new_trace = Trace::default();
921                new_trace.roundtrip_structure = trace.roundtrip_structure;
922                new_trace.downgrade_since(trace.since());
923                for batch in batches {
924                    // Ignore merge_reqs because whichever process generated
925                    // this diff is assigned the work.
926                    let () = new_trace.push_batch_no_merge_reqs(batch.clone());
927                }
928                *trace = new_trace;
929                metrics.state.apply_spine_slow_path_lenient.inc();
930                return Ok(());
931            }
932            Err(err) => {
933                return Err(format!(
934                    "lenient compaction result apply unexpectedly failed: {}",
935                    err
936                ));
937            }
938        }
939    }
940
941    // Something complicated is going on, so reconstruct the Trace from scratch.
942    metrics.state.apply_spine_slow_path.inc();
943    debug!(
944        "apply_diffs_spine didn't hit a fast-path diffs={:?} trace={:?}",
945        diffs, trace
946    );
947
948    let batches = {
949        let mut batches = BTreeMap::new();
950        trace.map_batches(|b| assert_none!(batches.insert(b.clone(), ())));
951        apply_diffs_map("spine", diffs.clone(), &mut batches).map(|_ok| batches)
952    };
953
954    let batches = match batches {
955        Ok(batches) => batches,
956        Err(err) => {
957            metrics
958                .state
959                .apply_spine_slow_path_with_reconstruction
960                .inc();
961            debug!(
962                "apply_diffs_spines could not apply diffs directly to existing trace batches: {}. diffs={:?} trace={:?}",
963                err, diffs, trace
964            );
965            // if we couldn't apply our diffs directly to our trace's batches, we can
966            // try one more trick: reconstruct a new spine with our existing batches,
967            // in an attempt to create different merges than we currently have. then,
968            // we can try to apply our diffs on top of these new (potentially) merged
969            // batches.
970            let mut reconstructed_spine = Trace::default();
971            reconstructed_spine.roundtrip_structure = trace.roundtrip_structure;
972            trace.map_batches(|b| {
973                // Ignore merge_reqs because whichever process generated this
974                // diff is assigned the work.
975                let () = reconstructed_spine.push_batch_no_merge_reqs(b.clone());
976            });
977
978            let mut batches = BTreeMap::new();
979            reconstructed_spine.map_batches(|b| assert_none!(batches.insert(b.clone(), ())));
980            apply_diffs_map("spine", diffs, &mut batches)?;
981            batches
982        }
983    };
984
985    let mut new_trace = Trace::default();
986    new_trace.roundtrip_structure = trace.roundtrip_structure;
987    new_trace.downgrade_since(trace.since());
988    for (batch, ()) in batches {
989        // Ignore merge_reqs because whichever process generated this diff is
990        // assigned the work.
991        let () = new_trace.push_batch_no_merge_reqs(batch);
992    }
993    *trace = new_trace;
994    Ok(())
995}
996
997fn sniff_insert<T: Timestamp + Lattice>(
998    diffs: &mut Vec<StateFieldDiff<HollowBatch<T>, ()>>,
999    upper: &Antichain<T>,
1000) -> Option<HollowBatch<T>> {
1001    for idx in 0..diffs.len() {
1002        match &diffs[idx] {
1003            StateFieldDiff {
1004                key,
1005                val: StateFieldValDiff::Insert(()),
1006            } if key.desc.lower() == upper => return Some(diffs.remove(idx).key),
1007            _ => continue,
1008        }
1009    }
1010    None
1011}
1012
1013// TODO: Instead of trying to sniff out a compaction from diffs, should we just
1014// be explicit?
1015fn sniff_compaction<'a, T: Timestamp + Lattice>(
1016    diffs: &'a [StateFieldDiff<HollowBatch<T>, ()>],
1017) -> Option<(Vec<&'a HollowBatch<T>>, HollowBatch<T>)> {
1018    // Compaction always produces exactly one output batch (with possibly many
1019    // parts, but we get one Insert for the whole batch.
1020    let mut inserts = diffs.iter().flat_map(|x| match x.val {
1021        StateFieldValDiff::Insert(()) => Some(&x.key),
1022        _ => None,
1023    });
1024    let compaction_output = match inserts.next() {
1025        Some(x) => x,
1026        None => return None,
1027    };
1028    if let Some(_) = inserts.next() {
1029        return None;
1030    }
1031
1032    // Grab all deletes and sanity check that there are no updates.
1033    let mut compaction_inputs = Vec::with_capacity(diffs.len() - 1);
1034    for diff in diffs.iter() {
1035        match diff.val {
1036            StateFieldValDiff::Delete(()) => {
1037                compaction_inputs.push(&diff.key);
1038            }
1039            StateFieldValDiff::Insert(()) => {}
1040            StateFieldValDiff::Update((), ()) => {
1041                // Fall through to let the general case create the error
1042                // message.
1043                return None;
1044            }
1045        }
1046    }
1047
1048    Some((compaction_inputs, compaction_output.clone()))
1049}
1050
1051/// Apply a compaction diff that doesn't exactly line up with the set of
1052/// HollowBatches.
1053///
1054/// Because of the way Spine internally optimizes only _some_ empty batches
1055/// (immediately merges them in), we can end up in a situation where a
1056/// compaction res applied on another copy of state, but when we replay all of
1057/// the state diffs against a new Spine locally, it merges empty batches
1058/// differently in-mem and we can't exactly apply the compaction diff. Example:
1059///
1060/// - compact: [1,2),[2,3) -> [1,3)
1061/// - this spine: [0,2),[2,3) (0,1 is empty)
1062///
1063/// Ideally, we'd figure out a way to avoid this, but nothing immediately comes
1064/// to mind. In the meantime, force the application (otherwise the shard is
1065/// stuck and we can't do anything with it) by manually splitting the empty
1066/// batch back out. For the example above:
1067///
1068/// - [0,1),[1,3) (0,1 is empty)
1069///
1070/// This can only happen when the batch needing to be split is empty, so error
1071/// out if it isn't because that means something unexpected is going on.
1072fn apply_compaction_lenient<'a, T: Timestamp + Lattice>(
1073    metrics: &Metrics,
1074    mut trace: Vec<HollowBatch<T>>,
1075    replacement: &'a HollowBatch<T>,
1076) -> Result<Vec<HollowBatch<T>>, String> {
1077    let mut overlapping_batches = Vec::new();
1078    trace.retain(|b| {
1079        let before_replacement = PartialOrder::less_equal(b.desc.upper(), replacement.desc.lower());
1080        let after_replacement = PartialOrder::less_equal(replacement.desc.upper(), b.desc.lower());
1081        let overlaps_replacement = !(before_replacement || after_replacement);
1082        if overlaps_replacement {
1083            overlapping_batches.push(b.clone());
1084            false
1085        } else {
1086            true
1087        }
1088    });
1089
1090    {
1091        let first_overlapping_batch = match overlapping_batches.first() {
1092            Some(x) => x,
1093            None => return Err("replacement didn't overlap any batches".into()),
1094        };
1095        if PartialOrder::less_than(
1096            first_overlapping_batch.desc.lower(),
1097            replacement.desc.lower(),
1098        ) {
1099            if first_overlapping_batch.len > 0 {
1100                return Err(format!(
1101                    "overlapping batch was unexpectedly non-empty: {:?}",
1102                    first_overlapping_batch
1103                ));
1104            }
1105            let desc = Description::new(
1106                first_overlapping_batch.desc.lower().clone(),
1107                replacement.desc.lower().clone(),
1108                first_overlapping_batch.desc.since().clone(),
1109            );
1110            trace.push(HollowBatch::empty(desc));
1111            metrics.state.apply_spine_slow_path_lenient_adjustment.inc();
1112        }
1113    }
1114
1115    {
1116        let last_overlapping_batch = match overlapping_batches.last() {
1117            Some(x) => x,
1118            None => return Err("replacement didn't overlap any batches".into()),
1119        };
1120        if PartialOrder::less_than(
1121            replacement.desc.upper(),
1122            last_overlapping_batch.desc.upper(),
1123        ) {
1124            if last_overlapping_batch.len > 0 {
1125                return Err(format!(
1126                    "overlapping batch was unexpectedly non-empty: {:?}",
1127                    last_overlapping_batch
1128                ));
1129            }
1130            let desc = Description::new(
1131                replacement.desc.upper().clone(),
1132                last_overlapping_batch.desc.upper().clone(),
1133                last_overlapping_batch.desc.since().clone(),
1134            );
1135            trace.push(HollowBatch::empty(desc));
1136            metrics.state.apply_spine_slow_path_lenient_adjustment.inc();
1137        }
1138    }
1139    trace.push(replacement.clone());
1140
1141    // We just inserted stuff at the end, so re-sort them into place.
1142    trace.sort_by(|a, b| a.desc.lower().elements().cmp(b.desc.lower().elements()));
1143
1144    // This impl is a touch complex, so sanity check our work.
1145    let mut expected_lower = &Antichain::from_elem(T::minimum());
1146    for b in trace.iter() {
1147        if b.desc.lower() != expected_lower {
1148            return Err(format!(
1149                "lower {:?} did not match expected {:?}: {:?}",
1150                b.desc.lower(),
1151                expected_lower,
1152                trace
1153            ));
1154        }
1155        expected_lower = b.desc.upper();
1156    }
1157    Ok(trace)
1158}
1159
1160/// A type that facilitates the proto encoding of a [`ProtoStateFieldDiffs`]
1161///
1162/// [`ProtoStateFieldDiffs`] is a columnar encoding of [`StateFieldDiff`]s, see
1163/// its doc comment for more info. The underlying buffer for a [`ProtoStateFieldDiffs`]
1164/// is a [`Bytes`] struct, which is an immutable, shared, reference counted,
1165/// buffer of data. Using a [`Bytes`] struct is a very efficient way to manage data
1166/// becuase multiple [`Bytes`] can reference different parts of the same underlying
1167/// portion of memory. See its doc comment for more info.
1168///
1169/// A [`ProtoStateFieldDiffsWriter`] maintains a mutable, unique, data buffer, i.e.
1170/// a [`BytesMut`], which we use when encoding a [`StateFieldDiff`]. And when
1171/// finished encoding, we convert it into a [`ProtoStateFieldDiffs`] by "freezing" the
1172/// underlying buffer, converting it into a [`Bytes`] struct, so it can be shared.
1173///
1174/// [`Bytes`]: bytes::Bytes
1175#[derive(Debug)]
1176pub struct ProtoStateFieldDiffsWriter {
1177    data_buf: BytesMut,
1178    proto: ProtoStateFieldDiffs,
1179}
1180
1181impl ProtoStateFieldDiffsWriter {
1182    /// Record a [`ProtoStateField`] for our columnar encoding.
1183    pub fn push_field(&mut self, field: ProtoStateField) {
1184        self.proto.fields.push(i32::from(field));
1185    }
1186
1187    /// Record a [`ProtoStateFieldDiffType`] for our columnar encoding.
1188    pub fn push_diff_type(&mut self, diff_type: ProtoStateFieldDiffType) {
1189        self.proto.diff_types.push(i32::from(diff_type));
1190    }
1191
1192    /// Encode a message for our columnar encoding.
1193    pub fn encode_proto<M: prost::Message>(&mut self, msg: &M) {
1194        let len_before = self.data_buf.len();
1195        self.data_buf.reserve(msg.encoded_len());
1196
1197        // Note: we use `encode_raw` as opposed to `encode` because all `encode` does is
1198        // check to make sure there's enough bytes in the buffer to fit our message
1199        // which we know there are because we just reserved the space. When benchmarking
1200        // `encode_raw` does offer a slight performance improvement over `encode`.
1201        msg.encode_raw(&mut self.data_buf);
1202
1203        // Record exactly how many bytes were written.
1204        let written_len = self.data_buf.len() - len_before;
1205        self.proto.data_lens.push(u64::cast_from(written_len));
1206    }
1207
1208    pub fn into_proto(self) -> ProtoStateFieldDiffs {
1209        let ProtoStateFieldDiffsWriter {
1210            data_buf,
1211            mut proto,
1212        } = self;
1213
1214        // Assert we didn't write into the proto's data_bytes field
1215        assert!(proto.data_bytes.is_empty());
1216
1217        // Move our buffer into the proto
1218        let data_bytes = data_buf.freeze();
1219        proto.data_bytes = data_bytes;
1220
1221        proto
1222    }
1223}
1224
1225impl ProtoStateFieldDiffs {
1226    pub fn into_writer(mut self) -> ProtoStateFieldDiffsWriter {
1227        // Create a new buffer which we'll encode data into.
1228        let mut data_buf = BytesMut::with_capacity(self.data_bytes.len());
1229
1230        // Take our existing data, and copy it into our buffer.
1231        let existing_data = std::mem::take(&mut self.data_bytes);
1232        data_buf.extend(existing_data);
1233
1234        ProtoStateFieldDiffsWriter {
1235            data_buf,
1236            proto: self,
1237        }
1238    }
1239
1240    pub fn iter<'a>(&'a self) -> ProtoStateFieldDiffsIter<'a> {
1241        let len = self.fields.len();
1242        assert_eq!(self.diff_types.len(), len);
1243
1244        ProtoStateFieldDiffsIter {
1245            len,
1246            diff_idx: 0,
1247            data_idx: 0,
1248            data_offset: 0,
1249            diffs: self,
1250        }
1251    }
1252
1253    pub fn validate(&self) -> Result<(), String> {
1254        if self.fields.len() != self.diff_types.len() {
1255            return Err(format!(
1256                "fields {} and diff_types {} lengths disagree",
1257                self.fields.len(),
1258                self.diff_types.len()
1259            ));
1260        }
1261
1262        let mut expected_data_slices = 0;
1263        for diff_type in self.diff_types.iter() {
1264            // We expect one for the key.
1265            expected_data_slices += 1;
1266            // And 1 or 2 for val depending on the diff type.
1267            match ProtoStateFieldDiffType::try_from(*diff_type) {
1268                Ok(ProtoStateFieldDiffType::Insert) => expected_data_slices += 1,
1269                Ok(ProtoStateFieldDiffType::Update) => expected_data_slices += 2,
1270                Ok(ProtoStateFieldDiffType::Delete) => expected_data_slices += 1,
1271                Err(_) => return Err(format!("unknown diff_type {}", diff_type)),
1272            }
1273        }
1274        if expected_data_slices != self.data_lens.len() {
1275            return Err(format!(
1276                "expected {} data slices got {}",
1277                expected_data_slices,
1278                self.data_lens.len()
1279            ));
1280        }
1281
1282        // NOTE: A crafted diff can declare lengths whose sum exceeds `u64::MAX`.
1283        // Overflow checks are off in release/optimized builds, so an unchecked
1284        // sum would wrap to a small value, match `data_bytes.len()`, and let the
1285        // diff past validation. `ProtoStateFieldDiffsIter` would then slice
1286        // `data_bytes` far out of range and panic.
1287        let Some(expected_data_bytes) = self
1288            .data_lens
1289            .iter()
1290            .copied()
1291            .try_fold(0u64, |acc, len| acc.checked_add(len))
1292            .and_then(|sum| usize::try_from(sum).ok())
1293        else {
1294            return Err(format!(
1295                "data_lens sum overflows, got {} lens over {} data bytes",
1296                self.data_lens.len(),
1297                self.data_bytes.len()
1298            ));
1299        };
1300        if expected_data_bytes != self.data_bytes.len() {
1301            return Err(format!(
1302                "expected {} data bytes got {}",
1303                expected_data_bytes,
1304                self.data_bytes.len()
1305            ));
1306        }
1307
1308        Ok(())
1309    }
1310}
1311
1312#[derive(Debug)]
1313pub struct ProtoStateFieldDiff<'a> {
1314    pub key: &'a [u8],
1315    pub diff_type: ProtoStateFieldDiffType,
1316    pub from: &'a [u8],
1317    pub to: &'a [u8],
1318}
1319
1320pub struct ProtoStateFieldDiffsIter<'a> {
1321    len: usize,
1322    diff_idx: usize,
1323    data_idx: usize,
1324    data_offset: usize,
1325    diffs: &'a ProtoStateFieldDiffs,
1326}
1327
1328impl<'a> Iterator for ProtoStateFieldDiffsIter<'a> {
1329    type Item = Result<(ProtoStateField, ProtoStateFieldDiff<'a>), TryFromProtoError>;
1330
1331    fn next(&mut self) -> Option<Self::Item> {
1332        if self.diff_idx >= self.len {
1333            return None;
1334        }
1335        let mut next_data = || {
1336            let start = self.data_offset;
1337            let end = start + usize::cast_from(self.diffs.data_lens[self.data_idx]);
1338            let data = &self.diffs.data_bytes[start..end];
1339            self.data_idx += 1;
1340            self.data_offset = end;
1341            data
1342        };
1343        let field = match ProtoStateField::try_from(self.diffs.fields[self.diff_idx]) {
1344            Ok(x) => x,
1345            Err(_) => {
1346                return Some(Err(TryFromProtoError::unknown_enum_variant(format!(
1347                    "ProtoStateField({})",
1348                    self.diffs.fields[self.diff_idx]
1349                ))));
1350            }
1351        };
1352        let diff_type =
1353            match ProtoStateFieldDiffType::try_from(self.diffs.diff_types[self.diff_idx]) {
1354                Ok(x) => x,
1355                Err(_) => {
1356                    return Some(Err(TryFromProtoError::unknown_enum_variant(format!(
1357                        "ProtoStateFieldDiffType({})",
1358                        self.diffs.diff_types[self.diff_idx]
1359                    ))));
1360                }
1361            };
1362        let key = next_data();
1363        let (from, to): (&[u8], &[u8]) = match diff_type {
1364            ProtoStateFieldDiffType::Insert => (&[], next_data()),
1365            ProtoStateFieldDiffType::Update => (next_data(), next_data()),
1366            ProtoStateFieldDiffType::Delete => (next_data(), &[]),
1367        };
1368        let diff = ProtoStateFieldDiff {
1369            key,
1370            diff_type,
1371            from,
1372            to,
1373        };
1374        self.diff_idx += 1;
1375        Some(Ok((field, diff)))
1376    }
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381    use semver::Version;
1382    use std::ops::ControlFlow::Continue;
1383
1384    use crate::internal::paths::{PartId, PartialBatchKey, RollupId, WriterKey};
1385    use mz_ore::metrics::MetricsRegistry;
1386
1387    use crate::ShardId;
1388    use crate::internal::state::TypedState;
1389
1390    use super::*;
1391
1392    #[mz_ore::test]
1393    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function on OS `linux`
1394    fn proto_state_diff_invalid_field_diffs_is_error() {
1395        // A `ProtoStateDiff` decoded from an untrusted blob whose field diffs
1396        // fail `validate()` must convert to an error, not panic. We previously
1397        // `debug_assert`ed validity, which panicked under debug assertions /
1398        // fuzzing. Regression for the state_diff_proto_roundtrip cargo-fuzz
1399        // finding.
1400        use crate::internal::state::ProtoStateDiff;
1401        use mz_proto::ProtoType;
1402        use prost::Message;
1403
1404        let bytes: &[u8] = &[0x2a, 0x04, 0x08, 0x00, 0x68, 0x00, 0x40, 0x48];
1405        let proto = ProtoStateDiff::decode(bytes).expect("crash input decodes as a proto");
1406        let result: Result<StateDiff<u64>, _> = proto.into_rust();
1407        assert!(
1408            result.is_err(),
1409            "invalid field diffs must be a decode error"
1410        );
1411    }
1412
1413    #[mz_ore::test]
1414    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function on OS `linux`
1415    fn proto_state_diff_data_lens_overflow_is_error() {
1416        // `data_lens` entries that sum past `u64::MAX` are the one shape that can
1417        // get *past* `validate()`: with overflow checks off the sum wraps to 0,
1418        // matching an empty `data_bytes`, and the iterator then slices
1419        // `0..u64::MAX` out of an empty slice.
1420        use crate::internal::state::ProtoStateDiff;
1421        use mz_proto::ProtoType;
1422
1423        let proto = ProtoStateDiff {
1424            applier_version: "0.1.2".into(),
1425            seqno_from: 0,
1426            seqno_to: 1,
1427            walltime_ms: 0,
1428            latest_rollup_key: "rollup".into(),
1429            field_diffs: Some(ProtoStateFieldDiffs {
1430                fields: vec![ProtoStateField::Hostname.into()],
1431                // Insert expects two data slices: the key and the new value.
1432                diff_types: vec![ProtoStateFieldDiffType::Insert.into()],
1433                data_lens: vec![u64::MAX, 1],
1434                data_bytes: Bytes::new(),
1435            }),
1436        };
1437        let result: Result<StateDiff<u64>, _> = proto.into_rust();
1438        assert!(
1439            result.is_err(),
1440            "data_lens that overflow must be a decode error"
1441        );
1442    }
1443
1444    /// Model a situation where a "leader" is constantly making changes to its state, and a "follower"
1445    /// is applying those changes as diffs.
1446    #[mz_ore::test]
1447    #[cfg_attr(miri, ignore)] // too slow
1448    fn test_state_sync() {
1449        use proptest::prelude::*;
1450
1451        #[derive(Debug, Clone)]
1452        enum Action {
1453            /// Append a (non)empty batch to the shard that covers the given length of time.
1454            Append { empty: bool, time_delta: u64 },
1455            /// Apply the Nth compaction request we've received to the shard state.
1456            Compact { req: usize },
1457        }
1458
1459        let action_gen: BoxedStrategy<Action> = {
1460            prop::strategy::Union::new([
1461                (any::<bool>(), 1u64..10u64)
1462                    .prop_map(|(empty, time_delta)| Action::Append { empty, time_delta })
1463                    .boxed(),
1464                (0usize..10usize)
1465                    .prop_map(|req| Action::Compact { req })
1466                    .boxed(),
1467            ])
1468            .boxed()
1469        };
1470
1471        fn run(actions: Vec<(Action, bool)>, metrics: &Metrics) {
1472            let version = Version::new(0, 100, 0);
1473            let writer_key = WriterKey::Version(version.to_string());
1474            let id = ShardId::new();
1475            let hostname = "computer";
1476            let typed: TypedState<String, (), u64, i64> =
1477                TypedState::new(version, id, hostname.to_string(), 0);
1478            let mut leader = typed.state;
1479
1480            let seqno = SeqNo::minimum();
1481            let mut lower = 0u64;
1482            let mut merge_reqs = vec![];
1483
1484            leader.collections.rollups.insert(
1485                seqno,
1486                HollowRollup {
1487                    key: PartialRollupKey::new(seqno, &RollupId::new()),
1488                    encoded_size_bytes: None,
1489                },
1490            );
1491            leader.collections.trace.roundtrip_structure = false;
1492            let mut follower = leader.clone();
1493
1494            for (action, roundtrip_structure) in actions {
1495                // Apply the given action and the new roundtrip_structure setting and take a diff.
1496                let mut old_leader = leader.clone();
1497                match action {
1498                    Action::Append { empty, time_delta } => {
1499                        let upper = lower + time_delta;
1500                        let key = if empty {
1501                            None
1502                        } else {
1503                            let id = PartId::new();
1504                            Some(PartialBatchKey::new(&writer_key, &id))
1505                        };
1506
1507                        let keys = key.as_ref().map(|k| k.0.as_str());
1508                        let reqs = leader.collections.trace.push_batch(
1509                            crate::internal::state::tests::hollow(
1510                                lower,
1511                                upper,
1512                                keys.as_slice(),
1513                                if empty { 0 } else { 1 },
1514                            ),
1515                        );
1516                        merge_reqs.extend(reqs);
1517                        lower = upper;
1518                    }
1519                    Action::Compact { req } => {
1520                        if !merge_reqs.is_empty() {
1521                            let req = merge_reqs.remove(req.min(merge_reqs.len() - 1));
1522                            let len = req.inputs.iter().map(|p| p.batch.len).sum();
1523                            let parts = req
1524                                .inputs
1525                                .into_iter()
1526                                .flat_map(|p| p.batch.parts.clone())
1527                                .collect();
1528                            let output = HollowBatch::new_run(req.desc, parts, len);
1529                            leader
1530                                .collections
1531                                .trace
1532                                .apply_merge_res_unchecked(&FueledMergeRes {
1533                                    output,
1534                                    input: CompactionInput::Legacy,
1535                                    new_active_compaction: None,
1536                                });
1537                        }
1538                    }
1539                }
1540                leader.collections.trace.roundtrip_structure = roundtrip_structure;
1541                leader.seqno.0 += 1;
1542                let diff = StateDiff::from_diff(&old_leader, &leader);
1543
1544                // Validate that the diff applies to both the previous state (also checked in
1545                // debug asserts) and our follower that's only synchronized via diffs.
1546                old_leader
1547                    .apply_diff(metrics, diff.clone())
1548                    .expect("diff applies to the old version of the leader state");
1549                follower
1550                    .apply_diff(metrics, diff.clone())
1551                    .expect("diff applies to the synced version of the follower state");
1552
1553                // TODO: once spine structure is roundtripped through diffs, assert that the follower
1554                // has the same batches etc. as the leader does.
1555            }
1556        }
1557
1558        let config = PersistConfig::new_for_tests();
1559        let metrics_registry = MetricsRegistry::new();
1560        let metrics: Metrics = Metrics::new(&config, &metrics_registry);
1561
1562        proptest!(|(actions in prop::collection::vec((action_gen, any::<bool>()), 1..20))| {
1563            run(actions, &metrics)
1564        })
1565    }
1566
1567    // Regression test for the apply_diffs_spine special case that sniffs out an
1568    // insert, applies it, and then lets the remaining diffs (if any) fall
1569    // through to the rest of the code. See database-issues#4431.
1570    #[mz_ore::test]
1571    fn regression_15493_sniff_insert() {
1572        fn hb(lower: u64, upper: u64, len: usize) -> HollowBatch<u64> {
1573            HollowBatch::new_run(
1574                Description::new(
1575                    Antichain::from_elem(lower),
1576                    Antichain::from_elem(upper),
1577                    Antichain::from_elem(0),
1578                ),
1579                Vec::new(),
1580                len,
1581            )
1582        }
1583
1584        // The bug handled here is essentially a set of batches that look like
1585        // the pattern matched by `apply_lenient` _plus_ an insert. In
1586        // apply_diffs_spine, we use `sniff_insert` to steal the insert out of
1587        // the diffs and fall back to the rest of the logic to handle the
1588        // remaining diffs.
1589        //
1590        // Concretely, something like (the numbers are truncated versions of the
1591        // actual bug posted in the issue):
1592        // - spine: [0][7094664]0, [7094664][7185234]100
1593        // - diffs: [0][6805359]0 del, [6805359][7083793]0 del, [0][7083793]0 ins,
1594        //   [7185234][7185859]20 ins
1595        //
1596        // Where this allows us to handle the [7185234,7185859) and then
1597        // apply_lenient handles splitting up [0,7094664) so we can apply the
1598        // [0,6805359)+[6805359,7083793)->[0,7083793) swap.
1599
1600        let batches_before = [hb(0, 7094664, 0), hb(7094664, 7185234, 100)];
1601
1602        let diffs = vec![
1603            StateFieldDiff {
1604                key: hb(0, 6805359, 0),
1605                val: StateFieldValDiff::Delete(()),
1606            },
1607            StateFieldDiff {
1608                key: hb(6805359, 7083793, 0),
1609                val: StateFieldValDiff::Delete(()),
1610            },
1611            StateFieldDiff {
1612                key: hb(0, 7083793, 0),
1613                val: StateFieldValDiff::Insert(()),
1614            },
1615            StateFieldDiff {
1616                key: hb(7185234, 7185859, 20),
1617                val: StateFieldValDiff::Insert(()),
1618            },
1619        ];
1620
1621        // Ideally this first batch would be [0][7083793], [7083793,7094664]
1622        // here because `apply_lenient` splits it out, but when `apply_lenient`
1623        // reconstructs the trace, Spine happens to (deterministically) collapse
1624        // them back together. The main value of this test is that the
1625        // `apply_diffs_spine` call below doesn't return an Err, so don't worry
1626        // too much about this, it's just a sanity check.
1627        let batches_after = vec![
1628            hb(0, 7094664, 0),
1629            hb(7094664, 7185234, 100),
1630            hb(7185234, 7185859, 20),
1631        ];
1632
1633        let cfg = PersistConfig::new_for_tests();
1634        let state = TypedState::<(), (), u64, i64>::new(
1635            cfg.build_version.clone(),
1636            ShardId::new(),
1637            cfg.hostname.clone(),
1638            (cfg.now)(),
1639        );
1640        let state = state.clone_apply(&cfg, &mut |_seqno, _cfg, state| {
1641            for b in batches_before.iter() {
1642                let _merge_reqs = state.trace.push_batch(b.clone());
1643            }
1644            Continue::<(), ()>(())
1645        });
1646        let mut state = match state {
1647            Continue((_, x)) => x,
1648            std::ops::ControlFlow::Break(_) => unreachable!(),
1649        };
1650
1651        let metrics = Metrics::new(&PersistConfig::new_for_tests(), &MetricsRegistry::new());
1652        assert_eq!(
1653            apply_diffs_spine(&metrics, diffs, &mut state.collections.trace),
1654            Ok(())
1655        );
1656
1657        let mut actual = Vec::new();
1658        state
1659            .collections
1660            .trace
1661            .map_batches(|b| actual.push(b.clone()));
1662        assert_eq!(actual, batches_after);
1663    }
1664
1665    #[mz_ore::test]
1666    #[cfg_attr(miri, ignore)] // too slow
1667    fn apply_lenient() {
1668        #[track_caller]
1669        fn testcase(
1670            replacement: (u64, u64, u64, usize),
1671            spine: &[(u64, u64, u64, usize)],
1672            expected: Result<&[(u64, u64, u64, usize)], &str>,
1673        ) {
1674            fn batch(x: &(u64, u64, u64, usize)) -> HollowBatch<u64> {
1675                let (lower, upper, since, len) = x;
1676                let desc = Description::new(
1677                    Antichain::from_elem(*lower),
1678                    Antichain::from_elem(*upper),
1679                    Antichain::from_elem(*since),
1680                );
1681                HollowBatch::new_run(desc, Vec::new(), *len)
1682            }
1683            let replacement = batch(&replacement);
1684            let batches = spine.iter().map(batch).collect::<Vec<_>>();
1685
1686            let metrics = Metrics::new(&PersistConfig::new_for_tests(), &MetricsRegistry::new());
1687            let actual = apply_compaction_lenient(&metrics, batches, &replacement);
1688            let expected = match expected {
1689                Ok(batches) => Ok(batches.iter().map(batch).collect::<Vec<_>>()),
1690                Err(err) => Err(err.to_owned()),
1691            };
1692            assert_eq!(actual, expected);
1693        }
1694
1695        // Exact swap of N batches
1696        testcase(
1697            (0, 3, 0, 100),
1698            &[(0, 1, 0, 0), (1, 2, 0, 0), (2, 3, 0, 0)],
1699            Ok(&[(0, 3, 0, 100)]),
1700        );
1701
1702        // Swap out the middle of a batch
1703        testcase(
1704            (1, 2, 0, 100),
1705            &[(0, 3, 0, 0)],
1706            Ok(&[(0, 1, 0, 0), (1, 2, 0, 100), (2, 3, 0, 0)]),
1707        );
1708
1709        // Split batch at replacement lower
1710        testcase(
1711            (2, 4, 0, 100),
1712            &[(0, 3, 0, 0), (3, 4, 0, 0)],
1713            Ok(&[(0, 2, 0, 0), (2, 4, 0, 100)]),
1714        );
1715
1716        // Err: split batch at replacement lower not empty
1717        testcase(
1718            (2, 4, 0, 100),
1719            &[(0, 3, 0, 1), (3, 4, 0, 0)],
1720            Err(
1721                "overlapping batch was unexpectedly non-empty: HollowBatch { desc: ([0], [3], [0]), parts: [], len: 1, runs: [], run_meta: [] }",
1722            ),
1723        );
1724
1725        // Split batch at replacement lower (untouched batch before the split one)
1726        testcase(
1727            (2, 4, 0, 100),
1728            &[(0, 1, 0, 0), (1, 3, 0, 0), (3, 4, 0, 0)],
1729            Ok(&[(0, 1, 0, 0), (1, 2, 0, 0), (2, 4, 0, 100)]),
1730        );
1731
1732        // Split batch at replacement lower (since is preserved)
1733        testcase(
1734            (2, 4, 0, 100),
1735            &[(0, 3, 200, 0), (3, 4, 0, 0)],
1736            Ok(&[(0, 2, 200, 0), (2, 4, 0, 100)]),
1737        );
1738
1739        // Split batch at replacement upper
1740        testcase(
1741            (0, 2, 0, 100),
1742            &[(0, 1, 0, 0), (1, 4, 0, 0)],
1743            Ok(&[(0, 2, 0, 100), (2, 4, 0, 0)]),
1744        );
1745
1746        // Err: split batch at replacement upper not empty
1747        testcase(
1748            (0, 2, 0, 100),
1749            &[(0, 1, 0, 0), (1, 4, 0, 1)],
1750            Err(
1751                "overlapping batch was unexpectedly non-empty: HollowBatch { desc: ([1], [4], [0]), parts: [], len: 1, runs: [], run_meta: [] }",
1752            ),
1753        );
1754
1755        // Split batch at replacement upper (untouched batch after the split one)
1756        testcase(
1757            (0, 2, 0, 100),
1758            &[(0, 1, 0, 0), (1, 3, 0, 0), (3, 4, 0, 0)],
1759            Ok(&[(0, 2, 0, 100), (2, 3, 0, 0), (3, 4, 0, 0)]),
1760        );
1761
1762        // Split batch at replacement upper (since is preserved)
1763        testcase(
1764            (0, 2, 0, 100),
1765            &[(0, 1, 0, 0), (1, 4, 200, 0)],
1766            Ok(&[(0, 2, 0, 100), (2, 4, 200, 0)]),
1767        );
1768
1769        // Split batch at replacement lower and upper
1770        testcase(
1771            (2, 6, 0, 100),
1772            &[(0, 3, 0, 0), (3, 5, 0, 0), (5, 8, 0, 0)],
1773            Ok(&[(0, 2, 0, 0), (2, 6, 0, 100), (6, 8, 0, 0)]),
1774        );
1775
1776        // Replacement doesn't overlap (after)
1777        testcase(
1778            (2, 3, 0, 100),
1779            &[(0, 1, 0, 0)],
1780            Err("replacement didn't overlap any batches"),
1781        );
1782
1783        // Replacement doesn't overlap (before, though this would never happen in practice)
1784        testcase(
1785            (2, 3, 0, 100),
1786            &[(4, 5, 0, 0)],
1787            Err("replacement didn't overlap any batches"),
1788        );
1789    }
1790}