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