Skip to main content

mz_persist_client/internal/
state_versions.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//! A durable, truncatable log of versions of [State].
11
12#[cfg(debug_assertions)]
13use std::collections::BTreeSet;
14use std::fmt::Debug;
15use std::ops::ControlFlow::{Break, Continue};
16use std::sync::Arc;
17use std::sync::atomic::Ordering;
18use std::time::SystemTime;
19
20use bytes::Bytes;
21use differential_dataflow::difference::Monoid;
22use differential_dataflow::lattice::Lattice;
23use differential_dataflow::trace::Description;
24use mz_ore::cast::CastFrom;
25use mz_persist::location::{
26    Blob, CaSResult, Consensus, Indeterminate, SCAN_ALL, SeqNo, VersionedData,
27};
28use mz_persist::retry::Retry;
29use mz_persist_types::{Codec, Codec64};
30use mz_proto::RustType;
31use prost::Message;
32use timely::progress::Timestamp;
33use tracing::{Instrument, debug, debug_span, trace, warn};
34
35use crate::cfg::STATE_VERSIONS_RECENT_LIVE_DIFFS_LIMIT;
36use crate::error::{CodecMismatch, CodecMismatchT};
37use crate::internal::encoding::{Rollup, UntypedState};
38use crate::internal::machine::{retry_determinate, retry_external};
39use crate::internal::metrics::ShardMetrics;
40use crate::internal::paths::{BlobKey, PartialBlobKey, PartialRollupKey, RollupId};
41#[cfg(debug_assertions)]
42use crate::internal::state::HollowBatch;
43use crate::internal::state::{
44    BatchPart, HollowBlobRef, HollowRollup, NoOpStateTransition, RunPart, State, TypedState,
45};
46use crate::internal::state_diff::{StateDiff, StateFieldValDiff};
47use crate::{Metrics, PersistConfig, ShardId};
48
49/// A durable, truncatable log of versions of [State].
50///
51/// As persist metadata changes over time, we make its versions (each identified
52/// by a [SeqNo]) durable in two ways:
53/// - `rollups`: Periodic copies of the entirety of [State], written to [Blob].
54/// - `diffs`: Incremental [StateDiff]s, written to [Consensus].
55///
56/// The following invariants are maintained at all times:
57/// - A shard is initialized iff there is at least one version of it in
58///   Consensus.
59/// - The first version of state is written to `SeqNo(0)`. Each successive state
60///   version is assigned its predecessor's SeqNo +1.
61/// - `current`: The latest version of state. By definition, the largest SeqNo
62///   present in Consensus.
63/// - As state changes over time, we keep a range of consecutive versions
64///   available. These are periodically `truncated` to prune old versions that
65///   are no longer necessary.
66/// - `earliest`: The first version of state that it is possible to reconstruct.
67///   - Invariant: `earliest <= current.seqno_since()` (we don't garbage collect
68///     versions still being used by some reader).
69///   - Invariant: `earliest` is always the smallest Seqno present in Consensus.
70///     - This doesn't have to be true, but we select to enforce it.
71///     - Because the data stored at that smallest Seqno is an incremental diff,
72///       to make this invariant work, there needs to be a rollup at either
73///       `earliest-1` or `earliest`. We choose `earliest` because it seems to
74///       make the code easier to reason about in practice.
75///     - A consequence of the above is when we garbage collect old versions of
76///       state, we're only free to truncate ones that are `<` the latest rollup
77///       that is `<= current.seqno_since`.
78/// - `live diffs`: The set of SeqNos present in Consensus at any given time.
79/// - `live states`: The range of state versions that it is possible to
80///   reconstruct: `[earliest,current]`.
81///   - Because of earliest and current invariants above, the range of `live
82///     diffs` and `live states` are the same.
83/// - The set of known rollups are tracked in the shard state itself.
84///   - For efficiency of common operations, the most recent rollup's Blob key
85///     is always denormalized in each StateDiff written to Consensus. (As
86///     described above, there is always a rollup at earliest, so we're
87///     guaranteed that there is always at least one live rollup.)
88///   - Invariant: The rollups in `current` exist in Blob.
89///     - A consequence is that, if a rollup in a state you believe is `current`
90///       doesn't exist, it's a guarantee that `current` has changed (or it's a
91///       bug).
92///   - Any rollup at a version `< earliest-1` is useless (we've lost the
93///     incremental diffs between it and the live states). GC is tasked with
94///     deleting these rollups from Blob before truncating diffs from Consensus.
95///     Thus, any rollup at a seqno < earliest can be considered "leaked" and
96///     deleted by the leaked blob detector.
97///   - Note that this means, while `current`'s rollups exist, it will be common
98///     for other live states to reference rollups that no longer exist.
99#[derive(Debug)]
100pub struct StateVersions {
101    pub(crate) cfg: PersistConfig,
102    pub(crate) consensus: Arc<dyn Consensus>,
103    pub(crate) blob: Arc<dyn Blob>,
104    pub(crate) metrics: Arc<Metrics>,
105}
106
107#[derive(Debug, Clone)]
108pub struct RecentLiveDiffs(pub Vec<VersionedData>);
109
110#[derive(Debug, Clone)]
111pub struct EncodedRollup {
112    pub(crate) shard_id: ShardId,
113    pub(crate) seqno: SeqNo,
114    pub(crate) key: PartialRollupKey,
115    pub(crate) _desc: Description<SeqNo>,
116    buf: Bytes,
117}
118
119impl EncodedRollup {
120    pub fn to_hollow(&self) -> HollowRollup {
121        HollowRollup {
122            key: self.key.clone(),
123            encoded_size_bytes: Some(self.buf.len()),
124        }
125    }
126}
127
128impl StateVersions {
129    pub fn new(
130        cfg: PersistConfig,
131        consensus: Arc<dyn Consensus>,
132        blob: Arc<dyn Blob>,
133        metrics: Arc<Metrics>,
134    ) -> Self {
135        StateVersions {
136            cfg,
137            consensus,
138            blob,
139            metrics,
140        }
141    }
142
143    /// Fetches the `current` state of the requested shard, or creates it if
144    /// uninitialized.
145    pub async fn maybe_init_shard<K, V, T, D>(
146        &self,
147        shard_metrics: &ShardMetrics,
148    ) -> Result<TypedState<K, V, T, D>, Box<CodecMismatch>>
149    where
150        K: Debug + Codec,
151        V: Debug + Codec,
152        T: Timestamp + Lattice + Codec64,
153        D: Monoid + Codec64,
154    {
155        let shard_id = shard_metrics.shard_id;
156
157        // The common case is that the shard is initialized, so try that first
158        let recent_live_diffs = self.fetch_recent_live_diffs::<T>(&shard_id).await;
159        if !recent_live_diffs.0.is_empty() {
160            return self
161                .fetch_current_state(&shard_id, recent_live_diffs.0)
162                .await
163                .check_codecs(&shard_id);
164        }
165
166        // Shard is not initialized, try initializing it.
167        let (initial_state, initial_diff) = self.write_initial_rollup(shard_metrics).await;
168        assert_eq!(
169            initial_state.seqno(),
170            SeqNo::minimum(),
171            "initial state should have the initial seqno"
172        );
173        let (cas_res, _diff) =
174            retry_external(&self.metrics.retries.external.maybe_init_cas, || async {
175                self.try_compare_and_set_current(
176                    "maybe_init_shard",
177                    shard_metrics,
178                    &initial_state,
179                    &initial_diff,
180                )
181                .await
182                .map_err(|err| err.into())
183            })
184            .await;
185        match cas_res {
186            CaSResult::Committed => Ok(initial_state),
187            CaSResult::ExpectationMismatch => {
188                let recent_live_diffs = self.fetch_recent_live_diffs::<T>(&shard_id).await;
189                let state = self
190                    .fetch_current_state(&shard_id, recent_live_diffs.0)
191                    .await
192                    .check_codecs(&shard_id);
193
194                // Clean up the rollup blob that we were trying to reference.
195                //
196                // SUBTLE: If we got an Indeterminate error in the CaS above,
197                // but it actually went through, then we'll "contend" with
198                // ourselves and get an expectation mismatch. Use the actual
199                // fetched state to determine if our rollup actually made it in
200                // and decide whether to delete based on that.
201                let (_, rollup) = initial_state.latest_rollup();
202                let should_delete_rollup = match state.as_ref() {
203                    Ok(state) => !state
204                        .collections
205                        .rollups
206                        .values()
207                        .any(|x| &x.key == &rollup.key),
208                    // If the codecs don't match, then we definitely didn't
209                    // write the state.
210                    Err(_codec_mismatch) => true,
211                };
212                if should_delete_rollup {
213                    self.delete_rollup(&shard_id, &rollup.key).await;
214                }
215
216                state
217            }
218        }
219    }
220
221    /// Updates the state of a shard to a new `current` iff `expected` matches
222    /// `current`.
223    ///
224    /// May be called on uninitialized shards.
225    pub async fn try_compare_and_set_current<K, V, T, D>(
226        &self,
227        cmd_name: &str,
228        shard_metrics: &ShardMetrics,
229        new_state: &TypedState<K, V, T, D>,
230        diff: &StateDiff<T>,
231    ) -> Result<(CaSResult, VersionedData), Indeterminate>
232    where
233        K: Debug + Codec,
234        V: Debug + Codec,
235        T: Timestamp + Lattice + Codec64,
236        D: Monoid + Codec64,
237    {
238        assert_eq!(shard_metrics.shard_id, new_state.shard_id);
239        let path = new_state.shard_id.to_string();
240
241        trace!(
242            "apply_unbatched_cmd {} attempting {}\n  new_state={:?}",
243            cmd_name,
244            new_state.seqno(),
245            new_state
246        );
247        let new = self.metrics.codecs.state_diff.encode(|| {
248            let mut buf = Vec::new();
249            diff.encode(&mut buf);
250            VersionedData {
251                seqno: new_state.seqno(),
252                data: Bytes::from(buf),
253            }
254        });
255        assert_eq!(new.seqno, diff.seqno_to);
256
257        let payload_len = new.data.len();
258        let cas_res = retry_determinate(
259            &self.metrics.retries.determinate.apply_unbatched_cmd_cas,
260            || async { self.consensus.compare_and_set(&path, new.clone()).await },
261        )
262        .instrument(debug_span!("apply_unbatched_cmd::cas", payload_len))
263        .await
264        .map_err(|err| {
265            debug!("apply_unbatched_cmd {} errored: {}", cmd_name, err);
266            err
267        })?;
268
269        match cas_res {
270            CaSResult::Committed => {
271                trace!(
272                    "apply_unbatched_cmd {} succeeded {}\n  new_state={:?}",
273                    cmd_name,
274                    new_state.seqno(),
275                    new_state
276                );
277
278                shard_metrics.seqnos_since_last_rollup.set(
279                    new_state
280                        .seqno
281                        .0
282                        .saturating_sub(new_state.latest_rollup().0.0),
283                );
284                shard_metrics
285                    .spine_batch_count
286                    .set(u64::cast_from(new_state.spine_batch_count()));
287                let size_metrics = new_state.size_metrics();
288                shard_metrics
289                    .hollow_batch_count
290                    .set(u64::cast_from(size_metrics.hollow_batch_count));
291                shard_metrics
292                    .batch_part_count
293                    .set(u64::cast_from(size_metrics.batch_part_count));
294                shard_metrics
295                    .update_count
296                    .set(u64::cast_from(size_metrics.num_updates));
297                shard_metrics
298                    .rollup_count
299                    .set(u64::cast_from(size_metrics.state_rollup_count));
300                shard_metrics
301                    .largest_batch_size
302                    .set(u64::cast_from(size_metrics.largest_batch_bytes));
303                shard_metrics
304                    .usage_current_state_batches_bytes
305                    .set(u64::cast_from(size_metrics.state_batches_bytes));
306                shard_metrics
307                    .usage_current_state_rollups_bytes
308                    .set(u64::cast_from(size_metrics.state_rollups_bytes));
309                shard_metrics
310                    .seqnos_held
311                    .set(u64::cast_from(new_state.seqnos_held()));
312                shard_metrics
313                    .encoded_diff_size
314                    .inc_by(u64::cast_from(payload_len));
315                shard_metrics
316                    .inline_part_count
317                    .set(u64::cast_from(size_metrics.inline_part_count));
318                shard_metrics.stale.store(
319                    new_state
320                        .state
321                        .collections
322                        .version
323                        .cmp_precedence(&self.cfg.build_version)
324                        .is_lt(),
325                    Ordering::Relaxed,
326                );
327
328                let spine_metrics = new_state.collections.trace.spine_metrics();
329                shard_metrics
330                    .compact_batches
331                    .set(spine_metrics.compact_batches);
332                shard_metrics
333                    .compacting_batches
334                    .set(spine_metrics.compacting_batches);
335                shard_metrics
336                    .noncompact_batches
337                    .set(spine_metrics.noncompact_batches);
338
339                let batch_parts_by_version = new_state
340                    .collections
341                    .trace
342                    .batches()
343                    .flat_map(|x| x.parts.iter())
344                    .flat_map(|part| {
345                        let key = match part {
346                            RunPart::Many(x) => Some(&x.key),
347                            RunPart::Single(BatchPart::Hollow(x)) => Some(&x.key),
348                            // TODO: Would be nice to include these too, but we lose the info atm.
349                            RunPart::Single(BatchPart::Inline { .. }) => None,
350                        }?;
351                        // Carefully avoid any String allocs by splitting.
352                        let (writer_key, _) = key.0.split_once('/')?;
353                        match &writer_key[..1] {
354                            "w" => Some("old"),
355                            "n" => Some(&writer_key[1..]),
356                            _ => None,
357                        }
358                    });
359                shard_metrics.set_batch_part_versions(batch_parts_by_version);
360
361                Ok((CaSResult::Committed, new))
362            }
363            CaSResult::ExpectationMismatch => {
364                debug!(
365                    "apply_unbatched_cmd {} {} lost the CaS race, retrying: {:?}",
366                    new_state.shard_id(),
367                    cmd_name,
368                    new_state.seqno.previous(),
369                );
370                Ok((CaSResult::ExpectationMismatch, new))
371            }
372        }
373    }
374
375    /// Fetches the `current` state of the requested shard.
376    ///
377    /// Uses the provided hint (live_diffs), which is a possibly outdated
378    /// copy of all or recent live diffs, to avoid fetches where possible.
379    ///
380    /// Panics if called on an uninitialized shard.
381    pub async fn fetch_current_state<T>(
382        &self,
383        shard_id: &ShardId,
384        mut live_diffs: Vec<VersionedData>,
385    ) -> UntypedState<T>
386    where
387        T: Timestamp + Lattice + Codec64,
388    {
389        let retry = self
390            .metrics
391            .retries
392            .fetch_latest_state
393            .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
394        loop {
395            let latest_diff = live_diffs
396                .last()
397                .expect("initialized shard should have at least one diff");
398            let latest_diff = self
399                .metrics
400                .codecs
401                .state_diff
402                // Note: `latest_diff.data` is a `Bytes`, so cloning just increments a ref count
403                .decode(|| {
404                    StateDiff::<T>::decode(&self.cfg.build_version, latest_diff.data.clone())
405                });
406            let mut state = match self
407                .fetch_rollup_at_key(shard_id, &latest_diff.latest_rollup_key)
408                .await
409            {
410                Some(x) => x,
411                None => {
412                    // The rollup that this diff referenced is gone, so the diff
413                    // must be out of date. Try again. Intentionally don't sleep on retry.
414                    retry.retries.inc();
415                    let earliest_before_refetch = live_diffs
416                        .first()
417                        .expect("initialized shard should have at least one diff")
418                        .seqno;
419                    live_diffs = self.fetch_recent_live_diffs::<T>(shard_id).await.0;
420
421                    // We should only hit the race condition that leads to a
422                    // refetch if the set of live diffs changed out from under
423                    // us.
424                    //
425                    // TODO: Make this an assert once we're 100% sure the above
426                    // is always true.
427                    let earliest_after_refetch = live_diffs
428                        .first()
429                        .expect("initialized shard should have at least one diff")
430                        .seqno;
431                    if earliest_before_refetch >= earliest_after_refetch {
432                        warn!(
433                            concat!(
434                                "fetch_current_state refetch expects earliest live diff to advance: {} vs {}. ",
435                                "In dev and testing, this happens when persist's Blob (files in mzdata) ",
436                                "is deleted out from under it or when two processes are talking to ",
437                                "different Blobs (e.g. docker containers without it shared)."
438                            ),
439                            earliest_before_refetch, earliest_after_refetch
440                        )
441                    }
442                    continue;
443                }
444            };
445
446            state.apply_encoded_diffs(&self.cfg, &self.metrics, &live_diffs);
447            return state;
448        }
449    }
450
451    /// Returns an iterator over all live states for the requested shard.
452    ///
453    /// Returns None if called on an uninitialized shard.
454    pub async fn fetch_all_live_states<T>(
455        &self,
456        shard_id: ShardId,
457    ) -> Option<UntypedStateVersionsIter<T>>
458    where
459        T: Timestamp + Lattice + Codec64,
460    {
461        let retry = self
462            .metrics
463            .retries
464            .fetch_live_states
465            .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
466        let mut all_live_diffs = self.fetch_all_live_diffs(&shard_id).await;
467        loop {
468            let earliest_live_diff = match all_live_diffs.first() {
469                Some(x) => x,
470                None => return None,
471            };
472            let state = match self
473                .fetch_rollup_at_seqno(&shard_id, all_live_diffs.clone(), earliest_live_diff.seqno)
474                .await
475            {
476                Some(x) => x,
477                None => {
478                    // We maintain an invariant that a rollup always exists for
479                    // the earliest live diff. Since we didn't find out, that
480                    // can only mean that the live_diffs we just fetched are
481                    // obsolete (there's a race condition with gc). This should
482                    // be rare in practice, so inc a counter and try again.
483                    // Intentionally don't sleep on retry.
484                    retry.retries.inc();
485                    let earliest_before_refetch = earliest_live_diff.seqno;
486                    all_live_diffs = self.fetch_all_live_diffs(&shard_id).await;
487
488                    // We should only hit the race condition that leads to a
489                    // refetch if the set of live diffs changed out from under
490                    // us.
491                    //
492                    // TODO: Make this an assert once we're 100% sure the above
493                    // is always true.
494                    let earliest_after_refetch = all_live_diffs
495                        .first()
496                        .expect("initialized shard should have at least one diff")
497                        .seqno;
498                    if earliest_before_refetch >= earliest_after_refetch {
499                        warn!(
500                            concat!(
501                                "fetch_all_live_states refetch expects earliest live diff to advance: {} vs {}. ",
502                                "In dev and testing, this happens when persist's Blob (files in mzdata) ",
503                                "is deleted out from under it or when two processes are talking to ",
504                                "different Blobs (e.g. docker containers without it shared)."
505                            ),
506                            earliest_before_refetch, earliest_after_refetch
507                        )
508                    }
509                    continue;
510                }
511            };
512            assert_eq!(earliest_live_diff.seqno, state.seqno());
513            return Some(UntypedStateVersionsIter {
514                shard_id,
515                cfg: self.cfg.clone(),
516                metrics: Arc::clone(&self.metrics),
517                state,
518                diffs: all_live_diffs,
519            });
520        }
521    }
522
523    /// Fetches all live_diffs for a shard. Intended only for when a caller needs to reconstruct
524    /// _all_ states still referenced by Consensus. Prefer [Self::fetch_recent_live_diffs] when
525    /// the caller simply needs to fetch the latest state.
526    ///
527    /// Returns an empty Vec iff called on an uninitialized shard.
528    pub async fn fetch_all_live_diffs(&self, shard_id: &ShardId) -> Vec<VersionedData> {
529        let path = shard_id.to_string();
530        let diffs = retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
531            self.consensus.scan(&path, SeqNo::minimum(), SCAN_ALL).await
532        })
533        .instrument(debug_span!("fetch_state::scan"))
534        .await;
535        diffs
536    }
537
538    /// Fetches live diffs for a shard. This is a thin wrapper around [Consensus::scan] with the
539    /// right retry policy and instrumentation.
540    async fn fetch_live_diffs(
541        &self,
542        shard_id: &ShardId,
543        from: SeqNo,
544        limit: usize,
545    ) -> Vec<VersionedData> {
546        let path = shard_id.to_string();
547        retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
548            self.consensus.scan(&path, from, limit).await
549        })
550        .instrument(debug_span!("fetch_state::scan"))
551        .await
552    }
553
554    /// Fetches all live_diffs for a shard up to and including a given threshold, allowing us to
555    /// reconstruct states up to and including that version.
556    pub async fn fetch_live_diffs_through(
557        &self,
558        shard_id: &ShardId,
559        through: SeqNo,
560    ) -> Vec<VersionedData> {
561        // Get an initial set of versions from consensus.
562        let scan_limit = STATE_VERSIONS_RECENT_LIVE_DIFFS_LIMIT.get(&self.cfg);
563        let mut versions = self
564            .fetch_live_diffs(shard_id, SeqNo::minimum(), scan_limit)
565            .await;
566
567        if versions.len() == scan_limit {
568            // Loop until our version range either covers the full set, or we stop getting data.
569            loop {
570                let Some(last_seqno) = versions.last().map(|v| v.seqno) else {
571                    break;
572                };
573                if through <= last_seqno {
574                    break;
575                }
576                let from = last_seqno.next();
577                let limit = usize::cast_from(through.0 - last_seqno.0).clamp(1, 10 * scan_limit);
578                let more_versions = self.fetch_live_diffs(shard_id, from, limit).await;
579                let more_versions_len = more_versions.len();
580                if let Some(first) = more_versions.first() {
581                    assert!(last_seqno < first.seqno);
582                }
583                versions.extend(more_versions);
584                if more_versions_len < limit {
585                    break;
586                }
587            }
588        }
589        // We may have fetched more versions than requested; find the index past the last
590        // requested version and truncate there.
591        let partition_index = versions.partition_point(|v| v.seqno <= through);
592        versions.truncate(partition_index);
593        versions
594    }
595
596    /// Fetches recent live_diffs for a shard. Intended for when a caller needs to fetch
597    /// the latest state in Consensus.
598    ///
599    /// "Recent" is defined as either:
600    /// * All of the diffs known in Consensus
601    /// * All of the diffs in Consensus after the latest rollup
602    pub async fn fetch_recent_live_diffs<T>(&self, shard_id: &ShardId) -> RecentLiveDiffs
603    where
604        T: Timestamp + Lattice + Codec64,
605    {
606        let path = shard_id.to_string();
607        let scan_limit = STATE_VERSIONS_RECENT_LIVE_DIFFS_LIMIT.get(&self.cfg);
608        let oldest_diffs =
609            retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
610                self.consensus
611                    .scan(&path, SeqNo::minimum(), scan_limit)
612                    .await
613            })
614            .instrument(debug_span!("fetch_state::scan"))
615            .await;
616
617        // fast-path: we found all known diffs in a single page of our scan. we expect almost all
618        // calls to go down this path, unless a reader has a very long seqno-hold on the shard.
619        if oldest_diffs.len() < scan_limit {
620            self.metrics.state.fetch_recent_live_diffs_fast_path.inc();
621            return RecentLiveDiffs(oldest_diffs);
622        }
623
624        // slow-path: we could be arbitrarily far behind the head of Consensus (either intentionally
625        // due to a long seqno-hold from a reader, or unintentionally from a bug that's preventing
626        // a seqno-hold from advancing). rather than scanning a potentially unbounded number of old
627        // states in Consensus, we jump to the latest state, determine the seqno of the most recent
628        // rollup, and then fetch all the diffs from that point onward.
629        //
630        // this approach requires more network calls, but it should smooth out our access pattern
631        // and use only bounded calls to Consensus. additionally, if `limit` is adequately tuned,
632        // this path will only be invoked when there's an excess number of states in Consensus and
633        // it might be slower to do a single long scan over unneeded rows.
634        let head = retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
635            self.consensus.head(&path).await
636        })
637        .instrument(debug_span!("fetch_state::slow_path::head"))
638        .await
639        .expect("initialized shard should have at least 1 diff");
640
641        let latest_diff = self
642            .metrics
643            .codecs
644            .state_diff
645            .decode(|| StateDiff::<T>::decode(&self.cfg.build_version, head.data));
646
647        match BlobKey::parse_ids(&latest_diff.latest_rollup_key.complete(shard_id)) {
648            Ok((_shard_id, PartialBlobKey::Rollup(seqno, _rollup))) => {
649                self.metrics.state.fetch_recent_live_diffs_slow_path.inc();
650                let diffs =
651                    retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
652                        // (pedantry) this call is technically unbounded, but something very strange
653                        // would have had to happen to have accumulated so many states between our
654                        // call to `head` and this invocation for it to become problematic
655                        self.consensus.scan(&path, seqno, SCAN_ALL).await
656                    })
657                    .instrument(debug_span!("fetch_state::slow_path::scan"))
658                    .await;
659                RecentLiveDiffs(diffs)
660            }
661            Ok(_) => panic!(
662                "invalid state diff rollup key: {}",
663                latest_diff.latest_rollup_key
664            ),
665            Err(err) => panic!("unparseable state diff rollup key: {}", err),
666        }
667    }
668
669    /// Fetches all live diffs greater than the given SeqNo.
670    ///
671    /// TODO: Apply a limit to this scan. This could additionally be used as an internal
672    /// call within `fetch_recent_live_diffs`.
673    pub async fn fetch_all_live_diffs_gt_seqno<K, V, T, D>(
674        &self,
675        shard_id: &ShardId,
676        seqno: SeqNo,
677    ) -> Vec<VersionedData> {
678        let path = shard_id.to_string();
679        retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
680            self.consensus.scan(&path, seqno.next(), SCAN_ALL).await
681        })
682        .instrument(debug_span!("fetch_state::scan"))
683        .await
684    }
685
686    /// Truncates any diffs in consensus less than the given seqno.
687    pub async fn truncate_diffs(&self, shard_id: &ShardId, seqno: SeqNo) {
688        let path = shard_id.to_string();
689        let _deleted_count = retry_external(&self.metrics.retries.external.gc_truncate, || async {
690            self.consensus.truncate(&path, seqno).await
691        })
692        .instrument(debug_span!("gc::truncate"))
693        .await;
694    }
695
696    // Writes a self-referential rollup to blob storage and returns the diff
697    // that should be compare_and_set into consensus to finish initializing the
698    // shard.
699    async fn write_initial_rollup<K, V, T, D>(
700        &self,
701        shard_metrics: &ShardMetrics,
702    ) -> (TypedState<K, V, T, D>, StateDiff<T>)
703    where
704        K: Debug + Codec,
705        V: Debug + Codec,
706        T: Timestamp + Lattice + Codec64,
707        D: Monoid + Codec64,
708    {
709        let empty_state = TypedState::new(
710            self.cfg.build_version.clone(),
711            shard_metrics.shard_id,
712            self.cfg.hostname.clone(),
713            (self.cfg.now)(),
714        );
715        let mut initial_state = empty_state.clone_for_rollup();
716        let rollup_seqno = initial_state.seqno();
717        let rollup = HollowRollup {
718            key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
719            // Chicken-and-egg problem here. We don't know the size of the
720            // rollup until we encode it, but it includes a reference back to
721            // itself.
722            encoded_size_bytes: None,
723        };
724        let applied = match initial_state
725            .collections
726            .add_rollup((rollup_seqno, &rollup))
727        {
728            Continue(x) => x,
729            Break(NoOpStateTransition(_)) => {
730                panic!("initial state transition should not be a no-op")
731            }
732        };
733        assert!(
734            applied,
735            "add_and_remove_rollups should apply to the empty state"
736        );
737
738        let rollup = self.encode_rollup_blob(
739            shard_metrics,
740            initial_state.clone_for_rollup(),
741            vec![],
742            rollup.key,
743        );
744        let () = self.write_rollup_blob(&rollup).await;
745        assert_eq!(initial_state.seqno, rollup.seqno);
746
747        let diff = StateDiff::from_diff(&empty_state.state, &initial_state.state);
748        (initial_state, diff)
749    }
750
751    pub async fn write_rollup_for_state<K, V, T, D>(
752        &self,
753        shard_metrics: &ShardMetrics,
754        state: TypedState<K, V, T, D>,
755        rollup_id: &RollupId,
756    ) -> Option<EncodedRollup>
757    where
758        K: Debug + Codec,
759        V: Debug + Codec,
760        T: Timestamp + Lattice + Codec64,
761        D: Monoid + Codec64,
762    {
763        let (latest_rollup_seqno, _rollup) = state.latest_rollup();
764        let seqno = state.seqno();
765
766        // TODO: maintain the diffs since the latest rollup in-memory rather than
767        // needing an additional API call here. This would reduce Consensus load
768        // / avoid races with Consensus truncation, but is trickier to write.
769        let diffs: Vec<_> = self
770            .fetch_all_live_diffs_gt_seqno::<K, V, T, D>(&state.shard_id, *latest_rollup_seqno)
771            .await;
772
773        match diffs.first() {
774            None => {
775                // early-out because these are no diffs past our latest rollup.
776                //
777                // this should only occur in the initial state, but we can write a more
778                // general assertion: if no live diffs exist past this state's latest
779                // known rollup, then that rollup must be for the latest known state.
780                self.metrics.state.rollup_write_noop_latest.inc();
781                assert_eq!(seqno, *latest_rollup_seqno);
782                return None;
783            }
784            Some(first) => {
785                // early-out if it is no longer possible to inline all the diffs from
786                // the last known rollup to the current state. some or all of the diffs
787                // have already been truncated by another process.
788                //
789                // this can happen if one process gets told to write a rollup, the
790                // maintenance task falls arbitrarily behind, and another process writes
791                // a new rollup / GCs and truncates past the first process's rollup.
792                self.metrics.state.rollup_write_noop_truncated.inc();
793                if first.seqno != latest_rollup_seqno.next() {
794                    assert!(
795                        first.seqno > latest_rollup_seqno.next(),
796                        "diff: {}, rollup: {}",
797                        first.seqno,
798                        latest_rollup_seqno,
799                    );
800                    return None;
801                }
802            }
803        }
804
805        // we may have fetched more diffs than we need: trim anything beyond the state's seqno
806        let diffs: Vec<_> = diffs.into_iter().filter(|x| x.seqno <= seqno).collect();
807
808        // verify that we've done all the filtering correctly and that our
809        // diffs have seqnos bounded by (last_rollup, current_state]
810        assert_eq!(
811            diffs.first().map(|x| x.seqno),
812            Some(latest_rollup_seqno.next())
813        );
814        assert_eq!(diffs.last().map(|x| x.seqno), Some(state.seqno));
815
816        let key = PartialRollupKey::new(state.seqno, rollup_id);
817        let rollup = self.encode_rollup_blob(shard_metrics, state, diffs, key);
818        let () = self.write_rollup_blob(&rollup).await;
819
820        self.metrics.state.rollup_write_success.inc();
821
822        Some(rollup)
823    }
824
825    /// Encodes the given state and diffs as a rollup to be written to the specified key.
826    ///
827    /// The diffs must span the seqno range `(state.last_rollup().seqno, state.seqno]`.
828    pub fn encode_rollup_blob<K, V, T, D>(
829        &self,
830        shard_metrics: &ShardMetrics,
831        state: TypedState<K, V, T, D>,
832        diffs: Vec<VersionedData>,
833        key: PartialRollupKey,
834    ) -> EncodedRollup
835    where
836        K: Debug + Codec,
837        V: Debug + Codec,
838        T: Timestamp + Lattice + Codec64,
839        D: Monoid + Codec64,
840    {
841        let shard_id = state.shard_id;
842        let rollup_seqno = state.seqno;
843
844        let rollup = Rollup::from(state.into(), diffs);
845        let desc = rollup.diffs.as_ref().expect("inlined diffs").description();
846
847        let buf = self.metrics.codecs.state.encode(|| {
848            let mut buf = Vec::new();
849            rollup
850                .into_proto()
851                .encode(&mut buf)
852                .expect("no required fields means no initialization errors");
853            Bytes::from(buf)
854        });
855        shard_metrics
856            .latest_rollup_size
857            .set(u64::cast_from(buf.len()));
858        EncodedRollup {
859            shard_id,
860            seqno: rollup_seqno,
861            key,
862            buf,
863            _desc: desc,
864        }
865    }
866
867    /// Writes the given state rollup out to blob.
868    pub async fn write_rollup_blob(&self, rollup: &EncodedRollup) {
869        let payload_len = rollup.buf.len();
870        retry_external(&self.metrics.retries.external.rollup_set, || async {
871            self.blob
872                .set(
873                    &rollup.key.complete(&rollup.shard_id),
874                    Bytes::clone(&rollup.buf),
875                )
876                .await
877        })
878        .instrument(debug_span!("rollup::set", payload_len))
879        .await;
880    }
881
882    /// Fetches a rollup for the given SeqNo, if it exists.
883    ///
884    /// Uses the provided hint, which is a possibly outdated copy of all
885    /// or recent live diffs, to avoid fetches where possible.
886    ///
887    /// Panics if called on an uninitialized shard.
888    async fn fetch_rollup_at_seqno<T>(
889        &self,
890        shard_id: &ShardId,
891        live_diffs: Vec<VersionedData>,
892        seqno: SeqNo,
893    ) -> Option<UntypedState<T>>
894    where
895        T: Timestamp + Lattice + Codec64,
896    {
897        let rollup_key_for_migration = live_diffs.iter().find_map(|x| {
898            let diff = self
899                .metrics
900                .codecs
901                .state_diff
902                // Note: `x.data` is a `Bytes`, so cloning just increments a ref count
903                .decode(|| StateDiff::<T>::decode(&self.cfg.build_version, x.data.clone()));
904            diff.rollups
905                .iter()
906                .find(|x| x.key == seqno)
907                .map(|x| match &x.val {
908                    StateFieldValDiff::Insert(x) => x.clone(),
909                    StateFieldValDiff::Update(_, x) => x.clone(),
910                    StateFieldValDiff::Delete(x) => x.clone(),
911                })
912        });
913
914        let state = self.fetch_current_state::<T>(shard_id, live_diffs).await;
915        if let Some(rollup) = state.rollups().get(&seqno) {
916            return self.fetch_rollup_at_key(shard_id, &rollup.key).await;
917        }
918
919        // MIGRATION: We maintain an invariant that the _current state_ contains
920        // a rollup for the _earliest live diff_ in consensus (and that the
921        // referenced rollup exists). At one point, we fixed a bug that could
922        // lead to that invariant being violated.
923        //
924        // If the earliest live diff is X and we receive a gc req for X+Y to
925        // X+Y+Z (this can happen e.g. if some cmd ignores an earlier req for X
926        // to X+Y, or if they're processing concurrently and the X to X+Y req
927        // loses the race), then the buggy version of gc would delete any
928        // rollups strictly less than old_seqno_since (X+Y in this example). But
929        // our invariant is that the rollup exists for the earliest live diff,
930        // in this case X. So if the first call to gc was interrupted after this
931        // but before truncate (when all the blob deletes happen), later calls
932        // to gc would attempt to call `fetch_live_states` and end up infinitely
933        // in its loop.
934        //
935        // The fix was to base which rollups are deleteable on the earliest live
936        // diff, not old_seqno_since.
937        //
938        // Sadly, some envs in prod now violate this invariant. So, even with
939        // the fix, existing shards will never successfully run gc. We add a
940        // temporary migration to fix them in `fetch_rollup_at_seqno`. This
941        // method normally looks in the latest version of state for the
942        // specifically requested seqno. In the invariant violation case, some
943        // version of state in the range `[earliest, current]` has a rollup for
944        // earliest, but current doesn't. So, for the migration, if
945        // fetch_rollup_at_seqno doesn't find a rollup in current, then we fall
946        // back to sniffing one out of raw diffs. If this success, we increment
947        // a counter and log, so we can track how often this migration is
948        // bailing us out. After the next deploy, this should initially start at
949        // > 0 and then settle down to 0. After the next prod envs wipe, we can
950        // remove the migration.
951        let rollup = rollup_key_for_migration.expect("someone should have a key for this rollup");
952        tracing::info!("only found rollup for {} {} via migration", shard_id, seqno);
953        self.metrics.state.rollup_at_seqno_migration.inc();
954        self.fetch_rollup_at_key(shard_id, &rollup.key).await
955    }
956
957    /// Fetches the rollup at the given key, if it exists.
958    pub async fn fetch_rollup_at_key<T>(
959        &self,
960        shard_id: &ShardId,
961        rollup_key: &PartialRollupKey,
962    ) -> Option<UntypedState<T>>
963    where
964        T: Timestamp + Lattice + Codec64,
965    {
966        retry_external(&self.metrics.retries.external.rollup_get, || async {
967            self.blob.get(&rollup_key.complete(shard_id)).await
968        })
969        .instrument(debug_span!("rollup::get"))
970        .await
971        .map(|buf| {
972            self.metrics
973                .codecs
974                .state
975                .decode(|| UntypedState::decode(&self.cfg.build_version, buf))
976        })
977    }
978
979    /// Deletes the rollup at the given key, if it exists.
980    pub async fn delete_rollup(&self, shard_id: &ShardId, key: &PartialRollupKey) {
981        let _ = retry_external(&self.metrics.retries.external.rollup_delete, || async {
982            self.blob.delete(&key.complete(shard_id)).await
983        })
984        .await
985        .instrument(debug_span!("rollup::delete"));
986    }
987}
988
989pub struct UntypedStateVersionsIter<T> {
990    shard_id: ShardId,
991    cfg: PersistConfig,
992    metrics: Arc<Metrics>,
993    state: UntypedState<T>,
994    diffs: Vec<VersionedData>,
995}
996
997impl<T: Timestamp + Lattice + Codec64> UntypedStateVersionsIter<T> {
998    pub(crate) fn new(
999        shard_id: ShardId,
1000        cfg: PersistConfig,
1001        metrics: Arc<Metrics>,
1002        state: UntypedState<T>,
1003        diffs: Vec<VersionedData>,
1004    ) -> Self {
1005        Self {
1006            shard_id,
1007            cfg,
1008            metrics,
1009            state,
1010            diffs,
1011        }
1012    }
1013
1014    pub(crate) fn check_ts_codec(self) -> Result<StateVersionsIter<T>, CodecMismatchT> {
1015        let key_codec = self.state.key_codec.clone();
1016        let val_codec = self.state.val_codec.clone();
1017        let diff_codec = self.state.diff_codec.clone();
1018        let state = self.state.check_ts_codec(&self.shard_id)?;
1019        Ok(StateVersionsIter::new(
1020            self.cfg,
1021            self.metrics,
1022            state,
1023            self.diffs,
1024            key_codec,
1025            val_codec,
1026            diff_codec,
1027        ))
1028    }
1029}
1030
1031/// An iterator over consecutive versions of [State].
1032pub struct StateVersionsIter<T> {
1033    cfg: PersistConfig,
1034    metrics: Arc<Metrics>,
1035    state: State<T>,
1036    diffs: Vec<VersionedData>,
1037    key_codec: String,
1038    val_codec: String,
1039    diff_codec: String,
1040    #[cfg(debug_assertions)]
1041    validator: ReferencedBlobValidator<T>,
1042}
1043
1044impl<T: Timestamp + Lattice + Codec64> StateVersionsIter<T> {
1045    fn new(
1046        cfg: PersistConfig,
1047        metrics: Arc<Metrics>,
1048        state: State<T>,
1049        // diffs is stored reversed so we can efficiently pop off the Vec.
1050        mut diffs: Vec<VersionedData>,
1051        key_codec: String,
1052        val_codec: String,
1053        diff_codec: String,
1054    ) -> Self {
1055        assert!(diffs.first().map_or(true, |x| x.seqno == state.seqno));
1056        diffs.reverse();
1057        StateVersionsIter {
1058            cfg,
1059            metrics,
1060            state,
1061            diffs,
1062            key_codec,
1063            val_codec,
1064            diff_codec,
1065            #[cfg(debug_assertions)]
1066            validator: ReferencedBlobValidator::default(),
1067        }
1068    }
1069
1070    pub fn len(&self) -> usize {
1071        self.diffs.len()
1072    }
1073
1074    /// Advances first to some starting state (in practice, usually the first
1075    /// live state), and then through each successive state, for as many diffs
1076    /// as this iterator was initialized with.
1077    ///
1078    /// The `inspect_diff_fn` callback can be used to inspect diffs directly as
1079    /// they are applied. The first call to `next` returns a
1080    /// [InspectDiff::FromInitial] representing a diff from the initial state.
1081    pub fn next<F: for<'a> FnMut(InspectDiff<'a, T>)>(
1082        &mut self,
1083        mut inspect_diff_fn: F,
1084    ) -> Option<&State<T>> {
1085        let diff = match self.diffs.pop() {
1086            Some(x) => x,
1087            None => return None,
1088        };
1089        let data = diff.data.clone();
1090        let diff = self
1091            .metrics
1092            .codecs
1093            .state_diff
1094            .decode(|| StateDiff::decode(&self.cfg.build_version, diff.data));
1095
1096        // A bit hacky, but the first diff in StateVersionsIter is always a
1097        // no-op.
1098        if diff.seqno_to == self.state.seqno {
1099            let inspect = InspectDiff::FromInitial(&self.state);
1100            #[cfg(debug_assertions)]
1101            {
1102                inspect
1103                    .referenced_blobs()
1104                    .for_each(|x| self.validator.add_inc_blob(x));
1105            }
1106            inspect_diff_fn(inspect);
1107        } else {
1108            let inspect = InspectDiff::Diff(&diff);
1109            #[cfg(debug_assertions)]
1110            {
1111                inspect
1112                    .referenced_blobs()
1113                    .for_each(|x| self.validator.add_inc_blob(x));
1114            }
1115            inspect_diff_fn(inspect);
1116        }
1117
1118        let diff_seqno_to = diff.seqno_to;
1119        self.state
1120            .apply_diffs(&self.metrics, std::iter::once((diff, data)));
1121        assert_eq!(self.state.seqno, diff_seqno_to);
1122        #[cfg(debug_assertions)]
1123        {
1124            self.validator.validate_against_state(&self.state);
1125        }
1126        Some(&self.state)
1127    }
1128
1129    pub fn state(&self) -> &State<T> {
1130        &self.state
1131    }
1132
1133    pub fn into_rollup_proto_without_diffs(&self) -> impl serde::Serialize + use<T> {
1134        Rollup::from_state_without_diffs(
1135            State {
1136                shard_id: self.state.shard_id.clone(),
1137                seqno: self.state.seqno.clone(),
1138                walltime_ms: self.state.walltime_ms.clone(),
1139                hostname: self.state.hostname.clone(),
1140                collections: self.state.collections.clone(),
1141            },
1142            self.key_codec.clone(),
1143            self.val_codec.clone(),
1144            T::codec_name(),
1145            self.diff_codec.clone(),
1146        )
1147        .into_proto()
1148    }
1149}
1150
1151/// This represents a diff, either directly or, in the case of the FromInitial
1152/// variant, a diff from the initial state. (We could instead compute the diff
1153/// from the initial state and replace this with only a `StateDiff<T>`, but don't
1154/// for efficiency.)
1155#[derive(Debug)]
1156pub enum InspectDiff<'a, T> {
1157    FromInitial(&'a State<T>),
1158    Diff(&'a StateDiff<T>),
1159}
1160
1161impl<T: Timestamp + Lattice + Codec64> InspectDiff<'_, T> {
1162    /// A callback invoked for each blob added this state transition.
1163    ///
1164    /// Blob removals, along with all other diffs, are ignored.
1165    pub fn referenced_blobs(&self) -> impl Iterator<Item = HollowBlobRef<'_, T>> {
1166        let (state, diff) = match self {
1167            InspectDiff::FromInitial(x) => (Some(x), None),
1168            InspectDiff::Diff(x) => (None, Some(x)),
1169        };
1170        let state_blobs = state.into_iter().flat_map(|s| s.blobs());
1171        let diff_blobs = diff.into_iter().flat_map(|d| d.blob_inserts());
1172        state_blobs.chain(diff_blobs)
1173    }
1174}
1175
1176#[cfg(debug_assertions)]
1177struct ReferencedBlobValidator<T> {
1178    // A copy of every batch and rollup referenced by some state iterator,
1179    // computed by scanning the full copy of state at each seqno.
1180    full_batches: BTreeSet<HollowBatch<T>>,
1181    full_rollups: BTreeSet<HollowRollup>,
1182    // A copy of every batch and rollup referenced by some state iterator,
1183    // computed incrementally.
1184    inc_batches: BTreeSet<HollowBatch<T>>,
1185    inc_rollups: BTreeSet<HollowRollup>,
1186}
1187
1188#[cfg(debug_assertions)]
1189impl<T> Default for ReferencedBlobValidator<T> {
1190    fn default() -> Self {
1191        Self {
1192            full_batches: Default::default(),
1193            full_rollups: Default::default(),
1194            inc_batches: Default::default(),
1195            inc_rollups: Default::default(),
1196        }
1197    }
1198}
1199
1200#[cfg(debug_assertions)]
1201impl<T: Timestamp + Lattice + Codec64> ReferencedBlobValidator<T> {
1202    fn add_inc_blob(&mut self, x: HollowBlobRef<'_, T>) {
1203        match x {
1204            HollowBlobRef::Batch(x) => assert!(
1205                self.inc_batches.insert(x.clone()) || x.desc.lower() == x.desc.upper(),
1206                "non-empty batches should only be appended once; duplicate: {x:?}"
1207            ),
1208            HollowBlobRef::Rollup(x) => assert!(self.inc_rollups.insert(x.clone())),
1209        }
1210    }
1211    fn validate_against_state(&mut self, x: &State<T>) {
1212        use std::hash::{DefaultHasher, Hash, Hasher};
1213
1214        use mz_ore::collections::HashSet;
1215        use timely::progress::Antichain;
1216
1217        use crate::internal::state::BatchPart;
1218
1219        x.blobs().for_each(|x| match x {
1220            HollowBlobRef::Batch(x) => {
1221                self.full_batches.insert(x.clone());
1222            }
1223            HollowBlobRef::Rollup(x) => {
1224                self.full_rollups.insert(x.clone());
1225            }
1226        });
1227
1228        // Check that the sets of batches overall cover the same pTVC.
1229        // Partial ordering means we can't just take the first and last batches; instead compute
1230        // bounds using the lattice operations.
1231        fn overall_desc<'a, T: Timestamp + Lattice>(
1232            iter: impl Iterator<Item = &'a Description<T>>,
1233        ) -> (Antichain<T>, Antichain<T>) {
1234            let mut lower = Antichain::new();
1235            let mut upper = Antichain::from_elem(T::minimum());
1236            for desc in iter {
1237                lower.meet_assign(desc.lower());
1238                upper.join_assign(desc.upper());
1239            }
1240            (lower, upper)
1241        }
1242        let (inc_lower, inc_upper) = overall_desc(self.inc_batches.iter().map(|a| &a.desc));
1243        let (full_lower, full_upper) = overall_desc(self.full_batches.iter().map(|a| &a.desc));
1244        assert_eq!(inc_lower, full_lower);
1245        assert_eq!(inc_upper, full_upper);
1246
1247        fn part_unique<T: Codec64>(x: &RunPart<T>) -> String {
1248            match x {
1249                RunPart::Single(BatchPart::Inline {
1250                    updates,
1251                    ts_rewrite,
1252                    ..
1253                }) => {
1254                    let mut h = DefaultHasher::new();
1255                    updates.hash(&mut h);
1256                    if let Some(frontier) = &ts_rewrite {
1257                        h.write_usize(frontier.len());
1258                        frontier.iter().for_each(|t| t.encode().hash(&mut h));
1259                    }
1260                    h.finish().to_string()
1261                }
1262                other => other.printable_name().to_string(),
1263            }
1264        }
1265
1266        // Check that the overall set of parts contained in both representations is the same.
1267        let inc_parts: HashSet<_> = self
1268            .inc_batches
1269            .iter()
1270            .flat_map(|x| x.parts.iter())
1271            .map(part_unique)
1272            .collect();
1273        let full_parts = self
1274            .full_batches
1275            .iter()
1276            .flat_map(|x| x.parts.iter())
1277            .map(part_unique)
1278            .collect();
1279        assert_eq!(inc_parts, full_parts);
1280
1281        // Check that both representations have the same rollups.
1282        assert_eq!(self.inc_rollups, self.full_rollups);
1283    }
1284}
1285
1286#[cfg(test)]
1287mod tests {
1288    use mz_dyncfg::ConfigUpdates;
1289
1290    use crate::tests::new_test_client;
1291
1292    use super::*;
1293
1294    /// Regression test for (part of) database-issues#5170, where an interrupted
1295    /// `bin/environmentd --reset` resulted in panic in persist usage code.
1296    #[mz_persist_proc::test(tokio::test)]
1297    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1298    async fn fetch_all_live_states_regression_uninitialized(dyncfgs: ConfigUpdates) {
1299        let client = new_test_client(&dyncfgs).await;
1300        let state_versions = StateVersions::new(
1301            client.cfg.clone(),
1302            Arc::clone(&client.consensus),
1303            Arc::clone(&client.blob),
1304            Arc::clone(&client.metrics),
1305        );
1306        assert!(
1307            state_versions
1308                .fetch_all_live_states::<u64>(ShardId::new())
1309                .await
1310                .is_none()
1311        );
1312    }
1313}