Skip to main content

mz_persist_client/internal/
state.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use anyhow::ensure;
11use async_stream::{stream, try_stream};
12use differential_dataflow::difference::Monoid;
13use mz_persist::metrics::ColumnarMetrics;
14use proptest::prelude::{Arbitrary, Strategy};
15use std::borrow::Cow;
16use std::cmp::Ordering;
17use std::collections::BTreeMap;
18use std::fmt::{Debug, Formatter};
19use std::marker::PhantomData;
20use std::ops::ControlFlow::{self, Break, Continue};
21use std::ops::{Deref, DerefMut};
22use std::time::Duration;
23
24use arrow::array::{Array, ArrayData, make_array};
25use arrow::datatypes::DataType;
26use bytes::Bytes;
27use differential_dataflow::Hashable;
28use differential_dataflow::lattice::Lattice;
29use differential_dataflow::trace::Description;
30use differential_dataflow::trace::implementations::BatchContainer;
31use futures::Stream;
32use futures_util::StreamExt;
33use itertools::Itertools;
34use mz_dyncfg::{Config, ParameterScope};
35use mz_ore::cast::CastFrom;
36use mz_ore::now::EpochMillis;
37use mz_ore::soft_panic_or_log;
38use mz_ore::vec::PartialOrdVecExt;
39use mz_persist::indexed::encoding::{BatchColumnarFormat, BlobTraceUpdates};
40use mz_persist::location::{Blob, SeqNo};
41use mz_persist_types::arrow::{ArrayBound, ProtoArrayData};
42use mz_persist_types::columnar::{ColumnEncoder, Schema};
43use mz_persist_types::schema::{SchemaId, backward_compatible};
44use mz_persist_types::{Codec, Codec64};
45use mz_proto::ProtoType;
46use mz_proto::RustType;
47use proptest_derive::Arbitrary;
48use semver::Version;
49use serde::ser::SerializeStruct;
50use serde::{Serialize, Serializer};
51use timely::PartialOrder;
52use timely::order::TotalOrder;
53use timely::progress::{Antichain, Timestamp};
54use tracing::info;
55use uuid::Uuid;
56
57use crate::critical::{CriticalReaderId, Opaque};
58use crate::error::InvalidUsage;
59use crate::internal::encoding::{
60    LazyInlineBatchPart, LazyPartStats, LazyProto, MetadataMap, parse_id,
61};
62use crate::internal::gc::GcReq;
63use crate::internal::machine::retry_external;
64use crate::internal::paths::{BlobKey, PartId, PartialBatchKey, PartialRollupKey, WriterKey};
65use crate::internal::trace::{
66    ActiveCompaction, ApplyMergeResult, FueledMergeReq, FueledMergeRes, Trace,
67};
68use crate::metrics::Metrics;
69use crate::read::LeasedReaderId;
70use crate::schema::CaESchema;
71use crate::write::WriterId;
72use crate::{PersistConfig, ShardId};
73
74include!(concat!(
75    env!("OUT_DIR"),
76    "/mz_persist_client.internal.state.rs"
77));
78
79include!(concat!(
80    env!("OUT_DIR"),
81    "/mz_persist_client.internal.diff.rs"
82));
83
84/// Determines how often to write rollups, assigning a maintenance task after
85/// `rollup_threshold` seqnos have passed since the last rollup.
86///
87/// Tuning note: in the absence of a long reader seqno hold, and with
88/// incremental GC, this threshold will determine about how many live diffs are
89/// held in Consensus. Lowering this value decreases the live diff count at the
90/// cost of more maintenance work + blob writes.
91pub(crate) const ROLLUP_THRESHOLD: Config<usize> = Config::new(
92    "persist_rollup_threshold",
93    128,
94    "The number of seqnos between rollups.",
95    ParameterScope::Environment,
96);
97
98/// Determines how long to wait before an active rollup is considered
99/// "stuck" and a new rollup is started.
100pub(crate) const ROLLUP_FALLBACK_THRESHOLD_MS: Config<usize> = Config::new(
101    "persist_rollup_fallback_threshold_ms",
102    5000,
103    "The number of milliseconds before a worker claims an already claimed rollup.",
104    ParameterScope::Environment,
105);
106
107/// Feature flag the new active rollup tracking mechanism.
108/// We musn't enable this until we are fully deployed on the new version.
109pub(crate) const ROLLUP_USE_ACTIVE_ROLLUP: Config<bool> = Config::new(
110    "persist_rollup_use_active_rollup",
111    true,
112    "Whether to use the new active rollup tracking mechanism.",
113    ParameterScope::Environment,
114);
115
116/// Determines how long to wait before an active GC is considered
117/// "stuck" and a new GC is started.
118pub(crate) const GC_FALLBACK_THRESHOLD_MS: Config<usize> = Config::new(
119    "persist_gc_fallback_threshold_ms",
120    900000,
121    "The number of milliseconds before a worker claims an already claimed GC.",
122    ParameterScope::Environment,
123);
124
125/// See the config description string.
126pub(crate) const GC_MIN_VERSIONS: Config<usize> = Config::new(
127    "persist_gc_min_versions",
128    32,
129    "The number of un-GCd versions that may exist in state before we'll trigger a GC.",
130    ParameterScope::Environment,
131);
132
133/// See the config description string.
134pub(crate) const GC_MAX_VERSIONS: Config<usize> = Config::new(
135    "persist_gc_max_versions",
136    128_000,
137    "The maximum number of versions to GC in a single GC run.",
138    ParameterScope::Environment,
139);
140
141/// Feature flag the new active GC tracking mechanism.
142/// We musn't enable this until we are fully deployed on the new version.
143pub(crate) const GC_USE_ACTIVE_GC: Config<bool> = Config::new(
144    "persist_gc_use_active_gc",
145    false,
146    "Whether to use the new active GC tracking mechanism.",
147    ParameterScope::Environment,
148);
149
150pub(crate) const ENABLE_INCREMENTAL_COMPACTION: Config<bool> = Config::new(
151    "persist_enable_incremental_compaction",
152    false,
153    "Whether to enable incremental compaction.",
154    ParameterScope::Environment,
155);
156
157/// A token to disambiguate state commands that could not otherwise be
158/// idempotent.
159#[derive(Arbitrary, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
160#[serde(into = "String")]
161pub struct IdempotencyToken(pub(crate) [u8; 16]);
162
163impl std::fmt::Display for IdempotencyToken {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        write!(f, "i{}", Uuid::from_bytes(self.0))
166    }
167}
168
169impl std::fmt::Debug for IdempotencyToken {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        write!(f, "IdempotencyToken({})", Uuid::from_bytes(self.0))
172    }
173}
174
175impl std::str::FromStr for IdempotencyToken {
176    type Err = String;
177
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        parse_id("i", "IdempotencyToken", s).map(IdempotencyToken)
180    }
181}
182
183impl From<IdempotencyToken> for String {
184    fn from(x: IdempotencyToken) -> Self {
185        x.to_string()
186    }
187}
188
189impl IdempotencyToken {
190    pub(crate) fn new() -> Self {
191        IdempotencyToken(*Uuid::new_v4().as_bytes())
192    }
193    pub(crate) const SENTINEL: IdempotencyToken = IdempotencyToken([17u8; 16]);
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize)]
197pub struct LeasedReaderState<T> {
198    /// The seqno capability of this reader.
199    pub seqno: SeqNo,
200    /// The since capability of this reader.
201    pub since: Antichain<T>,
202    /// UNIX_EPOCH timestamp (in millis) of this reader's most recent heartbeat
203    pub last_heartbeat_timestamp_ms: u64,
204    /// Duration (in millis) allowed after [Self::last_heartbeat_timestamp_ms]
205    /// after which this reader may be expired
206    pub lease_duration_ms: u64,
207    /// For debugging.
208    pub debug: HandleDebugState,
209}
210
211#[derive(Clone, Debug, PartialEq, Serialize)]
212pub struct CriticalReaderState<T> {
213    /// The since capability of this reader.
214    pub since: Antichain<T>,
215    /// An opaque token matched on by compare_and_downgrade_since.
216    pub opaque: Opaque,
217    /// For debugging.
218    pub debug: HandleDebugState,
219}
220
221#[derive(Clone, Debug, PartialEq, Serialize)]
222pub struct WriterState<T> {
223    /// UNIX_EPOCH timestamp (in millis) of this writer's most recent heartbeat
224    pub last_heartbeat_timestamp_ms: u64,
225    /// Duration (in millis) allowed after [Self::last_heartbeat_timestamp_ms]
226    /// after which this writer may be expired
227    pub lease_duration_ms: u64,
228    /// The idempotency token of the most recent successful compare_and_append
229    /// by this writer.
230    pub most_recent_write_token: IdempotencyToken,
231    /// The upper of the most recent successful compare_and_append by this
232    /// writer.
233    pub most_recent_write_upper: Antichain<T>,
234    /// For debugging.
235    pub debug: HandleDebugState,
236}
237
238/// Debugging info for a reader or writer.
239#[derive(Arbitrary, Clone, Debug, Default, PartialEq, Serialize)]
240pub struct HandleDebugState {
241    /// Hostname of the persist user that registered this writer or reader. For
242    /// critical readers, this is the _most recent_ registration.
243    pub hostname: String,
244    /// Plaintext description of this writer or reader's intent.
245    pub purpose: String,
246}
247
248/// Part of the updates in a Batch.
249///
250/// Either a pointer to ones stored in Blob or the updates themselves inlined.
251#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
252#[serde(tag = "type")]
253pub enum BatchPart<T> {
254    Hollow(HollowBatchPart<T>),
255    Inline {
256        updates: LazyInlineBatchPart,
257        ts_rewrite: Option<Antichain<T>>,
258        schema_id: Option<SchemaId>,
259
260        /// ID of a schema that has since been deprecated and exists only to cleanly roundtrip.
261        deprecated_schema_id: Option<SchemaId>,
262    },
263}
264
265fn decode_structured_lower(lower: &LazyProto<ProtoArrayData>) -> Option<ArrayBound> {
266    let try_decode = |lower: &LazyProto<ProtoArrayData>| {
267        let proto = lower.decode()?;
268        let data = ArrayData::from_proto(proto)?;
269        ensure!(data.len() == 1);
270        Ok(ArrayBound::new(make_array(data), 0))
271    };
272
273    let decoded: anyhow::Result<ArrayBound> = try_decode(lower);
274
275    match decoded {
276        Ok(bound) => Some(bound),
277        Err(e) => {
278            soft_panic_or_log!("failed to decode bound: {e:#?}");
279            None
280        }
281    }
282}
283
284impl<T> BatchPart<T> {
285    pub fn hollow_bytes(&self) -> usize {
286        match self {
287            BatchPart::Hollow(x) => x.encoded_size_bytes,
288            BatchPart::Inline { .. } => 0,
289        }
290    }
291
292    pub fn is_inline(&self) -> bool {
293        matches!(self, BatchPart::Inline { .. })
294    }
295
296    pub fn inline_bytes(&self) -> usize {
297        match self {
298            BatchPart::Hollow(_) => 0,
299            BatchPart::Inline { updates, .. } => updates.encoded_size_bytes(),
300        }
301    }
302
303    pub fn writer_key(&self) -> Option<WriterKey> {
304        match self {
305            BatchPart::Hollow(x) => x.key.split().map(|(writer, _part)| writer),
306            BatchPart::Inline { .. } => None,
307        }
308    }
309
310    pub fn encoded_size_bytes(&self) -> usize {
311        match self {
312            BatchPart::Hollow(x) => x.encoded_size_bytes,
313            BatchPart::Inline { updates, .. } => updates.encoded_size_bytes(),
314        }
315    }
316
317    // A user-interpretable identifier or description of the part (for logs and
318    // such).
319    pub fn printable_name(&self) -> &str {
320        match self {
321            BatchPart::Hollow(x) => x.key.0.as_str(),
322            BatchPart::Inline { .. } => "<inline>",
323        }
324    }
325
326    pub fn stats(&self) -> Option<&LazyPartStats> {
327        match self {
328            BatchPart::Hollow(x) => x.stats.as_ref(),
329            BatchPart::Inline { .. } => None,
330        }
331    }
332
333    pub fn key_lower(&self) -> &[u8] {
334        match self {
335            BatchPart::Hollow(x) => x.key_lower.as_slice(),
336            // We don't duplicate the lowest key because this can be
337            // considerable overhead for small parts.
338            //
339            // The empty key might not be a tight lower bound, but it is a valid
340            // lower bound. If a caller is interested in a tighter lower bound,
341            // the data is inline.
342            BatchPart::Inline { .. } => &[],
343        }
344    }
345
346    pub fn structured_key_lower(&self) -> Option<ArrayBound> {
347        let part = match self {
348            BatchPart::Hollow(part) => part,
349            BatchPart::Inline { .. } => return None,
350        };
351
352        decode_structured_lower(part.structured_key_lower.as_ref()?)
353    }
354
355    pub fn ts_rewrite(&self) -> Option<&Antichain<T>> {
356        match self {
357            BatchPart::Hollow(x) => x.ts_rewrite.as_ref(),
358            BatchPart::Inline { ts_rewrite, .. } => ts_rewrite.as_ref(),
359        }
360    }
361
362    pub fn schema_id(&self) -> Option<SchemaId> {
363        match self {
364            BatchPart::Hollow(x) => x.schema_id,
365            BatchPart::Inline { schema_id, .. } => *schema_id,
366        }
367    }
368
369    pub fn deprecated_schema_id(&self) -> Option<SchemaId> {
370        match self {
371            BatchPart::Hollow(x) => x.deprecated_schema_id,
372            BatchPart::Inline {
373                deprecated_schema_id,
374                ..
375            } => *deprecated_schema_id,
376        }
377    }
378}
379
380impl<T: Timestamp + Codec64> BatchPart<T> {
381    pub fn is_structured_only(&self, metrics: &ColumnarMetrics) -> bool {
382        match self {
383            BatchPart::Hollow(x) => matches!(x.format, Some(BatchColumnarFormat::Structured)),
384            BatchPart::Inline { updates, .. } => {
385                let inline_part = updates.decode::<T>(metrics).expect("valid inline part");
386                matches!(inline_part.updates, BlobTraceUpdates::Structured { .. })
387            }
388        }
389    }
390
391    pub fn diffs_sum<D: Codec64 + Monoid>(&self, metrics: &ColumnarMetrics) -> Option<D> {
392        match self {
393            BatchPart::Hollow(x) => x.diffs_sum.map(D::decode),
394            BatchPart::Inline { updates, .. } => Some(
395                updates
396                    .decode::<T>(metrics)
397                    .expect("valid inline part")
398                    .updates
399                    .diffs_sum(),
400            ),
401        }
402    }
403}
404
405/// An ordered list of parts, generally stored as part of a larger run.
406#[derive(Debug, Clone)]
407pub struct HollowRun<T> {
408    /// Pointers usable to retrieve the updates.
409    pub(crate) parts: Vec<RunPart<T>>,
410}
411
412/// A reference to a [HollowRun], including the key in the blob store and some denormalized
413/// metadata.
414#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
415pub struct HollowRunRef<T> {
416    pub key: PartialBatchKey,
417
418    /// The size of the referenced run object, plus all of the hollow objects it contains.
419    pub hollow_bytes: usize,
420
421    /// The size of the largest individual part in the run; useful for sizing compaction.
422    pub max_part_bytes: usize,
423
424    /// The lower bound of the data in this part, ordered by the codec ordering.
425    pub key_lower: Vec<u8>,
426
427    /// The lower bound of the data in this part, ordered by the structured ordering.
428    pub structured_key_lower: Option<LazyProto<ProtoArrayData>>,
429
430    pub diffs_sum: Option<[u8; 8]>,
431
432    pub(crate) _phantom_data: PhantomData<T>,
433}
434impl<T: Eq> PartialOrd<Self> for HollowRunRef<T> {
435    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
436        Some(self.cmp(other))
437    }
438}
439
440impl<T: Eq> Ord for HollowRunRef<T> {
441    fn cmp(&self, other: &Self) -> Ordering {
442        self.key.cmp(&other.key)
443    }
444}
445
446impl<T> HollowRunRef<T> {
447    pub fn writer_key(&self) -> Option<WriterKey> {
448        Some(self.key.split()?.0)
449    }
450}
451
452impl<T: Timestamp + Codec64> HollowRunRef<T> {
453    /// Stores the given runs and returns a [HollowRunRef] that points to them.
454    pub async fn set<D: Codec64 + Monoid>(
455        shard_id: ShardId,
456        blob: &dyn Blob,
457        writer: &WriterKey,
458        data: HollowRun<T>,
459        metrics: &Metrics,
460    ) -> Self {
461        let hollow_bytes = data.parts.iter().map(|p| p.hollow_bytes()).sum();
462        let max_part_bytes = data
463            .parts
464            .iter()
465            .map(|p| p.max_part_bytes())
466            .max()
467            .unwrap_or(0);
468        let key_lower = data
469            .parts
470            .first()
471            .map_or(vec![], |p| p.key_lower().to_vec());
472        let structured_key_lower = match data.parts.first() {
473            Some(RunPart::Many(r)) => r.structured_key_lower.clone(),
474            Some(RunPart::Single(BatchPart::Hollow(p))) => p.structured_key_lower.clone(),
475            Some(RunPart::Single(BatchPart::Inline { .. })) | None => None,
476        };
477        let diffs_sum = data
478            .parts
479            .iter()
480            .map(|p| {
481                p.diffs_sum::<D>(&metrics.columnar)
482                    .expect("valid diffs sum")
483            })
484            .reduce(|mut a, b| {
485                a.plus_equals(&b);
486                a
487            })
488            .expect("valid diffs sum")
489            .encode();
490
491        let key = PartialBatchKey::new(writer, &PartId::new());
492        let blob_key = key.complete(&shard_id);
493        let bytes = Bytes::from(prost::Message::encode_to_vec(&data.into_proto()));
494        let () = retry_external(&metrics.retries.external.hollow_run_set, || {
495            blob.set(&blob_key, bytes.clone())
496        })
497        .await;
498        Self {
499            key,
500            hollow_bytes,
501            max_part_bytes,
502            key_lower,
503            structured_key_lower,
504            diffs_sum: Some(diffs_sum),
505            _phantom_data: Default::default(),
506        }
507    }
508
509    /// Retrieve the [HollowRun] that this reference points to.
510    /// The caller is expected to ensure that this ref is the result of calling [HollowRunRef::set]
511    /// with the same shard id and backing store.
512    pub async fn get(
513        &self,
514        shard_id: ShardId,
515        blob: &dyn Blob,
516        metrics: &Metrics,
517    ) -> Option<HollowRun<T>> {
518        let blob_key = self.key.complete(&shard_id);
519        let mut bytes = retry_external(&metrics.retries.external.hollow_run_get, || {
520            blob.get(&blob_key)
521        })
522        .await?;
523        let proto_runs: ProtoHollowRun =
524            prost::Message::decode(&mut bytes).expect("illegal state: invalid proto bytes");
525        let runs = proto_runs
526            .into_rust()
527            .expect("illegal state: invalid encoded runs proto");
528        Some(runs)
529    }
530}
531
532/// Part of the updates in a run.
533///
534/// Either a pointer to ones stored in Blob or a single part stored inline.
535#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
536#[serde(untagged)]
537pub enum RunPart<T> {
538    Single(BatchPart<T>),
539    Many(HollowRunRef<T>),
540}
541
542impl<T: Ord> PartialOrd<Self> for RunPart<T> {
543    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
544        Some(self.cmp(other))
545    }
546}
547
548impl<T: Ord> Ord for RunPart<T> {
549    fn cmp(&self, other: &Self) -> Ordering {
550        match (self, other) {
551            (RunPart::Single(a), RunPart::Single(b)) => a.cmp(b),
552            (RunPart::Single(_), RunPart::Many(_)) => Ordering::Less,
553            (RunPart::Many(_), RunPart::Single(_)) => Ordering::Greater,
554            (RunPart::Many(a), RunPart::Many(b)) => a.cmp(b),
555        }
556    }
557}
558
559impl<T> RunPart<T> {
560    #[cfg(test)]
561    pub fn expect_hollow_part(&self) -> &HollowBatchPart<T> {
562        match self {
563            RunPart::Single(BatchPart::Hollow(hollow)) => hollow,
564            _ => panic!("expected hollow part!"),
565        }
566    }
567
568    pub fn hollow_bytes(&self) -> usize {
569        match self {
570            Self::Single(p) => p.hollow_bytes(),
571            Self::Many(r) => r.hollow_bytes,
572        }
573    }
574
575    pub fn is_inline(&self) -> bool {
576        match self {
577            Self::Single(p) => p.is_inline(),
578            Self::Many(_) => false,
579        }
580    }
581
582    pub fn inline_bytes(&self) -> usize {
583        match self {
584            Self::Single(p) => p.inline_bytes(),
585            Self::Many(_) => 0,
586        }
587    }
588
589    pub fn max_part_bytes(&self) -> usize {
590        match self {
591            Self::Single(p) => p.encoded_size_bytes(),
592            Self::Many(r) => r.max_part_bytes,
593        }
594    }
595
596    pub fn writer_key(&self) -> Option<WriterKey> {
597        match self {
598            Self::Single(p) => p.writer_key(),
599            Self::Many(r) => r.writer_key(),
600        }
601    }
602
603    pub fn encoded_size_bytes(&self) -> usize {
604        match self {
605            Self::Single(p) => p.encoded_size_bytes(),
606            Self::Many(r) => r.hollow_bytes,
607        }
608    }
609
610    pub fn schema_id(&self) -> Option<SchemaId> {
611        match self {
612            Self::Single(p) => p.schema_id(),
613            Self::Many(_) => None,
614        }
615    }
616
617    // A user-interpretable identifier or description of the part (for logs and
618    // such).
619    pub fn printable_name(&self) -> &str {
620        match self {
621            Self::Single(p) => p.printable_name(),
622            Self::Many(r) => r.key.0.as_str(),
623        }
624    }
625
626    pub fn stats(&self) -> Option<&LazyPartStats> {
627        match self {
628            Self::Single(p) => p.stats(),
629            // TODO: if we kept stats we could avoid fetching the metadata here.
630            Self::Many(_) => None,
631        }
632    }
633
634    pub fn key_lower(&self) -> &[u8] {
635        match self {
636            Self::Single(p) => p.key_lower(),
637            Self::Many(r) => r.key_lower.as_slice(),
638        }
639    }
640
641    pub fn structured_key_lower(&self) -> Option<ArrayBound> {
642        match self {
643            Self::Single(p) => p.structured_key_lower(),
644            Self::Many(_) => None,
645        }
646    }
647
648    pub fn ts_rewrite(&self) -> Option<&Antichain<T>> {
649        match self {
650            Self::Single(p) => p.ts_rewrite(),
651            Self::Many(_) => None,
652        }
653    }
654}
655
656impl<T> RunPart<T>
657where
658    T: Timestamp + Codec64,
659{
660    pub fn diffs_sum<D: Codec64 + Monoid>(&self, metrics: &ColumnarMetrics) -> Option<D> {
661        match self {
662            Self::Single(p) => p.diffs_sum(metrics),
663            Self::Many(hollow_run) => hollow_run.diffs_sum.map(D::decode),
664        }
665    }
666}
667
668/// A blob was missing!
669#[derive(Clone, Debug)]
670pub struct MissingBlob(BlobKey);
671
672impl std::fmt::Display for MissingBlob {
673    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
674        write!(f, "unexpectedly missing key: {}", self.0)
675    }
676}
677
678impl std::error::Error for MissingBlob {}
679
680impl<T: Timestamp + Codec64 + Sync> RunPart<T> {
681    pub fn part_stream<'a>(
682        &'a self,
683        shard_id: ShardId,
684        blob: &'a dyn Blob,
685        metrics: &'a Metrics,
686    ) -> impl Stream<Item = Result<Cow<'a, BatchPart<T>>, MissingBlob>> + Send + 'a {
687        try_stream! {
688            match self {
689                RunPart::Single(p) => {
690                    yield Cow::Borrowed(p);
691                }
692                RunPart::Many(r) => {
693                    let fetched = r.get(shard_id, blob, metrics).await
694                        .ok_or_else(|| MissingBlob(r.key.complete(&shard_id)))?;
695                    for run_part in fetched.parts {
696                        for await batch_part in
697                            run_part.part_stream(shard_id, blob, metrics).boxed()
698                        {
699                            yield Cow::Owned(batch_part?.into_owned());
700                        }
701                    }
702                }
703            }
704        }
705    }
706}
707
708impl<T: Ord> PartialOrd for BatchPart<T> {
709    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
710        Some(self.cmp(other))
711    }
712}
713
714impl<T: Ord> Ord for BatchPart<T> {
715    fn cmp(&self, other: &Self) -> Ordering {
716        match (self, other) {
717            (BatchPart::Hollow(s), BatchPart::Hollow(o)) => s.cmp(o),
718            (
719                BatchPart::Inline {
720                    updates: s_updates,
721                    ts_rewrite: s_ts_rewrite,
722                    schema_id: s_schema_id,
723                    deprecated_schema_id: s_deprecated_schema_id,
724                },
725                BatchPart::Inline {
726                    updates: o_updates,
727                    ts_rewrite: o_ts_rewrite,
728                    schema_id: o_schema_id,
729                    deprecated_schema_id: o_deprecated_schema_id,
730                },
731            ) => (
732                s_updates,
733                s_ts_rewrite.as_ref().map(|x| x.elements()),
734                s_schema_id,
735                s_deprecated_schema_id,
736            )
737                .cmp(&(
738                    o_updates,
739                    o_ts_rewrite.as_ref().map(|x| x.elements()),
740                    o_schema_id,
741                    o_deprecated_schema_id,
742                )),
743            (BatchPart::Hollow(_), BatchPart::Inline { .. }) => Ordering::Less,
744            (BatchPart::Inline { .. }, BatchPart::Hollow(_)) => Ordering::Greater,
745        }
746    }
747}
748
749/// What order are the parts in this run in?
750#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize)]
751pub(crate) enum RunOrder {
752    /// They're in no particular order.
753    Unordered,
754    /// They're ordered based on the codec-encoded K/V bytes.
755    Codec,
756    /// They're ordered by the natural ordering of the structured data.
757    Structured,
758}
759
760#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Copy, Hash)]
761pub struct RunId(pub(crate) [u8; 16]);
762
763impl std::fmt::Display for RunId {
764    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
765        write!(f, "ri{}", Uuid::from_bytes(self.0))
766    }
767}
768
769impl std::fmt::Debug for RunId {
770    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
771        write!(f, "RunId({})", Uuid::from_bytes(self.0))
772    }
773}
774
775impl std::str::FromStr for RunId {
776    type Err = String;
777
778    fn from_str(s: &str) -> Result<Self, Self::Err> {
779        parse_id("ri", "RunId", s).map(RunId)
780    }
781}
782
783impl From<RunId> for String {
784    fn from(x: RunId) -> Self {
785        x.to_string()
786    }
787}
788
789impl RunId {
790    pub(crate) fn new() -> Self {
791        RunId(*Uuid::new_v4().as_bytes())
792    }
793}
794
795impl Arbitrary for RunId {
796    type Parameters = ();
797    type Strategy = proptest::strategy::BoxedStrategy<Self>;
798    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
799        Strategy::prop_map(proptest::prelude::any::<u128>(), |n| {
800            RunId(*Uuid::from_u128(n).as_bytes())
801        })
802        .boxed()
803    }
804}
805
806/// Metadata shared across a run.
807#[derive(Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Serialize)]
808pub struct RunMeta {
809    /// If none, Persist should infer the order based on the proto metadata.
810    pub(crate) order: Option<RunOrder>,
811    /// All parts in a run should have the same schema.
812    pub(crate) schema: Option<SchemaId>,
813
814    /// ID of a schema that has since been deprecated and exists only to cleanly roundtrip.
815    pub(crate) deprecated_schema: Option<SchemaId>,
816
817    /// If set, a UUID that uniquely identifies this run.
818    pub(crate) id: Option<RunId>,
819
820    /// The number of updates in this run, or `None` if the number is unknown.
821    pub(crate) len: Option<usize>,
822
823    /// Additional unstructured metadata.
824    #[serde(skip_serializing_if = "MetadataMap::is_empty")]
825    pub(crate) meta: MetadataMap,
826}
827
828/// A subset of a [HollowBatch] corresponding 1:1 to a blob.
829#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
830pub struct HollowBatchPart<T> {
831    /// Pointer usable to retrieve the updates.
832    pub key: PartialBatchKey,
833    /// Miscellaneous metadata.
834    #[serde(skip_serializing_if = "MetadataMap::is_empty")]
835    pub meta: MetadataMap,
836    /// The encoded size of this part.
837    pub encoded_size_bytes: usize,
838    /// A lower bound on the keys in the part. (By default, this the minimum
839    /// possible key: `vec![]`.)
840    #[serde(serialize_with = "serialize_part_bytes")]
841    pub key_lower: Vec<u8>,
842    /// A lower bound on the keys in the part, stored as structured data.
843    #[serde(serialize_with = "serialize_lazy_proto")]
844    pub structured_key_lower: Option<LazyProto<ProtoArrayData>>,
845    /// Aggregate statistics about data contained in this part.
846    #[serde(serialize_with = "serialize_part_stats")]
847    pub stats: Option<LazyPartStats>,
848    /// A frontier to which timestamps in this part are advanced on read, if
849    /// set.
850    ///
851    /// A value of `Some([T::minimum()])` is functionally the same as `None`,
852    /// but we maintain the distinction between the two for some internal sanity
853    /// checking of invariants as well as metrics. If this ever becomes an
854    /// issue, everything still works with this as just `Antichain<T>`.
855    pub ts_rewrite: Option<Antichain<T>>,
856    /// A Codec64 encoded sum of all diffs in this part, if known.
857    ///
858    /// This is `None` if this part was written before we started storing this
859    /// information, or if it was written when the dyncfg was off.
860    ///
861    /// It could also make sense to model this as part of the pushdown stats, if
862    /// we later decide that's of some benefit.
863    #[serde(serialize_with = "serialize_diffs_sum")]
864    pub diffs_sum: Option<[u8; 8]>,
865    /// Columnar format that this batch was written in.
866    ///
867    /// This is `None` if this part was written before we started writing structured
868    /// columnar data.
869    pub format: Option<BatchColumnarFormat>,
870    /// The schemas used to encode the data in this batch part.
871    ///
872    /// Or None for historical data written before the schema registry was
873    /// added.
874    pub schema_id: Option<SchemaId>,
875
876    /// ID of a schema that has since been deprecated and exists only to cleanly roundtrip.
877    pub deprecated_schema_id: Option<SchemaId>,
878}
879
880/// A [Batch] but with the updates themselves stored externally.
881///
882/// [Batch]: differential_dataflow::trace::BatchReader
883#[derive(Clone, PartialEq, Eq)]
884pub struct HollowBatch<T> {
885    /// Describes the times of the updates in the batch.
886    pub desc: Description<T>,
887    /// The number of updates in the batch.
888    pub len: usize,
889    /// Pointers usable to retrieve the updates.
890    pub(crate) parts: Vec<RunPart<T>>,
891    /// Runs of sequential sorted batch parts, stored as indices into `parts`.
892    /// ex.
893    /// ```text
894    ///     parts=[p1, p2, p3], runs=[]     --> run  is  [p1, p2, p2]
895    ///     parts=[p1, p2, p3], runs=[1]    --> runs are [p1] and [p2, p3]
896    ///     parts=[p1, p2, p3], runs=[1, 2] --> runs are [p1], [p2], [p3]
897    /// ```
898    pub(crate) run_splits: Vec<usize>,
899    /// Run-level metadata: the first entry has metadata for the first run, and so on.
900    /// If there's no corresponding entry for a particular run, it's assumed to be [RunMeta::default()].
901    pub(crate) run_meta: Vec<RunMeta>,
902}
903
904impl<T: Debug> Debug for HollowBatch<T> {
905    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
906        let HollowBatch {
907            desc,
908            parts,
909            len,
910            run_splits: runs,
911            run_meta,
912        } = self;
913        f.debug_struct("HollowBatch")
914            .field(
915                "desc",
916                &(
917                    desc.lower().elements(),
918                    desc.upper().elements(),
919                    desc.since().elements(),
920                ),
921            )
922            .field("parts", &parts)
923            .field("len", &len)
924            .field("runs", &runs)
925            .field("run_meta", &run_meta)
926            .finish()
927    }
928}
929
930impl<T: Serialize> serde::Serialize for HollowBatch<T> {
931    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
932        let HollowBatch {
933            desc,
934            len,
935            // Both parts and runs are covered by the self.runs call.
936            parts: _,
937            run_splits: _,
938            run_meta: _,
939        } = self;
940        let mut s = s.serialize_struct("HollowBatch", 5)?;
941        let () = s.serialize_field("lower", &desc.lower().elements())?;
942        let () = s.serialize_field("upper", &desc.upper().elements())?;
943        let () = s.serialize_field("since", &desc.since().elements())?;
944        let () = s.serialize_field("len", len)?;
945        let () = s.serialize_field("part_runs", &self.runs().collect::<Vec<_>>())?;
946        s.end()
947    }
948}
949
950impl<T: Ord> PartialOrd for HollowBatch<T> {
951    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
952        Some(self.cmp(other))
953    }
954}
955
956impl<T: Ord> Ord for HollowBatch<T> {
957    fn cmp(&self, other: &Self) -> Ordering {
958        // Deconstruct self and other so we get a compile failure if new fields
959        // are added.
960        let HollowBatch {
961            desc: self_desc,
962            parts: self_parts,
963            len: self_len,
964            run_splits: self_runs,
965            run_meta: self_run_meta,
966        } = self;
967        let HollowBatch {
968            desc: other_desc,
969            parts: other_parts,
970            len: other_len,
971            run_splits: other_runs,
972            run_meta: other_run_meta,
973        } = other;
974        (
975            self_desc.lower().elements(),
976            self_desc.upper().elements(),
977            self_desc.since().elements(),
978            self_parts,
979            self_len,
980            self_runs,
981            self_run_meta,
982        )
983            .cmp(&(
984                other_desc.lower().elements(),
985                other_desc.upper().elements(),
986                other_desc.since().elements(),
987                other_parts,
988                other_len,
989                other_runs,
990                other_run_meta,
991            ))
992    }
993}
994
995impl<T: Timestamp + Codec64 + Sync> HollowBatch<T> {
996    pub(crate) fn part_stream<'a>(
997        &'a self,
998        shard_id: ShardId,
999        blob: &'a dyn Blob,
1000        metrics: &'a Metrics,
1001    ) -> impl Stream<Item = Result<Cow<'a, BatchPart<T>>, MissingBlob>> + 'a {
1002        stream! {
1003            for part in &self.parts {
1004                for await part in part.part_stream(shard_id, blob, metrics) {
1005                    yield part;
1006                }
1007            }
1008        }
1009    }
1010}
1011impl<T> HollowBatch<T> {
1012    /// Construct an in-memory hollow batch from the given metadata.
1013    ///
1014    /// This method checks that `runs` is a sequence of valid indices into `parts`. The caller
1015    /// is responsible for ensuring that the defined runs are valid.
1016    ///
1017    /// `len` should represent the number of valid updates in the referenced parts.
1018    pub(crate) fn new(
1019        desc: Description<T>,
1020        parts: Vec<RunPart<T>>,
1021        len: usize,
1022        run_meta: Vec<RunMeta>,
1023        run_splits: Vec<usize>,
1024    ) -> Self {
1025        debug_assert!(
1026            run_splits.is_strictly_sorted(),
1027            "run indices should be strictly increasing"
1028        );
1029        mz_ore::soft_assert_no_log!(
1030            run_splits.first().map_or(true, |i| *i > 0),
1031            "run indices should be positive"
1032        );
1033        mz_ore::soft_assert_no_log!(
1034            run_splits.last().map_or(true, |i| *i < parts.len()),
1035            "run indices should be valid indices into parts"
1036        );
1037        mz_ore::soft_assert_no_log!(
1038            parts.is_empty() || run_meta.len() == run_splits.len() + 1,
1039            "all metadata should correspond to a run"
1040        );
1041
1042        Self {
1043            desc,
1044            len,
1045            parts,
1046            run_splits,
1047            run_meta,
1048        }
1049    }
1050
1051    /// Construct a batch of a single run with default metadata. Mostly interesting for tests.
1052    pub(crate) fn new_run(desc: Description<T>, parts: Vec<RunPart<T>>, len: usize) -> Self {
1053        let run_meta = if parts.is_empty() {
1054            vec![]
1055        } else {
1056            vec![RunMeta::default()]
1057        };
1058        Self {
1059            desc,
1060            len,
1061            parts,
1062            run_splits: vec![],
1063            run_meta,
1064        }
1065    }
1066
1067    #[cfg(test)]
1068    pub(crate) fn new_run_for_test(
1069        desc: Description<T>,
1070        parts: Vec<RunPart<T>>,
1071        len: usize,
1072        run_id: RunId,
1073    ) -> Self {
1074        let run_meta = if parts.is_empty() {
1075            vec![]
1076        } else {
1077            let mut meta = RunMeta::default();
1078            meta.id = Some(run_id);
1079            vec![meta]
1080        };
1081        Self {
1082            desc,
1083            len,
1084            parts,
1085            run_splits: vec![],
1086            run_meta,
1087        }
1088    }
1089
1090    /// An empty hollow batch, representing no updates over the given desc.
1091    pub(crate) fn empty(desc: Description<T>) -> Self {
1092        Self {
1093            desc,
1094            len: 0,
1095            parts: vec![],
1096            run_splits: vec![],
1097            run_meta: vec![],
1098        }
1099    }
1100
1101    pub(crate) fn runs(&self) -> impl Iterator<Item = (&RunMeta, &[RunPart<T>])> {
1102        let run_ends = self
1103            .run_splits
1104            .iter()
1105            .copied()
1106            .chain(std::iter::once(self.parts.len()));
1107        let run_metas = self.run_meta.iter();
1108        let run_parts = run_ends
1109            .scan(0, |start, end| {
1110                let range = *start..end;
1111                *start = end;
1112                Some(range)
1113            })
1114            .filter(|range| !range.is_empty())
1115            .map(|range| &self.parts[range]);
1116        run_metas.zip_eq(run_parts)
1117    }
1118
1119    pub(crate) fn inline_bytes(&self) -> usize {
1120        self.parts.iter().map(|x| x.inline_bytes()).sum()
1121    }
1122
1123    pub(crate) fn is_empty(&self) -> bool {
1124        self.parts.is_empty()
1125    }
1126
1127    pub(crate) fn part_count(&self) -> usize {
1128        self.parts.len()
1129    }
1130
1131    /// The sum of the encoded sizes of all parts in the batch.
1132    pub fn encoded_size_bytes(&self) -> usize {
1133        self.parts.iter().map(|p| p.encoded_size_bytes()).sum()
1134    }
1135}
1136
1137// See the comment on [Batch::rewrite_ts] for why this is TotalOrder.
1138impl<T: Timestamp + TotalOrder> HollowBatch<T> {
1139    pub(crate) fn rewrite_ts(
1140        &mut self,
1141        frontier: &Antichain<T>,
1142        new_upper: Antichain<T>,
1143    ) -> Result<(), String> {
1144        if !PartialOrder::less_than(frontier, &new_upper) {
1145            return Err(format!(
1146                "rewrite frontier {:?} !< rewrite upper {:?}",
1147                frontier.elements(),
1148                new_upper.elements(),
1149            ));
1150        }
1151        if PartialOrder::less_than(&new_upper, self.desc.upper()) {
1152            return Err(format!(
1153                "rewrite upper {:?} < batch upper {:?}",
1154                new_upper.elements(),
1155                self.desc.upper().elements(),
1156            ));
1157        }
1158
1159        // The following are things that it seems like we could support, but
1160        // initially we don't because we don't have a use case for them.
1161        if PartialOrder::less_than(frontier, self.desc.lower()) {
1162            return Err(format!(
1163                "rewrite frontier {:?} < batch lower {:?}",
1164                frontier.elements(),
1165                self.desc.lower().elements(),
1166            ));
1167        }
1168        if self.desc.since() != &Antichain::from_elem(T::minimum()) {
1169            return Err(format!(
1170                "batch since {:?} != minimum antichain {:?}",
1171                self.desc.since().elements(),
1172                [T::minimum()],
1173            ));
1174        }
1175        for part in self.parts.iter() {
1176            let Some(ts_rewrite) = part.ts_rewrite() else {
1177                continue;
1178            };
1179            if PartialOrder::less_than(frontier, ts_rewrite) {
1180                return Err(format!(
1181                    "rewrite frontier {:?} < batch rewrite {:?}",
1182                    frontier.elements(),
1183                    ts_rewrite.elements(),
1184                ));
1185            }
1186        }
1187
1188        self.desc = Description::new(
1189            self.desc.lower().clone(),
1190            new_upper,
1191            self.desc.since().clone(),
1192        );
1193        for part in &mut self.parts {
1194            match part {
1195                RunPart::Single(BatchPart::Hollow(part)) => {
1196                    part.ts_rewrite = Some(frontier.clone())
1197                }
1198                RunPart::Single(BatchPart::Inline { ts_rewrite, .. }) => {
1199                    *ts_rewrite = Some(frontier.clone())
1200                }
1201                RunPart::Many(runs) => {
1202                    // Currently unreachable: we only apply rewrites to user batches, and we don't
1203                    // ever generate runs of >1 part for those.
1204                    panic!("unexpected rewrite of a hollow runs ref: {runs:?}");
1205                }
1206            }
1207        }
1208        Ok(())
1209    }
1210}
1211
1212impl<T: Ord> PartialOrd for HollowBatchPart<T> {
1213    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1214        Some(self.cmp(other))
1215    }
1216}
1217
1218impl<T: Ord> Ord for HollowBatchPart<T> {
1219    fn cmp(&self, other: &Self) -> Ordering {
1220        // Deconstruct self and other so we get a compile failure if new fields
1221        // are added.
1222        let HollowBatchPart {
1223            key: self_key,
1224            meta: self_meta,
1225            encoded_size_bytes: self_encoded_size_bytes,
1226            key_lower: self_key_lower,
1227            structured_key_lower: self_structured_key_lower,
1228            stats: self_stats,
1229            ts_rewrite: self_ts_rewrite,
1230            diffs_sum: self_diffs_sum,
1231            format: self_format,
1232            schema_id: self_schema_id,
1233            deprecated_schema_id: self_deprecated_schema_id,
1234        } = self;
1235        let HollowBatchPart {
1236            key: other_key,
1237            meta: other_meta,
1238            encoded_size_bytes: other_encoded_size_bytes,
1239            key_lower: other_key_lower,
1240            structured_key_lower: other_structured_key_lower,
1241            stats: other_stats,
1242            ts_rewrite: other_ts_rewrite,
1243            diffs_sum: other_diffs_sum,
1244            format: other_format,
1245            schema_id: other_schema_id,
1246            deprecated_schema_id: other_deprecated_schema_id,
1247        } = other;
1248        (
1249            self_key,
1250            self_meta,
1251            self_encoded_size_bytes,
1252            self_key_lower,
1253            self_structured_key_lower,
1254            self_stats,
1255            self_ts_rewrite.as_ref().map(|x| x.elements()),
1256            self_diffs_sum,
1257            self_format,
1258            self_schema_id,
1259            self_deprecated_schema_id,
1260        )
1261            .cmp(&(
1262                other_key,
1263                other_meta,
1264                other_encoded_size_bytes,
1265                other_key_lower,
1266                other_structured_key_lower,
1267                other_stats,
1268                other_ts_rewrite.as_ref().map(|x| x.elements()),
1269                other_diffs_sum,
1270                other_format,
1271                other_schema_id,
1272                other_deprecated_schema_id,
1273            ))
1274    }
1275}
1276
1277/// A pointer to a rollup stored externally.
1278#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1279pub struct HollowRollup {
1280    /// Pointer usable to retrieve the rollup.
1281    pub key: PartialRollupKey,
1282    /// The encoded size of this rollup, if known.
1283    pub encoded_size_bytes: Option<usize>,
1284}
1285
1286/// A pointer to a blob stored externally.
1287#[derive(Debug)]
1288pub enum HollowBlobRef<'a, T> {
1289    Batch(&'a HollowBatch<T>),
1290    Rollup(&'a HollowRollup),
1291}
1292
1293/// A rollup that is currently being computed.
1294#[derive(
1295    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Arbitrary, Serialize
1296)]
1297pub struct ActiveRollup {
1298    pub seqno: SeqNo,
1299    pub start_ms: u64,
1300}
1301
1302/// A garbage collection request that is currently being computed.
1303#[derive(
1304    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Arbitrary, Serialize
1305)]
1306pub struct ActiveGc {
1307    pub seqno: SeqNo,
1308    pub start_ms: u64,
1309}
1310
1311/// A sentinel for a state transition that was a no-op.
1312///
1313/// Critically, this also indicates that the no-op state transition was not
1314/// committed through compare_and_append and thus is _not linearized_.
1315#[derive(Debug)]
1316#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
1317pub struct NoOpStateTransition<T>(pub T);
1318
1319// TODO: Document invariants.
1320#[derive(Debug, Clone)]
1321#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
1322pub struct StateCollections<T> {
1323    /// The version of this state. This is typically identical to the version of the code
1324    /// that wrote it, but may diverge during 0dt upgrades and similar operations when a
1325    /// new version of code is intentionally interoperating with an older state format.
1326    pub(crate) version: Version,
1327
1328    // - Invariant: `<= all reader.since`
1329    // - Invariant: Doesn't regress across state versions.
1330    pub(crate) last_gc_req: SeqNo,
1331
1332    // - Invariant: There is a rollup with `seqno <= self.seqno_since`.
1333    pub(crate) rollups: BTreeMap<SeqNo, HollowRollup>,
1334
1335    /// The rollup that is currently being computed.
1336    pub(crate) active_rollup: Option<ActiveRollup>,
1337    /// The gc request that is currently being computed.
1338    pub(crate) active_gc: Option<ActiveGc>,
1339
1340    pub(crate) leased_readers: BTreeMap<LeasedReaderId, LeasedReaderState<T>>,
1341    pub(crate) critical_readers: BTreeMap<CriticalReaderId, CriticalReaderState<T>>,
1342    pub(crate) writers: BTreeMap<WriterId, WriterState<T>>,
1343    pub(crate) schemas: BTreeMap<SchemaId, EncodedSchemas>,
1344
1345    // - Invariant: `trace.since == meet(all reader.since)`
1346    // - Invariant: `trace.since` doesn't regress across state versions.
1347    // - Invariant: `trace.upper` doesn't regress across state versions.
1348    // - Invariant: `trace` upholds its own invariants.
1349    pub(crate) trace: Trace<T>,
1350}
1351
1352/// A key and val [Codec::Schema] encoded via [Codec::encode_schema].
1353///
1354/// This strategy of directly serializing the schema objects requires that
1355/// persist users do the right thing. Specifically, that an encoded schema
1356/// doesn't in some later version of mz decode to an in-mem object that acts
1357/// differently. In a sense, the current system (before the introduction of the
1358/// schema registry) where schemas are passed in unchecked to reader and writer
1359/// registration calls also has the same defect, so seems fine.
1360///
1361/// An alternative is to write down here some persist-specific representation of
1362/// the schema (e.g. the arrow DataType). This is a lot more work and also has
1363/// the potential to lead down a similar failure mode to the mz_persist_types
1364/// `Data` trait, where the boilerplate isn't worth the safety. Given that we
1365/// can always migrate later by rehydrating these, seems fine to start with the
1366/// easy thing.
1367#[derive(Debug, Clone, Serialize, PartialEq)]
1368pub struct EncodedSchemas {
1369    /// A full in-mem `K::Schema` impl encoded via [Codec::encode_schema].
1370    pub key: Bytes,
1371    /// The arrow `DataType` produced by this `K::Schema` at the time it was
1372    /// registered, encoded as a `ProtoDataType`.
1373    pub key_data_type: Bytes,
1374    /// A full in-mem `V::Schema` impl encoded via [Codec::encode_schema].
1375    pub val: Bytes,
1376    /// The arrow `DataType` produced by this `V::Schema` at the time it was
1377    /// registered, encoded as a `ProtoDataType`.
1378    pub val_data_type: Bytes,
1379}
1380
1381impl EncodedSchemas {
1382    pub(crate) fn decode_data_type(buf: &[u8]) -> DataType {
1383        let proto = prost::Message::decode(buf).expect("valid ProtoDataType");
1384        DataType::from_proto(proto).expect("valid DataType")
1385    }
1386}
1387
1388#[derive(Debug)]
1389#[cfg_attr(test, derive(PartialEq))]
1390pub enum CompareAndAppendBreak<T> {
1391    AlreadyCommitted,
1392    Upper {
1393        shard_upper: Antichain<T>,
1394        writer_upper: Antichain<T>,
1395    },
1396    InvalidUsage(InvalidUsage<T>),
1397    InlineBackpressure,
1398}
1399
1400#[derive(Debug)]
1401#[cfg_attr(test, derive(PartialEq))]
1402pub enum SnapshotErr<T> {
1403    AsOfNotYetAvailable(SeqNo, Upper<T>),
1404    AsOfHistoricalDistinctionsLost(Since<T>),
1405}
1406
1407impl<T> StateCollections<T>
1408where
1409    T: Timestamp + Lattice + Codec64,
1410{
1411    pub fn add_rollup(
1412        &mut self,
1413        add_rollup: (SeqNo, &HollowRollup),
1414    ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
1415        let (rollup_seqno, rollup) = add_rollup;
1416        let applied = match self.rollups.get(&rollup_seqno) {
1417            Some(x) => x.key == rollup.key,
1418            None => {
1419                // PER-16: refuse to insert a rollup at a seqno that GC has
1420                // already physically removed. `apply_unbatched_idempotent_cmd`
1421                // replays the captured `(rollup_seqno, rollup)` tuple on
1422                // every retry. If the original CAS committed indeterminately
1423                // and a concurrent GC then removed the rollup, the retry
1424                // would otherwise observe an empty map entry and re-insert
1425                // the same `PartialRollupKey` at the same seqno, producing
1426                // a second Insert (and, later, a second Delete) for that
1427                // key in the diff stream and tripping the `assert!` in
1428                // `find_removable_blobs`.
1429                //
1430                // We use the smallest seqno currently in `self.rollups` as
1431                // the GC watermark. GC's `remove_rollups` always keeps the
1432                // latest rollup `<= seqno_since` and removes the older
1433                // ones, so the surviving minimum is strictly above every
1434                // seqno that has been removed. (Unlike `last_gc_req`, this
1435                // is only advanced by an actual physical removal, not by
1436                // `maybe_gc` deciding to *request* one — `last_gc_req` runs
1437                // ahead of removals and would falsely reject legitimate
1438                // late `add_rollup` calls for older seqnos.)
1439                if let Some(min_kept) = self.rollups.keys().next() {
1440                    if rollup_seqno < *min_kept {
1441                        return Continue(false);
1442                    }
1443                }
1444                self.active_rollup = None;
1445                self.rollups.insert(rollup_seqno, rollup.to_owned());
1446                true
1447            }
1448        };
1449        // This state transition is a no-op if applied is false but we
1450        // still commit the state change so that this gets linearized
1451        // (maybe we're looking at old state).
1452        Continue(applied)
1453    }
1454
1455    pub fn remove_rollups(
1456        &mut self,
1457        remove_rollups: &[(SeqNo, PartialRollupKey)],
1458    ) -> ControlFlow<NoOpStateTransition<Vec<SeqNo>>, Vec<SeqNo>> {
1459        if self.is_tombstone() {
1460            return Break(NoOpStateTransition(vec![]));
1461        }
1462
1463        // This state transition is called at the end of the GC process, so we
1464        // need to unset the `active_gc` field.
1465        let active_gc_was_set = self.active_gc.take().is_some();
1466
1467        if remove_rollups.is_empty() {
1468            return if active_gc_was_set {
1469                Continue(vec![])
1470            } else {
1471                Break(NoOpStateTransition(vec![]))
1472            };
1473        }
1474
1475        let mut removed = vec![];
1476        for (seqno, key) in remove_rollups {
1477            let removed_key = self.rollups.remove(seqno);
1478            mz_ore::soft_assert_no_log!(
1479                removed_key.as_ref().map_or(true, |x| &x.key == key),
1480                "rollup at {} to be removed has key {:?} in state, but GC asked to remove {}",
1481                seqno,
1482                removed_key,
1483                key
1484            );
1485
1486            if removed_key.is_some() {
1487                removed.push(*seqno);
1488            }
1489        }
1490
1491        Continue(removed)
1492    }
1493
1494    pub fn register_leased_reader(
1495        &mut self,
1496        hostname: &str,
1497        reader_id: &LeasedReaderId,
1498        purpose: &str,
1499        seqno: SeqNo,
1500        lease_duration: Duration,
1501        heartbeat_timestamp_ms: u64,
1502        use_critical_since: bool,
1503    ) -> ControlFlow<
1504        NoOpStateTransition<(LeasedReaderState<T>, SeqNo)>,
1505        (LeasedReaderState<T>, SeqNo),
1506    > {
1507        let since = if use_critical_since {
1508            self.critical_since()
1509                .unwrap_or_else(|| self.trace.since().clone())
1510        } else {
1511            self.trace.since().clone()
1512        };
1513        let reader_state = LeasedReaderState {
1514            debug: HandleDebugState {
1515                hostname: hostname.to_owned(),
1516                purpose: purpose.to_owned(),
1517            },
1518            seqno,
1519            since,
1520            last_heartbeat_timestamp_ms: heartbeat_timestamp_ms,
1521            lease_duration_ms: u64::try_from(lease_duration.as_millis())
1522                .expect("lease duration as millis should fit within u64"),
1523        };
1524
1525        // If the shard-global upper and since are both the empty antichain,
1526        // then no further writes can ever commit and no further reads can be
1527        // served. Optimize this by no-op-ing reader registration so that we can
1528        // settle the shard into a final unchanging tombstone state.
1529        if self.is_tombstone() {
1530            return Break(NoOpStateTransition((reader_state, self.seqno_since(seqno))));
1531        }
1532
1533        // TODO: Handle if the reader or writer already exists.
1534        self.leased_readers
1535            .insert(reader_id.clone(), reader_state.clone());
1536        Continue((reader_state, self.seqno_since(seqno)))
1537    }
1538
1539    pub fn register_critical_reader(
1540        &mut self,
1541        hostname: &str,
1542        reader_id: &CriticalReaderId,
1543        opaque: Opaque,
1544        purpose: &str,
1545    ) -> ControlFlow<NoOpStateTransition<CriticalReaderState<T>>, CriticalReaderState<T>> {
1546        let state = CriticalReaderState {
1547            debug: HandleDebugState {
1548                hostname: hostname.to_owned(),
1549                purpose: purpose.to_owned(),
1550            },
1551            since: self.trace.since().clone(),
1552            opaque,
1553        };
1554
1555        // We expire all readers if the upper and since both advance to the
1556        // empty antichain. Gracefully handle this. At the same time,
1557        // short-circuit the cmd application so we don't needlessly create new
1558        // SeqNos.
1559        if self.is_tombstone() {
1560            return Break(NoOpStateTransition(state));
1561        }
1562
1563        let state = match self.critical_readers.get_mut(reader_id) {
1564            Some(existing_state) => {
1565                existing_state.debug = state.debug;
1566                existing_state.clone()
1567            }
1568            None => {
1569                self.critical_readers
1570                    .insert(reader_id.clone(), state.clone());
1571                state
1572            }
1573        };
1574        Continue(state)
1575    }
1576
1577    pub fn register_schema<K: Codec, V: Codec>(
1578        &mut self,
1579        key_schema: &K::Schema,
1580        val_schema: &V::Schema,
1581    ) -> ControlFlow<NoOpStateTransition<Option<SchemaId>>, Option<SchemaId>> {
1582        fn encode_data_type(data_type: &DataType) -> Bytes {
1583            let proto = data_type.into_proto();
1584            prost::Message::encode_to_vec(&proto).into()
1585        }
1586
1587        // Look for an existing registered SchemaId for these schemas.
1588        //
1589        // The common case is that this should be a recent one, so as a minor
1590        // optimization, do this search in reverse order.
1591        //
1592        // TODO: Note that this impl is `O(schemas)`. Combined with the
1593        // possibility of cmd retries, it's possible but unlikely for this to
1594        // get expensive. We could maintain a reverse map to speed this up in
1595        // necessary. This would either need to work on the encoded
1596        // representation (which, we'd have to fall back to the linear scan) or
1597        // we'd need to add a Hash/Ord bound to Schema.
1598        let existing_id = self.schemas.iter().rev().find(|(_, x)| {
1599            K::decode_schema(&x.key) == *key_schema && V::decode_schema(&x.val) == *val_schema
1600        });
1601        match existing_id {
1602            Some((schema_id, _)) => {
1603                // TODO: Validate that the decoded schemas still produce records
1604                // of the recorded DataType, to detect shenanigans. Probably
1605                // best to wait until we've turned on Schema2 in prod and thus
1606                // committed to the current mappings.
1607                Break(NoOpStateTransition(Some(*schema_id)))
1608            }
1609            None if self.is_tombstone() => {
1610                // TODO: Is this right?
1611                Break(NoOpStateTransition(None))
1612            }
1613            None if self.schemas.is_empty() => {
1614                // We'll have to do something more sophisticated here to
1615                // generate the next id if/when we start supporting the removal
1616                // of schemas.
1617                let id = SchemaId(self.schemas.len());
1618                let key_data_type = mz_persist_types::columnar::data_type::<K>(key_schema)
1619                    .expect("valid key schema");
1620                let val_data_type = mz_persist_types::columnar::data_type::<V>(val_schema)
1621                    .expect("valid val schema");
1622                let prev = self.schemas.insert(
1623                    id,
1624                    EncodedSchemas {
1625                        key: K::encode_schema(key_schema),
1626                        key_data_type: encode_data_type(&key_data_type),
1627                        val: V::encode_schema(val_schema),
1628                        val_data_type: encode_data_type(&val_data_type),
1629                    },
1630                );
1631                assert_eq!(prev, None);
1632                Continue(Some(id))
1633            }
1634            None => {
1635                info!(
1636                    "register_schemas got {:?} expected {:?}",
1637                    key_schema,
1638                    self.schemas
1639                        .iter()
1640                        .map(|(id, x)| (id, K::decode_schema(&x.key)))
1641                        .collect::<Vec<_>>()
1642                );
1643                // Until we implement persist schema changes, only allow at most
1644                // one registered schema.
1645                Break(NoOpStateTransition(None))
1646            }
1647        }
1648    }
1649
1650    pub fn compare_and_evolve_schema<K: Codec, V: Codec>(
1651        &mut self,
1652        expected: SchemaId,
1653        key_schema: &K::Schema,
1654        val_schema: &V::Schema,
1655    ) -> ControlFlow<NoOpStateTransition<CaESchema<K, V>>, CaESchema<K, V>> {
1656        fn data_type<T>(schema: &impl Schema<T>) -> DataType {
1657            // To be defensive, create an empty batch and inspect the resulting
1658            // data type (as opposed to something like allowing the `Schema` to
1659            // declare the DataType).
1660            let array = Schema::encoder(schema).expect("valid schema").finish();
1661            Array::data_type(&array).clone()
1662        }
1663
1664        let (current_id, current) = self
1665            .schemas
1666            .last_key_value()
1667            .expect("all shards have a schema");
1668        if *current_id != expected {
1669            return Break(NoOpStateTransition(CaESchema::ExpectedMismatch {
1670                schema_id: *current_id,
1671                key: K::decode_schema(&current.key),
1672                val: V::decode_schema(&current.val),
1673            }));
1674        }
1675
1676        let current_key = K::decode_schema(&current.key);
1677        let current_key_dt = EncodedSchemas::decode_data_type(&current.key_data_type);
1678        let current_val = V::decode_schema(&current.val);
1679        let current_val_dt = EncodedSchemas::decode_data_type(&current.val_data_type);
1680
1681        let key_dt = data_type(key_schema);
1682        let val_dt = data_type(val_schema);
1683
1684        // If the schema is exactly the same as the current one, no-op.
1685        if current_key == *key_schema
1686            && current_key_dt == key_dt
1687            && current_val == *val_schema
1688            && current_val_dt == val_dt
1689        {
1690            return Break(NoOpStateTransition(CaESchema::Ok(*current_id)));
1691        }
1692
1693        let key_fn = backward_compatible(&current_key_dt, &key_dt);
1694        let val_fn = backward_compatible(&current_val_dt, &val_dt);
1695        let (Some(key_fn), Some(val_fn)) = (key_fn, val_fn) else {
1696            return Break(NoOpStateTransition(CaESchema::Incompatible));
1697        };
1698        // Persist initially disallows dropping columns. This would require a
1699        // bunch more work (e.g. not safe to use the latest schema in
1700        // compaction) and isn't initially necessary in mz.
1701        if key_fn.contains_drop() || val_fn.contains_drop() {
1702            return Break(NoOpStateTransition(CaESchema::Incompatible));
1703        }
1704
1705        // We'll have to do something more sophisticated here to
1706        // generate the next id if/when we start supporting the removal
1707        // of schemas.
1708        let id = SchemaId(self.schemas.len());
1709        self.schemas.insert(
1710            id,
1711            EncodedSchemas {
1712                key: K::encode_schema(key_schema),
1713                key_data_type: prost::Message::encode_to_vec(&key_dt.into_proto()).into(),
1714                val: V::encode_schema(val_schema),
1715                val_data_type: prost::Message::encode_to_vec(&val_dt.into_proto()).into(),
1716            },
1717        );
1718        Continue(CaESchema::Ok(id))
1719    }
1720
1721    pub fn compare_and_append(
1722        &mut self,
1723        batch: &HollowBatch<T>,
1724        writer_id: &WriterId,
1725        heartbeat_timestamp_ms: u64,
1726        lease_duration_ms: u64,
1727        idempotency_token: &IdempotencyToken,
1728        debug_info: &HandleDebugState,
1729        inline_writes_total_max_bytes: usize,
1730        claim_compaction_percent: usize,
1731        claim_compaction_min_version: Option<&Version>,
1732    ) -> ControlFlow<CompareAndAppendBreak<T>, Vec<FueledMergeReq<T>>> {
1733        // We expire all writers if the upper and since both advance to the
1734        // empty antichain. Gracefully handle this. At the same time,
1735        // short-circuit the cmd application so we don't needlessly create new
1736        // SeqNos.
1737        if self.is_tombstone() {
1738            assert_eq!(self.trace.upper(), &Antichain::new());
1739            return Break(CompareAndAppendBreak::Upper {
1740                shard_upper: Antichain::new(),
1741                // This writer might have been registered before the shard upper
1742                // was advanced, which would make this pessimistic in the
1743                // Indeterminate handling of compare_and_append at the machine
1744                // level, but that's fine.
1745                writer_upper: Antichain::new(),
1746            });
1747        }
1748
1749        let writer_state = self
1750            .writers
1751            .entry(writer_id.clone())
1752            .or_insert_with(|| WriterState {
1753                last_heartbeat_timestamp_ms: heartbeat_timestamp_ms,
1754                lease_duration_ms,
1755                most_recent_write_token: IdempotencyToken::SENTINEL,
1756                most_recent_write_upper: Antichain::from_elem(T::minimum()),
1757                debug: debug_info.clone(),
1758            });
1759
1760        if PartialOrder::less_than(batch.desc.upper(), batch.desc.lower()) {
1761            return Break(CompareAndAppendBreak::InvalidUsage(
1762                InvalidUsage::InvalidBounds {
1763                    lower: batch.desc.lower().clone(),
1764                    upper: batch.desc.upper().clone(),
1765                },
1766            ));
1767        }
1768
1769        // If the time interval is empty, the list of updates must also be
1770        // empty.
1771        if batch.desc.upper() == batch.desc.lower() && !batch.is_empty() {
1772            return Break(CompareAndAppendBreak::InvalidUsage(
1773                InvalidUsage::InvalidEmptyTimeInterval {
1774                    lower: batch.desc.lower().clone(),
1775                    upper: batch.desc.upper().clone(),
1776                    keys: batch
1777                        .parts
1778                        .iter()
1779                        .map(|x| x.printable_name().to_owned())
1780                        .collect(),
1781                },
1782            ));
1783        }
1784
1785        if idempotency_token == &writer_state.most_recent_write_token {
1786            // If the last write had the same idempotency_token, then this must
1787            // have already committed. Sanity check that the most recent write
1788            // upper matches and that the shard upper is at least the write
1789            // upper, if it's not something very suspect is going on.
1790            assert_eq!(batch.desc.upper(), &writer_state.most_recent_write_upper);
1791            assert!(
1792                PartialOrder::less_equal(batch.desc.upper(), self.trace.upper()),
1793                "{:?} vs {:?}",
1794                batch.desc.upper(),
1795                self.trace.upper()
1796            );
1797            return Break(CompareAndAppendBreak::AlreadyCommitted);
1798        }
1799
1800        let shard_upper = self.trace.upper();
1801        if shard_upper != batch.desc.lower() {
1802            return Break(CompareAndAppendBreak::Upper {
1803                shard_upper: shard_upper.clone(),
1804                writer_upper: writer_state.most_recent_write_upper.clone(),
1805            });
1806        }
1807
1808        let new_inline_bytes = batch.inline_bytes();
1809        if new_inline_bytes > 0 {
1810            let mut existing_inline_bytes = 0;
1811            self.trace
1812                .map_batches(|x| existing_inline_bytes += x.inline_bytes());
1813            // TODO: For very small batches, it may actually _increase_ the size
1814            // of state to flush them out. Consider another threshold under
1815            // which an inline part can be appended no matter what.
1816            if existing_inline_bytes + new_inline_bytes >= inline_writes_total_max_bytes {
1817                return Break(CompareAndAppendBreak::InlineBackpressure);
1818            }
1819        }
1820
1821        let mut merge_reqs = if batch.desc.upper() != batch.desc.lower() {
1822            self.trace.push_batch(batch.clone())
1823        } else {
1824            Vec::new()
1825        };
1826
1827        // NB: we don't claim unclaimed compactions when the recording flag is off, even if we'd
1828        // otherwise be allowed to, to avoid triggering the same compactions in every writer.
1829        let all_empty_reqs = merge_reqs
1830            .iter()
1831            .all(|req| req.inputs.iter().all(|b| b.batch.is_empty()));
1832        if all_empty_reqs && !batch.is_empty() {
1833            let mut reqs_to_take = claim_compaction_percent / 100;
1834            if (usize::cast_from(idempotency_token.hashed()) % 100)
1835                < (claim_compaction_percent % 100)
1836            {
1837                reqs_to_take += 1;
1838            }
1839            let threshold_ms = heartbeat_timestamp_ms.saturating_sub(lease_duration_ms);
1840            let min_writer = claim_compaction_min_version.map(WriterKey::for_version);
1841            merge_reqs.extend(
1842                // We keep the oldest `reqs_to_take` batches, under the theory that they're least
1843                // likely to be compacted soon for other reasons.
1844                self.trace
1845                    .fueled_merge_reqs_before_ms(threshold_ms, min_writer)
1846                    .take(reqs_to_take),
1847            )
1848        }
1849
1850        for req in &merge_reqs {
1851            self.trace.claim_compaction(
1852                req.id,
1853                ActiveCompaction {
1854                    start_ms: heartbeat_timestamp_ms,
1855                },
1856            )
1857        }
1858
1859        mz_ore::soft_assert_eq_no_log!(self.trace.upper(), batch.desc.upper());
1860        writer_state.most_recent_write_token = idempotency_token.clone();
1861        // The writer's most recent upper should only go forward.
1862        assert!(
1863            PartialOrder::less_equal(&writer_state.most_recent_write_upper, batch.desc.upper()),
1864            "{:?} vs {:?}",
1865            writer_state.most_recent_write_upper,
1866            batch.desc.upper()
1867        );
1868        writer_state
1869            .most_recent_write_upper
1870            .clone_from(batch.desc.upper());
1871
1872        // Heartbeat the writer state to keep our idempotency token alive.
1873        writer_state.last_heartbeat_timestamp_ms = std::cmp::max(
1874            heartbeat_timestamp_ms,
1875            writer_state.last_heartbeat_timestamp_ms,
1876        );
1877
1878        Continue(merge_reqs)
1879    }
1880
1881    pub fn apply_merge_res<D: Codec64 + Monoid + PartialEq>(
1882        &mut self,
1883        res: &FueledMergeRes<T>,
1884        metrics: &ColumnarMetrics,
1885    ) -> ControlFlow<NoOpStateTransition<ApplyMergeResult>, ApplyMergeResult> {
1886        // We expire all writers if the upper and since both advance to the
1887        // empty antichain. Gracefully handle this. At the same time,
1888        // short-circuit the cmd application so we don't needlessly create new
1889        // SeqNos.
1890        if self.is_tombstone() {
1891            return Break(NoOpStateTransition(ApplyMergeResult::NotAppliedNoMatch));
1892        }
1893
1894        let apply_merge_result = self.trace.apply_merge_res_checked::<D>(res, metrics);
1895        Continue(apply_merge_result)
1896    }
1897
1898    pub fn spine_exert(
1899        &mut self,
1900        fuel: usize,
1901    ) -> ControlFlow<NoOpStateTransition<Vec<FueledMergeReq<T>>>, Vec<FueledMergeReq<T>>> {
1902        let (merge_reqs, did_work) = self.trace.exert(fuel);
1903        if did_work {
1904            Continue(merge_reqs)
1905        } else {
1906            assert!(merge_reqs.is_empty());
1907            // Break if we have nothing useful to do to save the seqno (and
1908            // resulting crdb traffic)
1909            Break(NoOpStateTransition(Vec::new()))
1910        }
1911    }
1912
1913    pub fn downgrade_since(
1914        &mut self,
1915        reader_id: &LeasedReaderId,
1916        seqno: SeqNo,
1917        outstanding_seqno: SeqNo,
1918        new_since: &Antichain<T>,
1919        heartbeat_timestamp_ms: u64,
1920    ) -> ControlFlow<NoOpStateTransition<Since<T>>, Since<T>> {
1921        // We expire all readers if the upper and since both advance to the
1922        // empty antichain. Gracefully handle this. At the same time,
1923        // short-circuit the cmd application so we don't needlessly create new
1924        // SeqNos.
1925        if self.is_tombstone() {
1926            return Break(NoOpStateTransition(Since(Antichain::new())));
1927        }
1928
1929        // The only way to have a missing reader in state is if it's been expired... and in that
1930        // case, we behave the same as though that reader had been downgraded to the empty antichain.
1931        let Some(reader_state) = self.leased_reader(reader_id) else {
1932            tracing::warn!(
1933                "Leased reader {reader_id} was expired due to inactivity. Did the machine go to sleep?",
1934            );
1935            return Break(NoOpStateTransition(Since(Antichain::new())));
1936        };
1937
1938        // Also use this as an opportunity to heartbeat the reader and downgrade
1939        // the seqno capability.
1940        reader_state.last_heartbeat_timestamp_ms = std::cmp::max(
1941            heartbeat_timestamp_ms,
1942            reader_state.last_heartbeat_timestamp_ms,
1943        );
1944
1945        let seqno = {
1946            assert!(
1947                outstanding_seqno >= reader_state.seqno,
1948                "SeqNos cannot go backward; however, oldest leased SeqNo ({:?}) \
1949                    is behind current reader_state ({:?})",
1950                outstanding_seqno,
1951                reader_state.seqno,
1952            );
1953            std::cmp::min(outstanding_seqno, seqno)
1954        };
1955
1956        reader_state.seqno = seqno;
1957
1958        let reader_current_since = if PartialOrder::less_than(&reader_state.since, new_since) {
1959            reader_state.since.clone_from(new_since);
1960            self.update_since();
1961            new_since.clone()
1962        } else {
1963            // No-op, but still commit the state change so that this gets
1964            // linearized.
1965            reader_state.since.clone()
1966        };
1967
1968        Continue(Since(reader_current_since))
1969    }
1970
1971    pub fn compare_and_downgrade_since(
1972        &mut self,
1973        reader_id: &CriticalReaderId,
1974        expected_opaque: &Opaque,
1975        (new_opaque, new_since): (&Opaque, &Antichain<T>),
1976    ) -> ControlFlow<
1977        NoOpStateTransition<Result<Since<T>, (Opaque, Since<T>)>>,
1978        Result<Since<T>, (Opaque, Since<T>)>,
1979    > {
1980        // We expire all readers if the upper and since both advance to the
1981        // empty antichain. Gracefully handle this. At the same time,
1982        // short-circuit the cmd application so we don't needlessly create new
1983        // SeqNos.
1984        if self.is_tombstone() {
1985            // Match the idempotence behavior below of ignoring the token if
1986            // since is already advanced enough (in this case, because it's a
1987            // tombstone, we know it's the empty antichain).
1988            return Break(NoOpStateTransition(Ok(Since(Antichain::new()))));
1989        }
1990
1991        let reader_state = self.critical_reader(reader_id);
1992
1993        if reader_state.opaque != *expected_opaque {
1994            // No-op, but still commit the state change so that this gets
1995            // linearized.
1996            return Continue(Err((
1997                reader_state.opaque.clone(),
1998                Since(reader_state.since.clone()),
1999            )));
2000        }
2001
2002        reader_state.opaque = new_opaque.clone();
2003        if PartialOrder::less_equal(&reader_state.since, new_since) {
2004            reader_state.since.clone_from(new_since);
2005            self.update_since();
2006            Continue(Ok(Since(new_since.clone())))
2007        } else {
2008            // no work to be done -- the reader state's `since` is already sufficiently
2009            // advanced. we may someday need to revisit this branch when it's possible
2010            // for two `since` frontiers to be incomparable.
2011            Continue(Ok(Since(reader_state.since.clone())))
2012        }
2013    }
2014
2015    pub fn expire_leased_reader(
2016        &mut self,
2017        reader_id: &LeasedReaderId,
2018    ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2019        // We expire all readers if the upper and since both advance to the
2020        // empty antichain. Gracefully handle this. At the same time,
2021        // short-circuit the cmd application so we don't needlessly create new
2022        // SeqNos.
2023        if self.is_tombstone() {
2024            return Break(NoOpStateTransition(false));
2025        }
2026
2027        let existed = self.leased_readers.remove(reader_id).is_some();
2028        if existed {
2029            // TODO(database-issues#6885): Re-enable this
2030            //
2031            // Temporarily disabling this because we think it might be the cause
2032            // of the remap since bug. Specifically, a clusterd process has a
2033            // ReadHandle for maintaining the once and one inside a Listen. If
2034            // we crash and stay down for longer than the read lease duration,
2035            // it's possible that an expiry of them both in quick succession
2036            // jumps the since forward to the Listen one.
2037            //
2038            // Don't forget to update the downgrade_since when this gets
2039            // switched back on.
2040            //
2041            // self.update_since();
2042        }
2043        // No-op if existed is false, but still commit the state change so that
2044        // this gets linearized.
2045        Continue(existed)
2046    }
2047
2048    pub fn expire_critical_reader(
2049        &mut self,
2050        reader_id: &CriticalReaderId,
2051    ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2052        // We expire all readers if the upper and since both advance to the
2053        // empty antichain. Gracefully handle this. At the same time,
2054        // short-circuit the cmd application so we don't needlessly create new
2055        // SeqNos.
2056        if self.is_tombstone() {
2057            return Break(NoOpStateTransition(false));
2058        }
2059
2060        let existed = self.critical_readers.remove(reader_id).is_some();
2061        if existed {
2062            // TODO(database-issues#6885): Re-enable this
2063            //
2064            // Temporarily disabling this because we think it might be the cause
2065            // of the remap since bug. Specifically, a clusterd process has a
2066            // ReadHandle for maintaining the once and one inside a Listen. If
2067            // we crash and stay down for longer than the read lease duration,
2068            // it's possible that an expiry of them both in quick succession
2069            // jumps the since forward to the Listen one.
2070            //
2071            // Don't forget to update the downgrade_since when this gets
2072            // switched back on.
2073            //
2074            // self.update_since();
2075        }
2076        // This state transition is a no-op if existed is false, but we still
2077        // commit the state change so that this gets linearized (maybe we're
2078        // looking at old state).
2079        Continue(existed)
2080    }
2081
2082    pub fn expire_writer(
2083        &mut self,
2084        writer_id: &WriterId,
2085    ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2086        // We expire all writers if the upper and since both advance to the
2087        // empty antichain. Gracefully handle this. At the same time,
2088        // short-circuit the cmd application so we don't needlessly create new
2089        // SeqNos.
2090        if self.is_tombstone() {
2091            return Break(NoOpStateTransition(false));
2092        }
2093
2094        let existed = self.writers.remove(writer_id).is_some();
2095        // This state transition is a no-op if existed is false, but we still
2096        // commit the state change so that this gets linearized (maybe we're
2097        // looking at old state).
2098        Continue(existed)
2099    }
2100
2101    fn leased_reader(&mut self, id: &LeasedReaderId) -> Option<&mut LeasedReaderState<T>> {
2102        self.leased_readers.get_mut(id)
2103    }
2104
2105    fn critical_reader(&mut self, id: &CriticalReaderId) -> &mut CriticalReaderState<T> {
2106        self.critical_readers
2107            .get_mut(id)
2108            .unwrap_or_else(|| {
2109                panic!(
2110                    "Unknown CriticalReaderId({}). It was either never registered, or has been manually expired.",
2111                    id
2112                )
2113            })
2114    }
2115
2116    fn critical_since(&self) -> Option<Antichain<T>> {
2117        let mut critical_sinces = self.critical_readers.values().map(|r| &r.since);
2118        let mut since = critical_sinces.next().cloned()?;
2119        for s in critical_sinces {
2120            since.meet_assign(s);
2121        }
2122        Some(since)
2123    }
2124
2125    fn update_since(&mut self) {
2126        let mut sinces_iter = self
2127            .leased_readers
2128            .values()
2129            .map(|x| &x.since)
2130            .chain(self.critical_readers.values().map(|x| &x.since));
2131        let mut since = match sinces_iter.next() {
2132            Some(since) => since.clone(),
2133            None => {
2134                // If there are no current readers, leave `since` unchanged so
2135                // it doesn't regress.
2136                return;
2137            }
2138        };
2139        while let Some(s) = sinces_iter.next() {
2140            since.meet_assign(s);
2141        }
2142        self.trace.downgrade_since(&since);
2143    }
2144
2145    fn seqno_since(&self, seqno: SeqNo) -> SeqNo {
2146        let mut seqno_since = seqno;
2147        for cap in self.leased_readers.values() {
2148            seqno_since = std::cmp::min(seqno_since, cap.seqno);
2149        }
2150        // critical_readers don't hold a seqno capability.
2151        seqno_since
2152    }
2153
2154    fn tombstone_batch() -> HollowBatch<T> {
2155        HollowBatch::empty(Description::new(
2156            Antichain::from_elem(T::minimum()),
2157            Antichain::new(),
2158            Antichain::new(),
2159        ))
2160    }
2161
2162    pub(crate) fn is_tombstone(&self) -> bool {
2163        self.trace.upper().is_empty()
2164            && self.trace.since().is_empty()
2165            && self.writers.is_empty()
2166            && self.leased_readers.is_empty()
2167            && self.critical_readers.is_empty()
2168    }
2169
2170    pub(crate) fn is_single_empty_batch(&self) -> bool {
2171        let mut batch_count = 0;
2172        let mut is_empty = true;
2173        self.trace.map_batches(|b| {
2174            batch_count += 1;
2175            is_empty &= b.is_empty()
2176        });
2177        batch_count <= 1 && is_empty
2178    }
2179
2180    pub fn become_tombstone_and_shrink(&mut self) -> ControlFlow<NoOpStateTransition<()>, ()> {
2181        assert_eq!(self.trace.upper(), &Antichain::new());
2182        assert_eq!(self.trace.since(), &Antichain::new());
2183
2184        // Remember our current state, so we can decide whether we have to
2185        // record a transition in durable state.
2186        let was_tombstone = self.is_tombstone();
2187
2188        // Enter the "tombstone" state, if we're not in it already.
2189        self.writers.clear();
2190        self.leased_readers.clear();
2191        self.critical_readers.clear();
2192
2193        mz_ore::soft_assert_no_log!(self.is_tombstone());
2194
2195        // Now that we're in a "tombstone" state -- ie. nobody can read the data from a shard or write to
2196        // it -- the actual contents of our batches no longer matter.
2197        // This method progressively replaces batches in our state with simpler versions, to allow
2198        // freeing up resources and to reduce the state size. (Since the state is unreadable, this
2199        // is not visible to clients.) We do this a little bit at a time to avoid really large state
2200        // transitions... most operations happen incrementally, and large single writes can overwhelm
2201        // a backing store. See comments for why we believe the relevant diffs are reasonably small.
2202
2203        let mut to_replace = None;
2204        let mut batch_count = 0;
2205        self.trace.map_batches(|b| {
2206            batch_count += 1;
2207            if !b.is_empty() && to_replace.is_none() {
2208                to_replace = Some(b.desc.clone());
2209            }
2210        });
2211        if let Some(desc) = to_replace {
2212            // We have a nonempty batch: replace it with an empty batch and return.
2213            // This should not produce an excessively large diff: if it did, we wouldn't have been
2214            // able to append that batch in the first place.
2215            let result = self.trace.apply_tombstone_merge(&desc);
2216            assert!(
2217                result.matched(),
2218                "merge with a matching desc should always match"
2219            );
2220            Continue(())
2221        } else if batch_count > 1 {
2222            // All our batches are empty, but we have more than one of them. Replace the whole set
2223            // with a new single-batch trace.
2224            // This produces a diff with a size proportional to the number of batches, but since
2225            // Spine keeps a logarithmic number of batches this should never be excessively large.
2226            let mut new_trace = Trace::default();
2227            new_trace.downgrade_since(&Antichain::new());
2228            let merge_reqs = new_trace.push_batch(Self::tombstone_batch());
2229            assert_eq!(merge_reqs, Vec::new());
2230            self.trace = new_trace;
2231            Continue(())
2232        } else if !was_tombstone {
2233            // We were not tombstoned before, so have to make sure this state
2234            // transition is recorded.
2235            Continue(())
2236        } else {
2237            // All our batches are empty, and there's only one... there's no shrinking this
2238            // tombstone further.
2239            Break(NoOpStateTransition(()))
2240        }
2241    }
2242}
2243
2244// TODO: Document invariants.
2245#[derive(Debug)]
2246#[cfg_attr(any(test, debug_assertions), derive(Clone, PartialEq))]
2247pub struct State<T> {
2248    pub(crate) shard_id: ShardId,
2249
2250    pub(crate) seqno: SeqNo,
2251    /// A strictly increasing wall time of when this state was written, in
2252    /// milliseconds since the unix epoch.
2253    pub(crate) walltime_ms: u64,
2254    /// Hostname of the persist user that created this version of state. For
2255    /// debugging.
2256    pub(crate) hostname: String,
2257    pub(crate) collections: StateCollections<T>,
2258}
2259
2260/// A newtype wrapper of State that guarantees the K, V, and D codecs match the
2261/// ones in durable storage.
2262pub struct TypedState<K, V, T, D> {
2263    pub(crate) state: State<T>,
2264
2265    // According to the docs, PhantomData is to "mark things that act like they
2266    // own a T". State doesn't actually own K, V, or D, just the ability to
2267    // produce them. Using the `fn() -> T` pattern gets us the same variance as
2268    // T [1], but also allows State to correctly derive Send+Sync.
2269    //
2270    // [1]:
2271    //     https://doc.rust-lang.org/nomicon/phantom-data.html#table-of-phantomdata-patterns
2272    pub(crate) _phantom: PhantomData<fn() -> (K, V, D)>,
2273}
2274
2275impl<K, V, T: Clone, D> TypedState<K, V, T, D> {
2276    #[cfg(any(test, debug_assertions))]
2277    pub(crate) fn clone(&self, hostname: String) -> Self {
2278        TypedState {
2279            state: State {
2280                shard_id: self.shard_id.clone(),
2281                seqno: self.seqno.clone(),
2282                walltime_ms: self.walltime_ms,
2283                hostname,
2284                collections: self.collections.clone(),
2285            },
2286            _phantom: PhantomData,
2287        }
2288    }
2289
2290    pub(crate) fn clone_for_rollup(&self) -> Self {
2291        TypedState {
2292            state: State {
2293                shard_id: self.shard_id.clone(),
2294                seqno: self.seqno.clone(),
2295                walltime_ms: self.walltime_ms,
2296                hostname: self.hostname.clone(),
2297                collections: self.collections.clone(),
2298            },
2299            _phantom: PhantomData,
2300        }
2301    }
2302}
2303
2304impl<K, V, T: Debug, D> Debug for TypedState<K, V, T, D> {
2305    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2306        // Deconstruct self so we get a compile failure if new fields
2307        // are added.
2308        let TypedState { state, _phantom } = self;
2309        f.debug_struct("TypedState").field("state", state).finish()
2310    }
2311}
2312
2313// Impl PartialEq regardless of the type params.
2314#[cfg(any(test, debug_assertions))]
2315impl<K, V, T: PartialEq, D> PartialEq for TypedState<K, V, T, D> {
2316    fn eq(&self, other: &Self) -> bool {
2317        // Deconstruct self and other so we get a compile failure if new fields
2318        // are added.
2319        let TypedState {
2320            state: self_state,
2321            _phantom,
2322        } = self;
2323        let TypedState {
2324            state: other_state,
2325            _phantom,
2326        } = other;
2327        self_state == other_state
2328    }
2329}
2330
2331impl<K, V, T, D> Deref for TypedState<K, V, T, D> {
2332    type Target = State<T>;
2333
2334    fn deref(&self) -> &Self::Target {
2335        &self.state
2336    }
2337}
2338
2339impl<K, V, T, D> DerefMut for TypedState<K, V, T, D> {
2340    fn deref_mut(&mut self) -> &mut Self::Target {
2341        &mut self.state
2342    }
2343}
2344
2345impl<K, V, T, D> TypedState<K, V, T, D>
2346where
2347    K: Codec,
2348    V: Codec,
2349    T: Timestamp + Lattice + Codec64,
2350    D: Codec64,
2351{
2352    pub fn new(
2353        applier_version: Version,
2354        shard_id: ShardId,
2355        hostname: String,
2356        walltime_ms: u64,
2357    ) -> Self {
2358        let state = State {
2359            shard_id,
2360            seqno: SeqNo::minimum(),
2361            walltime_ms,
2362            hostname,
2363            collections: StateCollections {
2364                version: applier_version,
2365                last_gc_req: SeqNo::minimum(),
2366                rollups: BTreeMap::new(),
2367                active_rollup: None,
2368                active_gc: None,
2369                leased_readers: BTreeMap::new(),
2370                critical_readers: BTreeMap::new(),
2371                writers: BTreeMap::new(),
2372                schemas: BTreeMap::new(),
2373                trace: Trace::default(),
2374            },
2375        };
2376        TypedState {
2377            state,
2378            _phantom: PhantomData,
2379        }
2380    }
2381
2382    pub fn clone_apply<R, E, WorkFn>(
2383        &self,
2384        cfg: &PersistConfig,
2385        work_fn: &mut WorkFn,
2386    ) -> ControlFlow<E, (R, Self)>
2387    where
2388        WorkFn: FnMut(SeqNo, &PersistConfig, &mut StateCollections<T>) -> ControlFlow<E, R>,
2389    {
2390        // We do not increment the version by default, though work_fn can if it chooses to.
2391        let mut new_state = State {
2392            shard_id: self.shard_id,
2393            seqno: self.seqno.next(),
2394            walltime_ms: (cfg.now)(),
2395            hostname: cfg.hostname.clone(),
2396            collections: self.collections.clone(),
2397        };
2398
2399        // Make sure walltime_ms is strictly increasing, in case clocks are
2400        // offset.
2401        if new_state.walltime_ms <= self.walltime_ms {
2402            new_state.walltime_ms = self.walltime_ms + 1;
2403        }
2404
2405        let work_ret = work_fn(new_state.seqno, cfg, &mut new_state.collections)?;
2406        let new_state = TypedState {
2407            state: new_state,
2408            _phantom: PhantomData,
2409        };
2410        Continue((work_ret, new_state))
2411    }
2412}
2413
2414#[derive(Copy, Clone, Debug)]
2415pub struct GcConfig {
2416    pub use_active_gc: bool,
2417    pub fallback_threshold_ms: u64,
2418    pub min_versions: usize,
2419    pub max_versions: usize,
2420}
2421
2422impl<T> State<T>
2423where
2424    T: Timestamp + Lattice + Codec64,
2425{
2426    pub fn shard_id(&self) -> ShardId {
2427        self.shard_id
2428    }
2429
2430    pub fn seqno(&self) -> SeqNo {
2431        self.seqno
2432    }
2433
2434    pub fn since(&self) -> &Antichain<T> {
2435        self.collections.trace.since()
2436    }
2437
2438    pub fn upper(&self) -> &Antichain<T> {
2439        self.collections.trace.upper()
2440    }
2441
2442    pub fn spine_batch_count(&self) -> usize {
2443        self.collections.trace.num_spine_batches()
2444    }
2445
2446    pub fn size_metrics(&self) -> StateSizeMetrics {
2447        let mut ret = StateSizeMetrics::default();
2448        self.blobs().for_each(|x| match x {
2449            HollowBlobRef::Batch(x) => {
2450                ret.hollow_batch_count += 1;
2451                ret.batch_part_count += x.part_count();
2452                ret.num_updates += x.len;
2453
2454                let batch_size = x.encoded_size_bytes();
2455                for x in x.parts.iter() {
2456                    if x.ts_rewrite().is_some() {
2457                        ret.rewrite_part_count += 1;
2458                    }
2459                    if x.is_inline() {
2460                        ret.inline_part_count += 1;
2461                        ret.inline_part_bytes += x.inline_bytes();
2462                    }
2463                }
2464                ret.largest_batch_bytes = std::cmp::max(ret.largest_batch_bytes, batch_size);
2465                ret.state_batches_bytes += batch_size;
2466            }
2467            HollowBlobRef::Rollup(x) => {
2468                ret.state_rollup_count += 1;
2469                ret.state_rollups_bytes += x.encoded_size_bytes.unwrap_or_default()
2470            }
2471        });
2472        ret
2473    }
2474
2475    pub fn latest_rollup(&self) -> (&SeqNo, &HollowRollup) {
2476        // We maintain the invariant that every version of state has at least
2477        // one rollup.
2478        self.collections
2479            .rollups
2480            .iter()
2481            .rev()
2482            .next()
2483            .expect("State should have at least one rollup if seqno > minimum")
2484    }
2485
2486    pub(crate) fn seqno_since(&self) -> SeqNo {
2487        self.collections.seqno_since(self.seqno)
2488    }
2489
2490    // Returns whether the cmd proposing this state has been selected to perform
2491    // background garbage collection work.
2492    //
2493    // If it was selected, this information is recorded in the state itself for
2494    // commit along with the cmd's state transition. This helps us to avoid
2495    // redundant work.
2496    //
2497    // Correctness does not depend on a gc assignment being executed, nor on
2498    // them being executed in the order they are given. But it is expected that
2499    // gc assignments are best-effort respected. In practice, cmds like
2500    // register_foo or expire_foo, where it would be awkward, ignore gc.
2501    pub fn maybe_gc(&mut self, is_write: bool, now: u64, cfg: GcConfig) -> Option<GcReq> {
2502        let GcConfig {
2503            use_active_gc,
2504            fallback_threshold_ms,
2505            min_versions,
2506            max_versions,
2507        } = cfg;
2508        // This is an arbitrary-ish threshold that scales with seqno, but never
2509        // gets particularly big. It probably could be much bigger and certainly
2510        // could use a tuning pass at some point.
2511        let gc_threshold = if use_active_gc {
2512            u64::cast_from(min_versions)
2513        } else {
2514            std::cmp::max(
2515                1,
2516                u64::cast_from(self.seqno.0.next_power_of_two().trailing_zeros()),
2517            )
2518        };
2519        let new_seqno_since = self.seqno_since();
2520        // Collect until the new seqno since... or the old since plus the max number of versions,
2521        // whatever is less.
2522        let gc_until_seqno = new_seqno_since.min(SeqNo(
2523            self.collections
2524                .last_gc_req
2525                .0
2526                .saturating_add(u64::cast_from(max_versions)),
2527        ));
2528        let should_gc = new_seqno_since
2529            .0
2530            .saturating_sub(self.collections.last_gc_req.0)
2531            >= gc_threshold;
2532
2533        // If we wouldn't otherwise gc, check if we have an active gc. If we do, and
2534        // it's been a while since it started, we should gc.
2535        let should_gc = if use_active_gc && !should_gc {
2536            match self.collections.active_gc {
2537                Some(active_gc) => now.saturating_sub(active_gc.start_ms) > fallback_threshold_ms,
2538                None => false,
2539            }
2540        } else {
2541            should_gc
2542        };
2543        // Assign GC traffic preferentially to writers, falling back to anyone
2544        // generating new state versions if there are no writers.
2545        let should_gc = should_gc && (is_write || self.collections.writers.is_empty());
2546        // Always assign GC work to a tombstoned shard to have the chance to
2547        // clean up any residual blobs. This is safe (won't cause excess gc)
2548        // as the only allowed command after becoming a tombstone is to write
2549        // the final rollup.
2550        let tombstone_needs_gc = self.collections.is_tombstone();
2551        let should_gc = should_gc || tombstone_needs_gc;
2552        let should_gc = if use_active_gc {
2553            // If we have an active gc, we should only gc if the active gc is
2554            // sufficiently old. This is to avoid doing more gc work than
2555            // necessary.
2556            should_gc
2557                && match self.collections.active_gc {
2558                    Some(active) => now.saturating_sub(active.start_ms) > fallback_threshold_ms,
2559                    None => true,
2560                }
2561        } else {
2562            should_gc
2563        };
2564        if should_gc {
2565            self.collections.last_gc_req = gc_until_seqno;
2566            Some(GcReq {
2567                shard_id: self.shard_id,
2568                new_seqno_since: gc_until_seqno,
2569            })
2570        } else {
2571            None
2572        }
2573    }
2574
2575    /// Return the number of gc-ineligible state versions.
2576    pub fn seqnos_held(&self) -> usize {
2577        usize::cast_from(self.seqno.0.saturating_sub(self.seqno_since().0))
2578    }
2579
2580    /// Expire all readers and writers up to the given walltime_ms.
2581    pub fn expire_at(&mut self, walltime_ms: EpochMillis) -> ExpiryMetrics {
2582        let mut metrics = ExpiryMetrics::default();
2583        let shard_id = self.shard_id();
2584        self.collections.leased_readers.retain(|id, state| {
2585            let retain = state.last_heartbeat_timestamp_ms + state.lease_duration_ms >= walltime_ms;
2586            if !retain {
2587                info!(
2588                    "Force expiring reader {id} ({}) of shard {shard_id} due to inactivity",
2589                    state.debug.purpose
2590                );
2591                metrics.readers_expired += 1;
2592            }
2593            retain
2594        });
2595        // critical_readers don't need forced expiration. (In fact, that's the point!)
2596        self.collections.writers.retain(|id, state| {
2597            let retain =
2598                (state.last_heartbeat_timestamp_ms + state.lease_duration_ms) >= walltime_ms;
2599            if !retain {
2600                info!(
2601                    "Force expiring writer {id} ({}) of shard {shard_id} due to inactivity",
2602                    state.debug.purpose
2603                );
2604                metrics.writers_expired += 1;
2605            }
2606            retain
2607        });
2608        metrics
2609    }
2610
2611    /// Returns the batches that contain updates up to (and including) the given `as_of`. The
2612    /// result `Vec` contains blob keys, along with a [`Description`] of what updates in the
2613    /// referenced parts are valid to read.
2614    pub fn snapshot(&self, as_of: &Antichain<T>) -> Result<Vec<HollowBatch<T>>, SnapshotErr<T>> {
2615        if PartialOrder::less_than(as_of, self.collections.trace.since()) {
2616            return Err(SnapshotErr::AsOfHistoricalDistinctionsLost(Since(
2617                self.collections.trace.since().clone(),
2618            )));
2619        }
2620        let upper = self.collections.trace.upper();
2621        if PartialOrder::less_equal(upper, as_of) {
2622            return Err(SnapshotErr::AsOfNotYetAvailable(
2623                self.seqno,
2624                Upper(upper.clone()),
2625            ));
2626        }
2627
2628        let batches = self
2629            .collections
2630            .trace
2631            .batches()
2632            .filter(|b| !PartialOrder::less_than(as_of, b.desc.lower()))
2633            .cloned()
2634            .collect();
2635        Ok(batches)
2636    }
2637
2638    // NB: Unlike the other methods here, this one is read-only.
2639    pub fn verify_listen(&self, as_of: &Antichain<T>) -> Result<(), Since<T>> {
2640        if PartialOrder::less_than(as_of, self.collections.trace.since()) {
2641            return Err(Since(self.collections.trace.since().clone()));
2642        }
2643        Ok(())
2644    }
2645
2646    pub fn next_listen_batch(&self, frontier: &Antichain<T>) -> Result<HollowBatch<T>, SeqNo> {
2647        // TODO: Avoid the O(n^2) here: `next_listen_batch` is called once per
2648        // batch and this iterates through all batches to find the next one.
2649        self.collections
2650            .trace
2651            .batches()
2652            .find(|b| {
2653                PartialOrder::less_equal(b.desc.lower(), frontier)
2654                    && PartialOrder::less_than(frontier, b.desc.upper())
2655            })
2656            .cloned()
2657            .ok_or(self.seqno)
2658    }
2659
2660    pub fn active_rollup(&self) -> Option<ActiveRollup> {
2661        self.collections.active_rollup
2662    }
2663
2664    pub fn need_rollup(
2665        &self,
2666        threshold: usize,
2667        use_active_rollup: bool,
2668        fallback_threshold_ms: u64,
2669        now: u64,
2670    ) -> Option<SeqNo> {
2671        let (latest_rollup_seqno, _) = self.latest_rollup();
2672
2673        // Tombstoned shards require one final rollup. However, because we
2674        // write a rollup as of SeqNo X and then link it in using a state
2675        // transition (in this case from X to X+1), the minimum number of
2676        // live diffs is actually two. Detect when we're in this minimal
2677        // two diff state and stop the (otherwise) infinite iteration.
2678        if self.collections.is_tombstone() && latest_rollup_seqno.next() < self.seqno {
2679            return Some(self.seqno);
2680        }
2681
2682        let seqnos_since_last_rollup = self.seqno.0.saturating_sub(latest_rollup_seqno.0);
2683
2684        if use_active_rollup {
2685            // If sequnos_since_last_rollup>threshold, and there is no existing rollup in progress,
2686            // we should start a new rollup.
2687            // If there is an active rollup, we should check if it has been running too long.
2688            // If it has, we should start a new rollup.
2689            // This is to guard against a worker dying/taking too long/etc.
2690            if seqnos_since_last_rollup > u64::cast_from(threshold) {
2691                match self.active_rollup() {
2692                    Some(active_rollup) => {
2693                        if now.saturating_sub(active_rollup.start_ms) > fallback_threshold_ms {
2694                            return Some(self.seqno);
2695                        }
2696                    }
2697                    None => {
2698                        return Some(self.seqno);
2699                    }
2700                }
2701            }
2702        } else {
2703            // every `threshold` seqnos since the latest rollup, assign rollup maintenance.
2704            // we avoid assigning rollups to every seqno past the threshold to avoid handles
2705            // racing / performing redundant work.
2706            if seqnos_since_last_rollup > 0
2707                && seqnos_since_last_rollup % u64::cast_from(threshold) == 0
2708            {
2709                return Some(self.seqno);
2710            }
2711
2712            // however, since maintenance is best-effort and could fail, do assign rollup
2713            // work to every seqno after a fallback threshold to ensure one is written.
2714            if seqnos_since_last_rollup
2715                > u64::cast_from(
2716                    threshold * PersistConfig::DEFAULT_FALLBACK_ROLLUP_THRESHOLD_MULTIPLIER,
2717                )
2718            {
2719                return Some(self.seqno);
2720            }
2721        }
2722
2723        None
2724    }
2725
2726    pub(crate) fn blobs(&self) -> impl Iterator<Item = HollowBlobRef<'_, T>> {
2727        let batches = self.collections.trace.batches().map(HollowBlobRef::Batch);
2728        let rollups = self.collections.rollups.values().map(HollowBlobRef::Rollup);
2729        batches.chain(rollups)
2730    }
2731}
2732
2733fn serialize_part_bytes<S: Serializer>(val: &[u8], s: S) -> Result<S::Ok, S::Error> {
2734    let val = hex::encode(val);
2735    val.serialize(s)
2736}
2737
2738fn serialize_lazy_proto<S: Serializer, T: prost::Message + Default>(
2739    val: &Option<LazyProto<T>>,
2740    s: S,
2741) -> Result<S::Ok, S::Error> {
2742    val.as_ref()
2743        .map(|lazy| hex::encode(&lazy.into_proto()))
2744        .serialize(s)
2745}
2746
2747fn serialize_part_stats<S: Serializer>(
2748    val: &Option<LazyPartStats>,
2749    s: S,
2750) -> Result<S::Ok, S::Error> {
2751    // These bytes come from blob and are never validated on the way in, so a
2752    // malformed or newer-version encoding reaches here intact. Report it as
2753    // absent rather than panicking, and keep the field's shape stable for
2754    // consumers of the inspect-state output by logging the failure instead of
2755    // serializing a differently typed value in its place.
2756    let stats = val.as_ref().and_then(|x| match x.try_decode() {
2757        Ok(stats) => Some(stats.key),
2758        Err(err) => {
2759            tracing::warn!("undecodable part stats, reporting as absent: {err}");
2760            None
2761        }
2762    });
2763    stats.serialize(s)
2764}
2765
2766fn serialize_diffs_sum<S: Serializer>(val: &Option<[u8; 8]>, s: S) -> Result<S::Ok, S::Error> {
2767    // This is only used for debugging, so hack to assume that D is i64.
2768    let val = val.map(i64::decode);
2769    val.serialize(s)
2770}
2771
2772// This Serialize impl is used for debugging/testing and exposed via SQL. It's
2773// intentionally gated from users, so not strictly subject to our backward
2774// compatibility guarantees, but still probably best to be thoughtful about
2775// making unnecessary changes. Additionally, it's nice to make the output as
2776// nice to use as possible without tying our hands for the actual code usages.
2777impl<T: Serialize + Timestamp + Lattice> Serialize for State<T> {
2778    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
2779        let State {
2780            shard_id,
2781            seqno,
2782            walltime_ms,
2783            hostname,
2784            collections:
2785                StateCollections {
2786                    version: applier_version,
2787                    last_gc_req,
2788                    rollups,
2789                    active_rollup,
2790                    active_gc,
2791                    leased_readers,
2792                    critical_readers,
2793                    writers,
2794                    schemas,
2795                    trace,
2796                },
2797        } = self;
2798        let mut s = s.serialize_struct("State", 13)?;
2799        let () = s.serialize_field("applier_version", &applier_version.to_string())?;
2800        let () = s.serialize_field("shard_id", shard_id)?;
2801        let () = s.serialize_field("seqno", seqno)?;
2802        let () = s.serialize_field("walltime_ms", walltime_ms)?;
2803        let () = s.serialize_field("hostname", hostname)?;
2804        let () = s.serialize_field("last_gc_req", last_gc_req)?;
2805        let () = s.serialize_field("rollups", rollups)?;
2806        let () = s.serialize_field("active_rollup", active_rollup)?;
2807        let () = s.serialize_field("active_gc", active_gc)?;
2808        let () = s.serialize_field("leased_readers", leased_readers)?;
2809        let () = s.serialize_field("critical_readers", critical_readers)?;
2810        let () = s.serialize_field("writers", writers)?;
2811        let () = s.serialize_field("schemas", schemas)?;
2812        let () = s.serialize_field("since", &trace.since().elements())?;
2813        let () = s.serialize_field("upper", &trace.upper().elements())?;
2814        let trace = trace.flatten();
2815        let () = s.serialize_field("batches", &trace.legacy_batches.keys().collect::<Vec<_>>())?;
2816        let () = s.serialize_field("hollow_batches", &trace.hollow_batches)?;
2817        let () = s.serialize_field("spine_batches", &trace.spine_batches)?;
2818        let () = s.serialize_field("merges", &trace.merges)?;
2819        s.end()
2820    }
2821}
2822
2823#[derive(Debug, Default)]
2824pub struct StateSizeMetrics {
2825    pub hollow_batch_count: usize,
2826    pub batch_part_count: usize,
2827    pub rewrite_part_count: usize,
2828    pub num_updates: usize,
2829    pub largest_batch_bytes: usize,
2830    pub state_batches_bytes: usize,
2831    pub state_rollups_bytes: usize,
2832    pub state_rollup_count: usize,
2833    pub inline_part_count: usize,
2834    pub inline_part_bytes: usize,
2835}
2836
2837#[derive(Default)]
2838pub struct ExpiryMetrics {
2839    pub(crate) readers_expired: usize,
2840    pub(crate) writers_expired: usize,
2841}
2842
2843/// Wrapper for Antichain that represents a Since
2844#[derive(Debug, Clone, PartialEq)]
2845pub struct Since<T>(pub Antichain<T>);
2846
2847/// Wrapper for Antichain that represents an Upper
2848#[derive(Debug, PartialEq)]
2849pub struct Upper<T>(pub Antichain<T>);
2850
2851#[cfg(test)]
2852pub(crate) mod tests {
2853    use std::ops::Range;
2854    use std::str::FromStr;
2855
2856    use bytes::Bytes;
2857    use mz_build_info::DUMMY_BUILD_INFO;
2858    use mz_dyncfg::ConfigUpdates;
2859    use mz_ore::now::SYSTEM_TIME;
2860    use mz_ore::{assert_none, assert_ok};
2861    use mz_proto::RustType;
2862    use proptest::prelude::*;
2863    use proptest::strategy::ValueTree;
2864
2865    use crate::InvalidUsage::{InvalidBounds, InvalidEmptyTimeInterval};
2866    use crate::cache::PersistClientCache;
2867    use crate::internal::encoding::any_some_lazy_part_stats;
2868    use crate::internal::paths::RollupId;
2869    use crate::internal::trace::tests::any_trace;
2870    use crate::tests::new_test_client_cache;
2871    use crate::{Diagnostics, PersistLocation};
2872
2873    use super::*;
2874
2875    const LEASE_DURATION_MS: u64 = 900 * 1000;
2876    fn debug_state() -> HandleDebugState {
2877        HandleDebugState {
2878            hostname: "debug".to_owned(),
2879            purpose: "finding the bugs".to_owned(),
2880        }
2881    }
2882
2883    pub fn any_hollow_batch_with_exact_runs<T: Arbitrary + Timestamp>(
2884        num_runs: usize,
2885    ) -> impl Strategy<Value = HollowBatch<T>> {
2886        (
2887            any::<T>(),
2888            any::<T>(),
2889            any::<T>(),
2890            proptest::collection::vec(any_run_part::<T>(), num_runs + 1..20),
2891            any::<usize>(),
2892        )
2893            .prop_map(move |(t0, t1, since, parts, len)| {
2894                let (lower, upper) = if t0 <= t1 {
2895                    (Antichain::from_elem(t0), Antichain::from_elem(t1))
2896                } else {
2897                    (Antichain::from_elem(t1), Antichain::from_elem(t0))
2898                };
2899                let since = Antichain::from_elem(since);
2900
2901                let run_splits = (1..num_runs)
2902                    .map(|i| i * parts.len() / num_runs)
2903                    .collect::<Vec<_>>();
2904
2905                let run_meta = (0..num_runs)
2906                    .map(|_| {
2907                        let mut meta = RunMeta::default();
2908                        meta.id = Some(RunId::new());
2909                        meta
2910                    })
2911                    .collect::<Vec<_>>();
2912
2913                HollowBatch::new(
2914                    Description::new(lower, upper, since),
2915                    parts,
2916                    len % 10,
2917                    run_meta,
2918                    run_splits,
2919                )
2920            })
2921    }
2922
2923    pub fn any_hollow_batch<T: Arbitrary + Timestamp>() -> impl Strategy<Value = HollowBatch<T>> {
2924        Strategy::prop_map(
2925            (
2926                any::<T>(),
2927                any::<T>(),
2928                any::<T>(),
2929                proptest::collection::vec(any_run_part::<T>(), 0..20),
2930                any::<usize>(),
2931                0..=10usize,
2932                proptest::collection::vec(any::<RunId>(), 10),
2933            ),
2934            |(t0, t1, since, parts, len, num_runs, run_ids)| {
2935                let (lower, upper) = if t0 <= t1 {
2936                    (Antichain::from_elem(t0), Antichain::from_elem(t1))
2937                } else {
2938                    (Antichain::from_elem(t1), Antichain::from_elem(t0))
2939                };
2940                let since = Antichain::from_elem(since);
2941                if num_runs > 0 && parts.len() > 2 && num_runs < parts.len() {
2942                    let run_splits = (1..num_runs)
2943                        .map(|i| i * parts.len() / num_runs)
2944                        .collect::<Vec<_>>();
2945
2946                    let run_meta = (0..num_runs)
2947                        .enumerate()
2948                        .map(|(i, _)| {
2949                            let mut meta = RunMeta::default();
2950                            meta.id = Some(run_ids[i]);
2951                            meta
2952                        })
2953                        .collect::<Vec<_>>();
2954
2955                    HollowBatch::new(
2956                        Description::new(lower, upper, since),
2957                        parts,
2958                        len % 10,
2959                        run_meta,
2960                        run_splits,
2961                    )
2962                } else {
2963                    HollowBatch::new_run_for_test(
2964                        Description::new(lower, upper, since),
2965                        parts,
2966                        len % 10,
2967                        run_ids[0],
2968                    )
2969                }
2970            },
2971        )
2972    }
2973
2974    pub fn any_batch_part<T: Arbitrary + Timestamp>() -> impl Strategy<Value = BatchPart<T>> {
2975        Strategy::prop_map(
2976            (
2977                any::<bool>(),
2978                any_hollow_batch_part(),
2979                any::<Option<T>>(),
2980                any::<Option<SchemaId>>(),
2981                any::<Option<SchemaId>>(),
2982            ),
2983            |(is_hollow, hollow, ts_rewrite, schema_id, deprecated_schema_id)| {
2984                if is_hollow {
2985                    BatchPart::Hollow(hollow)
2986                } else {
2987                    let updates = LazyInlineBatchPart::from_proto(Bytes::new()).unwrap();
2988                    let ts_rewrite = ts_rewrite.map(Antichain::from_elem);
2989                    BatchPart::Inline {
2990                        updates,
2991                        ts_rewrite,
2992                        schema_id,
2993                        deprecated_schema_id,
2994                    }
2995                }
2996            },
2997        )
2998    }
2999
3000    pub fn any_run_part<T: Arbitrary + Timestamp>() -> impl Strategy<Value = RunPart<T>> {
3001        Strategy::prop_map(any_batch_part(), |part| RunPart::Single(part))
3002    }
3003
3004    pub fn any_hollow_batch_part<T: Arbitrary + Timestamp>()
3005    -> impl Strategy<Value = HollowBatchPart<T>> {
3006        Strategy::prop_map(
3007            (
3008                any::<PartialBatchKey>(),
3009                any::<usize>(),
3010                any::<Vec<u8>>(),
3011                any_some_lazy_part_stats(),
3012                any::<Option<T>>(),
3013                any::<[u8; 8]>(),
3014                any::<Option<BatchColumnarFormat>>(),
3015                any::<Option<SchemaId>>(),
3016                any::<Option<SchemaId>>(),
3017            ),
3018            |(
3019                key,
3020                encoded_size_bytes,
3021                key_lower,
3022                stats,
3023                ts_rewrite,
3024                diffs_sum,
3025                format,
3026                schema_id,
3027                deprecated_schema_id,
3028            )| {
3029                HollowBatchPart {
3030                    key,
3031                    meta: Default::default(),
3032                    encoded_size_bytes,
3033                    key_lower,
3034                    structured_key_lower: None,
3035                    stats,
3036                    ts_rewrite: ts_rewrite.map(Antichain::from_elem),
3037                    diffs_sum: Some(diffs_sum),
3038                    format,
3039                    schema_id,
3040                    deprecated_schema_id,
3041                }
3042            },
3043        )
3044    }
3045
3046    pub fn any_leased_reader_state<T: Arbitrary>() -> impl Strategy<Value = LeasedReaderState<T>> {
3047        Strategy::prop_map(
3048            (
3049                any::<SeqNo>(),
3050                any::<Option<T>>(),
3051                any::<u64>(),
3052                any::<u64>(),
3053                any::<HandleDebugState>(),
3054            ),
3055            |(seqno, since, last_heartbeat_timestamp_ms, mut lease_duration_ms, debug)| {
3056                // lease_duration_ms of 0 means this state was written by an old
3057                // version of code, which means we'll migrate it in the decode
3058                // path. Avoid.
3059                if lease_duration_ms == 0 {
3060                    lease_duration_ms += 1;
3061                }
3062                LeasedReaderState {
3063                    seqno,
3064                    since: since.map_or_else(Antichain::new, Antichain::from_elem),
3065                    last_heartbeat_timestamp_ms,
3066                    lease_duration_ms,
3067                    debug,
3068                }
3069            },
3070        )
3071    }
3072
3073    pub fn any_critical_reader_state<T>() -> impl Strategy<Value = CriticalReaderState<T>>
3074    where
3075        T: Arbitrary,
3076    {
3077        Strategy::prop_map(
3078            (
3079                any::<Option<T>>(),
3080                any::<Opaque>(),
3081                any::<HandleDebugState>(),
3082            ),
3083            |(since, opaque, debug)| CriticalReaderState {
3084                since: since.map_or_else(Antichain::new, Antichain::from_elem),
3085                opaque,
3086                debug,
3087            },
3088        )
3089    }
3090
3091    pub fn any_writer_state<T: Arbitrary>() -> impl Strategy<Value = WriterState<T>> {
3092        Strategy::prop_map(
3093            (
3094                any::<u64>(),
3095                any::<u64>(),
3096                any::<IdempotencyToken>(),
3097                any::<Option<T>>(),
3098                any::<HandleDebugState>(),
3099            ),
3100            |(
3101                last_heartbeat_timestamp_ms,
3102                lease_duration_ms,
3103                most_recent_write_token,
3104                most_recent_write_upper,
3105                debug,
3106            )| WriterState {
3107                last_heartbeat_timestamp_ms,
3108                lease_duration_ms,
3109                most_recent_write_token,
3110                most_recent_write_upper: most_recent_write_upper
3111                    .map_or_else(Antichain::new, Antichain::from_elem),
3112                debug,
3113            },
3114        )
3115    }
3116
3117    pub fn any_encoded_schemas() -> impl Strategy<Value = EncodedSchemas> {
3118        Strategy::prop_map(
3119            (
3120                any::<Vec<u8>>(),
3121                any::<Vec<u8>>(),
3122                any::<Vec<u8>>(),
3123                any::<Vec<u8>>(),
3124            ),
3125            |(key, key_data_type, val, val_data_type)| EncodedSchemas {
3126                key: Bytes::from(key),
3127                key_data_type: Bytes::from(key_data_type),
3128                val: Bytes::from(val),
3129                val_data_type: Bytes::from(val_data_type),
3130            },
3131        )
3132    }
3133
3134    pub fn any_state<T: Arbitrary + Timestamp + Lattice>(
3135        num_trace_batches: Range<usize>,
3136    ) -> impl Strategy<Value = State<T>> {
3137        let part1 = (
3138            any::<ShardId>(),
3139            any::<SeqNo>(),
3140            any::<u64>(),
3141            any::<String>(),
3142            any::<SeqNo>(),
3143            proptest::collection::btree_map(any::<SeqNo>(), any::<HollowRollup>(), 1..3),
3144            proptest::option::of(any::<ActiveRollup>()),
3145        );
3146
3147        let part2 = (
3148            proptest::option::of(any::<ActiveGc>()),
3149            proptest::collection::btree_map(
3150                any::<LeasedReaderId>(),
3151                any_leased_reader_state::<T>(),
3152                1..3,
3153            ),
3154            proptest::collection::btree_map(
3155                any::<CriticalReaderId>(),
3156                any_critical_reader_state::<T>(),
3157                1..3,
3158            ),
3159            proptest::collection::btree_map(any::<WriterId>(), any_writer_state::<T>(), 0..3),
3160            proptest::collection::btree_map(any::<SchemaId>(), any_encoded_schemas(), 0..3),
3161            any_trace::<T>(num_trace_batches),
3162        );
3163
3164        (part1, part2).prop_map(
3165            |(
3166                (shard_id, seqno, walltime_ms, hostname, last_gc_req, rollups, active_rollup),
3167                (active_gc, leased_readers, critical_readers, writers, schemas, trace),
3168            )| State {
3169                shard_id,
3170                seqno,
3171                walltime_ms,
3172                hostname,
3173                collections: StateCollections {
3174                    version: Version::new(1, 2, 3),
3175                    last_gc_req,
3176                    rollups,
3177                    active_rollup,
3178                    active_gc,
3179                    leased_readers,
3180                    critical_readers,
3181                    writers,
3182                    schemas,
3183                    trace,
3184                },
3185            },
3186        )
3187    }
3188
3189    pub(crate) fn hollow<T: Timestamp>(
3190        lower: T,
3191        upper: T,
3192        keys: &[&str],
3193        len: usize,
3194    ) -> HollowBatch<T> {
3195        HollowBatch::new_run(
3196            Description::new(
3197                Antichain::from_elem(lower),
3198                Antichain::from_elem(upper),
3199                Antichain::from_elem(T::minimum()),
3200            ),
3201            keys.iter()
3202                .map(|x| {
3203                    RunPart::Single(BatchPart::Hollow(HollowBatchPart {
3204                        key: PartialBatchKey((*x).to_owned()),
3205                        meta: Default::default(),
3206                        encoded_size_bytes: 0,
3207                        key_lower: vec![],
3208                        structured_key_lower: None,
3209                        stats: None,
3210                        ts_rewrite: None,
3211                        diffs_sum: None,
3212                        format: None,
3213                        schema_id: None,
3214                        deprecated_schema_id: None,
3215                    }))
3216                })
3217                .collect(),
3218            len,
3219        )
3220    }
3221
3222    #[mz_ore::test]
3223    fn downgrade_since() {
3224        let mut state = TypedState::<(), (), u64, i64>::new(
3225            DUMMY_BUILD_INFO.semver_version(),
3226            ShardId::new(),
3227            "".to_owned(),
3228            0,
3229        );
3230        let reader = LeasedReaderId::new();
3231        let seqno = SeqNo::minimum();
3232        let now = SYSTEM_TIME.clone();
3233        let _ = state.collections.register_leased_reader(
3234            "",
3235            &reader,
3236            "",
3237            seqno,
3238            Duration::from_secs(10),
3239            now(),
3240            false,
3241        );
3242
3243        // The shard global since == 0 initially.
3244        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(0));
3245
3246        // Greater
3247        assert_eq!(
3248            state.collections.downgrade_since(
3249                &reader,
3250                seqno,
3251                seqno,
3252                &Antichain::from_elem(2),
3253                now()
3254            ),
3255            Continue(Since(Antichain::from_elem(2)))
3256        );
3257        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3258        // Equal (no-op)
3259        assert_eq!(
3260            state.collections.downgrade_since(
3261                &reader,
3262                seqno,
3263                seqno,
3264                &Antichain::from_elem(2),
3265                now()
3266            ),
3267            Continue(Since(Antichain::from_elem(2)))
3268        );
3269        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3270        // Less (no-op)
3271        assert_eq!(
3272            state.collections.downgrade_since(
3273                &reader,
3274                seqno,
3275                seqno,
3276                &Antichain::from_elem(1),
3277                now()
3278            ),
3279            Continue(Since(Antichain::from_elem(2)))
3280        );
3281        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3282
3283        // Create a second reader.
3284        let reader2 = LeasedReaderId::new();
3285        let _ = state.collections.register_leased_reader(
3286            "",
3287            &reader2,
3288            "",
3289            seqno,
3290            Duration::from_secs(10),
3291            now(),
3292            false,
3293        );
3294
3295        // Shard since doesn't change until the meet (min) of all reader sinces changes.
3296        assert_eq!(
3297            state.collections.downgrade_since(
3298                &reader2,
3299                seqno,
3300                seqno,
3301                &Antichain::from_elem(3),
3302                now()
3303            ),
3304            Continue(Since(Antichain::from_elem(3)))
3305        );
3306        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3307        // Shard since == 3 when all readers have since >= 3.
3308        assert_eq!(
3309            state.collections.downgrade_since(
3310                &reader,
3311                seqno,
3312                seqno,
3313                &Antichain::from_elem(5),
3314                now()
3315            ),
3316            Continue(Since(Antichain::from_elem(5)))
3317        );
3318        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3319
3320        // Shard since unaffected readers with since > shard since expiring.
3321        assert_eq!(
3322            state.collections.expire_leased_reader(&reader),
3323            Continue(true)
3324        );
3325        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3326
3327        // Create a third reader.
3328        let reader3 = LeasedReaderId::new();
3329        let _ = state.collections.register_leased_reader(
3330            "",
3331            &reader3,
3332            "",
3333            seqno,
3334            Duration::from_secs(10),
3335            now(),
3336            false,
3337        );
3338
3339        // Shard since doesn't change until the meet (min) of all reader sinces changes.
3340        assert_eq!(
3341            state.collections.downgrade_since(
3342                &reader3,
3343                seqno,
3344                seqno,
3345                &Antichain::from_elem(10),
3346                now()
3347            ),
3348            Continue(Since(Antichain::from_elem(10)))
3349        );
3350        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3351
3352        // Shard since advances when reader with the minimal since expires.
3353        assert_eq!(
3354            state.collections.expire_leased_reader(&reader2),
3355            Continue(true)
3356        );
3357        // TODO(database-issues#6885): expiry temporarily doesn't advance since
3358        // Switch this assertion back when we re-enable this.
3359        //
3360        // assert_eq!(state.collections.trace.since(), &Antichain::from_elem(10));
3361        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3362
3363        // Shard since unaffected when all readers are expired.
3364        assert_eq!(
3365            state.collections.expire_leased_reader(&reader3),
3366            Continue(true)
3367        );
3368        // TODO(database-issues#6885): expiry temporarily doesn't advance since
3369        // Switch this assertion back when we re-enable this.
3370        //
3371        // assert_eq!(state.collections.trace.since(), &Antichain::from_elem(10));
3372        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3373    }
3374
3375    #[mz_ore::test]
3376    fn compare_and_downgrade_since() {
3377        let mut state = TypedState::<(), (), u64, i64>::new(
3378            DUMMY_BUILD_INFO.semver_version(),
3379            ShardId::new(),
3380            "".to_owned(),
3381            0,
3382        );
3383        let reader = CriticalReaderId::new();
3384        let _ = state
3385            .collections
3386            .register_critical_reader("", &reader, Opaque::encode(&0u64), "");
3387
3388        // The shard global since == 0 initially.
3389        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(0));
3390        // The initial opaque value should be set.
3391        assert_eq!(
3392            state
3393                .collections
3394                .critical_reader(&reader)
3395                .opaque
3396                .decode::<u64>(),
3397            u64::MIN
3398        );
3399
3400        // Greater
3401        assert_eq!(
3402            state.collections.compare_and_downgrade_since(
3403                &reader,
3404                &Opaque::encode(&0u64),
3405                (&Opaque::encode(&1u64), &Antichain::from_elem(2)),
3406            ),
3407            Continue(Ok(Since(Antichain::from_elem(2))))
3408        );
3409        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3410        assert_eq!(
3411            state
3412                .collections
3413                .critical_reader(&reader)
3414                .opaque
3415                .decode::<u64>(),
3416            1
3417        );
3418        // Equal (no-op)
3419        assert_eq!(
3420            state.collections.compare_and_downgrade_since(
3421                &reader,
3422                &Opaque::encode(&1u64),
3423                (&Opaque::encode(&2u64), &Antichain::from_elem(2)),
3424            ),
3425            Continue(Ok(Since(Antichain::from_elem(2))))
3426        );
3427        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3428        assert_eq!(
3429            state
3430                .collections
3431                .critical_reader(&reader)
3432                .opaque
3433                .decode::<u64>(),
3434            2
3435        );
3436        // Less (no-op)
3437        assert_eq!(
3438            state.collections.compare_and_downgrade_since(
3439                &reader,
3440                &Opaque::encode(&2u64),
3441                (&Opaque::encode(&3u64), &Antichain::from_elem(1)),
3442            ),
3443            Continue(Ok(Since(Antichain::from_elem(2))))
3444        );
3445        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3446        assert_eq!(
3447            state
3448                .collections
3449                .critical_reader(&reader)
3450                .opaque
3451                .decode::<u64>(),
3452            3
3453        );
3454    }
3455
3456    #[mz_ore::test]
3457    fn compare_and_append() {
3458        let state = &mut TypedState::<String, String, u64, i64>::new(
3459            DUMMY_BUILD_INFO.semver_version(),
3460            ShardId::new(),
3461            "".to_owned(),
3462            0,
3463        )
3464        .collections;
3465
3466        let writer_id = WriterId::new();
3467        let now = SYSTEM_TIME.clone();
3468
3469        // State is initially empty.
3470        assert_eq!(state.trace.num_spine_batches(), 0);
3471        assert_eq!(state.trace.num_hollow_batches(), 0);
3472        assert_eq!(state.trace.num_updates(), 0);
3473
3474        // Cannot insert a batch with a lower != current shard upper.
3475        assert_eq!(
3476            state.compare_and_append(
3477                &hollow(1, 2, &["key1"], 1),
3478                &writer_id,
3479                now(),
3480                LEASE_DURATION_MS,
3481                &IdempotencyToken::new(),
3482                &debug_state(),
3483                0,
3484                100,
3485                None
3486            ),
3487            Break(CompareAndAppendBreak::Upper {
3488                shard_upper: Antichain::from_elem(0),
3489                writer_upper: Antichain::from_elem(0)
3490            })
3491        );
3492
3493        // Insert an empty batch with an upper > lower..
3494        assert!(
3495            state
3496                .compare_and_append(
3497                    &hollow(0, 5, &[], 0),
3498                    &writer_id,
3499                    now(),
3500                    LEASE_DURATION_MS,
3501                    &IdempotencyToken::new(),
3502                    &debug_state(),
3503                    0,
3504                    100,
3505                    None
3506                )
3507                .is_continue()
3508        );
3509
3510        // Cannot insert a batch with a upper less than the lower.
3511        assert_eq!(
3512            state.compare_and_append(
3513                &hollow(5, 4, &["key1"], 1),
3514                &writer_id,
3515                now(),
3516                LEASE_DURATION_MS,
3517                &IdempotencyToken::new(),
3518                &debug_state(),
3519                0,
3520                100,
3521                None
3522            ),
3523            Break(CompareAndAppendBreak::InvalidUsage(InvalidBounds {
3524                lower: Antichain::from_elem(5),
3525                upper: Antichain::from_elem(4)
3526            }))
3527        );
3528
3529        // Cannot insert a nonempty batch with an upper equal to lower.
3530        assert_eq!(
3531            state.compare_and_append(
3532                &hollow(5, 5, &["key1"], 1),
3533                &writer_id,
3534                now(),
3535                LEASE_DURATION_MS,
3536                &IdempotencyToken::new(),
3537                &debug_state(),
3538                0,
3539                100,
3540                None
3541            ),
3542            Break(CompareAndAppendBreak::InvalidUsage(
3543                InvalidEmptyTimeInterval {
3544                    lower: Antichain::from_elem(5),
3545                    upper: Antichain::from_elem(5),
3546                    keys: vec!["key1".to_owned()],
3547                }
3548            ))
3549        );
3550
3551        // Can insert an empty batch with an upper equal to lower.
3552        assert!(
3553            state
3554                .compare_and_append(
3555                    &hollow(5, 5, &[], 0),
3556                    &writer_id,
3557                    now(),
3558                    LEASE_DURATION_MS,
3559                    &IdempotencyToken::new(),
3560                    &debug_state(),
3561                    0,
3562                    100,
3563                    None
3564                )
3565                .is_continue()
3566        );
3567    }
3568
3569    #[mz_ore::test]
3570    fn snapshot() {
3571        let now = SYSTEM_TIME.clone();
3572
3573        let mut state = TypedState::<String, String, u64, i64>::new(
3574            DUMMY_BUILD_INFO.semver_version(),
3575            ShardId::new(),
3576            "".to_owned(),
3577            0,
3578        );
3579        // Cannot take a snapshot with as_of == shard upper.
3580        assert_eq!(
3581            state.snapshot(&Antichain::from_elem(0)),
3582            Err(SnapshotErr::AsOfNotYetAvailable(
3583                SeqNo(0),
3584                Upper(Antichain::from_elem(0))
3585            ))
3586        );
3587
3588        // Cannot take a snapshot with as_of > shard upper.
3589        assert_eq!(
3590            state.snapshot(&Antichain::from_elem(5)),
3591            Err(SnapshotErr::AsOfNotYetAvailable(
3592                SeqNo(0),
3593                Upper(Antichain::from_elem(0))
3594            ))
3595        );
3596
3597        let writer_id = WriterId::new();
3598
3599        // Advance upper to 5.
3600        assert!(
3601            state
3602                .collections
3603                .compare_and_append(
3604                    &hollow(0, 5, &["key1"], 1),
3605                    &writer_id,
3606                    now(),
3607                    LEASE_DURATION_MS,
3608                    &IdempotencyToken::new(),
3609                    &debug_state(),
3610                    0,
3611                    100,
3612                    None
3613                )
3614                .is_continue()
3615        );
3616
3617        // Can take a snapshot with as_of < upper.
3618        assert_eq!(
3619            state.snapshot(&Antichain::from_elem(0)),
3620            Ok(vec![hollow(0, 5, &["key1"], 1)])
3621        );
3622
3623        // Can take a snapshot with as_of >= shard since, as long as as_of < shard_upper.
3624        assert_eq!(
3625            state.snapshot(&Antichain::from_elem(4)),
3626            Ok(vec![hollow(0, 5, &["key1"], 1)])
3627        );
3628
3629        // Cannot take a snapshot with as_of >= upper.
3630        assert_eq!(
3631            state.snapshot(&Antichain::from_elem(5)),
3632            Err(SnapshotErr::AsOfNotYetAvailable(
3633                SeqNo(0),
3634                Upper(Antichain::from_elem(5))
3635            ))
3636        );
3637        assert_eq!(
3638            state.snapshot(&Antichain::from_elem(6)),
3639            Err(SnapshotErr::AsOfNotYetAvailable(
3640                SeqNo(0),
3641                Upper(Antichain::from_elem(5))
3642            ))
3643        );
3644
3645        let reader = LeasedReaderId::new();
3646        // Advance the since to 2.
3647        let _ = state.collections.register_leased_reader(
3648            "",
3649            &reader,
3650            "",
3651            SeqNo::minimum(),
3652            Duration::from_secs(10),
3653            now(),
3654            false,
3655        );
3656        assert_eq!(
3657            state.collections.downgrade_since(
3658                &reader,
3659                SeqNo::minimum(),
3660                SeqNo::minimum(),
3661                &Antichain::from_elem(2),
3662                now()
3663            ),
3664            Continue(Since(Antichain::from_elem(2)))
3665        );
3666        assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3667        // Cannot take a snapshot with as_of < shard_since.
3668        assert_eq!(
3669            state.snapshot(&Antichain::from_elem(1)),
3670            Err(SnapshotErr::AsOfHistoricalDistinctionsLost(Since(
3671                Antichain::from_elem(2)
3672            )))
3673        );
3674
3675        // Advance the upper to 10 via an empty batch.
3676        assert!(
3677            state
3678                .collections
3679                .compare_and_append(
3680                    &hollow(5, 10, &[], 0),
3681                    &writer_id,
3682                    now(),
3683                    LEASE_DURATION_MS,
3684                    &IdempotencyToken::new(),
3685                    &debug_state(),
3686                    0,
3687                    100,
3688                    None
3689                )
3690                .is_continue()
3691        );
3692
3693        // Can still take snapshots at times < upper.
3694        assert_eq!(
3695            state.snapshot(&Antichain::from_elem(7)),
3696            Ok(vec![hollow(0, 5, &["key1"], 1), hollow(5, 10, &[], 0)])
3697        );
3698
3699        // Cannot take snapshots with as_of >= upper.
3700        assert_eq!(
3701            state.snapshot(&Antichain::from_elem(10)),
3702            Err(SnapshotErr::AsOfNotYetAvailable(
3703                SeqNo(0),
3704                Upper(Antichain::from_elem(10))
3705            ))
3706        );
3707
3708        // Advance upper to 15.
3709        assert!(
3710            state
3711                .collections
3712                .compare_and_append(
3713                    &hollow(10, 15, &["key2"], 1),
3714                    &writer_id,
3715                    now(),
3716                    LEASE_DURATION_MS,
3717                    &IdempotencyToken::new(),
3718                    &debug_state(),
3719                    0,
3720                    100,
3721                    None
3722                )
3723                .is_continue()
3724        );
3725
3726        // Filter out batches whose lowers are less than the requested as of (the
3727        // batches that are too far in the future for the requested as_of).
3728        assert_eq!(
3729            state.snapshot(&Antichain::from_elem(9)),
3730            Ok(vec![hollow(0, 5, &["key1"], 1), hollow(5, 10, &[], 0)])
3731        );
3732
3733        // Don't filter out batches whose lowers are <= the requested as_of.
3734        assert_eq!(
3735            state.snapshot(&Antichain::from_elem(10)),
3736            Ok(vec![
3737                hollow(0, 5, &["key1"], 1),
3738                hollow(5, 10, &[], 0),
3739                hollow(10, 15, &["key2"], 1)
3740            ])
3741        );
3742
3743        assert_eq!(
3744            state.snapshot(&Antichain::from_elem(11)),
3745            Ok(vec![
3746                hollow(0, 5, &["key1"], 1),
3747                hollow(5, 10, &[], 0),
3748                hollow(10, 15, &["key2"], 1)
3749            ])
3750        );
3751    }
3752
3753    #[mz_ore::test]
3754    fn next_listen_batch() {
3755        let mut state = TypedState::<String, String, u64, i64>::new(
3756            DUMMY_BUILD_INFO.semver_version(),
3757            ShardId::new(),
3758            "".to_owned(),
3759            0,
3760        );
3761
3762        // Empty collection never has any batches to listen for, regardless of the
3763        // current frontier.
3764        assert_eq!(
3765            state.next_listen_batch(&Antichain::from_elem(0)),
3766            Err(SeqNo(0))
3767        );
3768        assert_eq!(state.next_listen_batch(&Antichain::new()), Err(SeqNo(0)));
3769
3770        let writer_id = WriterId::new();
3771        let now = SYSTEM_TIME.clone();
3772
3773        // Add two batches of data, one from [0, 5) and then another from [5, 10).
3774        assert!(
3775            state
3776                .collections
3777                .compare_and_append(
3778                    &hollow(0, 5, &["key1"], 1),
3779                    &writer_id,
3780                    now(),
3781                    LEASE_DURATION_MS,
3782                    &IdempotencyToken::new(),
3783                    &debug_state(),
3784                    0,
3785                    100,
3786                    None
3787                )
3788                .is_continue()
3789        );
3790        assert!(
3791            state
3792                .collections
3793                .compare_and_append(
3794                    &hollow(5, 10, &["key2"], 1),
3795                    &writer_id,
3796                    now(),
3797                    LEASE_DURATION_MS,
3798                    &IdempotencyToken::new(),
3799                    &debug_state(),
3800                    0,
3801                    100,
3802                    None
3803                )
3804                .is_continue()
3805        );
3806
3807        // All frontiers in [0, 5) return the first batch.
3808        for t in 0..=4 {
3809            assert_eq!(
3810                state.next_listen_batch(&Antichain::from_elem(t)),
3811                Ok(hollow(0, 5, &["key1"], 1))
3812            );
3813        }
3814
3815        // All frontiers in [5, 10) return the second batch.
3816        for t in 5..=9 {
3817            assert_eq!(
3818                state.next_listen_batch(&Antichain::from_elem(t)),
3819                Ok(hollow(5, 10, &["key2"], 1))
3820            );
3821        }
3822
3823        // There is no batch currently available for t = 10.
3824        assert_eq!(
3825            state.next_listen_batch(&Antichain::from_elem(10)),
3826            Err(SeqNo(0))
3827        );
3828
3829        // By definition, there is no frontier ever at the empty antichain which
3830        // is the time after all possible times.
3831        assert_eq!(state.next_listen_batch(&Antichain::new()), Err(SeqNo(0)));
3832    }
3833
3834    #[mz_ore::test]
3835    fn expire_writer() {
3836        let mut state = TypedState::<String, String, u64, i64>::new(
3837            DUMMY_BUILD_INFO.semver_version(),
3838            ShardId::new(),
3839            "".to_owned(),
3840            0,
3841        );
3842        let now = SYSTEM_TIME.clone();
3843
3844        let writer_id_one = WriterId::new();
3845
3846        let writer_id_two = WriterId::new();
3847
3848        // Writer is eligible to write
3849        assert!(
3850            state
3851                .collections
3852                .compare_and_append(
3853                    &hollow(0, 2, &["key1"], 1),
3854                    &writer_id_one,
3855                    now(),
3856                    LEASE_DURATION_MS,
3857                    &IdempotencyToken::new(),
3858                    &debug_state(),
3859                    0,
3860                    100,
3861                    None
3862                )
3863                .is_continue()
3864        );
3865
3866        assert!(
3867            state
3868                .collections
3869                .expire_writer(&writer_id_one)
3870                .is_continue()
3871        );
3872
3873        // Other writers should still be able to write
3874        assert!(
3875            state
3876                .collections
3877                .compare_and_append(
3878                    &hollow(2, 5, &["key2"], 1),
3879                    &writer_id_two,
3880                    now(),
3881                    LEASE_DURATION_MS,
3882                    &IdempotencyToken::new(),
3883                    &debug_state(),
3884                    0,
3885                    100,
3886                    None
3887                )
3888                .is_continue()
3889        );
3890    }
3891
3892    #[mz_ore::test]
3893    fn maybe_gc_active_gc() {
3894        const GC_CONFIG: GcConfig = GcConfig {
3895            use_active_gc: true,
3896            fallback_threshold_ms: 5000,
3897            min_versions: 99,
3898            max_versions: 500,
3899        };
3900        let now_fn = SYSTEM_TIME.clone();
3901
3902        let mut state = TypedState::<String, String, u64, i64>::new(
3903            DUMMY_BUILD_INFO.semver_version(),
3904            ShardId::new(),
3905            "".to_owned(),
3906            0,
3907        );
3908
3909        let now = now_fn();
3910        // Empty state doesn't need gc, regardless of is_write.
3911        assert_eq!(state.maybe_gc(true, now, GC_CONFIG), None);
3912        assert_eq!(state.maybe_gc(false, now, GC_CONFIG), None);
3913
3914        // Artificially advance the seqno so the seqno_since advances past our
3915        // internal gc_threshold.
3916        state.seqno = SeqNo(100);
3917        assert_eq!(state.seqno_since(), SeqNo(100));
3918
3919        // When a writer is present, non-writes don't gc.
3920        let writer_id = WriterId::new();
3921        let _ = state.collections.compare_and_append(
3922            &hollow(1, 2, &["key1"], 1),
3923            &writer_id,
3924            now,
3925            LEASE_DURATION_MS,
3926            &IdempotencyToken::new(),
3927            &debug_state(),
3928            0,
3929            100,
3930            None,
3931        );
3932        assert_eq!(state.maybe_gc(false, now, GC_CONFIG), None);
3933
3934        // A write will gc though.
3935        assert_eq!(
3936            state.maybe_gc(true, now, GC_CONFIG),
3937            Some(GcReq {
3938                shard_id: state.shard_id,
3939                new_seqno_since: SeqNo(100)
3940            })
3941        );
3942
3943        // But if we write down an active gc, we won't gc.
3944        state.collections.active_gc = Some(ActiveGc {
3945            seqno: state.seqno,
3946            start_ms: now,
3947        });
3948
3949        state.seqno = SeqNo(200);
3950        assert_eq!(state.seqno_since(), SeqNo(200));
3951
3952        assert_eq!(state.maybe_gc(true, now, GC_CONFIG), None);
3953
3954        state.seqno = SeqNo(300);
3955        assert_eq!(state.seqno_since(), SeqNo(300));
3956        // But if we advance the time past the threshold, we will gc.
3957        let new_now = now + GC_CONFIG.fallback_threshold_ms + 1;
3958        assert_eq!(
3959            state.maybe_gc(true, new_now, GC_CONFIG),
3960            Some(GcReq {
3961                shard_id: state.shard_id,
3962                new_seqno_since: SeqNo(300)
3963            })
3964        );
3965
3966        // Even if the sequence number doesn't pass the threshold, if the
3967        // active gc is expired, we will gc.
3968
3969        state.seqno = SeqNo(301);
3970        assert_eq!(state.seqno_since(), SeqNo(301));
3971        assert_eq!(
3972            state.maybe_gc(true, new_now, GC_CONFIG),
3973            Some(GcReq {
3974                shard_id: state.shard_id,
3975                new_seqno_since: SeqNo(301)
3976            })
3977        );
3978
3979        state.collections.active_gc = None;
3980
3981        // Artificially advance the seqno (again) so the seqno_since advances
3982        // past our internal gc_threshold (again).
3983        state.seqno = SeqNo(400);
3984        assert_eq!(state.seqno_since(), SeqNo(400));
3985
3986        let now = now_fn();
3987
3988        // If there are no writers, even a non-write will gc.
3989        let _ = state.collections.expire_writer(&writer_id);
3990        assert_eq!(
3991            state.maybe_gc(false, now, GC_CONFIG),
3992            Some(GcReq {
3993                shard_id: state.shard_id,
3994                new_seqno_since: SeqNo(400)
3995            })
3996        );
3997
3998        // Upper-bound the number of seqnos we'll attempt to collect in one go.
3999        let previous_seqno = state.seqno;
4000        state.seqno = SeqNo(10_000);
4001        assert_eq!(state.seqno_since(), SeqNo(10_000));
4002
4003        let now = now_fn();
4004        assert_eq!(
4005            state.maybe_gc(true, now, GC_CONFIG),
4006            Some(GcReq {
4007                shard_id: state.shard_id,
4008                new_seqno_since: SeqNo(previous_seqno.0 + u64::cast_from(GC_CONFIG.max_versions))
4009            })
4010        );
4011    }
4012
4013    #[mz_ore::test]
4014    fn maybe_gc_classic() {
4015        const GC_CONFIG: GcConfig = GcConfig {
4016            use_active_gc: false,
4017            fallback_threshold_ms: 5000,
4018            min_versions: 16,
4019            max_versions: 128,
4020        };
4021        const NOW_MS: u64 = 0;
4022
4023        let mut state = TypedState::<String, String, u64, i64>::new(
4024            DUMMY_BUILD_INFO.semver_version(),
4025            ShardId::new(),
4026            "".to_owned(),
4027            0,
4028        );
4029
4030        // Empty state doesn't need gc, regardless of is_write.
4031        assert_eq!(state.maybe_gc(true, NOW_MS, GC_CONFIG), None);
4032        assert_eq!(state.maybe_gc(false, NOW_MS, GC_CONFIG), None);
4033
4034        // Artificially advance the seqno so the seqno_since advances past our
4035        // internal gc_threshold.
4036        state.seqno = SeqNo(100);
4037        assert_eq!(state.seqno_since(), SeqNo(100));
4038
4039        // When a writer is present, non-writes don't gc.
4040        let writer_id = WriterId::new();
4041        let now = SYSTEM_TIME.clone();
4042        let _ = state.collections.compare_and_append(
4043            &hollow(1, 2, &["key1"], 1),
4044            &writer_id,
4045            now(),
4046            LEASE_DURATION_MS,
4047            &IdempotencyToken::new(),
4048            &debug_state(),
4049            0,
4050            100,
4051            None,
4052        );
4053        assert_eq!(state.maybe_gc(false, NOW_MS, GC_CONFIG), None);
4054
4055        // A write will gc though.
4056        assert_eq!(
4057            state.maybe_gc(true, NOW_MS, GC_CONFIG),
4058            Some(GcReq {
4059                shard_id: state.shard_id,
4060                new_seqno_since: SeqNo(100)
4061            })
4062        );
4063
4064        // Artificially advance the seqno (again) so the seqno_since advances
4065        // past our internal gc_threshold (again).
4066        state.seqno = SeqNo(200);
4067        assert_eq!(state.seqno_since(), SeqNo(200));
4068
4069        // If there are no writers, even a non-write will gc.
4070        let _ = state.collections.expire_writer(&writer_id);
4071        assert_eq!(
4072            state.maybe_gc(false, NOW_MS, GC_CONFIG),
4073            Some(GcReq {
4074                shard_id: state.shard_id,
4075                new_seqno_since: SeqNo(200)
4076            })
4077        );
4078    }
4079
4080    #[mz_ore::test]
4081    fn need_rollup_active_rollup() {
4082        const ROLLUP_THRESHOLD: usize = 3;
4083        const ROLLUP_USE_ACTIVE_ROLLUP: bool = true;
4084        const ROLLUP_FALLBACK_THRESHOLD_MS: u64 = 5000;
4085        let now = SYSTEM_TIME.clone();
4086
4087        mz_ore::test::init_logging();
4088        let mut state = TypedState::<String, String, u64, i64>::new(
4089            DUMMY_BUILD_INFO.semver_version(),
4090            ShardId::new(),
4091            "".to_owned(),
4092            0,
4093        );
4094
4095        let rollup_seqno = SeqNo(5);
4096        let rollup = HollowRollup {
4097            key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4098            encoded_size_bytes: None,
4099        };
4100
4101        assert!(
4102            state
4103                .collections
4104                .add_rollup((rollup_seqno, &rollup))
4105                .is_continue()
4106        );
4107
4108        // shouldn't need a rollup at the seqno of the rollup
4109        state.seqno = SeqNo(5);
4110        assert_none!(state.need_rollup(
4111            ROLLUP_THRESHOLD,
4112            ROLLUP_USE_ACTIVE_ROLLUP,
4113            ROLLUP_FALLBACK_THRESHOLD_MS,
4114            now()
4115        ));
4116
4117        // shouldn't need a rollup at seqnos less than our threshold
4118        state.seqno = SeqNo(6);
4119        assert_none!(state.need_rollup(
4120            ROLLUP_THRESHOLD,
4121            ROLLUP_USE_ACTIVE_ROLLUP,
4122            ROLLUP_FALLBACK_THRESHOLD_MS,
4123            now()
4124        ));
4125        state.seqno = SeqNo(7);
4126        assert_none!(state.need_rollup(
4127            ROLLUP_THRESHOLD,
4128            ROLLUP_USE_ACTIVE_ROLLUP,
4129            ROLLUP_FALLBACK_THRESHOLD_MS,
4130            now()
4131        ));
4132        state.seqno = SeqNo(8);
4133        assert_none!(state.need_rollup(
4134            ROLLUP_THRESHOLD,
4135            ROLLUP_USE_ACTIVE_ROLLUP,
4136            ROLLUP_FALLBACK_THRESHOLD_MS,
4137            now()
4138        ));
4139
4140        let mut current_time = now();
4141        // hit our threshold! we should need a rollup
4142        state.seqno = SeqNo(9);
4143        assert_eq!(
4144            state
4145                .need_rollup(
4146                    ROLLUP_THRESHOLD,
4147                    ROLLUP_USE_ACTIVE_ROLLUP,
4148                    ROLLUP_FALLBACK_THRESHOLD_MS,
4149                    current_time
4150                )
4151                .expect("rollup"),
4152            SeqNo(9)
4153        );
4154
4155        state.collections.active_rollup = Some(ActiveRollup {
4156            seqno: SeqNo(9),
4157            start_ms: current_time,
4158        });
4159
4160        // There is now an active rollup, so we shouldn't need a rollup.
4161        assert_none!(state.need_rollup(
4162            ROLLUP_THRESHOLD,
4163            ROLLUP_USE_ACTIVE_ROLLUP,
4164            ROLLUP_FALLBACK_THRESHOLD_MS,
4165            current_time
4166        ));
4167
4168        state.seqno = SeqNo(10);
4169        // We still don't need a rollup, even though the seqno is greater than
4170        // the rollup threshold.
4171        assert_none!(state.need_rollup(
4172            ROLLUP_THRESHOLD,
4173            ROLLUP_USE_ACTIVE_ROLLUP,
4174            ROLLUP_FALLBACK_THRESHOLD_MS,
4175            current_time
4176        ));
4177
4178        // But if we wait long enough, we should need a rollup again.
4179        current_time += u64::cast_from(ROLLUP_FALLBACK_THRESHOLD_MS) + 1;
4180        assert_eq!(
4181            state
4182                .need_rollup(
4183                    ROLLUP_THRESHOLD,
4184                    ROLLUP_USE_ACTIVE_ROLLUP,
4185                    ROLLUP_FALLBACK_THRESHOLD_MS,
4186                    current_time
4187                )
4188                .expect("rollup"),
4189            SeqNo(10)
4190        );
4191
4192        state.seqno = SeqNo(9);
4193        // Clear the active rollup and ensure we need a rollup again.
4194        state.collections.active_rollup = None;
4195        let rollup_seqno = SeqNo(9);
4196        let rollup = HollowRollup {
4197            key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4198            encoded_size_bytes: None,
4199        };
4200        assert!(
4201            state
4202                .collections
4203                .add_rollup((rollup_seqno, &rollup))
4204                .is_continue()
4205        );
4206
4207        state.seqno = SeqNo(11);
4208        // We shouldn't need a rollup at seqnos less than our threshold
4209        assert_none!(state.need_rollup(
4210            ROLLUP_THRESHOLD,
4211            ROLLUP_USE_ACTIVE_ROLLUP,
4212            ROLLUP_FALLBACK_THRESHOLD_MS,
4213            current_time
4214        ));
4215        // hit our threshold! we should need a rollup
4216        state.seqno = SeqNo(13);
4217        assert_eq!(
4218            state
4219                .need_rollup(
4220                    ROLLUP_THRESHOLD,
4221                    ROLLUP_USE_ACTIVE_ROLLUP,
4222                    ROLLUP_FALLBACK_THRESHOLD_MS,
4223                    current_time
4224                )
4225                .expect("rollup"),
4226            SeqNo(13)
4227        );
4228    }
4229
4230    #[mz_ore::test]
4231    fn need_rollup_classic() {
4232        const ROLLUP_THRESHOLD: usize = 3;
4233        const ROLLUP_USE_ACTIVE_ROLLUP: bool = false;
4234        const ROLLUP_FALLBACK_THRESHOLD_MS: u64 = 0;
4235        const NOW: u64 = 0;
4236
4237        mz_ore::test::init_logging();
4238        let mut state = TypedState::<String, String, u64, i64>::new(
4239            DUMMY_BUILD_INFO.semver_version(),
4240            ShardId::new(),
4241            "".to_owned(),
4242            0,
4243        );
4244
4245        let rollup_seqno = SeqNo(5);
4246        let rollup = HollowRollup {
4247            key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4248            encoded_size_bytes: None,
4249        };
4250
4251        assert!(
4252            state
4253                .collections
4254                .add_rollup((rollup_seqno, &rollup))
4255                .is_continue()
4256        );
4257
4258        // shouldn't need a rollup at the seqno of the rollup
4259        state.seqno = SeqNo(5);
4260        assert_none!(state.need_rollup(
4261            ROLLUP_THRESHOLD,
4262            ROLLUP_USE_ACTIVE_ROLLUP,
4263            ROLLUP_FALLBACK_THRESHOLD_MS,
4264            NOW
4265        ));
4266
4267        // shouldn't need a rollup at seqnos less than our threshold
4268        state.seqno = SeqNo(6);
4269        assert_none!(state.need_rollup(
4270            ROLLUP_THRESHOLD,
4271            ROLLUP_USE_ACTIVE_ROLLUP,
4272            ROLLUP_FALLBACK_THRESHOLD_MS,
4273            NOW
4274        ));
4275        state.seqno = SeqNo(7);
4276        assert_none!(state.need_rollup(
4277            ROLLUP_THRESHOLD,
4278            ROLLUP_USE_ACTIVE_ROLLUP,
4279            ROLLUP_FALLBACK_THRESHOLD_MS,
4280            NOW
4281        ));
4282
4283        // hit our threshold! we should need a rollup
4284        state.seqno = SeqNo(8);
4285        assert_eq!(
4286            state
4287                .need_rollup(
4288                    ROLLUP_THRESHOLD,
4289                    ROLLUP_USE_ACTIVE_ROLLUP,
4290                    ROLLUP_FALLBACK_THRESHOLD_MS,
4291                    NOW
4292                )
4293                .expect("rollup"),
4294            SeqNo(8)
4295        );
4296
4297        // but we don't need rollups for every seqno > the threshold
4298        state.seqno = SeqNo(9);
4299        assert_none!(state.need_rollup(
4300            ROLLUP_THRESHOLD,
4301            ROLLUP_USE_ACTIVE_ROLLUP,
4302            ROLLUP_FALLBACK_THRESHOLD_MS,
4303            NOW
4304        ));
4305
4306        // we only need a rollup each `ROLLUP_THRESHOLD` beyond our current seqno
4307        state.seqno = SeqNo(11);
4308        assert_eq!(
4309            state
4310                .need_rollup(
4311                    ROLLUP_THRESHOLD,
4312                    ROLLUP_USE_ACTIVE_ROLLUP,
4313                    ROLLUP_FALLBACK_THRESHOLD_MS,
4314                    NOW
4315                )
4316                .expect("rollup"),
4317            SeqNo(11)
4318        );
4319
4320        // add another rollup and ensure we're always picking the latest
4321        let rollup_seqno = SeqNo(6);
4322        let rollup = HollowRollup {
4323            key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4324            encoded_size_bytes: None,
4325        };
4326        assert!(
4327            state
4328                .collections
4329                .add_rollup((rollup_seqno, &rollup))
4330                .is_continue()
4331        );
4332
4333        state.seqno = SeqNo(8);
4334        assert_none!(state.need_rollup(
4335            ROLLUP_THRESHOLD,
4336            ROLLUP_USE_ACTIVE_ROLLUP,
4337            ROLLUP_FALLBACK_THRESHOLD_MS,
4338            NOW
4339        ));
4340        state.seqno = SeqNo(9);
4341        assert_eq!(
4342            state
4343                .need_rollup(
4344                    ROLLUP_THRESHOLD,
4345                    ROLLUP_USE_ACTIVE_ROLLUP,
4346                    ROLLUP_FALLBACK_THRESHOLD_MS,
4347                    NOW
4348                )
4349                .expect("rollup"),
4350            SeqNo(9)
4351        );
4352
4353        // and ensure that after a fallback point, we assign every seqno work
4354        let fallback_seqno = SeqNo(
4355            rollup_seqno.0
4356                * u64::cast_from(PersistConfig::DEFAULT_FALLBACK_ROLLUP_THRESHOLD_MULTIPLIER),
4357        );
4358        state.seqno = fallback_seqno;
4359        assert_eq!(
4360            state
4361                .need_rollup(
4362                    ROLLUP_THRESHOLD,
4363                    ROLLUP_USE_ACTIVE_ROLLUP,
4364                    ROLLUP_FALLBACK_THRESHOLD_MS,
4365                    NOW
4366                )
4367                .expect("rollup"),
4368            fallback_seqno
4369        );
4370        state.seqno = fallback_seqno.next();
4371        assert_eq!(
4372            state
4373                .need_rollup(
4374                    ROLLUP_THRESHOLD,
4375                    ROLLUP_USE_ACTIVE_ROLLUP,
4376                    ROLLUP_FALLBACK_THRESHOLD_MS,
4377                    NOW
4378                )
4379                .expect("rollup"),
4380            fallback_seqno.next()
4381        );
4382    }
4383
4384    #[mz_ore::test]
4385    fn idempotency_token_sentinel() {
4386        assert_eq!(
4387            IdempotencyToken::SENTINEL.to_string(),
4388            "i11111111-1111-1111-1111-111111111111"
4389        );
4390    }
4391
4392    /// This test generates an "arbitrary" State, but uses a fixed seed for the
4393    /// randomness, so that it's deterministic. This lets us assert the
4394    /// serialization of that State against a golden file that's committed,
4395    /// making it easy to see what the serialization (used in an upcoming
4396    /// INSPECT feature) looks like.
4397    ///
4398    /// This golden will have to be updated each time we change State, but
4399    /// that's a feature, not a bug.
4400    #[mz_ore::test]
4401    #[cfg_attr(miri, ignore)] // too slow
4402    fn state_inspect_serde_json() {
4403        const STATE_SERDE_JSON: &str = include_str!("state_serde.json");
4404        let mut runner = proptest::test_runner::TestRunner::deterministic();
4405        let tree = any_state::<u64>(6..8).new_tree(&mut runner).unwrap();
4406        let json = serde_json::to_string_pretty(&tree.current()).unwrap();
4407        assert_eq!(
4408            json.trim(),
4409            STATE_SERDE_JSON.trim(),
4410            "\n\nNEW GOLDEN\n{}\n",
4411            json
4412        );
4413    }
4414
4415    #[mz_persist_proc::test(tokio::test)]
4416    #[cfg_attr(miri, ignore)] // too slow
4417    async fn sneaky_downgrades(dyncfgs: ConfigUpdates) {
4418        let mut clients = new_test_client_cache(&dyncfgs);
4419        let shard_id = ShardId::new();
4420
4421        async fn open_and_write(
4422            clients: &mut PersistClientCache,
4423            version: semver::Version,
4424            shard_id: ShardId,
4425        ) -> Result<(), tokio::task::JoinError> {
4426            clients.cfg.build_version = version.clone();
4427            clients.clear_state_cache();
4428            let client = clients.open(PersistLocation::new_in_mem()).await.unwrap();
4429            // Run in a task so we can catch the panic.
4430            mz_ore::task::spawn(|| version.to_string(), async move {
4431                let () = client
4432                    .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
4433                    .await
4434                    .expect("valid usage");
4435                let (mut write, _) = client.expect_open::<String, (), u64, i64>(shard_id).await;
4436                let current = *write.upper().as_option().unwrap();
4437                // Do a write so that we tag the state with the version.
4438                write
4439                    .expect_compare_and_append_batch(&mut [], current, current + 1)
4440                    .await;
4441            })
4442            .into_tokio_handle()
4443            .await
4444        }
4445
4446        // Start at v0.10.0.
4447        let res = open_and_write(&mut clients, Version::new(0, 10, 0), shard_id).await;
4448        assert_ok!(res);
4449
4450        // Upgrade to v0.11.0 is allowed.
4451        let res = open_and_write(&mut clients, Version::new(0, 11, 0), shard_id).await;
4452        assert_ok!(res);
4453
4454        // Downgrade to v0.10.0 is no longer allowed.
4455        let res = open_and_write(&mut clients, Version::new(0, 10, 0), shard_id).await;
4456        assert!(res.unwrap_err().is_panic());
4457
4458        // Downgrade to v0.9.0 is _NOT_ allowed.
4459        let res = open_and_write(&mut clients, Version::new(0, 9, 0), shard_id).await;
4460        assert!(res.unwrap_err().is_panic());
4461    }
4462
4463    #[mz_ore::test]
4464    fn runid_roundtrip() {
4465        proptest!(|(runid: RunId)| {
4466            let runid_str = runid.to_string();
4467            let parsed = RunId::from_str(&runid_str);
4468            prop_assert_eq!(parsed, Ok(runid));
4469        });
4470    }
4471
4472    /// Regression for PER-16. `add_rollup` must refuse to (re-)insert a
4473    /// rollup at a seqno that GC has already physically removed.
4474    /// Without this guard, an `apply_unbatched_idempotent_cmd` retry of
4475    /// `add_rollup` that runs after a concurrent GC removed the rollup's
4476    /// original map entry will re-insert it under the same key, producing
4477    /// duplicate Insert events in the diff stream and (later) duplicate
4478    /// Delete events that trip the `assert!` in `gc.rs`'s
4479    /// `find_removable_blobs`.
4480    ///
4481    /// This test reaches the bug state via the public
4482    /// `add_rollup` → `add_rollup` (newer) → `remove_rollups` (the older
4483    /// one) → `add_rollup` (retry of the older one) sequence, mirroring
4484    /// how a delayed retry of an indeterminate add_rollup commit can race
4485    /// a concurrent GC pass that has since added a newer rollup and
4486    /// removed the older one.
4487    #[mz_ore::test]
4488    fn add_rollup_idempotent_across_gc_removal() {
4489        let mut state = TypedState::<String, String, u64, i64>::new(
4490            DUMMY_BUILD_INFO.semver_version(),
4491            ShardId::new(),
4492            "".to_owned(),
4493            0,
4494        );
4495
4496        let older_seqno = SeqNo(10);
4497        let older = HollowRollup {
4498            key: PartialRollupKey::new(older_seqno, &RollupId::new()),
4499            encoded_size_bytes: None,
4500        };
4501        let newer_seqno = SeqNo(20);
4502        let newer = HollowRollup {
4503            key: PartialRollupKey::new(newer_seqno, &RollupId::new()),
4504            encoded_size_bytes: None,
4505        };
4506        let add_older = |state: &mut StateCollections<u64>| state.add_rollup((older_seqno, &older));
4507
4508        // First attempt commits the older rollup.
4509        assert_eq!(add_older(&mut state.collections), Continue(true));
4510        // A repeat at the same seqno with the same key is the pre-existing
4511        // idempotent no-op and should report applied=true.
4512        assert_eq!(add_older(&mut state.collections), Continue(true));
4513        assert_eq!(state.collections.rollups.len(), 1);
4514
4515        // The shard keeps making progress and a later command commits a
4516        // newer rollup. This is what gives GC something to keep when it
4517        // later removes the older entry.
4518        assert_eq!(
4519            state.collections.add_rollup((newer_seqno, &newer)),
4520            Continue(true),
4521        );
4522
4523        // The GC worker, having found a kept rollup at `newer_seqno`,
4524        // commits the removal of the older one. State.rollups now only
4525        // contains the kept rollup; the older entry is gone.
4526        let _ = state
4527            .collections
4528            .remove_rollups(&[(older_seqno, older.key.clone())]);
4529        assert!(!state.collections.rollups.contains_key(&older_seqno));
4530        assert!(state.collections.rollups.contains_key(&newer_seqno));
4531
4532        // The retry replays the same captured `(older_seqno, older)`
4533        // tuple. Even though no entry references the key anymore,
4534        // re-inserting it must be refused: `older_seqno` falls below the
4535        // smallest live rollup seqno, which is the GC watermark.
4536        assert_eq!(add_older(&mut state.collections), Continue(false));
4537        assert!(!state.collections.rollups.contains_key(&older_seqno));
4538        assert_eq!(state.collections.rollups.len(), 1);
4539    }
4540}