Skip to main content

mz_persist_client/internal/
machine.rs

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