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