1use anyhow::ensure;
11use async_stream::{stream, try_stream};
12use differential_dataflow::difference::Monoid;
13use mz_persist::metrics::ColumnarMetrics;
14use proptest::prelude::{Arbitrary, Strategy};
15use std::borrow::Cow;
16use std::cmp::Ordering;
17use std::collections::BTreeMap;
18use std::fmt::{Debug, Formatter};
19use std::marker::PhantomData;
20use std::ops::ControlFlow::{self, Break, Continue};
21use std::ops::{Deref, DerefMut};
22use std::time::Duration;
23
24use arrow::array::{Array, ArrayData, make_array};
25use arrow::datatypes::DataType;
26use bytes::Bytes;
27use differential_dataflow::Hashable;
28use differential_dataflow::lattice::Lattice;
29use differential_dataflow::trace::Description;
30use differential_dataflow::trace::implementations::BatchContainer;
31use futures::Stream;
32use futures_util::StreamExt;
33use itertools::Itertools;
34use mz_dyncfg::{Config, ParameterScope};
35use mz_ore::cast::CastFrom;
36use mz_ore::now::EpochMillis;
37use mz_ore::soft_panic_or_log;
38use mz_ore::vec::PartialOrdVecExt;
39use mz_persist::indexed::encoding::{BatchColumnarFormat, BlobTraceUpdates};
40use mz_persist::location::{Blob, SeqNo};
41use mz_persist_types::arrow::{ArrayBound, ProtoArrayData};
42use mz_persist_types::columnar::{ColumnEncoder, Schema};
43use mz_persist_types::schema::{SchemaId, backward_compatible};
44use mz_persist_types::{Codec, Codec64};
45use mz_proto::ProtoType;
46use mz_proto::RustType;
47use proptest_derive::Arbitrary;
48use semver::Version;
49use serde::ser::SerializeStruct;
50use serde::{Serialize, Serializer};
51use timely::PartialOrder;
52use timely::order::TotalOrder;
53use timely::progress::{Antichain, Timestamp};
54use tracing::info;
55use uuid::Uuid;
56
57use crate::critical::{CriticalReaderId, Opaque};
58use crate::error::InvalidUsage;
59use crate::internal::encoding::{
60 LazyInlineBatchPart, LazyPartStats, LazyProto, MetadataKey, 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
84pub(crate) const ROLLUP_THRESHOLD: Config<usize> = Config::new(
92 "persist_rollup_threshold",
93 128,
94 "The number of seqnos between rollups.",
95 ParameterScope::Environment,
96);
97
98pub(crate) const ROLLUP_FALLBACK_THRESHOLD_MS: Config<usize> = Config::new(
101 "persist_rollup_fallback_threshold_ms",
102 5000,
103 "The number of milliseconds before a worker claims an already claimed rollup.",
104 ParameterScope::Environment,
105);
106
107pub(crate) const ROLLUP_USE_ACTIVE_ROLLUP: Config<bool> = Config::new(
110 "persist_rollup_use_active_rollup",
111 true,
112 "Whether to use the new active rollup tracking mechanism.",
113 ParameterScope::Environment,
114);
115
116pub(crate) const GC_FALLBACK_THRESHOLD_MS: Config<usize> = Config::new(
119 "persist_gc_fallback_threshold_ms",
120 900000,
121 "The number of milliseconds before a worker claims an already claimed GC.",
122 ParameterScope::Environment,
123);
124
125pub(crate) const GC_MIN_VERSIONS: Config<usize> = Config::new(
127 "persist_gc_min_versions",
128 32,
129 "The number of un-GCd versions that may exist in state before we'll trigger a GC.",
130 ParameterScope::Environment,
131);
132
133pub(crate) const GC_MAX_VERSIONS: Config<usize> = Config::new(
135 "persist_gc_max_versions",
136 128_000,
137 "The maximum number of versions to GC in a single GC run.",
138 ParameterScope::Environment,
139);
140
141pub(crate) const GC_USE_ACTIVE_GC: Config<bool> = Config::new(
144 "persist_gc_use_active_gc",
145 false,
146 "Whether to use the new active GC tracking mechanism.",
147 ParameterScope::Environment,
148);
149
150pub(crate) const ENABLE_INCREMENTAL_COMPACTION: Config<bool> = Config::new(
151 "persist_enable_incremental_compaction",
152 true,
153 "Whether to enable incremental compaction.",
154 ParameterScope::Environment,
155);
156
157#[derive(Arbitrary, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
160#[serde(into = "String")]
161pub struct IdempotencyToken(pub(crate) [u8; 16]);
162
163impl std::fmt::Display for IdempotencyToken {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 write!(f, "i{}", Uuid::from_bytes(self.0))
166 }
167}
168
169impl std::fmt::Debug for IdempotencyToken {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 write!(f, "IdempotencyToken({})", Uuid::from_bytes(self.0))
172 }
173}
174
175impl std::str::FromStr for IdempotencyToken {
176 type Err = String;
177
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 parse_id("i", "IdempotencyToken", s).map(IdempotencyToken)
180 }
181}
182
183impl From<IdempotencyToken> for String {
184 fn from(x: IdempotencyToken) -> Self {
185 x.to_string()
186 }
187}
188
189impl IdempotencyToken {
190 pub(crate) fn new() -> Self {
191 IdempotencyToken(*Uuid::new_v4().as_bytes())
192 }
193 pub(crate) const SENTINEL: IdempotencyToken = IdempotencyToken([17u8; 16]);
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize)]
197pub struct LeasedReaderState<T> {
198 pub seqno: SeqNo,
200 pub since: Antichain<T>,
202 pub last_heartbeat_timestamp_ms: u64,
204 pub lease_duration_ms: u64,
207 pub debug: HandleDebugState,
209}
210
211#[derive(Clone, Debug, PartialEq, Serialize)]
212pub struct CriticalReaderState<T> {
213 pub since: Antichain<T>,
215 pub opaque: Opaque,
217 pub debug: HandleDebugState,
219}
220
221#[derive(Clone, Debug, PartialEq, Serialize)]
222pub struct WriterState<T> {
223 pub last_heartbeat_timestamp_ms: u64,
225 pub lease_duration_ms: u64,
228 pub most_recent_write_token: IdempotencyToken,
231 pub most_recent_write_upper: Antichain<T>,
234 pub debug: HandleDebugState,
236}
237
238#[derive(Arbitrary, Clone, Debug, Default, PartialEq, Serialize)]
240pub struct HandleDebugState {
241 pub hostname: String,
244 pub purpose: String,
246}
247
248#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
252#[serde(tag = "type")]
253pub enum BatchPart<T> {
254 Hollow(HollowBatchPart<T>),
255 Inline {
256 updates: LazyInlineBatchPart,
257 ts_rewrite: Option<Antichain<T>>,
258 schema_id: Option<SchemaId>,
259
260 deprecated_schema_id: Option<SchemaId>,
262 },
263}
264
265fn decode_structured_lower(lower: &LazyProto<ProtoArrayData>) -> Option<ArrayBound> {
266 let try_decode = |lower: &LazyProto<ProtoArrayData>| {
267 let proto = lower.decode()?;
268 let data = ArrayData::from_proto(proto)?;
269 ensure!(data.len() == 1);
270 Ok(ArrayBound::new(make_array(data), 0))
271 };
272
273 let decoded: anyhow::Result<ArrayBound> = try_decode(lower);
274
275 match decoded {
276 Ok(bound) => Some(bound),
277 Err(e) => {
278 soft_panic_or_log!("failed to decode bound: {e:#?}");
279 None
280 }
281 }
282}
283
284impl<T> BatchPart<T> {
285 pub fn hollow_bytes(&self) -> usize {
286 match self {
287 BatchPart::Hollow(x) => x.encoded_size_bytes,
288 BatchPart::Inline { .. } => 0,
289 }
290 }
291
292 pub fn is_inline(&self) -> bool {
293 matches!(self, BatchPart::Inline { .. })
294 }
295
296 pub fn inline_bytes(&self) -> usize {
297 match self {
298 BatchPart::Hollow(_) => 0,
299 BatchPart::Inline { updates, .. } => updates.encoded_size_bytes(),
300 }
301 }
302
303 pub fn writer_key(&self) -> Option<WriterKey> {
304 match self {
305 BatchPart::Hollow(x) => x.key.split().map(|(writer, _part)| writer),
306 BatchPart::Inline { .. } => None,
307 }
308 }
309
310 pub fn encoded_size_bytes(&self) -> usize {
311 match self {
312 BatchPart::Hollow(x) => x.encoded_size_bytes,
313 BatchPart::Inline { updates, .. } => updates.encoded_size_bytes(),
314 }
315 }
316
317 pub fn printable_name(&self) -> &str {
320 match self {
321 BatchPart::Hollow(x) => x.key.0.as_str(),
322 BatchPart::Inline { .. } => "<inline>",
323 }
324 }
325
326 pub fn stats(&self) -> Option<&LazyPartStats> {
327 match self {
328 BatchPart::Hollow(x) => x.stats.as_ref(),
329 BatchPart::Inline { .. } => None,
330 }
331 }
332
333 pub fn key_lower(&self) -> &[u8] {
334 match self {
335 BatchPart::Hollow(x) => x.key_lower.as_slice(),
336 BatchPart::Inline { .. } => &[],
343 }
344 }
345
346 pub fn structured_key_lower(&self) -> Option<ArrayBound> {
347 let part = match self {
348 BatchPart::Hollow(part) => part,
349 BatchPart::Inline { .. } => return None,
350 };
351
352 decode_structured_lower(part.structured_key_lower.as_ref()?)
353 }
354
355 pub fn ts_rewrite(&self) -> Option<&Antichain<T>> {
356 match self {
357 BatchPart::Hollow(x) => x.ts_rewrite.as_ref(),
358 BatchPart::Inline { ts_rewrite, .. } => ts_rewrite.as_ref(),
359 }
360 }
361
362 pub fn schema_id(&self) -> Option<SchemaId> {
363 match self {
364 BatchPart::Hollow(x) => x.schema_id,
365 BatchPart::Inline { schema_id, .. } => *schema_id,
366 }
367 }
368
369 pub fn deprecated_schema_id(&self) -> Option<SchemaId> {
370 match self {
371 BatchPart::Hollow(x) => x.deprecated_schema_id,
372 BatchPart::Inline {
373 deprecated_schema_id,
374 ..
375 } => *deprecated_schema_id,
376 }
377 }
378}
379
380impl<T: Timestamp + Codec64> BatchPart<T> {
381 pub fn is_structured_only(&self, metrics: &ColumnarMetrics) -> bool {
382 match self {
383 BatchPart::Hollow(x) => matches!(x.format, Some(BatchColumnarFormat::Structured)),
384 BatchPart::Inline { updates, .. } => {
385 let inline_part = updates.decode::<T>(metrics).expect("valid inline part");
386 matches!(inline_part.updates, BlobTraceUpdates::Structured { .. })
387 }
388 }
389 }
390
391 pub fn diffs_sum<D: Codec64 + Monoid>(&self, metrics: &ColumnarMetrics) -> Option<D> {
392 match self {
393 BatchPart::Hollow(x) => x.diffs_sum.map(D::decode),
394 BatchPart::Inline { updates, .. } => Some(
395 updates
396 .decode::<T>(metrics)
397 .expect("valid inline part")
398 .updates
399 .diffs_sum(),
400 ),
401 }
402 }
403}
404
405#[derive(Debug, Clone)]
407pub struct HollowRun<T> {
408 pub(crate) parts: Vec<RunPart<T>>,
410}
411
412#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
415pub struct HollowRunRef<T> {
416 pub key: PartialBatchKey,
417
418 pub hollow_bytes: usize,
420
421 pub max_part_bytes: usize,
423
424 pub key_lower: Vec<u8>,
426
427 pub structured_key_lower: Option<LazyProto<ProtoArrayData>>,
429
430 pub diffs_sum: Option<[u8; 8]>,
431
432 pub(crate) _phantom_data: PhantomData<T>,
433}
434impl<T: Eq> PartialOrd<Self> for HollowRunRef<T> {
435 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
436 Some(self.cmp(other))
437 }
438}
439
440impl<T: Eq> Ord for HollowRunRef<T> {
441 fn cmp(&self, other: &Self) -> Ordering {
442 self.key.cmp(&other.key)
443 }
444}
445
446impl<T> HollowRunRef<T> {
447 pub fn writer_key(&self) -> Option<WriterKey> {
448 Some(self.key.split()?.0)
449 }
450}
451
452impl<T: Timestamp + Codec64> HollowRunRef<T> {
453 pub async fn set<D: Codec64 + Monoid>(
455 shard_id: ShardId,
456 blob: &dyn Blob,
457 writer: &WriterKey,
458 data: HollowRun<T>,
459 metrics: &Metrics,
460 ) -> Self {
461 let hollow_bytes = data.parts.iter().map(|p| p.hollow_bytes()).sum();
462 let max_part_bytes = data
463 .parts
464 .iter()
465 .map(|p| p.max_part_bytes())
466 .max()
467 .unwrap_or(0);
468 let key_lower = data
469 .parts
470 .first()
471 .map_or(vec![], |p| p.key_lower().to_vec());
472 let structured_key_lower = match data.parts.first() {
473 Some(RunPart::Many(r)) => r.structured_key_lower.clone(),
474 Some(RunPart::Single(BatchPart::Hollow(p))) => p.structured_key_lower.clone(),
475 Some(RunPart::Single(BatchPart::Inline { .. })) | None => None,
476 };
477 let diffs_sum = data
478 .parts
479 .iter()
480 .map(|p| {
481 p.diffs_sum::<D>(&metrics.columnar)
482 .expect("valid diffs sum")
483 })
484 .reduce(|mut a, b| {
485 a.plus_equals(&b);
486 a
487 })
488 .expect("valid diffs sum")
489 .encode();
490
491 let key = PartialBatchKey::new(writer, &PartId::new());
492 let blob_key = key.complete(&shard_id);
493 let bytes = Bytes::from(prost::Message::encode_to_vec(&data.into_proto()));
494 let () = retry_external(&metrics.retries.external.hollow_run_set, || {
495 blob.set(&blob_key, bytes.clone())
496 })
497 .await;
498 Self {
499 key,
500 hollow_bytes,
501 max_part_bytes,
502 key_lower,
503 structured_key_lower,
504 diffs_sum: Some(diffs_sum),
505 _phantom_data: Default::default(),
506 }
507 }
508
509 pub async fn get(
513 &self,
514 shard_id: ShardId,
515 blob: &dyn Blob,
516 metrics: &Metrics,
517 ) -> Option<HollowRun<T>> {
518 let blob_key = self.key.complete(&shard_id);
519 let mut bytes = retry_external(&metrics.retries.external.hollow_run_get, || {
520 blob.get(&blob_key)
521 })
522 .await?;
523 let proto_runs: ProtoHollowRun =
524 prost::Message::decode(&mut bytes).expect("illegal state: invalid proto bytes");
525 let runs = proto_runs
526 .into_rust()
527 .expect("illegal state: invalid encoded runs proto");
528 Some(runs)
529 }
530}
531
532#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
536#[serde(untagged)]
537pub enum RunPart<T> {
538 Single(BatchPart<T>),
539 Many(HollowRunRef<T>),
540}
541
542impl<T: Ord> PartialOrd<Self> for RunPart<T> {
543 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
544 Some(self.cmp(other))
545 }
546}
547
548impl<T: Ord> Ord for RunPart<T> {
549 fn cmp(&self, other: &Self) -> Ordering {
550 match (self, other) {
551 (RunPart::Single(a), RunPart::Single(b)) => a.cmp(b),
552 (RunPart::Single(_), RunPart::Many(_)) => Ordering::Less,
553 (RunPart::Many(_), RunPart::Single(_)) => Ordering::Greater,
554 (RunPart::Many(a), RunPart::Many(b)) => a.cmp(b),
555 }
556 }
557}
558
559impl<T> RunPart<T> {
560 #[cfg(test)]
561 pub fn expect_hollow_part(&self) -> &HollowBatchPart<T> {
562 match self {
563 RunPart::Single(BatchPart::Hollow(hollow)) => hollow,
564 _ => panic!("expected hollow part!"),
565 }
566 }
567
568 pub fn hollow_bytes(&self) -> usize {
569 match self {
570 Self::Single(p) => p.hollow_bytes(),
571 Self::Many(r) => r.hollow_bytes,
572 }
573 }
574
575 pub fn is_inline(&self) -> bool {
576 match self {
577 Self::Single(p) => p.is_inline(),
578 Self::Many(_) => false,
579 }
580 }
581
582 pub fn inline_bytes(&self) -> usize {
583 match self {
584 Self::Single(p) => p.inline_bytes(),
585 Self::Many(_) => 0,
586 }
587 }
588
589 pub fn max_part_bytes(&self) -> usize {
590 match self {
591 Self::Single(p) => p.encoded_size_bytes(),
592 Self::Many(r) => r.max_part_bytes,
593 }
594 }
595
596 pub fn writer_key(&self) -> Option<WriterKey> {
597 match self {
598 Self::Single(p) => p.writer_key(),
599 Self::Many(r) => r.writer_key(),
600 }
601 }
602
603 pub fn encoded_size_bytes(&self) -> usize {
604 match self {
605 Self::Single(p) => p.encoded_size_bytes(),
606 Self::Many(r) => r.hollow_bytes,
607 }
608 }
609
610 pub fn schema_id(&self) -> Option<SchemaId> {
611 match self {
612 Self::Single(p) => p.schema_id(),
613 Self::Many(_) => None,
614 }
615 }
616
617 pub fn printable_name(&self) -> &str {
620 match self {
621 Self::Single(p) => p.printable_name(),
622 Self::Many(r) => r.key.0.as_str(),
623 }
624 }
625
626 pub fn stats(&self) -> Option<&LazyPartStats> {
627 match self {
628 Self::Single(p) => p.stats(),
629 Self::Many(_) => None,
631 }
632 }
633
634 pub fn key_lower(&self) -> &[u8] {
635 match self {
636 Self::Single(p) => p.key_lower(),
637 Self::Many(r) => r.key_lower.as_slice(),
638 }
639 }
640
641 pub fn structured_key_lower(&self) -> Option<ArrayBound> {
642 match self {
643 Self::Single(p) => p.structured_key_lower(),
644 Self::Many(_) => None,
645 }
646 }
647
648 pub fn ts_rewrite(&self) -> Option<&Antichain<T>> {
649 match self {
650 Self::Single(p) => p.ts_rewrite(),
651 Self::Many(_) => None,
652 }
653 }
654}
655
656impl<T> RunPart<T>
657where
658 T: Timestamp + Codec64,
659{
660 pub fn diffs_sum<D: Codec64 + Monoid>(&self, metrics: &ColumnarMetrics) -> Option<D> {
661 match self {
662 Self::Single(p) => p.diffs_sum(metrics),
663 Self::Many(hollow_run) => hollow_run.diffs_sum.map(D::decode),
664 }
665 }
666}
667
668#[derive(Clone, Debug)]
670pub struct MissingBlob(BlobKey);
671
672impl std::fmt::Display for MissingBlob {
673 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
674 write!(f, "unexpectedly missing key: {}", self.0)
675 }
676}
677
678impl std::error::Error for MissingBlob {}
679
680impl<T: Timestamp + Codec64 + Sync> RunPart<T> {
681 pub fn part_stream<'a>(
682 &'a self,
683 shard_id: ShardId,
684 blob: &'a dyn Blob,
685 metrics: &'a Metrics,
686 ) -> impl Stream<Item = Result<Cow<'a, BatchPart<T>>, MissingBlob>> + Send + 'a {
687 try_stream! {
688 match self {
689 RunPart::Single(p) => {
690 yield Cow::Borrowed(p);
691 }
692 RunPart::Many(r) => {
693 let fetched = r.get(shard_id, blob, metrics).await
694 .ok_or_else(|| MissingBlob(r.key.complete(&shard_id)))?;
695 for run_part in fetched.parts {
696 for await batch_part in
697 run_part.part_stream(shard_id, blob, metrics).boxed()
698 {
699 yield Cow::Owned(batch_part?.into_owned());
700 }
701 }
702 }
703 }
704 }
705 }
706}
707
708impl<T: Ord> PartialOrd for BatchPart<T> {
709 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
710 Some(self.cmp(other))
711 }
712}
713
714impl<T: Ord> Ord for BatchPart<T> {
715 fn cmp(&self, other: &Self) -> Ordering {
716 match (self, other) {
717 (BatchPart::Hollow(s), BatchPart::Hollow(o)) => s.cmp(o),
718 (
719 BatchPart::Inline {
720 updates: s_updates,
721 ts_rewrite: s_ts_rewrite,
722 schema_id: s_schema_id,
723 deprecated_schema_id: s_deprecated_schema_id,
724 },
725 BatchPart::Inline {
726 updates: o_updates,
727 ts_rewrite: o_ts_rewrite,
728 schema_id: o_schema_id,
729 deprecated_schema_id: o_deprecated_schema_id,
730 },
731 ) => (
732 s_updates,
733 s_ts_rewrite.as_ref().map(|x| x.elements()),
734 s_schema_id,
735 s_deprecated_schema_id,
736 )
737 .cmp(&(
738 o_updates,
739 o_ts_rewrite.as_ref().map(|x| x.elements()),
740 o_schema_id,
741 o_deprecated_schema_id,
742 )),
743 (BatchPart::Hollow(_), BatchPart::Inline { .. }) => Ordering::Less,
744 (BatchPart::Inline { .. }, BatchPart::Hollow(_)) => Ordering::Greater,
745 }
746 }
747}
748
749#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize)]
751pub(crate) enum RunOrder {
752 Unordered,
754 Codec,
756 Structured,
758}
759
760#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Copy, Hash)]
761pub struct RunId(pub(crate) [u8; 16]);
762
763impl std::fmt::Display for RunId {
764 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
765 write!(f, "ri{}", Uuid::from_bytes(self.0))
766 }
767}
768
769impl std::fmt::Debug for RunId {
770 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
771 write!(f, "RunId({})", Uuid::from_bytes(self.0))
772 }
773}
774
775impl std::str::FromStr for RunId {
776 type Err = String;
777
778 fn from_str(s: &str) -> Result<Self, Self::Err> {
779 parse_id("ri", "RunId", s).map(RunId)
780 }
781}
782
783impl From<RunId> for String {
784 fn from(x: RunId) -> Self {
785 x.to_string()
786 }
787}
788
789impl RunId {
790 pub(crate) fn new() -> Self {
791 RunId(*Uuid::new_v4().as_bytes())
792 }
793}
794
795impl Arbitrary for RunId {
796 type Parameters = ();
797 type Strategy = proptest::strategy::BoxedStrategy<Self>;
798 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
799 Strategy::prop_map(proptest::prelude::any::<u128>(), |n| {
800 RunId(*Uuid::from_u128(n).as_bytes())
801 })
802 .boxed()
803 }
804}
805
806#[derive(Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Serialize)]
808pub struct RunMeta {
809 pub(crate) order: Option<RunOrder>,
811 pub(crate) schema: Option<SchemaId>,
813
814 pub(crate) deprecated_schema: Option<SchemaId>,
816
817 pub(crate) id: Option<RunId>,
819
820 pub(crate) len: Option<usize>,
822
823 #[serde(skip_serializing_if = "MetadataMap::is_empty")]
825 pub(crate) meta: MetadataMap,
826}
827
828const RUN_META_BOUNDS_TRUNCATED: MetadataKey<bool> = MetadataKey::new("truncated");
830
831impl RunMeta {
832 pub(crate) fn bounds_truncated(&self) -> bool {
842 self.meta.get(RUN_META_BOUNDS_TRUNCATED).unwrap_or(false)
843 }
844
845 pub(crate) fn set_bounds_truncated(&mut self) {
848 self.meta.set(RUN_META_BOUNDS_TRUNCATED, true);
849 }
850}
851
852#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
854pub struct HollowBatchPart<T> {
855 pub key: PartialBatchKey,
857 #[serde(skip_serializing_if = "MetadataMap::is_empty")]
859 pub meta: MetadataMap,
860 pub encoded_size_bytes: usize,
862 #[serde(serialize_with = "serialize_part_bytes")]
865 pub key_lower: Vec<u8>,
866 #[serde(serialize_with = "serialize_lazy_proto")]
868 pub structured_key_lower: Option<LazyProto<ProtoArrayData>>,
869 #[serde(serialize_with = "serialize_part_stats")]
871 pub stats: Option<LazyPartStats>,
872 pub ts_rewrite: Option<Antichain<T>>,
880 #[serde(serialize_with = "serialize_diffs_sum")]
888 pub diffs_sum: Option<[u8; 8]>,
889 pub format: Option<BatchColumnarFormat>,
894 pub schema_id: Option<SchemaId>,
899
900 pub deprecated_schema_id: Option<SchemaId>,
902}
903
904#[derive(Clone, PartialEq, Eq)]
906pub struct HollowBatch<T> {
907 pub desc: Description<T>,
909 pub len: usize,
911 pub(crate) parts: Vec<RunPart<T>>,
913 pub(crate) run_splits: Vec<usize>,
921 pub(crate) run_meta: Vec<RunMeta>,
924}
925
926impl<T: Debug> Debug for HollowBatch<T> {
927 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
928 let HollowBatch {
929 desc,
930 parts,
931 len,
932 run_splits: runs,
933 run_meta,
934 } = self;
935 f.debug_struct("HollowBatch")
936 .field(
937 "desc",
938 &(
939 desc.lower().elements(),
940 desc.upper().elements(),
941 desc.since().elements(),
942 ),
943 )
944 .field("parts", &parts)
945 .field("len", &len)
946 .field("runs", &runs)
947 .field("run_meta", &run_meta)
948 .finish()
949 }
950}
951
952impl<T: Serialize> serde::Serialize for HollowBatch<T> {
953 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
954 let HollowBatch {
955 desc,
956 len,
957 parts: _,
959 run_splits: _,
960 run_meta: _,
961 } = self;
962 let mut s = s.serialize_struct("HollowBatch", 5)?;
963 let () = s.serialize_field("lower", &desc.lower().elements())?;
964 let () = s.serialize_field("upper", &desc.upper().elements())?;
965 let () = s.serialize_field("since", &desc.since().elements())?;
966 let () = s.serialize_field("len", len)?;
967 let () = s.serialize_field("part_runs", &self.runs().collect::<Vec<_>>())?;
968 s.end()
969 }
970}
971
972impl<T: Ord> PartialOrd for HollowBatch<T> {
973 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
974 Some(self.cmp(other))
975 }
976}
977
978impl<T: Ord> Ord for HollowBatch<T> {
979 fn cmp(&self, other: &Self) -> Ordering {
980 let HollowBatch {
983 desc: self_desc,
984 parts: self_parts,
985 len: self_len,
986 run_splits: self_runs,
987 run_meta: self_run_meta,
988 } = self;
989 let HollowBatch {
990 desc: other_desc,
991 parts: other_parts,
992 len: other_len,
993 run_splits: other_runs,
994 run_meta: other_run_meta,
995 } = other;
996 (
997 self_desc.lower().elements(),
998 self_desc.upper().elements(),
999 self_desc.since().elements(),
1000 self_parts,
1001 self_len,
1002 self_runs,
1003 self_run_meta,
1004 )
1005 .cmp(&(
1006 other_desc.lower().elements(),
1007 other_desc.upper().elements(),
1008 other_desc.since().elements(),
1009 other_parts,
1010 other_len,
1011 other_runs,
1012 other_run_meta,
1013 ))
1014 }
1015}
1016
1017impl<T: Timestamp + Codec64 + Sync> HollowBatch<T> {
1018 pub(crate) fn part_stream<'a>(
1019 &'a self,
1020 shard_id: ShardId,
1021 blob: &'a dyn Blob,
1022 metrics: &'a Metrics,
1023 ) -> impl Stream<Item = Result<Cow<'a, BatchPart<T>>, MissingBlob>> + 'a {
1024 stream! {
1025 for part in &self.parts {
1026 for await part in part.part_stream(shard_id, blob, metrics) {
1027 yield part;
1028 }
1029 }
1030 }
1031 }
1032}
1033impl<T> HollowBatch<T> {
1034 pub(crate) fn new(
1041 desc: Description<T>,
1042 parts: Vec<RunPart<T>>,
1043 len: usize,
1044 run_meta: Vec<RunMeta>,
1045 run_splits: Vec<usize>,
1046 ) -> Self {
1047 debug_assert!(
1048 run_splits.is_strictly_sorted(),
1049 "run indices should be strictly increasing"
1050 );
1051 mz_ore::soft_assert_no_log!(
1052 run_splits.first().map_or(true, |i| *i > 0),
1053 "run indices should be positive"
1054 );
1055 mz_ore::soft_assert_no_log!(
1056 run_splits.last().map_or(true, |i| *i < parts.len()),
1057 "run indices should be valid indices into parts"
1058 );
1059 mz_ore::soft_assert_no_log!(
1060 parts.is_empty() || run_meta.len() == run_splits.len() + 1,
1061 "all metadata should correspond to a run"
1062 );
1063
1064 Self {
1065 desc,
1066 len,
1067 parts,
1068 run_splits,
1069 run_meta,
1070 }
1071 }
1072
1073 pub(crate) fn new_run(desc: Description<T>, parts: Vec<RunPart<T>>, len: usize) -> Self {
1075 let run_meta = if parts.is_empty() {
1076 vec![]
1077 } else {
1078 vec![RunMeta::default()]
1079 };
1080 Self {
1081 desc,
1082 len,
1083 parts,
1084 run_splits: vec![],
1085 run_meta,
1086 }
1087 }
1088
1089 #[cfg(test)]
1090 pub(crate) fn new_run_for_test(
1091 desc: Description<T>,
1092 parts: Vec<RunPart<T>>,
1093 len: usize,
1094 run_id: RunId,
1095 ) -> Self {
1096 let run_meta = if parts.is_empty() {
1097 vec![]
1098 } else {
1099 let mut meta = RunMeta::default();
1100 meta.id = Some(run_id);
1101 vec![meta]
1102 };
1103 Self {
1104 desc,
1105 len,
1106 parts,
1107 run_splits: vec![],
1108 run_meta,
1109 }
1110 }
1111
1112 pub(crate) fn empty(desc: Description<T>) -> Self {
1114 Self {
1115 desc,
1116 len: 0,
1117 parts: vec![],
1118 run_splits: vec![],
1119 run_meta: vec![],
1120 }
1121 }
1122
1123 pub(crate) fn runs(&self) -> impl Iterator<Item = (&RunMeta, &[RunPart<T>])> {
1124 let run_ends = self
1125 .run_splits
1126 .iter()
1127 .copied()
1128 .chain(std::iter::once(self.parts.len()));
1129 let run_metas = self.run_meta.iter();
1130 let run_parts = run_ends
1131 .scan(0, |start, end| {
1132 let range = *start..end;
1133 *start = end;
1134 Some(range)
1135 })
1136 .filter(|range| !range.is_empty())
1137 .map(|range| &self.parts[range]);
1138 run_metas.zip_eq(run_parts)
1139 }
1140
1141 pub(crate) fn inline_bytes(&self) -> usize {
1142 self.parts.iter().map(|x| x.inline_bytes()).sum()
1143 }
1144
1145 pub(crate) fn is_empty(&self) -> bool {
1146 self.parts.is_empty()
1147 }
1148
1149 pub(crate) fn part_count(&self) -> usize {
1150 self.parts.len()
1151 }
1152
1153 pub fn encoded_size_bytes(&self) -> usize {
1155 self.parts.iter().map(|p| p.encoded_size_bytes()).sum()
1156 }
1157}
1158
1159impl<T: Timestamp + TotalOrder> HollowBatch<T> {
1161 pub(crate) fn rewrite_ts(
1162 &mut self,
1163 frontier: &Antichain<T>,
1164 new_upper: Antichain<T>,
1165 ) -> Result<(), String> {
1166 if !PartialOrder::less_than(frontier, &new_upper) {
1167 return Err(format!(
1168 "rewrite frontier {:?} !< rewrite upper {:?}",
1169 frontier.elements(),
1170 new_upper.elements(),
1171 ));
1172 }
1173 if PartialOrder::less_than(&new_upper, self.desc.upper()) {
1174 return Err(format!(
1175 "rewrite upper {:?} < batch upper {:?}",
1176 new_upper.elements(),
1177 self.desc.upper().elements(),
1178 ));
1179 }
1180
1181 if PartialOrder::less_than(frontier, self.desc.lower()) {
1184 return Err(format!(
1185 "rewrite frontier {:?} < batch lower {:?}",
1186 frontier.elements(),
1187 self.desc.lower().elements(),
1188 ));
1189 }
1190 if self.desc.since() != &Antichain::from_elem(T::minimum()) {
1191 return Err(format!(
1192 "batch since {:?} != minimum antichain {:?}",
1193 self.desc.since().elements(),
1194 [T::minimum()],
1195 ));
1196 }
1197 for part in self.parts.iter() {
1198 let Some(ts_rewrite) = part.ts_rewrite() else {
1199 continue;
1200 };
1201 if PartialOrder::less_than(frontier, ts_rewrite) {
1202 return Err(format!(
1203 "rewrite frontier {:?} < batch rewrite {:?}",
1204 frontier.elements(),
1205 ts_rewrite.elements(),
1206 ));
1207 }
1208 }
1209
1210 self.desc = Description::new(
1211 self.desc.lower().clone(),
1212 new_upper,
1213 self.desc.since().clone(),
1214 );
1215 for part in &mut self.parts {
1216 match part {
1217 RunPart::Single(BatchPart::Hollow(part)) => {
1218 part.ts_rewrite = Some(frontier.clone())
1219 }
1220 RunPart::Single(BatchPart::Inline { ts_rewrite, .. }) => {
1221 *ts_rewrite = Some(frontier.clone())
1222 }
1223 RunPart::Many(runs) => {
1224 panic!("unexpected rewrite of a hollow runs ref: {runs:?}");
1227 }
1228 }
1229 }
1230 Ok(())
1231 }
1232}
1233
1234impl<T: Ord> PartialOrd for HollowBatchPart<T> {
1235 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1236 Some(self.cmp(other))
1237 }
1238}
1239
1240impl<T: Ord> Ord for HollowBatchPart<T> {
1241 fn cmp(&self, other: &Self) -> Ordering {
1242 let HollowBatchPart {
1245 key: self_key,
1246 meta: self_meta,
1247 encoded_size_bytes: self_encoded_size_bytes,
1248 key_lower: self_key_lower,
1249 structured_key_lower: self_structured_key_lower,
1250 stats: self_stats,
1251 ts_rewrite: self_ts_rewrite,
1252 diffs_sum: self_diffs_sum,
1253 format: self_format,
1254 schema_id: self_schema_id,
1255 deprecated_schema_id: self_deprecated_schema_id,
1256 } = self;
1257 let HollowBatchPart {
1258 key: other_key,
1259 meta: other_meta,
1260 encoded_size_bytes: other_encoded_size_bytes,
1261 key_lower: other_key_lower,
1262 structured_key_lower: other_structured_key_lower,
1263 stats: other_stats,
1264 ts_rewrite: other_ts_rewrite,
1265 diffs_sum: other_diffs_sum,
1266 format: other_format,
1267 schema_id: other_schema_id,
1268 deprecated_schema_id: other_deprecated_schema_id,
1269 } = other;
1270 (
1271 self_key,
1272 self_meta,
1273 self_encoded_size_bytes,
1274 self_key_lower,
1275 self_structured_key_lower,
1276 self_stats,
1277 self_ts_rewrite.as_ref().map(|x| x.elements()),
1278 self_diffs_sum,
1279 self_format,
1280 self_schema_id,
1281 self_deprecated_schema_id,
1282 )
1283 .cmp(&(
1284 other_key,
1285 other_meta,
1286 other_encoded_size_bytes,
1287 other_key_lower,
1288 other_structured_key_lower,
1289 other_stats,
1290 other_ts_rewrite.as_ref().map(|x| x.elements()),
1291 other_diffs_sum,
1292 other_format,
1293 other_schema_id,
1294 other_deprecated_schema_id,
1295 ))
1296 }
1297}
1298
1299#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1301pub struct HollowRollup {
1302 pub key: PartialRollupKey,
1304 pub encoded_size_bytes: Option<usize>,
1306 pub finalized: bool,
1309}
1310
1311#[derive(Debug)]
1313pub enum HollowBlobRef<'a, T> {
1314 Batch(&'a HollowBatch<T>),
1315 Rollup(&'a HollowRollup),
1316}
1317
1318#[derive(
1320 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Arbitrary, Serialize
1321)]
1322pub struct ActiveRollup {
1323 pub seqno: SeqNo,
1324 pub start_ms: u64,
1325}
1326
1327#[derive(
1329 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Arbitrary, Serialize
1330)]
1331pub struct ActiveGc {
1332 pub seqno: SeqNo,
1333 pub start_ms: u64,
1334}
1335
1336#[derive(Debug)]
1341#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
1342pub struct NoOpStateTransition<T>(pub T);
1343
1344#[derive(Debug, Clone)]
1346#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
1347pub struct StateCollections<T> {
1348 pub(crate) version: Version,
1352
1353 pub(crate) last_gc_req: SeqNo,
1356
1357 pub(crate) rollups: BTreeMap<SeqNo, HollowRollup>,
1359
1360 pub(crate) active_rollup: Option<ActiveRollup>,
1362 pub(crate) active_gc: Option<ActiveGc>,
1364
1365 pub(crate) leased_readers: BTreeMap<LeasedReaderId, LeasedReaderState<T>>,
1366 pub(crate) critical_readers: BTreeMap<CriticalReaderId, CriticalReaderState<T>>,
1367 pub(crate) writers: BTreeMap<WriterId, WriterState<T>>,
1368 pub(crate) schemas: BTreeMap<SchemaId, EncodedSchemas>,
1369
1370 pub(crate) trace: Trace<T>,
1375}
1376
1377#[derive(Debug, Clone, Serialize, PartialEq)]
1393pub struct EncodedSchemas {
1394 pub key: Bytes,
1396 pub key_data_type: Bytes,
1399 pub val: Bytes,
1401 pub val_data_type: Bytes,
1404}
1405
1406impl EncodedSchemas {
1407 pub(crate) fn decode_data_type(buf: &[u8]) -> DataType {
1408 let proto = prost::Message::decode(buf).expect("valid ProtoDataType");
1409 DataType::from_proto(proto).expect("valid DataType")
1410 }
1411}
1412
1413#[derive(Debug)]
1414#[cfg_attr(test, derive(PartialEq))]
1415pub enum CompareAndAppendBreak<T> {
1416 AlreadyCommitted,
1417 Upper {
1418 shard_upper: Antichain<T>,
1419 writer_upper: Antichain<T>,
1420 },
1421 InvalidUsage(InvalidUsage<T>),
1422 InlineBackpressure,
1423}
1424
1425#[derive(Debug)]
1426#[cfg_attr(test, derive(PartialEq))]
1427pub enum SnapshotErr<T> {
1428 AsOfNotYetAvailable(SeqNo, Upper<T>),
1429 AsOfHistoricalDistinctionsLost(Since<T>),
1430}
1431
1432impl<T> StateCollections<T>
1433where
1434 T: Timestamp + Lattice + Codec64,
1435{
1436 pub fn add_rollup(
1437 &mut self,
1438 add_rollup: (SeqNo, &HollowRollup),
1439 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
1440 let (rollup_seqno, rollup) = add_rollup;
1441 let applied = match self.rollups.get(&rollup_seqno) {
1442 Some(x) => x.key == rollup.key,
1443 None => {
1444 if let Some(min_kept) = self.rollups.keys().next() {
1465 if rollup_seqno < *min_kept {
1466 return Continue(false);
1467 }
1468 }
1469 self.active_rollup = None;
1470 self.rollups.insert(rollup_seqno, rollup.to_owned());
1471 true
1472 }
1473 };
1474 Continue(applied)
1478 }
1479
1480 pub fn remove_rollups(
1481 &mut self,
1482 remove_rollups: &[(SeqNo, PartialRollupKey)],
1483 remove_finalized: bool,
1484 ) -> ControlFlow<NoOpStateTransition<Vec<SeqNo>>, Vec<SeqNo>> {
1485 if self.is_tombstone() {
1486 if !remove_finalized || !self.is_finalized() {
1487 return Break(NoOpStateTransition(vec![]));
1488 }
1489 return self.remove_finalized_rollups(remove_rollups);
1490 }
1491
1492 let active_gc_was_set = self.active_gc.take().is_some();
1495
1496 if remove_rollups.is_empty() {
1497 return if active_gc_was_set {
1498 Continue(vec![])
1499 } else {
1500 Break(NoOpStateTransition(vec![]))
1501 };
1502 }
1503
1504 let mut removed = vec![];
1505 for (seqno, key) in remove_rollups {
1506 let removed_key = self.rollups.remove(seqno);
1507 mz_ore::soft_assert_no_log!(
1508 removed_key.as_ref().map_or(true, |x| &x.key == key),
1509 "rollup at {} to be removed has key {:?} in state, but GC asked to remove {}",
1510 seqno,
1511 removed_key,
1512 key
1513 );
1514
1515 if removed_key.is_some() {
1516 removed.push(*seqno);
1517 }
1518 }
1519
1520 Continue(removed)
1521 }
1522
1523 fn remove_finalized_rollups(
1529 &mut self,
1530 remove_rollups: &[(SeqNo, PartialRollupKey)],
1531 ) -> ControlFlow<NoOpStateTransition<Vec<SeqNo>>, Vec<SeqNo>> {
1532 let latest = self.rollups.keys().next_back().copied();
1533 let mut removed = vec![];
1534 for (seqno, key) in remove_rollups {
1535 if Some(*seqno) == latest {
1536 mz_ore::soft_panic_or_log!("GC asked to remove a finalized shard's latest rollup");
1537 continue;
1538 }
1539 if self
1541 .rollups
1542 .get(seqno)
1543 .is_some_and(|x| &x.key == key && !x.finalized)
1544 {
1545 self.rollups.remove(seqno);
1546 removed.push(*seqno);
1547 }
1548 }
1549 if removed.is_empty() {
1550 return Break(NoOpStateTransition(vec![]));
1551 }
1552 self.active_gc = None;
1553 Continue(removed)
1554 }
1555
1556 pub fn register_leased_reader(
1557 &mut self,
1558 hostname: &str,
1559 reader_id: &LeasedReaderId,
1560 purpose: &str,
1561 seqno: SeqNo,
1562 lease_duration: Duration,
1563 heartbeat_timestamp_ms: u64,
1564 use_critical_since: bool,
1565 ) -> ControlFlow<
1566 NoOpStateTransition<(LeasedReaderState<T>, SeqNo)>,
1567 (LeasedReaderState<T>, SeqNo),
1568 > {
1569 let since = if use_critical_since {
1570 self.critical_since()
1571 .unwrap_or_else(|| self.trace.since().clone())
1572 } else {
1573 self.trace.since().clone()
1574 };
1575 let reader_state = LeasedReaderState {
1576 debug: HandleDebugState {
1577 hostname: hostname.to_owned(),
1578 purpose: purpose.to_owned(),
1579 },
1580 seqno,
1581 since,
1582 last_heartbeat_timestamp_ms: heartbeat_timestamp_ms,
1583 lease_duration_ms: u64::try_from(lease_duration.as_millis())
1584 .expect("lease duration as millis should fit within u64"),
1585 };
1586
1587 if self.is_tombstone() {
1592 return Break(NoOpStateTransition((reader_state, self.seqno_since(seqno))));
1593 }
1594
1595 self.leased_readers
1597 .insert(reader_id.clone(), reader_state.clone());
1598 Continue((reader_state, self.seqno_since(seqno)))
1599 }
1600
1601 pub fn register_critical_reader(
1602 &mut self,
1603 hostname: &str,
1604 reader_id: &CriticalReaderId,
1605 opaque: Opaque,
1606 purpose: &str,
1607 ) -> ControlFlow<NoOpStateTransition<CriticalReaderState<T>>, CriticalReaderState<T>> {
1608 let state = CriticalReaderState {
1609 debug: HandleDebugState {
1610 hostname: hostname.to_owned(),
1611 purpose: purpose.to_owned(),
1612 },
1613 since: self.trace.since().clone(),
1614 opaque,
1615 };
1616
1617 if self.is_tombstone() {
1622 return Break(NoOpStateTransition(state));
1623 }
1624
1625 let state = match self.critical_readers.get_mut(reader_id) {
1626 Some(existing_state) => {
1627 existing_state.debug = state.debug;
1628 existing_state.clone()
1629 }
1630 None => {
1631 self.critical_readers
1632 .insert(reader_id.clone(), state.clone());
1633 state
1634 }
1635 };
1636 Continue(state)
1637 }
1638
1639 pub fn register_schema<K: Codec, V: Codec>(
1640 &mut self,
1641 key_schema: &K::Schema,
1642 val_schema: &V::Schema,
1643 ) -> ControlFlow<NoOpStateTransition<Option<SchemaId>>, Option<SchemaId>> {
1644 fn encode_data_type(data_type: &DataType) -> Bytes {
1645 let proto = data_type.into_proto();
1646 prost::Message::encode_to_vec(&proto).into()
1647 }
1648
1649 let existing_id = self.schemas.iter().rev().find(|(_, x)| {
1661 K::decode_schema(&x.key) == *key_schema && V::decode_schema(&x.val) == *val_schema
1662 });
1663 match existing_id {
1664 Some((schema_id, _)) => {
1665 Break(NoOpStateTransition(Some(*schema_id)))
1670 }
1671 None if self.is_tombstone() => {
1672 Break(NoOpStateTransition(None))
1674 }
1675 None if self.schemas.is_empty() => {
1676 let id = SchemaId(self.schemas.len());
1680 let key_data_type = mz_persist_types::columnar::data_type::<K>(key_schema)
1681 .expect("valid key schema");
1682 let val_data_type = mz_persist_types::columnar::data_type::<V>(val_schema)
1683 .expect("valid val schema");
1684 let prev = self.schemas.insert(
1685 id,
1686 EncodedSchemas {
1687 key: K::encode_schema(key_schema),
1688 key_data_type: encode_data_type(&key_data_type),
1689 val: V::encode_schema(val_schema),
1690 val_data_type: encode_data_type(&val_data_type),
1691 },
1692 );
1693 assert_eq!(prev, None);
1694 Continue(Some(id))
1695 }
1696 None => {
1697 info!(
1698 "register_schemas got {:?} expected {:?}",
1699 key_schema,
1700 self.schemas
1701 .iter()
1702 .map(|(id, x)| (id, K::decode_schema(&x.key)))
1703 .collect::<Vec<_>>()
1704 );
1705 Break(NoOpStateTransition(None))
1708 }
1709 }
1710 }
1711
1712 pub fn compare_and_evolve_schema<K: Codec, V: Codec>(
1713 &mut self,
1714 expected: SchemaId,
1715 key_schema: &K::Schema,
1716 val_schema: &V::Schema,
1717 ) -> ControlFlow<NoOpStateTransition<CaESchema<K, V>>, CaESchema<K, V>> {
1718 fn data_type<T>(schema: &impl Schema<T>) -> DataType {
1719 let array = Schema::encoder(schema).expect("valid schema").finish();
1723 Array::data_type(&array).clone()
1724 }
1725
1726 let (current_id, current) = self
1727 .schemas
1728 .last_key_value()
1729 .expect("all shards have a schema");
1730
1731 let current_key = K::decode_schema(¤t.key);
1732 let current_key_dt = EncodedSchemas::decode_data_type(¤t.key_data_type);
1733 let current_val = V::decode_schema(¤t.val);
1734 let current_val_dt = EncodedSchemas::decode_data_type(¤t.val_data_type);
1735
1736 let key_dt = data_type(key_schema);
1737 let val_dt = data_type(val_schema);
1738
1739 if current_key == *key_schema
1749 && current_key_dt == key_dt
1750 && current_val == *val_schema
1751 && current_val_dt == val_dt
1752 {
1753 return Break(NoOpStateTransition(CaESchema::Ok(*current_id)));
1754 }
1755
1756 if *current_id != expected {
1757 return Break(NoOpStateTransition(CaESchema::ExpectedMismatch {
1758 schema_id: *current_id,
1759 key: current_key,
1760 val: current_val,
1761 }));
1762 }
1763
1764 let key_fn = backward_compatible(¤t_key_dt, &key_dt);
1765 let val_fn = backward_compatible(¤t_val_dt, &val_dt);
1766 let (Some(key_fn), Some(val_fn)) = (key_fn, val_fn) else {
1767 return Break(NoOpStateTransition(CaESchema::Incompatible));
1768 };
1769 if key_fn.contains_drop() || val_fn.contains_drop() {
1773 return Break(NoOpStateTransition(CaESchema::Incompatible));
1774 }
1775
1776 let id = SchemaId(self.schemas.len());
1780 self.schemas.insert(
1781 id,
1782 EncodedSchemas {
1783 key: K::encode_schema(key_schema),
1784 key_data_type: prost::Message::encode_to_vec(&key_dt.into_proto()).into(),
1785 val: V::encode_schema(val_schema),
1786 val_data_type: prost::Message::encode_to_vec(&val_dt.into_proto()).into(),
1787 },
1788 );
1789 Continue(CaESchema::Ok(id))
1790 }
1791
1792 pub fn compare_and_append(
1793 &mut self,
1794 batch: &HollowBatch<T>,
1795 writer_id: &WriterId,
1796 heartbeat_timestamp_ms: u64,
1797 lease_duration_ms: u64,
1798 idempotency_token: &IdempotencyToken,
1799 debug_info: &HandleDebugState,
1800 inline_writes_total_max_bytes: usize,
1801 claim_compaction_percent: usize,
1802 claim_compaction_min_version: Option<&Version>,
1803 ) -> ControlFlow<CompareAndAppendBreak<T>, Vec<FueledMergeReq<T>>> {
1804 if self.is_tombstone() {
1809 assert_eq!(self.trace.upper(), &Antichain::new());
1810 return Break(CompareAndAppendBreak::Upper {
1811 shard_upper: Antichain::new(),
1812 writer_upper: Antichain::new(),
1817 });
1818 }
1819
1820 let writer_state = self
1821 .writers
1822 .entry(writer_id.clone())
1823 .or_insert_with(|| WriterState {
1824 last_heartbeat_timestamp_ms: heartbeat_timestamp_ms,
1825 lease_duration_ms,
1826 most_recent_write_token: IdempotencyToken::SENTINEL,
1827 most_recent_write_upper: Antichain::from_elem(T::minimum()),
1828 debug: debug_info.clone(),
1829 });
1830
1831 if PartialOrder::less_than(batch.desc.upper(), batch.desc.lower()) {
1832 return Break(CompareAndAppendBreak::InvalidUsage(
1833 InvalidUsage::InvalidBounds {
1834 lower: batch.desc.lower().clone(),
1835 upper: batch.desc.upper().clone(),
1836 },
1837 ));
1838 }
1839
1840 if batch.desc.upper() == batch.desc.lower() && !batch.is_empty() {
1843 return Break(CompareAndAppendBreak::InvalidUsage(
1844 InvalidUsage::InvalidEmptyTimeInterval {
1845 lower: batch.desc.lower().clone(),
1846 upper: batch.desc.upper().clone(),
1847 keys: batch
1848 .parts
1849 .iter()
1850 .map(|x| x.printable_name().to_owned())
1851 .collect(),
1852 },
1853 ));
1854 }
1855
1856 if idempotency_token == &writer_state.most_recent_write_token {
1857 assert_eq!(batch.desc.upper(), &writer_state.most_recent_write_upper);
1862 assert!(
1863 PartialOrder::less_equal(batch.desc.upper(), self.trace.upper()),
1864 "{:?} vs {:?}",
1865 batch.desc.upper(),
1866 self.trace.upper()
1867 );
1868 return Break(CompareAndAppendBreak::AlreadyCommitted);
1869 }
1870
1871 let shard_upper = self.trace.upper();
1872 if shard_upper != batch.desc.lower() {
1873 return Break(CompareAndAppendBreak::Upper {
1874 shard_upper: shard_upper.clone(),
1875 writer_upper: writer_state.most_recent_write_upper.clone(),
1876 });
1877 }
1878
1879 let new_inline_bytes = batch.inline_bytes();
1880 if new_inline_bytes > 0 {
1881 let mut existing_inline_bytes = 0;
1882 self.trace
1883 .map_batches(|x| existing_inline_bytes += x.inline_bytes());
1884 if existing_inline_bytes + new_inline_bytes >= inline_writes_total_max_bytes {
1888 return Break(CompareAndAppendBreak::InlineBackpressure);
1889 }
1890 }
1891
1892 let mut merge_reqs = if batch.desc.upper() != batch.desc.lower() {
1893 self.trace.push_batch(batch.clone())
1894 } else {
1895 Vec::new()
1896 };
1897
1898 let all_empty_reqs = merge_reqs
1901 .iter()
1902 .all(|req| req.inputs.iter().all(|b| b.batch.is_empty()));
1903 if all_empty_reqs && !batch.is_empty() {
1904 let mut reqs_to_take = claim_compaction_percent / 100;
1905 if (usize::cast_from(idempotency_token.hashed()) % 100)
1906 < (claim_compaction_percent % 100)
1907 {
1908 reqs_to_take += 1;
1909 }
1910 let threshold_ms = heartbeat_timestamp_ms.saturating_sub(lease_duration_ms);
1911 let min_writer = claim_compaction_min_version.map(WriterKey::for_version);
1912 merge_reqs.extend(
1913 self.trace
1916 .fueled_merge_reqs_before_ms(threshold_ms, min_writer)
1917 .take(reqs_to_take),
1918 )
1919 }
1920
1921 for req in &merge_reqs {
1922 self.trace.claim_compaction(
1923 req.id,
1924 ActiveCompaction {
1925 start_ms: heartbeat_timestamp_ms,
1926 },
1927 )
1928 }
1929
1930 mz_ore::soft_assert_eq_no_log!(self.trace.upper(), batch.desc.upper());
1931 writer_state.most_recent_write_token = idempotency_token.clone();
1932 assert!(
1934 PartialOrder::less_equal(&writer_state.most_recent_write_upper, batch.desc.upper()),
1935 "{:?} vs {:?}",
1936 writer_state.most_recent_write_upper,
1937 batch.desc.upper()
1938 );
1939 writer_state
1940 .most_recent_write_upper
1941 .clone_from(batch.desc.upper());
1942
1943 writer_state.last_heartbeat_timestamp_ms = std::cmp::max(
1945 heartbeat_timestamp_ms,
1946 writer_state.last_heartbeat_timestamp_ms,
1947 );
1948
1949 Continue(merge_reqs)
1950 }
1951
1952 pub fn apply_merge_res<D: Codec64 + Monoid + PartialEq>(
1953 &mut self,
1954 res: &FueledMergeRes<T>,
1955 metrics: &ColumnarMetrics,
1956 ) -> ControlFlow<NoOpStateTransition<ApplyMergeResult>, ApplyMergeResult> {
1957 if self.is_tombstone() {
1962 return Break(NoOpStateTransition(ApplyMergeResult::NotAppliedNoMatch));
1963 }
1964
1965 let apply_merge_result = self.trace.apply_merge_res_checked::<D>(res, metrics);
1966 Continue(apply_merge_result)
1967 }
1968
1969 pub fn spine_exert(
1970 &mut self,
1971 fuel: usize,
1972 ) -> ControlFlow<NoOpStateTransition<Vec<FueledMergeReq<T>>>, Vec<FueledMergeReq<T>>> {
1973 let (merge_reqs, did_work) = self.trace.exert(fuel);
1974 if did_work {
1975 Continue(merge_reqs)
1976 } else {
1977 assert!(merge_reqs.is_empty());
1978 Break(NoOpStateTransition(Vec::new()))
1981 }
1982 }
1983
1984 pub fn downgrade_since(
1985 &mut self,
1986 reader_id: &LeasedReaderId,
1987 seqno: SeqNo,
1988 outstanding_seqno: SeqNo,
1989 new_since: &Antichain<T>,
1990 heartbeat_timestamp_ms: u64,
1991 ) -> ControlFlow<NoOpStateTransition<Since<T>>, Since<T>> {
1992 if self.is_tombstone() {
1997 return Break(NoOpStateTransition(Since(Antichain::new())));
1998 }
1999
2000 let Some(reader_state) = self.leased_reader(reader_id) else {
2003 tracing::warn!(
2004 "Leased reader {reader_id} was expired due to inactivity. Did the machine go to sleep?",
2005 );
2006 return Break(NoOpStateTransition(Since(Antichain::new())));
2007 };
2008
2009 reader_state.last_heartbeat_timestamp_ms = std::cmp::max(
2012 heartbeat_timestamp_ms,
2013 reader_state.last_heartbeat_timestamp_ms,
2014 );
2015
2016 let seqno = {
2017 assert!(
2018 outstanding_seqno >= reader_state.seqno,
2019 "SeqNos cannot go backward; however, oldest leased SeqNo ({:?}) \
2020 is behind current reader_state ({:?})",
2021 outstanding_seqno,
2022 reader_state.seqno,
2023 );
2024 std::cmp::min(outstanding_seqno, seqno)
2025 };
2026
2027 reader_state.seqno = seqno;
2028
2029 let reader_current_since = if PartialOrder::less_than(&reader_state.since, new_since) {
2030 reader_state.since.clone_from(new_since);
2031 self.update_since();
2032 new_since.clone()
2033 } else {
2034 reader_state.since.clone()
2037 };
2038
2039 Continue(Since(reader_current_since))
2040 }
2041
2042 pub fn compare_and_downgrade_since(
2043 &mut self,
2044 reader_id: &CriticalReaderId,
2045 expected_opaque: &Opaque,
2046 (new_opaque, new_since): (&Opaque, &Antichain<T>),
2047 ) -> ControlFlow<
2048 NoOpStateTransition<Result<Since<T>, (Opaque, Since<T>)>>,
2049 Result<Since<T>, (Opaque, Since<T>)>,
2050 > {
2051 if self.is_tombstone() {
2056 return Break(NoOpStateTransition(Ok(Since(Antichain::new()))));
2060 }
2061
2062 let reader_state = self.critical_reader(reader_id);
2063
2064 if reader_state.opaque != *expected_opaque {
2065 return Continue(Err((
2068 reader_state.opaque.clone(),
2069 Since(reader_state.since.clone()),
2070 )));
2071 }
2072
2073 reader_state.opaque = new_opaque.clone();
2074 if PartialOrder::less_equal(&reader_state.since, new_since) {
2075 reader_state.since.clone_from(new_since);
2076 self.update_since();
2077 Continue(Ok(Since(new_since.clone())))
2078 } else {
2079 Continue(Ok(Since(reader_state.since.clone())))
2083 }
2084 }
2085
2086 pub fn expire_leased_reader(
2087 &mut self,
2088 reader_id: &LeasedReaderId,
2089 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2090 if self.is_tombstone() {
2095 return Break(NoOpStateTransition(false));
2096 }
2097
2098 let existed = self.leased_readers.remove(reader_id).is_some();
2099 if existed {
2100 }
2114 Continue(existed)
2117 }
2118
2119 pub fn expire_critical_reader(
2120 &mut self,
2121 reader_id: &CriticalReaderId,
2122 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2123 if self.is_tombstone() {
2128 return Break(NoOpStateTransition(false));
2129 }
2130
2131 let existed = self.critical_readers.remove(reader_id).is_some();
2132 if existed {
2133 }
2147 Continue(existed)
2151 }
2152
2153 pub fn expire_writer(
2154 &mut self,
2155 writer_id: &WriterId,
2156 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2157 if self.is_tombstone() {
2162 return Break(NoOpStateTransition(false));
2163 }
2164
2165 let existed = self.writers.remove(writer_id).is_some();
2166 Continue(existed)
2170 }
2171
2172 fn leased_reader(&mut self, id: &LeasedReaderId) -> Option<&mut LeasedReaderState<T>> {
2173 self.leased_readers.get_mut(id)
2174 }
2175
2176 fn critical_reader(&mut self, id: &CriticalReaderId) -> &mut CriticalReaderState<T> {
2177 self.critical_readers
2178 .get_mut(id)
2179 .unwrap_or_else(|| {
2180 panic!(
2181 "Unknown CriticalReaderId({}). It was either never registered, or has been manually expired.",
2182 id
2183 )
2184 })
2185 }
2186
2187 fn critical_since(&self) -> Option<Antichain<T>> {
2188 let mut critical_sinces = self.critical_readers.values().map(|r| &r.since);
2189 let mut since = critical_sinces.next().cloned()?;
2190 for s in critical_sinces {
2191 since.meet_assign(s);
2192 }
2193 Some(since)
2194 }
2195
2196 fn update_since(&mut self) {
2197 let mut sinces_iter = self
2198 .leased_readers
2199 .values()
2200 .map(|x| &x.since)
2201 .chain(self.critical_readers.values().map(|x| &x.since));
2202 let mut since = match sinces_iter.next() {
2203 Some(since) => since.clone(),
2204 None => {
2205 return;
2208 }
2209 };
2210 while let Some(s) = sinces_iter.next() {
2211 since.meet_assign(s);
2212 }
2213 self.trace.downgrade_since(&since);
2214 }
2215
2216 fn seqno_since(&self, seqno: SeqNo) -> SeqNo {
2217 let mut seqno_since = seqno;
2218 for cap in self.leased_readers.values() {
2219 seqno_since = std::cmp::min(seqno_since, cap.seqno);
2220 }
2221 seqno_since
2223 }
2224
2225 fn tombstone_batch() -> HollowBatch<T> {
2226 HollowBatch::empty(Description::new(
2227 Antichain::from_elem(T::minimum()),
2228 Antichain::new(),
2229 Antichain::new(),
2230 ))
2231 }
2232
2233 pub(crate) fn is_tombstone(&self) -> bool {
2234 self.trace.upper().is_empty()
2235 && self.trace.since().is_empty()
2236 && self.writers.is_empty()
2237 && self.leased_readers.is_empty()
2238 && self.critical_readers.is_empty()
2239 }
2240
2241 pub(crate) fn is_finalized(&self) -> bool {
2243 self.is_tombstone() && self.is_single_empty_batch()
2244 }
2245
2246 pub(crate) fn is_single_empty_batch(&self) -> bool {
2247 let mut batch_count = 0;
2248 let mut is_empty = true;
2249 self.trace.map_batches(|b| {
2250 batch_count += 1;
2251 is_empty &= b.is_empty()
2252 });
2253 batch_count <= 1 && is_empty
2254 }
2255
2256 pub fn become_tombstone_and_shrink(&mut self) -> ControlFlow<NoOpStateTransition<()>, ()> {
2257 assert_eq!(self.trace.upper(), &Antichain::new());
2258 assert_eq!(self.trace.since(), &Antichain::new());
2259
2260 let was_tombstone = self.is_tombstone();
2263
2264 self.writers.clear();
2266 self.leased_readers.clear();
2267 self.critical_readers.clear();
2268
2269 mz_ore::soft_assert_no_log!(self.is_tombstone());
2270
2271 let mut to_replace = None;
2280 let mut batch_count = 0;
2281 self.trace.map_batches(|b| {
2282 batch_count += 1;
2283 if !b.is_empty() && to_replace.is_none() {
2284 to_replace = Some(b.desc.clone());
2285 }
2286 });
2287 if let Some(desc) = to_replace {
2288 let result = self.trace.apply_tombstone_merge(&desc);
2292 assert!(
2293 result.matched(),
2294 "merge with a matching desc should always match"
2295 );
2296 Continue(())
2297 } else if batch_count > 1 {
2298 let mut new_trace = Trace::default();
2303 new_trace.downgrade_since(&Antichain::new());
2304 let merge_reqs = new_trace.push_batch(Self::tombstone_batch());
2305 assert_eq!(merge_reqs, Vec::new());
2306 self.trace = new_trace;
2307 Continue(())
2308 } else if !was_tombstone {
2309 Continue(())
2312 } else {
2313 Break(NoOpStateTransition(()))
2316 }
2317 }
2318}
2319
2320#[derive(Debug)]
2322#[cfg_attr(any(test, debug_assertions), derive(Clone, PartialEq))]
2323pub struct State<T> {
2324 pub(crate) shard_id: ShardId,
2325
2326 pub(crate) seqno: SeqNo,
2327 pub(crate) walltime_ms: u64,
2330 pub(crate) hostname: String,
2333 pub(crate) collections: StateCollections<T>,
2334}
2335
2336pub struct TypedState<K, V, T, D> {
2339 pub(crate) state: State<T>,
2340
2341 pub(crate) _phantom: PhantomData<fn() -> (K, V, D)>,
2349}
2350
2351impl<K, V, T: Clone, D> TypedState<K, V, T, D> {
2352 #[cfg(any(test, debug_assertions))]
2353 pub(crate) fn clone(&self, hostname: String) -> Self {
2354 TypedState {
2355 state: State {
2356 shard_id: self.shard_id.clone(),
2357 seqno: self.seqno.clone(),
2358 walltime_ms: self.walltime_ms,
2359 hostname,
2360 collections: self.collections.clone(),
2361 },
2362 _phantom: PhantomData,
2363 }
2364 }
2365
2366 pub(crate) fn clone_for_rollup(&self) -> Self {
2367 TypedState {
2368 state: State {
2369 shard_id: self.shard_id.clone(),
2370 seqno: self.seqno.clone(),
2371 walltime_ms: self.walltime_ms,
2372 hostname: self.hostname.clone(),
2373 collections: self.collections.clone(),
2374 },
2375 _phantom: PhantomData,
2376 }
2377 }
2378}
2379
2380impl<K, V, T: Debug, D> Debug for TypedState<K, V, T, D> {
2381 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2382 let TypedState { state, _phantom } = self;
2385 f.debug_struct("TypedState").field("state", state).finish()
2386 }
2387}
2388
2389#[cfg(any(test, debug_assertions))]
2391impl<K, V, T: PartialEq, D> PartialEq for TypedState<K, V, T, D> {
2392 fn eq(&self, other: &Self) -> bool {
2393 let TypedState {
2396 state: self_state,
2397 _phantom,
2398 } = self;
2399 let TypedState {
2400 state: other_state,
2401 _phantom,
2402 } = other;
2403 self_state == other_state
2404 }
2405}
2406
2407impl<K, V, T, D> Deref for TypedState<K, V, T, D> {
2408 type Target = State<T>;
2409
2410 fn deref(&self) -> &Self::Target {
2411 &self.state
2412 }
2413}
2414
2415impl<K, V, T, D> DerefMut for TypedState<K, V, T, D> {
2416 fn deref_mut(&mut self) -> &mut Self::Target {
2417 &mut self.state
2418 }
2419}
2420
2421impl<K, V, T, D> TypedState<K, V, T, D>
2422where
2423 K: Codec,
2424 V: Codec,
2425 T: Timestamp + Lattice + Codec64,
2426 D: Codec64,
2427{
2428 pub fn new(
2429 applier_version: Version,
2430 shard_id: ShardId,
2431 hostname: String,
2432 walltime_ms: u64,
2433 ) -> Self {
2434 let state = State {
2435 shard_id,
2436 seqno: SeqNo::minimum(),
2437 walltime_ms,
2438 hostname,
2439 collections: StateCollections {
2440 version: applier_version,
2441 last_gc_req: SeqNo::minimum(),
2442 rollups: BTreeMap::new(),
2443 active_rollup: None,
2444 active_gc: None,
2445 leased_readers: BTreeMap::new(),
2446 critical_readers: BTreeMap::new(),
2447 writers: BTreeMap::new(),
2448 schemas: BTreeMap::new(),
2449 trace: Trace::default(),
2450 },
2451 };
2452 TypedState {
2453 state,
2454 _phantom: PhantomData,
2455 }
2456 }
2457
2458 pub fn clone_apply<R, E, WorkFn>(
2459 &self,
2460 cfg: &PersistConfig,
2461 work_fn: &mut WorkFn,
2462 ) -> ControlFlow<E, (R, Self)>
2463 where
2464 WorkFn: FnMut(SeqNo, &PersistConfig, &mut StateCollections<T>) -> ControlFlow<E, R>,
2465 {
2466 let mut new_state = State {
2468 shard_id: self.shard_id,
2469 seqno: self.seqno.next(),
2470 walltime_ms: (cfg.now)(),
2471 hostname: cfg.hostname.clone(),
2472 collections: self.collections.clone(),
2473 };
2474
2475 if new_state.walltime_ms <= self.walltime_ms {
2478 new_state.walltime_ms = self.walltime_ms + 1;
2479 }
2480
2481 let work_ret = work_fn(new_state.seqno, cfg, &mut new_state.collections)?;
2482 let new_state = TypedState {
2483 state: new_state,
2484 _phantom: PhantomData,
2485 };
2486 Continue((work_ret, new_state))
2487 }
2488}
2489
2490#[derive(Copy, Clone, Debug)]
2491pub struct GcConfig {
2492 pub use_active_gc: bool,
2493 pub fallback_threshold_ms: u64,
2494 pub min_versions: usize,
2495 pub max_versions: usize,
2496 pub remove_finalized_rollups: bool,
2498}
2499
2500impl<T> State<T>
2501where
2502 T: Timestamp + Lattice + Codec64,
2503{
2504 pub fn shard_id(&self) -> ShardId {
2505 self.shard_id
2506 }
2507
2508 pub fn seqno(&self) -> SeqNo {
2509 self.seqno
2510 }
2511
2512 pub fn since(&self) -> &Antichain<T> {
2513 self.collections.trace.since()
2514 }
2515
2516 pub fn upper(&self) -> &Antichain<T> {
2517 self.collections.trace.upper()
2518 }
2519
2520 pub fn spine_batch_count(&self) -> usize {
2521 self.collections.trace.num_spine_batches()
2522 }
2523
2524 pub fn size_metrics(&self) -> StateSizeMetrics {
2525 let mut ret = StateSizeMetrics::default();
2526 self.blobs().for_each(|x| match x {
2527 HollowBlobRef::Batch(x) => {
2528 ret.hollow_batch_count += 1;
2529 ret.batch_part_count += x.part_count();
2530 ret.num_updates += x.len;
2531
2532 let batch_size = x.encoded_size_bytes();
2533 for x in x.parts.iter() {
2534 if x.is_inline() {
2535 ret.inline_part_count += 1;
2536 }
2537 }
2538 ret.largest_batch_bytes = std::cmp::max(ret.largest_batch_bytes, batch_size);
2539 ret.state_batches_bytes += batch_size;
2540 }
2541 HollowBlobRef::Rollup(x) => {
2542 ret.state_rollup_count += 1;
2543 ret.state_rollups_bytes += x.encoded_size_bytes.unwrap_or_default()
2544 }
2545 });
2546 ret
2547 }
2548
2549 pub fn latest_rollup(&self) -> (&SeqNo, &HollowRollup) {
2550 self.collections
2553 .rollups
2554 .iter()
2555 .rev()
2556 .next()
2557 .expect("State should have at least one rollup if seqno > minimum")
2558 }
2559
2560 pub(crate) fn seqno_since(&self) -> SeqNo {
2561 self.collections.seqno_since(self.seqno)
2562 }
2563
2564 pub fn maybe_gc(&mut self, is_write: bool, now: u64, cfg: GcConfig) -> Option<GcReq> {
2576 let GcConfig {
2577 use_active_gc,
2578 fallback_threshold_ms,
2579 min_versions,
2580 max_versions,
2581 remove_finalized_rollups,
2582 } = cfg;
2583 let gc_threshold = if use_active_gc {
2587 u64::cast_from(min_versions)
2588 } else {
2589 std::cmp::max(
2590 1,
2591 u64::cast_from(self.seqno.0.next_power_of_two().trailing_zeros()),
2592 )
2593 };
2594 let new_seqno_since = self.seqno_since();
2595 let gc_until_seqno = new_seqno_since.min(SeqNo(
2598 self.collections
2599 .last_gc_req
2600 .0
2601 .saturating_add(u64::cast_from(max_versions)),
2602 ));
2603 let should_gc = new_seqno_since
2604 .0
2605 .saturating_sub(self.collections.last_gc_req.0)
2606 >= gc_threshold;
2607
2608 let should_gc = if use_active_gc && !should_gc {
2611 match self.collections.active_gc {
2612 Some(active_gc) => now.saturating_sub(active_gc.start_ms) > fallback_threshold_ms,
2613 None => false,
2614 }
2615 } else {
2616 should_gc
2617 };
2618 let should_gc = should_gc && (is_write || self.collections.writers.is_empty());
2621 let tombstone_needs_gc = self.collections.is_tombstone();
2626 let should_gc = should_gc || (tombstone_needs_gc && !remove_finalized_rollups);
2627 let should_gc = if use_active_gc {
2628 should_gc
2632 && match self.collections.active_gc {
2633 Some(active) => now.saturating_sub(active.start_ms) > fallback_threshold_ms,
2634 None => true,
2635 }
2636 } else {
2637 should_gc
2638 };
2639 let should_gc = should_gc || (tombstone_needs_gc && remove_finalized_rollups);
2643 if should_gc {
2644 self.collections.last_gc_req = gc_until_seqno;
2645 Some(GcReq {
2646 shard_id: self.shard_id,
2647 new_seqno_since: gc_until_seqno,
2648 })
2649 } else {
2650 None
2651 }
2652 }
2653
2654 pub fn seqnos_held(&self) -> usize {
2656 usize::cast_from(self.seqno.0.saturating_sub(self.seqno_since().0))
2657 }
2658
2659 pub fn expire_at(&mut self, walltime_ms: EpochMillis) -> ExpiryMetrics {
2661 let mut metrics = ExpiryMetrics::default();
2662 let shard_id = self.shard_id();
2663 self.collections.leased_readers.retain(|id, state| {
2664 let retain = state.last_heartbeat_timestamp_ms + state.lease_duration_ms >= walltime_ms;
2665 if !retain {
2666 info!(
2667 "Force expiring reader {id} ({}) of shard {shard_id} due to inactivity",
2668 state.debug.purpose
2669 );
2670 metrics.readers_expired += 1;
2671 }
2672 retain
2673 });
2674 self.collections.writers.retain(|id, state| {
2676 let retain =
2677 (state.last_heartbeat_timestamp_ms + state.lease_duration_ms) >= walltime_ms;
2678 if !retain {
2679 info!(
2680 "Force expiring writer {id} ({}) of shard {shard_id} due to inactivity",
2681 state.debug.purpose
2682 );
2683 metrics.writers_expired += 1;
2684 }
2685 retain
2686 });
2687 metrics
2688 }
2689
2690 pub fn snapshot(&self, as_of: &Antichain<T>) -> Result<Vec<HollowBatch<T>>, SnapshotErr<T>> {
2694 if PartialOrder::less_than(as_of, self.collections.trace.since()) {
2695 return Err(SnapshotErr::AsOfHistoricalDistinctionsLost(Since(
2696 self.collections.trace.since().clone(),
2697 )));
2698 }
2699 let upper = self.collections.trace.upper();
2700 if PartialOrder::less_equal(upper, as_of) {
2701 return Err(SnapshotErr::AsOfNotYetAvailable(
2702 self.seqno,
2703 Upper(upper.clone()),
2704 ));
2705 }
2706
2707 let batches = self
2708 .collections
2709 .trace
2710 .batches()
2711 .filter(|b| !PartialOrder::less_than(as_of, b.desc.lower()))
2712 .cloned()
2713 .collect();
2714 Ok(batches)
2715 }
2716
2717 pub fn verify_listen(&self, as_of: &Antichain<T>) -> Result<(), Since<T>> {
2719 if PartialOrder::less_than(as_of, self.collections.trace.since()) {
2720 return Err(Since(self.collections.trace.since().clone()));
2721 }
2722 Ok(())
2723 }
2724
2725 pub fn next_listen_batch(&self, frontier: &Antichain<T>) -> Result<HollowBatch<T>, SeqNo> {
2726 self.collections
2729 .trace
2730 .batches()
2731 .find(|b| {
2732 PartialOrder::less_equal(b.desc.lower(), frontier)
2733 && PartialOrder::less_than(frontier, b.desc.upper())
2734 })
2735 .cloned()
2736 .ok_or(self.seqno)
2737 }
2738
2739 pub fn active_rollup(&self) -> Option<ActiveRollup> {
2740 self.collections.active_rollup
2741 }
2742
2743 pub fn need_rollup(
2744 &self,
2745 threshold: usize,
2746 use_active_rollup: bool,
2747 fallback_threshold_ms: u64,
2748 now: u64,
2749 ) -> Option<SeqNo> {
2750 let (latest_rollup_seqno, _) = self.latest_rollup();
2751
2752 if self.collections.is_tombstone() && latest_rollup_seqno.next() < self.seqno {
2758 return Some(self.seqno);
2759 }
2760
2761 let seqnos_since_last_rollup = self.seqno.0.saturating_sub(latest_rollup_seqno.0);
2762
2763 if use_active_rollup {
2764 if seqnos_since_last_rollup > u64::cast_from(threshold) {
2770 match self.active_rollup() {
2771 Some(active_rollup) => {
2772 if now.saturating_sub(active_rollup.start_ms) > fallback_threshold_ms {
2773 return Some(self.seqno);
2774 }
2775 }
2776 None => {
2777 return Some(self.seqno);
2778 }
2779 }
2780 }
2781 } else {
2782 if seqnos_since_last_rollup > 0
2786 && seqnos_since_last_rollup % u64::cast_from(threshold) == 0
2787 {
2788 return Some(self.seqno);
2789 }
2790
2791 if seqnos_since_last_rollup
2794 > u64::cast_from(
2795 threshold * PersistConfig::DEFAULT_FALLBACK_ROLLUP_THRESHOLD_MULTIPLIER,
2796 )
2797 {
2798 return Some(self.seqno);
2799 }
2800 }
2801
2802 None
2803 }
2804
2805 pub(crate) fn blobs(&self) -> impl Iterator<Item = HollowBlobRef<'_, T>> {
2806 let batches = self.collections.trace.batches().map(HollowBlobRef::Batch);
2807 let rollups = self.collections.rollups.values().map(HollowBlobRef::Rollup);
2808 batches.chain(rollups)
2809 }
2810}
2811
2812fn serialize_part_bytes<S: Serializer>(val: &[u8], s: S) -> Result<S::Ok, S::Error> {
2813 let val = hex::encode(val);
2814 val.serialize(s)
2815}
2816
2817fn serialize_lazy_proto<S: Serializer, T: prost::Message + Default>(
2818 val: &Option<LazyProto<T>>,
2819 s: S,
2820) -> Result<S::Ok, S::Error> {
2821 val.as_ref()
2822 .map(|lazy| hex::encode(&lazy.into_proto()))
2823 .serialize(s)
2824}
2825
2826fn serialize_part_stats<S: Serializer>(
2827 val: &Option<LazyPartStats>,
2828 s: S,
2829) -> Result<S::Ok, S::Error> {
2830 let stats = val.as_ref().and_then(|x| match x.try_decode() {
2836 Ok(stats) => Some(stats.key),
2837 Err(err) => {
2838 tracing::warn!("undecodable part stats, reporting as absent: {err}");
2839 None
2840 }
2841 });
2842 stats.serialize(s)
2843}
2844
2845fn serialize_diffs_sum<S: Serializer>(val: &Option<[u8; 8]>, s: S) -> Result<S::Ok, S::Error> {
2846 let val = val.map(i64::decode);
2848 val.serialize(s)
2849}
2850
2851impl<T: Serialize + Timestamp + Lattice> Serialize for State<T> {
2857 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
2858 let State {
2859 shard_id,
2860 seqno,
2861 walltime_ms,
2862 hostname,
2863 collections:
2864 StateCollections {
2865 version: applier_version,
2866 last_gc_req,
2867 rollups,
2868 active_rollup,
2869 active_gc,
2870 leased_readers,
2871 critical_readers,
2872 writers,
2873 schemas,
2874 trace,
2875 },
2876 } = self;
2877 let mut s = s.serialize_struct("State", 13)?;
2878 let () = s.serialize_field("applier_version", &applier_version.to_string())?;
2879 let () = s.serialize_field("shard_id", shard_id)?;
2880 let () = s.serialize_field("seqno", seqno)?;
2881 let () = s.serialize_field("walltime_ms", walltime_ms)?;
2882 let () = s.serialize_field("hostname", hostname)?;
2883 let () = s.serialize_field("last_gc_req", last_gc_req)?;
2884 let () = s.serialize_field("rollups", rollups)?;
2885 let () = s.serialize_field("active_rollup", active_rollup)?;
2886 let () = s.serialize_field("active_gc", active_gc)?;
2887 let () = s.serialize_field("leased_readers", leased_readers)?;
2888 let () = s.serialize_field("critical_readers", critical_readers)?;
2889 let () = s.serialize_field("writers", writers)?;
2890 let () = s.serialize_field("schemas", schemas)?;
2891 let () = s.serialize_field("since", &trace.since().elements())?;
2892 let () = s.serialize_field("upper", &trace.upper().elements())?;
2893 let trace = trace.flatten();
2894 let () = s.serialize_field("batches", &trace.legacy_batches.keys().collect::<Vec<_>>())?;
2895 let () = s.serialize_field("hollow_batches", &trace.hollow_batches)?;
2896 let () = s.serialize_field("spine_batches", &trace.spine_batches)?;
2897 let () = s.serialize_field("merges", &trace.merges)?;
2898 s.end()
2899 }
2900}
2901
2902#[derive(Debug, Default)]
2903pub struct StateSizeMetrics {
2904 pub hollow_batch_count: usize,
2905 pub batch_part_count: usize,
2906 pub num_updates: usize,
2907 pub largest_batch_bytes: usize,
2908 pub state_batches_bytes: usize,
2909 pub state_rollups_bytes: usize,
2910 pub state_rollup_count: usize,
2911 pub inline_part_count: usize,
2912}
2913
2914#[derive(Default)]
2915pub struct ExpiryMetrics {
2916 pub(crate) readers_expired: usize,
2917 pub(crate) writers_expired: usize,
2918}
2919
2920#[derive(Debug, Clone, PartialEq)]
2922pub struct Since<T>(pub Antichain<T>);
2923
2924#[derive(Debug, PartialEq)]
2926pub struct Upper<T>(pub Antichain<T>);
2927
2928#[cfg(test)]
2929pub(crate) mod tests {
2930 use std::ops::Range;
2931 use std::str::FromStr;
2932
2933 use bytes::Bytes;
2934 use mz_build_info::DUMMY_BUILD_INFO;
2935 use mz_dyncfg::ConfigUpdates;
2936 use mz_ore::now::SYSTEM_TIME;
2937 use mz_ore::{assert_none, assert_ok};
2938 use mz_proto::RustType;
2939 use proptest::prelude::*;
2940 use proptest::strategy::ValueTree;
2941
2942 use crate::InvalidUsage::{InvalidBounds, InvalidEmptyTimeInterval};
2943 use crate::cache::PersistClientCache;
2944 use crate::internal::encoding::any_some_lazy_part_stats;
2945 use crate::internal::paths::RollupId;
2946 use crate::internal::trace::tests::any_trace;
2947 use crate::tests::new_test_client_cache;
2948 use crate::{Diagnostics, PersistLocation};
2949
2950 use super::*;
2951
2952 const LEASE_DURATION_MS: u64 = 900 * 1000;
2953 fn debug_state() -> HandleDebugState {
2954 HandleDebugState {
2955 hostname: "debug".to_owned(),
2956 purpose: "finding the bugs".to_owned(),
2957 }
2958 }
2959
2960 pub fn any_hollow_batch_with_exact_runs<T: Arbitrary + Timestamp>(
2961 num_runs: usize,
2962 ) -> impl Strategy<Value = HollowBatch<T>> {
2963 (
2964 any::<T>(),
2965 any::<T>(),
2966 any::<T>(),
2967 proptest::collection::vec(any_run_part::<T>(), num_runs + 1..20),
2968 any::<usize>(),
2969 )
2970 .prop_map(move |(t0, t1, since, parts, len)| {
2971 let (lower, upper) = if t0 <= t1 {
2972 (Antichain::from_elem(t0), Antichain::from_elem(t1))
2973 } else {
2974 (Antichain::from_elem(t1), Antichain::from_elem(t0))
2975 };
2976 let since = Antichain::from_elem(since);
2977
2978 let run_splits = (1..num_runs)
2979 .map(|i| i * parts.len() / num_runs)
2980 .collect::<Vec<_>>();
2981
2982 let run_meta = (0..num_runs)
2983 .map(|_| {
2984 let mut meta = RunMeta::default();
2985 meta.id = Some(RunId::new());
2986 meta
2987 })
2988 .collect::<Vec<_>>();
2989
2990 HollowBatch::new(
2991 Description::new(lower, upper, since),
2992 parts,
2993 len % 10,
2994 run_meta,
2995 run_splits,
2996 )
2997 })
2998 }
2999
3000 pub fn any_hollow_batch<T: Arbitrary + Timestamp>() -> impl Strategy<Value = HollowBatch<T>> {
3001 Strategy::prop_map(
3002 (
3003 any::<T>(),
3004 any::<T>(),
3005 any::<T>(),
3006 proptest::collection::vec(any_run_part::<T>(), 0..20),
3007 any::<usize>(),
3008 0..=10usize,
3009 proptest::collection::vec(any::<RunId>(), 10),
3010 ),
3011 |(t0, t1, since, parts, len, num_runs, run_ids)| {
3012 let (lower, upper) = if t0 <= t1 {
3013 (Antichain::from_elem(t0), Antichain::from_elem(t1))
3014 } else {
3015 (Antichain::from_elem(t1), Antichain::from_elem(t0))
3016 };
3017 let since = Antichain::from_elem(since);
3018 if num_runs > 0 && parts.len() > 2 && num_runs < parts.len() {
3019 let run_splits = (1..num_runs)
3020 .map(|i| i * parts.len() / num_runs)
3021 .collect::<Vec<_>>();
3022
3023 let run_meta = (0..num_runs)
3024 .enumerate()
3025 .map(|(i, _)| {
3026 let mut meta = RunMeta::default();
3027 meta.id = Some(run_ids[i]);
3028 meta
3029 })
3030 .collect::<Vec<_>>();
3031
3032 HollowBatch::new(
3033 Description::new(lower, upper, since),
3034 parts,
3035 len % 10,
3036 run_meta,
3037 run_splits,
3038 )
3039 } else {
3040 HollowBatch::new_run_for_test(
3041 Description::new(lower, upper, since),
3042 parts,
3043 len % 10,
3044 run_ids[0],
3045 )
3046 }
3047 },
3048 )
3049 }
3050
3051 pub fn any_batch_part<T: Arbitrary + Timestamp>() -> impl Strategy<Value = BatchPart<T>> {
3052 Strategy::prop_map(
3053 (
3054 any::<bool>(),
3055 any_hollow_batch_part(),
3056 any::<Option<T>>(),
3057 any::<Option<SchemaId>>(),
3058 any::<Option<SchemaId>>(),
3059 ),
3060 |(is_hollow, hollow, ts_rewrite, schema_id, deprecated_schema_id)| {
3061 if is_hollow {
3062 BatchPart::Hollow(hollow)
3063 } else {
3064 let updates = LazyInlineBatchPart::from_proto(Bytes::new()).unwrap();
3065 let ts_rewrite = ts_rewrite.map(Antichain::from_elem);
3066 BatchPart::Inline {
3067 updates,
3068 ts_rewrite,
3069 schema_id,
3070 deprecated_schema_id,
3071 }
3072 }
3073 },
3074 )
3075 }
3076
3077 pub fn any_run_part<T: Arbitrary + Timestamp>() -> impl Strategy<Value = RunPart<T>> {
3078 Strategy::prop_map(any_batch_part(), |part| RunPart::Single(part))
3079 }
3080
3081 pub fn any_hollow_batch_part<T: Arbitrary + Timestamp>()
3082 -> impl Strategy<Value = HollowBatchPart<T>> {
3083 Strategy::prop_map(
3084 (
3085 any::<PartialBatchKey>(),
3086 any::<usize>(),
3087 any::<Vec<u8>>(),
3088 any_some_lazy_part_stats(),
3089 any::<Option<T>>(),
3090 any::<[u8; 8]>(),
3091 any::<Option<BatchColumnarFormat>>(),
3092 any::<Option<SchemaId>>(),
3093 any::<Option<SchemaId>>(),
3094 ),
3095 |(
3096 key,
3097 encoded_size_bytes,
3098 key_lower,
3099 stats,
3100 ts_rewrite,
3101 diffs_sum,
3102 format,
3103 schema_id,
3104 deprecated_schema_id,
3105 )| {
3106 HollowBatchPart {
3107 key,
3108 meta: Default::default(),
3109 encoded_size_bytes,
3110 key_lower,
3111 structured_key_lower: None,
3112 stats,
3113 ts_rewrite: ts_rewrite.map(Antichain::from_elem),
3114 diffs_sum: Some(diffs_sum),
3115 format,
3116 schema_id,
3117 deprecated_schema_id,
3118 }
3119 },
3120 )
3121 }
3122
3123 pub fn any_leased_reader_state<T: Arbitrary>() -> impl Strategy<Value = LeasedReaderState<T>> {
3124 Strategy::prop_map(
3125 (
3126 any::<SeqNo>(),
3127 any::<Option<T>>(),
3128 any::<u64>(),
3129 any::<u64>(),
3130 any::<HandleDebugState>(),
3131 ),
3132 |(seqno, since, last_heartbeat_timestamp_ms, mut lease_duration_ms, debug)| {
3133 if lease_duration_ms == 0 {
3137 lease_duration_ms += 1;
3138 }
3139 LeasedReaderState {
3140 seqno,
3141 since: since.map_or_else(Antichain::new, Antichain::from_elem),
3142 last_heartbeat_timestamp_ms,
3143 lease_duration_ms,
3144 debug,
3145 }
3146 },
3147 )
3148 }
3149
3150 pub fn any_critical_reader_state<T>() -> impl Strategy<Value = CriticalReaderState<T>>
3151 where
3152 T: Arbitrary,
3153 {
3154 Strategy::prop_map(
3155 (
3156 any::<Option<T>>(),
3157 any::<Opaque>(),
3158 any::<HandleDebugState>(),
3159 ),
3160 |(since, opaque, debug)| CriticalReaderState {
3161 since: since.map_or_else(Antichain::new, Antichain::from_elem),
3162 opaque,
3163 debug,
3164 },
3165 )
3166 }
3167
3168 pub fn any_writer_state<T: Arbitrary>() -> impl Strategy<Value = WriterState<T>> {
3169 Strategy::prop_map(
3170 (
3171 any::<u64>(),
3172 any::<u64>(),
3173 any::<IdempotencyToken>(),
3174 any::<Option<T>>(),
3175 any::<HandleDebugState>(),
3176 ),
3177 |(
3178 last_heartbeat_timestamp_ms,
3179 lease_duration_ms,
3180 most_recent_write_token,
3181 most_recent_write_upper,
3182 debug,
3183 )| WriterState {
3184 last_heartbeat_timestamp_ms,
3185 lease_duration_ms,
3186 most_recent_write_token,
3187 most_recent_write_upper: most_recent_write_upper
3188 .map_or_else(Antichain::new, Antichain::from_elem),
3189 debug,
3190 },
3191 )
3192 }
3193
3194 pub fn any_encoded_schemas() -> impl Strategy<Value = EncodedSchemas> {
3195 Strategy::prop_map(
3196 (
3197 any::<Vec<u8>>(),
3198 any::<Vec<u8>>(),
3199 any::<Vec<u8>>(),
3200 any::<Vec<u8>>(),
3201 ),
3202 |(key, key_data_type, val, val_data_type)| EncodedSchemas {
3203 key: Bytes::from(key),
3204 key_data_type: Bytes::from(key_data_type),
3205 val: Bytes::from(val),
3206 val_data_type: Bytes::from(val_data_type),
3207 },
3208 )
3209 }
3210
3211 pub fn any_state<T: Arbitrary + Timestamp + Lattice>(
3212 num_trace_batches: Range<usize>,
3213 ) -> impl Strategy<Value = State<T>> {
3214 let part1 = (
3215 any::<ShardId>(),
3216 any::<SeqNo>(),
3217 any::<u64>(),
3218 any::<String>(),
3219 any::<SeqNo>(),
3220 proptest::collection::btree_map(any::<SeqNo>(), any::<HollowRollup>(), 1..3),
3221 proptest::option::of(any::<ActiveRollup>()),
3222 );
3223
3224 let part2 = (
3225 proptest::option::of(any::<ActiveGc>()),
3226 proptest::collection::btree_map(
3227 any::<LeasedReaderId>(),
3228 any_leased_reader_state::<T>(),
3229 1..3,
3230 ),
3231 proptest::collection::btree_map(
3232 any::<CriticalReaderId>(),
3233 any_critical_reader_state::<T>(),
3234 1..3,
3235 ),
3236 proptest::collection::btree_map(any::<WriterId>(), any_writer_state::<T>(), 0..3),
3237 proptest::collection::btree_map(any::<SchemaId>(), any_encoded_schemas(), 0..3),
3238 any_trace::<T>(num_trace_batches),
3239 );
3240
3241 (part1, part2).prop_map(
3242 |(
3243 (shard_id, seqno, walltime_ms, hostname, last_gc_req, rollups, active_rollup),
3244 (active_gc, leased_readers, critical_readers, writers, schemas, trace),
3245 )| State {
3246 shard_id,
3247 seqno,
3248 walltime_ms,
3249 hostname,
3250 collections: StateCollections {
3251 version: Version::new(1, 2, 3),
3252 last_gc_req,
3253 rollups,
3254 active_rollup,
3255 active_gc,
3256 leased_readers,
3257 critical_readers,
3258 writers,
3259 schemas,
3260 trace,
3261 },
3262 },
3263 )
3264 }
3265
3266 pub(crate) fn hollow<T: Timestamp>(
3267 lower: T,
3268 upper: T,
3269 keys: &[&str],
3270 len: usize,
3271 ) -> HollowBatch<T> {
3272 HollowBatch::new_run(
3273 Description::new(
3274 Antichain::from_elem(lower),
3275 Antichain::from_elem(upper),
3276 Antichain::from_elem(T::minimum()),
3277 ),
3278 keys.iter()
3279 .map(|x| {
3280 RunPart::Single(BatchPart::Hollow(HollowBatchPart {
3281 key: PartialBatchKey((*x).to_owned()),
3282 meta: Default::default(),
3283 encoded_size_bytes: 0,
3284 key_lower: vec![],
3285 structured_key_lower: None,
3286 stats: None,
3287 ts_rewrite: None,
3288 diffs_sum: None,
3289 format: None,
3290 schema_id: None,
3291 deprecated_schema_id: None,
3292 }))
3293 })
3294 .collect(),
3295 len,
3296 )
3297 }
3298
3299 #[mz_ore::test]
3300 fn downgrade_since() {
3301 let mut state = TypedState::<(), (), u64, i64>::new(
3302 DUMMY_BUILD_INFO.semver_version(),
3303 ShardId::new(),
3304 "".to_owned(),
3305 0,
3306 );
3307 let reader = LeasedReaderId::new();
3308 let seqno = SeqNo::minimum();
3309 let now = SYSTEM_TIME.clone();
3310 let _ = state.collections.register_leased_reader(
3311 "",
3312 &reader,
3313 "",
3314 seqno,
3315 Duration::from_secs(10),
3316 now(),
3317 false,
3318 );
3319
3320 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(0));
3322
3323 assert_eq!(
3325 state.collections.downgrade_since(
3326 &reader,
3327 seqno,
3328 seqno,
3329 &Antichain::from_elem(2),
3330 now()
3331 ),
3332 Continue(Since(Antichain::from_elem(2)))
3333 );
3334 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3335 assert_eq!(
3337 state.collections.downgrade_since(
3338 &reader,
3339 seqno,
3340 seqno,
3341 &Antichain::from_elem(2),
3342 now()
3343 ),
3344 Continue(Since(Antichain::from_elem(2)))
3345 );
3346 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3347 assert_eq!(
3349 state.collections.downgrade_since(
3350 &reader,
3351 seqno,
3352 seqno,
3353 &Antichain::from_elem(1),
3354 now()
3355 ),
3356 Continue(Since(Antichain::from_elem(2)))
3357 );
3358 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3359
3360 let reader2 = LeasedReaderId::new();
3362 let _ = state.collections.register_leased_reader(
3363 "",
3364 &reader2,
3365 "",
3366 seqno,
3367 Duration::from_secs(10),
3368 now(),
3369 false,
3370 );
3371
3372 assert_eq!(
3374 state.collections.downgrade_since(
3375 &reader2,
3376 seqno,
3377 seqno,
3378 &Antichain::from_elem(3),
3379 now()
3380 ),
3381 Continue(Since(Antichain::from_elem(3)))
3382 );
3383 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3384 assert_eq!(
3386 state.collections.downgrade_since(
3387 &reader,
3388 seqno,
3389 seqno,
3390 &Antichain::from_elem(5),
3391 now()
3392 ),
3393 Continue(Since(Antichain::from_elem(5)))
3394 );
3395 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3396
3397 assert_eq!(
3399 state.collections.expire_leased_reader(&reader),
3400 Continue(true)
3401 );
3402 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3403
3404 let reader3 = LeasedReaderId::new();
3406 let _ = state.collections.register_leased_reader(
3407 "",
3408 &reader3,
3409 "",
3410 seqno,
3411 Duration::from_secs(10),
3412 now(),
3413 false,
3414 );
3415
3416 assert_eq!(
3418 state.collections.downgrade_since(
3419 &reader3,
3420 seqno,
3421 seqno,
3422 &Antichain::from_elem(10),
3423 now()
3424 ),
3425 Continue(Since(Antichain::from_elem(10)))
3426 );
3427 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3428
3429 assert_eq!(
3431 state.collections.expire_leased_reader(&reader2),
3432 Continue(true)
3433 );
3434 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3439
3440 assert_eq!(
3442 state.collections.expire_leased_reader(&reader3),
3443 Continue(true)
3444 );
3445 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3450 }
3451
3452 #[mz_ore::test]
3453 fn compare_and_downgrade_since() {
3454 let mut state = TypedState::<(), (), u64, i64>::new(
3455 DUMMY_BUILD_INFO.semver_version(),
3456 ShardId::new(),
3457 "".to_owned(),
3458 0,
3459 );
3460 let reader = CriticalReaderId::new();
3461 let _ = state
3462 .collections
3463 .register_critical_reader("", &reader, Opaque::encode(&0u64), "");
3464
3465 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(0));
3467 assert_eq!(
3469 state
3470 .collections
3471 .critical_reader(&reader)
3472 .opaque
3473 .decode::<u64>(),
3474 u64::MIN
3475 );
3476
3477 assert_eq!(
3479 state.collections.compare_and_downgrade_since(
3480 &reader,
3481 &Opaque::encode(&0u64),
3482 (&Opaque::encode(&1u64), &Antichain::from_elem(2)),
3483 ),
3484 Continue(Ok(Since(Antichain::from_elem(2))))
3485 );
3486 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3487 assert_eq!(
3488 state
3489 .collections
3490 .critical_reader(&reader)
3491 .opaque
3492 .decode::<u64>(),
3493 1
3494 );
3495 assert_eq!(
3497 state.collections.compare_and_downgrade_since(
3498 &reader,
3499 &Opaque::encode(&1u64),
3500 (&Opaque::encode(&2u64), &Antichain::from_elem(2)),
3501 ),
3502 Continue(Ok(Since(Antichain::from_elem(2))))
3503 );
3504 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3505 assert_eq!(
3506 state
3507 .collections
3508 .critical_reader(&reader)
3509 .opaque
3510 .decode::<u64>(),
3511 2
3512 );
3513 assert_eq!(
3515 state.collections.compare_and_downgrade_since(
3516 &reader,
3517 &Opaque::encode(&2u64),
3518 (&Opaque::encode(&3u64), &Antichain::from_elem(1)),
3519 ),
3520 Continue(Ok(Since(Antichain::from_elem(2))))
3521 );
3522 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3523 assert_eq!(
3524 state
3525 .collections
3526 .critical_reader(&reader)
3527 .opaque
3528 .decode::<u64>(),
3529 3
3530 );
3531 }
3532
3533 #[mz_ore::test]
3534 fn compare_and_append() {
3535 let state = &mut TypedState::<String, String, u64, i64>::new(
3536 DUMMY_BUILD_INFO.semver_version(),
3537 ShardId::new(),
3538 "".to_owned(),
3539 0,
3540 )
3541 .collections;
3542
3543 let writer_id = WriterId::new();
3544 let now = SYSTEM_TIME.clone();
3545
3546 assert_eq!(state.trace.num_spine_batches(), 0);
3548 assert_eq!(state.trace.num_hollow_batches(), 0);
3549 assert_eq!(state.trace.num_updates(), 0);
3550
3551 assert_eq!(
3553 state.compare_and_append(
3554 &hollow(1, 2, &["key1"], 1),
3555 &writer_id,
3556 now(),
3557 LEASE_DURATION_MS,
3558 &IdempotencyToken::new(),
3559 &debug_state(),
3560 0,
3561 100,
3562 None
3563 ),
3564 Break(CompareAndAppendBreak::Upper {
3565 shard_upper: Antichain::from_elem(0),
3566 writer_upper: Antichain::from_elem(0)
3567 })
3568 );
3569
3570 assert!(
3572 state
3573 .compare_and_append(
3574 &hollow(0, 5, &[], 0),
3575 &writer_id,
3576 now(),
3577 LEASE_DURATION_MS,
3578 &IdempotencyToken::new(),
3579 &debug_state(),
3580 0,
3581 100,
3582 None
3583 )
3584 .is_continue()
3585 );
3586
3587 assert_eq!(
3589 state.compare_and_append(
3590 &hollow(5, 4, &["key1"], 1),
3591 &writer_id,
3592 now(),
3593 LEASE_DURATION_MS,
3594 &IdempotencyToken::new(),
3595 &debug_state(),
3596 0,
3597 100,
3598 None
3599 ),
3600 Break(CompareAndAppendBreak::InvalidUsage(InvalidBounds {
3601 lower: Antichain::from_elem(5),
3602 upper: Antichain::from_elem(4)
3603 }))
3604 );
3605
3606 assert_eq!(
3608 state.compare_and_append(
3609 &hollow(5, 5, &["key1"], 1),
3610 &writer_id,
3611 now(),
3612 LEASE_DURATION_MS,
3613 &IdempotencyToken::new(),
3614 &debug_state(),
3615 0,
3616 100,
3617 None
3618 ),
3619 Break(CompareAndAppendBreak::InvalidUsage(
3620 InvalidEmptyTimeInterval {
3621 lower: Antichain::from_elem(5),
3622 upper: Antichain::from_elem(5),
3623 keys: vec!["key1".to_owned()],
3624 }
3625 ))
3626 );
3627
3628 assert!(
3630 state
3631 .compare_and_append(
3632 &hollow(5, 5, &[], 0),
3633 &writer_id,
3634 now(),
3635 LEASE_DURATION_MS,
3636 &IdempotencyToken::new(),
3637 &debug_state(),
3638 0,
3639 100,
3640 None
3641 )
3642 .is_continue()
3643 );
3644 }
3645
3646 #[mz_ore::test]
3647 fn snapshot() {
3648 let now = SYSTEM_TIME.clone();
3649
3650 let mut state = TypedState::<String, String, u64, i64>::new(
3651 DUMMY_BUILD_INFO.semver_version(),
3652 ShardId::new(),
3653 "".to_owned(),
3654 0,
3655 );
3656 assert_eq!(
3658 state.snapshot(&Antichain::from_elem(0)),
3659 Err(SnapshotErr::AsOfNotYetAvailable(
3660 SeqNo(0),
3661 Upper(Antichain::from_elem(0))
3662 ))
3663 );
3664
3665 assert_eq!(
3667 state.snapshot(&Antichain::from_elem(5)),
3668 Err(SnapshotErr::AsOfNotYetAvailable(
3669 SeqNo(0),
3670 Upper(Antichain::from_elem(0))
3671 ))
3672 );
3673
3674 let writer_id = WriterId::new();
3675
3676 assert!(
3678 state
3679 .collections
3680 .compare_and_append(
3681 &hollow(0, 5, &["key1"], 1),
3682 &writer_id,
3683 now(),
3684 LEASE_DURATION_MS,
3685 &IdempotencyToken::new(),
3686 &debug_state(),
3687 0,
3688 100,
3689 None
3690 )
3691 .is_continue()
3692 );
3693
3694 assert_eq!(
3696 state.snapshot(&Antichain::from_elem(0)),
3697 Ok(vec![hollow(0, 5, &["key1"], 1)])
3698 );
3699
3700 assert_eq!(
3702 state.snapshot(&Antichain::from_elem(4)),
3703 Ok(vec![hollow(0, 5, &["key1"], 1)])
3704 );
3705
3706 assert_eq!(
3708 state.snapshot(&Antichain::from_elem(5)),
3709 Err(SnapshotErr::AsOfNotYetAvailable(
3710 SeqNo(0),
3711 Upper(Antichain::from_elem(5))
3712 ))
3713 );
3714 assert_eq!(
3715 state.snapshot(&Antichain::from_elem(6)),
3716 Err(SnapshotErr::AsOfNotYetAvailable(
3717 SeqNo(0),
3718 Upper(Antichain::from_elem(5))
3719 ))
3720 );
3721
3722 let reader = LeasedReaderId::new();
3723 let _ = state.collections.register_leased_reader(
3725 "",
3726 &reader,
3727 "",
3728 SeqNo::minimum(),
3729 Duration::from_secs(10),
3730 now(),
3731 false,
3732 );
3733 assert_eq!(
3734 state.collections.downgrade_since(
3735 &reader,
3736 SeqNo::minimum(),
3737 SeqNo::minimum(),
3738 &Antichain::from_elem(2),
3739 now()
3740 ),
3741 Continue(Since(Antichain::from_elem(2)))
3742 );
3743 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3744 assert_eq!(
3746 state.snapshot(&Antichain::from_elem(1)),
3747 Err(SnapshotErr::AsOfHistoricalDistinctionsLost(Since(
3748 Antichain::from_elem(2)
3749 )))
3750 );
3751
3752 assert!(
3754 state
3755 .collections
3756 .compare_and_append(
3757 &hollow(5, 10, &[], 0),
3758 &writer_id,
3759 now(),
3760 LEASE_DURATION_MS,
3761 &IdempotencyToken::new(),
3762 &debug_state(),
3763 0,
3764 100,
3765 None
3766 )
3767 .is_continue()
3768 );
3769
3770 assert_eq!(
3772 state.snapshot(&Antichain::from_elem(7)),
3773 Ok(vec![hollow(0, 5, &["key1"], 1), hollow(5, 10, &[], 0)])
3774 );
3775
3776 assert_eq!(
3778 state.snapshot(&Antichain::from_elem(10)),
3779 Err(SnapshotErr::AsOfNotYetAvailable(
3780 SeqNo(0),
3781 Upper(Antichain::from_elem(10))
3782 ))
3783 );
3784
3785 assert!(
3787 state
3788 .collections
3789 .compare_and_append(
3790 &hollow(10, 15, &["key2"], 1),
3791 &writer_id,
3792 now(),
3793 LEASE_DURATION_MS,
3794 &IdempotencyToken::new(),
3795 &debug_state(),
3796 0,
3797 100,
3798 None
3799 )
3800 .is_continue()
3801 );
3802
3803 assert_eq!(
3806 state.snapshot(&Antichain::from_elem(9)),
3807 Ok(vec![hollow(0, 5, &["key1"], 1), hollow(5, 10, &[], 0)])
3808 );
3809
3810 assert_eq!(
3812 state.snapshot(&Antichain::from_elem(10)),
3813 Ok(vec![
3814 hollow(0, 5, &["key1"], 1),
3815 hollow(5, 10, &[], 0),
3816 hollow(10, 15, &["key2"], 1)
3817 ])
3818 );
3819
3820 assert_eq!(
3821 state.snapshot(&Antichain::from_elem(11)),
3822 Ok(vec![
3823 hollow(0, 5, &["key1"], 1),
3824 hollow(5, 10, &[], 0),
3825 hollow(10, 15, &["key2"], 1)
3826 ])
3827 );
3828 }
3829
3830 #[mz_ore::test]
3831 fn next_listen_batch() {
3832 let mut state = TypedState::<String, String, u64, i64>::new(
3833 DUMMY_BUILD_INFO.semver_version(),
3834 ShardId::new(),
3835 "".to_owned(),
3836 0,
3837 );
3838
3839 assert_eq!(
3842 state.next_listen_batch(&Antichain::from_elem(0)),
3843 Err(SeqNo(0))
3844 );
3845 assert_eq!(state.next_listen_batch(&Antichain::new()), Err(SeqNo(0)));
3846
3847 let writer_id = WriterId::new();
3848 let now = SYSTEM_TIME.clone();
3849
3850 assert!(
3852 state
3853 .collections
3854 .compare_and_append(
3855 &hollow(0, 5, &["key1"], 1),
3856 &writer_id,
3857 now(),
3858 LEASE_DURATION_MS,
3859 &IdempotencyToken::new(),
3860 &debug_state(),
3861 0,
3862 100,
3863 None
3864 )
3865 .is_continue()
3866 );
3867 assert!(
3868 state
3869 .collections
3870 .compare_and_append(
3871 &hollow(5, 10, &["key2"], 1),
3872 &writer_id,
3873 now(),
3874 LEASE_DURATION_MS,
3875 &IdempotencyToken::new(),
3876 &debug_state(),
3877 0,
3878 100,
3879 None
3880 )
3881 .is_continue()
3882 );
3883
3884 for t in 0..=4 {
3886 assert_eq!(
3887 state.next_listen_batch(&Antichain::from_elem(t)),
3888 Ok(hollow(0, 5, &["key1"], 1))
3889 );
3890 }
3891
3892 for t in 5..=9 {
3894 assert_eq!(
3895 state.next_listen_batch(&Antichain::from_elem(t)),
3896 Ok(hollow(5, 10, &["key2"], 1))
3897 );
3898 }
3899
3900 assert_eq!(
3902 state.next_listen_batch(&Antichain::from_elem(10)),
3903 Err(SeqNo(0))
3904 );
3905
3906 assert_eq!(state.next_listen_batch(&Antichain::new()), Err(SeqNo(0)));
3909 }
3910
3911 #[mz_ore::test]
3912 fn expire_writer() {
3913 let mut state = TypedState::<String, String, u64, i64>::new(
3914 DUMMY_BUILD_INFO.semver_version(),
3915 ShardId::new(),
3916 "".to_owned(),
3917 0,
3918 );
3919 let now = SYSTEM_TIME.clone();
3920
3921 let writer_id_one = WriterId::new();
3922
3923 let writer_id_two = WriterId::new();
3924
3925 assert!(
3927 state
3928 .collections
3929 .compare_and_append(
3930 &hollow(0, 2, &["key1"], 1),
3931 &writer_id_one,
3932 now(),
3933 LEASE_DURATION_MS,
3934 &IdempotencyToken::new(),
3935 &debug_state(),
3936 0,
3937 100,
3938 None
3939 )
3940 .is_continue()
3941 );
3942
3943 assert!(
3944 state
3945 .collections
3946 .expire_writer(&writer_id_one)
3947 .is_continue()
3948 );
3949
3950 assert!(
3952 state
3953 .collections
3954 .compare_and_append(
3955 &hollow(2, 5, &["key2"], 1),
3956 &writer_id_two,
3957 now(),
3958 LEASE_DURATION_MS,
3959 &IdempotencyToken::new(),
3960 &debug_state(),
3961 0,
3962 100,
3963 None
3964 )
3965 .is_continue()
3966 );
3967 }
3968
3969 #[mz_ore::test]
3970 fn maybe_gc_active_gc() {
3971 const GC_CONFIG: GcConfig = GcConfig {
3972 use_active_gc: true,
3973 fallback_threshold_ms: 5000,
3974 min_versions: 99,
3975 max_versions: 500,
3976 remove_finalized_rollups: true,
3977 };
3978 let now_fn = SYSTEM_TIME.clone();
3979
3980 let mut state = TypedState::<String, String, u64, i64>::new(
3981 DUMMY_BUILD_INFO.semver_version(),
3982 ShardId::new(),
3983 "".to_owned(),
3984 0,
3985 );
3986
3987 let now = now_fn();
3988 assert_eq!(state.maybe_gc(true, now, GC_CONFIG), None);
3990 assert_eq!(state.maybe_gc(false, now, GC_CONFIG), None);
3991
3992 state.seqno = SeqNo(100);
3995 assert_eq!(state.seqno_since(), SeqNo(100));
3996
3997 let writer_id = WriterId::new();
3999 let _ = state.collections.compare_and_append(
4000 &hollow(1, 2, &["key1"], 1),
4001 &writer_id,
4002 now,
4003 LEASE_DURATION_MS,
4004 &IdempotencyToken::new(),
4005 &debug_state(),
4006 0,
4007 100,
4008 None,
4009 );
4010 assert_eq!(state.maybe_gc(false, now, GC_CONFIG), None);
4011
4012 assert_eq!(
4014 state.maybe_gc(true, now, GC_CONFIG),
4015 Some(GcReq {
4016 shard_id: state.shard_id,
4017 new_seqno_since: SeqNo(100)
4018 })
4019 );
4020
4021 state.collections.active_gc = Some(ActiveGc {
4023 seqno: state.seqno,
4024 start_ms: now,
4025 });
4026
4027 state.seqno = SeqNo(200);
4028 assert_eq!(state.seqno_since(), SeqNo(200));
4029
4030 assert_eq!(state.maybe_gc(true, now, GC_CONFIG), None);
4031
4032 state.seqno = SeqNo(300);
4033 assert_eq!(state.seqno_since(), SeqNo(300));
4034 let new_now = now + GC_CONFIG.fallback_threshold_ms + 1;
4036 assert_eq!(
4037 state.maybe_gc(true, new_now, GC_CONFIG),
4038 Some(GcReq {
4039 shard_id: state.shard_id,
4040 new_seqno_since: SeqNo(300)
4041 })
4042 );
4043
4044 state.seqno = SeqNo(301);
4048 assert_eq!(state.seqno_since(), SeqNo(301));
4049 assert_eq!(
4050 state.maybe_gc(true, new_now, GC_CONFIG),
4051 Some(GcReq {
4052 shard_id: state.shard_id,
4053 new_seqno_since: SeqNo(301)
4054 })
4055 );
4056
4057 state.collections.active_gc = None;
4058
4059 state.seqno = SeqNo(400);
4062 assert_eq!(state.seqno_since(), SeqNo(400));
4063
4064 let now = now_fn();
4065
4066 let _ = state.collections.expire_writer(&writer_id);
4068 assert_eq!(
4069 state.maybe_gc(false, now, GC_CONFIG),
4070 Some(GcReq {
4071 shard_id: state.shard_id,
4072 new_seqno_since: SeqNo(400)
4073 })
4074 );
4075
4076 let previous_seqno = state.seqno;
4078 state.seqno = SeqNo(10_000);
4079 assert_eq!(state.seqno_since(), SeqNo(10_000));
4080
4081 let now = now_fn();
4082 assert_eq!(
4083 state.maybe_gc(true, now, GC_CONFIG),
4084 Some(GcReq {
4085 shard_id: state.shard_id,
4086 new_seqno_since: SeqNo(previous_seqno.0 + u64::cast_from(GC_CONFIG.max_versions))
4087 })
4088 );
4089 }
4090
4091 #[mz_ore::test]
4092 fn maybe_gc_classic() {
4093 const GC_CONFIG: GcConfig = GcConfig {
4094 use_active_gc: false,
4095 fallback_threshold_ms: 5000,
4096 min_versions: 16,
4097 max_versions: 128,
4098 remove_finalized_rollups: true,
4099 };
4100 const NOW_MS: u64 = 0;
4101
4102 let mut state = TypedState::<String, String, u64, i64>::new(
4103 DUMMY_BUILD_INFO.semver_version(),
4104 ShardId::new(),
4105 "".to_owned(),
4106 0,
4107 );
4108
4109 assert_eq!(state.maybe_gc(true, NOW_MS, GC_CONFIG), None);
4111 assert_eq!(state.maybe_gc(false, NOW_MS, GC_CONFIG), None);
4112
4113 state.seqno = SeqNo(100);
4116 assert_eq!(state.seqno_since(), SeqNo(100));
4117
4118 let writer_id = WriterId::new();
4120 let now = SYSTEM_TIME.clone();
4121 let _ = state.collections.compare_and_append(
4122 &hollow(1, 2, &["key1"], 1),
4123 &writer_id,
4124 now(),
4125 LEASE_DURATION_MS,
4126 &IdempotencyToken::new(),
4127 &debug_state(),
4128 0,
4129 100,
4130 None,
4131 );
4132 assert_eq!(state.maybe_gc(false, NOW_MS, GC_CONFIG), None);
4133
4134 assert_eq!(
4136 state.maybe_gc(true, NOW_MS, GC_CONFIG),
4137 Some(GcReq {
4138 shard_id: state.shard_id,
4139 new_seqno_since: SeqNo(100)
4140 })
4141 );
4142
4143 state.seqno = SeqNo(200);
4146 assert_eq!(state.seqno_since(), SeqNo(200));
4147
4148 let _ = state.collections.expire_writer(&writer_id);
4150 assert_eq!(
4151 state.maybe_gc(false, NOW_MS, GC_CONFIG),
4152 Some(GcReq {
4153 shard_id: state.shard_id,
4154 new_seqno_since: SeqNo(200)
4155 })
4156 );
4157 }
4158
4159 #[mz_ore::test]
4160 fn need_rollup_active_rollup() {
4161 const ROLLUP_THRESHOLD: usize = 3;
4162 const ROLLUP_USE_ACTIVE_ROLLUP: bool = true;
4163 const ROLLUP_FALLBACK_THRESHOLD_MS: u64 = 5000;
4164 let now = SYSTEM_TIME.clone();
4165
4166 mz_ore::test::init_logging();
4167 let mut state = TypedState::<String, String, u64, i64>::new(
4168 DUMMY_BUILD_INFO.semver_version(),
4169 ShardId::new(),
4170 "".to_owned(),
4171 0,
4172 );
4173
4174 let rollup_seqno = SeqNo(5);
4175 let rollup = HollowRollup {
4176 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4177 encoded_size_bytes: None,
4178 finalized: false,
4179 };
4180
4181 assert!(
4182 state
4183 .collections
4184 .add_rollup((rollup_seqno, &rollup))
4185 .is_continue()
4186 );
4187
4188 state.seqno = SeqNo(5);
4190 assert_none!(state.need_rollup(
4191 ROLLUP_THRESHOLD,
4192 ROLLUP_USE_ACTIVE_ROLLUP,
4193 ROLLUP_FALLBACK_THRESHOLD_MS,
4194 now()
4195 ));
4196
4197 state.seqno = SeqNo(6);
4199 assert_none!(state.need_rollup(
4200 ROLLUP_THRESHOLD,
4201 ROLLUP_USE_ACTIVE_ROLLUP,
4202 ROLLUP_FALLBACK_THRESHOLD_MS,
4203 now()
4204 ));
4205 state.seqno = SeqNo(7);
4206 assert_none!(state.need_rollup(
4207 ROLLUP_THRESHOLD,
4208 ROLLUP_USE_ACTIVE_ROLLUP,
4209 ROLLUP_FALLBACK_THRESHOLD_MS,
4210 now()
4211 ));
4212 state.seqno = SeqNo(8);
4213 assert_none!(state.need_rollup(
4214 ROLLUP_THRESHOLD,
4215 ROLLUP_USE_ACTIVE_ROLLUP,
4216 ROLLUP_FALLBACK_THRESHOLD_MS,
4217 now()
4218 ));
4219
4220 let mut current_time = now();
4221 state.seqno = SeqNo(9);
4223 assert_eq!(
4224 state
4225 .need_rollup(
4226 ROLLUP_THRESHOLD,
4227 ROLLUP_USE_ACTIVE_ROLLUP,
4228 ROLLUP_FALLBACK_THRESHOLD_MS,
4229 current_time
4230 )
4231 .expect("rollup"),
4232 SeqNo(9)
4233 );
4234
4235 state.collections.active_rollup = Some(ActiveRollup {
4236 seqno: SeqNo(9),
4237 start_ms: current_time,
4238 });
4239
4240 assert_none!(state.need_rollup(
4242 ROLLUP_THRESHOLD,
4243 ROLLUP_USE_ACTIVE_ROLLUP,
4244 ROLLUP_FALLBACK_THRESHOLD_MS,
4245 current_time
4246 ));
4247
4248 state.seqno = SeqNo(10);
4249 assert_none!(state.need_rollup(
4252 ROLLUP_THRESHOLD,
4253 ROLLUP_USE_ACTIVE_ROLLUP,
4254 ROLLUP_FALLBACK_THRESHOLD_MS,
4255 current_time
4256 ));
4257
4258 current_time += u64::cast_from(ROLLUP_FALLBACK_THRESHOLD_MS) + 1;
4260 assert_eq!(
4261 state
4262 .need_rollup(
4263 ROLLUP_THRESHOLD,
4264 ROLLUP_USE_ACTIVE_ROLLUP,
4265 ROLLUP_FALLBACK_THRESHOLD_MS,
4266 current_time
4267 )
4268 .expect("rollup"),
4269 SeqNo(10)
4270 );
4271
4272 state.seqno = SeqNo(9);
4273 state.collections.active_rollup = None;
4275 let rollup_seqno = SeqNo(9);
4276 let rollup = HollowRollup {
4277 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4278 encoded_size_bytes: None,
4279 finalized: false,
4280 };
4281 assert!(
4282 state
4283 .collections
4284 .add_rollup((rollup_seqno, &rollup))
4285 .is_continue()
4286 );
4287
4288 state.seqno = SeqNo(11);
4289 assert_none!(state.need_rollup(
4291 ROLLUP_THRESHOLD,
4292 ROLLUP_USE_ACTIVE_ROLLUP,
4293 ROLLUP_FALLBACK_THRESHOLD_MS,
4294 current_time
4295 ));
4296 state.seqno = SeqNo(13);
4298 assert_eq!(
4299 state
4300 .need_rollup(
4301 ROLLUP_THRESHOLD,
4302 ROLLUP_USE_ACTIVE_ROLLUP,
4303 ROLLUP_FALLBACK_THRESHOLD_MS,
4304 current_time
4305 )
4306 .expect("rollup"),
4307 SeqNo(13)
4308 );
4309 }
4310
4311 #[mz_ore::test]
4312 fn need_rollup_classic() {
4313 const ROLLUP_THRESHOLD: usize = 3;
4314 const ROLLUP_USE_ACTIVE_ROLLUP: bool = false;
4315 const ROLLUP_FALLBACK_THRESHOLD_MS: u64 = 0;
4316 const NOW: u64 = 0;
4317
4318 mz_ore::test::init_logging();
4319 let mut state = TypedState::<String, String, u64, i64>::new(
4320 DUMMY_BUILD_INFO.semver_version(),
4321 ShardId::new(),
4322 "".to_owned(),
4323 0,
4324 );
4325
4326 let rollup_seqno = SeqNo(5);
4327 let rollup = HollowRollup {
4328 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4329 encoded_size_bytes: None,
4330 finalized: false,
4331 };
4332
4333 assert!(
4334 state
4335 .collections
4336 .add_rollup((rollup_seqno, &rollup))
4337 .is_continue()
4338 );
4339
4340 state.seqno = SeqNo(5);
4342 assert_none!(state.need_rollup(
4343 ROLLUP_THRESHOLD,
4344 ROLLUP_USE_ACTIVE_ROLLUP,
4345 ROLLUP_FALLBACK_THRESHOLD_MS,
4346 NOW
4347 ));
4348
4349 state.seqno = SeqNo(6);
4351 assert_none!(state.need_rollup(
4352 ROLLUP_THRESHOLD,
4353 ROLLUP_USE_ACTIVE_ROLLUP,
4354 ROLLUP_FALLBACK_THRESHOLD_MS,
4355 NOW
4356 ));
4357 state.seqno = SeqNo(7);
4358 assert_none!(state.need_rollup(
4359 ROLLUP_THRESHOLD,
4360 ROLLUP_USE_ACTIVE_ROLLUP,
4361 ROLLUP_FALLBACK_THRESHOLD_MS,
4362 NOW
4363 ));
4364
4365 state.seqno = SeqNo(8);
4367 assert_eq!(
4368 state
4369 .need_rollup(
4370 ROLLUP_THRESHOLD,
4371 ROLLUP_USE_ACTIVE_ROLLUP,
4372 ROLLUP_FALLBACK_THRESHOLD_MS,
4373 NOW
4374 )
4375 .expect("rollup"),
4376 SeqNo(8)
4377 );
4378
4379 state.seqno = SeqNo(9);
4381 assert_none!(state.need_rollup(
4382 ROLLUP_THRESHOLD,
4383 ROLLUP_USE_ACTIVE_ROLLUP,
4384 ROLLUP_FALLBACK_THRESHOLD_MS,
4385 NOW
4386 ));
4387
4388 state.seqno = SeqNo(11);
4390 assert_eq!(
4391 state
4392 .need_rollup(
4393 ROLLUP_THRESHOLD,
4394 ROLLUP_USE_ACTIVE_ROLLUP,
4395 ROLLUP_FALLBACK_THRESHOLD_MS,
4396 NOW
4397 )
4398 .expect("rollup"),
4399 SeqNo(11)
4400 );
4401
4402 let rollup_seqno = SeqNo(6);
4404 let rollup = HollowRollup {
4405 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4406 encoded_size_bytes: None,
4407 finalized: false,
4408 };
4409 assert!(
4410 state
4411 .collections
4412 .add_rollup((rollup_seqno, &rollup))
4413 .is_continue()
4414 );
4415
4416 state.seqno = SeqNo(8);
4417 assert_none!(state.need_rollup(
4418 ROLLUP_THRESHOLD,
4419 ROLLUP_USE_ACTIVE_ROLLUP,
4420 ROLLUP_FALLBACK_THRESHOLD_MS,
4421 NOW
4422 ));
4423 state.seqno = SeqNo(9);
4424 assert_eq!(
4425 state
4426 .need_rollup(
4427 ROLLUP_THRESHOLD,
4428 ROLLUP_USE_ACTIVE_ROLLUP,
4429 ROLLUP_FALLBACK_THRESHOLD_MS,
4430 NOW
4431 )
4432 .expect("rollup"),
4433 SeqNo(9)
4434 );
4435
4436 let fallback_seqno = SeqNo(
4438 rollup_seqno.0
4439 * u64::cast_from(PersistConfig::DEFAULT_FALLBACK_ROLLUP_THRESHOLD_MULTIPLIER),
4440 );
4441 state.seqno = fallback_seqno;
4442 assert_eq!(
4443 state
4444 .need_rollup(
4445 ROLLUP_THRESHOLD,
4446 ROLLUP_USE_ACTIVE_ROLLUP,
4447 ROLLUP_FALLBACK_THRESHOLD_MS,
4448 NOW
4449 )
4450 .expect("rollup"),
4451 fallback_seqno
4452 );
4453 state.seqno = fallback_seqno.next();
4454 assert_eq!(
4455 state
4456 .need_rollup(
4457 ROLLUP_THRESHOLD,
4458 ROLLUP_USE_ACTIVE_ROLLUP,
4459 ROLLUP_FALLBACK_THRESHOLD_MS,
4460 NOW
4461 )
4462 .expect("rollup"),
4463 fallback_seqno.next()
4464 );
4465 }
4466
4467 #[mz_ore::test]
4468 fn idempotency_token_sentinel() {
4469 assert_eq!(
4470 IdempotencyToken::SENTINEL.to_string(),
4471 "i11111111-1111-1111-1111-111111111111"
4472 );
4473 }
4474
4475 #[mz_ore::test]
4484 #[cfg_attr(miri, ignore)] fn state_inspect_serde_json() {
4486 const STATE_SERDE_JSON: &str = include_str!("state_serde.json");
4487 let mut runner = proptest::test_runner::TestRunner::deterministic();
4488 let tree = any_state::<u64>(6..8).new_tree(&mut runner).unwrap();
4489 let json = serde_json::to_string_pretty(&tree.current()).unwrap();
4490 assert_eq!(
4491 json.trim(),
4492 STATE_SERDE_JSON.trim(),
4493 "\n\nNEW GOLDEN\n{}\n",
4494 json
4495 );
4496 }
4497
4498 #[mz_persist_proc::test(tokio::test)]
4499 #[cfg_attr(miri, ignore)] async fn sneaky_downgrades(dyncfgs: ConfigUpdates) {
4501 let mut clients = new_test_client_cache(&dyncfgs);
4502 let shard_id = ShardId::new();
4503
4504 async fn open_and_write(
4505 clients: &mut PersistClientCache,
4506 version: semver::Version,
4507 shard_id: ShardId,
4508 ) -> Result<(), tokio::task::JoinError> {
4509 clients.cfg.build_version = version.clone();
4510 clients.clear_state_cache();
4511 let client = clients.open(PersistLocation::new_in_mem()).await.unwrap();
4512 mz_ore::task::spawn(|| version.to_string(), async move {
4514 let () = client
4515 .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
4516 .await
4517 .expect("valid usage");
4518 let (mut write, _) = client.expect_open::<String, (), u64, i64>(shard_id).await;
4519 let current = *write.upper().as_option().unwrap();
4520 write
4522 .expect_compare_and_append_batch(&mut [], current, current + 1)
4523 .await;
4524 })
4525 .into_tokio_handle()
4526 .await
4527 }
4528
4529 let res = open_and_write(&mut clients, Version::new(0, 10, 0), shard_id).await;
4531 assert_ok!(res);
4532
4533 let res = open_and_write(&mut clients, Version::new(0, 11, 0), shard_id).await;
4535 assert_ok!(res);
4536
4537 let res = open_and_write(&mut clients, Version::new(0, 10, 0), shard_id).await;
4539 assert!(res.unwrap_err().is_panic());
4540
4541 let res = open_and_write(&mut clients, Version::new(0, 9, 0), shard_id).await;
4543 assert!(res.unwrap_err().is_panic());
4544 }
4545
4546 #[mz_ore::test]
4547 fn runid_roundtrip() {
4548 proptest!(|(runid: RunId)| {
4549 let runid_str = runid.to_string();
4550 let parsed = RunId::from_str(&runid_str);
4551 prop_assert_eq!(parsed, Ok(runid));
4552 });
4553 }
4554
4555 #[mz_ore::test]
4571 fn add_rollup_idempotent_across_gc_removal() {
4572 let mut state = TypedState::<String, String, u64, i64>::new(
4573 DUMMY_BUILD_INFO.semver_version(),
4574 ShardId::new(),
4575 "".to_owned(),
4576 0,
4577 );
4578
4579 let older_seqno = SeqNo(10);
4580 let older = HollowRollup {
4581 key: PartialRollupKey::new(older_seqno, &RollupId::new()),
4582 encoded_size_bytes: None,
4583 finalized: false,
4584 };
4585 let newer_seqno = SeqNo(20);
4586 let newer = HollowRollup {
4587 key: PartialRollupKey::new(newer_seqno, &RollupId::new()),
4588 encoded_size_bytes: None,
4589 finalized: false,
4590 };
4591 let add_older = |state: &mut StateCollections<u64>| state.add_rollup((older_seqno, &older));
4592
4593 assert_eq!(add_older(&mut state.collections), Continue(true));
4595 assert_eq!(add_older(&mut state.collections), Continue(true));
4598 assert_eq!(state.collections.rollups.len(), 1);
4599
4600 assert_eq!(
4604 state.collections.add_rollup((newer_seqno, &newer)),
4605 Continue(true),
4606 );
4607
4608 let _ = state
4612 .collections
4613 .remove_rollups(&[(older_seqno, older.key.clone())], false);
4614 assert!(!state.collections.rollups.contains_key(&older_seqno));
4615 assert!(state.collections.rollups.contains_key(&newer_seqno));
4616
4617 assert_eq!(add_older(&mut state.collections), Continue(false));
4622 assert!(!state.collections.rollups.contains_key(&older_seqno));
4623 assert_eq!(state.collections.rollups.len(), 1);
4624 }
4625
4626 #[mz_ore::test]
4627 fn finalized_rollups() {
4628 let mut state = TypedState::<String, String, u64, i64>::new(
4629 DUMMY_BUILD_INFO.semver_version(),
4630 ShardId::new(),
4631 "".to_owned(),
4632 0,
4633 );
4634 state.collections.trace.downgrade_since(&Antichain::new());
4635 let _ = state
4636 .collections
4637 .trace
4638 .push_batch(StateCollections::tombstone_batch());
4639 assert!(state.collections.is_finalized());
4640
4641 let rollup = |seqno, finalized| HollowRollup {
4642 key: PartialRollupKey::new(SeqNo(seqno), &RollupId::new()),
4643 encoded_size_bytes: None,
4644 finalized,
4645 };
4646 let (old, kept, latest) = (rollup(1, false), rollup(3, true), rollup(5, true));
4647 for (seqno, rollup) in [(1, &old), (3, &kept), (5, &latest)] {
4648 assert_eq!(
4649 state.collections.add_rollup((SeqNo(seqno), rollup)),
4650 Continue(true)
4651 );
4652 }
4653 let remove_old = [(SeqNo(1), old.key.clone()), (SeqNo(3), kept.key.clone())];
4654
4655 for (seqno, need) in [(6, None), (7, Some(SeqNo(7)))] {
4658 state.seqno = SeqNo(seqno);
4659 assert_eq!(state.need_rollup(3, false, 0, 0), need);
4660 }
4661
4662 let noop = Break(NoOpStateTransition(vec![]));
4663 assert_eq!(
4664 state.collections.remove_rollups(&remove_old, false),
4665 noop,
4666 "kill switch off"
4667 );
4668
4669 state.collections.active_gc = Some(ActiveGc {
4670 seqno: SeqNo(6),
4671 start_ms: 0,
4672 });
4673 assert_eq!(state.collections.remove_rollups(&[], true), noop);
4674 assert!(
4675 state.collections.active_gc.is_some(),
4676 "a no-op must not commit to clear active_gc"
4677 );
4678
4679 assert_eq!(
4680 state.collections.remove_rollups(&remove_old, true),
4681 Continue(vec![SeqNo(1)])
4682 );
4683 assert_eq!(state.collections.active_gc, None);
4684 assert_eq!(
4685 state.collections.rollups.keys().collect::<Vec<_>>(),
4686 vec![&SeqNo(3), &SeqNo(5)]
4687 );
4688 assert_eq!(
4689 state.collections.remove_rollups(&remove_old, true),
4690 noop,
4691 "replayed removal"
4692 );
4693 }
4694}