Skip to main content

mz_catalog/durable/
persist.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#[cfg(test)]
11mod tests;
12
13use std::cmp::max;
14use std::collections::{BTreeMap, VecDeque};
15use std::fmt::Debug;
16use std::str::FromStr;
17use std::sync::{Arc, LazyLock};
18use std::time::{Duration, Instant};
19
20use async_trait::async_trait;
21use differential_dataflow::lattice::Lattice;
22use futures::{FutureExt, StreamExt};
23use itertools::Itertools;
24use mz_audit_log::VersionedEvent;
25use mz_ore::cast::CastFrom;
26use mz_ore::metrics::MetricsFutureExt;
27use mz_ore::now::EpochMillis;
28use mz_ore::{
29    soft_assert_eq_no_log, soft_assert_eq_or_log, soft_assert_ne_or_log, soft_assert_no_log,
30    soft_assert_or_log, soft_panic_or_log,
31};
32use mz_persist_client::cfg::USE_CRITICAL_SINCE_CATALOG;
33use mz_persist_client::cli::admin::{CATALOG_FORCE_COMPACTION_FUEL, CATALOG_FORCE_COMPACTION_WAIT};
34use mz_persist_client::critical::{CriticalReaderId, Opaque, SinceHandle};
35use mz_persist_client::error::UpperMismatch;
36use mz_persist_client::read::{Listen, ListenEvent, ReadHandle};
37use mz_persist_client::write::WriteHandle;
38use mz_persist_client::{Diagnostics, PersistClient, ShardId};
39use mz_persist_types::codec_impls::UnitSchema;
40use mz_proto::TryFromProtoError;
41use mz_repr::Diff;
42use mz_storage_client::controller::PersistEpoch;
43use mz_storage_types::StorageDiff;
44use mz_storage_types::sources::SourceData;
45use sha2::Digest;
46use timely::progress::{Antichain, Timestamp as TimelyTimestamp};
47use tracing::{debug, info, warn};
48use uuid::Uuid;
49
50use crate::durable::debug::{Collection, CollectionType, DebugCatalogState, Trace};
51use crate::durable::error::FenceError;
52use crate::durable::initialize::{
53    ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT, SYSTEM_CONFIG_SYNCED_KEY, USER_VERSION_KEY,
54    WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL, WITH_0DT_DEPLOYMENT_MAX_WAIT,
55};
56use crate::durable::metrics::Metrics;
57use crate::durable::objects::serialization::RustType;
58use crate::durable::objects::state_update::{
59    IntoStateUpdateKindJson, StateUpdate, StateUpdateKind, StateUpdateKindJson,
60    TryIntoStateUpdateKind,
61};
62use crate::durable::objects::{AuditLogKey, FenceToken, Snapshot};
63use crate::durable::transaction::TransactionBatch;
64use crate::durable::upgrade::upgrade;
65use crate::durable::{
66    BootstrapArgs, CATALOG_CONTENT_VERSION_KEY, CatalogError, DryRunTransaction,
67    DurableCatalogError, DurableCatalogState, Epoch, OpenableDurableCatalogState,
68    ReadOnlyDurableCatalogState, Transaction, initialize, persist_desc,
69};
70use crate::memory;
71
72/// New-type used to represent timestamps in persist.
73pub(crate) type Timestamp = mz_repr::Timestamp;
74
75/// The minimum value of an epoch.
76const MIN_EPOCH: Epoch = Epoch::new(1).expect("1 is non-zero");
77
78/// Human readable catalog shard name.
79const CATALOG_SHARD_NAME: &str = "catalog";
80
81/// [`CriticalReaderId`] for the catalog shard's own since hold, separate from
82/// [`PersistClient::CONTROLLER_CRITICAL_SINCE`].
83static CATALOG_CRITICAL_SINCE: LazyLock<CriticalReaderId> = LazyLock::new(|| {
84    "c55555555-6666-7777-8888-999999999999"
85        .parse()
86        .expect("valid CriticalReaderId")
87});
88
89/// Seed used to generate the persist shard ID for the catalog.
90const CATALOG_SEED: usize = 1;
91/// Legacy seed used to generate the persist shard ID for the upgrade shard. DO NOT REUSE.
92const _UPGRADE_SEED: usize = 2;
93/// Legacy seed used to generate the persist shard ID for builtin table migrations. DO NOT REUSE.
94pub const _BUILTIN_MIGRATION_SEED: usize = 3;
95/// Legacy seed used to generate the persist shard ID for the expression cache. DO NOT REUSE.
96pub const _EXPRESSION_CACHE_SEED: usize = 4;
97
98/// Durable catalog mode that dictates the effect of mutable operations.
99#[derive(Debug, Copy, Clone, Eq, PartialEq)]
100pub(crate) enum Mode {
101    /// Mutable operations are prohibited.
102    Readonly,
103    /// Mutable operations have an effect in-memory, but aren't persisted durably.
104    Savepoint,
105    /// Mutable operations have an effect in-memory and durably.
106    Writable,
107}
108
109/// Enum representing the fenced state of the catalog.
110#[derive(Debug)]
111pub(crate) enum FenceableToken {
112    /// The catalog is still initializing and learning about previously written fence tokens. This
113    /// state can be fenced if it encounters a larger deploy generation.
114    Initializing {
115        /// The largest fence token durably written to the catalog, if any.
116        durable_token: Option<FenceToken>,
117        /// This process's deploy generation.
118        current_deploy_generation: Option<u64>,
119    },
120    /// The current token has not been fenced.
121    Unfenced { current_token: FenceToken },
122    /// The current token has been fenced.
123    Fenced {
124        current_token: FenceToken,
125        fence_token: FenceToken,
126    },
127}
128
129impl FenceableToken {
130    /// Returns a new token.
131    fn new(current_deploy_generation: Option<u64>) -> Self {
132        Self::Initializing {
133            durable_token: None,
134            current_deploy_generation,
135        }
136    }
137
138    /// Returns the current token if it is not fenced, otherwise returns an error.
139    fn validate(&self) -> Result<Option<FenceToken>, FenceError> {
140        match self {
141            FenceableToken::Initializing { durable_token, .. } => Ok(durable_token.clone()),
142            FenceableToken::Unfenced { current_token, .. } => Ok(Some(current_token.clone())),
143            FenceableToken::Fenced {
144                current_token,
145                fence_token,
146            } => {
147                assert!(
148                    fence_token > current_token,
149                    "must be fenced by higher token; current={current_token:?}, fence={fence_token:?}"
150                );
151                if fence_token.deploy_generation > current_token.deploy_generation {
152                    Err(FenceError::DeployGeneration {
153                        current_generation: current_token.deploy_generation,
154                        fence_generation: fence_token.deploy_generation,
155                    })
156                } else {
157                    assert!(
158                        fence_token.epoch > current_token.epoch,
159                        "must be fenced by higher token; current={current_token:?}, fence={fence_token:?}"
160                    );
161                    Err(FenceError::Epoch {
162                        current_epoch: current_token.epoch,
163                        fence_epoch: fence_token.epoch,
164                    })
165                }
166            }
167        }
168    }
169
170    /// Returns the current token.
171    fn token(&self) -> Option<FenceToken> {
172        match self {
173            FenceableToken::Initializing { durable_token, .. } => durable_token.clone(),
174            FenceableToken::Unfenced { current_token, .. } => Some(current_token.clone()),
175            FenceableToken::Fenced { current_token, .. } => Some(current_token.clone()),
176        }
177    }
178
179    /// Returns `Err` if `token` fences out `self`, `Ok` otherwise.
180    fn maybe_fence(&mut self, token: FenceToken) -> Result<(), FenceError> {
181        match self {
182            FenceableToken::Initializing {
183                durable_token,
184                current_deploy_generation,
185                ..
186            } => {
187                match durable_token {
188                    Some(durable_token) => {
189                        *durable_token = max(durable_token.clone(), token.clone());
190                    }
191                    None => {
192                        *durable_token = Some(token.clone());
193                    }
194                }
195                if let Some(current_deploy_generation) = current_deploy_generation {
196                    if *current_deploy_generation < token.deploy_generation {
197                        *self = FenceableToken::Fenced {
198                            current_token: FenceToken {
199                                deploy_generation: *current_deploy_generation,
200                                epoch: token.epoch,
201                            },
202                            fence_token: token,
203                        };
204                        self.validate()?;
205                    }
206                }
207            }
208            FenceableToken::Unfenced { current_token } => {
209                if *current_token < token {
210                    *self = FenceableToken::Fenced {
211                        current_token: current_token.clone(),
212                        fence_token: token,
213                    };
214                    self.validate()?;
215                }
216            }
217            FenceableToken::Fenced { .. } => {
218                self.validate()?;
219            }
220        }
221
222        Ok(())
223    }
224
225    /// Returns a [`FenceableToken::Unfenced`] token and the updates to the catalog required to
226    /// transition to the `Unfenced` state if `self` is [`FenceableToken::Initializing`], otherwise
227    /// returns `None`.
228    fn generate_unfenced_token(
229        &self,
230        mode: Mode,
231    ) -> Result<Option<(Vec<(StateUpdateKind, Diff)>, FenceableToken)>, DurableCatalogError> {
232        let (durable_token, current_deploy_generation) = match self {
233            FenceableToken::Initializing {
234                durable_token,
235                current_deploy_generation,
236            } => (durable_token.clone(), current_deploy_generation.clone()),
237            FenceableToken::Unfenced { .. } | FenceableToken::Fenced { .. } => return Ok(None),
238        };
239
240        let mut fence_updates = Vec::with_capacity(2);
241
242        if let Some(durable_token) = &durable_token {
243            fence_updates.push((
244                StateUpdateKind::FenceToken(durable_token.clone()),
245                Diff::MINUS_ONE,
246            ));
247        }
248
249        let current_deploy_generation = current_deploy_generation
250            .or_else(|| durable_token.as_ref().map(|token| token.deploy_generation))
251            // We cannot initialize a catalog without a deploy generation.
252            .ok_or(DurableCatalogError::Uninitialized)?;
253        let mut current_epoch = durable_token
254            .map(|token| token.epoch)
255            .unwrap_or(MIN_EPOCH)
256            .get();
257        // Only writable catalogs attempt to increment the epoch.
258        if matches!(mode, Mode::Writable) {
259            current_epoch = current_epoch + 1;
260        }
261        let current_epoch = Epoch::new(current_epoch).expect("known to be non-zero");
262        let current_token = FenceToken {
263            deploy_generation: current_deploy_generation,
264            epoch: current_epoch,
265        };
266
267        fence_updates.push((
268            StateUpdateKind::FenceToken(current_token.clone()),
269            Diff::ONE,
270        ));
271
272        let current_fenceable_token = FenceableToken::Unfenced { current_token };
273
274        Ok(Some((fence_updates, current_fenceable_token)))
275    }
276}
277
278/// An error that can occur while executing [`PersistHandle::compare_and_append`].
279#[derive(Debug, thiserror::Error)]
280pub(crate) enum CompareAndAppendError {
281    #[error(transparent)]
282    Fence(#[from] FenceError),
283    /// Catalog encountered an upper mismatch when trying to write to the catalog: another
284    /// writer moved the upper between our snapshot of it and the write. Handled by the conflict
285    /// classification in the commit and advance paths (rebase over empty progress, surface
286    /// content conflicts as out-of-sync).
287    #[error(
288        "expected catalog upper {expected_upper:?} did not match actual catalog upper {actual_upper:?}"
289    )]
290    UpperMismatch {
291        expected_upper: Timestamp,
292        actual_upper: Timestamp,
293    },
294}
295
296impl CompareAndAppendError {
297    pub(crate) fn unwrap_fence_error(self) -> FenceError {
298        match self {
299            CompareAndAppendError::Fence(e) => e,
300            e @ CompareAndAppendError::UpperMismatch { .. } => {
301                panic!("unexpected upper mismatch: {e:?}")
302            }
303        }
304    }
305}
306
307impl From<UpperMismatch<Timestamp>> for CompareAndAppendError {
308    fn from(upper_mismatch: UpperMismatch<Timestamp>) -> Self {
309        Self::UpperMismatch {
310            expected_upper: antichain_to_timestamp(upper_mismatch.expected),
311            actual_upper: antichain_to_timestamp(upper_mismatch.current),
312        }
313    }
314}
315
316pub(crate) trait ApplyUpdate<T: IntoStateUpdateKindJson> {
317    /// Process and apply `update`.
318    ///
319    /// Returns `Some` if `update` should be cached in memory and `None` otherwise.
320    fn apply_update(
321        &mut self,
322        update: StateUpdate<T>,
323        current_fence_token: &mut FenceableToken,
324        metrics: &Arc<Metrics>,
325    ) -> Result<Option<StateUpdate<T>>, FenceError>;
326}
327
328/// A handle for interacting with the persist catalog shard.
329///
330/// The catalog shard is used in multiple different contexts, for example pre-open and post-open,
331/// but for all contexts the majority of the durable catalog's behavior is identical. This struct
332/// implements those behaviors that are identical while allowing the user to specify the different
333/// behaviors via generic parameters.
334///
335/// The behavior of the durable catalog can be different along one of two axes. The first is the
336/// format of each individual update, i.e. raw binary, the current protobuf version, previous
337/// protobuf versions, etc. The second axis is what to do with each individual update, for example
338/// before opening we cache all config updates but don't cache them after opening. These behaviors
339/// are customizable via the `T: TryIntoStateUpdateKind` and `U: ApplyUpdate<T>` generic parameters
340/// respectively.
341#[derive(Debug)]
342pub(crate) struct PersistHandle<T: TryIntoStateUpdateKind, U: ApplyUpdate<T>> {
343    /// The [`Mode`] that this catalog was opened in.
344    pub(crate) mode: Mode,
345    /// Since handle to control compaction.
346    since_handle: SinceHandle<SourceData, (), Timestamp, StorageDiff>,
347    /// Write handle to persist.
348    write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
349    /// Listener to catalog changes.
350    listen: Listen<SourceData, (), Timestamp, StorageDiff>,
351    /// Handle for connecting to persist.
352    persist_client: PersistClient,
353    /// Catalog shard ID.
354    shard_id: ShardId,
355    /// Cache of the most recent catalog snapshot.
356    ///
357    /// We use a tuple instead of [`StateUpdate`] to make consolidation easier.
358    pub(crate) snapshot: Vec<(T, Timestamp, Diff)>,
359    /// Applies custom processing, filtering, and fencing for each individual update.
360    update_applier: U,
361    /// The current upper of the persist shard.
362    pub(crate) upper: Timestamp,
363    /// The fence token of the catalog, if one exists.
364    fenceable_token: FenceableToken,
365    /// The semantic version of the current binary.
366    catalog_content_version: semver::Version,
367    /// Flag to indicate if bootstrap is complete.
368    bootstrap_complete: bool,
369    /// Metrics for the persist catalog.
370    metrics: Arc<Metrics>,
371    /// Snapshot size at the last amortized consolidation, used by
372    /// [`Self::maybe_consolidate`] to decide when to consolidate. Initialized
373    /// lazily on the first call.
374    size_at_last_consolidation: Option<usize>,
375    /// Counts raw updates applied to this handle.
376    ///
377    /// This distinguishes empty upper progress from content. Memory updates are insufficient
378    /// because kinds such as ID allocators do not produce them.
379    updates_applied: u64,
380}
381
382impl<T: TryIntoStateUpdateKind, U: ApplyUpdate<T>> PersistHandle<T, U> {
383    /// Fetch the current upper of the catalog state.
384    #[mz_ore::instrument]
385    async fn current_upper(&mut self) -> Timestamp {
386        match self.mode {
387            Mode::Writable | Mode::Readonly => {
388                let upper = self.write_handle.fetch_recent_upper().await;
389                antichain_to_timestamp(upper.clone())
390            }
391            Mode::Savepoint => self.upper,
392        }
393    }
394
395    /// Appends `updates` iff the current global upper of the catalog is `self.upper`.
396    ///
397    /// Returns the next upper used to commit the transaction.
398    #[mz_ore::instrument]
399    pub(crate) async fn compare_and_append<S: IntoStateUpdateKindJson>(
400        &mut self,
401        updates: Vec<(S, Diff)>,
402        commit_ts: Timestamp,
403    ) -> Result<Timestamp, CompareAndAppendError> {
404        let updates = updates.into_iter().map(|(kind, diff)| {
405            let kind: StateUpdateKindJson = kind.into();
406            (
407                (Into::<SourceData>::into(kind), ()),
408                commit_ts,
409                diff.into_inner(),
410            )
411        });
412        let next_upper = commit_ts.step_forward();
413        // Upper mismatches are classified by the commit and advance callers.
414        self.compare_and_append_inner(updates, next_upper).await?;
415
416        self.sync(next_upper).await?;
417        Ok(next_upper)
418    }
419
420    /// Compare-and-append `updates` to the catalog shard, advancing the upper to `next_upper`.
421    ///
422    /// On success, updating `self.upper` is left to the caller. The caller can thus decide whether
423    /// or not it needs to sync the catalog.
424    ///
425    /// # Panics
426    ///
427    /// Panics if not in `Writable` mode.
428    /// Panics if `next_upper` is not greater than `self.upper`.
429    async fn compare_and_append_inner(
430        &mut self,
431        updates: impl IntoIterator<Item = ((SourceData, ()), Timestamp, StorageDiff)>,
432        next_upper: Timestamp,
433    ) -> Result<(), CompareAndAppendError> {
434        assert_eq!(self.mode, Mode::Writable);
435        assert!(
436            next_upper > self.upper,
437            "next_upper ({next_upper}) not greater than current upper ({})",
438            self.upper,
439        );
440
441        let res = self
442            .write_handle
443            .compare_and_append(
444                updates,
445                Antichain::from_elem(self.upper),
446                Antichain::from_elem(next_upper),
447            )
448            .await
449            .expect("invalid usage");
450
451        if let Err(e @ UpperMismatch { .. }) = res {
452            // Most likely we were fenced out.
453            // Sync to the current upper to detect that.
454            self.sync_to_current_upper().await?;
455            return Err(e.into());
456        }
457
458        // Lag the shard's upper by 1 to keep it readable.
459        let downgrade_to = Antichain::from_elem(next_upper.saturating_sub(1));
460
461        // The since handle gives us the ability to fence out other downgraders using an opaque token.
462        // (See the method documentation for details.)
463        // That's not needed here, so we use the since handle's opaque token to avoid any comparison
464        // failures.
465        let opaque = self.since_handle.opaque().clone();
466        let downgrade = self
467            .since_handle
468            .maybe_compare_and_downgrade_since(&opaque, (&opaque, &downgrade_to))
469            .await;
470        if let Some(Err(e)) = downgrade {
471            soft_panic_or_log!("found opaque value {e:?}, but expected {opaque:?}");
472        }
473
474        Ok(())
475    }
476
477    /// Accepts an upper mismatch caused only by empty progress.
478    ///
479    /// `updates_applied_before` must be captured before the compare-and-append, which synchronizes
480    /// this handle on mismatch.
481    fn classify_upper_mismatch(
482        &self,
483        updates_applied_before: u64,
484        actual_upper: Timestamp,
485    ) -> Result<(), DurableCatalogError> {
486        if self.updates_applied != updates_applied_before {
487            Err(DurableCatalogError::CatalogOutOfSync {
488                update_count: usize::cast_from(self.updates_applied - updates_applied_before),
489                upper: actual_upper,
490            })
491        } else {
492            Ok(())
493        }
494    }
495
496    /// Generates an iterator of [`StateUpdate`] that contain all unconsolidated updates to the
497    /// catalog state up to, and including, `as_of`.
498    #[mz_ore::instrument]
499    async fn snapshot_unconsolidated(&mut self) -> Vec<StateUpdate<StateUpdateKind>> {
500        let current_upper = self.current_upper().await;
501
502        let mut snapshot = Vec::new();
503        let mut read_handle = self.read_handle().await;
504        let as_of = as_of(&read_handle, current_upper);
505        let mut stream = Box::pin(
506            // We use `snapshot_and_stream` because it guarantees unconsolidated output.
507            read_handle
508                .snapshot_and_stream(Antichain::from_elem(as_of))
509                .await
510                .expect("we have advanced the restart_as_of by the since"),
511        );
512        while let Some(update) = stream.next().await {
513            snapshot.push(update)
514        }
515        read_handle.expire().await;
516        snapshot
517            .into_iter()
518            .map(Into::<StateUpdate<StateUpdateKindJson>>::into)
519            .map(|state_update| state_update.try_into().expect("kind decoding error"))
520            .collect()
521    }
522
523    /// Listen and apply all updates that are currently in persist.
524    ///
525    /// Returns an error if this instance has been fenced out.
526    #[mz_ore::instrument]
527    pub(crate) async fn sync_to_current_upper(&mut self) -> Result<(), FenceError> {
528        let upper = self.current_upper().await;
529        self.sync(upper).await
530    }
531
532    /// Listen and apply all updates up to `target_upper`.
533    ///
534    /// Returns an error if this instance has been fenced out.
535    #[mz_ore::instrument(level = "debug")]
536    pub(crate) async fn sync(&mut self, target_upper: Timestamp) -> Result<(), FenceError> {
537        self.metrics.syncs.inc();
538        let histogram = self.metrics.sync_latency_seconds.clone();
539        self.sync_inner(target_upper)
540            .wall_time()
541            .observe(histogram)
542            .await
543    }
544
545    #[mz_ore::instrument(level = "debug")]
546    async fn sync_inner(&mut self, target_upper: Timestamp) -> Result<(), FenceError> {
547        self.fenceable_token.validate()?;
548
549        // Savepoint catalogs do not yet know how to update themselves in response to concurrent
550        // writes from writer catalogs.
551        if self.mode == Mode::Savepoint {
552            self.upper = max(self.upper, target_upper);
553            return Ok(());
554        }
555
556        let mut updates: BTreeMap<_, Vec<_>> = BTreeMap::new();
557        let updates_applied_before = self.updates_applied;
558
559        // Reset the amortized consolidation tracker so it picks up the
560        // current snapshot size as its baseline.
561        self.size_at_last_consolidation = None;
562
563        while self.upper < target_upper {
564            let listen_events = self.listen.fetch_next().await;
565            for listen_event in listen_events {
566                match listen_event {
567                    ListenEvent::Progress(upper) => {
568                        debug!("synced up to {upper:?}");
569                        self.upper = antichain_to_timestamp(upper);
570                        // Attempt to apply updates in batches of a single timestamp. If another
571                        // catalog wrote a fence token at one timestamp and then updates in a new
572                        // format at a later timestamp, then we want to apply the fence token
573                        // before attempting to deserialize the new updates.
574                        while let Some((ts, updates)) = updates.pop_first() {
575                            assert!(ts < self.upper, "expected {} < {}", ts, self.upper);
576                            let updates = updates.into_iter().map(
577                                |update: StateUpdate<StateUpdateKindJson>| {
578                                    let kind =
579                                        T::try_from(update.kind).expect("kind decoding error");
580                                    StateUpdate {
581                                        kind,
582                                        ts: update.ts,
583                                        diff: update.diff,
584                                    }
585                                },
586                            );
587                            self.apply_updates(updates)?;
588                            self.maybe_consolidate();
589                        }
590                    }
591                    ListenEvent::Updates(batch_updates) => {
592                        for update in batch_updates {
593                            let update: StateUpdate<StateUpdateKindJson> = update.into();
594                            updates.entry(update.ts).or_default().push(update);
595                        }
596                    }
597                }
598            }
599        }
600        assert_eq!(updates, BTreeMap::new(), "all updates should be applied");
601        // Only consolidate when there are actual updates.
602        if self.updates_applied != updates_applied_before {
603            self.consolidate();
604        }
605        Ok(())
606    }
607
608    /// Apply a batch of updates and then consolidate the snapshot. This is the
609    /// typical entry point for callers that apply updates in a single batch.
610    ///
611    /// For hot loops that apply updates across many timestamps (e.g., `sync_inner`),
612    /// use `apply_updates` directly and call `consolidate()` periodically (e.g.,
613    /// on snapshot doubling) to bound memory while staying amortized O(N log N).
614    pub(crate) fn apply_updates_and_consolidate(
615        &mut self,
616        updates: impl IntoIterator<Item = StateUpdate<T>>,
617    ) -> Result<(), FenceError> {
618        self.apply_updates(updates)?;
619        self.consolidate();
620        Ok(())
621    }
622
623    /// Apply a batch of updates to the catalog state without consolidating.
624    ///
625    /// Does NOT consolidate the snapshot afterward. If you are calling this once,
626    /// prefer `apply_updates_and_consolidate`. This method exists for loops that
627    /// call it many times — consolidating per call would be O(K * N log N) instead
628    /// of O(N log N). Callers should consolidate periodically (e.g., on snapshot
629    /// doubling) to bound memory.
630    #[mz_ore::instrument(level = "debug")]
631    fn apply_updates(
632        &mut self,
633        updates: impl IntoIterator<Item = StateUpdate<T>>,
634    ) -> Result<(), FenceError> {
635        let mut updates: Vec<_> = updates
636            .into_iter()
637            .map(|StateUpdate { kind, ts, diff }| (kind, ts, diff))
638            .collect();
639
640        // This helps guarantee that for a single key, there is at most a single retraction and a
641        // single insertion per timestamp. Otherwise, we would need to match the retractions and
642        // insertions up by value and manually figure out what the end value should be.
643        differential_dataflow::consolidation::consolidate_updates(&mut updates);
644
645        // Updates must be applied in timestamp order. Within a timestamp retractions must be
646        // applied before insertions, or we might end up retracting the wrong value.
647        updates.sort_by(|(_, ts1, diff1), (_, ts2, diff2)| ts1.cmp(ts2).then(diff1.cmp(diff2)));
648
649        let mut errors = Vec::new();
650
651        for (kind, ts, diff) in updates {
652            if diff != Diff::ONE && diff != Diff::MINUS_ONE {
653                panic!("invalid update in consolidated trace: ({kind:?}, {ts:?}, {diff:?})");
654            }
655            self.updates_applied += 1;
656
657            match self.update_applier.apply_update(
658                StateUpdate { kind, ts, diff },
659                &mut self.fenceable_token,
660                &self.metrics,
661            ) {
662                Ok(Some(StateUpdate { kind, ts, diff })) => self.snapshot.push((kind, ts, diff)),
663                Ok(None) => {}
664                // Instead of returning immediately, we accumulate all the errors and return the one
665                // with the most information.
666                Err(err) => errors.push(err),
667            }
668        }
669
670        // Track the high-water mark of the unconsolidated snapshot size.
671        let len = i64::try_from(self.snapshot.len()).unwrap_or(i64::MAX);
672        if len > self.metrics.snapshot_max_entries.get() {
673            self.metrics.snapshot_max_entries.set(len);
674        }
675
676        errors.sort();
677        if let Some(err) = errors.into_iter().next() {
678            return Err(err);
679        }
680
681        Ok(())
682    }
683
684    /// Consolidate the snapshot if it has at least doubled in size since the
685    /// last consolidation. This amortizes the O(N log N) consolidation cost
686    /// over many small updates, keeping the total work O(N log N) rather than
687    /// O(K * N log N) for K timestamps.
688    fn maybe_consolidate(&mut self) {
689        let threshold = *self
690            .size_at_last_consolidation
691            // Use a minimum of 8 to avoid consolidating on every update when
692            // the snapshot is small or empty (since 0 * 2 = 0).
693            .get_or_insert_with(|| max(self.snapshot.len(), 8));
694        if self.snapshot.len() >= threshold * 2 {
695            self.consolidate();
696            self.size_at_last_consolidation = Some(self.snapshot.len());
697        }
698    }
699
700    #[mz_ore::instrument]
701    pub(crate) fn consolidate(&mut self) {
702        self.metrics.snapshot_consolidations.inc();
703        soft_assert_no_log!(
704            self.snapshot
705                .windows(2)
706                .all(|updates| updates[0].1 <= updates[1].1),
707            "snapshot should be sorted by timestamp, {:#?}",
708            self.snapshot
709        );
710
711        let new_ts = self
712            .snapshot
713            .last()
714            .map(|(_, ts, _)| *ts)
715            .unwrap_or_else(Timestamp::minimum);
716        for (_, ts, _) in &mut self.snapshot {
717            *ts = new_ts;
718        }
719        differential_dataflow::consolidation::consolidate_updates(&mut self.snapshot);
720    }
721
722    /// Execute and return the results of `f` on the current catalog trace.
723    ///
724    /// Will return an error if the catalog has been fenced out.
725    async fn with_trace<R>(
726        &mut self,
727        f: impl FnOnce(&Vec<(T, Timestamp, Diff)>) -> Result<R, CatalogError>,
728    ) -> Result<R, CatalogError> {
729        self.sync_to_current_upper().await?;
730        f(&self.snapshot)
731    }
732
733    /// Open a read handle to the catalog.
734    async fn read_handle(&self) -> ReadHandle<SourceData, (), Timestamp, StorageDiff> {
735        self.persist_client
736            .open_leased_reader(
737                self.shard_id,
738                Arc::new(persist_desc()),
739                Arc::new(UnitSchema::default()),
740                Diagnostics {
741                    shard_name: CATALOG_SHARD_NAME.to_string(),
742                    handle_purpose: "openable durable catalog state temporary reader".to_string(),
743                },
744                USE_CRITICAL_SINCE_CATALOG.get(self.persist_client.dyncfgs()),
745            )
746            .await
747            .expect("invalid usage")
748    }
749
750    /// Politely releases all external resources that can only be released in an async context.
751    async fn expire(self: Box<Self>) {
752        self.write_handle.expire().await;
753        self.listen.expire().await;
754    }
755}
756
757impl<U: ApplyUpdate<StateUpdateKind>> PersistHandle<StateUpdateKind, U> {
758    /// Execute and return the results of `f` on the current catalog snapshot.
759    ///
760    /// Will return an error if the catalog has been fenced out.
761    async fn with_snapshot<T>(
762        &mut self,
763        f: impl FnOnce(Snapshot) -> Result<T, CatalogError>,
764    ) -> Result<T, CatalogError> {
765        fn apply<K, V>(map: &mut BTreeMap<K, V>, key: &K, value: &V, diff: Diff)
766        where
767            K: Ord + Clone,
768            V: Ord + Clone + Debug,
769        {
770            let key = key.clone();
771            let value = value.clone();
772            if diff == Diff::ONE {
773                let prev = map.insert(key, value);
774                assert_eq!(
775                    prev, None,
776                    "values must be explicitly retracted before inserting a new value"
777                );
778            } else if diff == Diff::MINUS_ONE {
779                let prev = map.remove(&key);
780                assert_eq!(
781                    prev,
782                    Some(value),
783                    "retraction does not match existing value"
784                );
785            }
786        }
787
788        self.with_trace(|trace| {
789            let mut snapshot = Snapshot::empty();
790            for (kind, ts, diff) in trace {
791                let diff = *diff;
792                if diff != Diff::ONE && diff != Diff::MINUS_ONE {
793                    panic!("invalid update in consolidated trace: ({kind:?}, {ts:?}, {diff:?})");
794                }
795
796                match kind {
797                    StateUpdateKind::AuditLog(_key, ()) => {
798                        // Ignore for snapshots.
799                    }
800                    StateUpdateKind::Cluster(key, value) => {
801                        apply(&mut snapshot.clusters, key, value, diff);
802                    }
803                    StateUpdateKind::ClusterReplica(key, value) => {
804                        apply(&mut snapshot.cluster_replicas, key, value, diff);
805                    }
806                    StateUpdateKind::ClusterReplicaSize(key, value) => {
807                        apply(&mut snapshot.cluster_replica_sizes, key, value, diff);
808                    }
809                    StateUpdateKind::Comment(key, value) => {
810                        apply(&mut snapshot.comments, key, value, diff);
811                    }
812                    StateUpdateKind::IndexColumns(key, value) => {
813                        apply(&mut snapshot.index_columns, key, value, diff);
814                    }
815                    StateUpdateKind::ItemColumns(key, value) => {
816                        apply(&mut snapshot.item_columns, key, value, diff);
817                    }
818                    StateUpdateKind::Config(key, value) => {
819                        apply(&mut snapshot.configs, key, value, diff);
820                    }
821                    StateUpdateKind::Database(key, value) => {
822                        apply(&mut snapshot.databases, key, value, diff);
823                    }
824                    StateUpdateKind::DefaultPrivilege(key, value) => {
825                        apply(&mut snapshot.default_privileges, key, value, diff);
826                    }
827                    StateUpdateKind::FenceToken(_token) => {
828                        // Ignore for snapshots.
829                    }
830                    StateUpdateKind::IdAllocator(key, value) => {
831                        apply(&mut snapshot.id_allocator, key, value, diff);
832                    }
833                    StateUpdateKind::IntrospectionSourceIndex(key, value) => {
834                        apply(&mut snapshot.introspection_sources, key, value, diff);
835                    }
836                    StateUpdateKind::Item(key, value) => {
837                        apply(&mut snapshot.items, key, value, diff);
838                    }
839                    StateUpdateKind::NetworkPolicy(key, value) => {
840                        apply(&mut snapshot.network_policies, key, value, diff);
841                    }
842                    StateUpdateKind::Role(key, value) => {
843                        apply(&mut snapshot.roles, key, value, diff);
844                    }
845                    StateUpdateKind::Schema(key, value) => {
846                        apply(&mut snapshot.schemas, key, value, diff);
847                    }
848                    StateUpdateKind::Setting(key, value) => {
849                        apply(&mut snapshot.settings, key, value, diff);
850                    }
851                    StateUpdateKind::SourceReferences(key, value) => {
852                        apply(&mut snapshot.source_references, key, value, diff);
853                    }
854                    StateUpdateKind::SystemConfiguration(key, value) => {
855                        apply(&mut snapshot.system_configurations, key, value, diff);
856                    }
857                    StateUpdateKind::ClusterSystemConfiguration(key, value) => {
858                        apply(
859                            &mut snapshot.cluster_system_configurations,
860                            key,
861                            value,
862                            diff,
863                        );
864                    }
865                    StateUpdateKind::ReplicaSystemConfiguration(key, value) => {
866                        apply(
867                            &mut snapshot.replica_system_configurations,
868                            key,
869                            value,
870                            diff,
871                        );
872                    }
873                    StateUpdateKind::SystemObjectMapping(key, value) => {
874                        apply(&mut snapshot.system_object_mappings, key, value, diff);
875                    }
876                    StateUpdateKind::SystemPrivilege(key, value) => {
877                        apply(&mut snapshot.system_privileges, key, value, diff);
878                    }
879                    StateUpdateKind::StorageCollectionMetadata(key, value) => {
880                        apply(&mut snapshot.storage_collection_metadata, key, value, diff);
881                    }
882                    StateUpdateKind::UnfinalizedShard(key, ()) => {
883                        apply(&mut snapshot.unfinalized_shards, key, &(), diff);
884                    }
885                    StateUpdateKind::TxnWalShard((), value) => {
886                        apply(&mut snapshot.txn_wal_shard, &(), value, diff);
887                    }
888                    StateUpdateKind::RoleAuth(key, value) => {
889                        apply(&mut snapshot.role_auth, key, value, diff);
890                    }
891                }
892            }
893            f(snapshot)
894        })
895        .await
896    }
897
898    /// Generates an iterator of [`StateUpdate`] that contain all updates to the catalog
899    /// state.
900    ///
901    /// The output is fetched directly from persist instead of the in-memory cache.
902    ///
903    /// The output is consolidated and sorted by timestamp in ascending order.
904    #[mz_ore::instrument(level = "debug")]
905    async fn persist_snapshot(&self) -> impl Iterator<Item = StateUpdate> + DoubleEndedIterator {
906        let mut read_handle = self.read_handle().await;
907        let as_of = as_of(&read_handle, self.upper);
908        let snapshot = snapshot_binary(&mut read_handle, as_of, &self.metrics)
909            .await
910            .map(|update| update.try_into().expect("kind decoding error"));
911        read_handle.expire().await;
912        snapshot
913    }
914}
915
916/// Applies updates for an unopened catalog.
917#[derive(Debug)]
918pub(crate) struct UnopenedCatalogStateInner {
919    /// A cache of the config collection of the catalog.
920    configs: BTreeMap<String, u64>,
921    /// A cache of the settings collection of the catalog.
922    settings: BTreeMap<String, String>,
923}
924
925impl UnopenedCatalogStateInner {
926    fn new() -> UnopenedCatalogStateInner {
927        UnopenedCatalogStateInner {
928            configs: BTreeMap::new(),
929            settings: BTreeMap::new(),
930        }
931    }
932}
933
934impl ApplyUpdate<StateUpdateKindJson> for UnopenedCatalogStateInner {
935    fn apply_update(
936        &mut self,
937        update: StateUpdate<StateUpdateKindJson>,
938        current_fence_token: &mut FenceableToken,
939        _metrics: &Arc<Metrics>,
940    ) -> Result<Option<StateUpdate<StateUpdateKindJson>>, FenceError> {
941        if !update.kind.is_audit_log() && update.kind.is_always_deserializable() {
942            let kind = TryInto::try_into(&update.kind).expect("kind is known to be deserializable");
943            match (kind, update.diff) {
944                (StateUpdateKind::Config(key, value), Diff::ONE) => {
945                    let prev = self.configs.insert(key.key, value.value);
946                    assert_eq!(
947                        prev, None,
948                        "values must be explicitly retracted before inserting a new value"
949                    );
950                }
951                (StateUpdateKind::Config(key, value), Diff::MINUS_ONE) => {
952                    let prev = self.configs.remove(&key.key);
953                    assert_eq!(
954                        prev,
955                        Some(value.value),
956                        "retraction does not match existing value"
957                    );
958                }
959                (StateUpdateKind::Setting(key, value), Diff::ONE) => {
960                    let prev = self.settings.insert(key.name, value.value);
961                    assert_eq!(
962                        prev, None,
963                        "values must be explicitly retracted before inserting a new value"
964                    );
965                }
966                (StateUpdateKind::Setting(key, value), Diff::MINUS_ONE) => {
967                    let prev = self.settings.remove(&key.name);
968                    assert_eq!(
969                        prev,
970                        Some(value.value),
971                        "retraction does not match existing value"
972                    );
973                }
974                (StateUpdateKind::FenceToken(fence_token), Diff::ONE) => {
975                    current_fence_token.maybe_fence(fence_token)?;
976                }
977                _ => {}
978            }
979        }
980
981        Ok(Some(update))
982    }
983}
984
985/// A Handle to an unopened catalog stored in persist. The unopened catalog can serve `Config` data,
986/// `Setting` data, or the current epoch. All other catalog data may be un-migrated and should not
987/// be read until the catalog has been opened. The [`UnopenedPersistCatalogState`] is responsible
988/// for opening the catalog, see [`OpenableDurableCatalogState::open`] for more details.
989///
990/// Production users should call [`Self::expire`] before dropping an [`UnopenedPersistCatalogState`]
991/// so that it can expire its leases. If/when rust gets AsyncDrop, this will be done automatically.
992pub(crate) type UnopenedPersistCatalogState =
993    PersistHandle<StateUpdateKindJson, UnopenedCatalogStateInner>;
994
995impl UnopenedPersistCatalogState {
996    /// Create a new [`UnopenedPersistCatalogState`] to the catalog state associated with
997    /// `organization_id`.
998    ///
999    /// All usages of the persist catalog must go through this function. That includes the
1000    /// catalog-debug tool, the adapter's catalog, etc.
1001    #[mz_ore::instrument]
1002    pub(crate) async fn new(
1003        persist_client: PersistClient,
1004        organization_id: Uuid,
1005        version: semver::Version,
1006        deploy_generation: Option<u64>,
1007        metrics: Arc<Metrics>,
1008    ) -> Result<UnopenedPersistCatalogState, DurableCatalogError> {
1009        let catalog_shard_id = shard_id(organization_id, CATALOG_SEED);
1010        debug!(?catalog_shard_id, "new persist backed catalog state");
1011
1012        // Check the catalog shard version to ensure that we are compatible with the persist
1013        // data format. This lets us return an error gracefully, rather than panicking later in
1014        // persist.
1015        let version_in_catalog_shard =
1016            fetch_catalog_shard_version(&persist_client, catalog_shard_id).await;
1017        if let Some(version_in_catalog_shard) = version_in_catalog_shard {
1018            if !mz_persist_client::cfg::code_can_write_data(&version, &version_in_catalog_shard) {
1019                return Err(DurableCatalogError::IncompatiblePersistVersion {
1020                    found_version: version_in_catalog_shard,
1021                    catalog_version: version,
1022                });
1023            }
1024        }
1025
1026        let open_handles_start = Instant::now();
1027        info!("startup: envd serve: catalog init: open handles beginning");
1028        let since_handle = persist_client
1029            .open_critical_since(
1030                catalog_shard_id,
1031                CATALOG_CRITICAL_SINCE.clone(),
1032                Opaque::encode(&i64::MIN),
1033                Diagnostics {
1034                    shard_name: CATALOG_SHARD_NAME.to_string(),
1035                    handle_purpose: "durable catalog state critical since".to_string(),
1036                },
1037            )
1038            .await
1039            .expect("invalid usage");
1040
1041        let (mut write_handle, mut read_handle) = persist_client
1042            .open(
1043                catalog_shard_id,
1044                Arc::new(persist_desc()),
1045                Arc::new(UnitSchema::default()),
1046                Diagnostics {
1047                    shard_name: CATALOG_SHARD_NAME.to_string(),
1048                    handle_purpose: "durable catalog state handles".to_string(),
1049                },
1050                USE_CRITICAL_SINCE_CATALOG.get(persist_client.dyncfgs()),
1051            )
1052            .await
1053            .expect("invalid usage");
1054        info!(
1055            "startup: envd serve: catalog init: open handles complete in {:?}",
1056            open_handles_start.elapsed()
1057        );
1058
1059        // Commit an empty write at the minimum timestamp so the catalog is always readable.
1060        let upper = {
1061            const EMPTY_UPDATES: &[((SourceData, ()), Timestamp, StorageDiff)] = &[];
1062            let upper = Antichain::from_elem(Timestamp::minimum());
1063            let next_upper = Timestamp::minimum().step_forward();
1064            match write_handle
1065                .compare_and_append(EMPTY_UPDATES, upper, Antichain::from_elem(next_upper))
1066                .await
1067                .expect("invalid usage")
1068            {
1069                Ok(()) => next_upper,
1070                Err(mismatch) => antichain_to_timestamp(mismatch.current),
1071            }
1072        };
1073
1074        let snapshot_start = Instant::now();
1075        info!("startup: envd serve: catalog init: snapshot beginning");
1076        let as_of = as_of(&read_handle, upper);
1077        let snapshot: Vec<_> = snapshot_binary(&mut read_handle, as_of, &metrics)
1078            .await
1079            .map(|StateUpdate { kind, ts, diff }| (kind, ts, diff))
1080            .collect();
1081        let listen = read_handle
1082            .listen(Antichain::from_elem(as_of))
1083            .await
1084            .expect("invalid usage");
1085        info!(
1086            "startup: envd serve: catalog init: snapshot complete in {:?}",
1087            snapshot_start.elapsed()
1088        );
1089
1090        let mut handle = UnopenedPersistCatalogState {
1091            // Unopened catalogs are always writeable until they're opened in an explicit mode.
1092            mode: Mode::Writable,
1093            since_handle,
1094            write_handle,
1095            listen,
1096            persist_client,
1097            shard_id: catalog_shard_id,
1098            // Initialize empty in-memory state.
1099            snapshot: Vec::new(),
1100            update_applier: UnopenedCatalogStateInner::new(),
1101            upper,
1102            fenceable_token: FenceableToken::new(deploy_generation),
1103            catalog_content_version: version,
1104            bootstrap_complete: false,
1105            metrics,
1106            size_at_last_consolidation: None,
1107            updates_applied: 0,
1108        };
1109        // If the snapshot is not consolidated, and we see multiple epoch values while applying the
1110        // updates, then we might accidentally fence ourselves out.
1111        soft_assert_no_log!(
1112            snapshot.iter().all(|(_, _, diff)| *diff == Diff::ONE),
1113            "snapshot should be consolidated: {snapshot:#?}"
1114        );
1115
1116        let apply_start = Instant::now();
1117        info!("startup: envd serve: catalog init: apply updates beginning");
1118        let updates = snapshot
1119            .into_iter()
1120            .map(|(kind, ts, diff)| StateUpdate { kind, ts, diff });
1121        handle.apply_updates_and_consolidate(updates)?;
1122        info!(
1123            "startup: envd serve: catalog init: apply updates complete in {:?}",
1124            apply_start.elapsed()
1125        );
1126
1127        // Validate that the binary version of the current process is not less than any binary
1128        // version that has written to the catalog.
1129        // This condition is only checked once, right here. If a new process comes along with a
1130        // higher version, it must fence this process out with one of the existing fencing
1131        // mechanisms.
1132        if let Some(found_version) = handle.get_catalog_content_version().await? {
1133            // Use cmp_precedence() to ignore build metadata per SemVer 2.0.0 spec
1134            if handle
1135                .catalog_content_version
1136                .cmp_precedence(&found_version)
1137                == std::cmp::Ordering::Less
1138            {
1139                return Err(DurableCatalogError::IncompatiblePersistVersion {
1140                    found_version,
1141                    catalog_version: handle.catalog_content_version,
1142                });
1143            }
1144        }
1145
1146        Ok(handle)
1147    }
1148
1149    #[mz_ore::instrument]
1150    async fn open_inner(
1151        mut self,
1152        mode: Mode,
1153        initial_ts: Timestamp,
1154        bootstrap_args: &BootstrapArgs,
1155    ) -> Result<Box<dyn DurableCatalogState>, CatalogError> {
1156        // It would be nice to use `initial_ts` here, but it comes from the system clock, not the
1157        // timestamp oracle.
1158        let mut commit_ts = self.upper;
1159        self.mode = mode;
1160
1161        // Validate the current deploy generation.
1162        match (&self.mode, &self.fenceable_token) {
1163            (_, FenceableToken::Unfenced { .. } | FenceableToken::Fenced { .. }) => {
1164                return Err(DurableCatalogError::Internal(
1165                    "catalog should not have fenced before opening".to_string(),
1166                )
1167                .into());
1168            }
1169            (
1170                Mode::Writable | Mode::Savepoint,
1171                FenceableToken::Initializing {
1172                    current_deploy_generation: None,
1173                    ..
1174                },
1175            ) => {
1176                return Err(DurableCatalogError::Internal(format!(
1177                    "cannot open in mode '{:?}' without a deploy generation",
1178                    self.mode,
1179                ))
1180                .into());
1181            }
1182            _ => {}
1183        }
1184
1185        let read_only = matches!(self.mode, Mode::Readonly);
1186
1187        // Fence out previous catalogs.
1188        loop {
1189            self.sync_to_current_upper().await?;
1190            commit_ts = max(commit_ts, self.upper);
1191            let (fence_updates, current_fenceable_token) = self
1192                .fenceable_token
1193                .generate_unfenced_token(self.mode)?
1194                .ok_or_else(|| {
1195                    DurableCatalogError::Internal(
1196                        "catalog should not have fenced before opening".to_string(),
1197                    )
1198                })?;
1199            debug!(
1200                ?self.upper,
1201                ?self.fenceable_token,
1202                ?current_fenceable_token,
1203                "fencing previous catalogs"
1204            );
1205            if matches!(self.mode, Mode::Writable) {
1206                match self
1207                    .compare_and_append(fence_updates.clone(), commit_ts)
1208                    .await
1209                {
1210                    Ok(upper) => {
1211                        commit_ts = upper;
1212                    }
1213                    Err(CompareAndAppendError::Fence(e)) => return Err(e.into()),
1214                    Err(e @ CompareAndAppendError::UpperMismatch { .. }) => {
1215                        warn!("catalog write failed due to upper mismatch, retrying: {e:?}");
1216                        continue;
1217                    }
1218                }
1219            }
1220            self.fenceable_token = current_fenceable_token;
1221            break;
1222        }
1223
1224        if matches!(self.mode, Mode::Writable) {
1225            // One-time migration: The catalog previously used `CONTROLLER_CRITICAL_SINCE` for its
1226            // since handle. Now it uses its own `CATALOG_CRITICAL_SINCE`, to free
1227            // `CONTROLLER_CRITICAL_SINCE` up for the storage controller. The catalog and
1228            // controller handles differ in the `Opaque` codec, so we need a migration.
1229            //
1230            // TODO: Remove this once we don't support upgrading from v26 anymore.
1231            let mut controller_handle = self
1232                .persist_client
1233                .open_critical_since::<SourceData, (), Timestamp, StorageDiff>(
1234                    self.shard_id,
1235                    PersistClient::CONTROLLER_CRITICAL_SINCE,
1236                    Opaque::encode(&i64::MIN),
1237                    Diagnostics {
1238                        shard_name: CATALOG_SHARD_NAME.to_string(),
1239                        handle_purpose: "durable catalog state critical since (migration)"
1240                            .to_string(),
1241                    },
1242                )
1243                .await
1244                .expect("invalid usage");
1245
1246            let since = controller_handle.since().clone();
1247            let res = controller_handle
1248                .compare_and_downgrade_since(
1249                    &Opaque::encode(&i64::MIN),
1250                    (&Opaque::encode(&PersistEpoch::default()), &since),
1251                )
1252                .await;
1253            match res {
1254                Ok(_) => info!("migrated Opaque of catalog since handle"),
1255                Err(_) => { /* critical since was already migrated */ }
1256            }
1257        }
1258
1259        let is_initialized = self.is_initialized_inner();
1260        if !matches!(self.mode, Mode::Writable) && !is_initialized {
1261            return Err(CatalogError::Durable(DurableCatalogError::NotWritable(
1262                format!(
1263                    "catalog tables do not exist; will not create in {:?} mode",
1264                    self.mode
1265                ),
1266            )));
1267        }
1268        soft_assert_ne_or_log!(self.upper, Timestamp::minimum());
1269
1270        // Audit log entries are served from `mz_internal.mz_catalog_raw` via
1271        // the `mz_audit_events` materialized view, so they do not need to live
1272        // in the in-memory catalog snapshot. Drop them here and only keep the
1273        // count for metrics.
1274        let (audit_logs, snapshot): (Vec<_>, Vec<_>) = self
1275            .snapshot
1276            .into_iter()
1277            .partition(|(update, _, _)| update.is_audit_log());
1278        self.snapshot = snapshot;
1279        let audit_log_count = audit_logs.iter().map(|(_, _, diff)| diff).sum::<Diff>();
1280        drop(audit_logs);
1281
1282        // Perform data migrations.
1283        if is_initialized && !read_only {
1284            commit_ts = upgrade(&mut self, commit_ts).await?;
1285        }
1286
1287        debug!(
1288            ?is_initialized,
1289            ?self.upper,
1290            "initializing catalog state"
1291        );
1292        let mut catalog = PersistCatalogState {
1293            mode: self.mode,
1294            since_handle: self.since_handle,
1295            write_handle: self.write_handle,
1296            listen: self.listen,
1297            persist_client: self.persist_client,
1298            shard_id: self.shard_id,
1299            upper: self.upper,
1300            fenceable_token: self.fenceable_token,
1301            // Initialize empty in-memory state.
1302            snapshot: Vec::new(),
1303            update_applier: CatalogStateInner::new(),
1304            catalog_content_version: self.catalog_content_version,
1305            bootstrap_complete: false,
1306            metrics: self.metrics,
1307            size_at_last_consolidation: None,
1308            updates_applied: 0,
1309        };
1310        catalog.metrics.collection_entries.reset();
1311        // Normally, `collection_entries` is updated in `apply_updates`. The audit log updates skip
1312        // over that function so we manually update it here.
1313        catalog
1314            .metrics
1315            .collection_entries
1316            .with_label_values(&[&CollectionType::AuditLog.to_string()])
1317            .add(audit_log_count.into_inner());
1318        let updates = self.snapshot.into_iter().map(|(kind, ts, diff)| {
1319            let kind = TryIntoStateUpdateKind::try_into(kind).expect("kind decoding error");
1320            StateUpdate { kind, ts, diff }
1321        });
1322        catalog.apply_updates_and_consolidate(updates)?;
1323
1324        let catalog_content_version = catalog.catalog_content_version.to_string();
1325        let txn = if is_initialized {
1326            let mut txn = catalog.transaction_unchecked().await?;
1327
1328            // Ad-hoc migration: Initialize the `migration_version` expected by adapter to be
1329            // present in existing catalogs.
1330            //
1331            // Note: Need to exclude read-only catalog mode here, because in that mode all
1332            // transactions are expected to be no-ops.
1333            // TODO: remove this once we only support upgrades from version >= 0.164
1334            if txn.get_setting("migration_version".into()).is_none() && mode != Mode::Readonly {
1335                let old_version = txn.get_catalog_content_version();
1336                txn.set_setting("migration_version".into(), old_version.map(Into::into))?;
1337            }
1338
1339            // Opening the catalog with write intent fences out every previous
1340            // catalog owner, so all sessions served by previous owners are
1341            // dead. Reclaim the temporary items they owned here, before
1342            // anything else reads the catalog.
1343            //
1344            // NOTE: This only reclaims on the fence, which covers a
1345            // single-writer world where every crash is followed by some
1346            // process's writable open. Once several serving envds run
1347            // concurrently, a peer crash triggers no fence here, so
1348            // reclaiming its sessions' items needs the durable
1349            // envd-heartbeat mechanism described in the durable temporary
1350            // objects design doc.
1351            if mode != Mode::Readonly {
1352                txn.remove_ephemeral_items();
1353            }
1354
1355            txn.set_catalog_content_version(catalog_content_version)?;
1356            txn
1357        } else {
1358            soft_assert_eq_no_log!(
1359                catalog
1360                    .snapshot
1361                    .iter()
1362                    .filter(|(kind, _, _)| !matches!(kind, StateUpdateKind::FenceToken(_)))
1363                    .count(),
1364                0,
1365                "trace should not contain any updates for an uninitialized catalog: {:#?}",
1366                catalog.snapshot
1367            );
1368
1369            let mut txn = catalog.transaction_unchecked().await?;
1370            initialize::initialize(
1371                &mut txn,
1372                bootstrap_args,
1373                initial_ts.into(),
1374                catalog_content_version,
1375            )
1376            .await?;
1377            txn
1378        };
1379
1380        if read_only {
1381            let (txn_batch, _) = txn.into_parts()?;
1382            // The upper here doesn't matter because we are only applying the updates in memory.
1383            let updates = StateUpdate::from_txn_batch_ts(txn_batch, catalog.upper);
1384            catalog.apply_updates_and_consolidate(updates)?;
1385        } else {
1386            txn.commit_internal(commit_ts).await?;
1387        }
1388
1389        if matches!(catalog.mode, Mode::Writable) {
1390            let write_handle = catalog
1391                .persist_client
1392                .open_writer::<SourceData, (), Timestamp, i64>(
1393                    catalog.write_handle.shard_id(),
1394                    Arc::new(persist_desc()),
1395                    Arc::new(UnitSchema::default()),
1396                    Diagnostics {
1397                        shard_name: CATALOG_SHARD_NAME.to_string(),
1398                        handle_purpose: "compact catalog".to_string(),
1399                    },
1400                )
1401                .await
1402                .expect("invalid usage");
1403            let fuel = CATALOG_FORCE_COMPACTION_FUEL.handle(catalog.persist_client.dyncfgs());
1404            let wait = CATALOG_FORCE_COMPACTION_WAIT.handle(catalog.persist_client.dyncfgs());
1405            // We're going to gradually turn this on via dyncfgs. Run it in a task so that it
1406            // doesn't block startup.
1407            let _task = mz_ore::task::spawn(|| "catalog::force_shard_compaction", async move {
1408                let () =
1409                    mz_persist_client::cli::admin::dangerous_force_compaction_and_break_pushdown(
1410                        &write_handle,
1411                        || fuel.get(),
1412                        || wait.get(),
1413                    )
1414                    .await;
1415            });
1416        }
1417
1418        Ok(Box::new(catalog))
1419    }
1420
1421    /// Reports if the catalog state has been initialized.
1422    ///
1423    /// NOTE: This is the answer as of the last call to [`PersistHandle::sync`] or [`PersistHandle::sync_to_current_upper`],
1424    /// not necessarily what is currently in persist.
1425    #[mz_ore::instrument]
1426    fn is_initialized_inner(&self) -> bool {
1427        !self.update_applier.configs.is_empty()
1428    }
1429
1430    /// Get the current value of config `key`.
1431    ///
1432    /// Some configs need to be read before the catalog is opened for bootstrapping.
1433    #[mz_ore::instrument]
1434    async fn get_current_config(&mut self, key: &str) -> Result<Option<u64>, DurableCatalogError> {
1435        self.sync_to_current_upper().await?;
1436        Ok(self.update_applier.configs.get(key).cloned())
1437    }
1438
1439    /// Get the user version of this instance.
1440    ///
1441    /// The user version is used to determine if a migration is needed.
1442    #[mz_ore::instrument]
1443    pub(crate) async fn get_user_version(&mut self) -> Result<Option<u64>, DurableCatalogError> {
1444        self.get_current_config(USER_VERSION_KEY).await
1445    }
1446
1447    /// Get the current value of setting `name`.
1448    ///
1449    /// Some settings need to be read before the catalog is opened for bootstrapping.
1450    #[mz_ore::instrument]
1451    async fn get_current_setting(
1452        &mut self,
1453        name: &str,
1454    ) -> Result<Option<String>, DurableCatalogError> {
1455        self.sync_to_current_upper().await?;
1456        Ok(self.update_applier.settings.get(name).cloned())
1457    }
1458
1459    /// Get the catalog content version.
1460    ///
1461    /// The catalog content version is the semantic version of the most recent binary that wrote to
1462    /// the catalog.
1463    #[mz_ore::instrument]
1464    async fn get_catalog_content_version(
1465        &mut self,
1466    ) -> Result<Option<semver::Version>, DurableCatalogError> {
1467        let version = self
1468            .get_current_setting(CATALOG_CONTENT_VERSION_KEY)
1469            .await?;
1470        let version = version.map(|version| version.parse().expect("invalid version persisted"));
1471        Ok(version)
1472    }
1473}
1474
1475#[async_trait]
1476impl OpenableDurableCatalogState for UnopenedPersistCatalogState {
1477    #[mz_ore::instrument]
1478    async fn open_savepoint(
1479        mut self: Box<Self>,
1480        initial_ts: Timestamp,
1481        bootstrap_args: &BootstrapArgs,
1482    ) -> Result<Box<dyn DurableCatalogState>, CatalogError> {
1483        self.open_inner(Mode::Savepoint, initial_ts, bootstrap_args)
1484            .boxed()
1485            .await
1486    }
1487
1488    #[mz_ore::instrument]
1489    async fn open_read_only(
1490        mut self: Box<Self>,
1491        bootstrap_args: &BootstrapArgs,
1492    ) -> Result<Box<dyn DurableCatalogState>, CatalogError> {
1493        self.open_inner(Mode::Readonly, EpochMillis::MIN.into(), bootstrap_args)
1494            .boxed()
1495            .await
1496    }
1497
1498    #[mz_ore::instrument]
1499    async fn open(
1500        mut self: Box<Self>,
1501        initial_ts: Timestamp,
1502        bootstrap_args: &BootstrapArgs,
1503    ) -> Result<Box<dyn DurableCatalogState>, CatalogError> {
1504        self.open_inner(Mode::Writable, initial_ts, bootstrap_args)
1505            .boxed()
1506            .await
1507    }
1508
1509    #[mz_ore::instrument(level = "debug")]
1510    async fn open_debug(mut self: Box<Self>) -> Result<DebugCatalogState, CatalogError> {
1511        Ok(DebugCatalogState(*self))
1512    }
1513
1514    #[mz_ore::instrument]
1515    async fn is_initialized(&mut self) -> Result<bool, CatalogError> {
1516        self.sync_to_current_upper().await?;
1517        Ok(self.is_initialized_inner())
1518    }
1519
1520    #[mz_ore::instrument]
1521    async fn epoch(&mut self) -> Result<Epoch, CatalogError> {
1522        self.sync_to_current_upper().await?;
1523        self.fenceable_token
1524            .validate()?
1525            .map(|token| token.epoch)
1526            .ok_or(CatalogError::Durable(DurableCatalogError::Uninitialized))
1527    }
1528
1529    #[mz_ore::instrument]
1530    async fn get_deployment_generation(&mut self) -> Result<u64, CatalogError> {
1531        self.sync_to_current_upper().await?;
1532        self.fenceable_token
1533            .token()
1534            .map(|token| token.deploy_generation)
1535            .ok_or(CatalogError::Durable(DurableCatalogError::Uninitialized))
1536    }
1537
1538    #[mz_ore::instrument(level = "debug")]
1539    async fn get_0dt_deployment_max_wait(&mut self) -> Result<Option<Duration>, CatalogError> {
1540        let value = self
1541            .get_current_config(WITH_0DT_DEPLOYMENT_MAX_WAIT)
1542            .await?;
1543        match value {
1544            None => Ok(None),
1545            Some(millis) => Ok(Some(Duration::from_millis(millis))),
1546        }
1547    }
1548
1549    #[mz_ore::instrument(level = "debug")]
1550    async fn get_0dt_deployment_ddl_check_interval(
1551        &mut self,
1552    ) -> Result<Option<Duration>, CatalogError> {
1553        let value = self
1554            .get_current_config(WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL)
1555            .await?;
1556        match value {
1557            None => Ok(None),
1558            Some(millis) => Ok(Some(Duration::from_millis(millis))),
1559        }
1560    }
1561
1562    #[mz_ore::instrument(level = "debug")]
1563    async fn get_enable_0dt_deployment_panic_after_timeout(
1564        &mut self,
1565    ) -> Result<Option<bool>, CatalogError> {
1566        let value = self
1567            .get_current_config(ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT)
1568            .await?;
1569        match value {
1570            None => Ok(None),
1571            Some(0) => Ok(Some(false)),
1572            Some(1) => Ok(Some(true)),
1573            Some(v) => Err(
1574                DurableCatalogError::from(TryFromProtoError::UnknownEnumVariant(format!(
1575                    "{v} is not a valid boolean value"
1576                )))
1577                .into(),
1578            ),
1579        }
1580    }
1581
1582    #[mz_ore::instrument]
1583    async fn has_system_config_synced_once(&mut self) -> Result<bool, DurableCatalogError> {
1584        self.get_current_config(SYSTEM_CONFIG_SYNCED_KEY)
1585            .await
1586            .map(|value| value.map(|value| value > 0).unwrap_or(false))
1587    }
1588
1589    #[mz_ore::instrument]
1590    async fn trace_unconsolidated(&mut self) -> Result<Trace, CatalogError> {
1591        self.sync_to_current_upper().await?;
1592        if self.is_initialized_inner() {
1593            let snapshot = self.snapshot_unconsolidated().await;
1594            Ok(Trace::from_snapshot(snapshot))
1595        } else {
1596            Err(CatalogError::Durable(DurableCatalogError::Uninitialized))
1597        }
1598    }
1599
1600    #[mz_ore::instrument]
1601    async fn trace_consolidated(&mut self) -> Result<Trace, CatalogError> {
1602        self.sync_to_current_upper().await?;
1603        if self.is_initialized_inner() {
1604            let snapshot = self.current_snapshot().await?;
1605            Ok(Trace::from_snapshot(snapshot))
1606        } else {
1607            Err(CatalogError::Durable(DurableCatalogError::Uninitialized))
1608        }
1609    }
1610
1611    #[mz_ore::instrument(level = "debug")]
1612    async fn expire(self: Box<Self>) {
1613        self.expire().await
1614    }
1615}
1616
1617/// Applies updates for an opened catalog.
1618#[derive(Debug)]
1619struct CatalogStateInner {
1620    /// A trace of all catalog updates that can be consumed by some higher layer.
1621    updates: VecDeque<memory::objects::StateUpdate>,
1622}
1623
1624impl CatalogStateInner {
1625    fn new() -> CatalogStateInner {
1626        CatalogStateInner {
1627            updates: VecDeque::new(),
1628        }
1629    }
1630}
1631
1632impl ApplyUpdate<StateUpdateKind> for CatalogStateInner {
1633    fn apply_update(
1634        &mut self,
1635        update: StateUpdate<StateUpdateKind>,
1636        current_fence_token: &mut FenceableToken,
1637        metrics: &Arc<Metrics>,
1638    ) -> Result<Option<StateUpdate<StateUpdateKind>>, FenceError> {
1639        if let Some(collection_type) = update.kind.collection_type() {
1640            metrics
1641                .collection_entries
1642                .with_label_values(&[&collection_type.to_string()])
1643                .add(update.diff.into_inner());
1644        }
1645
1646        {
1647            let update: Option<memory::objects::StateUpdate> = (&update)
1648                .try_into()
1649                .expect("invalid persisted update: {update:#?}");
1650            if let Some(update) = update {
1651                self.updates.push_back(update);
1652            }
1653        }
1654
1655        match (update.kind, update.diff) {
1656            (StateUpdateKind::AuditLog(_, ()), _) => Ok(None),
1657            // Nothing to due for fence token retractions but wait for the next insertion.
1658            (StateUpdateKind::FenceToken(_), Diff::MINUS_ONE) => Ok(None),
1659            (StateUpdateKind::FenceToken(token), Diff::ONE) => {
1660                current_fence_token.maybe_fence(token)?;
1661                Ok(None)
1662            }
1663            (kind, diff) => Ok(Some(StateUpdate {
1664                kind,
1665                ts: update.ts,
1666                diff,
1667            })),
1668        }
1669    }
1670}
1671
1672/// A durable store of the catalog state using Persist as an implementation. The durable store can
1673/// serve any catalog data and transactionally modify catalog data.
1674///
1675/// Production users should call [`Self::expire`] before dropping a [`PersistCatalogState`]
1676/// so that it can expire its leases. If/when rust gets AsyncDrop, this will be done automatically.
1677type PersistCatalogState = PersistHandle<StateUpdateKind, CatalogStateInner>;
1678
1679impl PersistHandle<StateUpdateKind, CatalogStateInner> {
1680    /// Creates a transaction without validating pending catalog updates.
1681    async fn transaction_unchecked(&mut self) -> Result<Transaction<'_>, CatalogError> {
1682        self.metrics.transactions_started.inc();
1683        let snapshot = self.snapshot().await?;
1684        let commit_ts = self.upper;
1685        Transaction::new(self, snapshot, commit_ts)
1686    }
1687}
1688
1689#[async_trait]
1690impl ReadOnlyDurableCatalogState for PersistCatalogState {
1691    fn epoch(&self) -> Epoch {
1692        self.fenceable_token
1693            .token()
1694            .expect("opened catalog state must have an epoch")
1695            .epoch
1696    }
1697
1698    fn metrics(&self) -> &Metrics {
1699        &self.metrics
1700    }
1701
1702    #[mz_ore::instrument(level = "debug")]
1703    async fn expire(self: Box<Self>) {
1704        self.expire().await
1705    }
1706
1707    fn is_bootstrap_complete(&self) -> bool {
1708        self.bootstrap_complete
1709    }
1710
1711    async fn get_audit_logs(&mut self) -> Result<Vec<VersionedEvent>, CatalogError> {
1712        self.sync_to_current_upper().await?;
1713        let audit_logs: Vec<_> = self
1714            .persist_snapshot()
1715            .await
1716            .filter_map(
1717                |StateUpdate {
1718                     kind,
1719                     ts: _,
1720                     diff: _,
1721                 }| match kind {
1722                    StateUpdateKind::AuditLog(key, ()) => Some(key),
1723                    _ => None,
1724                },
1725            )
1726            .collect();
1727        let mut audit_logs: Vec<_> = audit_logs
1728            .into_iter()
1729            .map(RustType::from_proto)
1730            .map_ok(|key: AuditLogKey| key.event)
1731            .collect::<Result<_, _>>()?;
1732        audit_logs.sort_by(|a, b| a.sortable_id().cmp(&b.sortable_id()));
1733        Ok(audit_logs)
1734    }
1735
1736    #[mz_ore::instrument(level = "debug")]
1737    async fn get_next_id(&mut self, id_type: &str) -> Result<u64, CatalogError> {
1738        self.with_trace(|trace| {
1739            Ok(trace
1740                .into_iter()
1741                .rev()
1742                .filter_map(|(kind, _, _)| match kind {
1743                    StateUpdateKind::IdAllocator(key, value) if key.name == id_type => {
1744                        Some(value.next_id)
1745                    }
1746                    _ => None,
1747                })
1748                .next()
1749                .expect("must exist"))
1750        })
1751        .await
1752    }
1753
1754    #[mz_ore::instrument(level = "debug")]
1755    async fn get_deployment_generation(&mut self) -> Result<u64, CatalogError> {
1756        self.sync_to_current_upper().await?;
1757        Ok(self
1758            .fenceable_token
1759            .token()
1760            .expect("opened catalogs must have a token")
1761            .deploy_generation)
1762    }
1763
1764    #[mz_ore::instrument(level = "debug")]
1765    async fn snapshot(&mut self) -> Result<Snapshot, CatalogError> {
1766        self.with_snapshot(Ok).await
1767    }
1768
1769    #[mz_ore::instrument(level = "debug")]
1770    async fn sync_to_current_updates(
1771        &mut self,
1772    ) -> Result<Vec<memory::objects::StateUpdate>, CatalogError> {
1773        let upper = self.current_upper().await;
1774        self.sync_updates(upper).await
1775    }
1776
1777    #[mz_ore::instrument(level = "debug")]
1778    async fn sync_updates(
1779        &mut self,
1780        target_upper: mz_repr::Timestamp,
1781    ) -> Result<Vec<memory::objects::StateUpdate>, CatalogError> {
1782        self.sync(target_upper).await?;
1783        let mut updates = Vec::new();
1784        while let Some(update) = self.update_applier.updates.front() {
1785            if update.ts >= target_upper {
1786                break;
1787            }
1788
1789            let update = self
1790                .update_applier
1791                .updates
1792                .pop_front()
1793                .expect("peeked above");
1794            updates.push(update);
1795        }
1796        Ok(updates)
1797    }
1798
1799    #[mz_ore::instrument(level = "debug")]
1800    async fn ensure_not_out_of_sync(
1801        &mut self,
1802        target_upper: Timestamp,
1803    ) -> Result<(), CatalogError> {
1804        self.sync(target_upper).await?;
1805        let update_count = self
1806            .update_applier
1807            .updates
1808            .iter()
1809            .take_while(|update| update.ts < target_upper)
1810            .count();
1811        if update_count == 0 {
1812            Ok(())
1813        } else {
1814            Err(DurableCatalogError::CatalogOutOfSync {
1815                update_count,
1816                upper: target_upper,
1817            }
1818            .into())
1819        }
1820    }
1821
1822    async fn current_upper(&mut self) -> Timestamp {
1823        self.current_upper().await
1824    }
1825}
1826
1827#[async_trait]
1828#[allow(mismatched_lifetime_syntaxes)]
1829impl DurableCatalogState for PersistCatalogState {
1830    fn is_read_only(&self) -> bool {
1831        matches!(self.mode, Mode::Readonly)
1832    }
1833
1834    fn is_savepoint(&self) -> bool {
1835        matches!(self.mode, Mode::Savepoint)
1836    }
1837
1838    async fn mark_bootstrap_complete(&mut self) {
1839        self.bootstrap_complete = true;
1840        if matches!(self.mode, Mode::Writable) {
1841            self.since_handle
1842                .upgrade_version()
1843                .await
1844                .expect("invalid usage")
1845        }
1846    }
1847
1848    #[mz_ore::instrument(level = "debug")]
1849    async fn transaction(&mut self) -> Result<Transaction, CatalogError> {
1850        let mut txn = self.transaction_unchecked().await?;
1851        txn.ensure_not_out_of_sync().await?;
1852        Ok(txn)
1853    }
1854
1855    fn transaction_from_snapshot(
1856        &mut self,
1857        snapshot: Snapshot,
1858    ) -> Result<DryRunTransaction, CatalogError> {
1859        let commit_ts = self.upper;
1860        Transaction::new(self, snapshot, commit_ts).map(DryRunTransaction::new)
1861    }
1862
1863    #[mz_ore::instrument(level = "debug")]
1864    async fn allocate_id(
1865        &mut self,
1866        id_type: &str,
1867        amount: u64,
1868        commit_ts: Timestamp,
1869    ) -> Result<Vec<u64>, CatalogError> {
1870        let start = Instant::now();
1871        if amount == 0 {
1872            return Ok(Vec::new());
1873        }
1874        let mut txn = self.transaction_unchecked().await?;
1875        let ids = txn.get_and_increment_id_by(id_type.to_string(), amount)?;
1876        txn.commit_internal(commit_ts).await?;
1877        self.metrics
1878            .allocate_id_seconds
1879            .observe(start.elapsed().as_secs_f64());
1880        Ok(ids)
1881    }
1882
1883    #[mz_ore::instrument(level = "debug")]
1884    async fn commit_transaction(
1885        &mut self,
1886        txn_batch: TransactionBatch,
1887        commit_ts: Timestamp,
1888    ) -> Result<Timestamp, CatalogError> {
1889        async fn commit_transaction_inner(
1890            catalog: &mut PersistCatalogState,
1891            txn_batch: TransactionBatch,
1892            commit_ts: Timestamp,
1893        ) -> Result<Timestamp, CatalogError> {
1894            // If the transaction is empty then we don't error, even in read-only mode.
1895            // This is mostly for legacy reasons (i.e. with enough elbow grease this
1896            // behavior can be changed without breaking any fundamental assumptions).
1897            if catalog.mode == Mode::Readonly {
1898                let updates: Vec<_> = StateUpdate::from_txn_batch(txn_batch).collect();
1899                if !updates.is_empty() {
1900                    let collection_types: Vec<_> = updates
1901                        .iter()
1902                        .filter_map(|u| u.0.collection_type())
1903                        .collect();
1904                    return Err(DurableCatalogError::NotWritable(format!(
1905                        "cannot commit a transaction in a read-only catalog: \
1906                         {} updates across collections: {collection_types:?}",
1907                        updates.len(),
1908                    ))
1909                    .into());
1910                }
1911                return Ok(catalog.upper);
1912            }
1913
1914            // The handle is mutex-protected from transaction creation through commit, so only a
1915            // local programming error can change its cached upper here.
1916            assert_eq!(
1917                catalog.upper, txn_batch.upper,
1918                "the handle was mutated mid-transaction"
1919            );
1920
1921            let updates: Vec<_> = StateUpdate::from_txn_batch(txn_batch).collect();
1922            debug!("committing updates: {updates:?}");
1923
1924            // Empty upper progress does not invalidate the transaction.
1925            let mut commit_ts = max(commit_ts, catalog.upper);
1926
1927            let next_upper = match catalog.mode {
1928                Mode::Writable => loop {
1929                    let updates_applied_before = catalog.updates_applied;
1930                    match catalog.compare_and_append(updates.clone(), commit_ts).await {
1931                        Ok(next_upper) => break next_upper,
1932                        Err(CompareAndAppendError::Fence(e)) => return Err(e.into()),
1933                        Err(CompareAndAppendError::UpperMismatch { actual_upper, .. }) => {
1934                            // The mismatch synchronized the handle. Retry only if it applied no
1935                            // content.
1936                            catalog
1937                                .classify_upper_mismatch(updates_applied_before, actual_upper)?;
1938                            commit_ts = max(commit_ts, catalog.upper);
1939                        }
1940                    }
1941                },
1942                Mode::Savepoint => {
1943                    let updates = updates.into_iter().map(|(kind, diff)| StateUpdate {
1944                        kind,
1945                        ts: commit_ts,
1946                        diff,
1947                    });
1948                    catalog.apply_updates_and_consolidate(updates)?;
1949                    catalog.upper = commit_ts.step_forward();
1950                    catalog.upper
1951                }
1952                Mode::Readonly => unreachable!("handled above"),
1953            };
1954
1955            Ok(next_upper)
1956        }
1957        self.metrics.transaction_commits.inc();
1958        let histogram = self.metrics.transaction_commit_latency_seconds.clone();
1959        commit_transaction_inner(self, txn_batch, commit_ts)
1960            .wall_time()
1961            .observe(histogram)
1962            .await
1963    }
1964
1965    #[mz_ore::instrument(level = "debug")]
1966    async fn advance_upper(&mut self, new_upper: Timestamp) -> Result<(), CatalogError> {
1967        loop {
1968            if self.upper >= new_upper {
1969                // This does not consult Persist. It only revalidates a fence already cached by
1970                // this handle, since a sync can advance `upper` while recording the fence.
1971                self.fenceable_token.validate()?;
1972                return Ok(());
1973            }
1974
1975            match self.mode {
1976                Mode::Writable => {}
1977                Mode::Savepoint => {
1978                    self.upper = new_upper;
1979                    return Ok(());
1980                }
1981                Mode::Readonly => {
1982                    return Err(DurableCatalogError::NotWritable(
1983                        "cannot advance upper of a read-only catalog".into(),
1984                    )
1985                    .into());
1986                }
1987            }
1988
1989            let updates_applied_before = self.updates_applied;
1990            match self.compare_and_append_inner([], new_upper).await {
1991                Ok(()) => {
1992                    self.upper = new_upper;
1993                    // No sync needed since no data was written.
1994                    return Ok(());
1995                }
1996                Err(CompareAndAppendError::Fence(e)) => return Err(e.into()),
1997                Err(CompareAndAppendError::UpperMismatch { actual_upper, .. }) => {
1998                    // The mismatch synchronized the handle. Retry only if it applied no content.
1999                    self.classify_upper_mismatch(updates_applied_before, actual_upper)?;
2000                }
2001            }
2002        }
2003    }
2004
2005    fn shard_id(&self) -> ShardId {
2006        self.shard_id
2007    }
2008}
2009
2010/// Deterministically generate a shard ID for the given `organization_id` and `seed`.
2011pub fn shard_id(organization_id: Uuid, seed: usize) -> ShardId {
2012    let hash = sha2::Sha256::digest(format!("{organization_id}{seed}")).to_vec();
2013    soft_assert_eq_or_log!(hash.len(), 32, "SHA256 returns 32 bytes (256 bits)");
2014    let uuid = Uuid::from_slice(&hash[0..16]).expect("from_slice accepts exactly 16 bytes");
2015    ShardId::from_str(&format!("s{uuid}")).expect("known to be valid")
2016}
2017
2018/// Generates a timestamp for reading from `read_handle` that is as fresh as possible, given
2019/// `upper`.
2020fn as_of(
2021    read_handle: &ReadHandle<SourceData, (), Timestamp, StorageDiff>,
2022    upper: Timestamp,
2023) -> Timestamp {
2024    let since = read_handle.since().clone();
2025    let mut as_of = upper.checked_sub(1).unwrap_or_else(|| {
2026        panic!("catalog persist shard should be initialize, found upper: {upper:?}")
2027    });
2028    // We only downgrade the since after writing, and we always set the since to one less than the
2029    // upper.
2030    soft_assert_or_log!(
2031        since.less_equal(&as_of),
2032        "since={since:?}, as_of={as_of:?}; since must be less than or equal to as_of"
2033    );
2034    // This should be a no-op if the assert above passes, however if it doesn't then we'd like to
2035    // continue with a correct timestamp instead of entering a panic loop.
2036    as_of.advance_by(since.borrow());
2037    as_of
2038}
2039
2040/// Fetch the persist version of the catalog shard, if one exists. A version will not
2041/// exist if we are creating a brand-new environment.
2042async fn fetch_catalog_shard_version(
2043    persist_client: &PersistClient,
2044    catalog_shard_id: ShardId,
2045) -> Option<semver::Version> {
2046    let shard_state = persist_client
2047        .inspect_shard::<Timestamp>(&catalog_shard_id)
2048        .await
2049        .ok()?;
2050    let json_state = serde_json::to_value(shard_state).expect("state serialization error");
2051    let json_version = json_state
2052        .get("applier_version")
2053        .cloned()
2054        .expect("missing applier_version");
2055    let version = serde_json::from_value(json_version).expect("version deserialization error");
2056    Some(version)
2057}
2058
2059/// Generates an iterator of [`StateUpdate`] that contain all updates to the catalog
2060/// state up to, and including, `as_of`.
2061///
2062/// The output is consolidated and sorted by timestamp in ascending order.
2063#[mz_ore::instrument(level = "debug")]
2064async fn snapshot_binary(
2065    read_handle: &mut ReadHandle<SourceData, (), Timestamp, StorageDiff>,
2066    as_of: Timestamp,
2067    metrics: &Arc<Metrics>,
2068) -> impl Iterator<Item = StateUpdate<StateUpdateKindJson>> + DoubleEndedIterator + use<> {
2069    metrics.snapshots_taken.inc();
2070    let histogram = metrics.snapshot_latency_seconds.clone();
2071    snapshot_binary_inner(read_handle, as_of)
2072        .wall_time()
2073        .observe(histogram)
2074        .await
2075}
2076
2077/// Generates an iterator of [`StateUpdate`] that contain all updates to the catalog
2078/// state up to, and including, `as_of`.
2079///
2080/// The output is consolidated and sorted by timestamp in ascending order.
2081#[mz_ore::instrument(level = "debug")]
2082async fn snapshot_binary_inner(
2083    read_handle: &mut ReadHandle<SourceData, (), Timestamp, StorageDiff>,
2084    as_of: Timestamp,
2085) -> impl Iterator<Item = StateUpdate<StateUpdateKindJson>> + DoubleEndedIterator + use<> {
2086    let snapshot = read_handle
2087        .snapshot_and_fetch(Antichain::from_elem(as_of))
2088        .await
2089        .expect("we have advanced the restart_as_of by the since");
2090    soft_assert_no_log!(
2091        snapshot.iter().all(|(_, _, diff)| *diff == 1),
2092        "snapshot_and_fetch guarantees a consolidated result: {snapshot:#?}"
2093    );
2094    snapshot
2095        .into_iter()
2096        .map(Into::<StateUpdate<StateUpdateKindJson>>::into)
2097        .sorted_by(|a, b| Ord::cmp(&b.ts, &a.ts))
2098}
2099
2100/// Convert an [`Antichain<Timestamp>`] to a [`Timestamp`].
2101///
2102/// The correctness of this function relies on [`Timestamp`] being totally ordered and never
2103/// finalizing the catalog shard.
2104pub(crate) fn antichain_to_timestamp(antichain: Antichain<Timestamp>) -> Timestamp {
2105    antichain
2106        .into_option()
2107        .expect("we use a totally ordered time and never finalize the shard")
2108}
2109
2110// Debug methods used by the catalog-debug tool.
2111
2112impl Trace {
2113    /// Generates a [`Trace`] from snapshot.
2114    fn from_snapshot(snapshot: impl IntoIterator<Item = StateUpdate>) -> Trace {
2115        let mut trace = Trace::new();
2116        for StateUpdate { kind, ts, diff } in snapshot {
2117            match kind {
2118                StateUpdateKind::AuditLog(k, v) => trace.audit_log.values.push(((k, v), ts, diff)),
2119                StateUpdateKind::Cluster(k, v) => trace.clusters.values.push(((k, v), ts, diff)),
2120                StateUpdateKind::ClusterReplica(k, v) => {
2121                    trace.cluster_replicas.values.push(((k, v), ts, diff))
2122                }
2123                StateUpdateKind::ClusterReplicaSize(k, v) => {
2124                    trace.cluster_replica_sizes.values.push(((k, v), ts, diff))
2125                }
2126                StateUpdateKind::Comment(k, v) => trace.comments.values.push(((k, v), ts, diff)),
2127                StateUpdateKind::IndexColumns(k, v) => {
2128                    trace.index_columns.values.push(((k, v), ts, diff))
2129                }
2130                StateUpdateKind::ItemColumns(k, v) => {
2131                    trace.item_columns.values.push(((k, v), ts, diff))
2132                }
2133                StateUpdateKind::Config(k, v) => trace.configs.values.push(((k, v), ts, diff)),
2134                StateUpdateKind::Database(k, v) => trace.databases.values.push(((k, v), ts, diff)),
2135                StateUpdateKind::DefaultPrivilege(k, v) => {
2136                    trace.default_privileges.values.push(((k, v), ts, diff))
2137                }
2138                StateUpdateKind::FenceToken(_) => {
2139                    // Fence token not included in trace.
2140                }
2141                StateUpdateKind::IdAllocator(k, v) => {
2142                    trace.id_allocator.values.push(((k, v), ts, diff))
2143                }
2144                StateUpdateKind::IntrospectionSourceIndex(k, v) => {
2145                    trace.introspection_sources.values.push(((k, v), ts, diff))
2146                }
2147                StateUpdateKind::Item(k, v) => trace.items.values.push(((k, v), ts, diff)),
2148                StateUpdateKind::NetworkPolicy(k, v) => {
2149                    trace.network_policies.values.push(((k, v), ts, diff))
2150                }
2151                StateUpdateKind::Role(k, v) => trace.roles.values.push(((k, v), ts, diff)),
2152                StateUpdateKind::Schema(k, v) => trace.schemas.values.push(((k, v), ts, diff)),
2153                StateUpdateKind::Setting(k, v) => trace.settings.values.push(((k, v), ts, diff)),
2154                StateUpdateKind::SourceReferences(k, v) => {
2155                    trace.source_references.values.push(((k, v), ts, diff))
2156                }
2157                StateUpdateKind::SystemConfiguration(k, v) => {
2158                    trace.system_configurations.values.push(((k, v), ts, diff))
2159                }
2160                StateUpdateKind::ClusterSystemConfiguration(k, v) => trace
2161                    .cluster_system_configurations
2162                    .values
2163                    .push(((k, v), ts, diff)),
2164                StateUpdateKind::ReplicaSystemConfiguration(k, v) => trace
2165                    .replica_system_configurations
2166                    .values
2167                    .push(((k, v), ts, diff)),
2168                StateUpdateKind::SystemObjectMapping(k, v) => {
2169                    trace.system_object_mappings.values.push(((k, v), ts, diff))
2170                }
2171                StateUpdateKind::SystemPrivilege(k, v) => {
2172                    trace.system_privileges.values.push(((k, v), ts, diff))
2173                }
2174                StateUpdateKind::StorageCollectionMetadata(k, v) => trace
2175                    .storage_collection_metadata
2176                    .values
2177                    .push(((k, v), ts, diff)),
2178                StateUpdateKind::UnfinalizedShard(k, ()) => {
2179                    trace.unfinalized_shards.values.push(((k, ()), ts, diff))
2180                }
2181                StateUpdateKind::TxnWalShard((), v) => {
2182                    trace.txn_wal_shard.values.push((((), v), ts, diff))
2183                }
2184                StateUpdateKind::RoleAuth(k, v) => trace.role_auth.values.push(((k, v), ts, diff)),
2185            }
2186        }
2187        trace
2188    }
2189}
2190
2191impl UnopenedPersistCatalogState {
2192    /// Manually update value of `key` in collection `T` to `value`.
2193    #[mz_ore::instrument]
2194    pub(crate) async fn debug_edit<T: Collection>(
2195        &mut self,
2196        key: T::Key,
2197        value: T::Value,
2198    ) -> Result<Option<T::Value>, CatalogError>
2199    where
2200        T::Key: PartialEq + Eq + Debug + Clone,
2201        T::Value: Debug + Clone,
2202    {
2203        let prev_value = loop {
2204            let key = key.clone();
2205            let value = value.clone();
2206            let snapshot = self.current_snapshot().await?;
2207            let trace = Trace::from_snapshot(snapshot);
2208            let collection_trace = T::collection_trace(trace);
2209            let prev_values: Vec<_> = collection_trace
2210                .values
2211                .into_iter()
2212                .filter(|((k, _), _, diff)| {
2213                    soft_assert_eq_or_log!(*diff, Diff::ONE, "trace is consolidated");
2214                    &key == k
2215                })
2216                .collect();
2217
2218            let prev_value = match &prev_values[..] {
2219                [] => None,
2220                [((_, v), _, _)] => Some(v.clone()),
2221                prev_values => panic!("multiple values found for key {key:?}: {prev_values:?}"),
2222            };
2223
2224            let mut updates: Vec<_> = prev_values
2225                .into_iter()
2226                .map(|((k, v), _, _)| (T::update(k, v), Diff::MINUS_ONE))
2227                .collect();
2228            updates.push((T::update(key, value), Diff::ONE));
2229            // We must fence out all other catalogs, if we haven't already, since we are writing.
2230            match self.fenceable_token.generate_unfenced_token(self.mode)? {
2231                Some((fence_updates, current_fenceable_token)) => {
2232                    updates.extend(fence_updates.clone());
2233                    match self.compare_and_append(updates, self.upper).await {
2234                        Ok(_) => {
2235                            self.fenceable_token = current_fenceable_token;
2236                            break prev_value;
2237                        }
2238                        Err(CompareAndAppendError::Fence(e)) => return Err(e.into()),
2239                        Err(e @ CompareAndAppendError::UpperMismatch { .. }) => {
2240                            warn!("catalog write failed due to upper mismatch, retrying: {e:?}");
2241                            continue;
2242                        }
2243                    }
2244                }
2245                None => {
2246                    self.compare_and_append(updates, self.upper)
2247                        .await
2248                        .map_err(|e| e.unwrap_fence_error())?;
2249                    break prev_value;
2250                }
2251            }
2252        };
2253        Ok(prev_value)
2254    }
2255
2256    /// Manually delete `key` from collection `T`.
2257    #[mz_ore::instrument]
2258    pub(crate) async fn debug_delete<T: Collection>(
2259        &mut self,
2260        key: T::Key,
2261    ) -> Result<(), CatalogError>
2262    where
2263        T::Key: PartialEq + Eq + Debug + Clone,
2264        T::Value: Debug,
2265    {
2266        loop {
2267            let key = key.clone();
2268            let snapshot = self.current_snapshot().await?;
2269            let trace = Trace::from_snapshot(snapshot);
2270            let collection_trace = T::collection_trace(trace);
2271            let mut retractions: Vec<_> = collection_trace
2272                .values
2273                .into_iter()
2274                .filter(|((k, _), _, diff)| {
2275                    soft_assert_eq_or_log!(*diff, Diff::ONE, "trace is consolidated");
2276                    &key == k
2277                })
2278                .map(|((k, v), _, _)| (T::update(k, v), Diff::MINUS_ONE))
2279                .collect();
2280
2281            // We must fence out all other catalogs, if we haven't already, since we are writing.
2282            match self.fenceable_token.generate_unfenced_token(self.mode)? {
2283                Some((fence_updates, current_fenceable_token)) => {
2284                    retractions.extend(fence_updates.clone());
2285                    match self.compare_and_append(retractions, self.upper).await {
2286                        Ok(_) => {
2287                            self.fenceable_token = current_fenceable_token;
2288                            break;
2289                        }
2290                        Err(CompareAndAppendError::Fence(e)) => return Err(e.into()),
2291                        Err(e @ CompareAndAppendError::UpperMismatch { .. }) => {
2292                            warn!("catalog write failed due to upper mismatch, retrying: {e:?}");
2293                            continue;
2294                        }
2295                    }
2296                }
2297                None => {
2298                    self.compare_and_append(retractions, self.upper)
2299                        .await
2300                        .map_err(|e| e.unwrap_fence_error())?;
2301                    break;
2302                }
2303            }
2304        }
2305        Ok(())
2306    }
2307
2308    /// Generates a [`Vec<StateUpdate>`] that contain all updates to the catalog
2309    /// state.
2310    ///
2311    /// The output is consolidated and sorted by timestamp in ascending order and the current upper.
2312    async fn current_snapshot(
2313        &mut self,
2314    ) -> Result<impl IntoIterator<Item = StateUpdate> + '_, CatalogError> {
2315        self.sync_to_current_upper().await?;
2316        self.consolidate();
2317        Ok(self.snapshot.iter().cloned().map(|(kind, ts, diff)| {
2318            let kind = TryIntoStateUpdateKind::try_into(kind).expect("kind decoding error");
2319            StateUpdate { kind, ts, diff }
2320        }))
2321    }
2322}