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