Skip to main content

mz_catalog/
durable.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//! This crate is responsible for durably storing and modifying the catalog contents.
11
12use std::fmt::Debug;
13use std::num::NonZeroI64;
14use std::sync::Arc;
15use std::time::Duration;
16
17use async_trait::async_trait;
18use mz_audit_log::VersionedEvent;
19use mz_controller_types::{ClusterId, ReplicaId};
20use mz_ore::collections::CollectionExt;
21use mz_ore::metrics::MetricsRegistry;
22use mz_persist_client::PersistClient;
23use mz_persist_types::ShardId;
24use mz_repr::{CatalogItemId, GlobalId, RelationDesc, SqlScalarType};
25use mz_sql::catalog::CatalogError as SqlCatalogError;
26use uuid::Uuid;
27
28use crate::config::ClusterReplicaSizeMap;
29use crate::durable::debug::{DebugCatalogState, Trace};
30pub use crate::durable::error::{CatalogError, DurableCatalogError, FenceError};
31pub use crate::durable::metrics::Metrics;
32pub use crate::durable::objects::Snapshot;
33pub use crate::durable::objects::state_update::StateUpdate;
34pub use crate::durable::objects::{
35    BurstState, Cluster, ClusterConfig, ClusterReplica, ClusterReplicaSize,
36    ClusterSystemConfiguration, ClusterVariant, ClusterVariantManaged, Comment, Database,
37    DefaultPrivilege, IndexColumn, IndexColumns, IntrospectionSourceIndex, Item, ItemColumn,
38    ItemColumns, ItemMetadata, NetworkPolicy, ReconfigurationState, ReconfigurationStatus,
39    ReconfigurationTarget, ReplicaConfig, ReplicaLocation, ReplicaSystemConfiguration, Role,
40    RoleAuth, Schema, SourceReference, SourceReferences, StorageCollectionMetadata,
41    SystemConfiguration, SystemObjectDescription, SystemObjectMapping, UnfinalizedShard,
42    managed_cluster_replica_name,
43};
44pub use crate::durable::persist::shard_id;
45use crate::durable::persist::{Timestamp, UnopenedPersistCatalogState};
46use crate::durable::transaction::TransactionBatch;
47pub use crate::durable::transaction::{DryRunTransaction, Transaction};
48pub use crate::durable::upgrade::CATALOG_VERSION;
49use crate::memory;
50
51pub mod debug;
52mod error;
53pub mod initialize;
54mod metrics;
55pub mod objects;
56mod persist;
57mod traits;
58mod transaction;
59mod upgrade;
60
61pub const DATABASE_ID_ALLOC_KEY: &str = "database";
62pub const SCHEMA_ID_ALLOC_KEY: &str = "schema";
63pub const USER_ITEM_ALLOC_KEY: &str = "user";
64pub const SYSTEM_ITEM_ALLOC_KEY: &str = "system";
65pub const USER_ROLE_ID_ALLOC_KEY: &str = "user_role";
66pub const USER_CLUSTER_ID_ALLOC_KEY: &str = "user_compute";
67pub const SYSTEM_CLUSTER_ID_ALLOC_KEY: &str = "system_compute";
68pub const USER_REPLICA_ID_ALLOC_KEY: &str = "replica";
69pub const SYSTEM_REPLICA_ID_ALLOC_KEY: &str = "system_replica";
70pub const AUDIT_LOG_ID_ALLOC_KEY: &str = "auditlog";
71pub const STORAGE_USAGE_ID_ALLOC_KEY: &str = "storage_usage";
72pub const USER_NETWORK_POLICY_ID_ALLOC_KEY: &str = "user_network_policy";
73pub const USER_CLUSTER_REPLICA_SIZE_ID_ALLOC_KEY: &str = "user_cluster_replica_size";
74pub const SYSTEM_CLUSTER_REPLICA_SIZE_ID_ALLOC_KEY: &str = "system_cluster_replica_size";
75pub const OID_ALLOC_KEY: &str = "oid";
76pub(crate) const CATALOG_CONTENT_VERSION_KEY: &str = "catalog_content_version";
77pub const BUILTIN_MIGRATION_SHARD_KEY: &str = "builtin_migration_shard";
78pub const EXPRESSION_CACHE_SHARD_KEY: &str = "expression_cache_shard";
79pub const MOCK_AUTHENTICATION_NONCE_KEY: &str = "mock_authentication_nonce";
80
81#[derive(Clone, Debug)]
82pub struct BootstrapArgs {
83    pub cluster_replica_size_map: ClusterReplicaSizeMap,
84    pub default_cluster_replica_size: String,
85    pub default_cluster_replication_factor: u32,
86    pub bootstrap_role: Option<String>,
87    /// Testing only: every open that runs migrations also applies an empty migration.
88    pub force_catalog_migration: bool,
89}
90
91pub type Epoch = NonZeroI64;
92
93/// An API for opening a durable catalog state.
94///
95/// If a catalog is not opened, then resources should be release via [`Self::expire`].
96#[async_trait]
97pub trait OpenableDurableCatalogState: Debug + Send {
98    // TODO(jkosh44) Teaching savepoint mode how to listen to additional
99    // durable updates will be necessary for zero down time upgrades.
100    /// Opens the catalog in a mode that accepts and buffers all writes,
101    /// but never durably commits them. This is used to check and see if
102    /// opening the catalog would be successful, without making any durable
103    /// changes.
104    ///
105    /// Once a savepoint catalog reads an initial snapshot from durable
106    /// storage, it will never read another update from durable storage. As a
107    /// consequence, savepoint catalogs can never be fenced.
108    ///
109    /// Will return an error in the following scenarios:
110    ///   - Catalog initialization fails.
111    ///   - Catalog migrations fail.
112    ///
113    /// `initial_ts` is used as the initial timestamp for new environments.
114    async fn open_savepoint(
115        mut self: Box<Self>,
116        initial_ts: Timestamp,
117        bootstrap_args: &BootstrapArgs,
118    ) -> Result<Box<dyn DurableCatalogState>, CatalogError>;
119
120    /// Opens the catalog in read only mode. All mutating methods
121    /// will return an error.
122    ///
123    /// If the catalog is uninitialized or requires a migrations, then
124    /// it will fail to open in read only mode.
125    async fn open_read_only(
126        mut self: Box<Self>,
127        bootstrap_args: &BootstrapArgs,
128    ) -> Result<Box<dyn DurableCatalogState>, CatalogError>;
129
130    /// Opens the catalog in a writeable mode. Optionally initializes the
131    /// catalog, if it has not been initialized, and perform any migrations
132    /// needed.
133    ///
134    /// `initial_ts` is used as the initial timestamp for new environments.
135    async fn open(
136        mut self: Box<Self>,
137        initial_ts: Timestamp,
138        bootstrap_args: &BootstrapArgs,
139    ) -> Result<Box<dyn DurableCatalogState>, CatalogError>;
140
141    /// Opens the catalog for manual editing of the underlying data. This is helpful for
142    /// fixing a corrupt catalog.
143    async fn open_debug(mut self: Box<Self>) -> Result<DebugCatalogState, CatalogError>;
144
145    /// Reports if the catalog state has been initialized.
146    async fn is_initialized(&mut self) -> Result<bool, CatalogError>;
147
148    /// Returns the epoch of the current durable catalog state. The epoch acts as
149    /// a fencing token to prevent split brain issues across two
150    /// [`DurableCatalogState`]s. When a new [`DurableCatalogState`] opens the
151    /// catalog, it will increment the epoch by one (or initialize it to some
152    /// value if there's no existing epoch) and store the value in memory. It's
153    /// guaranteed that no two [`DurableCatalogState`]s will return the same value
154    /// for their epoch.
155    ///
156    /// NB: We may remove this in later iterations of Pv2.
157    async fn epoch(&mut self) -> Result<Epoch, CatalogError>;
158
159    /// Get the most recent deployment generation written to the catalog. Not necessarily the
160    /// deploy generation of this instance.
161    async fn get_deployment_generation(&mut self) -> Result<u64, CatalogError>;
162
163    /// Get the `with_0dt_deployment_max_wait` config value of this instance.
164    ///
165    /// This mirrors the `with_0dt_deployment_max_wait` "system var" so that we can
166    /// toggle the flag with LaunchDarkly, but use it in boot before
167    /// LaunchDarkly is available.
168    async fn get_0dt_deployment_max_wait(&mut self) -> Result<Option<Duration>, CatalogError>;
169
170    /// Get the `with_0dt_deployment_ddl_check_interval` config value of this instance.
171    ///
172    /// This mirrors the `with_0dt_deployment_ddl_check_interval` "system var" so that we can
173    /// toggle the flag with LaunchDarkly, but use it in boot before
174    /// LaunchDarkly is available.
175    async fn get_0dt_deployment_ddl_check_interval(
176        &mut self,
177    ) -> Result<Option<Duration>, CatalogError>;
178
179    /// Get the `enable_0dt_deployment_panic_after_timeout` config value of this
180    /// instance.
181    ///
182    /// This mirrors the `enable_0dt_deployment_panic_after_timeout` "system var"
183    /// so that we can toggle the flag with LaunchDarkly, but use it in boot
184    /// before LaunchDarkly is available.
185    async fn get_enable_0dt_deployment_panic_after_timeout(
186        &mut self,
187    ) -> Result<Option<bool>, CatalogError>;
188
189    /// Reports if the remote configuration was synchronized at least once.
190    async fn has_system_config_synced_once(&mut self) -> Result<bool, DurableCatalogError>;
191
192    /// Generate an unconsolidated [`Trace`] of catalog contents.
193    async fn trace_unconsolidated(&mut self) -> Result<Trace, CatalogError>;
194
195    /// Generate a consolidated [`Trace`] of catalog contents.
196    async fn trace_consolidated(&mut self) -> Result<Trace, CatalogError>;
197
198    /// Politely releases all external resources that can only be released in an async context.
199    async fn expire(self: Box<Self>);
200}
201
202/// A read only API for the durable catalog state.
203#[async_trait]
204pub trait ReadOnlyDurableCatalogState: Debug + Send + Sync {
205    /// Returns the epoch of the current durable catalog state. The epoch acts as
206    /// a fencing token to prevent split brain issues across two
207    /// [`DurableCatalogState`]s. When a new [`DurableCatalogState`] opens the
208    /// catalog, it will increment the epoch by one (or initialize it to some
209    /// value if there's no existing epoch) and store the value in memory. It's
210    /// guaranteed that no two [`DurableCatalogState`]s will return the same value
211    /// for their epoch.
212    ///
213    /// NB: We may remove this in later iterations of Pv2.
214    fn epoch(&self) -> Epoch;
215
216    /// Returns the metrics for this catalog state.
217    fn metrics(&self) -> &Metrics;
218
219    /// Politely releases all external resources that can only be released in an async context.
220    async fn expire(self: Box<Self>);
221
222    /// Returns true if the system bootstrapping process is complete, false otherwise.
223    fn is_bootstrap_complete(&self) -> bool;
224
225    /// Get all audit log events.
226    ///
227    /// Results are guaranteed to be sorted by ID.
228    ///
229    /// WARNING: This is meant for use in integration tests and has bad performance.
230    async fn get_audit_logs(&mut self) -> Result<Vec<VersionedEvent>, CatalogError>;
231
232    /// Get the next ID of `id_type`, without allocating it.
233    async fn get_next_id(&mut self, id_type: &str) -> Result<u64, CatalogError>;
234
235    /// Get the next user ID without allocating it.
236    async fn get_next_user_item_id(&mut self) -> Result<u64, CatalogError> {
237        self.get_next_id(USER_ITEM_ALLOC_KEY).await
238    }
239
240    /// Get the next system ID without allocating it.
241    async fn get_next_system_item_id(&mut self) -> Result<u64, CatalogError> {
242        self.get_next_id(SYSTEM_ITEM_ALLOC_KEY).await
243    }
244
245    /// Get the next system replica id without allocating it.
246    async fn get_next_system_replica_id(&mut self) -> Result<u64, CatalogError> {
247        self.get_next_id(SYSTEM_REPLICA_ID_ALLOC_KEY).await
248    }
249
250    /// Get the next user replica id without allocating it.
251    async fn get_next_user_replica_id(&mut self) -> Result<u64, CatalogError> {
252        self.get_next_id(USER_REPLICA_ID_ALLOC_KEY).await
253    }
254
255    /// Get the deployment generation of this instance.
256    async fn get_deployment_generation(&mut self) -> Result<u64, CatalogError>;
257
258    /// Get a snapshot of the catalog.
259    async fn snapshot(&mut self) -> Result<Snapshot, CatalogError>;
260
261    /// Listen and return all updates that are currently in the catalog.
262    ///
263    /// IMPORTANT: This excludes updates to storage usage.
264    ///
265    /// Returns an error if this instance has been fenced out.
266    async fn sync_to_current_updates(
267        &mut self,
268    ) -> Result<Vec<memory::objects::StateUpdate>, CatalogError>;
269
270    // TODO(jkosh44) The fact that the timestamp argument is an exclusive upper bound makes
271    // it difficult to use for readers. For now it's correct and easy to implement, but we should
272    // consider a better API.
273    /// Listen and return all updates in the catalog up to `target_upper`.
274    ///
275    /// IMPORTANT: This excludes updates to storage usage.
276    ///
277    /// Returns an error if this instance has been fenced out.
278    async fn sync_updates(
279        &mut self,
280        target_upper: Timestamp,
281    ) -> Result<Vec<memory::objects::StateUpdate>, CatalogError>;
282
283    /// Checks for unapplied catalog content before `target_upper` without consuming it.
284    ///
285    /// Returns an error if this instance has been fenced out.
286    async fn ensure_not_out_of_sync(&mut self, target_upper: Timestamp)
287    -> Result<(), CatalogError>;
288
289    /// Fetch the current upper of the catalog state.
290    async fn current_upper(&mut self) -> Timestamp;
291}
292
293/// A read-write API for the durable catalog state.
294#[async_trait]
295#[allow(mismatched_lifetime_syntaxes)]
296pub trait DurableCatalogState: ReadOnlyDurableCatalogState {
297    /// Returns true if the catalog is opened in read only mode, false otherwise.
298    fn is_read_only(&self) -> bool;
299
300    /// Returns true if the catalog is opened is savepoint mode, false otherwise.
301    fn is_savepoint(&self) -> bool;
302
303    /// Marks the bootstrap process as complete.
304    async fn mark_bootstrap_complete(&mut self);
305
306    /// Creates a transaction only if no durable catalog content is pending.
307    ///
308    /// Empty upper progress is accepted.
309    async fn transaction(&mut self) -> Result<Transaction, CatalogError>;
310
311    /// Creates a non-committable dry-run transaction from the given [`Snapshot`].
312    ///
313    /// The snapshot may include changes accumulated by earlier incremental dry runs.
314    fn transaction_from_snapshot(
315        &mut self,
316        snapshot: Snapshot,
317    ) -> Result<DryRunTransaction, CatalogError>;
318
319    /// Commits a durable catalog state transaction. The transaction will be committed at
320    /// `commit_ts`.
321    ///
322    /// Returns what the upper was directly after the transaction committed.
323    ///
324    /// Panics if `commit_ts` is not greater than or equal to the most recent upper seen by this
325    /// process.
326    async fn commit_transaction(
327        &mut self,
328        txn_batch: TransactionBatch,
329        commit_ts: Timestamp,
330    ) -> Result<Timestamp, CatalogError>;
331
332    /// Advances the catalog upper to at least `new_upper`.
333    ///
334    /// A durable attempt observes fencing and reports concurrent non-fencing content as
335    /// [`DurableCatalogError::CatalogOutOfSync`]. A no-op only validates a fence already observed
336    /// by this handle.
337    async fn advance_upper(&mut self, new_upper: Timestamp) -> Result<(), CatalogError>;
338
339    /// Allocates and returns `amount` IDs of `id_type`.
340    ///
341    /// Allocation rebases over empty upper progress. Fresh IDs do not require a catalog
342    /// staleness check.
343    async fn allocate_id(
344        &mut self,
345        id_type: &str,
346        amount: u64,
347        commit_ts: Timestamp,
348    ) -> Result<Vec<u64>, CatalogError>;
349
350    /// Allocates and returns `amount` many user [`CatalogItemId`] and [`GlobalId`].
351    ///
352    /// See [`Self::commit_transaction`] for details on `commit_ts`.
353    async fn allocate_user_ids(
354        &mut self,
355        amount: u64,
356        commit_ts: Timestamp,
357    ) -> Result<Vec<(CatalogItemId, GlobalId)>, CatalogError> {
358        let ids = self
359            .allocate_id(USER_ITEM_ALLOC_KEY, amount, commit_ts)
360            .await?;
361        let ids = ids
362            .iter()
363            .map(|id| (CatalogItemId::User(*id), GlobalId::User(*id)))
364            .collect();
365        Ok(ids)
366    }
367
368    /// Allocates and returns both a user [`CatalogItemId`] and [`GlobalId`].
369    ///
370    /// See [`Self::commit_transaction`] for details on `commit_ts`.
371    async fn allocate_user_id(
372        &mut self,
373        commit_ts: Timestamp,
374    ) -> Result<(CatalogItemId, GlobalId), CatalogError> {
375        let id = self.allocate_id(USER_ITEM_ALLOC_KEY, 1, commit_ts).await?;
376        let id = id.into_element();
377        Ok((CatalogItemId::User(id), GlobalId::User(id)))
378    }
379
380    /// Allocates and returns a user [`ClusterId`].
381    ///
382    /// See [`Self::commit_transaction`] for details on `commit_ts`.
383    async fn allocate_user_cluster_id(
384        &mut self,
385        commit_ts: Timestamp,
386    ) -> Result<ClusterId, CatalogError> {
387        let id = self
388            .allocate_id(USER_CLUSTER_ID_ALLOC_KEY, 1, commit_ts)
389            .await?
390            .into_element();
391        Ok(ClusterId::user(id).ok_or(SqlCatalogError::IdExhaustion)?)
392    }
393
394    /// Allocates and returns `amount` many user [`ReplicaId`]s.
395    ///
396    /// See [`Self::commit_transaction`] for details on `commit_ts`.
397    async fn allocate_user_replica_ids(
398        &mut self,
399        amount: u64,
400        commit_ts: Timestamp,
401    ) -> Result<Vec<ReplicaId>, CatalogError> {
402        let ids = self
403            .allocate_id(USER_REPLICA_ID_ALLOC_KEY, amount, commit_ts)
404            .await?;
405        let ids = ids.into_iter().map(ReplicaId::User).collect();
406        Ok(ids)
407    }
408
409    /// Allocates and returns `amount` many system [`ReplicaId`]s.
410    ///
411    /// See [`Self::commit_transaction`] for details on `commit_ts`.
412    async fn allocate_system_replica_ids(
413        &mut self,
414        amount: u64,
415        commit_ts: Timestamp,
416    ) -> Result<Vec<ReplicaId>, CatalogError> {
417        let ids = self
418            .allocate_id(SYSTEM_REPLICA_ID_ALLOC_KEY, amount, commit_ts)
419            .await?;
420        let ids = ids.into_iter().map(ReplicaId::System).collect();
421        Ok(ids)
422    }
423
424    fn shard_id(&self) -> ShardId;
425}
426
427/// Returns the schema of the `Row`s/`SourceData`s stored in the persist
428/// shard backing the catalog.
429pub fn persist_desc() -> RelationDesc {
430    RelationDesc::builder()
431        .with_column("data", SqlScalarType::Jsonb.nullable(false))
432        .finish()
433}
434
435/// A builder to help create an [`OpenableDurableCatalogState`] for tests.
436#[derive(Debug, Clone)]
437pub struct TestCatalogStateBuilder {
438    persist_client: PersistClient,
439    organization_id: Uuid,
440    version: semver::Version,
441    deploy_generation: Option<u64>,
442    metrics: Arc<Metrics>,
443}
444
445impl TestCatalogStateBuilder {
446    pub fn new(persist_client: PersistClient) -> Self {
447        Self {
448            persist_client,
449            organization_id: Uuid::new_v4(),
450            version: semver::Version::new(0, 0, 0),
451            deploy_generation: None,
452            metrics: Arc::new(Metrics::new(&MetricsRegistry::new())),
453        }
454    }
455
456    pub fn with_organization_id(mut self, organization_id: Uuid) -> Self {
457        self.organization_id = organization_id;
458        self
459    }
460
461    pub fn with_version(mut self, version: semver::Version) -> Self {
462        self.version = version;
463        self
464    }
465
466    pub fn with_deploy_generation(mut self, deploy_generation: u64) -> Self {
467        self.deploy_generation = Some(deploy_generation);
468        self
469    }
470
471    pub fn with_default_deploy_generation(self) -> Self {
472        self.with_deploy_generation(0)
473    }
474
475    pub fn with_metrics(mut self, metrics: Arc<Metrics>) -> Self {
476        self.metrics = metrics;
477        self
478    }
479
480    pub async fn build(self) -> Result<Box<dyn OpenableDurableCatalogState>, DurableCatalogError> {
481        persist_backed_catalog_state(
482            self.persist_client,
483            self.organization_id,
484            self.version,
485            self.deploy_generation,
486            self.metrics,
487        )
488        .await
489    }
490
491    pub async fn unwrap_build(self) -> Box<dyn OpenableDurableCatalogState> {
492        self.expect_build("failed to build").await
493    }
494
495    pub async fn expect_build(self, msg: &str) -> Box<dyn OpenableDurableCatalogState> {
496        self.build().await.expect(msg)
497    }
498}
499
500/// Creates an openable durable catalog state implemented using persist.
501///
502/// `deploy_generation` MUST be `Some` to initialize a new catalog.
503pub async fn persist_backed_catalog_state(
504    persist_client: PersistClient,
505    organization_id: Uuid,
506    version: semver::Version,
507    deploy_generation: Option<u64>,
508    metrics: Arc<Metrics>,
509) -> Result<Box<dyn OpenableDurableCatalogState>, DurableCatalogError> {
510    let state = UnopenedPersistCatalogState::new(
511        persist_client,
512        organization_id,
513        version,
514        deploy_generation,
515        metrics,
516    )
517    .await?;
518    Ok(Box::new(state))
519}
520
521pub fn test_bootstrap_args() -> BootstrapArgs {
522    BootstrapArgs {
523        default_cluster_replica_size: "scale=1,workers=1".into(),
524        default_cluster_replication_factor: 1,
525        bootstrap_role: None,
526        cluster_replica_size_map: ClusterReplicaSizeMap::for_tests(),
527        force_catalog_migration: false,
528    }
529}