Skip to main content

mz_persist_client/internal/
machine.rs

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