Skip to main content

mz_persist_client/internal/
machine.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Implementation of the persist state machine.
11
12use 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};
22use mz_ore::cast::CastFrom;
23use mz_ore::error::ErrorExt;
24#[allow(unused_imports)] // False positive.
25use 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
66// Impl Clone regardless of the type params.
67impl<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);
82
83pub(crate) const CLAIM_COMPACTION_PERCENT: Config<usize> = Config::new(
84    "persist_claim_compaction_percent",
85    100,
86    "Claim a compaction with the given percent chance, if claiming compactions is enabled. \
87    (If over 100, we'll always claim at least one; for example, if set to 365, we'll claim at least \
88    three and have a 65% chance of claiming a fourth.)",
89);
90
91pub(crate) const CLAIM_COMPACTION_MIN_VERSION: Config<String> = Config::new(
92    "persist_claim_compaction_min_version",
93    String::new(),
94    "If set to a valid version string, compact away any earlier versions if possible.",
95);
96
97impl<K, V, T, D> Machine<K, V, T, D>
98where
99    K: Debug + Codec,
100    V: Debug + Codec,
101    T: Timestamp + Lattice + Codec64 + Sync,
102    D: Monoid + Codec64,
103{
104    pub async fn new(
105        cfg: PersistConfig,
106        shard_id: ShardId,
107        metrics: Arc<Metrics>,
108        state_versions: Arc<StateVersions>,
109        shared_states: Arc<StateCache>,
110        pubsub_sender: Arc<dyn PubSubSender>,
111        isolated_runtime: Arc<IsolatedRuntime>,
112        diagnostics: Diagnostics,
113    ) -> Result<Self, Box<CodecMismatch>> {
114        let applier = Applier::new(
115            cfg,
116            shard_id,
117            metrics,
118            state_versions,
119            shared_states,
120            pubsub_sender,
121            diagnostics,
122        )
123        .await?;
124        Ok(Machine {
125            applier,
126            isolated_runtime,
127        })
128    }
129
130    pub fn shard_id(&self) -> ShardId {
131        self.applier.shard_id
132    }
133
134    pub fn seqno(&self) -> SeqNo {
135        self.applier.seqno()
136    }
137
138    pub async fn add_rollup_for_current_seqno(&self) -> RoutineMaintenance {
139        let rollup = self.applier.write_rollup_for_state().await;
140        let Some(rollup) = rollup else {
141            return RoutineMaintenance::default();
142        };
143
144        let (applied, maintenance) = self.add_rollup((rollup.seqno, &rollup.to_hollow())).await;
145        if !applied {
146            // Someone else already wrote a rollup at this seqno, so ours didn't
147            // get added. Delete it.
148            self.applier
149                .state_versions
150                .delete_rollup(&rollup.shard_id, &rollup.key)
151                .await;
152        }
153        maintenance
154    }
155
156    pub async fn add_rollup(
157        &self,
158        add_rollup: (SeqNo, &HollowRollup),
159    ) -> (bool, RoutineMaintenance) {
160        // See the big SUBTLE comment in [Self::merge_res] for what's going on
161        // here.
162        let mut applied_ever_true = false;
163        let metrics = Arc::clone(&self.applier.metrics);
164        let (_seqno, _applied, maintenance) = self
165            .apply_unbatched_idempotent_cmd(&metrics.cmds.add_rollup, |_, _, state| {
166                let ret = state.add_rollup(add_rollup);
167                if let Continue(applied) = ret {
168                    applied_ever_true = applied_ever_true || applied;
169                }
170                ret
171            })
172            .await;
173        (applied_ever_true, maintenance)
174    }
175
176    pub async fn remove_rollups(
177        &self,
178        remove_rollups: &[(SeqNo, PartialRollupKey)],
179    ) -> (Vec<SeqNo>, RoutineMaintenance) {
180        let metrics = Arc::clone(&self.applier.metrics);
181        let (_seqno, removed_rollup_seqnos, maintenance) = self
182            .apply_unbatched_idempotent_cmd(&metrics.cmds.remove_rollups, |_, _, state| {
183                state.remove_rollups(remove_rollups)
184            })
185            .await;
186        (removed_rollup_seqnos, maintenance)
187    }
188
189    /// Attempt to upgrade the state to the latest version. If that's not possible, return the
190    /// actual data version of the shard.
191    pub async fn upgrade_version(&self) -> Result<RoutineMaintenance, Version> {
192        let metrics = Arc::clone(&self.applier.metrics);
193        let (_seqno, upgrade_result, maintenance) = self
194            .apply_unbatched_idempotent_cmd(&metrics.cmds.remove_rollups, |_, cfg, state| {
195                if state.version <= cfg.build_version {
196                    // This would be the place to remove any deprecated items from state, now
197                    // that we're dropping compatibility with any previous versions.
198                    state.version = cfg.build_version.clone();
199                    Continue(Ok(()))
200                } else {
201                    Break(NoOpStateTransition(Err(state.version.clone())))
202                }
203            })
204            .await;
205
206        match upgrade_result {
207            Ok(()) => Ok(maintenance),
208            Err(version) => {
209                soft_assert_no_log!(
210                    maintenance.is_empty(),
211                    "should not generate maintenance on failed upgrade"
212                );
213                Err(version)
214            }
215        }
216    }
217
218    pub async fn register_leased_reader(
219        &self,
220        reader_id: &LeasedReaderId,
221        purpose: &str,
222        lease_duration: Duration,
223        heartbeat_timestamp_ms: u64,
224        use_critical_since: bool,
225    ) -> (LeasedReaderState<T>, RoutineMaintenance) {
226        let metrics = Arc::clone(&self.applier.metrics);
227        let (_seqno, (reader_state, seqno_since), maintenance) = self
228            .apply_unbatched_idempotent_cmd(&metrics.cmds.register, |seqno, cfg, state| {
229                state.register_leased_reader(
230                    &cfg.hostname,
231                    reader_id,
232                    purpose,
233                    seqno,
234                    lease_duration,
235                    heartbeat_timestamp_ms,
236                    use_critical_since,
237                )
238            })
239            .await;
240        // Usually, the reader gets an initial seqno hold of the seqno at which
241        // it was registered. However, on a tombstone shard the seqno hold
242        // happens to get computed as the tombstone seqno + 1
243        // (State::clone_apply provided seqno.next(), the non-no-op commit
244        // seqno, to the work fn and this is what register_reader uses for the
245        // seqno hold). The real invariant we want to protect here is that the
246        // hold is >= the seqno_since, so validate that instead of anything more
247        // specific.
248        debug_assert!(
249            reader_state.seqno >= seqno_since,
250            "{} vs {}",
251            reader_state.seqno,
252            seqno_since,
253        );
254        (reader_state, maintenance)
255    }
256
257    pub async fn register_critical_reader(
258        &self,
259        reader_id: &CriticalReaderId,
260        default_opaque: Opaque,
261        purpose: &str,
262    ) -> (CriticalReaderState<T>, RoutineMaintenance) {
263        let metrics = Arc::clone(&self.applier.metrics);
264        let (_seqno, state, maintenance) = self
265            .apply_unbatched_idempotent_cmd(&metrics.cmds.register, |_seqno, cfg, state| {
266                state.register_critical_reader(
267                    &cfg.hostname,
268                    reader_id,
269                    default_opaque.clone(),
270                    purpose,
271                )
272            })
273            .await;
274        (state, maintenance)
275    }
276
277    pub async fn register_schema(
278        &self,
279        key_schema: &K::Schema,
280        val_schema: &V::Schema,
281    ) -> (Option<SchemaId>, RoutineMaintenance) {
282        let metrics = Arc::clone(&self.applier.metrics);
283        let (_seqno, state, maintenance) = self
284            .apply_unbatched_idempotent_cmd(&metrics.cmds.register, |_seqno, _cfg, state| {
285                state.register_schema::<K, V>(key_schema, val_schema)
286            })
287            .await;
288        (state, maintenance)
289    }
290
291    pub async fn spine_exert(&self, fuel: usize) -> (Vec<CompactReq<T>>, RoutineMaintenance) {
292        // Performance special case for no-ops, to avoid the State clones.
293        if fuel == 0 || self.applier.all_batches().len() < 2 {
294            return (Vec::new(), RoutineMaintenance::default());
295        }
296
297        let metrics = Arc::clone(&self.applier.metrics);
298        let (_seqno, reqs, maintenance) = self
299            .apply_unbatched_idempotent_cmd(&metrics.cmds.spine_exert, |_seqno, _cfg, state| {
300                state.spine_exert(fuel)
301            })
302            .await;
303        let reqs = reqs
304            .into_iter()
305            .map(|req| CompactReq {
306                shard_id: self.shard_id(),
307                desc: req.desc,
308                inputs: req.inputs,
309            })
310            .collect();
311        (reqs, maintenance)
312    }
313
314    pub async fn compare_and_append(
315        &self,
316        batch: &HollowBatch<T>,
317        writer_id: &WriterId,
318        debug_info: &HandleDebugState,
319        heartbeat_timestamp_ms: u64,
320    ) -> CompareAndAppendRes<T> {
321        let idempotency_token = IdempotencyToken::new();
322        loop {
323            let res = self
324                .compare_and_append_idempotent(
325                    batch,
326                    writer_id,
327                    heartbeat_timestamp_ms,
328                    &idempotency_token,
329                    debug_info,
330                    None,
331                )
332                .await;
333            match res {
334                CompareAndAppendRes::Success(seqno, maintenance) => {
335                    return CompareAndAppendRes::Success(seqno, maintenance);
336                }
337                CompareAndAppendRes::InvalidUsage(x) => {
338                    return CompareAndAppendRes::InvalidUsage(x);
339                }
340                CompareAndAppendRes::InlineBackpressure => {
341                    return CompareAndAppendRes::InlineBackpressure;
342                }
343                CompareAndAppendRes::UpperMismatch(seqno, _current_upper) => {
344                    // If the state machine thinks that the shard upper is not
345                    // far enough along, it could be because the caller of this
346                    // method has found out that it advanced via some some
347                    // side-channel that didn't update our local cache of the
348                    // machine state. So, fetch the latest state and try again
349                    // if we indeed get something different.
350                    self.applier.fetch_and_update_state(Some(seqno)).await;
351                    let (current_seqno, current_upper) =
352                        self.applier.upper(|seqno, upper| (seqno, upper.clone()));
353
354                    // We tried to to a compare_and_append with the wrong
355                    // expected upper, that won't work.
356                    if &current_upper != batch.desc.lower() {
357                        return CompareAndAppendRes::UpperMismatch(current_seqno, current_upper);
358                    } else {
359                        // The upper stored in state was outdated. Retry after
360                        // updating.
361                    }
362                }
363            }
364        }
365    }
366
367    async fn compare_and_append_idempotent(
368        &self,
369        batch: &HollowBatch<T>,
370        writer_id: &WriterId,
371        heartbeat_timestamp_ms: u64,
372        idempotency_token: &IdempotencyToken,
373        debug_info: &HandleDebugState,
374        // Only exposed for testing. In prod, this always starts as None, but
375        // making it a parameter allows us to simulate hitting an indeterminate
376        // error on the first attempt in tests.
377        mut indeterminate: Option<Indeterminate>,
378    ) -> CompareAndAppendRes<T> {
379        let metrics = Arc::clone(&self.applier.metrics);
380        let lease_duration_ms = self
381            .applier
382            .cfg
383            .writer_lease_duration
384            .as_millis()
385            .try_into()
386            .expect("reasonable duration");
387        // SUBTLE: Retries of compare_and_append with Indeterminate errors are
388        // tricky (more discussion of this in database-issues#3680):
389        //
390        // - (1) We compare_and_append and get an Indeterminate error back from
391        //   CRDB/Consensus. This means we don't know if it committed or not.
392        // - (2) We retry it.
393        // - (3) We get back an upper mismatch. The tricky bit is deciding if we
394        //   conflicted with some other writer OR if the write in (1) actually
395        //   went through and we're "conflicting" with ourself.
396        //
397        // A number of scenarios can be distinguished with per-writer
398        // idempotency tokens, so I'll jump straight to the hardest one:
399        //
400        // - (1) A compare_and_append is issued for e.g. `[5,7)`, the consensus
401        //   call makes it onto the network before the operation is cancelled
402        //   (by dropping the future).
403        // - (2) A compare_and_append is issued from the same WriteHandle for
404        //   `[3,5)`, it uses a different conn from the consensus pool and gets
405        //   an Indeterminate error.
406        // - (3) The call in (1) is received by consensus and commits.
407        // - (4) The retry of (2) receives an upper mismatch with an upper of 7.
408        //
409        // At this point, how do we determine whether (2) committed or not and
410        // thus whether we should return success or upper mismatch? Getting this
411        // right is very important for correctness (imagine this is a table
412        // write and we either return success or failure to the client).
413        //
414        // - If we use per-writer IdempotencyTokens but only store the latest
415        //   one in state, then the `[5,7)` one will have clobbered whatever our
416        //   `[3,5)` one was.
417        // - We could store every IdempotencyToken that ever committed, but that
418        //   would require unbounded storage in state (non-starter).
419        // - We could require that IdempotencyTokens are comparable and that
420        //   each call issued by a WriteHandle uses one that is strictly greater
421        //   than every call before it. A previous version of this PR tried this
422        //   and it's remarkably subtle. As a result, I (Dan) have developed
423        //   strong feels that our correctness protocol _should not depend on
424        //   WriteHandle, only Machine_.
425        // - We could require a new WriterId if a request is ever cancelled by
426        //   making `compare_and_append` take ownership of `self` and then
427        //   handing it back for any call polled to completion. The ergonomics
428        //   of this are quite awkward and, like the previous idea, it depends
429        //   on the WriteHandle impl for correctness.
430        // - Any ideas that involve reading back the data are foiled by a step
431        //   `(0) set the since to 100` (plus the latency and memory usage would
432        //   be too unpredictable).
433        //
434        // The technique used here derives from the following observations:
435        //
436        // - In practice, we don't use compare_and_append with the sort of
437        //   "regressing frontiers" described above.
438        // - In practice, Indeterminate errors are rare-ish. They happen enough
439        //   that we don't want to always panic on them, but this is still a
440        //   useful property to build on.
441        //
442        // At a high level, we do just enough to be able to distinguish the
443        // cases that we think will happen in practice and then leave the rest
444        // for a panic! that we think we'll never see. Concretely:
445        //
446        // - Run compare_and_append in a loop, retrying on Indeterminate errors
447        //   but noting if we've ever done that.
448        // - If we haven't seen an Indeterminate error (i.e. this is the first
449        //   time though the loop) then the result we got is guaranteed to be
450        //   correct, so pass it up.
451        // - Otherwise, any result other than an expected upper mismatch is
452        //   guaranteed to be correct, so just pass it up.
453        // - Otherwise examine the writer's most recent upper and break it into
454        //   two cases:
455        // - Case 1 `expected_upper.less_than(writer_most_recent_upper)`: it's
456        //   impossible that we committed on a previous iteration because the
457        //   overall upper of the shard is less_than what this call would have
458        //   advanced it to. Pass up the expectation mismatch.
459        // - Case 2 `!Case1`: First note that this means our IdempotencyToken
460        //   didn't match, otherwise we would have gotten `AlreadyCommitted`. It
461        //   also means some previous write from _this writer_ has committed an
462        //   upper that is beyond the one in this call, which is a weird usage
463        //   (NB can't be a future write because that would mean someone is
464        //   still polling us, but `&mut self` prevents that).
465        //
466        // TODO: If this technique works in practice (leads to zero panics),
467        // then commit to it and remove the Indeterminate from
468        // [WriteHandle::compare_and_append_batch].
469        let mut retry = self
470            .applier
471            .metrics
472            .retries
473            .compare_and_append_idempotent
474            .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
475        let mut writer_was_present = false;
476        loop {
477            let cmd_res = self
478                .applier
479                .apply_unbatched_cmd(&metrics.cmds.compare_and_append, |_, cfg, state| {
480                    writer_was_present = state.writers.contains_key(writer_id);
481                    state.compare_and_append(
482                        batch,
483                        writer_id,
484                        heartbeat_timestamp_ms,
485                        lease_duration_ms,
486                        idempotency_token,
487                        debug_info,
488                        INLINE_WRITES_TOTAL_MAX_BYTES.get(cfg),
489                        if CLAIM_UNCLAIMED_COMPACTIONS.get(cfg) {
490                            CLAIM_COMPACTION_PERCENT.get(cfg)
491                        } else {
492                            0
493                        },
494                        Version::parse(&CLAIM_COMPACTION_MIN_VERSION.get(cfg))
495                            .ok()
496                            .as_ref(),
497                    )
498                })
499                .await;
500            let (seqno, res, routine) = match cmd_res {
501                Ok(x) => x,
502                Err(err) => {
503                    // These are rare and interesting enough that we always log
504                    // them at info!.
505                    info!(
506                        "compare_and_append received an indeterminate error, retrying in {:?}: {}",
507                        retry.next_sleep(),
508                        err
509                    );
510                    if indeterminate.is_none() {
511                        indeterminate = Some(err);
512                    }
513                    retry = retry.sleep().await;
514                    continue;
515                }
516            };
517            match res {
518                Ok(merge_reqs) => {
519                    // We got explicit confirmation that we succeeded, so
520                    // anything that happened in a previous retry is irrelevant.
521                    let mut compact_reqs = Vec::with_capacity(merge_reqs.len());
522                    for req in merge_reqs {
523                        let req = CompactReq {
524                            shard_id: self.shard_id(),
525                            desc: req.desc,
526                            inputs: req.inputs,
527                        };
528                        compact_reqs.push(req);
529                    }
530                    let writer_maintenance = WriterMaintenance {
531                        routine,
532                        compaction: compact_reqs,
533                    };
534
535                    if !writer_was_present {
536                        metrics.state.writer_added.inc();
537                    }
538                    for part in &batch.parts {
539                        if part.is_inline() {
540                            let bytes = u64::cast_from(part.inline_bytes());
541                            metrics.inline.part_commit_bytes.inc_by(bytes);
542                            metrics.inline.part_commit_count.inc();
543                        }
544                    }
545                    return CompareAndAppendRes::Success(seqno, writer_maintenance);
546                }
547                Err(CompareAndAppendBreak::AlreadyCommitted) => {
548                    // A previous iteration through this loop got an
549                    // Indeterminate error but was successful. Sanity check this
550                    // and pass along the good news.
551                    assert!(indeterminate.is_some());
552                    self.applier.metrics.cmds.compare_and_append_noop.inc();
553                    if !writer_was_present {
554                        metrics.state.writer_added.inc();
555                    }
556                    return CompareAndAppendRes::Success(seqno, WriterMaintenance::default());
557                }
558                Err(CompareAndAppendBreak::InvalidUsage(err)) => {
559                    // InvalidUsage is (or should be) a deterministic function
560                    // of the inputs and independent of anything in persist
561                    // state. It's handed back via a Break, so we never even try
562                    // to commit it. No network, no Indeterminate.
563                    assert_none!(indeterminate);
564                    return CompareAndAppendRes::InvalidUsage(err);
565                }
566                Err(CompareAndAppendBreak::InlineBackpressure) => {
567                    // We tried to write an inline part, but there was already
568                    // too much in state. Flush it out to s3 and try again.
569                    return CompareAndAppendRes::InlineBackpressure;
570                }
571                Err(CompareAndAppendBreak::Upper {
572                    shard_upper,
573                    writer_upper,
574                }) => {
575                    // NB the below intentionally compares to writer_upper
576                    // (because it gives a tighter bound on the bad case), but
577                    // returns shard_upper (what the persist caller cares
578                    // about).
579                    assert!(
580                        PartialOrder::less_equal(&writer_upper, &shard_upper),
581                        "{:?} vs {:?}",
582                        &writer_upper,
583                        &shard_upper
584                    );
585                    if PartialOrder::less_than(&writer_upper, batch.desc.upper()) {
586                        // No way this could have committed in some previous
587                        // attempt of this loop: the upper of the writer is
588                        // strictly less than the proposed new upper.
589                        return CompareAndAppendRes::UpperMismatch(seqno, shard_upper);
590                    }
591                    if indeterminate.is_none() {
592                        // No way this could have committed in some previous
593                        // attempt of this loop: we never saw an indeterminate
594                        // error (thus there was no previous iteration of the
595                        // loop).
596                        return CompareAndAppendRes::UpperMismatch(seqno, shard_upper);
597                    }
598                    // This is the bad case. We can't distinguish if some
599                    // previous attempt that got an Indeterminate error
600                    // succeeded or not. This should be sufficiently rare in
601                    // practice (hopefully ~never) that we give up and let
602                    // process restart fix things. See the big comment above for
603                    // more context.
604                    //
605                    // NB: This is intentionally not a halt! because it's quite
606                    // unexpected.
607                    panic!(
608                        concat!(
609                            "cannot distinguish compare_and_append success or failure ",
610                            "caa_lower={:?} caa_upper={:?} writer_upper={:?} shard_upper={:?} err={:?}"
611                        ),
612                        batch.desc.lower().elements(),
613                        batch.desc.upper().elements(),
614                        writer_upper.elements(),
615                        shard_upper.elements(),
616                        indeterminate,
617                    );
618                }
619            };
620        }
621    }
622
623    pub async fn downgrade_since(
624        &self,
625        reader_id: &LeasedReaderId,
626        outstanding_seqno: SeqNo,
627        new_since: &Antichain<T>,
628        heartbeat_timestamp_ms: u64,
629    ) -> (SeqNo, Since<T>, RoutineMaintenance) {
630        let metrics = Arc::clone(&self.applier.metrics);
631        self.apply_unbatched_idempotent_cmd(&metrics.cmds.downgrade_since, |seqno, _cfg, state| {
632            state.downgrade_since(
633                reader_id,
634                seqno,
635                outstanding_seqno,
636                new_since,
637                heartbeat_timestamp_ms,
638            )
639        })
640        .await
641    }
642
643    pub async fn compare_and_downgrade_since(
644        &self,
645        reader_id: &CriticalReaderId,
646        expected_opaque: &Opaque,
647        (new_opaque, new_since): (&Opaque, &Antichain<T>),
648    ) -> (Result<Since<T>, (Opaque, Since<T>)>, RoutineMaintenance) {
649        let metrics = Arc::clone(&self.applier.metrics);
650        let (_seqno, res, maintenance) = self
651            .apply_unbatched_idempotent_cmd(
652                &metrics.cmds.compare_and_downgrade_since,
653                |_seqno, _cfg, state| {
654                    state.compare_and_downgrade_since(
655                        reader_id,
656                        expected_opaque,
657                        (new_opaque, new_since),
658                    )
659                },
660            )
661            .await;
662
663        match res {
664            Ok(since) => (Ok(since), maintenance),
665            Err((opaque, since)) => (Err((opaque, since)), maintenance),
666        }
667    }
668
669    pub async fn expire_leased_reader(
670        &self,
671        reader_id: &LeasedReaderId,
672    ) -> (SeqNo, RoutineMaintenance) {
673        let metrics = Arc::clone(&self.applier.metrics);
674        let (seqno, _existed, maintenance) = self
675            .apply_unbatched_idempotent_cmd(&metrics.cmds.expire_reader, |_, _, state| {
676                state.expire_leased_reader(reader_id)
677            })
678            .await;
679        (seqno, maintenance)
680    }
681
682    #[allow(dead_code)] // TODO(bkirwi): remove this when since behaviour on expiry has settled
683    pub async fn expire_critical_reader(
684        &self,
685        reader_id: &CriticalReaderId,
686    ) -> (SeqNo, RoutineMaintenance) {
687        let metrics = Arc::clone(&self.applier.metrics);
688        let (seqno, _existed, maintenance) = self
689            .apply_unbatched_idempotent_cmd(&metrics.cmds.expire_reader, |_, _, state| {
690                state.expire_critical_reader(reader_id)
691            })
692            .await;
693        (seqno, maintenance)
694    }
695
696    pub async fn expire_writer(&self, writer_id: &WriterId) -> (SeqNo, RoutineMaintenance) {
697        let metrics = Arc::clone(&self.applier.metrics);
698        let (seqno, _existed, maintenance) = self
699            .apply_unbatched_idempotent_cmd(&metrics.cmds.expire_writer, |_, _, state| {
700                state.expire_writer(writer_id)
701            })
702            .await;
703        metrics.state.writer_removed.inc();
704        (seqno, maintenance)
705    }
706
707    pub fn is_finalized(&self) -> bool {
708        self.applier.is_finalized()
709    }
710
711    /// See [crate::PersistClient::get_schema].
712    pub fn get_schema(&self, schema_id: SchemaId) -> Option<(K::Schema, V::Schema)> {
713        self.applier.get_schema(schema_id)
714    }
715
716    /// See [crate::PersistClient::latest_schema].
717    pub fn latest_schema(&self) -> Option<(SchemaId, K::Schema, V::Schema)> {
718        self.applier.latest_schema()
719    }
720
721    /// Returns the ID of the given schema, if known at the current state.
722    pub fn find_schema(&self, key_schema: &K::Schema, val_schema: &V::Schema) -> Option<SchemaId> {
723        self.applier.find_schema(key_schema, val_schema)
724    }
725
726    /// See [crate::PersistClient::compare_and_evolve_schema].
727    ///
728    /// TODO: Unify this with [Self::register_schema]?
729    pub async fn compare_and_evolve_schema(
730        &self,
731        expected: SchemaId,
732        key_schema: &K::Schema,
733        val_schema: &V::Schema,
734    ) -> (CaESchema<K, V>, RoutineMaintenance) {
735        let metrics = Arc::clone(&self.applier.metrics);
736        let (_seqno, state, maintenance) = self
737            .apply_unbatched_idempotent_cmd(
738                &metrics.cmds.compare_and_evolve_schema,
739                |_seqno, _cfg, state| {
740                    state.compare_and_evolve_schema::<K, V>(expected, key_schema, val_schema)
741                },
742            )
743            .await;
744        (state, maintenance)
745    }
746
747    async fn tombstone_step(&self) -> Result<(bool, RoutineMaintenance), InvalidUsage<T>> {
748        let metrics = Arc::clone(&self.applier.metrics);
749        let mut retry = self
750            .applier
751            .metrics
752            .retries
753            .idempotent_cmd
754            .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
755        loop {
756            let res = self
757                .applier
758                .apply_unbatched_cmd(&metrics.cmds.become_tombstone, |_, _, state| {
759                    state.become_tombstone_and_shrink()
760                })
761                .await;
762            let err = match res {
763                Ok((_seqno, Ok(()), maintenance)) => return Ok((true, maintenance)),
764                Ok((_seqno, Err(NoOpStateTransition(())), maintenance)) => {
765                    return Ok((false, maintenance));
766                }
767                Err(err) => err,
768            };
769            if retry.attempt() >= INFO_MIN_ATTEMPTS {
770                info!(
771                    "become_tombstone received an indeterminate error, retrying in {:?}: {}",
772                    retry.next_sleep(),
773                    err
774                );
775            } else {
776                debug!(
777                    "become_tombstone received an indeterminate error, retrying in {:?}: {}",
778                    retry.next_sleep(),
779                    err
780                );
781            }
782            retry = retry.sleep().await;
783        }
784    }
785
786    pub async fn become_tombstone(&self) -> Result<RoutineMaintenance, InvalidUsage<T>> {
787        self.applier.check_since_upper_both_empty()?;
788
789        let mut maintenance = RoutineMaintenance::default();
790
791        loop {
792            let (made_progress, more_maintenance) = self.tombstone_step().await?;
793            maintenance.merge(more_maintenance);
794            if !made_progress {
795                break;
796            }
797        }
798
799        Ok(maintenance)
800    }
801
802    /// Fetch a snapshot at the frontier without taking a lease on it. This may be useful for stats
803    /// or testing, but most callers will wish to wait for the frontier to advance and obtain the
804    /// snapshot separately.
805    pub async fn unleased_snapshot(
806        &self,
807        as_of: &Antichain<T>,
808    ) -> Result<Vec<HollowBatch<T>>, Since<T>> {
809        if let Ok(data) = self.applier.snapshot(as_of) {
810            return Ok(data);
811        }
812        let mut watch = self.applier.watch();
813        self.wait_for_upper_past(
814            as_of,
815            &mut watch,
816            None,
817            &self.applier.metrics.retries.snapshot,
818            RetryParameters::persist_defaults(),
819        )
820        .await;
821        match self.applier.snapshot(as_of) {
822            Ok(data) => Ok(data),
823            Err(SnapshotErr::AsOfHistoricalDistinctionsLost(since)) => Err(since),
824            Err(SnapshotErr::AsOfNotYetAvailable(seqno, upper)) => {
825                panic!(
826                    "waited for upper past {as_of:?}, but at latest seqno {seqno:?} the frontier was only {upper:?}",
827                    as_of = as_of.elements(),
828                    upper = upper.0.elements(),
829                )
830            }
831        }
832    }
833
834    // NB: Unlike the other methods here, this one is read-only.
835    pub fn verify_listen(&self, as_of: &Antichain<T>) -> Result<(), Since<T>> {
836        self.applier.verify_listen(as_of)
837    }
838
839    pub async fn wait_for_upper_past(
840        &self,
841        frontier: &Antichain<T>,
842        watch: &mut StateWatch<K, V, T, D>,
843        reader_id: Option<&LeasedReaderId>,
844        metrics: &RetryMetrics,
845        retry: RetryParameters,
846    ) {
847        let start = Instant::now();
848        let wait_for_seqno_past = self.applier.upper(|seqno, upper| {
849            if PartialOrder::less_than(frontier, upper) {
850                None
851            } else {
852                Some((seqno, upper.clone()))
853            }
854        });
855        let Some((mut seqno, mut upper)) = wait_for_seqno_past else {
856            // The current state's upper is already past the given frontier.
857            return;
858        };
859
860        // The latest state still doesn't have a new frontier for us:
861        // watch+sleep in a loop until it does.
862        let sleeps = metrics.stream(retry.into_retry(SystemTime::now()).into_retry_stream());
863
864        enum Wake<'a, K, V, T, D> {
865            Watch(&'a mut StateWatch<K, V, T, D>),
866            Sleep(MetricsRetryStream),
867        }
868        let mut watch_fut = std::pin::pin!(
869            watch
870                .wait_for_upper_past(frontier)
871                .map(Wake::Watch)
872                .instrument(trace_span!("snapshot::watch"))
873        );
874        let mut sleep_fut = std::pin::pin!(
875            sleeps
876                .sleep()
877                .map(Wake::Sleep)
878                .instrument(trace_span!("snapshot::sleep"))
879        );
880
881        // To reduce log spam, we log "not yet available" only once at info if
882        // it passes a certain threshold. Then, if it did one info log, we log
883        // again at info when it resolves.
884        let mut logged_at_info = false;
885        loop {
886            // Use a duration based threshold here instead of the usual
887            // INFO_MIN_ATTEMPTS because here we're waiting on an
888            // external thing to arrive.
889            if !logged_at_info
890                && start.elapsed() >= Duration::from_millis(1024)
891                && metrics.name.as_str() == "snapshot"
892            {
893                logged_at_info = true;
894                info!(
895                    shard_id =? self.shard_id(),
896                    shard_name =? self.applier.shard_metrics.name,
897                    reader_id =? reader_id,
898                    wait_frontier =? frontier.elements(),
899                    current_upper =? upper.elements(),
900                    current_seqno =? seqno,
901                    wait_for = &metrics.name,
902                    "desired upper not yet available",
903                );
904            } else {
905                debug!(
906                    shard_id =? self.shard_id(),
907                    shard_name =? self.applier.shard_metrics.name,
908                    reader_id =? reader_id,
909                    wait_frontier =? frontier.elements(),
910                    current_upper =? upper.elements(),
911                    current_seqno =? seqno,
912                    wait_for = &metrics.name,
913                    "desired upper not yet available",
914                );
915            }
916
917            let wake = match future::select(watch_fut.as_mut(), sleep_fut.as_mut()).await {
918                future::Either::Left((wake, _)) => wake,
919                future::Either::Right((wake, _)) => wake,
920            };
921            // Note that we don't need to fetch in the Watch case, because the
922            // Watch wakeup is a signal that the shared state has already been
923            // updated.
924            match &wake {
925                Wake::Watch(_) => self.applier.metrics.watch.wait_woken_via_watch.inc(),
926                Wake::Sleep(_) => {
927                    self.applier.metrics.watch.wait_woken_via_sleep.inc();
928                    self.applier.fetch_and_update_state(Some(seqno)).await;
929                }
930            }
931
932            let wait_for_seqno_past = self.applier.upper(|seqno, upper| {
933                if PartialOrder::less_than(frontier, upper) {
934                    None
935                } else {
936                    Some((seqno, upper.clone()))
937                }
938            });
939            match wait_for_seqno_past {
940                None => {
941                    match &wake {
942                        Wake::Watch(_) => self.applier.metrics.watch.wait_resolved_via_watch.inc(),
943                        Wake::Sleep(_) => self.applier.metrics.watch.wait_resolved_via_sleep.inc(),
944                    }
945                    return;
946                }
947                Some((s, u)) => {
948                    seqno = s;
949                    upper = u;
950                }
951            };
952
953            // Wait a bit and try again. Intentionally don't ever log
954            // this at info level.
955            match wake {
956                Wake::Watch(watch) => {
957                    watch_fut.set(
958                        watch
959                            .wait_for_upper_past(frontier)
960                            .map(Wake::Watch)
961                            .instrument(trace_span!("snapshot::watch")),
962                    );
963                }
964                Wake::Sleep(sleeps) => {
965                    debug!(
966                        shard_id =? self.shard_id(),
967                        shard_name =? self.applier.shard_metrics.name,
968                        reader_id =? reader_id,
969                        wait_frontier =? frontier.elements(),
970                        current_upper =? upper.elements(),
971                        current_seqno =? seqno,
972                        wait_for = &metrics.name,
973                        "didn't find new data, retrying in {:?}",
974                        sleeps.next_sleep(),
975                    );
976                    sleep_fut.set(
977                        sleeps
978                            .sleep()
979                            .map(Wake::Sleep)
980                            .instrument(trace_span!("snapshot::sleep")),
981                    );
982                }
983            }
984        }
985    }
986
987    async fn apply_unbatched_idempotent_cmd<
988        R,
989        WorkFn: FnMut(
990            SeqNo,
991            &PersistConfig,
992            &mut StateCollections<T>,
993        ) -> ControlFlow<NoOpStateTransition<R>, R>,
994    >(
995        &self,
996        cmd: &CmdMetrics,
997        mut work_fn: WorkFn,
998    ) -> (SeqNo, R, RoutineMaintenance) {
999        let mut retry = self
1000            .applier
1001            .metrics
1002            .retries
1003            .idempotent_cmd
1004            .stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
1005        loop {
1006            match self.applier.apply_unbatched_cmd(cmd, &mut work_fn).await {
1007                Ok((seqno, x, maintenance)) => match x {
1008                    Ok(x) => {
1009                        return (seqno, x, maintenance);
1010                    }
1011                    Err(NoOpStateTransition(x)) => {
1012                        return (seqno, x, maintenance);
1013                    }
1014                },
1015                Err(err) => {
1016                    if retry.attempt() >= INFO_MIN_ATTEMPTS {
1017                        info!(
1018                            "apply_unbatched_idempotent_cmd {} received an indeterminate error, retrying in {:?}: {}",
1019                            cmd.name,
1020                            retry.next_sleep(),
1021                            err
1022                        );
1023                    } else {
1024                        debug!(
1025                            "apply_unbatched_idempotent_cmd {} received an indeterminate error, retrying in {:?}: {}",
1026                            cmd.name,
1027                            retry.next_sleep(),
1028                            err
1029                        );
1030                    }
1031                    retry = retry.sleep().await;
1032                    continue;
1033                }
1034            }
1035        }
1036    }
1037}
1038
1039impl<K, V, T, D> Machine<K, V, T, D>
1040where
1041    K: Debug + Codec,
1042    V: Debug + Codec,
1043    T: Timestamp + Lattice + Codec64 + Sync,
1044    D: Monoid + Codec64 + PartialEq,
1045{
1046    pub async fn merge_res(
1047        &self,
1048        res: &FueledMergeRes<T>,
1049    ) -> (ApplyMergeResult, RoutineMaintenance) {
1050        let metrics = Arc::clone(&self.applier.metrics);
1051
1052        // SUBTLE! If Machine::merge_res returns false, the blobs referenced in
1053        // compaction output are deleted so we don't leak them. Naively passing
1054        // back the value returned by State::apply_merge_res might give a false
1055        // negative in the presence of retries and Indeterminate errors.
1056        // Specifically, something like the following:
1057        //
1058        // - We try to apply_merge_res, it matches.
1059        // - When apply_unbatched_cmd goes to commit the new state, the
1060        //   Consensus::compare_and_set returns an Indeterminate error (but
1061        //   actually succeeds). The committed State now contains references to
1062        //   the compaction output blobs.
1063        // - Machine::apply_unbatched_idempotent_cmd retries the Indeterminate
1064        //   error. For whatever reason, this time though it doesn't match
1065        //   (maybe the batches simply get grouped difference when deserialized
1066        //   from state, or more unavoidably perhaps another compaction
1067        //   happens).
1068        // - This now bubbles up applied=false to the caller, which uses it as a
1069        //   signal that the blobs in the compaction output should be deleted so
1070        //   that we don't leak them.
1071        // - We now contain references in committed State to blobs that don't
1072        //   exist.
1073        //
1074        // The fix is to keep track of whether applied ever was true, even for a
1075        // compare_and_set that returned an Indeterminate error. This has the
1076        // chance of false positive (leaking a blob) but that's better than a
1077        // false negative (a blob we can never recover referenced by state). We
1078        // anyway need a mechanism to clean up leaked blobs because of process
1079        // crashes.
1080        let mut merge_result_ever_applied = ApplyMergeResult::NotAppliedNoMatch;
1081        let (_seqno, _apply_merge_result, maintenance) = self
1082            .apply_unbatched_idempotent_cmd(&metrics.cmds.merge_res, |_, _, state| {
1083                let ret = state.apply_merge_res::<D>(res, &Arc::clone(&metrics).columnar);
1084                if let Continue(result) = ret {
1085                    // record if we've ever applied the merge
1086                    if result.applied() {
1087                        merge_result_ever_applied = result;
1088                    }
1089                    // otherwise record the most granular reason for _not_
1090                    // applying the merge when there was a matching batch
1091                    if result.matched() && !result.applied() && !merge_result_ever_applied.applied()
1092                    {
1093                        merge_result_ever_applied = result;
1094                    }
1095                }
1096                ret
1097            })
1098            .await;
1099        (merge_result_ever_applied, maintenance)
1100    }
1101}
1102
1103pub(crate) struct ExpireFn(
1104    /// This is stored on WriteHandle and ReadHandle, which we require to be
1105    /// Send + Sync, but the Future is only Send and not Sync. Instead store a
1106    /// FnOnce that returns the Future. This could also be made an `IntoFuture`,
1107    /// once producing one of those is made easier.
1108    pub(crate) Box<dyn FnOnce() -> BoxFuture<'static, ()> + Send + Sync + 'static>,
1109);
1110
1111impl Debug for ExpireFn {
1112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1113        f.debug_struct("ExpireFn").finish_non_exhaustive()
1114    }
1115}
1116
1117#[derive(Debug)]
1118pub(crate) enum CompareAndAppendRes<T> {
1119    Success(SeqNo, WriterMaintenance<T>),
1120    InvalidUsage(InvalidUsage<T>),
1121    UpperMismatch(SeqNo, Antichain<T>),
1122    InlineBackpressure,
1123}
1124
1125#[cfg(test)]
1126impl<T: Debug> CompareAndAppendRes<T> {
1127    #[track_caller]
1128    fn unwrap(self) -> (SeqNo, WriterMaintenance<T>) {
1129        match self {
1130            CompareAndAppendRes::Success(seqno, maintenance) => (seqno, maintenance),
1131            x => panic!("{:?}", x),
1132        }
1133    }
1134}
1135
1136pub(crate) const NEXT_LISTEN_BATCH_RETRYER_FIXED_SLEEP: Config<Duration> = Config::new(
1137    "persist_next_listen_batch_retryer_fixed_sleep",
1138    Duration::from_millis(1200), // pubsub is on by default!
1139    "\
1140    The fixed sleep when polling for new batches from a Listen or Subscribe. Skipped if zero.",
1141);
1142
1143pub(crate) const NEXT_LISTEN_BATCH_RETRYER_INITIAL_BACKOFF: Config<Duration> = Config::new(
1144    "persist_next_listen_batch_retryer_initial_backoff",
1145    Duration::from_millis(100), // pubsub is on by default!
1146    "The initial backoff when polling for new batches from a Listen or Subscribe.",
1147);
1148
1149pub(crate) const NEXT_LISTEN_BATCH_RETRYER_MULTIPLIER: Config<u32> = Config::new(
1150    "persist_next_listen_batch_retryer_multiplier",
1151    2,
1152    "The backoff multiplier when polling for new batches from a Listen or Subscribe.",
1153);
1154
1155pub(crate) const NEXT_LISTEN_BATCH_RETRYER_CLAMP: Config<Duration> = Config::new(
1156    "persist_next_listen_batch_retryer_clamp",
1157    Duration::from_secs(16), // pubsub is on by default!
1158    "The backoff clamp duration when polling for new batches from a Listen or Subscribe.",
1159);
1160
1161pub(crate) fn next_listen_batch_retry_params(cfg: &ConfigSet) -> RetryParameters {
1162    RetryParameters {
1163        fixed_sleep: NEXT_LISTEN_BATCH_RETRYER_FIXED_SLEEP.get(cfg),
1164        initial_backoff: NEXT_LISTEN_BATCH_RETRYER_INITIAL_BACKOFF.get(cfg),
1165        multiplier: NEXT_LISTEN_BATCH_RETRYER_MULTIPLIER.get(cfg),
1166        clamp: NEXT_LISTEN_BATCH_RETRYER_CLAMP.get(cfg),
1167    }
1168}
1169
1170pub const INFO_MIN_ATTEMPTS: usize = 3;
1171
1172/// Attempts after which a still-failing retry loop escalates from INFO to WARN.
1173/// `retry_external` always uses the persist backoff (clamped at 16s), so this
1174/// is roughly five minutes of continuous failure: well past transient retries,
1175/// and a sign the operation (e.g. a blob whose GET never returns) is wedged.
1176pub const WARN_MIN_ATTEMPTS: usize = 30;
1177
1178pub async fn retry_external<R, F, WorkFn>(metrics: &RetryMetrics, mut work_fn: WorkFn) -> R
1179where
1180    F: std::future::Future<Output = Result<R, ExternalError>>,
1181    WorkFn: FnMut() -> F,
1182{
1183    let mut retry = metrics.stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
1184    loop {
1185        match work_fn().await {
1186            Ok(x) => {
1187                if retry.attempt() > 0 {
1188                    debug!(
1189                        "external operation {} succeeded after failing at least once",
1190                        metrics.name,
1191                    );
1192                }
1193                return x;
1194            }
1195            Err(err) => {
1196                if retry.attempt() >= WARN_MIN_ATTEMPTS {
1197                    warn!(
1198                        "external operation {} has failed {} times, retrying in {:?}: {}",
1199                        metrics.name,
1200                        retry.attempt(),
1201                        retry.next_sleep(),
1202                        err.display_with_causes()
1203                    );
1204                } else if retry.attempt() >= INFO_MIN_ATTEMPTS {
1205                    info!(
1206                        "external operation {} failed, retrying in {:?}: {}",
1207                        metrics.name,
1208                        retry.next_sleep(),
1209                        err.display_with_causes()
1210                    );
1211                } else {
1212                    debug!(
1213                        "external operation {} failed, retrying in {:?}: {}",
1214                        metrics.name,
1215                        retry.next_sleep(),
1216                        err.display_with_causes()
1217                    );
1218                }
1219                retry = retry.sleep().await;
1220            }
1221        }
1222    }
1223}
1224
1225pub async fn retry_determinate<R, F, WorkFn>(
1226    metrics: &RetryMetrics,
1227    mut work_fn: WorkFn,
1228) -> Result<R, Indeterminate>
1229where
1230    F: std::future::Future<Output = Result<R, ExternalError>>,
1231    WorkFn: FnMut() -> F,
1232{
1233    let mut retry = metrics.stream(Retry::persist_defaults(SystemTime::now()).into_retry_stream());
1234    loop {
1235        match work_fn().await {
1236            Ok(x) => {
1237                if retry.attempt() > 0 {
1238                    debug!(
1239                        "external operation {} succeeded after failing at least once",
1240                        metrics.name,
1241                    );
1242                }
1243                return Ok(x);
1244            }
1245            Err(ExternalError::Determinate(err)) => {
1246                // The determinate "could not serialize access" errors
1247                // happen often enough in dev (which uses Postgres) that
1248                // it's impeding people's work. At the same time, it's been
1249                // a source of confusion for eng. The situation is much
1250                // better on CRDB and we have metrics coverage in prod, so
1251                // this is redundant enough that it's more hurtful than
1252                // helpful. As a result, this intentionally ignores
1253                // INFO_MIN_ATTEMPTS and always logs at debug.
1254                debug!(
1255                    "external operation {} failed, retrying in {:?}: {}",
1256                    metrics.name,
1257                    retry.next_sleep(),
1258                    err.display_with_causes()
1259                );
1260                retry = retry.sleep().await;
1261                continue;
1262            }
1263            Err(ExternalError::Indeterminate(x)) => return Err(x),
1264        }
1265    }
1266}
1267
1268#[cfg(test)]
1269pub mod datadriven {
1270    use std::collections::{BTreeMap, BTreeSet};
1271    use std::pin::pin;
1272    use std::sync::{Arc, LazyLock};
1273
1274    use anyhow::anyhow;
1275    use differential_dataflow::consolidation::consolidate_updates;
1276    use differential_dataflow::trace::Description;
1277    use futures::StreamExt;
1278    use mz_dyncfg::{ConfigUpdates, ConfigVal};
1279    use mz_persist::indexed::encoding::BlobTraceBatchPart;
1280    use mz_persist_types::codec_impls::{StringSchema, UnitSchema};
1281
1282    use crate::batch::{
1283        BLOB_TARGET_SIZE, Batch, BatchBuilder, BatchBuilderConfig, BatchBuilderInternal,
1284        BatchParts, validate_truncate_batch,
1285    };
1286    use crate::cfg::COMPACTION_MEMORY_BOUND_BYTES;
1287    use crate::fetch::{EncodedPart, FetchConfig};
1288    use crate::internal::compact::{CompactConfig, CompactReq, Compactor};
1289    use crate::internal::datadriven::DirectiveArgs;
1290    use crate::internal::encoding::Schemas;
1291    use crate::internal::gc::GcReq;
1292    use crate::internal::paths::{BlobKey, BlobKeyPrefix, PartialBlobKey};
1293    use crate::internal::state::{BatchPart, RunOrder, RunPart, Upper};
1294    use crate::internal::state_versions::EncodedRollup;
1295    use crate::internal::trace::{CompactionInput, IdHollowBatch, SpineId};
1296    use crate::read::{Listen, ListenEvent, READER_LEASE_DURATION};
1297    use crate::rpc::NoopPubSubSender;
1298    use crate::tests::new_test_client;
1299    use crate::write::COMBINE_INLINE_WRITES;
1300    use crate::{GarbageCollector, PersistClient};
1301
1302    use super::*;
1303
1304    static SCHEMAS: LazyLock<Schemas<String, ()>> = LazyLock::new(|| Schemas {
1305        id: Some(SchemaId(0)),
1306        key: Arc::new(StringSchema),
1307        val: Arc::new(UnitSchema),
1308    });
1309
1310    /// Shared state for a single [crate::internal::machine] [datadriven::TestFile].
1311    #[derive(Debug)]
1312    pub struct MachineState {
1313        pub client: PersistClient,
1314        pub shard_id: ShardId,
1315        pub state_versions: Arc<StateVersions>,
1316        pub machine: Machine<String, (), u64, i64>,
1317        pub gc: GarbageCollector<String, (), u64, i64>,
1318        pub batches: BTreeMap<String, IdHollowBatch<u64>>,
1319        pub next_id: usize,
1320        pub rollups: BTreeMap<String, EncodedRollup>,
1321        pub listens: BTreeMap<String, Listen<String, (), u64, i64>>,
1322        pub routine: Vec<RoutineMaintenance>,
1323        pub compactions: BTreeMap<String, CompactReq<u64>>,
1324    }
1325
1326    impl MachineState {
1327        pub async fn new(dyncfgs: &ConfigUpdates) -> Self {
1328            let shard_id = ShardId::new();
1329            let client = new_test_client(dyncfgs).await;
1330            // Reset blob_target_size. Individual batch writes and compactions
1331            // can override it with an arg.
1332            client
1333                .cfg
1334                .set_config(&BLOB_TARGET_SIZE, *BLOB_TARGET_SIZE.default());
1335            // Our structured compaction code uses slightly different estimates
1336            // for array size than the old path, which can affect the results of
1337            // some compaction tests.
1338            client.cfg.set_config(&COMBINE_INLINE_WRITES, false);
1339            let state_versions = Arc::new(StateVersions::new(
1340                client.cfg.clone(),
1341                Arc::clone(&client.consensus),
1342                Arc::clone(&client.blob),
1343                Arc::clone(&client.metrics),
1344            ));
1345            let machine = Machine::new(
1346                client.cfg.clone(),
1347                shard_id,
1348                Arc::clone(&client.metrics),
1349                Arc::clone(&state_versions),
1350                Arc::clone(&client.shared_states),
1351                Arc::new(NoopPubSubSender),
1352                Arc::clone(&client.isolated_runtime),
1353                Diagnostics::for_tests(),
1354            )
1355            .await
1356            .expect("codecs should match");
1357            let gc = GarbageCollector::new(machine.clone(), Arc::clone(&client.isolated_runtime));
1358            MachineState {
1359                shard_id,
1360                client,
1361                state_versions,
1362                machine,
1363                gc,
1364                batches: BTreeMap::default(),
1365                rollups: BTreeMap::default(),
1366                listens: BTreeMap::default(),
1367                routine: Vec::new(),
1368                compactions: BTreeMap::default(),
1369                next_id: 0,
1370            }
1371        }
1372
1373        fn to_batch(&self, hollow: HollowBatch<u64>) -> Batch<String, (), u64, i64> {
1374            Batch::new(
1375                true,
1376                Arc::clone(&self.client.metrics),
1377                Arc::clone(&self.client.blob),
1378                self.client.metrics.shards.shard(&self.shard_id, "test"),
1379                self.client.cfg.build_version.clone(),
1380                (
1381                    <String>::encode_schema(&*SCHEMAS.key),
1382                    <()>::encode_schema(&*SCHEMAS.val),
1383                ),
1384                hollow,
1385            )
1386        }
1387    }
1388
1389    /// Scans consensus and returns all states with their SeqNos
1390    /// and which batches they reference
1391    pub async fn consensus_scan(
1392        datadriven: &MachineState,
1393        args: DirectiveArgs<'_>,
1394    ) -> Result<String, anyhow::Error> {
1395        let from = args.expect("from_seqno");
1396
1397        let mut states = datadriven
1398            .state_versions
1399            .fetch_all_live_states::<u64>(datadriven.shard_id)
1400            .await
1401            .expect("should only be called on an initialized shard")
1402            .check_ts_codec()
1403            .expect("shard codecs should not change");
1404        let mut s = String::new();
1405        while let Some(x) = states.next(|_| {}) {
1406            if x.seqno < from {
1407                continue;
1408            }
1409            let rollups: Vec<_> = x
1410                .collections
1411                .rollups
1412                .keys()
1413                .map(|seqno| seqno.to_string())
1414                .collect();
1415            let batches: Vec<_> = x
1416                .collections
1417                .trace
1418                .batches()
1419                .filter(|b| !b.is_empty())
1420                .filter_map(|b| {
1421                    datadriven
1422                        .batches
1423                        .iter()
1424                        .find(|(_, original_batch)| original_batch.batch.parts == b.parts)
1425                        .map(|(batch_name, _)| batch_name.to_owned())
1426                })
1427                .collect();
1428            write!(
1429                s,
1430                "seqno={} batches={} rollups={}\n",
1431                x.seqno,
1432                batches.join(","),
1433                rollups.join(","),
1434            );
1435        }
1436        Ok(s)
1437    }
1438
1439    pub async fn consensus_truncate(
1440        datadriven: &MachineState,
1441        args: DirectiveArgs<'_>,
1442    ) -> Result<String, anyhow::Error> {
1443        let to = args.expect("to_seqno");
1444        let removed = datadriven
1445            .client
1446            .consensus
1447            .truncate(&datadriven.shard_id.to_string(), to)
1448            .await
1449            .expect("valid truncation");
1450        Ok(format!("{:?}\n", removed))
1451    }
1452
1453    pub async fn blob_scan_batches(
1454        datadriven: &MachineState,
1455        _args: DirectiveArgs<'_>,
1456    ) -> Result<String, anyhow::Error> {
1457        let key_prefix = BlobKeyPrefix::Shard(&datadriven.shard_id).to_string();
1458
1459        let mut s = String::new();
1460        let () = datadriven
1461            .state_versions
1462            .blob
1463            .list_keys_and_metadata(&key_prefix, &mut |x| {
1464                let (_, key) = BlobKey::parse_ids(x.key).expect("key should be valid");
1465                if let PartialBlobKey::Batch(_, _) = key {
1466                    write!(s, "{}: {}b\n", x.key, x.size_in_bytes);
1467                }
1468            })
1469            .await?;
1470        Ok(s)
1471    }
1472
1473    #[allow(clippy::unused_async)]
1474    pub async fn shard_desc(
1475        datadriven: &MachineState,
1476        _args: DirectiveArgs<'_>,
1477    ) -> Result<String, anyhow::Error> {
1478        Ok(format!(
1479            "since={:?} upper={:?}\n",
1480            datadriven.machine.applier.since().elements(),
1481            datadriven.machine.applier.clone_upper().elements()
1482        ))
1483    }
1484
1485    pub async fn downgrade_since(
1486        datadriven: &mut MachineState,
1487        args: DirectiveArgs<'_>,
1488    ) -> Result<String, anyhow::Error> {
1489        let since = args.expect_antichain("since");
1490        let seqno = args
1491            .optional("seqno")
1492            .unwrap_or_else(|| datadriven.machine.seqno());
1493        let reader_id = args.expect("reader_id");
1494        let (_, since, routine) = datadriven
1495            .machine
1496            .downgrade_since(
1497                &reader_id,
1498                seqno,
1499                &since,
1500                (datadriven.machine.applier.cfg.now)(),
1501            )
1502            .await;
1503        datadriven.routine.push(routine);
1504        Ok(format!(
1505            "{} {:?}\n",
1506            datadriven.machine.seqno(),
1507            since.0.elements()
1508        ))
1509    }
1510
1511    #[allow(clippy::unused_async)]
1512    pub async fn dyncfg(
1513        datadriven: &MachineState,
1514        args: DirectiveArgs<'_>,
1515    ) -> Result<String, anyhow::Error> {
1516        let mut updates = ConfigUpdates::default();
1517        for x in args.input.trim().split('\n') {
1518            match x.split(' ').collect::<Vec<_>>().as_slice() {
1519                &[name, val] => {
1520                    let config = datadriven
1521                        .client
1522                        .cfg
1523                        .entries()
1524                        .find(|x| x.name() == name)
1525                        .ok_or_else(|| anyhow!("unknown dyncfg: {}", name))?;
1526                    match config.val() {
1527                        ConfigVal::Usize(_) => {
1528                            let val = val.parse().map_err(anyhow::Error::new)?;
1529                            updates.add_dynamic(name, ConfigVal::Usize(val));
1530                        }
1531                        ConfigVal::Bool(_) => {
1532                            let val = val.parse().map_err(anyhow::Error::new)?;
1533                            updates.add_dynamic(name, ConfigVal::Bool(val));
1534                        }
1535                        x => unimplemented!("dyncfg type: {:?}", x),
1536                    }
1537                }
1538                x => return Err(anyhow!("expected `name val` got: {:?}", x)),
1539            }
1540        }
1541        updates.apply(&datadriven.client.cfg);
1542
1543        Ok("ok\n".to_string())
1544    }
1545
1546    pub async fn compare_and_downgrade_since(
1547        datadriven: &mut MachineState,
1548        args: DirectiveArgs<'_>,
1549    ) -> Result<String, anyhow::Error> {
1550        let expected_opaque: u64 = args.expect("expect_opaque");
1551        let new_opaque: u64 = args.expect("opaque");
1552        let new_since = args.expect_antichain("since");
1553        let reader_id = args.expect("reader_id");
1554        let (res, routine) = datadriven
1555            .machine
1556            .compare_and_downgrade_since(
1557                &reader_id,
1558                &Opaque::encode(&expected_opaque),
1559                (&Opaque::encode(&new_opaque), &new_since),
1560            )
1561            .await;
1562        datadriven.routine.push(routine);
1563        let since = res.map_err(|(opaque, since)| {
1564            anyhow!(
1565                "mismatch: opaque={} since={:?}",
1566                opaque.decode::<u64>(),
1567                since.0.elements()
1568            )
1569        })?;
1570        Ok(format!(
1571            "{} {} {:?}\n",
1572            datadriven.machine.seqno(),
1573            new_opaque,
1574            since.0.elements()
1575        ))
1576    }
1577
1578    pub async fn write_rollup(
1579        datadriven: &mut MachineState,
1580        args: DirectiveArgs<'_>,
1581    ) -> Result<String, anyhow::Error> {
1582        let output = args.expect_str("output");
1583
1584        let rollup = datadriven
1585            .machine
1586            .applier
1587            .write_rollup_for_state()
1588            .await
1589            .expect("rollup");
1590
1591        datadriven
1592            .rollups
1593            .insert(output.to_string(), rollup.clone());
1594
1595        Ok(format!(
1596            "state={} diffs=[{}, {})\n",
1597            rollup.seqno,
1598            rollup._desc.lower().first().expect("seqno"),
1599            rollup._desc.upper().first().expect("seqno"),
1600        ))
1601    }
1602
1603    pub async fn add_rollup(
1604        datadriven: &mut MachineState,
1605        args: DirectiveArgs<'_>,
1606    ) -> Result<String, anyhow::Error> {
1607        let input = args.expect_str("input");
1608        let rollup = datadriven
1609            .rollups
1610            .get(input)
1611            .expect("unknown batch")
1612            .clone();
1613
1614        let (applied, maintenance) = datadriven
1615            .machine
1616            .add_rollup((rollup.seqno, &rollup.to_hollow()))
1617            .await;
1618
1619        if !applied {
1620            return Err(anyhow!("failed to apply rollup for: {}", rollup.seqno));
1621        }
1622
1623        datadriven.routine.push(maintenance);
1624        Ok(format!("{}\n", datadriven.machine.seqno()))
1625    }
1626
1627    pub async fn write_batch(
1628        datadriven: &mut MachineState,
1629        args: DirectiveArgs<'_>,
1630    ) -> Result<String, anyhow::Error> {
1631        let output = args.expect_str("output");
1632        let lower = args.expect_antichain("lower");
1633        let upper = args.expect_antichain("upper");
1634        assert!(PartialOrder::less_than(&lower, &upper));
1635        let since = args
1636            .optional_antichain("since")
1637            .unwrap_or_else(|| Antichain::from_elem(0));
1638        let target_size = args.optional("target_size");
1639        let parts_size_override = args.optional("parts_size_override");
1640        let consolidate = args.optional("consolidate").unwrap_or(true);
1641        let mut updates: Vec<_> = args
1642            .input
1643            .split('\n')
1644            .flat_map(DirectiveArgs::parse_update)
1645            .collect();
1646
1647        let mut cfg = BatchBuilderConfig::new(&datadriven.client.cfg, datadriven.shard_id);
1648        if let Some(target_size) = target_size {
1649            cfg.blob_target_size = target_size;
1650        };
1651        if consolidate {
1652            consolidate_updates(&mut updates);
1653        }
1654        let run_order = if consolidate {
1655            cfg.preferred_order
1656        } else {
1657            RunOrder::Unordered
1658        };
1659        let parts = BatchParts::new_ordered::<i64>(
1660            cfg.clone(),
1661            run_order,
1662            Arc::clone(&datadriven.client.metrics),
1663            Arc::clone(&datadriven.machine.applier.shard_metrics),
1664            datadriven.shard_id,
1665            Arc::clone(&datadriven.client.blob),
1666            Arc::clone(&datadriven.client.isolated_runtime),
1667            &datadriven.client.metrics.user,
1668        );
1669        let builder = BatchBuilderInternal::new(
1670            cfg.clone(),
1671            parts,
1672            Arc::clone(&datadriven.client.metrics),
1673            SCHEMAS.clone(),
1674            Arc::clone(&datadriven.client.blob),
1675            datadriven.shard_id.clone(),
1676            datadriven.client.cfg.build_version.clone(),
1677        );
1678        let mut builder = BatchBuilder::new(builder, Description::new(lower, upper.clone(), since));
1679        for ((k, ()), t, d) in updates {
1680            builder.add(&k, &(), &t, &d).await.expect("invalid batch");
1681        }
1682        let mut batch = builder.finish(upper).await?;
1683        // We can only reasonably use parts_size_override with hollow batches,
1684        // so if it's set, flush any inline batches out.
1685        if parts_size_override.is_some() {
1686            batch
1687                .flush_to_blob(
1688                    &cfg,
1689                    &datadriven.client.metrics.user,
1690                    &datadriven.client.isolated_runtime,
1691                    &SCHEMAS,
1692                )
1693                .await;
1694        }
1695        let batch = batch.into_hollow_batch();
1696        let batch = IdHollowBatch {
1697            batch: Arc::new(batch),
1698            id: SpineId(datadriven.next_id, datadriven.next_id + 1),
1699        };
1700        datadriven.next_id += 1;
1701
1702        if let Some(size) = parts_size_override {
1703            let mut batch = batch.clone();
1704            let mut hollow_batch = (*batch.batch).clone();
1705            for part in hollow_batch.parts.iter_mut() {
1706                match part {
1707                    RunPart::Many(run) => run.max_part_bytes = size,
1708                    RunPart::Single(BatchPart::Hollow(part)) => part.encoded_size_bytes = size,
1709                    RunPart::Single(BatchPart::Inline { .. }) => unreachable!("flushed out above"),
1710                }
1711            }
1712            batch.batch = Arc::new(hollow_batch);
1713            datadriven.batches.insert(output.to_owned(), batch);
1714        } else {
1715            datadriven.batches.insert(output.to_owned(), batch.clone());
1716        }
1717        Ok(format!(
1718            "parts={} len={}\n",
1719            batch.batch.part_count(),
1720            batch.batch.len
1721        ))
1722    }
1723
1724    pub async fn fetch_batch(
1725        datadriven: &MachineState,
1726        args: DirectiveArgs<'_>,
1727    ) -> Result<String, anyhow::Error> {
1728        let input = args.expect_str("input");
1729        let stats = args.optional_str("stats");
1730        let batch = datadriven.batches.get(input).expect("unknown batch");
1731
1732        let mut s = String::new();
1733        let mut stream = pin!(
1734            batch
1735                .batch
1736                .part_stream(
1737                    datadriven.shard_id,
1738                    &*datadriven.state_versions.blob,
1739                    &*datadriven.state_versions.metrics
1740                )
1741                .enumerate()
1742        );
1743        while let Some((idx, part)) = stream.next().await {
1744            let part = &*part?;
1745            write!(s, "<part {idx}>\n");
1746
1747            let lower = match part {
1748                BatchPart::Inline { updates, .. } => {
1749                    let updates: BlobTraceBatchPart<u64> =
1750                        updates.decode(&datadriven.client.metrics.columnar)?;
1751                    updates.structured_key_lower()
1752                }
1753                other @ BatchPart::Hollow(_) => other.structured_key_lower(),
1754            };
1755
1756            if let Some(lower) = lower {
1757                if stats == Some("lower") {
1758                    writeln!(s, "<key lower={}>", lower.get())
1759                }
1760            }
1761
1762            match part {
1763                BatchPart::Hollow(part) => {
1764                    let blob_batch = datadriven
1765                        .client
1766                        .blob
1767                        .get(&part.key.complete(&datadriven.shard_id))
1768                        .await;
1769                    match blob_batch {
1770                        Ok(Some(_)) | Err(_) => {}
1771                        // don't try to fetch/print the keys of the batch part
1772                        // if the blob store no longer has it
1773                        Ok(None) => {
1774                            s.push_str("<empty>\n");
1775                            continue;
1776                        }
1777                    };
1778                }
1779                BatchPart::Inline { .. } => {}
1780            };
1781            let part = EncodedPart::fetch(
1782                &FetchConfig::from_persist_config(&datadriven.client.cfg),
1783                &datadriven.shard_id,
1784                datadriven.client.blob.as_ref(),
1785                datadriven.client.metrics.as_ref(),
1786                datadriven.machine.applier.shard_metrics.as_ref(),
1787                &datadriven.client.metrics.read.batch_fetcher,
1788                &batch.batch.desc,
1789                part,
1790            )
1791            .await
1792            .expect("invalid batch part");
1793            let part = part
1794                .normalize(&datadriven.client.metrics.columnar)
1795                .into_part::<String, ()>(&*SCHEMAS.key, &*SCHEMAS.val);
1796
1797            for ((k, _v), t, d) in part
1798                .decode_iter::<_, _, u64, i64>(&*SCHEMAS.key, &*SCHEMAS.val)
1799                .expect("valid schemas")
1800            {
1801                writeln!(s, "{k} {t} {d}");
1802            }
1803        }
1804        if !s.is_empty() {
1805            for (idx, (_meta, run)) in batch.batch.runs().enumerate() {
1806                write!(s, "<run {idx}>\n");
1807                for part in run {
1808                    let part_idx = batch
1809                        .batch
1810                        .parts
1811                        .iter()
1812                        .position(|p| p == part)
1813                        .expect("part should exist");
1814                    write!(s, "part {part_idx}\n");
1815                }
1816            }
1817        }
1818        Ok(s)
1819    }
1820
1821    #[allow(clippy::unused_async)]
1822    pub async fn truncate_batch_desc(
1823        datadriven: &mut MachineState,
1824        args: DirectiveArgs<'_>,
1825    ) -> Result<String, anyhow::Error> {
1826        let input = args.expect_str("input");
1827        let output = args.expect_str("output");
1828        let lower = args.expect_antichain("lower");
1829        let upper = args.expect_antichain("upper");
1830
1831        let batch = datadriven
1832            .batches
1833            .get(input)
1834            .expect("unknown batch")
1835            .clone();
1836        let truncated_desc = Description::new(lower, upper, batch.batch.desc.since().clone());
1837        let () = validate_truncate_batch(&batch.batch, &truncated_desc, false, true)?;
1838        let mut new_hollow_batch = (*batch.batch).clone();
1839        new_hollow_batch.desc = truncated_desc;
1840        let new_batch = IdHollowBatch {
1841            batch: Arc::new(new_hollow_batch),
1842            id: batch.id,
1843        };
1844        datadriven
1845            .batches
1846            .insert(output.to_owned(), new_batch.clone());
1847        Ok(format!(
1848            "parts={} len={}\n",
1849            batch.batch.part_count(),
1850            batch.batch.len
1851        ))
1852    }
1853
1854    #[allow(clippy::unused_async)]
1855    pub async fn set_batch_parts_size(
1856        datadriven: &mut MachineState,
1857        args: DirectiveArgs<'_>,
1858    ) -> Result<String, anyhow::Error> {
1859        let input = args.expect_str("input");
1860        let size = args.expect("size");
1861        let batch = datadriven.batches.get_mut(input).expect("unknown batch");
1862        let mut hollow_batch = (*batch.batch).clone();
1863        for part in hollow_batch.parts.iter_mut() {
1864            match part {
1865                RunPart::Single(BatchPart::Hollow(x)) => x.encoded_size_bytes = size,
1866                _ => {
1867                    panic!("set_batch_parts_size only supports hollow parts")
1868                }
1869            }
1870        }
1871        batch.batch = Arc::new(hollow_batch);
1872        Ok("ok\n".to_string())
1873    }
1874
1875    pub async fn compact(
1876        datadriven: &mut MachineState,
1877        args: DirectiveArgs<'_>,
1878    ) -> Result<String, anyhow::Error> {
1879        let output = args.expect_str("output");
1880        let lower = args.expect_antichain("lower");
1881        let upper = args.expect_antichain("upper");
1882        let since = args.expect_antichain("since");
1883        let target_size = args.optional("target_size");
1884        let memory_bound = args.optional("memory_bound");
1885
1886        let mut inputs = Vec::new();
1887        for input in args.args.get("inputs").expect("missing inputs") {
1888            inputs.push(
1889                datadriven
1890                    .batches
1891                    .get(input)
1892                    .expect("unknown batch")
1893                    .clone(),
1894            );
1895        }
1896
1897        let cfg = datadriven.client.cfg.clone();
1898        if let Some(target_size) = target_size {
1899            cfg.set_config(&BLOB_TARGET_SIZE, target_size);
1900        };
1901        if let Some(memory_bound) = memory_bound {
1902            cfg.set_config(&COMPACTION_MEMORY_BOUND_BYTES, memory_bound);
1903        }
1904        let req = CompactReq {
1905            shard_id: datadriven.shard_id,
1906            desc: Description::new(lower, upper, since),
1907            inputs: inputs.clone(),
1908        };
1909        datadriven
1910            .compactions
1911            .insert(output.to_owned(), req.clone());
1912        let spine_lower = inputs
1913            .first()
1914            .map_or_else(|| datadriven.next_id, |x| x.id.0);
1915        let spine_upper = inputs.last().map_or_else(
1916            || {
1917                datadriven.next_id += 1;
1918                datadriven.next_id
1919            },
1920            |x| x.id.1,
1921        );
1922        let new_spine_id = SpineId(spine_lower, spine_upper);
1923        let res = Compactor::<String, (), u64, i64>::compact(
1924            CompactConfig::new(&cfg, datadriven.shard_id),
1925            Arc::clone(&datadriven.client.blob),
1926            Arc::clone(&datadriven.client.metrics),
1927            Arc::clone(&datadriven.machine.applier.shard_metrics),
1928            Arc::clone(&datadriven.client.isolated_runtime),
1929            req,
1930            SCHEMAS.clone(),
1931        )
1932        .await?;
1933
1934        let batch = IdHollowBatch {
1935            batch: Arc::new(res.output.clone()),
1936            id: new_spine_id,
1937        };
1938
1939        datadriven.batches.insert(output.to_owned(), batch.clone());
1940        Ok(format!(
1941            "parts={} len={}\n",
1942            res.output.part_count(),
1943            res.output.len
1944        ))
1945    }
1946
1947    pub async fn clear_blob(
1948        datadriven: &MachineState,
1949        _args: DirectiveArgs<'_>,
1950    ) -> Result<String, anyhow::Error> {
1951        let mut to_delete = vec![];
1952        datadriven
1953            .client
1954            .blob
1955            .list_keys_and_metadata("", &mut |meta| {
1956                to_delete.push(meta.key.to_owned());
1957            })
1958            .await?;
1959        for blob in &to_delete {
1960            datadriven.client.blob.delete(blob).await?;
1961        }
1962        Ok(format!("deleted={}\n", to_delete.len()))
1963    }
1964
1965    pub async fn restore_blob(
1966        datadriven: &MachineState,
1967        _args: DirectiveArgs<'_>,
1968    ) -> Result<String, anyhow::Error> {
1969        let not_restored = crate::internal::restore::restore_blob(
1970            &datadriven.state_versions,
1971            datadriven.client.blob.as_ref(),
1972            &datadriven.client.cfg.build_version,
1973            datadriven.shard_id,
1974            &*datadriven.state_versions.metrics,
1975        )
1976        .await?;
1977        let mut out = String::new();
1978        for key in not_restored {
1979            writeln!(&mut out, "{key}");
1980        }
1981        Ok(out)
1982    }
1983
1984    #[allow(clippy::unused_async)]
1985    pub async fn rewrite_ts(
1986        datadriven: &mut MachineState,
1987        args: DirectiveArgs<'_>,
1988    ) -> Result<String, anyhow::Error> {
1989        let input = args.expect_str("input");
1990        let ts_rewrite = args.expect_antichain("frontier");
1991        let upper = args.expect_antichain("upper");
1992
1993        let batch = datadriven.batches.get_mut(input).expect("unknown batch");
1994        let mut hollow_batch = (*batch.batch).clone();
1995        let () = hollow_batch
1996            .rewrite_ts(&ts_rewrite, upper)
1997            .map_err(|err| anyhow!("invalid rewrite: {}", err))?;
1998        batch.batch = Arc::new(hollow_batch);
1999        Ok("ok\n".into())
2000    }
2001
2002    pub async fn gc(
2003        datadriven: &mut MachineState,
2004        args: DirectiveArgs<'_>,
2005    ) -> Result<String, anyhow::Error> {
2006        let new_seqno_since = args.expect("to_seqno");
2007
2008        let req = GcReq {
2009            shard_id: datadriven.shard_id,
2010            new_seqno_since,
2011        };
2012        let (maintenance, stats) =
2013            GarbageCollector::gc_and_truncate(&datadriven.machine, req).await;
2014        datadriven.routine.push(maintenance);
2015
2016        Ok(format!(
2017            "{} batch_parts={} rollups={} truncated={} state_rollups={}\n",
2018            datadriven.machine.seqno(),
2019            stats.batch_parts_deleted_from_blob,
2020            stats.rollups_deleted_from_blob,
2021            stats
2022                .truncated_consensus_to
2023                .iter()
2024                .map(|x| x.to_string())
2025                .collect::<Vec<_>>()
2026                .join(","),
2027            stats
2028                .rollups_removed_from_state
2029                .iter()
2030                .map(|x| x.to_string())
2031                .collect::<Vec<_>>()
2032                .join(","),
2033        ))
2034    }
2035
2036    pub async fn snapshot(
2037        datadriven: &MachineState,
2038        args: DirectiveArgs<'_>,
2039    ) -> Result<String, anyhow::Error> {
2040        let as_of = args.expect_antichain("as_of");
2041        let snapshot = datadriven
2042            .machine
2043            .unleased_snapshot(&as_of)
2044            .await
2045            .map_err(|err| anyhow!("{:?}", err))?;
2046
2047        let mut result = String::new();
2048
2049        for batch in snapshot {
2050            writeln!(
2051                result,
2052                "<batch {:?}-{:?}>",
2053                batch.desc.lower().elements(),
2054                batch.desc.upper().elements()
2055            );
2056            for (run, (_meta, parts)) in batch.runs().enumerate() {
2057                writeln!(result, "<run {run}>");
2058                let mut stream = pin!(
2059                    futures::stream::iter(parts)
2060                        .flat_map(|part| part.part_stream(
2061                            datadriven.shard_id,
2062                            &*datadriven.state_versions.blob,
2063                            &*datadriven.state_versions.metrics
2064                        ))
2065                        .enumerate()
2066                );
2067
2068                while let Some((idx, part)) = stream.next().await {
2069                    let part = &*part?;
2070                    writeln!(result, "<part {idx}>");
2071
2072                    let part = EncodedPart::fetch(
2073                        &FetchConfig::from_persist_config(&datadriven.client.cfg),
2074                        &datadriven.shard_id,
2075                        datadriven.client.blob.as_ref(),
2076                        datadriven.client.metrics.as_ref(),
2077                        datadriven.machine.applier.shard_metrics.as_ref(),
2078                        &datadriven.client.metrics.read.batch_fetcher,
2079                        &batch.desc,
2080                        part,
2081                    )
2082                    .await
2083                    .expect("invalid batch part");
2084                    let part = part
2085                        .normalize(&datadriven.client.metrics.columnar)
2086                        .into_part::<String, ()>(&*SCHEMAS.key, &*SCHEMAS.val);
2087
2088                    let mut updates = Vec::new();
2089
2090                    for ((k, _v), mut t, d) in part
2091                        .decode_iter::<_, _, u64, i64>(&*SCHEMAS.key, &*SCHEMAS.val)
2092                        .expect("valid schemas")
2093                    {
2094                        t.advance_by(as_of.borrow());
2095                        updates.push((k, t, d));
2096                    }
2097
2098                    consolidate_updates(&mut updates);
2099
2100                    for (k, t, d) in updates {
2101                        writeln!(result, "{k} {t} {d}");
2102                    }
2103                }
2104            }
2105        }
2106
2107        Ok(result)
2108    }
2109
2110    pub async fn register_listen(
2111        datadriven: &mut MachineState,
2112        args: DirectiveArgs<'_>,
2113    ) -> Result<String, anyhow::Error> {
2114        let output = args.expect_str("output");
2115        let as_of = args.expect_antichain("as_of");
2116        let read = datadriven
2117            .client
2118            .open_leased_reader::<String, (), u64, i64>(
2119                datadriven.shard_id,
2120                Arc::new(StringSchema),
2121                Arc::new(UnitSchema),
2122                Diagnostics::for_tests(),
2123                true,
2124            )
2125            .await
2126            .expect("invalid shard types");
2127        let listen = read
2128            .listen(as_of)
2129            .await
2130            .map_err(|err| anyhow!("{:?}", err))?;
2131        datadriven.listens.insert(output.to_owned(), listen);
2132        Ok("ok\n".into())
2133    }
2134
2135    pub async fn listen_through(
2136        datadriven: &mut MachineState,
2137        args: DirectiveArgs<'_>,
2138    ) -> Result<String, anyhow::Error> {
2139        let input = args.expect_str("input");
2140        // It's not possible to listen _through_ the empty antichain, so this is
2141        // intentionally `expect` instead of `expect_antichain`.
2142        let frontier = args.expect("frontier");
2143        let listen = datadriven.listens.get_mut(input).expect("unknown listener");
2144        let mut s = String::new();
2145        loop {
2146            for event in listen.fetch_next().await {
2147                match event {
2148                    ListenEvent::Updates(x) => {
2149                        for ((k, _v), t, d) in x.iter() {
2150                            write!(s, "{} {} {}\n", k, t, d);
2151                        }
2152                    }
2153                    ListenEvent::Progress(x) => {
2154                        if !x.less_than(&frontier) {
2155                            return Ok(s);
2156                        }
2157                    }
2158                }
2159            }
2160        }
2161    }
2162
2163    pub async fn register_critical_reader(
2164        datadriven: &mut MachineState,
2165        args: DirectiveArgs<'_>,
2166    ) -> Result<String, anyhow::Error> {
2167        let reader_id = args.expect("reader_id");
2168        let (state, maintenance) = datadriven
2169            .machine
2170            .register_critical_reader(&reader_id, Opaque::encode(&0u64), "tests")
2171            .await;
2172        datadriven.routine.push(maintenance);
2173        Ok(format!(
2174            "{} {:?}\n",
2175            datadriven.machine.seqno(),
2176            state.since.elements(),
2177        ))
2178    }
2179
2180    pub async fn register_leased_reader(
2181        datadriven: &mut MachineState,
2182        args: DirectiveArgs<'_>,
2183    ) -> Result<String, anyhow::Error> {
2184        let reader_id = args.expect("reader_id");
2185        let (reader_state, maintenance) = datadriven
2186            .machine
2187            .register_leased_reader(
2188                &reader_id,
2189                "tests",
2190                READER_LEASE_DURATION.get(&datadriven.client.cfg),
2191                (datadriven.client.cfg.now)(),
2192                false,
2193            )
2194            .await;
2195        datadriven.routine.push(maintenance);
2196        Ok(format!(
2197            "{} {:?}\n",
2198            datadriven.machine.seqno(),
2199            reader_state.since.elements(),
2200        ))
2201    }
2202
2203    pub async fn expire_critical_reader(
2204        datadriven: &mut MachineState,
2205        args: DirectiveArgs<'_>,
2206    ) -> Result<String, anyhow::Error> {
2207        let reader_id = args.expect("reader_id");
2208        let (_, maintenance) = datadriven.machine.expire_critical_reader(&reader_id).await;
2209        datadriven.routine.push(maintenance);
2210        Ok(format!("{} ok\n", datadriven.machine.seqno()))
2211    }
2212
2213    pub async fn expire_leased_reader(
2214        datadriven: &mut MachineState,
2215        args: DirectiveArgs<'_>,
2216    ) -> Result<String, anyhow::Error> {
2217        let reader_id = args.expect("reader_id");
2218        let (_, maintenance) = datadriven.machine.expire_leased_reader(&reader_id).await;
2219        datadriven.routine.push(maintenance);
2220        Ok(format!("{} ok\n", datadriven.machine.seqno()))
2221    }
2222
2223    pub async fn compare_and_append_batches(
2224        datadriven: &MachineState,
2225        args: DirectiveArgs<'_>,
2226    ) -> Result<String, anyhow::Error> {
2227        let expected_upper = args.expect_antichain("expected_upper");
2228        let new_upper = args.expect_antichain("new_upper");
2229
2230        let mut batches: Vec<Batch<String, (), u64, i64>> = args
2231            .args
2232            .get("batches")
2233            .expect("missing batches")
2234            .into_iter()
2235            .map(|batch| {
2236                let hollow = (*datadriven
2237                    .batches
2238                    .get(batch)
2239                    .expect("unknown batch")
2240                    .clone()
2241                    .batch)
2242                    .clone();
2243                datadriven.to_batch(hollow)
2244            })
2245            .collect();
2246
2247        let mut writer = datadriven
2248            .client
2249            .open_writer(
2250                datadriven.shard_id,
2251                Arc::new(StringSchema),
2252                Arc::new(UnitSchema),
2253                Diagnostics::for_tests(),
2254            )
2255            .await?;
2256
2257        let mut batch_refs: Vec<_> = batches.iter_mut().collect();
2258
2259        let () = writer
2260            .compare_and_append_batch(batch_refs.as_mut_slice(), expected_upper, new_upper, true)
2261            .await?
2262            .map_err(|err| anyhow!("upper mismatch: {:?}", err))?;
2263
2264        writer.expire().await;
2265
2266        Ok("ok\n".into())
2267    }
2268
2269    pub async fn expire_writer(
2270        datadriven: &mut MachineState,
2271        args: DirectiveArgs<'_>,
2272    ) -> Result<String, anyhow::Error> {
2273        let writer_id = args.expect("writer_id");
2274        let (_, maintenance) = datadriven.machine.expire_writer(&writer_id).await;
2275        datadriven.routine.push(maintenance);
2276        Ok(format!("{} ok\n", datadriven.machine.seqno()))
2277    }
2278
2279    pub(crate) async fn finalize(
2280        datadriven: &mut MachineState,
2281        _args: DirectiveArgs<'_>,
2282    ) -> anyhow::Result<String> {
2283        let maintenance = datadriven.machine.become_tombstone().await?;
2284        datadriven.routine.push(maintenance);
2285        Ok(format!("{} ok\n", datadriven.machine.seqno()))
2286    }
2287
2288    pub(crate) fn is_finalized(
2289        datadriven: &MachineState,
2290        _args: DirectiveArgs<'_>,
2291    ) -> anyhow::Result<String> {
2292        let seqno = datadriven.machine.seqno();
2293        let tombstone = datadriven.machine.is_finalized();
2294        Ok(format!("{seqno} {tombstone}\n"))
2295    }
2296
2297    pub async fn compare_and_append(
2298        datadriven: &mut MachineState,
2299        args: DirectiveArgs<'_>,
2300    ) -> Result<String, anyhow::Error> {
2301        let input = args.expect_str("input");
2302        let writer_id = args.expect("writer_id");
2303        let mut batch = datadriven
2304            .batches
2305            .get(input)
2306            .expect("unknown batch")
2307            .clone();
2308        let token = args.optional("token").unwrap_or_else(IdempotencyToken::new);
2309        let now = (datadriven.client.cfg.now)();
2310
2311        let (id, maintenance) = datadriven
2312            .machine
2313            .register_schema(&*SCHEMAS.key, &*SCHEMAS.val)
2314            .await;
2315        assert_eq!(id, SCHEMAS.id);
2316        datadriven.routine.push(maintenance);
2317        let maintenance = loop {
2318            let indeterminate = args
2319                .optional::<String>("prev_indeterminate")
2320                .map(|x| Indeterminate::new(anyhow::Error::msg(x)));
2321            let res = datadriven
2322                .machine
2323                .compare_and_append_idempotent(
2324                    &batch.batch,
2325                    &writer_id,
2326                    now,
2327                    &token,
2328                    &HandleDebugState::default(),
2329                    indeterminate,
2330                )
2331                .await;
2332            match res {
2333                CompareAndAppendRes::Success(_, x) => break x,
2334                CompareAndAppendRes::UpperMismatch(_seqno, upper) => {
2335                    return Err(anyhow!("{:?}", Upper(upper)));
2336                }
2337                CompareAndAppendRes::InlineBackpressure => {
2338                    let hollow_batch = (*batch.batch).clone();
2339                    let mut b = datadriven.to_batch(hollow_batch);
2340                    let cfg = BatchBuilderConfig::new(&datadriven.client.cfg, datadriven.shard_id);
2341                    b.flush_to_blob(
2342                        &cfg,
2343                        &datadriven.client.metrics.user,
2344                        &datadriven.client.isolated_runtime,
2345                        &*SCHEMAS,
2346                    )
2347                    .await;
2348                    batch.batch = Arc::new(b.into_hollow_batch());
2349                    continue;
2350                }
2351                CompareAndAppendRes::InvalidUsage(_) => panic!("{:?}", res),
2352            };
2353        };
2354        // TODO: Don't throw away writer maintenance. It's slightly tricky
2355        // because we need a WriterId for Compactor.
2356        datadriven.routine.push(maintenance.routine);
2357        Ok(format!(
2358            "{} {:?}\n",
2359            datadriven.machine.seqno(),
2360            datadriven.machine.applier.clone_upper().elements(),
2361        ))
2362    }
2363
2364    pub async fn apply_merge_res(
2365        datadriven: &mut MachineState,
2366        args: DirectiveArgs<'_>,
2367    ) -> Result<String, anyhow::Error> {
2368        let input = args.expect_str("input");
2369        let batch = datadriven
2370            .batches
2371            .get(input)
2372            .expect("unknown batch")
2373            .clone();
2374        let compact_req = datadriven
2375            .compactions
2376            .get(input)
2377            .expect("unknown compact req")
2378            .clone();
2379        let input_batches = compact_req
2380            .inputs
2381            .iter()
2382            .map(|x| x.id)
2383            .collect::<BTreeSet<_>>();
2384        let lower_spine_bound = input_batches
2385            .first()
2386            .map(|id| id.0)
2387            .expect("at least one batch must be present");
2388        let upper_spine_bound = input_batches
2389            .last()
2390            .map(|id| id.1)
2391            .expect("at least one batch must be present");
2392        let id = SpineId(lower_spine_bound, upper_spine_bound);
2393        let hollow_batch = (*batch.batch).clone();
2394
2395        let (merge_res, maintenance) = datadriven
2396            .machine
2397            .merge_res(&FueledMergeRes {
2398                output: hollow_batch,
2399                input: CompactionInput::IdRange(id),
2400                new_active_compaction: None,
2401            })
2402            .await;
2403        datadriven.routine.push(maintenance);
2404        Ok(format!(
2405            "{} {}\n",
2406            datadriven.machine.seqno(),
2407            merge_res.applied()
2408        ))
2409    }
2410
2411    pub async fn perform_maintenance(
2412        datadriven: &mut MachineState,
2413        _args: DirectiveArgs<'_>,
2414    ) -> Result<String, anyhow::Error> {
2415        let mut s = String::new();
2416        for maintenance in datadriven.routine.drain(..) {
2417            let () = maintenance
2418                .perform(&datadriven.machine, &datadriven.gc)
2419                .await;
2420            let () = datadriven
2421                .machine
2422                .applier
2423                .fetch_and_update_state(None)
2424                .await;
2425            write!(s, "{} ok\n", datadriven.machine.seqno());
2426        }
2427        Ok(s)
2428    }
2429}
2430
2431#[cfg(test)]
2432pub mod tests {
2433    use std::sync::Arc;
2434
2435    use mz_dyncfg::ConfigUpdates;
2436    use mz_ore::cast::CastFrom;
2437    use mz_ore::task::spawn;
2438    use mz_persist::intercept::{InterceptBlob, InterceptHandle};
2439    use mz_persist::location::SeqNo;
2440    use mz_persist_types::PersistLocation;
2441    use semver::Version;
2442    use timely::progress::Antichain;
2443
2444    use crate::batch::BatchBuilderConfig;
2445    use crate::cache::StateCache;
2446    use crate::internal::gc::{GarbageCollector, GcReq};
2447    use crate::internal::state::{HandleDebugState, ROLLUP_THRESHOLD};
2448    use crate::tests::{new_test_client, new_test_client_cache};
2449    use crate::{Diagnostics, PersistClient, ShardId};
2450
2451    #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2452    #[cfg_attr(miri, ignore)] // error: unsupported operation: integer-to-pointer casts and `ptr::from_exposed_addr` are not supported with `-Zmiri-strict-provenance`
2453    async fn apply_unbatched_cmd_truncate(dyncfgs: ConfigUpdates) {
2454        mz_ore::test::init_logging();
2455
2456        let client = new_test_client(&dyncfgs).await;
2457        // set a low rollup threshold so GC/truncation is more aggressive
2458        client.cfg.set_config(&ROLLUP_THRESHOLD, 5);
2459        let (mut write, read) = client
2460            .expect_open::<String, (), u64, i64>(ShardId::new())
2461            .await;
2462
2463        // Ensure the reader is not holding back the since.
2464        read.expire().await;
2465
2466        // Write a bunch of batches. This should result in a bounded number of
2467        // live entries in consensus.
2468        const NUM_BATCHES: u64 = 100;
2469        for idx in 0..NUM_BATCHES {
2470            let mut batch = write
2471                .expect_batch(&[((idx.to_string(), ()), idx, 1)], idx, idx + 1)
2472                .await;
2473            // Flush this batch out so the CaA doesn't get inline writes
2474            // backpressure.
2475            let cfg = BatchBuilderConfig::new(&client.cfg, write.shard_id());
2476            batch
2477                .flush_to_blob(
2478                    &cfg,
2479                    &client.metrics.user,
2480                    &client.isolated_runtime,
2481                    &write.write_schemas,
2482                )
2483                .await;
2484            let (_, writer_maintenance) = write
2485                .machine
2486                .compare_and_append(
2487                    &batch.into_hollow_batch(),
2488                    &write.writer_id,
2489                    &HandleDebugState::default(),
2490                    (write.cfg.now)(),
2491                )
2492                .await
2493                .unwrap();
2494            writer_maintenance
2495                .perform(&write.machine, &write.gc, write.compact.as_ref())
2496                .await;
2497        }
2498        let live_diffs = write
2499            .machine
2500            .applier
2501            .state_versions
2502            .fetch_all_live_diffs(&write.machine.shard_id())
2503            .await;
2504        // Make sure we constructed the key correctly.
2505        assert!(live_diffs.len() > 0);
2506        // Make sure the number of entries is bounded. (I think we could work
2507        // out a tighter bound than this, but the point is only that it's
2508        // bounded).
2509        let max_live_diffs = 2 * usize::cast_from(NUM_BATCHES.next_power_of_two().trailing_zeros());
2510        assert!(
2511            live_diffs.len() <= max_live_diffs,
2512            "{} vs {}",
2513            live_diffs.len(),
2514            max_live_diffs
2515        );
2516    }
2517
2518    // A regression test for database-issues#4206, where a bug in gc led to an incremental
2519    // state invariant being violated which resulted in gc being permanently
2520    // wedged for the shard.
2521    #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2522    #[cfg_attr(miri, ignore)] // error: unsupported operation: integer-to-pointer casts and `ptr::from_exposed_addr` are not supported with `-Zmiri-strict-provenance`
2523    async fn regression_gc_skipped_req_and_interrupted(dyncfgs: ConfigUpdates) {
2524        let mut client = new_test_client(&dyncfgs).await;
2525        let intercept = InterceptHandle::default();
2526        client.blob = Arc::new(InterceptBlob::new(
2527            Arc::clone(&client.blob),
2528            intercept.clone(),
2529        ));
2530        let (_, mut read) = client
2531            .expect_open::<String, String, u64, i64>(ShardId::new())
2532            .await;
2533
2534        // Create a new SeqNo
2535        read.downgrade_since(&Antichain::from_elem(1)).await;
2536        let new_seqno_since = read.machine.applier.seqno_since();
2537
2538        // Start a GC in the background for some SeqNo range that is not
2539        // contiguous compared to the last gc req (in this case, n/a) and then
2540        // crash when it gets to the blob deletes. In the regression case, this
2541        // renders the shard permanently un-gc-able.
2542        assert!(new_seqno_since > SeqNo::minimum());
2543        intercept.set_post_delete(Some(Arc::new(|_, _| panic!("boom"))));
2544        let machine = read.machine.clone();
2545        // Run this in a spawn so we can catch the boom panic
2546        let gc = spawn(|| "", async move {
2547            let req = GcReq {
2548                shard_id: machine.shard_id(),
2549                new_seqno_since,
2550            };
2551            GarbageCollector::gc_and_truncate(&machine, req).await
2552        });
2553        // Wait for gc to either panic (regression case) or finish (good case)
2554        // because it happens to not call blob delete.
2555        let _ = gc.await;
2556
2557        // Allow blob deletes to go through and try GC again. In the regression
2558        // case, this hangs.
2559        intercept.set_post_delete(None);
2560        let req = GcReq {
2561            shard_id: read.machine.shard_id(),
2562            new_seqno_since,
2563        };
2564        let _ = GarbageCollector::gc_and_truncate(&read.machine, req.clone()).await;
2565    }
2566
2567    // A regression test for materialize#20776, where a bug meant that compare_and_append
2568    // would not fetch the latest state after an upper mismatch. This meant that
2569    // a write that could succeed if retried on the latest state would instead
2570    // return an UpperMismatch.
2571    #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2572    #[cfg_attr(miri, ignore)] // error: unsupported operation: integer-to-pointer casts and `ptr::from_exposed_addr` are not supported with `-Zmiri-strict-provenance`
2573    async fn regression_update_state_after_upper_mismatch(dyncfgs: ConfigUpdates) {
2574        let client = new_test_client(&dyncfgs).await;
2575        let mut client2 = client.clone();
2576
2577        // The bug can only happen if the two WriteHandles have separate copies
2578        // of state, so make sure that each is given its own StateCache.
2579        let new_state_cache = Arc::new(StateCache::new_no_metrics());
2580        client2.shared_states = new_state_cache;
2581
2582        let shard_id = ShardId::new();
2583        let (mut write1, _) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2584        let (mut write2, _) = client2.expect_open::<String, (), u64, i64>(shard_id).await;
2585
2586        let data = [
2587            (("1".to_owned(), ()), 1, 1),
2588            (("2".to_owned(), ()), 2, 1),
2589            (("3".to_owned(), ()), 3, 1),
2590            (("4".to_owned(), ()), 4, 1),
2591            (("5".to_owned(), ()), 5, 1),
2592        ];
2593
2594        write1.expect_compare_and_append(&data[..1], 0, 2).await;
2595
2596        // this handle's upper now lags behind. if compare_and_append fails to update
2597        // state after an upper mismatch then this call would (incorrectly) fail
2598        write2.expect_compare_and_append(&data[1..2], 2, 3).await;
2599    }
2600
2601    #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
2602    #[cfg_attr(miri, ignore)]
2603    async fn version_upgrade(dyncfgs: ConfigUpdates) {
2604        let mut cache = new_test_client_cache(&dyncfgs);
2605        cache.cfg.build_version = Version::new(26, 1, 0);
2606        let shard_id = ShardId::new();
2607
2608        async fn fetch_catalog_upgrade_shard_version(
2609            persist_client: &PersistClient,
2610            upgrade_shard_id: ShardId,
2611        ) -> Option<semver::Version> {
2612            let shard_state = persist_client
2613                .inspect_shard::<u64>(&upgrade_shard_id)
2614                .await
2615                .ok()?;
2616            let json_state = serde_json::to_value(shard_state).expect("state serialization error");
2617            let upgrade_version = json_state
2618                .get("applier_version")
2619                .cloned()
2620                .expect("missing applier_version");
2621            let upgrade_version =
2622                serde_json::from_value(upgrade_version).expect("version deserialization error");
2623            Some(upgrade_version)
2624        }
2625
2626        cache.cfg.build_version = Version::new(26, 1, 0);
2627        let client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
2628        let (write, mut reader) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2629        reader.downgrade_since(&Antichain::from_elem(1)).await;
2630        assert_eq!(
2631            fetch_catalog_upgrade_shard_version(&client, shard_id).await,
2632            Some(Version::new(26, 1, 0)),
2633        );
2634
2635        // Expire the old-version handles before bumping the build version. They
2636        // share the in-mem state cache, and if their background tasks (reader
2637        // heartbeat / writer expiry) are left to linger they can observe the
2638        // upgraded 27.1.0 state below and panic on the version mismatch
2639        // (apply.rs `code_can_write_data` / encoding.rs `assert_code_can_read_data`),
2640        // poisoning the shared state lock and flaking the test. `expire` does
2641        // the final cleanup at the current version and awaits the background
2642        // task, so nothing at 26.1.0 outlives the upgrade.
2643        write.expire().await;
2644        reader.expire().await;
2645
2646        // Merely opening and operating on the shard at a new version doesn't bump version...
2647        cache.cfg.build_version = Version::new(27, 1, 0);
2648        let client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
2649        let (write, mut reader) = client.expect_open::<String, (), u64, i64>(shard_id).await;
2650        reader.downgrade_since(&Antichain::from_elem(2)).await;
2651        assert_eq!(
2652            fetch_catalog_upgrade_shard_version(&client, shard_id).await,
2653            Some(Version::new(26, 1, 0)),
2654        );
2655
2656        // ...but an explicit call will.
2657        client
2658            .upgrade_version::<String, (), u64, i64>(shard_id, Diagnostics::for_tests())
2659            .await
2660            .unwrap();
2661        assert_eq!(
2662            fetch_catalog_upgrade_shard_version(&client, shard_id).await,
2663            Some(Version::new(27, 1, 0)),
2664        );
2665
2666        write.expire().await;
2667        reader.expire().await;
2668    }
2669}