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