1use std::fmt::Debug;
13use std::ops::ControlFlow::{self, Break, Continue};
14use std::sync::Arc;
15use std::time::{Duration, Instant, SystemTime};
16
17use differential_dataflow::difference::Monoid;
18use differential_dataflow::lattice::Lattice;
19use futures::FutureExt;
20use futures::future::{self, BoxFuture};
21use mz_dyncfg::{Config, ConfigSet, ParameterScope};
22use mz_ore::cast::CastFrom;
23use mz_ore::error::ErrorExt;
24#[allow(unused_imports)] use mz_ore::fmt::FormatBuffer;
26use mz_ore::{assert_none, soft_assert_no_log};
27use mz_persist::location::{ExternalError, Indeterminate, SeqNo};
28use mz_persist::retry::Retry;
29use mz_persist_types::schema::SchemaId;
30use mz_persist_types::{Codec, Codec64};
31use semver::Version;
32use timely::PartialOrder;
33use timely::progress::{Antichain, Timestamp};
34use tracing::{Instrument, debug, info, trace_span, warn};
35
36use crate::async_runtime::IsolatedRuntime;
37use crate::batch::INLINE_WRITES_TOTAL_MAX_BYTES;
38use crate::cache::StateCache;
39use crate::cfg::RetryParameters;
40use crate::critical::{CriticalReaderId, Opaque};
41use crate::error::{CodecMismatch, InvalidUsage};
42use crate::internal::apply::Applier;
43use crate::internal::compact::CompactReq;
44use crate::internal::maintenance::{RoutineMaintenance, WriterMaintenance};
45use crate::internal::metrics::{CmdMetrics, Metrics, MetricsRetryStream, RetryMetrics};
46use crate::internal::paths::PartialRollupKey;
47use crate::internal::state::{
48 CompareAndAppendBreak, CriticalReaderState, HandleDebugState, HollowBatch, HollowRollup,
49 IdempotencyToken, LeasedReaderState, NoOpStateTransition, Since, SnapshotErr, StateCollections,
50};
51use crate::internal::state_versions::StateVersions;
52use crate::internal::trace::{ApplyMergeResult, FueledMergeRes};
53use crate::internal::watch::StateWatch;
54use crate::read::LeasedReaderId;
55use crate::rpc::PubSubSender;
56use crate::schema::CaESchema;
57use crate::write::WriterId;
58use crate::{Diagnostics, PersistConfig, ShardId};
59
60#[derive(Debug)]
61pub struct Machine<K, V, T, D> {
62 pub(crate) applier: Applier<K, V, T, D>,
63 pub(crate) isolated_runtime: Arc<IsolatedRuntime>,
64}
65
66impl<K, V, T: Clone, D> Clone for Machine<K, V, T, D> {
68 fn clone(&self) -> Self {
69 Self {
70 applier: self.applier.clone(),
71 isolated_runtime: Arc::clone(&self.isolated_runtime),
72 }
73 }
74}
75
76pub(crate) const CLAIM_UNCLAIMED_COMPACTIONS: Config<bool> = Config::new(
77 "persist_claim_unclaimed_compactions",
78 false,
79 "If an append doesn't result in a compaction request, but there is some uncompacted batch \
80 in state, compact that instead.",
81 ParameterScope::Environment,
82);
83
84pub(crate) const CLAIM_COMPACTION_PERCENT: Config<usize> = Config::new(
85 "persist_claim_compaction_percent",
86 100,
87 "Claim a compaction with the given percent chance, if claiming compactions is enabled. \
88 (If over 100, we'll always claim at least one; for example, if set to 365, we'll claim at least \
89 three and have a 65% chance of claiming a fourth.)",
90 ParameterScope::Environment,
91);
92
93pub(crate) const CLAIM_COMPACTION_MIN_VERSION: Config<String> = Config::new(
94 "persist_claim_compaction_min_version",
95 String::new(),
96 "If set to a valid version string, compact away any earlier versions if possible.",
97 ParameterScope::Environment,
98);
99
100impl<K, V, T, D> Machine<K, V, T, D>
101where
102 K: Debug + Codec,
103 V: Debug + Codec,
104 T: Timestamp + Lattice + Codec64 + Sync,
105 D: Monoid + Codec64,
106{
107 pub async fn new(
108 cfg: PersistConfig,
109 shard_id: ShardId,
110 metrics: Arc<Metrics>,
111 state_versions: Arc<StateVersions>,
112 shared_states: Arc<StateCache>,
113 pubsub_sender: Arc<dyn PubSubSender>,
114 isolated_runtime: Arc<IsolatedRuntime>,
115 diagnostics: Diagnostics,
116 ) -> Result<Self, Box<CodecMismatch>> {
117 let applier = Applier::new(
118 cfg,
119 shard_id,
120 metrics,
121 state_versions,
122 shared_states,
123 pubsub_sender,
124 diagnostics,
125 )
126 .await?;
127 Ok(Machine {
128 applier,
129 isolated_runtime,
130 })
131 }
132
133 pub fn shard_id(&self) -> ShardId {
134 self.applier.shard_id
135 }
136
137 pub fn seqno(&self) -> SeqNo {
138 self.applier.seqno()
139 }
140
141 pub async fn add_rollup_for_current_seqno(&self) -> RoutineMaintenance {
142 let rollup = self.applier.write_rollup_for_state().await;
143 let Some(rollup) = rollup else {
144 return RoutineMaintenance::default();
145 };
146
147 let (applied, maintenance) = self.add_rollup((rollup.seqno, &rollup.to_hollow())).await;
148 if !applied {
149 self.applier
152 .state_versions
153 .delete_rollup(&rollup.shard_id, &rollup.key)
154 .await;
155 }
156 maintenance
157 }
158
159 pub async fn add_rollup(
160 &self,
161 add_rollup: (SeqNo, &HollowRollup),
162 ) -> (bool, RoutineMaintenance) {
163 let mut applied_ever_true = false;
166 let metrics = Arc::clone(&self.applier.metrics);
167 let (_seqno, _applied, maintenance) = self
168 .apply_unbatched_idempotent_cmd(&metrics.cmds.add_rollup, |_, _, state| {
169 let ret = state.add_rollup(add_rollup);
170 if let Continue(applied) = ret {
171 applied_ever_true = applied_ever_true || applied;
172 }
173 ret
174 })
175 .await;
176 (applied_ever_true, maintenance)
177 }
178
179 pub async fn remove_rollups(
180 &self,
181 remove_rollups: &[(SeqNo, PartialRollupKey)],
182 remove_finalized: bool,
183 ) -> (Vec<SeqNo>, RoutineMaintenance) {
184 let metrics = Arc::clone(&self.applier.metrics);
185 let (_seqno, removed_rollup_seqnos, maintenance) = self
186 .apply_unbatched_idempotent_cmd(&metrics.cmds.remove_rollups, |_, _, state| {
187 state.remove_rollups(remove_rollups, remove_finalized)
188 })
189 .await;
190 (removed_rollup_seqnos, maintenance)
191 }
192
193 pub async fn upgrade_version(&self) -> Result<RoutineMaintenance, Version> {
196 let metrics = Arc::clone(&self.applier.metrics);
197 let (_seqno, upgrade_result, maintenance) = self
198 .apply_unbatched_idempotent_cmd(&metrics.cmds.upgrade_version, |_, cfg, state| {
199 if state.is_tombstone() {
203 return Break(NoOpStateTransition(Ok(())));
204 }
205
206 if state.version <= cfg.build_version {
207 state.version = cfg.build_version.clone();
210 Continue(Ok(()))
211 } else {
212 Break(NoOpStateTransition(Err(state.version.clone())))
213 }
214 })
215 .await;
216
217 match upgrade_result {
218 Ok(()) => Ok(maintenance),
219 Err(version) => {
220 soft_assert_no_log!(
221 maintenance.is_empty(),
222 "should not generate maintenance on failed upgrade"
223 );
224 Err(version)
225 }
226 }
227 }
228
229 pub async fn register_leased_reader(
231 &self,
232 reader_id: &LeasedReaderId,
233 purpose: &str,
234 lease_duration: Duration,
235 use_critical_since: bool,
236 ) -> (LeasedReaderState<T>, RoutineMaintenance) {
237 let metrics = Arc::clone(&self.applier.metrics);
238 let (_seqno, (reader_state, seqno_since), maintenance) = self
239 .apply_unbatched_idempotent_cmd(&metrics.cmds.register, |seqno, cfg, state| {
240 state.register_leased_reader(
241 &cfg.hostname,
242 reader_id,
243 purpose,
244 seqno,
245 lease_duration,
246 (cfg.now)(),
249 use_critical_since,
250 )
251 })
252 .await;
253 mz_ore::soft_assert_no_log!(
262 reader_state.seqno >= seqno_since,
263 "leased reader {} registered with seqno hold {} below the shard's seqno_since {}",
264 reader_id,
265 reader_state.seqno,
266 seqno_since,
267 );
268 (reader_state, maintenance)
269 }
270
271 pub async fn register_critical_reader(
272 &self,
273 reader_id: &CriticalReaderId,
274 default_opaque: Opaque,
275 purpose: &str,
276 ) -> (CriticalReaderState<T>, RoutineMaintenance) {
277 let metrics = Arc::clone(&self.applier.metrics);
278 let (_seqno, state, maintenance) = self
279 .apply_unbatched_idempotent_cmd(&metrics.cmds.register, |_seqno, cfg, state| {
280 state.register_critical_reader(
281 &cfg.hostname,
282 reader_id,
283 default_opaque.clone(),
284 purpose,
285 )
286 })
287 .await;
288 (state, maintenance)
289 }
290
291 pub async fn register_schema(
292 &self,
293 key_schema: &K::Schema,
294 val_schema: &V::Schema,
295 ) -> (Option<SchemaId>, RoutineMaintenance) {
296 let metrics = Arc::clone(&self.applier.metrics);
297 let register = || {
298 self.apply_unbatched_idempotent_cmd(&metrics.cmds.register, |_seqno, _cfg, state| {
299 state.register_schema::<K, V>(key_schema, val_schema)
300 })
301 };
302 let (_seqno, mut schema_id, mut maintenance) = register().await;
303 if schema_id.is_none() && !self.applier.is_tombstone() {
310 self.applier.fetch_and_update_state(None).await;
311 (_, schema_id, maintenance) = register().await;
312 }
313 (schema_id, maintenance)
314 }
315
316 pub async fn spine_exert(&self, fuel: usize) -> (Vec<CompactReq<T>>, RoutineMaintenance) {
317 if fuel == 0 || self.applier.all_batches().len() < 2 {
319 return (Vec::new(), RoutineMaintenance::default());
320 }
321
322 let metrics = Arc::clone(&self.applier.metrics);
323 let (_seqno, reqs, maintenance) = self
324 .apply_unbatched_idempotent_cmd(&metrics.cmds.spine_exert, |_seqno, _cfg, state| {
325 state.spine_exert(fuel)
326 })
327 .await;
328 let reqs = reqs
329 .into_iter()
330 .map(|req| CompactReq {
331 shard_id: self.shard_id(),
332 desc: req.desc,
333 inputs: req.inputs,
334 })
335 .collect();
336 (reqs, maintenance)
337 }
338
339 pub async fn compare_and_append(
341 &self,
342 batch: &HollowBatch<T>,
343 writer_id: &WriterId,
344 debug_info: &HandleDebugState,
345 ) -> CompareAndAppendRes<T> {
346 let idempotency_token = IdempotencyToken::new();
347 loop {
348 let res = self
349 .compare_and_append_idempotent(
350 batch,
351 writer_id,
352 &idempotency_token,
353 debug_info,
354 None,
355 )
356 .await;
357 match res {
358 CompareAndAppendRes::Success(seqno, maintenance) => {
359 return CompareAndAppendRes::Success(seqno, maintenance);
360 }
361 CompareAndAppendRes::InvalidUsage(x) => {
362 return CompareAndAppendRes::InvalidUsage(x);
363 }
364 CompareAndAppendRes::InlineBackpressure => {
365 return CompareAndAppendRes::InlineBackpressure;
366 }
367 CompareAndAppendRes::UpperMismatch(seqno, _current_upper) => {
368 self.applier.fetch_and_update_state(Some(seqno)).await;
375 let (current_seqno, current_upper) =
376 self.applier.upper(|seqno, upper| (seqno, upper.clone()));
377
378 if ¤t_upper != batch.desc.lower() {
381 return CompareAndAppendRes::UpperMismatch(current_seqno, current_upper);
382 } else {
383 }
386 }
387 }
388 }
389 }
390
391 async fn compare_and_append_idempotent(
392 &self,
393 batch: &HollowBatch<T>,
394 writer_id: &WriterId,
395 idempotency_token: &IdempotencyToken,
396 debug_info: &HandleDebugState,
397 mut indeterminate: Option<Indeterminate>,
401 ) -> CompareAndAppendRes<T> {
402 let metrics = Arc::clone(&self.applier.metrics);
403 let lease_duration_ms = self
404 .applier
405 .cfg
406 .writer_lease_duration
407 .as_millis()
408 .try_into()
409 .expect("reasonable duration");
410 let mut retry = self
493 .applier
494 .metrics
495 .retries
496 .compare_and_append_idempotent
497 .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
498 let mut writer_was_present = false;
499 loop {
500 let cmd_res = self
501 .applier
502 .apply_unbatched_cmd(&metrics.cmds.compare_and_append, |_, cfg, state| {
503 writer_was_present = state.writers.contains_key(writer_id);
504 state.compare_and_append(
505 batch,
506 writer_id,
507 (cfg.now)(),
510 lease_duration_ms,
511 idempotency_token,
512 debug_info,
513 INLINE_WRITES_TOTAL_MAX_BYTES.get(cfg),
514 if CLAIM_UNCLAIMED_COMPACTIONS.get(cfg) {
515 CLAIM_COMPACTION_PERCENT.get(cfg)
516 } else {
517 0
518 },
519 Version::parse(&CLAIM_COMPACTION_MIN_VERSION.get(cfg))
520 .ok()
521 .as_ref(),
522 )
523 })
524 .await;
525 let (seqno, res, routine) = match cmd_res {
526 Ok(x) => x,
527 Err(err) => {
528 info!(
531 "compare_and_append received an indeterminate error, retrying in {:?}: {}",
532 retry.next_sleep(),
533 err.display_with_causes()
534 );
535 if indeterminate.is_none() {
536 indeterminate = Some(err);
537 }
538 retry = retry.sleep().await;
539 continue;
540 }
541 };
542 match res {
543 Ok(merge_reqs) => {
544 let mut compact_reqs = Vec::with_capacity(merge_reqs.len());
547 for req in merge_reqs {
548 let req = CompactReq {
549 shard_id: self.shard_id(),
550 desc: req.desc,
551 inputs: req.inputs,
552 };
553 compact_reqs.push(req);
554 }
555 let writer_maintenance = WriterMaintenance {
556 routine,
557 compaction: compact_reqs,
558 };
559
560 if !writer_was_present {
561 metrics.state.writer_added.inc();
562 }
563 for part in &batch.parts {
564 if part.is_inline() {
565 let bytes = u64::cast_from(part.inline_bytes());
566 metrics.inline.part_commit_bytes.inc_by(bytes);
567 metrics.inline.part_commit_count.inc();
568 }
569 }
570 return CompareAndAppendRes::Success(seqno, writer_maintenance);
571 }
572 Err(CompareAndAppendBreak::AlreadyCommitted) => {
573 assert!(indeterminate.is_some());
577 self.applier.metrics.cmds.compare_and_append_noop.inc();
578 if !writer_was_present {
579 metrics.state.writer_added.inc();
580 }
581 return CompareAndAppendRes::Success(seqno, WriterMaintenance::default());
582 }
583 Err(CompareAndAppendBreak::InvalidUsage(err)) => {
584 assert_none!(indeterminate);
589 return CompareAndAppendRes::InvalidUsage(err);
590 }
591 Err(CompareAndAppendBreak::InlineBackpressure) => {
592 return CompareAndAppendRes::InlineBackpressure;
595 }
596 Err(CompareAndAppendBreak::Upper {
597 shard_upper,
598 writer_upper,
599 }) => {
600 assert!(
605 PartialOrder::less_equal(&writer_upper, &shard_upper),
606 "{:?} vs {:?}",
607 writer_upper,
608 shard_upper
609 );
610 if PartialOrder::less_than(&writer_upper, batch.desc.upper()) {
611 return CompareAndAppendRes::UpperMismatch(seqno, shard_upper);
615 }
616 if indeterminate.is_none() {
617 return CompareAndAppendRes::UpperMismatch(seqno, shard_upper);
622 }
623 panic!(
633 concat!(
634 "cannot distinguish compare_and_append success or failure ",
635 "caa_lower={:?} caa_upper={:?} writer_upper={:?} shard_upper={:?} err={:?}"
636 ),
637 batch.desc.lower().elements(),
638 batch.desc.upper().elements(),
639 writer_upper.elements(),
640 shard_upper.elements(),
641 indeterminate,
642 );
643 }
644 };
645 }
646 }
647
648 pub async fn downgrade_since(
650 &self,
651 reader_id: &LeasedReaderId,
652 outstanding_seqno: SeqNo,
653 new_since: &Antichain<T>,
654 ) -> (SeqNo, Since<T>, RoutineMaintenance) {
655 let metrics = Arc::clone(&self.applier.metrics);
656 self.apply_unbatched_idempotent_cmd(&metrics.cmds.downgrade_since, |seqno, cfg, state| {
657 state.downgrade_since(reader_id, seqno, outstanding_seqno, new_since, (cfg.now)())
660 })
661 .await
662 }
663
664 pub async fn compare_and_downgrade_since(
665 &self,
666 reader_id: &CriticalReaderId,
667 expected_opaque: &Opaque,
668 (new_opaque, new_since): (&Opaque, &Antichain<T>),
669 ) -> (Result<Since<T>, (Opaque, Since<T>)>, RoutineMaintenance) {
670 let metrics = Arc::clone(&self.applier.metrics);
671 let (_seqno, res, maintenance) = self
672 .apply_unbatched_idempotent_cmd(
673 &metrics.cmds.compare_and_downgrade_since,
674 |_seqno, _cfg, state| {
675 state.compare_and_downgrade_since(
676 reader_id,
677 expected_opaque,
678 (new_opaque, new_since),
679 )
680 },
681 )
682 .await;
683
684 match res {
685 Ok(since) => (Ok(since), maintenance),
686 Err((opaque, since)) => (Err((opaque, since)), maintenance),
687 }
688 }
689
690 pub async fn expire_leased_reader(
691 &self,
692 reader_id: &LeasedReaderId,
693 ) -> (SeqNo, RoutineMaintenance) {
694 let metrics = Arc::clone(&self.applier.metrics);
695 let (seqno, _existed, maintenance) = self
696 .apply_unbatched_idempotent_cmd(&metrics.cmds.expire_reader, |_, _, state| {
697 state.expire_leased_reader(reader_id)
698 })
699 .await;
700 (seqno, maintenance)
701 }
702
703 #[allow(dead_code)] pub async fn expire_critical_reader(
705 &self,
706 reader_id: &CriticalReaderId,
707 ) -> (SeqNo, RoutineMaintenance) {
708 let metrics = Arc::clone(&self.applier.metrics);
709 let (seqno, _existed, maintenance) = self
710 .apply_unbatched_idempotent_cmd(&metrics.cmds.expire_reader, |_, _, state| {
711 state.expire_critical_reader(reader_id)
712 })
713 .await;
714 (seqno, maintenance)
715 }
716
717 pub async fn expire_writer(&self, writer_id: &WriterId) -> (SeqNo, RoutineMaintenance) {
718 let metrics = Arc::clone(&self.applier.metrics);
719 let (seqno, _existed, maintenance) = self
720 .apply_unbatched_idempotent_cmd(&metrics.cmds.expire_writer, |_, _, state| {
721 state.expire_writer(writer_id)
722 })
723 .await;
724 metrics.state.writer_removed.inc();
725 (seqno, maintenance)
726 }
727
728 pub fn is_finalized(&self) -> bool {
729 self.applier.is_finalized()
730 }
731
732 pub fn get_schema(&self, schema_id: SchemaId) -> Option<(K::Schema, V::Schema)> {
734 self.applier.get_schema(schema_id)
735 }
736
737 pub fn latest_schema(&self) -> Option<(SchemaId, K::Schema, V::Schema)> {
739 self.applier.latest_schema()
740 }
741
742 pub fn find_schema(&self, key_schema: &K::Schema, val_schema: &V::Schema) -> Option<SchemaId> {
744 self.applier.find_schema(key_schema, val_schema)
745 }
746
747 pub async fn compare_and_evolve_schema(
751 &self,
752 expected: SchemaId,
753 key_schema: &K::Schema,
754 val_schema: &V::Schema,
755 ) -> (CaESchema<K, V>, RoutineMaintenance) {
756 let metrics = Arc::clone(&self.applier.metrics);
757 let (_seqno, state, maintenance) = self
758 .apply_unbatched_idempotent_cmd(
759 &metrics.cmds.compare_and_evolve_schema,
760 |_seqno, _cfg, state| {
761 state.compare_and_evolve_schema::<K, V>(expected, key_schema, val_schema)
762 },
763 )
764 .await;
765 (state, maintenance)
766 }
767
768 async fn tombstone_step(&self) -> Result<(bool, RoutineMaintenance), InvalidUsage<T>> {
769 let metrics = Arc::clone(&self.applier.metrics);
770 let mut retry = self
771 .applier
772 .metrics
773 .retries
774 .idempotent_cmd
775 .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
776 loop {
777 let res = self
778 .applier
779 .apply_unbatched_cmd(&metrics.cmds.become_tombstone, |_, _, state| {
780 state.become_tombstone_and_shrink()
781 })
782 .await;
783 let err = match res {
784 Ok((_seqno, Ok(()), maintenance)) => return Ok((true, maintenance)),
785 Ok((_seqno, Err(NoOpStateTransition(())), maintenance)) => {
786 return Ok((false, maintenance));
787 }
788 Err(err) => err,
789 };
790 if retry.attempt() >= INFO_MIN_ATTEMPTS {
791 info!(
792 "become_tombstone received an indeterminate error, retrying in {:?}: {}",
793 retry.next_sleep(),
794 err
795 );
796 } else {
797 debug!(
798 "become_tombstone received an indeterminate error, retrying in {:?}: {}",
799 retry.next_sleep(),
800 err
801 );
802 }
803 retry = retry.sleep().await;
804 }
805 }
806
807 pub async fn become_tombstone(&self) -> Result<RoutineMaintenance, InvalidUsage<T>> {
808 self.applier.check_since_upper_both_empty()?;
809
810 let mut maintenance = RoutineMaintenance::default();
811
812 loop {
813 let (made_progress, more_maintenance) = self.tombstone_step().await?;
814 maintenance.merge(more_maintenance);
815 if !made_progress {
816 break;
817 }
818 }
819
820 Ok(maintenance)
821 }
822
823 pub async fn unleased_snapshot(
827 &self,
828 as_of: &Antichain<T>,
829 ) -> Result<Vec<HollowBatch<T>>, Since<T>> {
830 if let Ok(data) = self.applier.snapshot(as_of) {
831 return Ok(data);
832 }
833 let mut watch = self.applier.watch();
834 self.wait_for_upper_past(
835 as_of,
836 &mut watch,
837 None,
838 &self.applier.metrics.retries.snapshot,
839 RetryParameters::persist_defaults(),
840 )
841 .await;
842 match self.applier.snapshot(as_of) {
843 Ok(data) => Ok(data),
844 Err(SnapshotErr::AsOfHistoricalDistinctionsLost(since)) => Err(since),
845 Err(SnapshotErr::AsOfNotYetAvailable(seqno, upper)) => {
846 panic!(
847 "waited for upper past {as_of:?}, but at latest seqno {seqno:?} the frontier was only {upper:?}",
848 as_of = as_of.elements(),
849 upper = upper.0.elements(),
850 )
851 }
852 }
853 }
854
855 pub fn verify_listen(&self, as_of: &Antichain<T>) -> Result<(), Since<T>> {
857 self.applier.verify_listen(as_of)
858 }
859
860 pub async fn wait_for_upper_past(
861 &self,
862 frontier: &Antichain<T>,
863 watch: &mut StateWatch<K, V, T, D>,
864 reader_id: Option<&LeasedReaderId>,
865 metrics: &RetryMetrics,
866 retry: RetryParameters,
867 ) {
868 let start = Instant::now();
869 let wait_for_seqno_past = self.applier.upper(|seqno, upper| {
870 if PartialOrder::less_than(frontier, upper) {
871 None
872 } else {
873 Some((seqno, upper.clone()))
874 }
875 });
876 let Some((mut seqno, mut upper)) = wait_for_seqno_past else {
877 return;
879 };
880
881 let sleeps = metrics.stream(retry.into_retry(SystemTime::now()).into_retry_stream());
884
885 enum Wake<'a, K, V, T, D> {
886 Watch(&'a mut StateWatch<K, V, T, D>),
887 Sleep(MetricsRetryStream),
888 }
889 let mut watch_fut = std::pin::pin!(
890 watch
891 .wait_for_upper_past(frontier)
892 .map(Wake::Watch)
893 .instrument(trace_span!("snapshot::watch"))
894 );
895 let mut sleep_fut = std::pin::pin!(
896 sleeps
897 .sleep()
898 .map(Wake::Sleep)
899 .instrument(trace_span!("snapshot::sleep"))
900 );
901
902 let mut logged_at_info = false;
906 loop {
907 if !logged_at_info
911 && start.elapsed() >= Duration::from_millis(1024)
912 && metrics.name.as_str() == "snapshot"
913 {
914 logged_at_info = true;
915 info!(
916 shard_id =? self.shard_id(),
917 shard_name =? self.applier.shard_metrics.name,
918 reader_id =? reader_id,
919 wait_frontier =? frontier.elements(),
920 current_upper =? upper.elements(),
921 current_seqno =? seqno,
922 wait_for = &metrics.name,
923 "desired upper not yet available",
924 );
925 } else {
926 debug!(
927 shard_id =? self.shard_id(),
928 shard_name =? self.applier.shard_metrics.name,
929 reader_id =? reader_id,
930 wait_frontier =? frontier.elements(),
931 current_upper =? upper.elements(),
932 current_seqno =? seqno,
933 wait_for = &metrics.name,
934 "desired upper not yet available",
935 );
936 }
937
938 let wake = match future::select(watch_fut.as_mut(), sleep_fut.as_mut()).await {
939 future::Either::Left((wake, _)) => wake,
940 future::Either::Right((wake, _)) => wake,
941 };
942 match &wake {
946 Wake::Watch(_) => self.applier.metrics.watch.wait_woken_via_watch.inc(),
947 Wake::Sleep(_) => {
948 self.applier.metrics.watch.wait_woken_via_sleep.inc();
949 self.applier.fetch_and_update_state(Some(seqno)).await;
950 }
951 }
952
953 let wait_for_seqno_past = self.applier.upper(|seqno, upper| {
954 if PartialOrder::less_than(frontier, upper) {
955 None
956 } else {
957 Some((seqno, upper.clone()))
958 }
959 });
960 match wait_for_seqno_past {
961 None => {
962 match &wake {
963 Wake::Watch(_) => self.applier.metrics.watch.wait_resolved_via_watch.inc(),
964 Wake::Sleep(_) => self.applier.metrics.watch.wait_resolved_via_sleep.inc(),
965 }
966 return;
967 }
968 Some((s, u)) => {
969 seqno = s;
970 upper = u;
971 }
972 };
973
974 match wake {
977 Wake::Watch(watch) => {
978 watch_fut.set(
979 watch
980 .wait_for_upper_past(frontier)
981 .map(Wake::Watch)
982 .instrument(trace_span!("snapshot::watch")),
983 );
984 }
985 Wake::Sleep(sleeps) => {
986 debug!(
987 shard_id =? self.shard_id(),
988 shard_name =? self.applier.shard_metrics.name,
989 reader_id =? reader_id,
990 wait_frontier =? frontier.elements(),
991 current_upper =? upper.elements(),
992 current_seqno =? seqno,
993 wait_for = &metrics.name,
994 "didn't find new data, retrying in {:?}",
995 sleeps.next_sleep(),
996 );
997 sleep_fut.set(
998 sleeps
999 .sleep()
1000 .map(Wake::Sleep)
1001 .instrument(trace_span!("snapshot::sleep")),
1002 );
1003 }
1004 }
1005 }
1006 }
1007
1008 async fn apply_unbatched_idempotent_cmd<
1009 R,
1010 WorkFn: FnMut(
1011 SeqNo,
1012 &PersistConfig,
1013 &mut StateCollections<T>,
1014 ) -> ControlFlow<NoOpStateTransition<R>, R>,
1015 >(
1016 &self,
1017 cmd: &CmdMetrics,
1018 mut work_fn: WorkFn,
1019 ) -> (SeqNo, R, RoutineMaintenance) {
1020 let mut retry = self
1021 .applier
1022 .metrics
1023 .retries
1024 .idempotent_cmd
1025 .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
1026 loop {
1027 match self.applier.apply_unbatched_cmd(cmd, &mut work_fn).await {
1028 Ok((seqno, x, maintenance)) => match x {
1029 Ok(x) => {
1030 return (seqno, x, maintenance);
1031 }
1032 Err(NoOpStateTransition(x)) => {
1033 return (seqno, x, maintenance);
1034 }
1035 },
1036 Err(err) => {
1037 if retry.attempt() >= INFO_MIN_ATTEMPTS {
1038 info!(
1039 "apply_unbatched_idempotent_cmd {} received an indeterminate error, retrying in {:?}: {}",
1040 cmd.name,
1041 retry.next_sleep(),
1042 err
1043 );
1044 } else {
1045 debug!(
1046 "apply_unbatched_idempotent_cmd {} received an indeterminate error, retrying in {:?}: {}",
1047 cmd.name,
1048 retry.next_sleep(),
1049 err
1050 );
1051 }
1052 retry = retry.sleep().await;
1053 continue;
1054 }
1055 }
1056 }
1057 }
1058}
1059
1060impl<K, V, T, D> Machine<K, V, T, D>
1061where
1062 K: Debug + Codec,
1063 V: Debug + Codec,
1064 T: Timestamp + Lattice + Codec64 + Sync,
1065 D: Monoid + Codec64 + PartialEq,
1066{
1067 pub async fn merge_res(
1068 &self,
1069 res: &FueledMergeRes<T>,
1070 ) -> (ApplyMergeResult, RoutineMaintenance) {
1071 let metrics = Arc::clone(&self.applier.metrics);
1072
1073 let mut merge_result_ever_applied = ApplyMergeResult::NotAppliedNoMatch;
1102 let (_seqno, _apply_merge_result, maintenance) = self
1103 .apply_unbatched_idempotent_cmd(&metrics.cmds.merge_res, |_, _, state| {
1104 let ret = state.apply_merge_res::<D>(res, &Arc::clone(&metrics).columnar);
1105 if let Continue(result) = ret {
1106 if result.applied() {
1108 merge_result_ever_applied = result;
1109 }
1110 if result.matched() && !result.applied() && !merge_result_ever_applied.applied()
1113 {
1114 merge_result_ever_applied = result;
1115 }
1116 }
1117 ret
1118 })
1119 .await;
1120 (merge_result_ever_applied, maintenance)
1121 }
1122}
1123
1124pub(crate) struct ExpireFn(
1125 pub(crate) Box<dyn FnOnce() -> BoxFuture<'static, ()> + Send + Sync + 'static>,
1130);
1131
1132impl Debug for ExpireFn {
1133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134 f.debug_struct("ExpireFn").finish_non_exhaustive()
1135 }
1136}
1137
1138#[derive(Debug)]
1139pub(crate) enum CompareAndAppendRes<T> {
1140 Success(SeqNo, WriterMaintenance<T>),
1141 InvalidUsage(InvalidUsage<T>),
1142 UpperMismatch(SeqNo, Antichain<T>),
1143 InlineBackpressure,
1144}
1145
1146#[cfg(test)]
1147impl<T: Debug> CompareAndAppendRes<T> {
1148 #[track_caller]
1149 fn unwrap(self) -> (SeqNo, WriterMaintenance<T>) {
1150 match self {
1151 CompareAndAppendRes::Success(seqno, maintenance) => (seqno, maintenance),
1152 x => panic!("{:?}", x),
1153 }
1154 }
1155}
1156
1157pub(crate) const NEXT_LISTEN_BATCH_RETRYER_FIXED_SLEEP: Config<Duration> = Config::new(
1158 "persist_next_listen_batch_retryer_fixed_sleep",
1159 Duration::from_millis(1200), "\
1161 The fixed sleep when polling for new batches from a Listen or Subscribe. Skipped if zero.",
1162 ParameterScope::Environment,
1163);
1164
1165pub(crate) const NEXT_LISTEN_BATCH_RETRYER_INITIAL_BACKOFF: Config<Duration> = Config::new(
1166 "persist_next_listen_batch_retryer_initial_backoff",
1167 Duration::from_millis(100), "The initial backoff when polling for new batches from a Listen or Subscribe.",
1169 ParameterScope::Environment,
1170);
1171
1172pub(crate) const NEXT_LISTEN_BATCH_RETRYER_MULTIPLIER: Config<u32> = Config::new(
1173 "persist_next_listen_batch_retryer_multiplier",
1174 2,
1175 "The backoff multiplier when polling for new batches from a Listen or Subscribe.",
1176 ParameterScope::Environment,
1177);
1178
1179pub(crate) const NEXT_LISTEN_BATCH_RETRYER_CLAMP: Config<Duration> = Config::new(
1180 "persist_next_listen_batch_retryer_clamp",
1181 Duration::from_secs(16), "The backoff clamp duration when polling for new batches from a Listen or Subscribe.",
1183 ParameterScope::Environment,
1184);
1185
1186pub(crate) fn next_listen_batch_retry_params(cfg: &ConfigSet) -> RetryParameters {
1187 RetryParameters {
1188 fixed_sleep: NEXT_LISTEN_BATCH_RETRYER_FIXED_SLEEP.get(cfg),
1189 initial_backoff: NEXT_LISTEN_BATCH_RETRYER_INITIAL_BACKOFF.get(cfg),
1190 multiplier: NEXT_LISTEN_BATCH_RETRYER_MULTIPLIER.get(cfg),
1191 clamp: NEXT_LISTEN_BATCH_RETRYER_CLAMP.get(cfg),
1192 }
1193}
1194
1195pub const INFO_MIN_ATTEMPTS: usize = 3;
1196
1197pub const WARN_MIN_ATTEMPTS: usize = 30;
1202
1203pub async fn retry_external<R, F, WorkFn>(metrics: &RetryMetrics, mut work_fn: WorkFn) -> R
1204where
1205 F: std::future::Future<Output = Result<R, ExternalError>>,
1206 WorkFn: FnMut() -> F,
1207{
1208 let mut retry = metrics.stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
1209 loop {
1210 match work_fn().await {
1211 Ok(x) => {
1212 if retry.attempt() > 0 {
1213 debug!(
1214 "external operation {} succeeded after failing at least once",
1215 metrics.name,
1216 );
1217 }
1218 return x;
1219 }
1220 Err(err) => {
1221 if retry.attempt() >= WARN_MIN_ATTEMPTS {
1222 warn!(
1223 "external operation {} has failed {} times, retrying in {:?}: {}",
1224 metrics.name,
1225 retry.attempt(),
1226 retry.next_sleep(),
1227 err.display_with_causes()
1228 );
1229 } else if retry.attempt() >= INFO_MIN_ATTEMPTS {
1230 info!(
1231 "external operation {} failed, retrying in {:?}: {}",
1232 metrics.name,
1233 retry.next_sleep(),
1234 err.display_with_causes()
1235 );
1236 } else {
1237 debug!(
1238 "external operation {} failed, retrying in {:?}: {}",
1239 metrics.name,
1240 retry.next_sleep(),
1241 err.display_with_causes()
1242 );
1243 }
1244 retry = retry.sleep().await;
1245 }
1246 }
1247 }
1248}
1249
1250pub async fn retry_determinate<R, F, WorkFn>(
1251 metrics: &RetryMetrics,
1252 mut work_fn: WorkFn,
1253) -> Result<R, Indeterminate>
1254where
1255 F: std::future::Future<Output = Result<R, ExternalError>>,
1256 WorkFn: FnMut() -> F,
1257{
1258 let mut retry = metrics.stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
1259 loop {
1260 match work_fn().await {
1261 Ok(x) => {
1262 if retry.attempt() > 0 {
1263 debug!(
1264 "external operation {} succeeded after failing at least once",
1265 metrics.name,
1266 );
1267 }
1268 return Ok(x);
1269 }
1270 Err(ExternalError::Determinate(err)) => {
1271 debug!(
1280 "external operation {} failed, retrying in {:?}: {}",
1281 metrics.name,
1282 retry.next_sleep(),
1283 err.display_with_causes()
1284 );
1285 retry = retry.sleep().await;
1286 continue;
1287 }
1288 Err(ExternalError::Indeterminate(x)) => return Err(x),
1289 }
1290 }
1291}
1292
1293#[cfg(test)]
1294pub mod datadriven {
1295 use std::collections::{BTreeMap, BTreeSet};
1296 use std::pin::pin;
1297 use std::sync::{Arc, LazyLock};
1298
1299 use anyhow::anyhow;
1300 use differential_dataflow::consolidation::consolidate_updates;
1301 use differential_dataflow::trace::Description;
1302 use futures::StreamExt;
1303 use mz_dyncfg::{ConfigUpdates, ConfigVal};
1304 use mz_persist::indexed::encoding::BlobTraceBatchPart;
1305 use mz_persist_types::codec_impls::{StringSchema, UnitSchema};
1306
1307 use crate::batch::{
1308 BLOB_TARGET_SIZE, Batch, BatchBuilder, BatchBuilderConfig, BatchBuilderInternal,
1309 BatchParts, validate_truncate_batch,
1310 };
1311 use crate::cfg::COMPACTION_MEMORY_BOUND_BYTES;
1312 use crate::fetch::{EncodedPart, FetchConfig};
1313 use crate::internal::compact::{CompactConfig, CompactReq, Compactor};
1314 use crate::internal::datadriven::DirectiveArgs;
1315 use crate::internal::encoding::Schemas;
1316 use crate::internal::gc::GcReq;
1317 use crate::internal::paths::{BlobKey, BlobKeyPrefix, PartialBlobKey};
1318 use crate::internal::state::{BatchPart, RunOrder, RunPart, Upper};
1319 use crate::internal::state_versions::EncodedRollup;
1320 use crate::internal::trace::{CompactionInput, IdHollowBatch, SpineId};
1321 use crate::read::{Listen, ListenEvent, READER_LEASE_DURATION};
1322 use crate::rpc::NoopPubSubSender;
1323 use crate::tests::new_test_client;
1324 use crate::write::COMBINE_INLINE_WRITES;
1325 use crate::{GarbageCollector, PersistClient};
1326
1327 use super::*;
1328
1329 static SCHEMAS: LazyLock<Schemas<String, ()>> = LazyLock::new(|| Schemas {
1330 id: Some(SchemaId(0)),
1331 key: Arc::new(StringSchema),
1332 val: Arc::new(UnitSchema),
1333 });
1334
1335 #[derive(Debug)]
1337 pub struct MachineState {
1338 pub client: PersistClient,
1339 pub shard_id: ShardId,
1340 pub state_versions: Arc<StateVersions>,
1341 pub machine: Machine<String, (), u64, i64>,
1342 pub batches: BTreeMap<String, IdHollowBatch<u64>>,
1343 pub next_id: usize,
1344 pub rollups: BTreeMap<String, EncodedRollup>,
1345 pub listens: BTreeMap<String, Listen<String, (), u64, i64>>,
1346 pub routine: Vec<RoutineMaintenance>,
1347 pub compactions: BTreeMap<String, CompactReq<u64>>,
1348 }
1349
1350 impl MachineState {
1351 pub async fn new(dyncfgs: &ConfigUpdates) -> Self {
1352 let shard_id = ShardId::new();
1353 let client = new_test_client(dyncfgs).await;
1354 client
1357 .cfg
1358 .set_config(&BLOB_TARGET_SIZE, *BLOB_TARGET_SIZE.default());
1359 client.cfg.set_config(&COMBINE_INLINE_WRITES, false);
1363 let state_versions = Arc::new(StateVersions::new(
1364 client.cfg.clone(),
1365 Arc::clone(&client.consensus),
1366 Arc::clone(&client.blob),
1367 Arc::clone(&client.metrics),
1368 ));
1369 let machine = Machine::new(
1370 client.cfg.clone(),
1371 shard_id,
1372 Arc::clone(&client.metrics),
1373 Arc::clone(&state_versions),
1374 Arc::clone(&client.shared_states),
1375 Arc::new(NoopPubSubSender),
1376 Arc::clone(&client.isolated_runtime),
1377 Diagnostics::for_tests(),
1378 )
1379 .await
1380 .expect("codecs should match");
1381 MachineState {
1382 shard_id,
1383 client,
1384 state_versions,
1385 machine,
1386 batches: BTreeMap::default(),
1387 rollups: BTreeMap::default(),
1388 listens: BTreeMap::default(),
1389 routine: Vec::new(),
1390 compactions: BTreeMap::default(),
1391 next_id: 0,
1392 }
1393 }
1394
1395 fn to_batch(&self, hollow: HollowBatch<u64>) -> Batch<String, (), u64, i64> {
1396 Batch::new(
1397 true,
1398 Arc::clone(&self.client.metrics),
1399 Arc::clone(&self.client.blob),
1400 self.client.metrics.shards.shard(&self.shard_id, "test"),
1401 self.client.cfg.build_version.clone(),
1402 (
1403 <String>::encode_schema(&*SCHEMAS.key),
1404 <()>::encode_schema(&*SCHEMAS.val),
1405 ),
1406 hollow,
1407 )
1408 }
1409 }
1410
1411 pub async fn consensus_scan(
1414 datadriven: &MachineState,
1415 args: DirectiveArgs<'_>,
1416 ) -> Result<String, anyhow::Error> {
1417 let from = args.expect("from_seqno");
1418
1419 let mut states = datadriven
1420 .state_versions
1421 .fetch_all_live_states::<u64>(datadriven.shard_id)
1422 .await
1423 .expect("should only be called on an initialized shard")
1424 .check_ts_codec()
1425 .expect("shard codecs should not change");
1426 let mut s = String::new();
1427 while let Some(x) = states.next(|_| {}) {
1428 if x.seqno < from {
1429 continue;
1430 }
1431 let rollups: Vec<_> = x
1432 .collections
1433 .rollups
1434 .keys()
1435 .map(|seqno| seqno.to_string())
1436 .collect();
1437 let batches: Vec<_> = x
1438 .collections
1439 .trace
1440 .batches()
1441 .filter(|b| !b.is_empty())
1442 .filter_map(|b| {
1443 datadriven
1444 .batches
1445 .iter()
1446 .find(|(_, original_batch)| original_batch.batch.parts == b.parts)
1447 .map(|(batch_name, _)| batch_name.to_owned())
1448 })
1449 .collect();
1450 write!(
1451 s,
1452 "seqno={} batches={} rollups={}\n",
1453 x.seqno,
1454 batches.join(","),
1455 rollups.join(","),
1456 );
1457 }
1458 Ok(s)
1459 }
1460
1461 pub async fn consensus_truncate(
1462 datadriven: &MachineState,
1463 args: DirectiveArgs<'_>,
1464 ) -> Result<String, anyhow::Error> {
1465 let to = args.expect("to_seqno");
1466 let removed = datadriven
1467 .client
1468 .consensus
1469 .truncate(&datadriven.shard_id.to_string(), to)
1470 .await
1471 .expect("valid truncation");
1472 Ok(format!("{:?}\n", removed))
1473 }
1474
1475 pub async fn blob_scan_batches(
1476 datadriven: &MachineState,
1477 _args: DirectiveArgs<'_>,
1478 ) -> Result<String, anyhow::Error> {
1479 let key_prefix = BlobKeyPrefix::Shard(&datadriven.shard_id).to_string();
1480
1481 let mut s = String::new();
1482 let () = datadriven
1483 .state_versions
1484 .blob
1485 .list_keys_and_metadata(&key_prefix, &mut |x| {
1486 let (_, key) = BlobKey::parse_ids(x.key).expect("key should be valid");
1487 if let PartialBlobKey::Batch(_, _) = key {
1488 write!(s, "{}: {}b\n", x.key, x.size_in_bytes);
1489 }
1490 })
1491 .await?;
1492 Ok(s)
1493 }
1494
1495 pub async fn blob_scan_rollups(
1498 datadriven: &MachineState,
1499 _args: DirectiveArgs<'_>,
1500 ) -> Result<String, anyhow::Error> {
1501 let key_prefix = BlobKeyPrefix::Shard(&datadriven.shard_id).to_string();
1502
1503 let mut seqnos = vec![];
1505 let () = datadriven
1506 .state_versions
1507 .blob
1508 .list_keys_and_metadata(&key_prefix, &mut |x| {
1509 let (_, key) = BlobKey::parse_ids(x.key).expect("key should be valid");
1510 if let PartialBlobKey::Rollup(seqno, _) = key {
1511 seqnos.push(seqno);
1512 }
1513 })
1514 .await?;
1515 seqnos.sort();
1516 Ok(seqnos.iter().map(|seqno| format!("{seqno}\n")).collect())
1517 }
1518
1519 #[allow(clippy::unused_async)]
1520 pub async fn shard_desc(
1521 datadriven: &MachineState,
1522 _args: DirectiveArgs<'_>,
1523 ) -> Result<String, anyhow::Error> {
1524 Ok(format!(
1525 "since={:?} upper={:?}\n",
1526 datadriven.machine.applier.since().elements(),
1527 datadriven.machine.applier.clone_upper().elements()
1528 ))
1529 }
1530
1531 pub async fn downgrade_since(
1532 datadriven: &mut MachineState,
1533 args: DirectiveArgs<'_>,
1534 ) -> Result<String, anyhow::Error> {
1535 let since = args.expect_antichain("since");
1536 let seqno = args
1537 .optional("seqno")
1538 .unwrap_or_else(|| datadriven.machine.seqno());
1539 let reader_id = args.expect("reader_id");
1540 let (_, since, routine) = datadriven
1541 .machine
1542 .downgrade_since(&reader_id, seqno, &since)
1543 .await;
1544 datadriven.routine.push(routine);
1545 Ok(format!(
1546 "{} {:?}\n",
1547 datadriven.machine.seqno(),
1548 since.0.elements()
1549 ))
1550 }
1551
1552 #[allow(clippy::unused_async)]
1553 pub async fn dyncfg(
1554 datadriven: &MachineState,
1555 args: DirectiveArgs<'_>,
1556 ) -> Result<String, anyhow::Error> {
1557 let mut updates = ConfigUpdates::default();
1558 for x in args.input.trim().split('\n') {
1559 match x.split(' ').collect::<Vec<_>>().as_slice() {
1560 &[name, val] => {
1561 let config = datadriven
1562 .client
1563 .cfg
1564 .entries()
1565 .find(|x| x.name() == name)
1566 .ok_or_else(|| anyhow!("unknown dyncfg: {}", name))?;
1567 match config.val() {
1568 ConfigVal::Usize(_) => {
1569 let val = val.parse().map_err(anyhow::Error::new)?;
1570 updates.add_dynamic(name, ConfigVal::Usize(val));
1571 }
1572 ConfigVal::Bool(_) => {
1573 let val = val.parse().map_err(anyhow::Error::new)?;
1574 updates.add_dynamic(name, ConfigVal::Bool(val));
1575 }
1576 x => unimplemented!("dyncfg type: {:?}", x),
1577 }
1578 }
1579 x => return Err(anyhow!("expected `name val` got: {:?}", x)),
1580 }
1581 }
1582 updates.apply(&datadriven.client.cfg);
1583
1584 Ok("ok\n".to_string())
1585 }
1586
1587 pub async fn compare_and_downgrade_since(
1588 datadriven: &mut MachineState,
1589 args: DirectiveArgs<'_>,
1590 ) -> Result<String, anyhow::Error> {
1591 let expected_opaque: u64 = args.expect("expect_opaque");
1592 let new_opaque: u64 = args.expect("opaque");
1593 let new_since = args.expect_antichain("since");
1594 let reader_id = args.expect("reader_id");
1595 let (res, routine) = datadriven
1596 .machine
1597 .compare_and_downgrade_since(
1598 &reader_id,
1599 &Opaque::encode(&expected_opaque),
1600 (&Opaque::encode(&new_opaque), &new_since),
1601 )
1602 .await;
1603 datadriven.routine.push(routine);
1604 let since = res.map_err(|(opaque, since)| {
1605 anyhow!(
1606 "mismatch: opaque={} since={:?}",
1607 opaque.decode::<u64>(),
1608 since.0.elements()
1609 )
1610 })?;
1611 Ok(format!(
1612 "{} {} {:?}\n",
1613 datadriven.machine.seqno(),
1614 new_opaque,
1615 since.0.elements()
1616 ))
1617 }
1618
1619 pub async fn write_rollup(
1620 datadriven: &mut MachineState,
1621 args: DirectiveArgs<'_>,
1622 ) -> Result<String, anyhow::Error> {
1623 let output = args.expect_str("output");
1624
1625 let rollup = datadriven
1626 .machine
1627 .applier
1628 .write_rollup_for_state()
1629 .await
1630 .expect("rollup");
1631
1632 datadriven
1633 .rollups
1634 .insert(output.to_string(), rollup.clone());
1635
1636 Ok(format!(
1637 "state={} diffs=[{}, {})\n",
1638 rollup.seqno,
1639 rollup._desc.lower().first().expect("seqno"),
1640 rollup._desc.upper().first().expect("seqno"),
1641 ))
1642 }
1643
1644 pub async fn add_rollup(
1645 datadriven: &mut MachineState,
1646 args: DirectiveArgs<'_>,
1647 ) -> Result<String, anyhow::Error> {
1648 let input = args.expect_str("input");
1649 let rollup = datadriven
1650 .rollups
1651 .get(input)
1652 .expect("unknown batch")
1653 .clone();
1654
1655 let (applied, maintenance) = datadriven
1656 .machine
1657 .add_rollup((rollup.seqno, &rollup.to_hollow()))
1658 .await;
1659
1660 if !applied {
1661 return Err(anyhow!("failed to apply rollup for: {}", rollup.seqno));
1662 }
1663
1664 datadriven.routine.push(maintenance);
1665 Ok(format!("{}\n", datadriven.machine.seqno()))
1666 }
1667
1668 pub async fn write_batch(
1669 datadriven: &mut MachineState,
1670 args: DirectiveArgs<'_>,
1671 ) -> Result<String, anyhow::Error> {
1672 let output = args.expect_str("output");
1673 let lower = args.expect_antichain("lower");
1674 let upper = args.expect_antichain("upper");
1675 assert!(PartialOrder::less_than(&lower, &upper));
1676 let since = args
1677 .optional_antichain("since")
1678 .unwrap_or_else(|| Antichain::from_elem(0));
1679 let target_size = args.optional("target_size");
1680 let parts_size_override = args.optional("parts_size_override");
1681 let consolidate = args.optional("consolidate").unwrap_or(true);
1682 let mut updates: Vec<_> = args
1683 .input
1684 .split('\n')
1685 .flat_map(DirectiveArgs::parse_update)
1686 .collect();
1687
1688 let mut cfg = BatchBuilderConfig::new(&datadriven.client.cfg, datadriven.shard_id);
1689 if let Some(target_size) = target_size {
1690 cfg.blob_target_size = target_size;
1691 };
1692 if consolidate {
1693 consolidate_updates(&mut updates);
1694 }
1695 let run_order = if consolidate {
1696 cfg.preferred_order
1697 } else {
1698 RunOrder::Unordered
1699 };
1700 let parts = BatchParts::new_ordered::<i64>(
1701 cfg.clone(),
1702 run_order,
1703 Arc::clone(&datadriven.client.metrics),
1704 Arc::clone(&datadriven.machine.applier.shard_metrics),
1705 datadriven.shard_id,
1706 Arc::clone(&datadriven.client.blob),
1707 Arc::clone(&datadriven.client.isolated_runtime),
1708 &datadriven.client.metrics.user,
1709 );
1710 let builder = BatchBuilderInternal::new(
1711 cfg.clone(),
1712 parts,
1713 Arc::clone(&datadriven.client.metrics),
1714 SCHEMAS.clone(),
1715 Arc::clone(&datadriven.client.blob),
1716 datadriven.shard_id.clone(),
1717 datadriven.client.cfg.build_version.clone(),
1718 );
1719 let mut builder = BatchBuilder::new(builder, Description::new(lower, upper.clone(), since));
1720 for ((k, ()), t, d) in updates {
1721 builder.add(&k, &(), &t, &d).await.expect("invalid batch");
1722 }
1723 let mut batch = builder.finish(upper).await?;
1724 if parts_size_override.is_some() {
1727 batch
1728 .flush_to_blob(
1729 &cfg,
1730 &datadriven.client.metrics.user,
1731 &datadriven.client.isolated_runtime,
1732 &SCHEMAS,
1733 )
1734 .await;
1735 }
1736 let batch = batch.into_hollow_batch();
1737 let batch = IdHollowBatch {
1738 batch: Arc::new(batch),
1739 id: SpineId(datadriven.next_id, datadriven.next_id + 1),
1740 };
1741 datadriven.next_id += 1;
1742
1743 if let Some(size) = parts_size_override {
1744 let mut batch = batch.clone();
1745 let mut hollow_batch = (*batch.batch).clone();
1746 for part in hollow_batch.parts.iter_mut() {
1747 match part {
1748 RunPart::Many(run) => run.max_part_bytes = size,
1749 RunPart::Single(BatchPart::Hollow(part)) => part.encoded_size_bytes = size,
1750 RunPart::Single(BatchPart::Inline { .. }) => unreachable!("flushed out above"),
1751 }
1752 }
1753 batch.batch = Arc::new(hollow_batch);
1754 datadriven.batches.insert(output.to_owned(), batch);
1755 } else {
1756 datadriven.batches.insert(output.to_owned(), batch.clone());
1757 }
1758 Ok(format!(
1759 "parts={} len={}\n",
1760 batch.batch.part_count(),
1761 batch.batch.len
1762 ))
1763 }
1764
1765 pub async fn fetch_batch(
1766 datadriven: &MachineState,
1767 args: DirectiveArgs<'_>,
1768 ) -> Result<String, anyhow::Error> {
1769 let input = args.expect_str("input");
1770 let stats = args.optional_str("stats");
1771 let batch = datadriven.batches.get(input).expect("unknown batch");
1772
1773 let mut s = String::new();
1774 let mut stream = pin!(
1775 batch
1776 .batch
1777 .part_stream(
1778 datadriven.shard_id,
1779 &*datadriven.state_versions.blob,
1780 &*datadriven.state_versions.metrics
1781 )
1782 .enumerate()
1783 );
1784 while let Some((idx, part)) = stream.next().await {
1785 let part = &*part?;
1786 write!(s, "<part {idx}>\n");
1787
1788 let lower = match part {
1789 BatchPart::Inline { updates, .. } => {
1790 let updates: BlobTraceBatchPart<u64> =
1791 updates.decode(&datadriven.client.metrics.columnar)?;
1792 updates.structured_key_lower()
1793 }
1794 other @ BatchPart::Hollow(_) => other.structured_key_lower(),
1795 };
1796
1797 if let Some(lower) = lower {
1798 if stats == Some("lower") {
1799 writeln!(s, "<key lower={}>", lower.get())
1800 }
1801 }
1802
1803 match part {
1804 BatchPart::Hollow(part) => {
1805 let blob_batch = datadriven
1806 .client
1807 .blob
1808 .get(&part.key.complete(&datadriven.shard_id))
1809 .await;
1810 match blob_batch {
1811 Ok(Some(_)) | Err(_) => {}
1812 Ok(None) => {
1815 s.push_str("<empty>\n");
1816 continue;
1817 }
1818 };
1819 }
1820 BatchPart::Inline { .. } => {}
1821 };
1822 let part = EncodedPart::fetch(
1823 &FetchConfig::from_persist_config(&datadriven.client.cfg),
1824 &datadriven.shard_id,
1825 datadriven.client.blob.as_ref(),
1826 datadriven.client.metrics.as_ref(),
1827 datadriven.machine.applier.shard_metrics.as_ref(),
1828 &datadriven.client.metrics.read.batch_fetcher,
1829 &batch.batch.desc,
1830 part,
1831 )
1832 .await
1833 .expect("invalid batch part");
1834 let part = part
1835 .normalize(&datadriven.client.metrics.columnar)
1836 .into_part::<String, ()>(&*SCHEMAS.key, &*SCHEMAS.val);
1837
1838 for ((k, _v), t, d) in part
1839 .decode_iter::<_, _, u64, i64>(&*SCHEMAS.key, &*SCHEMAS.val)
1840 .expect("valid schemas")
1841 {
1842 writeln!(s, "{k} {t} {d}");
1843 }
1844 }
1845 if !s.is_empty() {
1846 for (idx, (_meta, run)) in batch.batch.runs().enumerate() {
1847 write!(s, "<run {idx}>\n");
1848 for part in run {
1849 let part_idx = batch
1850 .batch
1851 .parts
1852 .iter()
1853 .position(|p| p == part)
1854 .expect("part should exist");
1855 write!(s, "part {part_idx}\n");
1856 }
1857 }
1858 }
1859 Ok(s)
1860 }
1861
1862 #[allow(clippy::unused_async)]
1863 pub async fn truncate_batch_desc(
1864 datadriven: &mut MachineState,
1865 args: DirectiveArgs<'_>,
1866 ) -> Result<String, anyhow::Error> {
1867 let input = args.expect_str("input");
1868 let output = args.expect_str("output");
1869 let lower = args.expect_antichain("lower");
1870 let upper = args.expect_antichain("upper");
1871
1872 let batch = datadriven
1873 .batches
1874 .get(input)
1875 .expect("unknown batch")
1876 .clone();
1877 let truncated_desc = Description::new(lower, upper, batch.batch.desc.since().clone());
1878 let bounds_truncated = validate_truncate_batch(&batch.batch, &truncated_desc, false, true)?;
1879 let mut new_hollow_batch = (*batch.batch).clone();
1880 if bounds_truncated {
1881 for run_meta in &mut new_hollow_batch.run_meta {
1882 run_meta.set_bounds_truncated();
1883 }
1884 }
1885 new_hollow_batch.desc = truncated_desc;
1886 let new_batch = IdHollowBatch {
1887 batch: Arc::new(new_hollow_batch),
1888 id: batch.id,
1889 };
1890 datadriven
1891 .batches
1892 .insert(output.to_owned(), new_batch.clone());
1893 Ok(format!(
1894 "parts={} len={}\n",
1895 batch.batch.part_count(),
1896 batch.batch.len
1897 ))
1898 }
1899
1900 #[allow(clippy::unused_async)]
1901 pub async fn set_batch_parts_size(
1902 datadriven: &mut MachineState,
1903 args: DirectiveArgs<'_>,
1904 ) -> Result<String, anyhow::Error> {
1905 let input = args.expect_str("input");
1906 let size = args.expect("size");
1907 let batch = datadriven.batches.get_mut(input).expect("unknown batch");
1908 let mut hollow_batch = (*batch.batch).clone();
1909 for part in hollow_batch.parts.iter_mut() {
1910 match part {
1911 RunPart::Single(BatchPart::Hollow(x)) => x.encoded_size_bytes = size,
1912 _ => {
1913 panic!("set_batch_parts_size only supports hollow parts")
1914 }
1915 }
1916 }
1917 batch.batch = Arc::new(hollow_batch);
1918 Ok("ok\n".to_string())
1919 }
1920
1921 pub async fn compact(
1922 datadriven: &mut MachineState,
1923 args: DirectiveArgs<'_>,
1924 ) -> Result<String, anyhow::Error> {
1925 let output = args.expect_str("output");
1926 let lower = args.expect_antichain("lower");
1927 let upper = args.expect_antichain("upper");
1928 let since = args.expect_antichain("since");
1929 let target_size = args.optional("target_size");
1930 let memory_bound = args.optional("memory_bound");
1931
1932 let mut inputs = Vec::new();
1933 for input in args.args.get("inputs").expect("missing inputs") {
1934 inputs.push(
1935 datadriven
1936 .batches
1937 .get(input)
1938 .expect("unknown batch")
1939 .clone(),
1940 );
1941 }
1942
1943 let cfg = datadriven.client.cfg.clone();
1944 if let Some(target_size) = target_size {
1945 cfg.set_config(&BLOB_TARGET_SIZE, target_size);
1946 };
1947 if let Some(memory_bound) = memory_bound {
1948 cfg.set_config(&COMPACTION_MEMORY_BOUND_BYTES, memory_bound);
1949 }
1950 let req = CompactReq {
1951 shard_id: datadriven.shard_id,
1952 desc: Description::new(lower, upper, since),
1953 inputs: inputs.clone(),
1954 };
1955 datadriven
1956 .compactions
1957 .insert(output.to_owned(), req.clone());
1958 let spine_lower = inputs
1959 .first()
1960 .map_or_else(|| datadriven.next_id, |x| x.id.0);
1961 let spine_upper = inputs.last().map_or_else(
1962 || {
1963 datadriven.next_id += 1;
1964 datadriven.next_id
1965 },
1966 |x| x.id.1,
1967 );
1968 let new_spine_id = SpineId(spine_lower, spine_upper);
1969 let res = Compactor::<String, (), u64, i64>::compact(
1970 CompactConfig::new(&cfg, datadriven.shard_id),
1971 Arc::clone(&datadriven.client.blob),
1972 Arc::clone(&datadriven.client.metrics),
1973 Arc::clone(&datadriven.machine.applier.shard_metrics),
1974 Arc::clone(&datadriven.client.isolated_runtime),
1975 req,
1976 SCHEMAS.clone(),
1977 )
1978 .await?;
1979
1980 let batch = IdHollowBatch {
1981 batch: Arc::new(res.output.clone()),
1982 id: new_spine_id,
1983 };
1984
1985 datadriven.batches.insert(output.to_owned(), batch.clone());
1986 Ok(format!(
1987 "parts={} len={}\n",
1988 res.output.part_count(),
1989 res.output.len
1990 ))
1991 }
1992
1993 pub async fn clear_blob(
1994 datadriven: &MachineState,
1995 _args: DirectiveArgs<'_>,
1996 ) -> Result<String, anyhow::Error> {
1997 let mut to_delete = vec![];
1998 datadriven
1999 .client
2000 .blob
2001 .list_keys_and_metadata("", &mut |meta| {
2002 to_delete.push(meta.key.to_owned());
2003 })
2004 .await?;
2005 for blob in &to_delete {
2006 datadriven.client.blob.delete(blob).await?;
2007 }
2008 Ok(format!("deleted={}\n", to_delete.len()))
2009 }
2010
2011 pub async fn restore_blob(
2012 datadriven: &MachineState,
2013 _args: DirectiveArgs<'_>,
2014 ) -> Result<String, anyhow::Error> {
2015 let not_restored = crate::internal::restore::restore_blob(
2016 &datadriven.state_versions,
2017 datadriven.client.blob.as_ref(),
2018 &datadriven.client.cfg.build_version,
2019 datadriven.shard_id,
2020 &*datadriven.state_versions.metrics,
2021 )
2022 .await?;
2023 let mut out = String::new();
2024 for key in not_restored {
2025 writeln!(&mut out, "{key}");
2026 }
2027 Ok(out)
2028 }
2029
2030 #[allow(clippy::unused_async)]
2031 pub async fn rewrite_ts(
2032 datadriven: &mut MachineState,
2033 args: DirectiveArgs<'_>,
2034 ) -> Result<String, anyhow::Error> {
2035 let input = args.expect_str("input");
2036 let ts_rewrite = args.expect_antichain("frontier");
2037 let upper = args.expect_antichain("upper");
2038
2039 let batch = datadriven.batches.get_mut(input).expect("unknown batch");
2040 let mut hollow_batch = (*batch.batch).clone();
2041 let () = hollow_batch
2042 .rewrite_ts(&ts_rewrite, upper)
2043 .map_err(|err| anyhow!("invalid rewrite: {}", err))?;
2044 batch.batch = Arc::new(hollow_batch);
2045 Ok("ok\n".into())
2046 }
2047
2048 pub async fn gc(
2049 datadriven: &mut MachineState,
2050 args: DirectiveArgs<'_>,
2051 ) -> Result<String, anyhow::Error> {
2052 let new_seqno_since = args.expect("to_seqno");
2053
2054 let req = GcReq {
2055 shard_id: datadriven.shard_id,
2056 new_seqno_since,
2057 };
2058 let (maintenance, stats) =
2059 GarbageCollector::gc_and_truncate(&datadriven.machine, req).await;
2060 datadriven.routine.push(maintenance);
2061
2062 Ok(format!(
2063 "{} batch_parts={} rollups={} truncated={} state_rollups={}\n",
2064 datadriven.machine.seqno(),
2065 stats.batch_parts_deleted_from_blob,
2066 stats.rollups_deleted_from_blob,
2067 stats
2068 .truncated_consensus_to
2069 .iter()
2070 .map(|x| x.to_string())
2071 .collect::<Vec<_>>()
2072 .join(","),
2073 stats
2074 .rollups_removed_from_state
2075 .iter()
2076 .map(|x| x.to_string())
2077 .collect::<Vec<_>>()
2078 .join(","),
2079 ))
2080 }
2081
2082 pub async fn snapshot(
2083 datadriven: &MachineState,
2084 args: DirectiveArgs<'_>,
2085 ) -> Result<String, anyhow::Error> {
2086 let as_of = args.expect_antichain("as_of");
2087 let snapshot = datadriven
2088 .machine
2089 .unleased_snapshot(&as_of)
2090 .await
2091 .map_err(|err| anyhow!("{:?}", err))?;
2092
2093 let mut result = String::new();
2094
2095 for batch in snapshot {
2096 writeln!(
2097 result,
2098 "<batch {:?}-{:?}>",
2099 batch.desc.lower().elements(),
2100 batch.desc.upper().elements()
2101 );
2102 for (run, (_meta, parts)) in batch.runs().enumerate() {
2103 writeln!(result, "<run {run}>");
2104 let mut stream = pin!(
2105 futures::stream::iter(parts)
2106 .flat_map(|part| part.part_stream(
2107 datadriven.shard_id,
2108 &*datadriven.state_versions.blob,
2109 &*datadriven.state_versions.metrics
2110 ))
2111 .enumerate()
2112 );
2113
2114 while let Some((idx, part)) = stream.next().await {
2115 let part = &*part?;
2116 writeln!(result, "<part {idx}>");
2117
2118 let part = EncodedPart::fetch(
2119 &FetchConfig::from_persist_config(&datadriven.client.cfg),
2120 &datadriven.shard_id,
2121 datadriven.client.blob.as_ref(),
2122 datadriven.client.metrics.as_ref(),
2123 datadriven.machine.applier.shard_metrics.as_ref(),
2124 &datadriven.client.metrics.read.batch_fetcher,
2125 &batch.desc,
2126 part,
2127 )
2128 .await
2129 .expect("invalid batch part");
2130 let part = part
2131 .normalize(&datadriven.client.metrics.columnar)
2132 .into_part::<String, ()>(&*SCHEMAS.key, &*SCHEMAS.val);
2133
2134 let mut updates = Vec::new();
2135
2136 for ((k, _v), mut t, d) in part
2137 .decode_iter::<_, _, u64, i64>(&*SCHEMAS.key, &*SCHEMAS.val)
2138 .expect("valid schemas")
2139 {
2140 t.advance_by(as_of.borrow());
2141 updates.push((k, t, d));
2142 }
2143
2144 consolidate_updates(&mut updates);
2145
2146 for (k, t, d) in updates {
2147 writeln!(result, "{k} {t} {d}");
2148 }
2149 }
2150 }
2151 }
2152
2153 Ok(result)
2154 }
2155
2156 pub async fn register_listen(
2157 datadriven: &mut MachineState,
2158 args: DirectiveArgs<'_>,
2159 ) -> Result<String, anyhow::Error> {
2160 let output = args.expect_str("output");
2161 let as_of = args.expect_antichain("as_of");
2162 let read = datadriven
2163 .client
2164 .open_leased_reader::<String, (), u64, i64>(
2165 datadriven.shard_id,
2166 Arc::new(StringSchema),
2167 Arc::new(UnitSchema),
2168 Diagnostics::for_tests(),
2169 true,
2170 )
2171 .await
2172 .expect("invalid shard types");
2173 let listen = read
2174 .listen(as_of)
2175 .await
2176 .map_err(|err| anyhow!("{:?}", err))?;
2177 datadriven.listens.insert(output.to_owned(), listen);
2178 Ok("ok\n".into())
2179 }
2180
2181 pub async fn listen_through(
2182 datadriven: &mut MachineState,
2183 args: DirectiveArgs<'_>,
2184 ) -> Result<String, anyhow::Error> {
2185 let input = args.expect_str("input");
2186 let frontier = args.expect("frontier");
2189 let listen = datadriven.listens.get_mut(input).expect("unknown listener");
2190 let mut s = String::new();
2191 loop {
2192 for event in listen.fetch_next().await {
2193 match event {
2194 ListenEvent::Updates(x) => {
2195 for ((k, _v), t, d) in x.iter() {
2196 write!(s, "{} {} {}\n", k, t, d);
2197 }
2198 }
2199 ListenEvent::Progress(x) => {
2200 if !x.less_than(&frontier) {
2201 return Ok(s);
2202 }
2203 }
2204 }
2205 }
2206 }
2207 }
2208
2209 pub async fn register_critical_reader(
2210 datadriven: &mut MachineState,
2211 args: DirectiveArgs<'_>,
2212 ) -> Result<String, anyhow::Error> {
2213 let reader_id = args.expect("reader_id");
2214 let (state, maintenance) = datadriven
2215 .machine
2216 .register_critical_reader(&reader_id, Opaque::encode(&0u64), "tests")
2217 .await;
2218 datadriven.routine.push(maintenance);
2219 Ok(format!(
2220 "{} {:?}\n",
2221 datadriven.machine.seqno(),
2222 state.since.elements(),
2223 ))
2224 }
2225
2226 pub async fn register_leased_reader(
2227 datadriven: &mut MachineState,
2228 args: DirectiveArgs<'_>,
2229 ) -> Result<String, anyhow::Error> {
2230 let reader_id = args.expect("reader_id");
2231 let (reader_state, maintenance) = datadriven
2232 .machine
2233 .register_leased_reader(
2234 &reader_id,
2235 "tests",
2236 READER_LEASE_DURATION.get(&datadriven.client.cfg),
2237 false,
2238 )
2239 .await;
2240 datadriven.routine.push(maintenance);
2241 Ok(format!(
2242 "{} {:?}\n",
2243 datadriven.machine.seqno(),
2244 reader_state.since.elements(),
2245 ))
2246 }
2247
2248 pub async fn expire_critical_reader(
2249 datadriven: &mut MachineState,
2250 args: DirectiveArgs<'_>,
2251 ) -> Result<String, anyhow::Error> {
2252 let reader_id = args.expect("reader_id");
2253 let (_, maintenance) = datadriven.machine.expire_critical_reader(&reader_id).await;
2254 datadriven.routine.push(maintenance);
2255 Ok(format!("{} ok\n", datadriven.machine.seqno()))
2256 }
2257
2258 pub async fn expire_leased_reader(
2259 datadriven: &mut MachineState,
2260 args: DirectiveArgs<'_>,
2261 ) -> Result<String, anyhow::Error> {
2262 let reader_id = args.expect("reader_id");
2263 let (_, maintenance) = datadriven.machine.expire_leased_reader(&reader_id).await;
2264 datadriven.routine.push(maintenance);
2265 Ok(format!("{} ok\n", datadriven.machine.seqno()))
2266 }
2267
2268 pub async fn compare_and_append_batches(
2269 datadriven: &MachineState,
2270 args: DirectiveArgs<'_>,
2271 ) -> Result<String, anyhow::Error> {
2272 let expected_upper = args.expect_antichain("expected_upper");
2273 let new_upper = args.expect_antichain("new_upper");
2274
2275 let mut batches: Vec<Batch<String, (), u64, i64>> = args
2276 .args
2277 .get("batches")
2278 .expect("missing batches")
2279 .into_iter()
2280 .map(|batch| {
2281 let hollow = (*datadriven
2282 .batches
2283 .get(batch)
2284 .expect("unknown batch")
2285 .clone()
2286 .batch)
2287 .clone();
2288 datadriven.to_batch(hollow)
2289 })
2290 .collect();
2291
2292 let mut writer = datadriven
2293 .client
2294 .open_writer(
2295 datadriven.shard_id,
2296 Arc::new(StringSchema),
2297 Arc::new(UnitSchema),
2298 Diagnostics::for_tests(),
2299 )
2300 .await?;
2301
2302 let mut batch_refs: Vec<_> = batches.iter_mut().collect();
2303
2304 let () = writer
2305 .compare_and_append_batch(batch_refs.as_mut_slice(), expected_upper, new_upper, true)
2306 .await?
2307 .map_err(|err| anyhow!("upper mismatch: {:?}", err))?;
2308
2309 writer.expire().await;
2310
2311 Ok("ok\n".into())
2312 }
2313
2314 pub async fn expire_writer(
2315 datadriven: &mut MachineState,
2316 args: DirectiveArgs<'_>,
2317 ) -> Result<String, anyhow::Error> {
2318 let writer_id = args.expect("writer_id");
2319 let (_, maintenance) = datadriven.machine.expire_writer(&writer_id).await;
2320 datadriven.routine.push(maintenance);
2321 Ok(format!("{} ok\n", datadriven.machine.seqno()))
2322 }
2323
2324 pub(crate) async fn finalize(
2325 datadriven: &mut MachineState,
2326 _args: DirectiveArgs<'_>,
2327 ) -> anyhow::Result<String> {
2328 let maintenance = datadriven.machine.become_tombstone().await?;
2329 datadriven.routine.push(maintenance);
2330 Ok(format!("{} ok\n", datadriven.machine.seqno()))
2331 }
2332
2333 pub(crate) fn is_finalized(
2334 datadriven: &MachineState,
2335 _args: DirectiveArgs<'_>,
2336 ) -> anyhow::Result<String> {
2337 let seqno = datadriven.machine.seqno();
2338 let tombstone = datadriven.machine.is_finalized();
2339 Ok(format!("{seqno} {tombstone}\n"))
2340 }
2341
2342 pub async fn compare_and_append(
2343 datadriven: &mut MachineState,
2344 args: DirectiveArgs<'_>,
2345 ) -> Result<String, anyhow::Error> {
2346 let input = args.expect_str("input");
2347 let writer_id = args.expect("writer_id");
2348 let mut batch = datadriven
2349 .batches
2350 .get(input)
2351 .expect("unknown batch")
2352 .clone();
2353 let token = args.optional("token").unwrap_or_else(IdempotencyToken::new);
2354
2355 let (id, maintenance) = datadriven
2356 .machine
2357 .register_schema(&*SCHEMAS.key, &*SCHEMAS.val)
2358 .await;
2359 assert_eq!(id, SCHEMAS.id);
2360 datadriven.routine.push(maintenance);
2361 let maintenance = loop {
2362 let indeterminate = args
2363 .optional::<String>("prev_indeterminate")
2364 .map(|x| Indeterminate::new(anyhow::Error::msg(x)));
2365 let res = datadriven
2366 .machine
2367 .compare_and_append_idempotent(
2368 &batch.batch,
2369 &writer_id,
2370 &token,
2371 &HandleDebugState::default(),
2372 indeterminate,
2373 )
2374 .await;
2375 match res {
2376 CompareAndAppendRes::Success(_, x) => break x,
2377 CompareAndAppendRes::UpperMismatch(_seqno, upper) => {
2378 return Err(anyhow!("{:?}", Upper(upper)));
2379 }
2380 CompareAndAppendRes::InlineBackpressure => {
2381 let hollow_batch = (*batch.batch).clone();
2382 let mut b = datadriven.to_batch(hollow_batch);
2383 let cfg = BatchBuilderConfig::new(&datadriven.client.cfg, datadriven.shard_id);
2384 b.flush_to_blob(
2385 &cfg,
2386 &datadriven.client.metrics.user,
2387 &datadriven.client.isolated_runtime,
2388 &*SCHEMAS,
2389 )
2390 .await;
2391 batch.batch = Arc::new(b.into_hollow_batch());
2392 continue;
2393 }
2394 CompareAndAppendRes::InvalidUsage(_) => panic!("{:?}", res),
2395 };
2396 };
2397 datadriven.routine.push(maintenance.routine);
2400 Ok(format!(
2401 "{} {:?}\n",
2402 datadriven.machine.seqno(),
2403 datadriven.machine.applier.clone_upper().elements(),
2404 ))
2405 }
2406
2407 pub async fn apply_merge_res(
2408 datadriven: &mut MachineState,
2409 args: DirectiveArgs<'_>,
2410 ) -> Result<String, anyhow::Error> {
2411 let input = args.expect_str("input");
2412 let legacy = args.optional("legacy").unwrap_or(false);
2413 let batch = datadriven
2414 .batches
2415 .get(input)
2416 .expect("unknown batch")
2417 .clone();
2418 let compaction_input = if legacy {
2419 CompactionInput::Legacy
2420 } else {
2421 let compact_req = datadriven
2422 .compactions
2423 .get(input)
2424 .expect("unknown compact req")
2425 .clone();
2426 let input_batches = compact_req
2427 .inputs
2428 .iter()
2429 .map(|x| x.id)
2430 .collect::<BTreeSet<_>>();
2431 let lower_spine_bound = input_batches
2432 .first()
2433 .map(|id| id.0)
2434 .expect("at least one batch must be present");
2435 let upper_spine_bound = input_batches
2436 .last()
2437 .map(|id| id.1)
2438 .expect("at least one batch must be present");
2439 CompactionInput::IdRange(SpineId(lower_spine_bound, upper_spine_bound))
2440 };
2441 let hollow_batch = (*batch.batch).clone();
2442
2443 let (merge_res, maintenance) = datadriven
2444 .machine
2445 .merge_res(&FueledMergeRes {
2446 output: hollow_batch,
2447 input: compaction_input,
2448 new_active_compaction: None,
2449 })
2450 .await;
2451 datadriven.routine.push(maintenance);
2452 Ok(format!(
2453 "{} {}\n",
2454 datadriven.machine.seqno(),
2455 merge_res.applied()
2456 ))
2457 }
2458
2459 pub async fn perform_maintenance(
2460 datadriven: &mut MachineState,
2461 _args: DirectiveArgs<'_>,
2462 ) -> Result<String, anyhow::Error> {
2463 let mut s = String::new();
2464 for maintenance in datadriven.routine.drain(..) {
2465 let mut pending = vec![maintenance];
2468 while let Some(m) = pending.pop() {
2469 if m.write_rollup.is_some() {
2470 pending.push(datadriven.machine.add_rollup_for_current_seqno().await);
2471 }
2472 if let Some(req) = m.garbage_collection {
2473 let machine = &datadriven.machine;
2474 pending.push(GarbageCollector::gc_and_truncate(machine, req).await.0);
2475 }
2476 }
2477 let () = datadriven
2478 .machine
2479 .applier
2480 .fetch_and_update_state(None)
2481 .await;
2482 write!(s, "{} ok\n", datadriven.machine.seqno());
2483 }
2484 Ok(s)
2485 }
2486}
2487
2488#[cfg(test)]
2489pub mod tests {
2490 use std::sync::Arc;
2491
2492 use mz_dyncfg::ConfigUpdates;
2493 use mz_ore::cast::CastFrom;
2494 use mz_ore::task::spawn;
2495 use mz_persist::intercept::{InterceptBlob, InterceptHandle};
2496 use mz_persist::location::SeqNo;
2497 use mz_persist_types::PersistLocation;
2498 use semver::Version;
2499 use timely::progress::Antichain;
2500
2501 use crate::batch::BatchBuilderConfig;
2502 use crate::cache::StateCache;
2503 use crate::cfg::GC_REMOVE_FINALIZED_ROLLUPS;
2504 use crate::internal::gc::{GarbageCollector, GcReq, GcResults};
2505 use crate::internal::machine::Machine;
2506 use crate::internal::state::{GC_USE_ACTIVE_GC, HandleDebugState, ROLLUP_THRESHOLD};
2507 use crate::tests::{new_test_client, new_test_client_cache};
2508 use crate::{Diagnostics, PersistClient, ShardId};
2509
2510 #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2511 #[cfg_attr(miri, ignore)] async fn apply_unbatched_cmd_truncate(dyncfgs: ConfigUpdates) {
2513 mz_ore::test::init_logging();
2514
2515 let client = new_test_client(&dyncfgs).await;
2516 client.cfg.set_config(&ROLLUP_THRESHOLD, 5);
2518 let (mut write, read) = client
2519 .expect_open::<String, (), u64, i64>(ShardId::new())
2520 .await;
2521
2522 read.expire().await;
2524
2525 const NUM_BATCHES: u64 = 100;
2528 for idx in 0..NUM_BATCHES {
2529 let mut batch = write
2530 .expect_batch(&[((idx.to_string(), ()), idx, 1)], idx, idx + 1)
2531 .await;
2532 let cfg = BatchBuilderConfig::new(&client.cfg, write.shard_id());
2535 batch
2536 .flush_to_blob(
2537 &cfg,
2538 &client.metrics.user,
2539 &client.isolated_runtime,
2540 &write.write_schemas,
2541 )
2542 .await;
2543 let (_, writer_maintenance) = write
2544 .machine
2545 .compare_and_append(
2546 &batch.into_hollow_batch(),
2547 &write.writer_id,
2548 &HandleDebugState::default(),
2549 )
2550 .await
2551 .unwrap();
2552 writer_maintenance
2553 .perform(&write.machine, &write.gc, write.compact.as_ref())
2554 .await;
2555 }
2556 let live_diffs = write
2557 .machine
2558 .applier
2559 .state_versions
2560 .fetch_all_live_diffs(&write.machine.shard_id())
2561 .await;
2562 assert!(live_diffs.len() > 0);
2564 let max_live_diffs = 2 * usize::cast_from(NUM_BATCHES.next_power_of_two().trailing_zeros());
2568 assert!(
2569 live_diffs.len() <= max_live_diffs,
2570 "{} vs {}",
2571 live_diffs.len(),
2572 max_live_diffs
2573 );
2574 }
2575
2576 #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2580 #[cfg_attr(miri, ignore)] async fn regression_gc_skipped_req_and_interrupted(dyncfgs: ConfigUpdates) {
2582 let mut client = new_test_client(&dyncfgs).await;
2583 let intercept = InterceptHandle::default();
2584 client.blob = Arc::new(InterceptBlob::new(
2585 Arc::clone(&client.blob),
2586 intercept.clone(),
2587 ));
2588 let (_, mut read) = client
2589 .expect_open::<String, String, u64, i64>(ShardId::new())
2590 .await;
2591
2592 read.downgrade_since(&Antichain::from_elem(1)).await;
2594 let new_seqno_since = read.machine.applier.seqno_since();
2595
2596 assert!(new_seqno_since > SeqNo::minimum());
2601 intercept.set_post_delete(Some(Arc::new(|_, _| panic!("boom"))));
2602 let machine = read.machine.clone();
2603 let gc = spawn(|| "", async move {
2605 let req = GcReq {
2606 shard_id: machine.shard_id(),
2607 new_seqno_since,
2608 };
2609 GarbageCollector::gc_and_truncate(&machine, req).await
2610 });
2611 let _ = gc.await;
2614
2615 intercept.set_post_delete(None);
2618 let req = GcReq {
2619 shard_id: read.machine.shard_id(),
2620 new_seqno_since,
2621 };
2622 let _ = GarbageCollector::gc_and_truncate(&read.machine, req.clone()).await;
2623 }
2624
2625 #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2630 #[cfg_attr(miri, ignore)] async fn regression_update_state_after_upper_mismatch(dyncfgs: ConfigUpdates) {
2632 let client = new_test_client(&dyncfgs).await;
2633 let mut client2 = client.clone();
2634
2635 let new_state_cache = Arc::new(StateCache::new_no_metrics());
2638 client2.shared_states = new_state_cache;
2639
2640 let shard_id = ShardId::new();
2641 let (mut write1, _) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2642 let (mut write2, _) = client2.expect_open::<String, (), u64, i64>(shard_id).await;
2643
2644 let data = [
2645 (("1".to_owned(), ()), 1, 1),
2646 (("2".to_owned(), ()), 2, 1),
2647 (("3".to_owned(), ()), 3, 1),
2648 (("4".to_owned(), ()), 4, 1),
2649 (("5".to_owned(), ()), 5, 1),
2650 ];
2651
2652 write1.expect_compare_and_append(&data[..1], 0, 2).await;
2653
2654 write2.expect_compare_and_append(&data[1..2], 2, 3).await;
2657 }
2658
2659 #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2660 #[cfg_attr(miri, ignore)]
2661 async fn version_upgrade(dyncfgs: ConfigUpdates) {
2662 let mut cache = new_test_client_cache(&dyncfgs);
2663 cache.cfg.build_version = Version::new(26, 1, 0);
2664 let shard_id = ShardId::new();
2665
2666 async fn fetch_catalog_upgrade_shard_version(
2667 persist_client: &PersistClient,
2668 upgrade_shard_id: ShardId,
2669 ) -> Option<semver::Version> {
2670 let shard_state = persist_client
2671 .inspect_shard::<u64>(&upgrade_shard_id)
2672 .await
2673 .ok()?;
2674 let json_state = serde_json::to_value(shard_state).expect("state serialization error");
2675 let upgrade_version = json_state
2676 .get("applier_version")
2677 .cloned()
2678 .expect("missing applier_version");
2679 let upgrade_version =
2680 serde_json::from_value(upgrade_version).expect("version deserialization error");
2681 Some(upgrade_version)
2682 }
2683
2684 cache.cfg.build_version = Version::new(26, 1, 0);
2685 let client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
2686 let (write, mut reader) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2687 reader.downgrade_since(&Antichain::from_elem(1)).await;
2688 assert_eq!(
2689 fetch_catalog_upgrade_shard_version(&client, shard_id).await,
2690 Some(Version::new(26, 1, 0)),
2691 );
2692
2693 write.expire().await;
2702 reader.expire().await;
2703
2704 cache.cfg.build_version = Version::new(27, 1, 0);
2706 let client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
2707 let (write, mut reader) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2708 reader.downgrade_since(&Antichain::from_elem(2)).await;
2709 assert_eq!(
2710 fetch_catalog_upgrade_shard_version(&client, shard_id).await,
2711 Some(Version::new(26, 1, 0)),
2712 );
2713
2714 client
2716 .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
2717 .await
2718 .unwrap();
2719 assert_eq!(
2720 fetch_catalog_upgrade_shard_version(&client, shard_id).await,
2721 Some(Version::new(27, 1, 0)),
2722 );
2723
2724 write.expire().await;
2725 reader.expire().await;
2726 }
2727
2728 #[mz_persist_proc::test(tokio::test)]
2732 #[cfg_attr(miri, ignore)] async fn version_upgrade_tombstone(dyncfgs: ConfigUpdates) {
2734 async fn shard_version(
2735 persist_client: &PersistClient,
2736 shard_id: ShardId,
2737 ) -> Option<semver::Version> {
2738 let shard_state = persist_client.inspect_shard::<u64>(&shard_id).await.ok()?;
2739 let json_state = serde_json::to_value(shard_state).expect("state serialization error");
2740 let version = json_state
2741 .get("applier_version")
2742 .cloned()
2743 .expect("missing applier_version");
2744 Some(serde_json::from_value(version).expect("version deserialization error"))
2745 }
2746
2747 let mut cache = new_test_client_cache(&dyncfgs);
2748 cache.cfg.build_version = Version::new(26, 1, 0);
2749 let client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
2750 let shard_id = ShardId::new();
2751
2752 let (mut write, mut read) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2755 read.downgrade_since(&Antichain::new()).await;
2756 write.advance_upper(&Antichain::new()).await;
2757 write.expire().await;
2758 read.expire().await;
2759 client
2760 .finalize_shard::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
2761 .await
2762 .expect("finalization must succeed");
2763
2764 client
2767 .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
2768 .await
2769 .expect("upgrade on tombstone must succeed");
2770
2771 cache.cfg.build_version = Version::new(27, 1, 0);
2773 let client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
2774 client
2775 .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
2776 .await
2777 .expect("upgrade on tombstone must succeed");
2778
2779 assert_eq!(
2780 shard_version(&client, shard_id).await,
2781 Some(Version::new(26, 1, 0)),
2782 );
2783 let is_finalized = client
2784 .is_finalized::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
2785 .await
2786 .expect("invalid persist usage");
2787 assert!(is_finalized, "shard must still be finalized");
2788 }
2789
2790 async fn cold_machine(
2792 client: &PersistClient,
2793 shard_id: ShardId,
2794 ) -> Machine<String, (), u64, i64> {
2795 let mut client = client.clone();
2796 client.shared_states = Arc::new(StateCache::new_no_metrics());
2797 client
2798 .make_machine(shard_id, Diagnostics::for_tests())
2799 .await
2800 .expect("codecs match")
2801 }
2802
2803 async fn gc(machine: &Machine<String, (), u64, i64>) -> GcResults {
2804 let req = GcReq {
2805 shard_id: machine.shard_id(),
2806 new_seqno_since: machine.applier.seqno_since(),
2807 };
2808 GarbageCollector::gc_and_truncate(machine, req).await.1
2809 }
2810
2811 #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2815 #[cfg_attr(miri, ignore)] async fn finalized_gc_removes_old_rollups(dyncfgs: ConfigUpdates) {
2817 let client = new_test_client(&dyncfgs).await;
2818 client.cfg.set_config(&GC_USE_ACTIVE_GC, true);
2821 let shard_id = ShardId::new();
2822 let (mut write, mut read) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2823 read.downgrade_since(&Antichain::new()).await;
2824 write.advance_upper(&Antichain::new()).await;
2825 let machine = write.machine.clone();
2826
2827 let mut maintenance = machine
2831 .become_tombstone()
2832 .await
2833 .expect("since and upper are empty");
2834 while maintenance.write_rollup.is_some() {
2835 maintenance = machine.add_rollup_for_current_seqno().await;
2836 }
2837 assert!(machine.is_finalized());
2838 let stale = cold_machine(&client, shard_id).await;
2839
2840 let mut rollups = machine
2841 .applier
2842 .rollups_lte_seqno(machine.applier.seqno_since());
2843 let (kept_seqno, kept) = rollups.pop().expect("tombstone has a rollup");
2844 assert!(!rollups.is_empty(), "need an old rollup to delete");
2845 let old_keys: Vec<String> = rollups
2846 .iter()
2847 .map(|(_, key)| key.complete(&shard_id).to_string())
2848 .collect();
2849 let head_seqno = || async {
2850 client
2851 .consensus
2852 .head(&shard_id.to_string())
2853 .await
2854 .expect("head")
2855 .expect("shard is initialized")
2856 .seqno
2857 };
2858 let listed_rollups = || async {
2859 cold_machine(&client, shard_id)
2860 .await
2861 .applier
2862 .rollups_lte_seqno(SeqNo::maximum())
2863 };
2864 let old_blobs = || async {
2865 let mut n = 0;
2866 for key in &old_keys {
2867 n += usize::from(client.blob.get(key).await.expect("get").is_some());
2868 }
2869 n
2870 };
2871 let rollups_assigned = || client.metrics.state.finalized_rollup_assigned.get();
2872 let assigned = rollups_assigned();
2873
2874 client.cfg.set_config(&GC_REMOVE_FINALIZED_ROLLUPS, false);
2877 let seqno = head_seqno().await;
2878 gc(&cold_machine(&client, shard_id).await).await;
2879 client.cfg.set_config(&GC_REMOVE_FINALIZED_ROLLUPS, true);
2880 let live_diffs = machine
2881 .applier
2882 .state_versions
2883 .fetch_all_live_diffs(&shard_id)
2884 .await;
2885 assert_eq!(live_diffs.first().map(|x| x.seqno), Some(kept_seqno));
2886 assert_eq!(
2887 head_seqno().await,
2888 seqno,
2889 "kill switch off but gc committed"
2890 );
2891 assert_eq!(listed_rollups().await.len(), old_keys.len() + 1);
2892 assert_eq!(old_blobs().await, old_keys.len());
2893
2894 let _ = cold_machine(&client, shard_id)
2896 .await
2897 .remove_rollups(&rollups, true)
2898 .await;
2899 let committed = head_seqno().await;
2900 assert_eq!(committed, seqno.next(), "gc should commit one removal");
2901 assert_eq!(listed_rollups().await, vec![(kept_seqno, kept.clone())]);
2902 assert_eq!(old_blobs().await, old_keys.len());
2903
2904 let _ = cold_machine(&client, shard_id)
2908 .await
2909 .add_rollup_for_current_seqno()
2910 .await;
2911 let committed = head_seqno().await;
2912 let kept = listed_rollups().await;
2913 for i in 0..3 {
2914 let results = gc(&cold_machine(&client, shard_id).await).await;
2915 let expected_deleted = if i == 0 { old_keys.len() } else { 0 };
2916 assert_eq!(results.rollups_deleted_from_blob, expected_deleted);
2917 assert_eq!(old_blobs().await, 0, "old rollups still in blob");
2918 assert_eq!(
2919 head_seqno().await,
2920 committed,
2921 "gc at the fixed point committed"
2922 );
2923 }
2924 assert_eq!(listed_rollups().await, kept);
2925
2926 let results = gc(&stale).await;
2928 assert!(results.truncated_consensus_to.is_empty());
2929 assert_eq!(results.rollups_deleted_from_blob, 0);
2930 assert_eq!(
2931 rollups_assigned(),
2932 assigned + 1,
2933 "rollup assigned after finalizing"
2934 );
2935
2936 write.expire().await;
2937 read.expire().await;
2938 }
2939}