1#[cfg(debug_assertions)]
13use std::collections::BTreeSet;
14use std::fmt::Debug;
15use std::ops::ControlFlow::{Break, Continue};
16use std::sync::Arc;
17use std::sync::atomic::Ordering;
18use std::time::SystemTime;
19
20use bytes::Bytes;
21use differential_dataflow::difference::Monoid;
22use differential_dataflow::lattice::Lattice;
23use differential_dataflow::trace::Description;
24use mz_ore::cast::CastFrom;
25use mz_persist::location::{
26 Blob, CaSResult, Consensus, Indeterminate, SCAN_ALL, SeqNo, VersionedData,
27};
28use mz_persist::retry::Retry;
29use mz_persist_types::{Codec, Codec64};
30use mz_proto::RustType;
31use prost::Message;
32use timely::progress::Timestamp;
33use tracing::{Instrument, debug, debug_span, trace, warn};
34
35use crate::cfg::{GC_REMOVE_FINALIZED_ROLLUPS, STATE_VERSIONS_RECENT_LIVE_DIFFS_LIMIT};
36use crate::error::{CodecMismatch, CodecMismatchT};
37use crate::internal::encoding::{Rollup, UntypedState};
38use crate::internal::machine::{retry_determinate, retry_external};
39use crate::internal::metrics::ShardMetrics;
40use crate::internal::paths::{BlobKey, PartialBlobKey, PartialRollupKey, RollupId};
41#[cfg(debug_assertions)]
42use crate::internal::state::HollowBatch;
43use crate::internal::state::{
44 BatchPart, HollowBlobRef, HollowRollup, NoOpStateTransition, RunPart, State, TypedState,
45};
46use crate::internal::state_diff::{StateDiff, StateFieldValDiff};
47use crate::{Metrics, PersistConfig, ShardId};
48
49#[derive(Debug)]
100pub struct StateVersions {
101 pub(crate) cfg: PersistConfig,
102 pub(crate) consensus: Arc<dyn Consensus>,
103 pub(crate) blob: Arc<dyn Blob>,
104 pub(crate) metrics: Arc<Metrics>,
105}
106
107#[derive(Debug, Clone)]
108pub struct RecentLiveDiffs(pub Vec<VersionedData>);
109
110#[derive(Debug, Clone)]
111pub struct EncodedRollup {
112 pub(crate) shard_id: ShardId,
113 pub(crate) seqno: SeqNo,
114 pub(crate) key: PartialRollupKey,
115 pub(crate) _desc: Description<SeqNo>,
116 finalized: bool,
117 buf: Bytes,
118}
119
120impl EncodedRollup {
121 pub fn to_hollow(&self) -> HollowRollup {
122 HollowRollup {
123 key: self.key.clone(),
124 encoded_size_bytes: Some(self.buf.len()),
125 finalized: self.finalized,
126 }
127 }
128}
129
130impl StateVersions {
131 pub fn new(
132 cfg: PersistConfig,
133 consensus: Arc<dyn Consensus>,
134 blob: Arc<dyn Blob>,
135 metrics: Arc<Metrics>,
136 ) -> Self {
137 StateVersions {
138 cfg,
139 consensus,
140 blob,
141 metrics,
142 }
143 }
144
145 pub async fn maybe_init_shard<K, V, T, D>(
148 &self,
149 shard_metrics: &ShardMetrics,
150 ) -> Result<TypedState<K, V, T, D>, Box<CodecMismatch>>
151 where
152 K: Debug + Codec,
153 V: Debug + Codec,
154 T: Timestamp + Lattice + Codec64,
155 D: Monoid + Codec64,
156 {
157 let shard_id = shard_metrics.shard_id;
158
159 let recent_live_diffs = self.fetch_recent_live_diffs::<T>(&shard_id).await;
161 if !recent_live_diffs.0.is_empty() {
162 return self
163 .fetch_current_state(&shard_id, recent_live_diffs.0)
164 .await
165 .check_codecs(&shard_id);
166 }
167
168 let (initial_state, initial_diff) = self.write_initial_rollup(shard_metrics).await;
170 assert_eq!(
171 initial_state.seqno(),
172 SeqNo::minimum(),
173 "initial state should have the initial seqno"
174 );
175 let (cas_res, _diff) =
176 retry_external(&self.metrics.retries.external.maybe_init_cas, || async {
177 self.try_compare_and_set_current(
178 "maybe_init_shard",
179 shard_metrics,
180 &initial_state,
181 &initial_diff,
182 )
183 .await
184 .map_err(|err| err.into())
185 })
186 .await;
187 match cas_res {
188 CaSResult::Committed => Ok(initial_state),
189 CaSResult::ExpectationMismatch => {
190 let recent_live_diffs = self.fetch_recent_live_diffs::<T>(&shard_id).await;
191 let state = self
192 .fetch_current_state(&shard_id, recent_live_diffs.0)
193 .await
194 .check_codecs(&shard_id);
195
196 let (_, rollup) = initial_state.latest_rollup();
204 let should_delete_rollup = match state.as_ref() {
205 Ok(state) => !state
206 .collections
207 .rollups
208 .values()
209 .any(|x| &x.key == &rollup.key),
210 Err(_codec_mismatch) => true,
213 };
214 if should_delete_rollup {
215 self.delete_rollup(&shard_id, &rollup.key).await;
216 }
217
218 state
219 }
220 }
221 }
222
223 pub async fn try_compare_and_set_current<K, V, T, D>(
228 &self,
229 cmd_name: &str,
230 shard_metrics: &ShardMetrics,
231 new_state: &TypedState<K, V, T, D>,
232 diff: &StateDiff<T>,
233 ) -> Result<(CaSResult, VersionedData), Indeterminate>
234 where
235 K: Debug + Codec,
236 V: Debug + Codec,
237 T: Timestamp + Lattice + Codec64,
238 D: Monoid + Codec64,
239 {
240 assert_eq!(shard_metrics.shard_id, new_state.shard_id);
241 let path = new_state.shard_id.to_string();
242
243 trace!(
244 "apply_unbatched_cmd {} attempting {}\n new_state={:?}",
245 cmd_name,
246 new_state.seqno(),
247 new_state
248 );
249 let new = self.metrics.codecs.state_diff.encode(|| {
250 let mut buf = Vec::new();
251 diff.encode(&mut buf);
252 VersionedData {
253 seqno: new_state.seqno(),
254 data: Bytes::from(buf),
255 }
256 });
257 assert_eq!(new.seqno, diff.seqno_to);
258
259 let payload_len = new.data.len();
260 let cas_res = retry_determinate(
261 &self.metrics.retries.determinate.apply_unbatched_cmd_cas,
262 || async { self.consensus.compare_and_set(&path, new.clone()).await },
263 )
264 .instrument(debug_span!("apply_unbatched_cmd::cas", payload_len))
265 .await
266 .map_err(|err| {
267 debug!("apply_unbatched_cmd {} errored: {}", cmd_name, err);
268 err
269 })?;
270
271 match cas_res {
272 CaSResult::Committed => {
273 trace!(
274 "apply_unbatched_cmd {} succeeded {}\n new_state={:?}",
275 cmd_name,
276 new_state.seqno(),
277 new_state
278 );
279
280 shard_metrics.seqnos_since_last_rollup.set(
281 new_state
282 .seqno
283 .0
284 .saturating_sub(new_state.latest_rollup().0.0),
285 );
286 shard_metrics
287 .spine_batch_count
288 .set(u64::cast_from(new_state.spine_batch_count()));
289 let size_metrics = new_state.size_metrics();
290 shard_metrics
291 .hollow_batch_count
292 .set(u64::cast_from(size_metrics.hollow_batch_count));
293 shard_metrics
294 .batch_part_count
295 .set(u64::cast_from(size_metrics.batch_part_count));
296 shard_metrics
297 .update_count
298 .set(u64::cast_from(size_metrics.num_updates));
299 shard_metrics
300 .rollup_count
301 .set(u64::cast_from(size_metrics.state_rollup_count));
302 shard_metrics
303 .largest_batch_size
304 .set(u64::cast_from(size_metrics.largest_batch_bytes));
305 shard_metrics
306 .usage_current_state_batches_bytes
307 .set(u64::cast_from(size_metrics.state_batches_bytes));
308 shard_metrics
309 .usage_current_state_rollups_bytes
310 .set(u64::cast_from(size_metrics.state_rollups_bytes));
311 shard_metrics
312 .seqnos_held
313 .set(u64::cast_from(new_state.seqnos_held()));
314 shard_metrics
315 .encoded_diff_size
316 .inc_by(u64::cast_from(payload_len));
317 shard_metrics
318 .inline_part_count
319 .set(u64::cast_from(size_metrics.inline_part_count));
320 shard_metrics.stale.store(
321 new_state
322 .state
323 .collections
324 .version
325 .cmp_precedence(&self.cfg.build_version)
326 .is_lt(),
327 Ordering::Relaxed,
328 );
329
330 let spine_metrics = new_state.collections.trace.spine_metrics();
331 shard_metrics
332 .compact_batches
333 .set(spine_metrics.compact_batches);
334 shard_metrics
335 .compacting_batches
336 .set(spine_metrics.compacting_batches);
337 shard_metrics
338 .noncompact_batches
339 .set(spine_metrics.noncompact_batches);
340
341 let batch_parts_by_version = new_state
342 .collections
343 .trace
344 .batches()
345 .flat_map(|x| x.parts.iter())
346 .flat_map(|part| {
347 let key = match part {
348 RunPart::Many(x) => Some(&x.key),
349 RunPart::Single(BatchPart::Hollow(x)) => Some(&x.key),
350 RunPart::Single(BatchPart::Inline { .. }) => None,
352 }?;
353 let (writer_key, _) = key.0.split_once('/')?;
355 match &writer_key[..1] {
356 "w" => Some("old"),
357 "n" => Some(&writer_key[1..]),
358 _ => None,
359 }
360 });
361 shard_metrics.set_batch_part_versions(batch_parts_by_version);
362
363 Ok((CaSResult::Committed, new))
364 }
365 CaSResult::ExpectationMismatch => {
366 debug!(
367 "apply_unbatched_cmd {} {} lost the CaS race, retrying: {:?}",
368 new_state.shard_id(),
369 cmd_name,
370 new_state.seqno.previous(),
371 );
372 Ok((CaSResult::ExpectationMismatch, new))
373 }
374 }
375 }
376
377 pub async fn fetch_current_state<T>(
384 &self,
385 shard_id: &ShardId,
386 mut live_diffs: Vec<VersionedData>,
387 ) -> UntypedState<T>
388 where
389 T: Timestamp + Lattice + Codec64,
390 {
391 let retry = self
392 .metrics
393 .retries
394 .fetch_latest_state
395 .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
396 loop {
397 let latest_diff = live_diffs
398 .last()
399 .expect("initialized shard should have at least one diff");
400 let latest_diff = self
401 .metrics
402 .codecs
403 .state_diff
404 .decode(|| {
406 StateDiff::<T>::decode(&self.cfg.build_version, latest_diff.data.clone())
407 });
408 let mut state = match self
409 .fetch_rollup_at_key(shard_id, &latest_diff.latest_rollup_key)
410 .await
411 {
412 Some(x) => x,
413 None => {
414 retry.retries.inc();
417 let earliest_before_refetch = live_diffs
418 .first()
419 .expect("initialized shard should have at least one diff")
420 .seqno;
421 live_diffs = self.fetch_recent_live_diffs::<T>(shard_id).await.0;
422
423 let earliest_after_refetch = live_diffs
430 .first()
431 .expect("initialized shard should have at least one diff")
432 .seqno;
433 if earliest_before_refetch >= earliest_after_refetch {
434 warn!(
435 concat!(
436 "fetch_current_state refetch expects earliest live diff to advance: {} vs {}. ",
437 "In dev and testing, this happens when persist's Blob (files in mzdata) ",
438 "is deleted out from under it or when two processes are talking to ",
439 "different Blobs (e.g. docker containers without it shared)."
440 ),
441 earliest_before_refetch, earliest_after_refetch
442 )
443 }
444 continue;
445 }
446 };
447
448 state.apply_encoded_diffs(&self.cfg, &self.metrics, &live_diffs);
449 return state;
450 }
451 }
452
453 pub async fn fetch_all_live_states<T>(
457 &self,
458 shard_id: ShardId,
459 ) -> Option<UntypedStateVersionsIter<T>>
460 where
461 T: Timestamp + Lattice + Codec64,
462 {
463 let retry = self
464 .metrics
465 .retries
466 .fetch_live_states
467 .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
468 let mut all_live_diffs = self.fetch_all_live_diffs(&shard_id).await;
469 loop {
470 let earliest_live_diff = match all_live_diffs.first() {
471 Some(x) => x,
472 None => return None,
473 };
474 let state = match self
475 .fetch_rollup_at_seqno(&shard_id, all_live_diffs.clone(), earliest_live_diff.seqno)
476 .await
477 {
478 Some(x) => x,
479 None => {
480 retry.retries.inc();
487 let earliest_before_refetch = earliest_live_diff.seqno;
488 all_live_diffs = self.fetch_all_live_diffs(&shard_id).await;
489
490 let earliest_after_refetch = all_live_diffs
497 .first()
498 .expect("initialized shard should have at least one diff")
499 .seqno;
500 if earliest_before_refetch >= earliest_after_refetch {
501 warn!(
502 concat!(
503 "fetch_all_live_states refetch expects earliest live diff to advance: {} vs {}. ",
504 "In dev and testing, this happens when persist's Blob (files in mzdata) ",
505 "is deleted out from under it or when two processes are talking to ",
506 "different Blobs (e.g. docker containers without it shared)."
507 ),
508 earliest_before_refetch, earliest_after_refetch
509 )
510 }
511 continue;
512 }
513 };
514 assert_eq!(earliest_live_diff.seqno, state.seqno());
515 return Some(UntypedStateVersionsIter {
516 shard_id,
517 cfg: self.cfg.clone(),
518 metrics: Arc::clone(&self.metrics),
519 state,
520 diffs: all_live_diffs,
521 });
522 }
523 }
524
525 pub async fn fetch_all_live_diffs(&self, shard_id: &ShardId) -> Vec<VersionedData> {
531 let path = shard_id.to_string();
532 let diffs = retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
533 self.consensus.scan(&path, SeqNo::minimum(), SCAN_ALL).await
534 })
535 .instrument(debug_span!("fetch_state::scan"))
536 .await;
537 diffs
538 }
539
540 async fn fetch_live_diffs(
543 &self,
544 shard_id: &ShardId,
545 from: SeqNo,
546 limit: usize,
547 ) -> Vec<VersionedData> {
548 let path = shard_id.to_string();
549 retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
550 self.consensus.scan(&path, from, limit).await
551 })
552 .instrument(debug_span!("fetch_state::scan"))
553 .await
554 }
555
556 pub async fn fetch_live_diffs_through(
559 &self,
560 shard_id: &ShardId,
561 through: SeqNo,
562 ) -> Vec<VersionedData> {
563 let scan_limit = STATE_VERSIONS_RECENT_LIVE_DIFFS_LIMIT.get(&self.cfg);
565 let mut versions = self
566 .fetch_live_diffs(shard_id, SeqNo::minimum(), scan_limit)
567 .await;
568
569 if versions.len() == scan_limit {
570 loop {
572 let Some(last_seqno) = versions.last().map(|v| v.seqno) else {
573 break;
574 };
575 if through <= last_seqno {
576 break;
577 }
578 let from = last_seqno.next();
579 let limit = usize::cast_from(through.0 - last_seqno.0).clamp(1, 10 * scan_limit);
580 let more_versions = self.fetch_live_diffs(shard_id, from, limit).await;
581 let more_versions_len = more_versions.len();
582 if let Some(first) = more_versions.first() {
583 assert!(last_seqno < first.seqno);
584 }
585 versions.extend(more_versions);
586 if more_versions_len < limit {
587 break;
588 }
589 }
590 }
591 let partition_index = versions.partition_point(|v| v.seqno <= through);
594 versions.truncate(partition_index);
595 versions
596 }
597
598 pub async fn fetch_recent_live_diffs<T>(&self, shard_id: &ShardId) -> RecentLiveDiffs
605 where
606 T: Timestamp + Lattice + Codec64,
607 {
608 let path = shard_id.to_string();
609 let scan_limit = STATE_VERSIONS_RECENT_LIVE_DIFFS_LIMIT.get(&self.cfg);
610 let oldest_diffs =
611 retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
612 self.consensus
613 .scan(&path, SeqNo::minimum(), scan_limit)
614 .await
615 })
616 .instrument(debug_span!("fetch_state::scan"))
617 .await;
618
619 if oldest_diffs.len() < scan_limit {
622 self.metrics.state.fetch_recent_live_diffs_fast_path.inc();
623 return RecentLiveDiffs(oldest_diffs);
624 }
625
626 let head = retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
637 self.consensus.head(&path).await
638 })
639 .instrument(debug_span!("fetch_state::slow_path::head"))
640 .await
641 .expect("initialized shard should have at least 1 diff");
642
643 let latest_diff = self
644 .metrics
645 .codecs
646 .state_diff
647 .decode(|| StateDiff::<T>::decode(&self.cfg.build_version, head.data));
648
649 match BlobKey::parse_ids(&latest_diff.latest_rollup_key.complete(shard_id)) {
650 Ok((_shard_id, PartialBlobKey::Rollup(seqno, _rollup))) => {
651 self.metrics.state.fetch_recent_live_diffs_slow_path.inc();
652 let diffs =
653 retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
654 self.consensus.scan(&path, seqno, SCAN_ALL).await
658 })
659 .instrument(debug_span!("fetch_state::slow_path::scan"))
660 .await;
661 RecentLiveDiffs(diffs)
662 }
663 Ok(_) => panic!(
664 "invalid state diff rollup key: {}",
665 latest_diff.latest_rollup_key
666 ),
667 Err(err) => panic!("unparseable state diff rollup key: {}", err),
668 }
669 }
670
671 pub async fn fetch_all_live_diffs_gt_seqno<K, V, T, D>(
676 &self,
677 shard_id: &ShardId,
678 seqno: SeqNo,
679 ) -> Vec<VersionedData> {
680 let path = shard_id.to_string();
681 retry_external(&self.metrics.retries.external.fetch_state_scan, || async {
682 self.consensus.scan(&path, seqno.next(), SCAN_ALL).await
683 })
684 .instrument(debug_span!("fetch_state::scan"))
685 .await
686 }
687
688 pub async fn truncate_diffs(&self, shard_id: &ShardId, seqno: SeqNo) {
690 let path = shard_id.to_string();
691 let _deleted_count = retry_external(&self.metrics.retries.external.gc_truncate, || async {
692 self.consensus.truncate(&path, seqno).await
693 })
694 .instrument(debug_span!("gc::truncate"))
695 .await;
696 }
697
698 async fn write_initial_rollup<K, V, T, D>(
702 &self,
703 shard_metrics: &ShardMetrics,
704 ) -> (TypedState<K, V, T, D>, StateDiff<T>)
705 where
706 K: Debug + Codec,
707 V: Debug + Codec,
708 T: Timestamp + Lattice + Codec64,
709 D: Monoid + Codec64,
710 {
711 let empty_state = TypedState::new(
712 self.cfg.build_version.clone(),
713 shard_metrics.shard_id,
714 self.cfg.hostname.clone(),
715 (self.cfg.now)(),
716 );
717 let mut initial_state = empty_state.clone_for_rollup();
718 let rollup_seqno = initial_state.seqno();
719 let rollup = HollowRollup {
720 key: PartialRollupKey::new(rollup_seqno, &RollupId::new()),
721 encoded_size_bytes: None,
725 finalized: false,
726 };
727 let applied = match initial_state
728 .collections
729 .add_rollup((rollup_seqno, &rollup))
730 {
731 Continue(x) => x,
732 Break(NoOpStateTransition(_)) => {
733 panic!("initial state transition should not be a no-op")
734 }
735 };
736 assert!(
737 applied,
738 "add_and_remove_rollups should apply to the empty state"
739 );
740
741 let rollup = self.encode_rollup_blob(
742 shard_metrics,
743 initial_state.clone_for_rollup(),
744 vec![],
745 rollup.key,
746 );
747 let () = self.write_rollup_blob(&rollup).await;
748 assert_eq!(initial_state.seqno, rollup.seqno);
749
750 let diff = StateDiff::from_diff(&empty_state.state, &initial_state.state);
751 (initial_state, diff)
752 }
753
754 pub async fn write_rollup_for_state<K, V, T, D>(
755 &self,
756 shard_metrics: &ShardMetrics,
757 state: TypedState<K, V, T, D>,
758 rollup_id: &RollupId,
759 ) -> Option<EncodedRollup>
760 where
761 K: Debug + Codec,
762 V: Debug + Codec,
763 T: Timestamp + Lattice + Codec64,
764 D: Monoid + Codec64,
765 {
766 let (latest_rollup_seqno, _rollup) = state.latest_rollup();
767 let seqno = state.seqno();
768
769 let diffs: Vec<_> = self
773 .fetch_all_live_diffs_gt_seqno::<K, V, T, D>(&state.shard_id, *latest_rollup_seqno)
774 .await;
775
776 match diffs.first() {
777 None => {
778 self.metrics.state.rollup_write_noop_latest.inc();
784 assert_eq!(seqno, *latest_rollup_seqno);
785 return None;
786 }
787 Some(first) => {
788 self.metrics.state.rollup_write_noop_truncated.inc();
796 if first.seqno != latest_rollup_seqno.next() {
797 assert!(
798 first.seqno > latest_rollup_seqno.next(),
799 "diff: {}, rollup: {}",
800 first.seqno,
801 latest_rollup_seqno,
802 );
803 return None;
804 }
805 }
806 }
807
808 let diffs: Vec<_> = diffs.into_iter().filter(|x| x.seqno <= seqno).collect();
810
811 assert_eq!(
814 diffs.first().map(|x| x.seqno),
815 Some(latest_rollup_seqno.next())
816 );
817 assert_eq!(diffs.last().map(|x| x.seqno), Some(state.seqno));
818
819 let key = PartialRollupKey::new(state.seqno, rollup_id);
820 let rollup = self.encode_rollup_blob(shard_metrics, state, diffs, key);
821 let () = self.write_rollup_blob(&rollup).await;
822
823 self.metrics.state.rollup_write_success.inc();
824
825 Some(rollup)
826 }
827
828 pub fn encode_rollup_blob<K, V, T, D>(
832 &self,
833 shard_metrics: &ShardMetrics,
834 state: TypedState<K, V, T, D>,
835 diffs: Vec<VersionedData>,
836 key: PartialRollupKey,
837 ) -> EncodedRollup
838 where
839 K: Debug + Codec,
840 V: Debug + Codec,
841 T: Timestamp + Lattice + Codec64,
842 D: Monoid + Codec64,
843 {
844 let shard_id = state.shard_id;
845 let rollup_seqno = state.seqno;
846 let finalized =
847 state.collections.is_finalized() && GC_REMOVE_FINALIZED_ROLLUPS.get(&self.cfg);
848
849 let rollup = Rollup::from(state.into(), diffs);
850 let desc = rollup.diffs.as_ref().expect("inlined diffs").description();
851
852 let buf = self.metrics.codecs.state.encode(|| {
853 let mut buf = Vec::new();
854 rollup
855 .into_proto()
856 .encode(&mut buf)
857 .expect("no required fields means no initialization errors");
858 Bytes::from(buf)
859 });
860 shard_metrics
861 .latest_rollup_size
862 .set(u64::cast_from(buf.len()));
863 EncodedRollup {
864 shard_id,
865 seqno: rollup_seqno,
866 key,
867 buf,
868 _desc: desc,
869 finalized,
870 }
871 }
872
873 pub async fn write_rollup_blob(&self, rollup: &EncodedRollup) {
875 let payload_len = rollup.buf.len();
876 retry_external(&self.metrics.retries.external.rollup_set, || async {
877 self.blob
878 .set(
879 &rollup.key.complete(&rollup.shard_id),
880 Bytes::clone(&rollup.buf),
881 )
882 .await
883 })
884 .instrument(debug_span!("rollup::set", payload_len))
885 .await;
886 }
887
888 async fn fetch_rollup_at_seqno<T>(
895 &self,
896 shard_id: &ShardId,
897 live_diffs: Vec<VersionedData>,
898 seqno: SeqNo,
899 ) -> Option<UntypedState<T>>
900 where
901 T: Timestamp + Lattice + Codec64,
902 {
903 let rollup_key_for_migration = live_diffs.iter().find_map(|x| {
904 let diff = self
905 .metrics
906 .codecs
907 .state_diff
908 .decode(|| StateDiff::<T>::decode(&self.cfg.build_version, x.data.clone()));
910 diff.rollups
911 .iter()
912 .find(|x| x.key == seqno)
913 .map(|x| match &x.val {
914 StateFieldValDiff::Insert(x) => x.clone(),
915 StateFieldValDiff::Update(_, x) => x.clone(),
916 StateFieldValDiff::Delete(x) => x.clone(),
917 })
918 });
919
920 let state = self.fetch_current_state::<T>(shard_id, live_diffs).await;
921 if let Some(rollup) = state.rollups().get(&seqno) {
922 return self.fetch_rollup_at_key(shard_id, &rollup.key).await;
923 }
924
925 let rollup = rollup_key_for_migration.expect("someone should have a key for this rollup");
958 tracing::info!("only found rollup for {} {} via migration", shard_id, seqno);
959 self.metrics.state.rollup_at_seqno_migration.inc();
960 self.fetch_rollup_at_key(shard_id, &rollup.key).await
961 }
962
963 pub async fn fetch_rollup_at_key<T>(
965 &self,
966 shard_id: &ShardId,
967 rollup_key: &PartialRollupKey,
968 ) -> Option<UntypedState<T>>
969 where
970 T: Timestamp + Lattice + Codec64,
971 {
972 retry_external(&self.metrics.retries.external.rollup_get, || async {
973 self.blob.get(&rollup_key.complete(shard_id)).await
974 })
975 .instrument(debug_span!("rollup::get"))
976 .await
977 .map(|buf| {
978 self.metrics
979 .codecs
980 .state
981 .decode(|| UntypedState::decode(&self.cfg.build_version, buf))
982 })
983 }
984
985 pub async fn delete_rollup(&self, shard_id: &ShardId, key: &PartialRollupKey) {
987 let _ = retry_external(&self.metrics.retries.external.rollup_delete, || async {
988 self.blob.delete(&key.complete(shard_id)).await
989 })
990 .await
991 .instrument(debug_span!("rollup::delete"));
992 }
993}
994
995pub struct UntypedStateVersionsIter<T> {
996 shard_id: ShardId,
997 cfg: PersistConfig,
998 metrics: Arc<Metrics>,
999 state: UntypedState<T>,
1000 diffs: Vec<VersionedData>,
1001}
1002
1003impl<T: Timestamp + Lattice + Codec64> UntypedStateVersionsIter<T> {
1004 pub(crate) fn new(
1005 shard_id: ShardId,
1006 cfg: PersistConfig,
1007 metrics: Arc<Metrics>,
1008 state: UntypedState<T>,
1009 diffs: Vec<VersionedData>,
1010 ) -> Self {
1011 Self {
1012 shard_id,
1013 cfg,
1014 metrics,
1015 state,
1016 diffs,
1017 }
1018 }
1019
1020 pub(crate) fn check_ts_codec(self) -> Result<StateVersionsIter<T>, CodecMismatchT> {
1021 let key_codec = self.state.key_codec.clone();
1022 let val_codec = self.state.val_codec.clone();
1023 let diff_codec = self.state.diff_codec.clone();
1024 let state = self.state.check_ts_codec(&self.shard_id)?;
1025 Ok(StateVersionsIter::new(
1026 self.cfg,
1027 self.metrics,
1028 state,
1029 self.diffs,
1030 key_codec,
1031 val_codec,
1032 diff_codec,
1033 ))
1034 }
1035}
1036
1037pub struct StateVersionsIter<T> {
1039 cfg: PersistConfig,
1040 metrics: Arc<Metrics>,
1041 state: State<T>,
1042 diffs: Vec<VersionedData>,
1043 key_codec: String,
1044 val_codec: String,
1045 diff_codec: String,
1046 #[cfg(debug_assertions)]
1047 validator: ReferencedBlobValidator<T>,
1048}
1049
1050impl<T: Timestamp + Lattice + Codec64> StateVersionsIter<T> {
1051 fn new(
1052 cfg: PersistConfig,
1053 metrics: Arc<Metrics>,
1054 state: State<T>,
1055 mut diffs: Vec<VersionedData>,
1057 key_codec: String,
1058 val_codec: String,
1059 diff_codec: String,
1060 ) -> Self {
1061 assert!(diffs.first().map_or(true, |x| x.seqno == state.seqno));
1062 diffs.reverse();
1063 StateVersionsIter {
1064 cfg,
1065 metrics,
1066 state,
1067 diffs,
1068 key_codec,
1069 val_codec,
1070 diff_codec,
1071 #[cfg(debug_assertions)]
1072 validator: ReferencedBlobValidator::default(),
1073 }
1074 }
1075
1076 pub fn len(&self) -> usize {
1077 self.diffs.len()
1078 }
1079
1080 pub fn next<F: for<'a> FnMut(InspectDiff<'a, T>)>(
1088 &mut self,
1089 mut inspect_diff_fn: F,
1090 ) -> Option<&State<T>> {
1091 let diff = match self.diffs.pop() {
1092 Some(x) => x,
1093 None => return None,
1094 };
1095 let data = diff.data.clone();
1096 let diff = self
1097 .metrics
1098 .codecs
1099 .state_diff
1100 .decode(|| StateDiff::decode(&self.cfg.build_version, diff.data));
1101
1102 if diff.seqno_to == self.state.seqno {
1105 let inspect = InspectDiff::FromInitial(&self.state);
1106 #[cfg(debug_assertions)]
1107 {
1108 inspect
1109 .referenced_blobs()
1110 .for_each(|x| self.validator.add_inc_blob(x));
1111 }
1112 inspect_diff_fn(inspect);
1113 } else {
1114 let inspect = InspectDiff::Diff(&diff);
1115 #[cfg(debug_assertions)]
1116 {
1117 inspect
1118 .referenced_blobs()
1119 .for_each(|x| self.validator.add_inc_blob(x));
1120 }
1121 inspect_diff_fn(inspect);
1122 }
1123
1124 let diff_seqno_to = diff.seqno_to;
1125 self.state
1126 .apply_diffs(&self.metrics, std::iter::once((diff, data)));
1127 assert_eq!(self.state.seqno, diff_seqno_to);
1128 #[cfg(debug_assertions)]
1129 {
1130 self.validator.validate_against_state(&self.state);
1131 }
1132 Some(&self.state)
1133 }
1134
1135 pub fn state(&self) -> &State<T> {
1136 &self.state
1137 }
1138
1139 pub fn into_rollup_proto_without_diffs(&self) -> impl serde::Serialize + use<T> {
1140 Rollup::from_state_without_diffs(
1141 State {
1142 shard_id: self.state.shard_id.clone(),
1143 seqno: self.state.seqno.clone(),
1144 walltime_ms: self.state.walltime_ms.clone(),
1145 hostname: self.state.hostname.clone(),
1146 collections: self.state.collections.clone(),
1147 },
1148 self.key_codec.clone(),
1149 self.val_codec.clone(),
1150 T::codec_name(),
1151 self.diff_codec.clone(),
1152 )
1153 .into_proto()
1154 }
1155}
1156
1157#[derive(Debug)]
1162pub enum InspectDiff<'a, T> {
1163 FromInitial(&'a State<T>),
1164 Diff(&'a StateDiff<T>),
1165}
1166
1167impl<T: Timestamp + Lattice + Codec64> InspectDiff<'_, T> {
1168 pub fn referenced_blobs(&self) -> impl Iterator<Item = HollowBlobRef<'_, T>> {
1172 let (state, diff) = match self {
1173 InspectDiff::FromInitial(x) => (Some(x), None),
1174 InspectDiff::Diff(x) => (None, Some(x)),
1175 };
1176 let state_blobs = state.into_iter().flat_map(|s| s.blobs());
1177 let diff_blobs = diff.into_iter().flat_map(|d| d.blob_inserts());
1178 state_blobs.chain(diff_blobs)
1179 }
1180}
1181
1182#[cfg(debug_assertions)]
1183struct ReferencedBlobValidator<T> {
1184 full_batches: BTreeSet<HollowBatch<T>>,
1187 full_rollups: BTreeSet<HollowRollup>,
1188 inc_batches: BTreeSet<HollowBatch<T>>,
1191 inc_rollups: BTreeSet<HollowRollup>,
1192}
1193
1194#[cfg(debug_assertions)]
1195impl<T> Default for ReferencedBlobValidator<T> {
1196 fn default() -> Self {
1197 Self {
1198 full_batches: Default::default(),
1199 full_rollups: Default::default(),
1200 inc_batches: Default::default(),
1201 inc_rollups: Default::default(),
1202 }
1203 }
1204}
1205
1206#[cfg(debug_assertions)]
1207impl<T: Timestamp + Lattice + Codec64> ReferencedBlobValidator<T> {
1208 fn add_inc_blob(&mut self, x: HollowBlobRef<'_, T>) {
1209 match x {
1210 HollowBlobRef::Batch(x) => assert!(
1211 self.inc_batches.insert(x.clone()) || x.desc.lower() == x.desc.upper(),
1212 "non-empty batches should only be appended once; duplicate: {x:?}"
1213 ),
1214 HollowBlobRef::Rollup(x) => assert!(self.inc_rollups.insert(x.clone())),
1215 }
1216 }
1217 fn validate_against_state(&mut self, x: &State<T>) {
1218 use std::hash::{DefaultHasher, Hash, Hasher};
1219
1220 use mz_ore::collections::HashSet;
1221 use timely::progress::Antichain;
1222
1223 use crate::internal::state::BatchPart;
1224
1225 x.blobs().for_each(|x| match x {
1226 HollowBlobRef::Batch(x) => {
1227 self.full_batches.insert(x.clone());
1228 }
1229 HollowBlobRef::Rollup(x) => {
1230 self.full_rollups.insert(x.clone());
1231 }
1232 });
1233
1234 fn overall_desc<'a, T: Timestamp + Lattice>(
1238 iter: impl Iterator<Item = &'a Description<T>>,
1239 ) -> (Antichain<T>, Antichain<T>) {
1240 let mut lower = Antichain::new();
1241 let mut upper = Antichain::from_elem(T::minimum());
1242 for desc in iter {
1243 lower.meet_assign(desc.lower());
1244 upper.join_assign(desc.upper());
1245 }
1246 (lower, upper)
1247 }
1248 let (inc_lower, inc_upper) = overall_desc(self.inc_batches.iter().map(|a| &a.desc));
1249 let (full_lower, full_upper) = overall_desc(self.full_batches.iter().map(|a| &a.desc));
1250 assert_eq!(inc_lower, full_lower);
1251 assert_eq!(inc_upper, full_upper);
1252
1253 fn part_unique<T: Codec64>(x: &RunPart<T>) -> String {
1254 match x {
1255 RunPart::Single(BatchPart::Inline {
1256 updates,
1257 ts_rewrite,
1258 ..
1259 }) => {
1260 let mut h = DefaultHasher::new();
1261 updates.hash(&mut h);
1262 if let Some(frontier) = &ts_rewrite {
1263 h.write_usize(frontier.len());
1264 frontier.iter().for_each(|t| t.encode().hash(&mut h));
1265 }
1266 h.finish().to_string()
1267 }
1268 other => other.printable_name().to_string(),
1269 }
1270 }
1271
1272 let inc_parts: HashSet<_> = self
1274 .inc_batches
1275 .iter()
1276 .flat_map(|x| x.parts.iter())
1277 .map(part_unique)
1278 .collect();
1279 let full_parts = self
1280 .full_batches
1281 .iter()
1282 .flat_map(|x| x.parts.iter())
1283 .map(part_unique)
1284 .collect();
1285 assert_eq!(inc_parts, full_parts);
1286
1287 assert_eq!(self.inc_rollups, self.full_rollups);
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 use mz_dyncfg::ConfigUpdates;
1295
1296 use crate::tests::new_test_client;
1297
1298 use super::*;
1299
1300 #[mz_persist_proc::test(tokio::test)]
1303 #[cfg_attr(miri, ignore)] async fn fetch_all_live_states_regression_uninitialized(dyncfgs: ConfigUpdates) {
1305 let client = new_test_client(&dyncfgs).await;
1306 let state_versions = StateVersions::new(
1307 client.cfg.clone(),
1308 Arc::clone(&client.consensus),
1309 Arc::clone(&client.blob),
1310 Arc::clone(&client.metrics),
1311 );
1312 assert!(
1313 state_versions
1314 .fetch_all_live_states::<u64>(ShardId::new())
1315 .await
1316 .is_none()
1317 );
1318 }
1319}