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, 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 false,
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
828#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
830pub struct HollowBatchPart<T> {
831 pub key: PartialBatchKey,
833 #[serde(skip_serializing_if = "MetadataMap::is_empty")]
835 pub meta: MetadataMap,
836 pub encoded_size_bytes: usize,
838 #[serde(serialize_with = "serialize_part_bytes")]
841 pub key_lower: Vec<u8>,
842 #[serde(serialize_with = "serialize_lazy_proto")]
844 pub structured_key_lower: Option<LazyProto<ProtoArrayData>>,
845 #[serde(serialize_with = "serialize_part_stats")]
847 pub stats: Option<LazyPartStats>,
848 pub ts_rewrite: Option<Antichain<T>>,
856 #[serde(serialize_with = "serialize_diffs_sum")]
864 pub diffs_sum: Option<[u8; 8]>,
865 pub format: Option<BatchColumnarFormat>,
870 pub schema_id: Option<SchemaId>,
875
876 pub deprecated_schema_id: Option<SchemaId>,
878}
879
880#[derive(Clone, PartialEq, Eq)]
884pub struct HollowBatch<T> {
885 pub desc: Description<T>,
887 pub len: usize,
889 pub(crate) parts: Vec<RunPart<T>>,
891 pub(crate) run_splits: Vec<usize>,
899 pub(crate) run_meta: Vec<RunMeta>,
902}
903
904impl<T: Debug> Debug for HollowBatch<T> {
905 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
906 let HollowBatch {
907 desc,
908 parts,
909 len,
910 run_splits: runs,
911 run_meta,
912 } = self;
913 f.debug_struct("HollowBatch")
914 .field(
915 "desc",
916 &(
917 desc.lower().elements(),
918 desc.upper().elements(),
919 desc.since().elements(),
920 ),
921 )
922 .field("parts", &parts)
923 .field("len", &len)
924 .field("runs", &runs)
925 .field("run_meta", &run_meta)
926 .finish()
927 }
928}
929
930impl<T: Serialize> serde::Serialize for HollowBatch<T> {
931 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
932 let HollowBatch {
933 desc,
934 len,
935 parts: _,
937 run_splits: _,
938 run_meta: _,
939 } = self;
940 let mut s = s.serialize_struct("HollowBatch", 5)?;
941 let () = s.serialize_field("lower", &desc.lower().elements())?;
942 let () = s.serialize_field("upper", &desc.upper().elements())?;
943 let () = s.serialize_field("since", &desc.since().elements())?;
944 let () = s.serialize_field("len", len)?;
945 let () = s.serialize_field("part_runs", &self.runs().collect::<Vec<_>>())?;
946 s.end()
947 }
948}
949
950impl<T: Ord> PartialOrd for HollowBatch<T> {
951 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
952 Some(self.cmp(other))
953 }
954}
955
956impl<T: Ord> Ord for HollowBatch<T> {
957 fn cmp(&self, other: &Self) -> Ordering {
958 let HollowBatch {
961 desc: self_desc,
962 parts: self_parts,
963 len: self_len,
964 run_splits: self_runs,
965 run_meta: self_run_meta,
966 } = self;
967 let HollowBatch {
968 desc: other_desc,
969 parts: other_parts,
970 len: other_len,
971 run_splits: other_runs,
972 run_meta: other_run_meta,
973 } = other;
974 (
975 self_desc.lower().elements(),
976 self_desc.upper().elements(),
977 self_desc.since().elements(),
978 self_parts,
979 self_len,
980 self_runs,
981 self_run_meta,
982 )
983 .cmp(&(
984 other_desc.lower().elements(),
985 other_desc.upper().elements(),
986 other_desc.since().elements(),
987 other_parts,
988 other_len,
989 other_runs,
990 other_run_meta,
991 ))
992 }
993}
994
995impl<T: Timestamp + Codec64 + Sync> HollowBatch<T> {
996 pub(crate) fn part_stream<'a>(
997 &'a self,
998 shard_id: ShardId,
999 blob: &'a dyn Blob,
1000 metrics: &'a Metrics,
1001 ) -> impl Stream<Item = Result<Cow<'a, BatchPart<T>>, MissingBlob>> + 'a {
1002 stream! {
1003 for part in &self.parts {
1004 for await part in part.part_stream(shard_id, blob, metrics) {
1005 yield part;
1006 }
1007 }
1008 }
1009 }
1010}
1011impl<T> HollowBatch<T> {
1012 pub(crate) fn new(
1019 desc: Description<T>,
1020 parts: Vec<RunPart<T>>,
1021 len: usize,
1022 run_meta: Vec<RunMeta>,
1023 run_splits: Vec<usize>,
1024 ) -> Self {
1025 debug_assert!(
1026 run_splits.is_strictly_sorted(),
1027 "run indices should be strictly increasing"
1028 );
1029 mz_ore::soft_assert_no_log!(
1030 run_splits.first().map_or(true, |i| *i > 0),
1031 "run indices should be positive"
1032 );
1033 mz_ore::soft_assert_no_log!(
1034 run_splits.last().map_or(true, |i| *i < parts.len()),
1035 "run indices should be valid indices into parts"
1036 );
1037 mz_ore::soft_assert_no_log!(
1038 parts.is_empty() || run_meta.len() == run_splits.len() + 1,
1039 "all metadata should correspond to a run"
1040 );
1041
1042 Self {
1043 desc,
1044 len,
1045 parts,
1046 run_splits,
1047 run_meta,
1048 }
1049 }
1050
1051 pub(crate) fn new_run(desc: Description<T>, parts: Vec<RunPart<T>>, len: usize) -> Self {
1053 let run_meta = if parts.is_empty() {
1054 vec![]
1055 } else {
1056 vec![RunMeta::default()]
1057 };
1058 Self {
1059 desc,
1060 len,
1061 parts,
1062 run_splits: vec![],
1063 run_meta,
1064 }
1065 }
1066
1067 #[cfg(test)]
1068 pub(crate) fn new_run_for_test(
1069 desc: Description<T>,
1070 parts: Vec<RunPart<T>>,
1071 len: usize,
1072 run_id: RunId,
1073 ) -> Self {
1074 let run_meta = if parts.is_empty() {
1075 vec![]
1076 } else {
1077 let mut meta = RunMeta::default();
1078 meta.id = Some(run_id);
1079 vec![meta]
1080 };
1081 Self {
1082 desc,
1083 len,
1084 parts,
1085 run_splits: vec![],
1086 run_meta,
1087 }
1088 }
1089
1090 pub(crate) fn empty(desc: Description<T>) -> Self {
1092 Self {
1093 desc,
1094 len: 0,
1095 parts: vec![],
1096 run_splits: vec![],
1097 run_meta: vec![],
1098 }
1099 }
1100
1101 pub(crate) fn runs(&self) -> impl Iterator<Item = (&RunMeta, &[RunPart<T>])> {
1102 let run_ends = self
1103 .run_splits
1104 .iter()
1105 .copied()
1106 .chain(std::iter::once(self.parts.len()));
1107 let run_metas = self.run_meta.iter();
1108 let run_parts = run_ends
1109 .scan(0, |start, end| {
1110 let range = *start..end;
1111 *start = end;
1112 Some(range)
1113 })
1114 .filter(|range| !range.is_empty())
1115 .map(|range| &self.parts[range]);
1116 run_metas.zip_eq(run_parts)
1117 }
1118
1119 pub(crate) fn inline_bytes(&self) -> usize {
1120 self.parts.iter().map(|x| x.inline_bytes()).sum()
1121 }
1122
1123 pub(crate) fn is_empty(&self) -> bool {
1124 self.parts.is_empty()
1125 }
1126
1127 pub(crate) fn part_count(&self) -> usize {
1128 self.parts.len()
1129 }
1130
1131 pub fn encoded_size_bytes(&self) -> usize {
1133 self.parts.iter().map(|p| p.encoded_size_bytes()).sum()
1134 }
1135}
1136
1137impl<T: Timestamp + TotalOrder> HollowBatch<T> {
1139 pub(crate) fn rewrite_ts(
1140 &mut self,
1141 frontier: &Antichain<T>,
1142 new_upper: Antichain<T>,
1143 ) -> Result<(), String> {
1144 if !PartialOrder::less_than(frontier, &new_upper) {
1145 return Err(format!(
1146 "rewrite frontier {:?} !< rewrite upper {:?}",
1147 frontier.elements(),
1148 new_upper.elements(),
1149 ));
1150 }
1151 if PartialOrder::less_than(&new_upper, self.desc.upper()) {
1152 return Err(format!(
1153 "rewrite upper {:?} < batch upper {:?}",
1154 new_upper.elements(),
1155 self.desc.upper().elements(),
1156 ));
1157 }
1158
1159 if PartialOrder::less_than(frontier, self.desc.lower()) {
1162 return Err(format!(
1163 "rewrite frontier {:?} < batch lower {:?}",
1164 frontier.elements(),
1165 self.desc.lower().elements(),
1166 ));
1167 }
1168 if self.desc.since() != &Antichain::from_elem(T::minimum()) {
1169 return Err(format!(
1170 "batch since {:?} != minimum antichain {:?}",
1171 self.desc.since().elements(),
1172 [T::minimum()],
1173 ));
1174 }
1175 for part in self.parts.iter() {
1176 let Some(ts_rewrite) = part.ts_rewrite() else {
1177 continue;
1178 };
1179 if PartialOrder::less_than(frontier, ts_rewrite) {
1180 return Err(format!(
1181 "rewrite frontier {:?} < batch rewrite {:?}",
1182 frontier.elements(),
1183 ts_rewrite.elements(),
1184 ));
1185 }
1186 }
1187
1188 self.desc = Description::new(
1189 self.desc.lower().clone(),
1190 new_upper,
1191 self.desc.since().clone(),
1192 );
1193 for part in &mut self.parts {
1194 match part {
1195 RunPart::Single(BatchPart::Hollow(part)) => {
1196 part.ts_rewrite = Some(frontier.clone())
1197 }
1198 RunPart::Single(BatchPart::Inline { ts_rewrite, .. }) => {
1199 *ts_rewrite = Some(frontier.clone())
1200 }
1201 RunPart::Many(runs) => {
1202 panic!("unexpected rewrite of a hollow runs ref: {runs:?}");
1205 }
1206 }
1207 }
1208 Ok(())
1209 }
1210}
1211
1212impl<T: Ord> PartialOrd for HollowBatchPart<T> {
1213 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1214 Some(self.cmp(other))
1215 }
1216}
1217
1218impl<T: Ord> Ord for HollowBatchPart<T> {
1219 fn cmp(&self, other: &Self) -> Ordering {
1220 let HollowBatchPart {
1223 key: self_key,
1224 meta: self_meta,
1225 encoded_size_bytes: self_encoded_size_bytes,
1226 key_lower: self_key_lower,
1227 structured_key_lower: self_structured_key_lower,
1228 stats: self_stats,
1229 ts_rewrite: self_ts_rewrite,
1230 diffs_sum: self_diffs_sum,
1231 format: self_format,
1232 schema_id: self_schema_id,
1233 deprecated_schema_id: self_deprecated_schema_id,
1234 } = self;
1235 let HollowBatchPart {
1236 key: other_key,
1237 meta: other_meta,
1238 encoded_size_bytes: other_encoded_size_bytes,
1239 key_lower: other_key_lower,
1240 structured_key_lower: other_structured_key_lower,
1241 stats: other_stats,
1242 ts_rewrite: other_ts_rewrite,
1243 diffs_sum: other_diffs_sum,
1244 format: other_format,
1245 schema_id: other_schema_id,
1246 deprecated_schema_id: other_deprecated_schema_id,
1247 } = other;
1248 (
1249 self_key,
1250 self_meta,
1251 self_encoded_size_bytes,
1252 self_key_lower,
1253 self_structured_key_lower,
1254 self_stats,
1255 self_ts_rewrite.as_ref().map(|x| x.elements()),
1256 self_diffs_sum,
1257 self_format,
1258 self_schema_id,
1259 self_deprecated_schema_id,
1260 )
1261 .cmp(&(
1262 other_key,
1263 other_meta,
1264 other_encoded_size_bytes,
1265 other_key_lower,
1266 other_structured_key_lower,
1267 other_stats,
1268 other_ts_rewrite.as_ref().map(|x| x.elements()),
1269 other_diffs_sum,
1270 other_format,
1271 other_schema_id,
1272 other_deprecated_schema_id,
1273 ))
1274 }
1275}
1276
1277#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1279pub struct HollowRollup {
1280 pub key: PartialRollupKey,
1282 pub encoded_size_bytes: Option<usize>,
1284}
1285
1286#[derive(Debug)]
1288pub enum HollowBlobRef<'a, T> {
1289 Batch(&'a HollowBatch<T>),
1290 Rollup(&'a HollowRollup),
1291}
1292
1293#[derive(
1295 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Arbitrary, Serialize
1296)]
1297pub struct ActiveRollup {
1298 pub seqno: SeqNo,
1299 pub start_ms: u64,
1300}
1301
1302#[derive(
1304 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Arbitrary, Serialize
1305)]
1306pub struct ActiveGc {
1307 pub seqno: SeqNo,
1308 pub start_ms: u64,
1309}
1310
1311#[derive(Debug)]
1316#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
1317pub struct NoOpStateTransition<T>(pub T);
1318
1319#[derive(Debug, Clone)]
1321#[cfg_attr(any(test, debug_assertions), derive(PartialEq))]
1322pub struct StateCollections<T> {
1323 pub(crate) version: Version,
1327
1328 pub(crate) last_gc_req: SeqNo,
1331
1332 pub(crate) rollups: BTreeMap<SeqNo, HollowRollup>,
1334
1335 pub(crate) active_rollup: Option<ActiveRollup>,
1337 pub(crate) active_gc: Option<ActiveGc>,
1339
1340 pub(crate) leased_readers: BTreeMap<LeasedReaderId, LeasedReaderState<T>>,
1341 pub(crate) critical_readers: BTreeMap<CriticalReaderId, CriticalReaderState<T>>,
1342 pub(crate) writers: BTreeMap<WriterId, WriterState<T>>,
1343 pub(crate) schemas: BTreeMap<SchemaId, EncodedSchemas>,
1344
1345 pub(crate) trace: Trace<T>,
1350}
1351
1352#[derive(Debug, Clone, Serialize, PartialEq)]
1368pub struct EncodedSchemas {
1369 pub key: Bytes,
1371 pub key_data_type: Bytes,
1374 pub val: Bytes,
1376 pub val_data_type: Bytes,
1379}
1380
1381impl EncodedSchemas {
1382 pub(crate) fn decode_data_type(buf: &[u8]) -> DataType {
1383 let proto = prost::Message::decode(buf).expect("valid ProtoDataType");
1384 DataType::from_proto(proto).expect("valid DataType")
1385 }
1386}
1387
1388#[derive(Debug)]
1389#[cfg_attr(test, derive(PartialEq))]
1390pub enum CompareAndAppendBreak<T> {
1391 AlreadyCommitted,
1392 Upper {
1393 shard_upper: Antichain<T>,
1394 writer_upper: Antichain<T>,
1395 },
1396 InvalidUsage(InvalidUsage<T>),
1397 InlineBackpressure,
1398}
1399
1400#[derive(Debug)]
1401#[cfg_attr(test, derive(PartialEq))]
1402pub enum SnapshotErr<T> {
1403 AsOfNotYetAvailable(SeqNo, Upper<T>),
1404 AsOfHistoricalDistinctionsLost(Since<T>),
1405}
1406
1407impl<T> StateCollections<T>
1408where
1409 T: Timestamp + Lattice + Codec64,
1410{
1411 pub fn add_rollup(
1412 &mut self,
1413 add_rollup: (SeqNo, &HollowRollup),
1414 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
1415 let (rollup_seqno, rollup) = add_rollup;
1416 let applied = match self.rollups.get(&rollup_seqno) {
1417 Some(x) => x.key == rollup.key,
1418 None => {
1419 if let Some(min_kept) = self.rollups.keys().next() {
1440 if rollup_seqno < *min_kept {
1441 return Continue(false);
1442 }
1443 }
1444 self.active_rollup = None;
1445 self.rollups.insert(rollup_seqno, rollup.to_owned());
1446 true
1447 }
1448 };
1449 Continue(applied)
1453 }
1454
1455 pub fn remove_rollups(
1456 &mut self,
1457 remove_rollups: &[(SeqNo, PartialRollupKey)],
1458 ) -> ControlFlow<NoOpStateTransition<Vec<SeqNo>>, Vec<SeqNo>> {
1459 if self.is_tombstone() {
1460 return Break(NoOpStateTransition(vec![]));
1461 }
1462
1463 let active_gc_was_set = self.active_gc.take().is_some();
1466
1467 if remove_rollups.is_empty() {
1468 return if active_gc_was_set {
1469 Continue(vec![])
1470 } else {
1471 Break(NoOpStateTransition(vec![]))
1472 };
1473 }
1474
1475 let mut removed = vec![];
1476 for (seqno, key) in remove_rollups {
1477 let removed_key = self.rollups.remove(seqno);
1478 mz_ore::soft_assert_no_log!(
1479 removed_key.as_ref().map_or(true, |x| &x.key == key),
1480 "rollup at {} to be removed has key {:?} in state, but GC asked to remove {}",
1481 seqno,
1482 removed_key,
1483 key
1484 );
1485
1486 if removed_key.is_some() {
1487 removed.push(*seqno);
1488 }
1489 }
1490
1491 Continue(removed)
1492 }
1493
1494 pub fn register_leased_reader(
1495 &mut self,
1496 hostname: &str,
1497 reader_id: &LeasedReaderId,
1498 purpose: &str,
1499 seqno: SeqNo,
1500 lease_duration: Duration,
1501 heartbeat_timestamp_ms: u64,
1502 use_critical_since: bool,
1503 ) -> ControlFlow<
1504 NoOpStateTransition<(LeasedReaderState<T>, SeqNo)>,
1505 (LeasedReaderState<T>, SeqNo),
1506 > {
1507 let since = if use_critical_since {
1508 self.critical_since()
1509 .unwrap_or_else(|| self.trace.since().clone())
1510 } else {
1511 self.trace.since().clone()
1512 };
1513 let reader_state = LeasedReaderState {
1514 debug: HandleDebugState {
1515 hostname: hostname.to_owned(),
1516 purpose: purpose.to_owned(),
1517 },
1518 seqno,
1519 since,
1520 last_heartbeat_timestamp_ms: heartbeat_timestamp_ms,
1521 lease_duration_ms: u64::try_from(lease_duration.as_millis())
1522 .expect("lease duration as millis should fit within u64"),
1523 };
1524
1525 if self.is_tombstone() {
1530 return Break(NoOpStateTransition((reader_state, self.seqno_since(seqno))));
1531 }
1532
1533 self.leased_readers
1535 .insert(reader_id.clone(), reader_state.clone());
1536 Continue((reader_state, self.seqno_since(seqno)))
1537 }
1538
1539 pub fn register_critical_reader(
1540 &mut self,
1541 hostname: &str,
1542 reader_id: &CriticalReaderId,
1543 opaque: Opaque,
1544 purpose: &str,
1545 ) -> ControlFlow<NoOpStateTransition<CriticalReaderState<T>>, CriticalReaderState<T>> {
1546 let state = CriticalReaderState {
1547 debug: HandleDebugState {
1548 hostname: hostname.to_owned(),
1549 purpose: purpose.to_owned(),
1550 },
1551 since: self.trace.since().clone(),
1552 opaque,
1553 };
1554
1555 if self.is_tombstone() {
1560 return Break(NoOpStateTransition(state));
1561 }
1562
1563 let state = match self.critical_readers.get_mut(reader_id) {
1564 Some(existing_state) => {
1565 existing_state.debug = state.debug;
1566 existing_state.clone()
1567 }
1568 None => {
1569 self.critical_readers
1570 .insert(reader_id.clone(), state.clone());
1571 state
1572 }
1573 };
1574 Continue(state)
1575 }
1576
1577 pub fn register_schema<K: Codec, V: Codec>(
1578 &mut self,
1579 key_schema: &K::Schema,
1580 val_schema: &V::Schema,
1581 ) -> ControlFlow<NoOpStateTransition<Option<SchemaId>>, Option<SchemaId>> {
1582 fn encode_data_type(data_type: &DataType) -> Bytes {
1583 let proto = data_type.into_proto();
1584 prost::Message::encode_to_vec(&proto).into()
1585 }
1586
1587 let existing_id = self.schemas.iter().rev().find(|(_, x)| {
1599 K::decode_schema(&x.key) == *key_schema && V::decode_schema(&x.val) == *val_schema
1600 });
1601 match existing_id {
1602 Some((schema_id, _)) => {
1603 Break(NoOpStateTransition(Some(*schema_id)))
1608 }
1609 None if self.is_tombstone() => {
1610 Break(NoOpStateTransition(None))
1612 }
1613 None if self.schemas.is_empty() => {
1614 let id = SchemaId(self.schemas.len());
1618 let key_data_type = mz_persist_types::columnar::data_type::<K>(key_schema)
1619 .expect("valid key schema");
1620 let val_data_type = mz_persist_types::columnar::data_type::<V>(val_schema)
1621 .expect("valid val schema");
1622 let prev = self.schemas.insert(
1623 id,
1624 EncodedSchemas {
1625 key: K::encode_schema(key_schema),
1626 key_data_type: encode_data_type(&key_data_type),
1627 val: V::encode_schema(val_schema),
1628 val_data_type: encode_data_type(&val_data_type),
1629 },
1630 );
1631 assert_eq!(prev, None);
1632 Continue(Some(id))
1633 }
1634 None => {
1635 info!(
1636 "register_schemas got {:?} expected {:?}",
1637 key_schema,
1638 self.schemas
1639 .iter()
1640 .map(|(id, x)| (id, K::decode_schema(&x.key)))
1641 .collect::<Vec<_>>()
1642 );
1643 Break(NoOpStateTransition(None))
1646 }
1647 }
1648 }
1649
1650 pub fn compare_and_evolve_schema<K: Codec, V: Codec>(
1651 &mut self,
1652 expected: SchemaId,
1653 key_schema: &K::Schema,
1654 val_schema: &V::Schema,
1655 ) -> ControlFlow<NoOpStateTransition<CaESchema<K, V>>, CaESchema<K, V>> {
1656 fn data_type<T>(schema: &impl Schema<T>) -> DataType {
1657 let array = Schema::encoder(schema).expect("valid schema").finish();
1661 Array::data_type(&array).clone()
1662 }
1663
1664 let (current_id, current) = self
1665 .schemas
1666 .last_key_value()
1667 .expect("all shards have a schema");
1668 if *current_id != expected {
1669 return Break(NoOpStateTransition(CaESchema::ExpectedMismatch {
1670 schema_id: *current_id,
1671 key: K::decode_schema(¤t.key),
1672 val: V::decode_schema(¤t.val),
1673 }));
1674 }
1675
1676 let current_key = K::decode_schema(¤t.key);
1677 let current_key_dt = EncodedSchemas::decode_data_type(¤t.key_data_type);
1678 let current_val = V::decode_schema(¤t.val);
1679 let current_val_dt = EncodedSchemas::decode_data_type(¤t.val_data_type);
1680
1681 let key_dt = data_type(key_schema);
1682 let val_dt = data_type(val_schema);
1683
1684 if current_key == *key_schema
1686 && current_key_dt == key_dt
1687 && current_val == *val_schema
1688 && current_val_dt == val_dt
1689 {
1690 return Break(NoOpStateTransition(CaESchema::Ok(*current_id)));
1691 }
1692
1693 let key_fn = backward_compatible(¤t_key_dt, &key_dt);
1694 let val_fn = backward_compatible(¤t_val_dt, &val_dt);
1695 let (Some(key_fn), Some(val_fn)) = (key_fn, val_fn) else {
1696 return Break(NoOpStateTransition(CaESchema::Incompatible));
1697 };
1698 if key_fn.contains_drop() || val_fn.contains_drop() {
1702 return Break(NoOpStateTransition(CaESchema::Incompatible));
1703 }
1704
1705 let id = SchemaId(self.schemas.len());
1709 self.schemas.insert(
1710 id,
1711 EncodedSchemas {
1712 key: K::encode_schema(key_schema),
1713 key_data_type: prost::Message::encode_to_vec(&key_dt.into_proto()).into(),
1714 val: V::encode_schema(val_schema),
1715 val_data_type: prost::Message::encode_to_vec(&val_dt.into_proto()).into(),
1716 },
1717 );
1718 Continue(CaESchema::Ok(id))
1719 }
1720
1721 pub fn compare_and_append(
1722 &mut self,
1723 batch: &HollowBatch<T>,
1724 writer_id: &WriterId,
1725 heartbeat_timestamp_ms: u64,
1726 lease_duration_ms: u64,
1727 idempotency_token: &IdempotencyToken,
1728 debug_info: &HandleDebugState,
1729 inline_writes_total_max_bytes: usize,
1730 claim_compaction_percent: usize,
1731 claim_compaction_min_version: Option<&Version>,
1732 ) -> ControlFlow<CompareAndAppendBreak<T>, Vec<FueledMergeReq<T>>> {
1733 if self.is_tombstone() {
1738 assert_eq!(self.trace.upper(), &Antichain::new());
1739 return Break(CompareAndAppendBreak::Upper {
1740 shard_upper: Antichain::new(),
1741 writer_upper: Antichain::new(),
1746 });
1747 }
1748
1749 let writer_state = self
1750 .writers
1751 .entry(writer_id.clone())
1752 .or_insert_with(|| WriterState {
1753 last_heartbeat_timestamp_ms: heartbeat_timestamp_ms,
1754 lease_duration_ms,
1755 most_recent_write_token: IdempotencyToken::SENTINEL,
1756 most_recent_write_upper: Antichain::from_elem(T::minimum()),
1757 debug: debug_info.clone(),
1758 });
1759
1760 if PartialOrder::less_than(batch.desc.upper(), batch.desc.lower()) {
1761 return Break(CompareAndAppendBreak::InvalidUsage(
1762 InvalidUsage::InvalidBounds {
1763 lower: batch.desc.lower().clone(),
1764 upper: batch.desc.upper().clone(),
1765 },
1766 ));
1767 }
1768
1769 if batch.desc.upper() == batch.desc.lower() && !batch.is_empty() {
1772 return Break(CompareAndAppendBreak::InvalidUsage(
1773 InvalidUsage::InvalidEmptyTimeInterval {
1774 lower: batch.desc.lower().clone(),
1775 upper: batch.desc.upper().clone(),
1776 keys: batch
1777 .parts
1778 .iter()
1779 .map(|x| x.printable_name().to_owned())
1780 .collect(),
1781 },
1782 ));
1783 }
1784
1785 if idempotency_token == &writer_state.most_recent_write_token {
1786 assert_eq!(batch.desc.upper(), &writer_state.most_recent_write_upper);
1791 assert!(
1792 PartialOrder::less_equal(batch.desc.upper(), self.trace.upper()),
1793 "{:?} vs {:?}",
1794 batch.desc.upper(),
1795 self.trace.upper()
1796 );
1797 return Break(CompareAndAppendBreak::AlreadyCommitted);
1798 }
1799
1800 let shard_upper = self.trace.upper();
1801 if shard_upper != batch.desc.lower() {
1802 return Break(CompareAndAppendBreak::Upper {
1803 shard_upper: shard_upper.clone(),
1804 writer_upper: writer_state.most_recent_write_upper.clone(),
1805 });
1806 }
1807
1808 let new_inline_bytes = batch.inline_bytes();
1809 if new_inline_bytes > 0 {
1810 let mut existing_inline_bytes = 0;
1811 self.trace
1812 .map_batches(|x| existing_inline_bytes += x.inline_bytes());
1813 if existing_inline_bytes + new_inline_bytes >= inline_writes_total_max_bytes {
1817 return Break(CompareAndAppendBreak::InlineBackpressure);
1818 }
1819 }
1820
1821 let mut merge_reqs = if batch.desc.upper() != batch.desc.lower() {
1822 self.trace.push_batch(batch.clone())
1823 } else {
1824 Vec::new()
1825 };
1826
1827 let all_empty_reqs = merge_reqs
1830 .iter()
1831 .all(|req| req.inputs.iter().all(|b| b.batch.is_empty()));
1832 if all_empty_reqs && !batch.is_empty() {
1833 let mut reqs_to_take = claim_compaction_percent / 100;
1834 if (usize::cast_from(idempotency_token.hashed()) % 100)
1835 < (claim_compaction_percent % 100)
1836 {
1837 reqs_to_take += 1;
1838 }
1839 let threshold_ms = heartbeat_timestamp_ms.saturating_sub(lease_duration_ms);
1840 let min_writer = claim_compaction_min_version.map(WriterKey::for_version);
1841 merge_reqs.extend(
1842 self.trace
1845 .fueled_merge_reqs_before_ms(threshold_ms, min_writer)
1846 .take(reqs_to_take),
1847 )
1848 }
1849
1850 for req in &merge_reqs {
1851 self.trace.claim_compaction(
1852 req.id,
1853 ActiveCompaction {
1854 start_ms: heartbeat_timestamp_ms,
1855 },
1856 )
1857 }
1858
1859 mz_ore::soft_assert_eq_no_log!(self.trace.upper(), batch.desc.upper());
1860 writer_state.most_recent_write_token = idempotency_token.clone();
1861 assert!(
1863 PartialOrder::less_equal(&writer_state.most_recent_write_upper, batch.desc.upper()),
1864 "{:?} vs {:?}",
1865 writer_state.most_recent_write_upper,
1866 batch.desc.upper()
1867 );
1868 writer_state
1869 .most_recent_write_upper
1870 .clone_from(batch.desc.upper());
1871
1872 writer_state.last_heartbeat_timestamp_ms = std::cmp::max(
1874 heartbeat_timestamp_ms,
1875 writer_state.last_heartbeat_timestamp_ms,
1876 );
1877
1878 Continue(merge_reqs)
1879 }
1880
1881 pub fn apply_merge_res<D: Codec64 + Monoid + PartialEq>(
1882 &mut self,
1883 res: &FueledMergeRes<T>,
1884 metrics: &ColumnarMetrics,
1885 ) -> ControlFlow<NoOpStateTransition<ApplyMergeResult>, ApplyMergeResult> {
1886 if self.is_tombstone() {
1891 return Break(NoOpStateTransition(ApplyMergeResult::NotAppliedNoMatch));
1892 }
1893
1894 let apply_merge_result = self.trace.apply_merge_res_checked::<D>(res, metrics);
1895 Continue(apply_merge_result)
1896 }
1897
1898 pub fn spine_exert(
1899 &mut self,
1900 fuel: usize,
1901 ) -> ControlFlow<NoOpStateTransition<Vec<FueledMergeReq<T>>>, Vec<FueledMergeReq<T>>> {
1902 let (merge_reqs, did_work) = self.trace.exert(fuel);
1903 if did_work {
1904 Continue(merge_reqs)
1905 } else {
1906 assert!(merge_reqs.is_empty());
1907 Break(NoOpStateTransition(Vec::new()))
1910 }
1911 }
1912
1913 pub fn downgrade_since(
1914 &mut self,
1915 reader_id: &LeasedReaderId,
1916 seqno: SeqNo,
1917 outstanding_seqno: SeqNo,
1918 new_since: &Antichain<T>,
1919 heartbeat_timestamp_ms: u64,
1920 ) -> ControlFlow<NoOpStateTransition<Since<T>>, Since<T>> {
1921 if self.is_tombstone() {
1926 return Break(NoOpStateTransition(Since(Antichain::new())));
1927 }
1928
1929 let Some(reader_state) = self.leased_reader(reader_id) else {
1932 tracing::warn!(
1933 "Leased reader {reader_id} was expired due to inactivity. Did the machine go to sleep?",
1934 );
1935 return Break(NoOpStateTransition(Since(Antichain::new())));
1936 };
1937
1938 reader_state.last_heartbeat_timestamp_ms = std::cmp::max(
1941 heartbeat_timestamp_ms,
1942 reader_state.last_heartbeat_timestamp_ms,
1943 );
1944
1945 let seqno = {
1946 assert!(
1947 outstanding_seqno >= reader_state.seqno,
1948 "SeqNos cannot go backward; however, oldest leased SeqNo ({:?}) \
1949 is behind current reader_state ({:?})",
1950 outstanding_seqno,
1951 reader_state.seqno,
1952 );
1953 std::cmp::min(outstanding_seqno, seqno)
1954 };
1955
1956 reader_state.seqno = seqno;
1957
1958 let reader_current_since = if PartialOrder::less_than(&reader_state.since, new_since) {
1959 reader_state.since.clone_from(new_since);
1960 self.update_since();
1961 new_since.clone()
1962 } else {
1963 reader_state.since.clone()
1966 };
1967
1968 Continue(Since(reader_current_since))
1969 }
1970
1971 pub fn compare_and_downgrade_since(
1972 &mut self,
1973 reader_id: &CriticalReaderId,
1974 expected_opaque: &Opaque,
1975 (new_opaque, new_since): (&Opaque, &Antichain<T>),
1976 ) -> ControlFlow<
1977 NoOpStateTransition<Result<Since<T>, (Opaque, Since<T>)>>,
1978 Result<Since<T>, (Opaque, Since<T>)>,
1979 > {
1980 if self.is_tombstone() {
1985 return Break(NoOpStateTransition(Ok(Since(Antichain::new()))));
1989 }
1990
1991 let reader_state = self.critical_reader(reader_id);
1992
1993 if reader_state.opaque != *expected_opaque {
1994 return Continue(Err((
1997 reader_state.opaque.clone(),
1998 Since(reader_state.since.clone()),
1999 )));
2000 }
2001
2002 reader_state.opaque = new_opaque.clone();
2003 if PartialOrder::less_equal(&reader_state.since, new_since) {
2004 reader_state.since.clone_from(new_since);
2005 self.update_since();
2006 Continue(Ok(Since(new_since.clone())))
2007 } else {
2008 Continue(Ok(Since(reader_state.since.clone())))
2012 }
2013 }
2014
2015 pub fn expire_leased_reader(
2016 &mut self,
2017 reader_id: &LeasedReaderId,
2018 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2019 if self.is_tombstone() {
2024 return Break(NoOpStateTransition(false));
2025 }
2026
2027 let existed = self.leased_readers.remove(reader_id).is_some();
2028 if existed {
2029 }
2043 Continue(existed)
2046 }
2047
2048 pub fn expire_critical_reader(
2049 &mut self,
2050 reader_id: &CriticalReaderId,
2051 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2052 if self.is_tombstone() {
2057 return Break(NoOpStateTransition(false));
2058 }
2059
2060 let existed = self.critical_readers.remove(reader_id).is_some();
2061 if existed {
2062 }
2076 Continue(existed)
2080 }
2081
2082 pub fn expire_writer(
2083 &mut self,
2084 writer_id: &WriterId,
2085 ) -> ControlFlow<NoOpStateTransition<bool>, bool> {
2086 if self.is_tombstone() {
2091 return Break(NoOpStateTransition(false));
2092 }
2093
2094 let existed = self.writers.remove(writer_id).is_some();
2095 Continue(existed)
2099 }
2100
2101 fn leased_reader(&mut self, id: &LeasedReaderId) -> Option<&mut LeasedReaderState<T>> {
2102 self.leased_readers.get_mut(id)
2103 }
2104
2105 fn critical_reader(&mut self, id: &CriticalReaderId) -> &mut CriticalReaderState<T> {
2106 self.critical_readers
2107 .get_mut(id)
2108 .unwrap_or_else(|| {
2109 panic!(
2110 "Unknown CriticalReaderId({}). It was either never registered, or has been manually expired.",
2111 id
2112 )
2113 })
2114 }
2115
2116 fn critical_since(&self) -> Option<Antichain<T>> {
2117 let mut critical_sinces = self.critical_readers.values().map(|r| &r.since);
2118 let mut since = critical_sinces.next().cloned()?;
2119 for s in critical_sinces {
2120 since.meet_assign(s);
2121 }
2122 Some(since)
2123 }
2124
2125 fn update_since(&mut self) {
2126 let mut sinces_iter = self
2127 .leased_readers
2128 .values()
2129 .map(|x| &x.since)
2130 .chain(self.critical_readers.values().map(|x| &x.since));
2131 let mut since = match sinces_iter.next() {
2132 Some(since) => since.clone(),
2133 None => {
2134 return;
2137 }
2138 };
2139 while let Some(s) = sinces_iter.next() {
2140 since.meet_assign(s);
2141 }
2142 self.trace.downgrade_since(&since);
2143 }
2144
2145 fn seqno_since(&self, seqno: SeqNo) -> SeqNo {
2146 let mut seqno_since = seqno;
2147 for cap in self.leased_readers.values() {
2148 seqno_since = std::cmp::min(seqno_since, cap.seqno);
2149 }
2150 seqno_since
2152 }
2153
2154 fn tombstone_batch() -> HollowBatch<T> {
2155 HollowBatch::empty(Description::new(
2156 Antichain::from_elem(T::minimum()),
2157 Antichain::new(),
2158 Antichain::new(),
2159 ))
2160 }
2161
2162 pub(crate) fn is_tombstone(&self) -> bool {
2163 self.trace.upper().is_empty()
2164 && self.trace.since().is_empty()
2165 && self.writers.is_empty()
2166 && self.leased_readers.is_empty()
2167 && self.critical_readers.is_empty()
2168 }
2169
2170 pub(crate) fn is_single_empty_batch(&self) -> bool {
2171 let mut batch_count = 0;
2172 let mut is_empty = true;
2173 self.trace.map_batches(|b| {
2174 batch_count += 1;
2175 is_empty &= b.is_empty()
2176 });
2177 batch_count <= 1 && is_empty
2178 }
2179
2180 pub fn become_tombstone_and_shrink(&mut self) -> ControlFlow<NoOpStateTransition<()>, ()> {
2181 assert_eq!(self.trace.upper(), &Antichain::new());
2182 assert_eq!(self.trace.since(), &Antichain::new());
2183
2184 let was_tombstone = self.is_tombstone();
2187
2188 self.writers.clear();
2190 self.leased_readers.clear();
2191 self.critical_readers.clear();
2192
2193 mz_ore::soft_assert_no_log!(self.is_tombstone());
2194
2195 let mut to_replace = None;
2204 let mut batch_count = 0;
2205 self.trace.map_batches(|b| {
2206 batch_count += 1;
2207 if !b.is_empty() && to_replace.is_none() {
2208 to_replace = Some(b.desc.clone());
2209 }
2210 });
2211 if let Some(desc) = to_replace {
2212 let result = self.trace.apply_tombstone_merge(&desc);
2216 assert!(
2217 result.matched(),
2218 "merge with a matching desc should always match"
2219 );
2220 Continue(())
2221 } else if batch_count > 1 {
2222 let mut new_trace = Trace::default();
2227 new_trace.downgrade_since(&Antichain::new());
2228 let merge_reqs = new_trace.push_batch(Self::tombstone_batch());
2229 assert_eq!(merge_reqs, Vec::new());
2230 self.trace = new_trace;
2231 Continue(())
2232 } else if !was_tombstone {
2233 Continue(())
2236 } else {
2237 Break(NoOpStateTransition(()))
2240 }
2241 }
2242}
2243
2244#[derive(Debug)]
2246#[cfg_attr(any(test, debug_assertions), derive(Clone, PartialEq))]
2247pub struct State<T> {
2248 pub(crate) shard_id: ShardId,
2249
2250 pub(crate) seqno: SeqNo,
2251 pub(crate) walltime_ms: u64,
2254 pub(crate) hostname: String,
2257 pub(crate) collections: StateCollections<T>,
2258}
2259
2260pub struct TypedState<K, V, T, D> {
2263 pub(crate) state: State<T>,
2264
2265 pub(crate) _phantom: PhantomData<fn() -> (K, V, D)>,
2273}
2274
2275impl<K, V, T: Clone, D> TypedState<K, V, T, D> {
2276 #[cfg(any(test, debug_assertions))]
2277 pub(crate) fn clone(&self, hostname: String) -> Self {
2278 TypedState {
2279 state: State {
2280 shard_id: self.shard_id.clone(),
2281 seqno: self.seqno.clone(),
2282 walltime_ms: self.walltime_ms,
2283 hostname,
2284 collections: self.collections.clone(),
2285 },
2286 _phantom: PhantomData,
2287 }
2288 }
2289
2290 pub(crate) fn clone_for_rollup(&self) -> Self {
2291 TypedState {
2292 state: State {
2293 shard_id: self.shard_id.clone(),
2294 seqno: self.seqno.clone(),
2295 walltime_ms: self.walltime_ms,
2296 hostname: self.hostname.clone(),
2297 collections: self.collections.clone(),
2298 },
2299 _phantom: PhantomData,
2300 }
2301 }
2302}
2303
2304impl<K, V, T: Debug, D> Debug for TypedState<K, V, T, D> {
2305 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2306 let TypedState { state, _phantom } = self;
2309 f.debug_struct("TypedState").field("state", state).finish()
2310 }
2311}
2312
2313#[cfg(any(test, debug_assertions))]
2315impl<K, V, T: PartialEq, D> PartialEq for TypedState<K, V, T, D> {
2316 fn eq(&self, other: &Self) -> bool {
2317 let TypedState {
2320 state: self_state,
2321 _phantom,
2322 } = self;
2323 let TypedState {
2324 state: other_state,
2325 _phantom,
2326 } = other;
2327 self_state == other_state
2328 }
2329}
2330
2331impl<K, V, T, D> Deref for TypedState<K, V, T, D> {
2332 type Target = State<T>;
2333
2334 fn deref(&self) -> &Self::Target {
2335 &self.state
2336 }
2337}
2338
2339impl<K, V, T, D> DerefMut for TypedState<K, V, T, D> {
2340 fn deref_mut(&mut self) -> &mut Self::Target {
2341 &mut self.state
2342 }
2343}
2344
2345impl<K, V, T, D> TypedState<K, V, T, D>
2346where
2347 K: Codec,
2348 V: Codec,
2349 T: Timestamp + Lattice + Codec64,
2350 D: Codec64,
2351{
2352 pub fn new(
2353 applier_version: Version,
2354 shard_id: ShardId,
2355 hostname: String,
2356 walltime_ms: u64,
2357 ) -> Self {
2358 let state = State {
2359 shard_id,
2360 seqno: SeqNo::minimum(),
2361 walltime_ms,
2362 hostname,
2363 collections: StateCollections {
2364 version: applier_version,
2365 last_gc_req: SeqNo::minimum(),
2366 rollups: BTreeMap::new(),
2367 active_rollup: None,
2368 active_gc: None,
2369 leased_readers: BTreeMap::new(),
2370 critical_readers: BTreeMap::new(),
2371 writers: BTreeMap::new(),
2372 schemas: BTreeMap::new(),
2373 trace: Trace::default(),
2374 },
2375 };
2376 TypedState {
2377 state,
2378 _phantom: PhantomData,
2379 }
2380 }
2381
2382 pub fn clone_apply<R, E, WorkFn>(
2383 &self,
2384 cfg: &PersistConfig,
2385 work_fn: &mut WorkFn,
2386 ) -> ControlFlow<E, (R, Self)>
2387 where
2388 WorkFn: FnMut(SeqNo, &PersistConfig, &mut StateCollections<T>) -> ControlFlow<E, R>,
2389 {
2390 let mut new_state = State {
2392 shard_id: self.shard_id,
2393 seqno: self.seqno.next(),
2394 walltime_ms: (cfg.now)(),
2395 hostname: cfg.hostname.clone(),
2396 collections: self.collections.clone(),
2397 };
2398
2399 if new_state.walltime_ms <= self.walltime_ms {
2402 new_state.walltime_ms = self.walltime_ms + 1;
2403 }
2404
2405 let work_ret = work_fn(new_state.seqno, cfg, &mut new_state.collections)?;
2406 let new_state = TypedState {
2407 state: new_state,
2408 _phantom: PhantomData,
2409 };
2410 Continue((work_ret, new_state))
2411 }
2412}
2413
2414#[derive(Copy, Clone, Debug)]
2415pub struct GcConfig {
2416 pub use_active_gc: bool,
2417 pub fallback_threshold_ms: u64,
2418 pub min_versions: usize,
2419 pub max_versions: usize,
2420}
2421
2422impl<T> State<T>
2423where
2424 T: Timestamp + Lattice + Codec64,
2425{
2426 pub fn shard_id(&self) -> ShardId {
2427 self.shard_id
2428 }
2429
2430 pub fn seqno(&self) -> SeqNo {
2431 self.seqno
2432 }
2433
2434 pub fn since(&self) -> &Antichain<T> {
2435 self.collections.trace.since()
2436 }
2437
2438 pub fn upper(&self) -> &Antichain<T> {
2439 self.collections.trace.upper()
2440 }
2441
2442 pub fn spine_batch_count(&self) -> usize {
2443 self.collections.trace.num_spine_batches()
2444 }
2445
2446 pub fn size_metrics(&self) -> StateSizeMetrics {
2447 let mut ret = StateSizeMetrics::default();
2448 self.blobs().for_each(|x| match x {
2449 HollowBlobRef::Batch(x) => {
2450 ret.hollow_batch_count += 1;
2451 ret.batch_part_count += x.part_count();
2452 ret.num_updates += x.len;
2453
2454 let batch_size = x.encoded_size_bytes();
2455 for x in x.parts.iter() {
2456 if x.ts_rewrite().is_some() {
2457 ret.rewrite_part_count += 1;
2458 }
2459 if x.is_inline() {
2460 ret.inline_part_count += 1;
2461 ret.inline_part_bytes += x.inline_bytes();
2462 }
2463 }
2464 ret.largest_batch_bytes = std::cmp::max(ret.largest_batch_bytes, batch_size);
2465 ret.state_batches_bytes += batch_size;
2466 }
2467 HollowBlobRef::Rollup(x) => {
2468 ret.state_rollup_count += 1;
2469 ret.state_rollups_bytes += x.encoded_size_bytes.unwrap_or_default()
2470 }
2471 });
2472 ret
2473 }
2474
2475 pub fn latest_rollup(&self) -> (&SeqNo, &HollowRollup) {
2476 self.collections
2479 .rollups
2480 .iter()
2481 .rev()
2482 .next()
2483 .expect("State should have at least one rollup if seqno > minimum")
2484 }
2485
2486 pub(crate) fn seqno_since(&self) -> SeqNo {
2487 self.collections.seqno_since(self.seqno)
2488 }
2489
2490 pub fn maybe_gc(&mut self, is_write: bool, now: u64, cfg: GcConfig) -> Option<GcReq> {
2502 let GcConfig {
2503 use_active_gc,
2504 fallback_threshold_ms,
2505 min_versions,
2506 max_versions,
2507 } = cfg;
2508 let gc_threshold = if use_active_gc {
2512 u64::cast_from(min_versions)
2513 } else {
2514 std::cmp::max(
2515 1,
2516 u64::cast_from(self.seqno.0.next_power_of_two().trailing_zeros()),
2517 )
2518 };
2519 let new_seqno_since = self.seqno_since();
2520 let gc_until_seqno = new_seqno_since.min(SeqNo(
2523 self.collections
2524 .last_gc_req
2525 .0
2526 .saturating_add(u64::cast_from(max_versions)),
2527 ));
2528 let should_gc = new_seqno_since
2529 .0
2530 .saturating_sub(self.collections.last_gc_req.0)
2531 >= gc_threshold;
2532
2533 let should_gc = if use_active_gc && !should_gc {
2536 match self.collections.active_gc {
2537 Some(active_gc) => now.saturating_sub(active_gc.start_ms) > fallback_threshold_ms,
2538 None => false,
2539 }
2540 } else {
2541 should_gc
2542 };
2543 let should_gc = should_gc && (is_write || self.collections.writers.is_empty());
2546 let tombstone_needs_gc = self.collections.is_tombstone();
2551 let should_gc = should_gc || tombstone_needs_gc;
2552 let should_gc = if use_active_gc {
2553 should_gc
2557 && match self.collections.active_gc {
2558 Some(active) => now.saturating_sub(active.start_ms) > fallback_threshold_ms,
2559 None => true,
2560 }
2561 } else {
2562 should_gc
2563 };
2564 if should_gc {
2565 self.collections.last_gc_req = gc_until_seqno;
2566 Some(GcReq {
2567 shard_id: self.shard_id,
2568 new_seqno_since: gc_until_seqno,
2569 })
2570 } else {
2571 None
2572 }
2573 }
2574
2575 pub fn seqnos_held(&self) -> usize {
2577 usize::cast_from(self.seqno.0.saturating_sub(self.seqno_since().0))
2578 }
2579
2580 pub fn expire_at(&mut self, walltime_ms: EpochMillis) -> ExpiryMetrics {
2582 let mut metrics = ExpiryMetrics::default();
2583 let shard_id = self.shard_id();
2584 self.collections.leased_readers.retain(|id, state| {
2585 let retain = state.last_heartbeat_timestamp_ms + state.lease_duration_ms >= walltime_ms;
2586 if !retain {
2587 info!(
2588 "Force expiring reader {id} ({}) of shard {shard_id} due to inactivity",
2589 state.debug.purpose
2590 );
2591 metrics.readers_expired += 1;
2592 }
2593 retain
2594 });
2595 self.collections.writers.retain(|id, state| {
2597 let retain =
2598 (state.last_heartbeat_timestamp_ms + state.lease_duration_ms) >= walltime_ms;
2599 if !retain {
2600 info!(
2601 "Force expiring writer {id} ({}) of shard {shard_id} due to inactivity",
2602 state.debug.purpose
2603 );
2604 metrics.writers_expired += 1;
2605 }
2606 retain
2607 });
2608 metrics
2609 }
2610
2611 pub fn snapshot(&self, as_of: &Antichain<T>) -> Result<Vec<HollowBatch<T>>, SnapshotErr<T>> {
2615 if PartialOrder::less_than(as_of, self.collections.trace.since()) {
2616 return Err(SnapshotErr::AsOfHistoricalDistinctionsLost(Since(
2617 self.collections.trace.since().clone(),
2618 )));
2619 }
2620 let upper = self.collections.trace.upper();
2621 if PartialOrder::less_equal(upper, as_of) {
2622 return Err(SnapshotErr::AsOfNotYetAvailable(
2623 self.seqno,
2624 Upper(upper.clone()),
2625 ));
2626 }
2627
2628 let batches = self
2629 .collections
2630 .trace
2631 .batches()
2632 .filter(|b| !PartialOrder::less_than(as_of, b.desc.lower()))
2633 .cloned()
2634 .collect();
2635 Ok(batches)
2636 }
2637
2638 pub fn verify_listen(&self, as_of: &Antichain<T>) -> Result<(), Since<T>> {
2640 if PartialOrder::less_than(as_of, self.collections.trace.since()) {
2641 return Err(Since(self.collections.trace.since().clone()));
2642 }
2643 Ok(())
2644 }
2645
2646 pub fn next_listen_batch(&self, frontier: &Antichain<T>) -> Result<HollowBatch<T>, SeqNo> {
2647 self.collections
2650 .trace
2651 .batches()
2652 .find(|b| {
2653 PartialOrder::less_equal(b.desc.lower(), frontier)
2654 && PartialOrder::less_than(frontier, b.desc.upper())
2655 })
2656 .cloned()
2657 .ok_or(self.seqno)
2658 }
2659
2660 pub fn active_rollup(&self) -> Option<ActiveRollup> {
2661 self.collections.active_rollup
2662 }
2663
2664 pub fn need_rollup(
2665 &self,
2666 threshold: usize,
2667 use_active_rollup: bool,
2668 fallback_threshold_ms: u64,
2669 now: u64,
2670 ) -> Option<SeqNo> {
2671 let (latest_rollup_seqno, _) = self.latest_rollup();
2672
2673 if self.collections.is_tombstone() && latest_rollup_seqno.next() < self.seqno {
2679 return Some(self.seqno);
2680 }
2681
2682 let seqnos_since_last_rollup = self.seqno.0.saturating_sub(latest_rollup_seqno.0);
2683
2684 if use_active_rollup {
2685 if seqnos_since_last_rollup > u64::cast_from(threshold) {
2691 match self.active_rollup() {
2692 Some(active_rollup) => {
2693 if now.saturating_sub(active_rollup.start_ms) > fallback_threshold_ms {
2694 return Some(self.seqno);
2695 }
2696 }
2697 None => {
2698 return Some(self.seqno);
2699 }
2700 }
2701 }
2702 } else {
2703 if seqnos_since_last_rollup > 0
2707 && seqnos_since_last_rollup % u64::cast_from(threshold) == 0
2708 {
2709 return Some(self.seqno);
2710 }
2711
2712 if seqnos_since_last_rollup
2715 > u64::cast_from(
2716 threshold * PersistConfig::DEFAULT_FALLBACK_ROLLUP_THRESHOLD_MULTIPLIER,
2717 )
2718 {
2719 return Some(self.seqno);
2720 }
2721 }
2722
2723 None
2724 }
2725
2726 pub(crate) fn blobs(&self) -> impl Iterator<Item = HollowBlobRef<'_, T>> {
2727 let batches = self.collections.trace.batches().map(HollowBlobRef::Batch);
2728 let rollups = self.collections.rollups.values().map(HollowBlobRef::Rollup);
2729 batches.chain(rollups)
2730 }
2731}
2732
2733fn serialize_part_bytes<S: Serializer>(val: &[u8], s: S) -> Result<S::Ok, S::Error> {
2734 let val = hex::encode(val);
2735 val.serialize(s)
2736}
2737
2738fn serialize_lazy_proto<S: Serializer, T: prost::Message + Default>(
2739 val: &Option<LazyProto<T>>,
2740 s: S,
2741) -> Result<S::Ok, S::Error> {
2742 val.as_ref()
2743 .map(|lazy| hex::encode(&lazy.into_proto()))
2744 .serialize(s)
2745}
2746
2747fn serialize_part_stats<S: Serializer>(
2748 val: &Option<LazyPartStats>,
2749 s: S,
2750) -> Result<S::Ok, S::Error> {
2751 let stats = val.as_ref().and_then(|x| match x.try_decode() {
2757 Ok(stats) => Some(stats.key),
2758 Err(err) => {
2759 tracing::warn!("undecodable part stats, reporting as absent: {err}");
2760 None
2761 }
2762 });
2763 stats.serialize(s)
2764}
2765
2766fn serialize_diffs_sum<S: Serializer>(val: &Option<[u8; 8]>, s: S) -> Result<S::Ok, S::Error> {
2767 let val = val.map(i64::decode);
2769 val.serialize(s)
2770}
2771
2772impl<T: Serialize + Timestamp + Lattice> Serialize for State<T> {
2778 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
2779 let State {
2780 shard_id,
2781 seqno,
2782 walltime_ms,
2783 hostname,
2784 collections:
2785 StateCollections {
2786 version: applier_version,
2787 last_gc_req,
2788 rollups,
2789 active_rollup,
2790 active_gc,
2791 leased_readers,
2792 critical_readers,
2793 writers,
2794 schemas,
2795 trace,
2796 },
2797 } = self;
2798 let mut s = s.serialize_struct("State", 13)?;
2799 let () = s.serialize_field("applier_version", &applier_version.to_string())?;
2800 let () = s.serialize_field("shard_id", shard_id)?;
2801 let () = s.serialize_field("seqno", seqno)?;
2802 let () = s.serialize_field("walltime_ms", walltime_ms)?;
2803 let () = s.serialize_field("hostname", hostname)?;
2804 let () = s.serialize_field("last_gc_req", last_gc_req)?;
2805 let () = s.serialize_field("rollups", rollups)?;
2806 let () = s.serialize_field("active_rollup", active_rollup)?;
2807 let () = s.serialize_field("active_gc", active_gc)?;
2808 let () = s.serialize_field("leased_readers", leased_readers)?;
2809 let () = s.serialize_field("critical_readers", critical_readers)?;
2810 let () = s.serialize_field("writers", writers)?;
2811 let () = s.serialize_field("schemas", schemas)?;
2812 let () = s.serialize_field("since", &trace.since().elements())?;
2813 let () = s.serialize_field("upper", &trace.upper().elements())?;
2814 let trace = trace.flatten();
2815 let () = s.serialize_field("batches", &trace.legacy_batches.keys().collect::<Vec<_>>())?;
2816 let () = s.serialize_field("hollow_batches", &trace.hollow_batches)?;
2817 let () = s.serialize_field("spine_batches", &trace.spine_batches)?;
2818 let () = s.serialize_field("merges", &trace.merges)?;
2819 s.end()
2820 }
2821}
2822
2823#[derive(Debug, Default)]
2824pub struct StateSizeMetrics {
2825 pub hollow_batch_count: usize,
2826 pub batch_part_count: usize,
2827 pub rewrite_part_count: usize,
2828 pub num_updates: usize,
2829 pub largest_batch_bytes: usize,
2830 pub state_batches_bytes: usize,
2831 pub state_rollups_bytes: usize,
2832 pub state_rollup_count: usize,
2833 pub inline_part_count: usize,
2834 pub inline_part_bytes: usize,
2835}
2836
2837#[derive(Default)]
2838pub struct ExpiryMetrics {
2839 pub(crate) readers_expired: usize,
2840 pub(crate) writers_expired: usize,
2841}
2842
2843#[derive(Debug, Clone, PartialEq)]
2845pub struct Since<T>(pub Antichain<T>);
2846
2847#[derive(Debug, PartialEq)]
2849pub struct Upper<T>(pub Antichain<T>);
2850
2851#[cfg(test)]
2852pub(crate) mod tests {
2853 use std::ops::Range;
2854 use std::str::FromStr;
2855
2856 use bytes::Bytes;
2857 use mz_build_info::DUMMY_BUILD_INFO;
2858 use mz_dyncfg::ConfigUpdates;
2859 use mz_ore::now::SYSTEM_TIME;
2860 use mz_ore::{assert_none, assert_ok};
2861 use mz_proto::RustType;
2862 use proptest::prelude::*;
2863 use proptest::strategy::ValueTree;
2864
2865 use crate::InvalidUsage::{InvalidBounds, InvalidEmptyTimeInterval};
2866 use crate::cache::PersistClientCache;
2867 use crate::internal::encoding::any_some_lazy_part_stats;
2868 use crate::internal::paths::RollupId;
2869 use crate::internal::trace::tests::any_trace;
2870 use crate::tests::new_test_client_cache;
2871 use crate::{Diagnostics, PersistLocation};
2872
2873 use super::*;
2874
2875 const LEASE_DURATION_MS: u64 = 900 * 1000;
2876 fn debug_state() -> HandleDebugState {
2877 HandleDebugState {
2878 hostname: "debug".to_owned(),
2879 purpose: "finding the bugs".to_owned(),
2880 }
2881 }
2882
2883 pub fn any_hollow_batch_with_exact_runs<T: Arbitrary + Timestamp>(
2884 num_runs: usize,
2885 ) -> impl Strategy<Value = HollowBatch<T>> {
2886 (
2887 any::<T>(),
2888 any::<T>(),
2889 any::<T>(),
2890 proptest::collection::vec(any_run_part::<T>(), num_runs + 1..20),
2891 any::<usize>(),
2892 )
2893 .prop_map(move |(t0, t1, since, parts, len)| {
2894 let (lower, upper) = if t0 <= t1 {
2895 (Antichain::from_elem(t0), Antichain::from_elem(t1))
2896 } else {
2897 (Antichain::from_elem(t1), Antichain::from_elem(t0))
2898 };
2899 let since = Antichain::from_elem(since);
2900
2901 let run_splits = (1..num_runs)
2902 .map(|i| i * parts.len() / num_runs)
2903 .collect::<Vec<_>>();
2904
2905 let run_meta = (0..num_runs)
2906 .map(|_| {
2907 let mut meta = RunMeta::default();
2908 meta.id = Some(RunId::new());
2909 meta
2910 })
2911 .collect::<Vec<_>>();
2912
2913 HollowBatch::new(
2914 Description::new(lower, upper, since),
2915 parts,
2916 len % 10,
2917 run_meta,
2918 run_splits,
2919 )
2920 })
2921 }
2922
2923 pub fn any_hollow_batch<T: Arbitrary + Timestamp>() -> impl Strategy<Value = HollowBatch<T>> {
2924 Strategy::prop_map(
2925 (
2926 any::<T>(),
2927 any::<T>(),
2928 any::<T>(),
2929 proptest::collection::vec(any_run_part::<T>(), 0..20),
2930 any::<usize>(),
2931 0..=10usize,
2932 proptest::collection::vec(any::<RunId>(), 10),
2933 ),
2934 |(t0, t1, since, parts, len, num_runs, run_ids)| {
2935 let (lower, upper) = if t0 <= t1 {
2936 (Antichain::from_elem(t0), Antichain::from_elem(t1))
2937 } else {
2938 (Antichain::from_elem(t1), Antichain::from_elem(t0))
2939 };
2940 let since = Antichain::from_elem(since);
2941 if num_runs > 0 && parts.len() > 2 && num_runs < parts.len() {
2942 let run_splits = (1..num_runs)
2943 .map(|i| i * parts.len() / num_runs)
2944 .collect::<Vec<_>>();
2945
2946 let run_meta = (0..num_runs)
2947 .enumerate()
2948 .map(|(i, _)| {
2949 let mut meta = RunMeta::default();
2950 meta.id = Some(run_ids[i]);
2951 meta
2952 })
2953 .collect::<Vec<_>>();
2954
2955 HollowBatch::new(
2956 Description::new(lower, upper, since),
2957 parts,
2958 len % 10,
2959 run_meta,
2960 run_splits,
2961 )
2962 } else {
2963 HollowBatch::new_run_for_test(
2964 Description::new(lower, upper, since),
2965 parts,
2966 len % 10,
2967 run_ids[0],
2968 )
2969 }
2970 },
2971 )
2972 }
2973
2974 pub fn any_batch_part<T: Arbitrary + Timestamp>() -> impl Strategy<Value = BatchPart<T>> {
2975 Strategy::prop_map(
2976 (
2977 any::<bool>(),
2978 any_hollow_batch_part(),
2979 any::<Option<T>>(),
2980 any::<Option<SchemaId>>(),
2981 any::<Option<SchemaId>>(),
2982 ),
2983 |(is_hollow, hollow, ts_rewrite, schema_id, deprecated_schema_id)| {
2984 if is_hollow {
2985 BatchPart::Hollow(hollow)
2986 } else {
2987 let updates = LazyInlineBatchPart::from_proto(Bytes::new()).unwrap();
2988 let ts_rewrite = ts_rewrite.map(Antichain::from_elem);
2989 BatchPart::Inline {
2990 updates,
2991 ts_rewrite,
2992 schema_id,
2993 deprecated_schema_id,
2994 }
2995 }
2996 },
2997 )
2998 }
2999
3000 pub fn any_run_part<T: Arbitrary + Timestamp>() -> impl Strategy<Value = RunPart<T>> {
3001 Strategy::prop_map(any_batch_part(), |part| RunPart::Single(part))
3002 }
3003
3004 pub fn any_hollow_batch_part<T: Arbitrary + Timestamp>()
3005 -> impl Strategy<Value = HollowBatchPart<T>> {
3006 Strategy::prop_map(
3007 (
3008 any::<PartialBatchKey>(),
3009 any::<usize>(),
3010 any::<Vec<u8>>(),
3011 any_some_lazy_part_stats(),
3012 any::<Option<T>>(),
3013 any::<[u8; 8]>(),
3014 any::<Option<BatchColumnarFormat>>(),
3015 any::<Option<SchemaId>>(),
3016 any::<Option<SchemaId>>(),
3017 ),
3018 |(
3019 key,
3020 encoded_size_bytes,
3021 key_lower,
3022 stats,
3023 ts_rewrite,
3024 diffs_sum,
3025 format,
3026 schema_id,
3027 deprecated_schema_id,
3028 )| {
3029 HollowBatchPart {
3030 key,
3031 meta: Default::default(),
3032 encoded_size_bytes,
3033 key_lower,
3034 structured_key_lower: None,
3035 stats,
3036 ts_rewrite: ts_rewrite.map(Antichain::from_elem),
3037 diffs_sum: Some(diffs_sum),
3038 format,
3039 schema_id,
3040 deprecated_schema_id,
3041 }
3042 },
3043 )
3044 }
3045
3046 pub fn any_leased_reader_state<T: Arbitrary>() -> impl Strategy<Value = LeasedReaderState<T>> {
3047 Strategy::prop_map(
3048 (
3049 any::<SeqNo>(),
3050 any::<Option<T>>(),
3051 any::<u64>(),
3052 any::<u64>(),
3053 any::<HandleDebugState>(),
3054 ),
3055 |(seqno, since, last_heartbeat_timestamp_ms, mut lease_duration_ms, debug)| {
3056 if lease_duration_ms == 0 {
3060 lease_duration_ms += 1;
3061 }
3062 LeasedReaderState {
3063 seqno,
3064 since: since.map_or_else(Antichain::new, Antichain::from_elem),
3065 last_heartbeat_timestamp_ms,
3066 lease_duration_ms,
3067 debug,
3068 }
3069 },
3070 )
3071 }
3072
3073 pub fn any_critical_reader_state<T>() -> impl Strategy<Value = CriticalReaderState<T>>
3074 where
3075 T: Arbitrary,
3076 {
3077 Strategy::prop_map(
3078 (
3079 any::<Option<T>>(),
3080 any::<Opaque>(),
3081 any::<HandleDebugState>(),
3082 ),
3083 |(since, opaque, debug)| CriticalReaderState {
3084 since: since.map_or_else(Antichain::new, Antichain::from_elem),
3085 opaque,
3086 debug,
3087 },
3088 )
3089 }
3090
3091 pub fn any_writer_state<T: Arbitrary>() -> impl Strategy<Value = WriterState<T>> {
3092 Strategy::prop_map(
3093 (
3094 any::<u64>(),
3095 any::<u64>(),
3096 any::<IdempotencyToken>(),
3097 any::<Option<T>>(),
3098 any::<HandleDebugState>(),
3099 ),
3100 |(
3101 last_heartbeat_timestamp_ms,
3102 lease_duration_ms,
3103 most_recent_write_token,
3104 most_recent_write_upper,
3105 debug,
3106 )| WriterState {
3107 last_heartbeat_timestamp_ms,
3108 lease_duration_ms,
3109 most_recent_write_token,
3110 most_recent_write_upper: most_recent_write_upper
3111 .map_or_else(Antichain::new, Antichain::from_elem),
3112 debug,
3113 },
3114 )
3115 }
3116
3117 pub fn any_encoded_schemas() -> impl Strategy<Value = EncodedSchemas> {
3118 Strategy::prop_map(
3119 (
3120 any::<Vec<u8>>(),
3121 any::<Vec<u8>>(),
3122 any::<Vec<u8>>(),
3123 any::<Vec<u8>>(),
3124 ),
3125 |(key, key_data_type, val, val_data_type)| EncodedSchemas {
3126 key: Bytes::from(key),
3127 key_data_type: Bytes::from(key_data_type),
3128 val: Bytes::from(val),
3129 val_data_type: Bytes::from(val_data_type),
3130 },
3131 )
3132 }
3133
3134 pub fn any_state<T: Arbitrary + Timestamp + Lattice>(
3135 num_trace_batches: Range<usize>,
3136 ) -> impl Strategy<Value = State<T>> {
3137 let part1 = (
3138 any::<ShardId>(),
3139 any::<SeqNo>(),
3140 any::<u64>(),
3141 any::<String>(),
3142 any::<SeqNo>(),
3143 proptest::collection::btree_map(any::<SeqNo>(), any::<HollowRollup>(), 1..3),
3144 proptest::option::of(any::<ActiveRollup>()),
3145 );
3146
3147 let part2 = (
3148 proptest::option::of(any::<ActiveGc>()),
3149 proptest::collection::btree_map(
3150 any::<LeasedReaderId>(),
3151 any_leased_reader_state::<T>(),
3152 1..3,
3153 ),
3154 proptest::collection::btree_map(
3155 any::<CriticalReaderId>(),
3156 any_critical_reader_state::<T>(),
3157 1..3,
3158 ),
3159 proptest::collection::btree_map(any::<WriterId>(), any_writer_state::<T>(), 0..3),
3160 proptest::collection::btree_map(any::<SchemaId>(), any_encoded_schemas(), 0..3),
3161 any_trace::<T>(num_trace_batches),
3162 );
3163
3164 (part1, part2).prop_map(
3165 |(
3166 (shard_id, seqno, walltime_ms, hostname, last_gc_req, rollups, active_rollup),
3167 (active_gc, leased_readers, critical_readers, writers, schemas, trace),
3168 )| State {
3169 shard_id,
3170 seqno,
3171 walltime_ms,
3172 hostname,
3173 collections: StateCollections {
3174 version: Version::new(1, 2, 3),
3175 last_gc_req,
3176 rollups,
3177 active_rollup,
3178 active_gc,
3179 leased_readers,
3180 critical_readers,
3181 writers,
3182 schemas,
3183 trace,
3184 },
3185 },
3186 )
3187 }
3188
3189 pub(crate) fn hollow<T: Timestamp>(
3190 lower: T,
3191 upper: T,
3192 keys: &[&str],
3193 len: usize,
3194 ) -> HollowBatch<T> {
3195 HollowBatch::new_run(
3196 Description::new(
3197 Antichain::from_elem(lower),
3198 Antichain::from_elem(upper),
3199 Antichain::from_elem(T::minimum()),
3200 ),
3201 keys.iter()
3202 .map(|x| {
3203 RunPart::Single(BatchPart::Hollow(HollowBatchPart {
3204 key: PartialBatchKey((*x).to_owned()),
3205 meta: Default::default(),
3206 encoded_size_bytes: 0,
3207 key_lower: vec![],
3208 structured_key_lower: None,
3209 stats: None,
3210 ts_rewrite: None,
3211 diffs_sum: None,
3212 format: None,
3213 schema_id: None,
3214 deprecated_schema_id: None,
3215 }))
3216 })
3217 .collect(),
3218 len,
3219 )
3220 }
3221
3222 #[mz_ore::test]
3223 fn downgrade_since() {
3224 let mut state = TypedState::<(), (), u64, i64>::new(
3225 DUMMY_BUILD_INFO.semver_version(),
3226 ShardId::new(),
3227 "".to_owned(),
3228 0,
3229 );
3230 let reader = LeasedReaderId::new();
3231 let seqno = SeqNo::minimum();
3232 let now = SYSTEM_TIME.clone();
3233 let _ = state.collections.register_leased_reader(
3234 "",
3235 &reader,
3236 "",
3237 seqno,
3238 Duration::from_secs(10),
3239 now(),
3240 false,
3241 );
3242
3243 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(0));
3245
3246 assert_eq!(
3248 state.collections.downgrade_since(
3249 &reader,
3250 seqno,
3251 seqno,
3252 &Antichain::from_elem(2),
3253 now()
3254 ),
3255 Continue(Since(Antichain::from_elem(2)))
3256 );
3257 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3258 assert_eq!(
3260 state.collections.downgrade_since(
3261 &reader,
3262 seqno,
3263 seqno,
3264 &Antichain::from_elem(2),
3265 now()
3266 ),
3267 Continue(Since(Antichain::from_elem(2)))
3268 );
3269 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3270 assert_eq!(
3272 state.collections.downgrade_since(
3273 &reader,
3274 seqno,
3275 seqno,
3276 &Antichain::from_elem(1),
3277 now()
3278 ),
3279 Continue(Since(Antichain::from_elem(2)))
3280 );
3281 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3282
3283 let reader2 = LeasedReaderId::new();
3285 let _ = state.collections.register_leased_reader(
3286 "",
3287 &reader2,
3288 "",
3289 seqno,
3290 Duration::from_secs(10),
3291 now(),
3292 false,
3293 );
3294
3295 assert_eq!(
3297 state.collections.downgrade_since(
3298 &reader2,
3299 seqno,
3300 seqno,
3301 &Antichain::from_elem(3),
3302 now()
3303 ),
3304 Continue(Since(Antichain::from_elem(3)))
3305 );
3306 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3307 assert_eq!(
3309 state.collections.downgrade_since(
3310 &reader,
3311 seqno,
3312 seqno,
3313 &Antichain::from_elem(5),
3314 now()
3315 ),
3316 Continue(Since(Antichain::from_elem(5)))
3317 );
3318 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3319
3320 assert_eq!(
3322 state.collections.expire_leased_reader(&reader),
3323 Continue(true)
3324 );
3325 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3326
3327 let reader3 = LeasedReaderId::new();
3329 let _ = state.collections.register_leased_reader(
3330 "",
3331 &reader3,
3332 "",
3333 seqno,
3334 Duration::from_secs(10),
3335 now(),
3336 false,
3337 );
3338
3339 assert_eq!(
3341 state.collections.downgrade_since(
3342 &reader3,
3343 seqno,
3344 seqno,
3345 &Antichain::from_elem(10),
3346 now()
3347 ),
3348 Continue(Since(Antichain::from_elem(10)))
3349 );
3350 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3351
3352 assert_eq!(
3354 state.collections.expire_leased_reader(&reader2),
3355 Continue(true)
3356 );
3357 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3362
3363 assert_eq!(
3365 state.collections.expire_leased_reader(&reader3),
3366 Continue(true)
3367 );
3368 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(3));
3373 }
3374
3375 #[mz_ore::test]
3376 fn compare_and_downgrade_since() {
3377 let mut state = TypedState::<(), (), u64, i64>::new(
3378 DUMMY_BUILD_INFO.semver_version(),
3379 ShardId::new(),
3380 "".to_owned(),
3381 0,
3382 );
3383 let reader = CriticalReaderId::new();
3384 let _ = state
3385 .collections
3386 .register_critical_reader("", &reader, Opaque::encode(&0u64), "");
3387
3388 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(0));
3390 assert_eq!(
3392 state
3393 .collections
3394 .critical_reader(&reader)
3395 .opaque
3396 .decode::<u64>(),
3397 u64::MIN
3398 );
3399
3400 assert_eq!(
3402 state.collections.compare_and_downgrade_since(
3403 &reader,
3404 &Opaque::encode(&0u64),
3405 (&Opaque::encode(&1u64), &Antichain::from_elem(2)),
3406 ),
3407 Continue(Ok(Since(Antichain::from_elem(2))))
3408 );
3409 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3410 assert_eq!(
3411 state
3412 .collections
3413 .critical_reader(&reader)
3414 .opaque
3415 .decode::<u64>(),
3416 1
3417 );
3418 assert_eq!(
3420 state.collections.compare_and_downgrade_since(
3421 &reader,
3422 &Opaque::encode(&1u64),
3423 (&Opaque::encode(&2u64), &Antichain::from_elem(2)),
3424 ),
3425 Continue(Ok(Since(Antichain::from_elem(2))))
3426 );
3427 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3428 assert_eq!(
3429 state
3430 .collections
3431 .critical_reader(&reader)
3432 .opaque
3433 .decode::<u64>(),
3434 2
3435 );
3436 assert_eq!(
3438 state.collections.compare_and_downgrade_since(
3439 &reader,
3440 &Opaque::encode(&2u64),
3441 (&Opaque::encode(&3u64), &Antichain::from_elem(1)),
3442 ),
3443 Continue(Ok(Since(Antichain::from_elem(2))))
3444 );
3445 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3446 assert_eq!(
3447 state
3448 .collections
3449 .critical_reader(&reader)
3450 .opaque
3451 .decode::<u64>(),
3452 3
3453 );
3454 }
3455
3456 #[mz_ore::test]
3457 fn compare_and_append() {
3458 let state = &mut TypedState::<String, String, u64, i64>::new(
3459 DUMMY_BUILD_INFO.semver_version(),
3460 ShardId::new(),
3461 "".to_owned(),
3462 0,
3463 )
3464 .collections;
3465
3466 let writer_id = WriterId::new();
3467 let now = SYSTEM_TIME.clone();
3468
3469 assert_eq!(state.trace.num_spine_batches(), 0);
3471 assert_eq!(state.trace.num_hollow_batches(), 0);
3472 assert_eq!(state.trace.num_updates(), 0);
3473
3474 assert_eq!(
3476 state.compare_and_append(
3477 &hollow(1, 2, &["key1"], 1),
3478 &writer_id,
3479 now(),
3480 LEASE_DURATION_MS,
3481 &IdempotencyToken::new(),
3482 &debug_state(),
3483 0,
3484 100,
3485 None
3486 ),
3487 Break(CompareAndAppendBreak::Upper {
3488 shard_upper: Antichain::from_elem(0),
3489 writer_upper: Antichain::from_elem(0)
3490 })
3491 );
3492
3493 assert!(
3495 state
3496 .compare_and_append(
3497 &hollow(0, 5, &[], 0),
3498 &writer_id,
3499 now(),
3500 LEASE_DURATION_MS,
3501 &IdempotencyToken::new(),
3502 &debug_state(),
3503 0,
3504 100,
3505 None
3506 )
3507 .is_continue()
3508 );
3509
3510 assert_eq!(
3512 state.compare_and_append(
3513 &hollow(5, 4, &["key1"], 1),
3514 &writer_id,
3515 now(),
3516 LEASE_DURATION_MS,
3517 &IdempotencyToken::new(),
3518 &debug_state(),
3519 0,
3520 100,
3521 None
3522 ),
3523 Break(CompareAndAppendBreak::InvalidUsage(InvalidBounds {
3524 lower: Antichain::from_elem(5),
3525 upper: Antichain::from_elem(4)
3526 }))
3527 );
3528
3529 assert_eq!(
3531 state.compare_and_append(
3532 &hollow(5, 5, &["key1"], 1),
3533 &writer_id,
3534 now(),
3535 LEASE_DURATION_MS,
3536 &IdempotencyToken::new(),
3537 &debug_state(),
3538 0,
3539 100,
3540 None
3541 ),
3542 Break(CompareAndAppendBreak::InvalidUsage(
3543 InvalidEmptyTimeInterval {
3544 lower: Antichain::from_elem(5),
3545 upper: Antichain::from_elem(5),
3546 keys: vec!["key1".to_owned()],
3547 }
3548 ))
3549 );
3550
3551 assert!(
3553 state
3554 .compare_and_append(
3555 &hollow(5, 5, &[], 0),
3556 &writer_id,
3557 now(),
3558 LEASE_DURATION_MS,
3559 &IdempotencyToken::new(),
3560 &debug_state(),
3561 0,
3562 100,
3563 None
3564 )
3565 .is_continue()
3566 );
3567 }
3568
3569 #[mz_ore::test]
3570 fn snapshot() {
3571 let now = SYSTEM_TIME.clone();
3572
3573 let mut state = TypedState::<String, String, u64, i64>::new(
3574 DUMMY_BUILD_INFO.semver_version(),
3575 ShardId::new(),
3576 "".to_owned(),
3577 0,
3578 );
3579 assert_eq!(
3581 state.snapshot(&Antichain::from_elem(0)),
3582 Err(SnapshotErr::AsOfNotYetAvailable(
3583 SeqNo(0),
3584 Upper(Antichain::from_elem(0))
3585 ))
3586 );
3587
3588 assert_eq!(
3590 state.snapshot(&Antichain::from_elem(5)),
3591 Err(SnapshotErr::AsOfNotYetAvailable(
3592 SeqNo(0),
3593 Upper(Antichain::from_elem(0))
3594 ))
3595 );
3596
3597 let writer_id = WriterId::new();
3598
3599 assert!(
3601 state
3602 .collections
3603 .compare_and_append(
3604 &hollow(0, 5, &["key1"], 1),
3605 &writer_id,
3606 now(),
3607 LEASE_DURATION_MS,
3608 &IdempotencyToken::new(),
3609 &debug_state(),
3610 0,
3611 100,
3612 None
3613 )
3614 .is_continue()
3615 );
3616
3617 assert_eq!(
3619 state.snapshot(&Antichain::from_elem(0)),
3620 Ok(vec![hollow(0, 5, &["key1"], 1)])
3621 );
3622
3623 assert_eq!(
3625 state.snapshot(&Antichain::from_elem(4)),
3626 Ok(vec![hollow(0, 5, &["key1"], 1)])
3627 );
3628
3629 assert_eq!(
3631 state.snapshot(&Antichain::from_elem(5)),
3632 Err(SnapshotErr::AsOfNotYetAvailable(
3633 SeqNo(0),
3634 Upper(Antichain::from_elem(5))
3635 ))
3636 );
3637 assert_eq!(
3638 state.snapshot(&Antichain::from_elem(6)),
3639 Err(SnapshotErr::AsOfNotYetAvailable(
3640 SeqNo(0),
3641 Upper(Antichain::from_elem(5))
3642 ))
3643 );
3644
3645 let reader = LeasedReaderId::new();
3646 let _ = state.collections.register_leased_reader(
3648 "",
3649 &reader,
3650 "",
3651 SeqNo::minimum(),
3652 Duration::from_secs(10),
3653 now(),
3654 false,
3655 );
3656 assert_eq!(
3657 state.collections.downgrade_since(
3658 &reader,
3659 SeqNo::minimum(),
3660 SeqNo::minimum(),
3661 &Antichain::from_elem(2),
3662 now()
3663 ),
3664 Continue(Since(Antichain::from_elem(2)))
3665 );
3666 assert_eq!(state.collections.trace.since(), &Antichain::from_elem(2));
3667 assert_eq!(
3669 state.snapshot(&Antichain::from_elem(1)),
3670 Err(SnapshotErr::AsOfHistoricalDistinctionsLost(Since(
3671 Antichain::from_elem(2)
3672 )))
3673 );
3674
3675 assert!(
3677 state
3678 .collections
3679 .compare_and_append(
3680 &hollow(5, 10, &[], 0),
3681 &writer_id,
3682 now(),
3683 LEASE_DURATION_MS,
3684 &IdempotencyToken::new(),
3685 &debug_state(),
3686 0,
3687 100,
3688 None
3689 )
3690 .is_continue()
3691 );
3692
3693 assert_eq!(
3695 state.snapshot(&Antichain::from_elem(7)),
3696 Ok(vec![hollow(0, 5, &["key1"], 1), hollow(5, 10, &[], 0)])
3697 );
3698
3699 assert_eq!(
3701 state.snapshot(&Antichain::from_elem(10)),
3702 Err(SnapshotErr::AsOfNotYetAvailable(
3703 SeqNo(0),
3704 Upper(Antichain::from_elem(10))
3705 ))
3706 );
3707
3708 assert!(
3710 state
3711 .collections
3712 .compare_and_append(
3713 &hollow(10, 15, &["key2"], 1),
3714 &writer_id,
3715 now(),
3716 LEASE_DURATION_MS,
3717 &IdempotencyToken::new(),
3718 &debug_state(),
3719 0,
3720 100,
3721 None
3722 )
3723 .is_continue()
3724 );
3725
3726 assert_eq!(
3729 state.snapshot(&Antichain::from_elem(9)),
3730 Ok(vec![hollow(0, 5, &["key1"], 1), hollow(5, 10, &[], 0)])
3731 );
3732
3733 assert_eq!(
3735 state.snapshot(&Antichain::from_elem(10)),
3736 Ok(vec![
3737 hollow(0, 5, &["key1"], 1),
3738 hollow(5, 10, &[], 0),
3739 hollow(10, 15, &["key2"], 1)
3740 ])
3741 );
3742
3743 assert_eq!(
3744 state.snapshot(&Antichain::from_elem(11)),
3745 Ok(vec![
3746 hollow(0, 5, &["key1"], 1),
3747 hollow(5, 10, &[], 0),
3748 hollow(10, 15, &["key2"], 1)
3749 ])
3750 );
3751 }
3752
3753 #[mz_ore::test]
3754 fn next_listen_batch() {
3755 let mut state = TypedState::<String, String, u64, i64>::new(
3756 DUMMY_BUILD_INFO.semver_version(),
3757 ShardId::new(),
3758 "".to_owned(),
3759 0,
3760 );
3761
3762 assert_eq!(
3765 state.next_listen_batch(&Antichain::from_elem(0)),
3766 Err(SeqNo(0))
3767 );
3768 assert_eq!(state.next_listen_batch(&Antichain::new()), Err(SeqNo(0)));
3769
3770 let writer_id = WriterId::new();
3771 let now = SYSTEM_TIME.clone();
3772
3773 assert!(
3775 state
3776 .collections
3777 .compare_and_append(
3778 &hollow(0, 5, &["key1"], 1),
3779 &writer_id,
3780 now(),
3781 LEASE_DURATION_MS,
3782 &IdempotencyToken::new(),
3783 &debug_state(),
3784 0,
3785 100,
3786 None
3787 )
3788 .is_continue()
3789 );
3790 assert!(
3791 state
3792 .collections
3793 .compare_and_append(
3794 &hollow(5, 10, &["key2"], 1),
3795 &writer_id,
3796 now(),
3797 LEASE_DURATION_MS,
3798 &IdempotencyToken::new(),
3799 &debug_state(),
3800 0,
3801 100,
3802 None
3803 )
3804 .is_continue()
3805 );
3806
3807 for t in 0..=4 {
3809 assert_eq!(
3810 state.next_listen_batch(&Antichain::from_elem(t)),
3811 Ok(hollow(0, 5, &["key1"], 1))
3812 );
3813 }
3814
3815 for t in 5..=9 {
3817 assert_eq!(
3818 state.next_listen_batch(&Antichain::from_elem(t)),
3819 Ok(hollow(5, 10, &["key2"], 1))
3820 );
3821 }
3822
3823 assert_eq!(
3825 state.next_listen_batch(&Antichain::from_elem(10)),
3826 Err(SeqNo(0))
3827 );
3828
3829 assert_eq!(state.next_listen_batch(&Antichain::new()), Err(SeqNo(0)));
3832 }
3833
3834 #[mz_ore::test]
3835 fn expire_writer() {
3836 let mut state = TypedState::<String, String, u64, i64>::new(
3837 DUMMY_BUILD_INFO.semver_version(),
3838 ShardId::new(),
3839 "".to_owned(),
3840 0,
3841 );
3842 let now = SYSTEM_TIME.clone();
3843
3844 let writer_id_one = WriterId::new();
3845
3846 let writer_id_two = WriterId::new();
3847
3848 assert!(
3850 state
3851 .collections
3852 .compare_and_append(
3853 &hollow(0, 2, &["key1"], 1),
3854 &writer_id_one,
3855 now(),
3856 LEASE_DURATION_MS,
3857 &IdempotencyToken::new(),
3858 &debug_state(),
3859 0,
3860 100,
3861 None
3862 )
3863 .is_continue()
3864 );
3865
3866 assert!(
3867 state
3868 .collections
3869 .expire_writer(&writer_id_one)
3870 .is_continue()
3871 );
3872
3873 assert!(
3875 state
3876 .collections
3877 .compare_and_append(
3878 &hollow(2, 5, &["key2"], 1),
3879 &writer_id_two,
3880 now(),
3881 LEASE_DURATION_MS,
3882 &IdempotencyToken::new(),
3883 &debug_state(),
3884 0,
3885 100,
3886 None
3887 )
3888 .is_continue()
3889 );
3890 }
3891
3892 #[mz_ore::test]
3893 fn maybe_gc_active_gc() {
3894 const GC_CONFIG: GcConfig = GcConfig {
3895 use_active_gc: true,
3896 fallback_threshold_ms: 5000,
3897 min_versions: 99,
3898 max_versions: 500,
3899 };
3900 let now_fn = SYSTEM_TIME.clone();
3901
3902 let mut state = TypedState::<String, String, u64, i64>::new(
3903 DUMMY_BUILD_INFO.semver_version(),
3904 ShardId::new(),
3905 "".to_owned(),
3906 0,
3907 );
3908
3909 let now = now_fn();
3910 assert_eq!(state.maybe_gc(true, now, GC_CONFIG), None);
3912 assert_eq!(state.maybe_gc(false, now, GC_CONFIG), None);
3913
3914 state.seqno = SeqNo(100);
3917 assert_eq!(state.seqno_since(), SeqNo(100));
3918
3919 let writer_id = WriterId::new();
3921 let _ = state.collections.compare_and_append(
3922 &hollow(1, 2, &["key1"], 1),
3923 &writer_id,
3924 now,
3925 LEASE_DURATION_MS,
3926 &IdempotencyToken::new(),
3927 &debug_state(),
3928 0,
3929 100,
3930 None,
3931 );
3932 assert_eq!(state.maybe_gc(false, now, GC_CONFIG), None);
3933
3934 assert_eq!(
3936 state.maybe_gc(true, now, GC_CONFIG),
3937 Some(GcReq {
3938 shard_id: state.shard_id,
3939 new_seqno_since: SeqNo(100)
3940 })
3941 );
3942
3943 state.collections.active_gc = Some(ActiveGc {
3945 seqno: state.seqno,
3946 start_ms: now,
3947 });
3948
3949 state.seqno = SeqNo(200);
3950 assert_eq!(state.seqno_since(), SeqNo(200));
3951
3952 assert_eq!(state.maybe_gc(true, now, GC_CONFIG), None);
3953
3954 state.seqno = SeqNo(300);
3955 assert_eq!(state.seqno_since(), SeqNo(300));
3956 let new_now = now + GC_CONFIG.fallback_threshold_ms + 1;
3958 assert_eq!(
3959 state.maybe_gc(true, new_now, GC_CONFIG),
3960 Some(GcReq {
3961 shard_id: state.shard_id,
3962 new_seqno_since: SeqNo(300)
3963 })
3964 );
3965
3966 state.seqno = SeqNo(301);
3970 assert_eq!(state.seqno_since(), SeqNo(301));
3971 assert_eq!(
3972 state.maybe_gc(true, new_now, GC_CONFIG),
3973 Some(GcReq {
3974 shard_id: state.shard_id,
3975 new_seqno_since: SeqNo(301)
3976 })
3977 );
3978
3979 state.collections.active_gc = None;
3980
3981 state.seqno = SeqNo(400);
3984 assert_eq!(state.seqno_since(), SeqNo(400));
3985
3986 let now = now_fn();
3987
3988 let _ = state.collections.expire_writer(&writer_id);
3990 assert_eq!(
3991 state.maybe_gc(false, now, GC_CONFIG),
3992 Some(GcReq {
3993 shard_id: state.shard_id,
3994 new_seqno_since: SeqNo(400)
3995 })
3996 );
3997
3998 let previous_seqno = state.seqno;
4000 state.seqno = SeqNo(10_000);
4001 assert_eq!(state.seqno_since(), SeqNo(10_000));
4002
4003 let now = now_fn();
4004 assert_eq!(
4005 state.maybe_gc(true, now, GC_CONFIG),
4006 Some(GcReq {
4007 shard_id: state.shard_id,
4008 new_seqno_since: SeqNo(previous_seqno.0 + u64::cast_from(GC_CONFIG.max_versions))
4009 })
4010 );
4011 }
4012
4013 #[mz_ore::test]
4014 fn maybe_gc_classic() {
4015 const GC_CONFIG: GcConfig = GcConfig {
4016 use_active_gc: false,
4017 fallback_threshold_ms: 5000,
4018 min_versions: 16,
4019 max_versions: 128,
4020 };
4021 const NOW_MS: u64 = 0;
4022
4023 let mut state = TypedState::<String, String, u64, i64>::new(
4024 DUMMY_BUILD_INFO.semver_version(),
4025 ShardId::new(),
4026 "".to_owned(),
4027 0,
4028 );
4029
4030 assert_eq!(state.maybe_gc(true, NOW_MS, GC_CONFIG), None);
4032 assert_eq!(state.maybe_gc(false, NOW_MS, GC_CONFIG), None);
4033
4034 state.seqno = SeqNo(100);
4037 assert_eq!(state.seqno_since(), SeqNo(100));
4038
4039 let writer_id = WriterId::new();
4041 let now = SYSTEM_TIME.clone();
4042 let _ = state.collections.compare_and_append(
4043 &hollow(1, 2, &["key1"], 1),
4044 &writer_id,
4045 now(),
4046 LEASE_DURATION_MS,
4047 &IdempotencyToken::new(),
4048 &debug_state(),
4049 0,
4050 100,
4051 None,
4052 );
4053 assert_eq!(state.maybe_gc(false, NOW_MS, GC_CONFIG), None);
4054
4055 assert_eq!(
4057 state.maybe_gc(true, NOW_MS, GC_CONFIG),
4058 Some(GcReq {
4059 shard_id: state.shard_id,
4060 new_seqno_since: SeqNo(100)
4061 })
4062 );
4063
4064 state.seqno = SeqNo(200);
4067 assert_eq!(state.seqno_since(), SeqNo(200));
4068
4069 let _ = state.collections.expire_writer(&writer_id);
4071 assert_eq!(
4072 state.maybe_gc(false, NOW_MS, GC_CONFIG),
4073 Some(GcReq {
4074 shard_id: state.shard_id,
4075 new_seqno_since: SeqNo(200)
4076 })
4077 );
4078 }
4079
4080 #[mz_ore::test]
4081 fn need_rollup_active_rollup() {
4082 const ROLLUP_THRESHOLD: usize = 3;
4083 const ROLLUP_USE_ACTIVE_ROLLUP: bool = true;
4084 const ROLLUP_FALLBACK_THRESHOLD_MS: u64 = 5000;
4085 let now = SYSTEM_TIME.clone();
4086
4087 mz_ore::test::init_logging();
4088 let mut state = TypedState::<String, String, u64, i64>::new(
4089 DUMMY_BUILD_INFO.semver_version(),
4090 ShardId::new(),
4091 "".to_owned(),
4092 0,
4093 );
4094
4095 let rollup_seqno = SeqNo(5);
4096 let rollup = HollowRollup {
4097 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4098 encoded_size_bytes: None,
4099 };
4100
4101 assert!(
4102 state
4103 .collections
4104 .add_rollup((rollup_seqno, &rollup))
4105 .is_continue()
4106 );
4107
4108 state.seqno = SeqNo(5);
4110 assert_none!(state.need_rollup(
4111 ROLLUP_THRESHOLD,
4112 ROLLUP_USE_ACTIVE_ROLLUP,
4113 ROLLUP_FALLBACK_THRESHOLD_MS,
4114 now()
4115 ));
4116
4117 state.seqno = SeqNo(6);
4119 assert_none!(state.need_rollup(
4120 ROLLUP_THRESHOLD,
4121 ROLLUP_USE_ACTIVE_ROLLUP,
4122 ROLLUP_FALLBACK_THRESHOLD_MS,
4123 now()
4124 ));
4125 state.seqno = SeqNo(7);
4126 assert_none!(state.need_rollup(
4127 ROLLUP_THRESHOLD,
4128 ROLLUP_USE_ACTIVE_ROLLUP,
4129 ROLLUP_FALLBACK_THRESHOLD_MS,
4130 now()
4131 ));
4132 state.seqno = SeqNo(8);
4133 assert_none!(state.need_rollup(
4134 ROLLUP_THRESHOLD,
4135 ROLLUP_USE_ACTIVE_ROLLUP,
4136 ROLLUP_FALLBACK_THRESHOLD_MS,
4137 now()
4138 ));
4139
4140 let mut current_time = now();
4141 state.seqno = SeqNo(9);
4143 assert_eq!(
4144 state
4145 .need_rollup(
4146 ROLLUP_THRESHOLD,
4147 ROLLUP_USE_ACTIVE_ROLLUP,
4148 ROLLUP_FALLBACK_THRESHOLD_MS,
4149 current_time
4150 )
4151 .expect("rollup"),
4152 SeqNo(9)
4153 );
4154
4155 state.collections.active_rollup = Some(ActiveRollup {
4156 seqno: SeqNo(9),
4157 start_ms: current_time,
4158 });
4159
4160 assert_none!(state.need_rollup(
4162 ROLLUP_THRESHOLD,
4163 ROLLUP_USE_ACTIVE_ROLLUP,
4164 ROLLUP_FALLBACK_THRESHOLD_MS,
4165 current_time
4166 ));
4167
4168 state.seqno = SeqNo(10);
4169 assert_none!(state.need_rollup(
4172 ROLLUP_THRESHOLD,
4173 ROLLUP_USE_ACTIVE_ROLLUP,
4174 ROLLUP_FALLBACK_THRESHOLD_MS,
4175 current_time
4176 ));
4177
4178 current_time += u64::cast_from(ROLLUP_FALLBACK_THRESHOLD_MS) + 1;
4180 assert_eq!(
4181 state
4182 .need_rollup(
4183 ROLLUP_THRESHOLD,
4184 ROLLUP_USE_ACTIVE_ROLLUP,
4185 ROLLUP_FALLBACK_THRESHOLD_MS,
4186 current_time
4187 )
4188 .expect("rollup"),
4189 SeqNo(10)
4190 );
4191
4192 state.seqno = SeqNo(9);
4193 state.collections.active_rollup = None;
4195 let rollup_seqno = SeqNo(9);
4196 let rollup = HollowRollup {
4197 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4198 encoded_size_bytes: None,
4199 };
4200 assert!(
4201 state
4202 .collections
4203 .add_rollup((rollup_seqno, &rollup))
4204 .is_continue()
4205 );
4206
4207 state.seqno = SeqNo(11);
4208 assert_none!(state.need_rollup(
4210 ROLLUP_THRESHOLD,
4211 ROLLUP_USE_ACTIVE_ROLLUP,
4212 ROLLUP_FALLBACK_THRESHOLD_MS,
4213 current_time
4214 ));
4215 state.seqno = SeqNo(13);
4217 assert_eq!(
4218 state
4219 .need_rollup(
4220 ROLLUP_THRESHOLD,
4221 ROLLUP_USE_ACTIVE_ROLLUP,
4222 ROLLUP_FALLBACK_THRESHOLD_MS,
4223 current_time
4224 )
4225 .expect("rollup"),
4226 SeqNo(13)
4227 );
4228 }
4229
4230 #[mz_ore::test]
4231 fn need_rollup_classic() {
4232 const ROLLUP_THRESHOLD: usize = 3;
4233 const ROLLUP_USE_ACTIVE_ROLLUP: bool = false;
4234 const ROLLUP_FALLBACK_THRESHOLD_MS: u64 = 0;
4235 const NOW: u64 = 0;
4236
4237 mz_ore::test::init_logging();
4238 let mut state = TypedState::<String, String, u64, i64>::new(
4239 DUMMY_BUILD_INFO.semver_version(),
4240 ShardId::new(),
4241 "".to_owned(),
4242 0,
4243 );
4244
4245 let rollup_seqno = SeqNo(5);
4246 let rollup = HollowRollup {
4247 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4248 encoded_size_bytes: None,
4249 };
4250
4251 assert!(
4252 state
4253 .collections
4254 .add_rollup((rollup_seqno, &rollup))
4255 .is_continue()
4256 );
4257
4258 state.seqno = SeqNo(5);
4260 assert_none!(state.need_rollup(
4261 ROLLUP_THRESHOLD,
4262 ROLLUP_USE_ACTIVE_ROLLUP,
4263 ROLLUP_FALLBACK_THRESHOLD_MS,
4264 NOW
4265 ));
4266
4267 state.seqno = SeqNo(6);
4269 assert_none!(state.need_rollup(
4270 ROLLUP_THRESHOLD,
4271 ROLLUP_USE_ACTIVE_ROLLUP,
4272 ROLLUP_FALLBACK_THRESHOLD_MS,
4273 NOW
4274 ));
4275 state.seqno = SeqNo(7);
4276 assert_none!(state.need_rollup(
4277 ROLLUP_THRESHOLD,
4278 ROLLUP_USE_ACTIVE_ROLLUP,
4279 ROLLUP_FALLBACK_THRESHOLD_MS,
4280 NOW
4281 ));
4282
4283 state.seqno = SeqNo(8);
4285 assert_eq!(
4286 state
4287 .need_rollup(
4288 ROLLUP_THRESHOLD,
4289 ROLLUP_USE_ACTIVE_ROLLUP,
4290 ROLLUP_FALLBACK_THRESHOLD_MS,
4291 NOW
4292 )
4293 .expect("rollup"),
4294 SeqNo(8)
4295 );
4296
4297 state.seqno = SeqNo(9);
4299 assert_none!(state.need_rollup(
4300 ROLLUP_THRESHOLD,
4301 ROLLUP_USE_ACTIVE_ROLLUP,
4302 ROLLUP_FALLBACK_THRESHOLD_MS,
4303 NOW
4304 ));
4305
4306 state.seqno = SeqNo(11);
4308 assert_eq!(
4309 state
4310 .need_rollup(
4311 ROLLUP_THRESHOLD,
4312 ROLLUP_USE_ACTIVE_ROLLUP,
4313 ROLLUP_FALLBACK_THRESHOLD_MS,
4314 NOW
4315 )
4316 .expect("rollup"),
4317 SeqNo(11)
4318 );
4319
4320 let rollup_seqno = SeqNo(6);
4322 let rollup = HollowRollup {
4323 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
4324 encoded_size_bytes: None,
4325 };
4326 assert!(
4327 state
4328 .collections
4329 .add_rollup((rollup_seqno, &rollup))
4330 .is_continue()
4331 );
4332
4333 state.seqno = SeqNo(8);
4334 assert_none!(state.need_rollup(
4335 ROLLUP_THRESHOLD,
4336 ROLLUP_USE_ACTIVE_ROLLUP,
4337 ROLLUP_FALLBACK_THRESHOLD_MS,
4338 NOW
4339 ));
4340 state.seqno = SeqNo(9);
4341 assert_eq!(
4342 state
4343 .need_rollup(
4344 ROLLUP_THRESHOLD,
4345 ROLLUP_USE_ACTIVE_ROLLUP,
4346 ROLLUP_FALLBACK_THRESHOLD_MS,
4347 NOW
4348 )
4349 .expect("rollup"),
4350 SeqNo(9)
4351 );
4352
4353 let fallback_seqno = SeqNo(
4355 rollup_seqno.0
4356 * u64::cast_from(PersistConfig::DEFAULT_FALLBACK_ROLLUP_THRESHOLD_MULTIPLIER),
4357 );
4358 state.seqno = fallback_seqno;
4359 assert_eq!(
4360 state
4361 .need_rollup(
4362 ROLLUP_THRESHOLD,
4363 ROLLUP_USE_ACTIVE_ROLLUP,
4364 ROLLUP_FALLBACK_THRESHOLD_MS,
4365 NOW
4366 )
4367 .expect("rollup"),
4368 fallback_seqno
4369 );
4370 state.seqno = fallback_seqno.next();
4371 assert_eq!(
4372 state
4373 .need_rollup(
4374 ROLLUP_THRESHOLD,
4375 ROLLUP_USE_ACTIVE_ROLLUP,
4376 ROLLUP_FALLBACK_THRESHOLD_MS,
4377 NOW
4378 )
4379 .expect("rollup"),
4380 fallback_seqno.next()
4381 );
4382 }
4383
4384 #[mz_ore::test]
4385 fn idempotency_token_sentinel() {
4386 assert_eq!(
4387 IdempotencyToken::SENTINEL.to_string(),
4388 "i11111111-1111-1111-1111-111111111111"
4389 );
4390 }
4391
4392 #[mz_ore::test]
4401 #[cfg_attr(miri, ignore)] fn state_inspect_serde_json() {
4403 const STATE_SERDE_JSON: &str = include_str!("state_serde.json");
4404 let mut runner = proptest::test_runner::TestRunner::deterministic();
4405 let tree = any_state::<u64>(6..8).new_tree(&mut runner).unwrap();
4406 let json = serde_json::to_string_pretty(&tree.current()).unwrap();
4407 assert_eq!(
4408 json.trim(),
4409 STATE_SERDE_JSON.trim(),
4410 "\n\nNEW GOLDEN\n{}\n",
4411 json
4412 );
4413 }
4414
4415 #[mz_persist_proc::test(tokio::test)]
4416 #[cfg_attr(miri, ignore)] async fn sneaky_downgrades(dyncfgs: ConfigUpdates) {
4418 let mut clients = new_test_client_cache(&dyncfgs);
4419 let shard_id = ShardId::new();
4420
4421 async fn open_and_write(
4422 clients: &mut PersistClientCache,
4423 version: semver::Version,
4424 shard_id: ShardId,
4425 ) -> Result<(), tokio::task::JoinError> {
4426 clients.cfg.build_version = version.clone();
4427 clients.clear_state_cache();
4428 let client = clients.open(PersistLocation::new_in_mem()).await.unwrap();
4429 mz_ore::task::spawn(|| version.to_string(), async move {
4431 let () = client
4432 .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
4433 .await
4434 .expect("valid usage");
4435 let (mut write, _) = client.expect_open::<String, (), u64, i64>(shard_id).await;
4436 let current = *write.upper().as_option().unwrap();
4437 write
4439 .expect_compare_and_append_batch(&mut [], current, current + 1)
4440 .await;
4441 })
4442 .into_tokio_handle()
4443 .await
4444 }
4445
4446 let res = open_and_write(&mut clients, Version::new(0, 10, 0), shard_id).await;
4448 assert_ok!(res);
4449
4450 let res = open_and_write(&mut clients, Version::new(0, 11, 0), shard_id).await;
4452 assert_ok!(res);
4453
4454 let res = open_and_write(&mut clients, Version::new(0, 10, 0), shard_id).await;
4456 assert!(res.unwrap_err().is_panic());
4457
4458 let res = open_and_write(&mut clients, Version::new(0, 9, 0), shard_id).await;
4460 assert!(res.unwrap_err().is_panic());
4461 }
4462
4463 #[mz_ore::test]
4464 fn runid_roundtrip() {
4465 proptest!(|(runid: RunId)| {
4466 let runid_str = runid.to_string();
4467 let parsed = RunId::from_str(&runid_str);
4468 prop_assert_eq!(parsed, Ok(runid));
4469 });
4470 }
4471
4472 #[mz_ore::test]
4488 fn add_rollup_idempotent_across_gc_removal() {
4489 let mut state = TypedState::<String, String, u64, i64>::new(
4490 DUMMY_BUILD_INFO.semver_version(),
4491 ShardId::new(),
4492 "".to_owned(),
4493 0,
4494 );
4495
4496 let older_seqno = SeqNo(10);
4497 let older = HollowRollup {
4498 key: PartialRollupKey::new(older_seqno, &RollupId::new()),
4499 encoded_size_bytes: None,
4500 };
4501 let newer_seqno = SeqNo(20);
4502 let newer = HollowRollup {
4503 key: PartialRollupKey::new(newer_seqno, &RollupId::new()),
4504 encoded_size_bytes: None,
4505 };
4506 let add_older = |state: &mut StateCollections<u64>| state.add_rollup((older_seqno, &older));
4507
4508 assert_eq!(add_older(&mut state.collections), Continue(true));
4510 assert_eq!(add_older(&mut state.collections), Continue(true));
4513 assert_eq!(state.collections.rollups.len(), 1);
4514
4515 assert_eq!(
4519 state.collections.add_rollup((newer_seqno, &newer)),
4520 Continue(true),
4521 );
4522
4523 let _ = state
4527 .collections
4528 .remove_rollups(&[(older_seqno, older.key.clone())]);
4529 assert!(!state.collections.rollups.contains_key(&older_seqno));
4530 assert!(state.collections.rollups.contains_key(&newer_seqno));
4531
4532 assert_eq!(add_older(&mut state.collections), Continue(false));
4537 assert!(!state.collections.rollups.contains_key(&older_seqno));
4538 assert_eq!(state.collections.rollups.len(), 1);
4539 }
4540}