Skip to main content

mz_adapter/coord/
ddl.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 module encapsulates all of the [`Coordinator`]'s logic for creating, dropping,
11//! and altering objects.
12
13use std::collections::{BTreeMap, BTreeSet};
14use std::pin::Pin;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18use fail::fail_point;
19use maplit::{btreemap, btreeset};
20use mz_adapter_types::compaction::SINCE_GRANULARITY;
21use mz_adapter_types::connection::ConnectionId;
22use mz_audit_log::VersionedEvent;
23use mz_catalog::SYSTEM_CONN_ID;
24use mz_catalog::memory::objects::{CatalogItem, DataSourceDesc, Sink};
25use mz_cluster_client::ReplicaId;
26use mz_controller::clusters::ReplicaLocation;
27use mz_controller_types::ClusterId;
28use mz_ore::instrument;
29use mz_ore::metrics::MetricsFutureExt;
30use mz_ore::now::to_datetime;
31use mz_ore::retry::Retry;
32use mz_ore::task;
33use mz_repr::adt::numeric::Numeric;
34use mz_repr::{CatalogItemId, GlobalId, Timestamp};
35use mz_sql::catalog::{CatalogClusterReplica, CatalogSchema};
36use mz_sql::names::ResolvedDatabaseSpecifier;
37use mz_sql::plan::ConnectionDetails;
38use mz_sql::session::metadata::SessionMetadata;
39use mz_sql::session::vars::{
40    self, DEFAULT_TIMESTAMP_INTERVAL, MAX_AWS_PRIVATELINK_CONNECTIONS, MAX_CLUSTERS,
41    MAX_CREDIT_CONSUMPTION_RATE, MAX_DATABASES, MAX_KAFKA_CONNECTIONS, MAX_MATERIALIZED_VIEWS,
42    MAX_MYSQL_CONNECTIONS, MAX_NETWORK_POLICIES, MAX_OBJECTS_PER_SCHEMA, MAX_POSTGRES_CONNECTIONS,
43    MAX_REPLICAS_PER_CLUSTER, MAX_ROLES, MAX_SCHEMAS_PER_DATABASE, MAX_SECRETS, MAX_SINKS,
44    MAX_SOURCES, MAX_SQL_SERVER_CONNECTIONS, MAX_TABLES, SystemVars, Var,
45};
46use mz_storage_client::controller::{CollectionDescription, DataSource, ExportDescription};
47use mz_storage_types::connections::inline::IntoInlineConnection;
48use mz_storage_types::read_policy::ReadPolicy;
49use mz_storage_types::sources::kafka::KAFKA_PROGRESS_DESC;
50use serde_json::json;
51use tracing::{Instrument, Level, event, info_span, warn};
52
53use crate::active_compute_sink::{ActiveComputeSink, ActiveComputeSinkRetireReason};
54use crate::catalog::{DropObjectInfo, Op, ReplicaCreateDropReason, TransactionResult};
55use crate::coord::Coordinator;
56use crate::coord::appends::{BuiltinTableAppendCompletion, BuiltinTableAppendNotify};
57use crate::coord::catalog_implications::parsed_state_updates::ParsedStateUpdate;
58use crate::session::{Session, Transaction, TransactionOps};
59use crate::telemetry::{EventDetails, SegmentClientExt};
60use crate::util::ResultExt;
61use crate::{AdapterError, ExecuteContext, catalog, flags};
62
63impl Coordinator {
64    /// Same as [`Self::catalog_transact_with_context`] but takes a [`Session`].
65    #[instrument(name = "coord::catalog_transact")]
66    pub(crate) async fn catalog_transact(
67        &mut self,
68        session: Option<&Session>,
69        ops: Vec<catalog::Op>,
70    ) -> Result<(), AdapterError> {
71        let start = Instant::now();
72        let result = self
73            .catalog_transact_with_context(session.map(|session| session.conn_id()), None, ops)
74            .await;
75        self.metrics
76            .catalog_transact_seconds
77            .with_label_values(&["catalog_transact"])
78            .observe(start.elapsed().as_secs_f64());
79        result
80    }
81
82    /// Same as [`Self::catalog_transact_with_context`] but takes a [`Session`]
83    /// and runs builtin table updates concurrently with any side effects (e.g.
84    /// creating collections).
85    // TODO(aljoscha): Remove this method once all call-sites have been migrated
86    // to the newer catalog_transact_with_context. The latter is what allows us
87    // to apply catalog implications that we derive from catalog chanages either
88    // when initially applying the ops to the catalog _or_ when following
89    // catalog changes from another process.
90    #[instrument(name = "coord::catalog_transact_with_side_effects")]
91    pub(crate) async fn catalog_transact_with_side_effects<F>(
92        &mut self,
93        mut ctx: Option<&mut ExecuteContext>,
94        ops: Vec<catalog::Op>,
95        side_effect: F,
96    ) -> Result<(), AdapterError>
97    where
98        F: for<'a> FnOnce(
99                &'a mut Coordinator,
100                Option<&'a mut ExecuteContext>,
101            ) -> Pin<Box<dyn Future<Output = ()> + 'a>>
102            + 'static,
103    {
104        let start = Instant::now();
105
106        let (table_updates, catalog_updates) = self
107            .catalog_transact_inner(ctx.as_ref().map(|ctx| ctx.session().conn_id()), ops)
108            .await?;
109
110        // We can't run this concurrently with the explicit side effects,
111        // because both want to borrow self mutably.
112        let apply_implications_res = self
113            .apply_catalog_implications(ctx.as_deref_mut(), catalog_updates)
114            .await;
115
116        // We would get into an inconsistent state if we updated the catalog but
117        // then failed to apply commands/updates to the controller. Easiest
118        // thing to do is panic and let restart/bootstrap handle it.
119        apply_implications_res.expect("cannot fail to apply catalog update implications");
120
121        // NOTE: `check_consistency` only runs with soft assertions enabled, so
122        // this phase reads about zero in production. We time it because a local
123        // rig debugging a transact stall commonly has them on, where the check is
124        // O(catalog size) and would otherwise appear as an unexplained remainder
125        // against the wrapper metric. The observation stays outside the macro,
126        // anything inside it compiles out exactly where soft assertions are off.
127        let consistency_start = Instant::now();
128
129        // Note: It's important that we keep the function call inside macro, this way we only run
130        // the consistency checks if soft assertions are enabled.
131        mz_ore::soft_assert_eq_no_log!(
132            self.check_consistency(),
133            Ok(()),
134            "coordinator inconsistency detected"
135        );
136
137        self.metrics
138            .catalog_transact_phase_seconds
139            .with_label_values(&["consistency_check"])
140            .observe(consistency_start.elapsed().as_secs_f64());
141
142        let side_effects_seconds = self
143            .metrics
144            .catalog_transact_phase_seconds
145            .with_label_values(&["side_effects"]);
146        // Distinct from `table_updates_wait` in `catalog_transact_with_context`.
147        // Here the group commit has already been running concurrently with
148        // `apply_catalog_implications` above, so this wrapper is first polled
149        // late and only records the residual wait.
150        let table_updates_wait = self
151            .metrics
152            .catalog_transact_phase_seconds
153            .with_label_values(&["table_updates_residual_wait"]);
154        let side_effects_fut = side_effect(self, ctx);
155
156        // Run our side effects concurrently with the table updates.
157        let ((), ()) = futures::future::join(
158            side_effects_fut
159                .wall_time()
160                .observe(side_effects_seconds)
161                .instrument(info_span!(
162                    "coord::catalog_transact_with_side_effects::side_effects_fut"
163                )),
164            table_updates
165                .wall_time()
166                .observe(table_updates_wait)
167                .instrument(info_span!(
168                    "coord::catalog_transact_with_side_effects::table_updates"
169                )),
170        )
171        .await;
172
173        self.metrics
174            .catalog_transact_seconds
175            .with_label_values(&["catalog_transact_with_side_effects"])
176            .observe(start.elapsed().as_secs_f64());
177
178        Ok(())
179    }
180
181    /// Same as [`Self::catalog_transact_inner`] but takes an execution context
182    /// or connection ID and runs builtin table updates concurrently with any
183    /// catalog implications that are generated as part of applying the given
184    /// `ops` (e.g. creating collections).
185    ///
186    /// This will use a connection ID if provided and otherwise fall back to
187    /// getting a connection ID from the execution context.
188    #[instrument(name = "coord::catalog_transact_with_context")]
189    pub(crate) async fn catalog_transact_with_context(
190        &mut self,
191        conn_id: Option<&ConnectionId>,
192        ctx: Option<&mut ExecuteContext>,
193        ops: Vec<catalog::Op>,
194    ) -> Result<(), AdapterError> {
195        let start = Instant::now();
196
197        let conn_id = conn_id.or_else(|| ctx.as_ref().map(|ctx| ctx.session().conn_id()));
198
199        let (table_updates, catalog_updates) = self.catalog_transact_inner(conn_id, ops).await?;
200
201        let table_updates_wait = self
202            .metrics
203            .catalog_transact_phase_seconds
204            .with_label_values(&["table_updates_wait"]);
205        let apply_catalog_implications_fut = self.apply_catalog_implications(ctx, catalog_updates);
206
207        // Apply catalog implications concurrently with the table updates.
208        let (combined_apply_res, ()) = futures::future::join(
209            apply_catalog_implications_fut.instrument(info_span!(
210                "coord::catalog_transact_with_context::side_effects_fut"
211            )),
212            table_updates
213                .wall_time()
214                .observe(table_updates_wait)
215                .instrument(info_span!(
216                    "coord::catalog_transact_with_context::table_updates"
217                )),
218        )
219        .await;
220
221        // We would get into an inconsistent state if we updated the catalog but
222        // then failed to apply implications. Easiest thing to do is panic and
223        // let restart/bootstrap handle it.
224        combined_apply_res.expect("cannot fail to apply catalog implications");
225
226        // See the note in `catalog_transact_with_side_effects` on why this is
227        // timed outside the macro and reads about zero in production.
228        let consistency_start = Instant::now();
229
230        // Note: It's important that we keep the function call inside macro, this way we only run
231        // the consistency checks if soft assertions are enabled.
232        mz_ore::soft_assert_eq_no_log!(
233            self.check_consistency(),
234            Ok(()),
235            "coordinator inconsistency detected"
236        );
237
238        self.metrics
239            .catalog_transact_phase_seconds
240            .with_label_values(&["consistency_check"])
241            .observe(consistency_start.elapsed().as_secs_f64());
242
243        self.metrics
244            .catalog_transact_seconds
245            .with_label_values(&["catalog_transact_with_context"])
246            .observe(start.elapsed().as_secs_f64());
247
248        Ok(())
249    }
250
251    /// Executes a Catalog transaction with handling if the provided [`Session`]
252    /// is in a SQL transaction that is executing DDL.
253    #[instrument(name = "coord::catalog_transact_with_ddl_transaction")]
254    pub(crate) async fn catalog_transact_with_ddl_transaction<F>(
255        &mut self,
256        ctx: &mut ExecuteContext,
257        ops: Vec<catalog::Op>,
258        side_effect: F,
259    ) -> Result<(), AdapterError>
260    where
261        F: for<'a> FnOnce(
262                &'a mut Coordinator,
263                Option<&'a mut ExecuteContext>,
264            ) -> Pin<Box<dyn Future<Output = ()> + 'a>>
265            + Send
266            + Sync
267            + 'static,
268    {
269        let start = Instant::now();
270
271        let Some(Transaction {
272            ops:
273                TransactionOps::DDL {
274                    ops: txn_ops,
275                    revision: txn_revision,
276                    state: txn_state,
277                    snapshot: txn_snapshot,
278                    side_effects: _,
279                },
280            ..
281        }) = ctx.session().transaction().inner()
282        else {
283            let result = self
284                .catalog_transact_with_side_effects(Some(ctx), ops, side_effect)
285                .await;
286            self.metrics
287                .catalog_transact_seconds
288                .with_label_values(&["catalog_transact_with_ddl_transaction"])
289                .observe(start.elapsed().as_secs_f64());
290            return result;
291        };
292
293        // Make sure our Catalog hasn't changed since openning the transaction.
294        if self.catalog().transient_revision() != *txn_revision {
295            self.metrics
296                .catalog_transact_seconds
297                .with_label_values(&["catalog_transact_with_ddl_transaction"])
298                .observe(start.elapsed().as_secs_f64());
299            return Err(AdapterError::DDLTransactionRace);
300        }
301
302        // The per-statement phases of a DDL transaction carry their own labels.
303        // The work differs from a real transaction's phases, and it is billed
304        // once per statement rather than once per transaction, so pooling the two
305        // populations under one label would blur both.
306        let phase_seconds = self.metrics.catalog_transact_phase_seconds.clone();
307
308        // Clone what we need from the session before taking &mut below.
309        let clone_start = Instant::now();
310        let txn_ops_clone = txn_ops.clone();
311        let txn_state_clone = txn_state.clone();
312        // NOTE: `txn_snapshot` is a deep clone of the durable `Snapshot`, which is
313        // O(catalog size) in allocations, once per statement. `txn_state` next to
314        // it is cheap, `CatalogState` holds its large collections in `imbl` maps.
315        let prev_snapshot = txn_snapshot.clone();
316        phase_seconds
317            .with_label_values(&["ddl_txn_snapshot_clone"])
318            .observe(clone_start.elapsed().as_secs_f64());
319
320        // Validate resource limits with all accumulated + new ops (cheap O(N) counting).
321        let prep_start = Instant::now();
322        let mut combined_ops = txn_ops_clone;
323        combined_ops.extend(ops.iter().cloned());
324        let conn_id = ctx.session().conn_id().clone();
325        let validate_res = self.validate_resource_limits(&combined_ops, &conn_id);
326        phase_seconds
327            .with_label_values(&["ddl_txn_prep"])
328            .observe(prep_start.elapsed().as_secs_f64());
329        validate_res?;
330
331        // Get oracle timestamp for audit log entries.
332        let oracle_write_ts = self
333            .get_local_write_ts()
334            .wall_time()
335            .observe(phase_seconds.with_label_values(&["ddl_txn_write_ts"]))
336            .await
337            .timestamp;
338
339        // Get ConnMeta for the session.
340        let conn = self.active_conns.get(ctx.session().conn_id());
341
342        // Incremental dry run: process only NEW ops against accumulated state.
343        // If we have a saved snapshot from a previous dry run, use it to
344        // initialize the transaction so it starts in sync with the accumulated
345        // state. Otherwise (first statement), the fresh durable transaction is
346        // already in sync with the real catalog state.
347        let (new_state, new_snapshot) = self
348            .catalog()
349            .transact_incremental_dry_run(
350                &txn_state_clone,
351                ops.clone(),
352                conn,
353                prev_snapshot,
354                oracle_write_ts,
355            )
356            .wall_time()
357            .observe(phase_seconds.with_label_values(&["ddl_txn_dry_run"]))
358            .await?;
359
360        // Accumulate ops for eventual COMMIT.
361        let result = ctx
362            .session_mut()
363            .transaction_mut()
364            .add_ops(TransactionOps::DDL {
365                ops: combined_ops,
366                state: new_state,
367                side_effects: vec![Box::new(side_effect)],
368                revision: self.catalog().transient_revision(),
369                snapshot: Some(new_snapshot),
370            });
371
372        self.metrics
373            .catalog_transact_seconds
374            .with_label_values(&["catalog_transact_with_ddl_transaction"])
375            .observe(start.elapsed().as_secs_f64());
376
377        result
378    }
379
380    /// Perform a catalog transaction. [`Coordinator::ship_dataflow`] must be
381    /// called after this function successfully returns on any built
382    /// [`DataflowDesc`](mz_compute_types::dataflows::DataflowDesc).
383    #[instrument(name = "coord::catalog_transact_inner")]
384    pub(crate) async fn catalog_transact_inner(
385        &mut self,
386        conn_id: Option<&ConnectionId>,
387        ops: Vec<catalog::Op>,
388    ) -> Result<(BuiltinTableAppendNotify, Vec<ParsedStateUpdate>), AdapterError> {
389        if self.controller.read_only() {
390            return Err(AdapterError::ReadOnly);
391        }
392
393        event!(Level::TRACE, ops = format!("{:?}", ops));
394
395        let phase_seconds = self.metrics.catalog_transact_phase_seconds.clone();
396        let phase_start = Instant::now();
397
398        let mut webhook_sources_to_restart = BTreeSet::new();
399        let mut clusters_to_drop = vec![];
400        let mut cluster_replicas_to_drop = vec![];
401        let mut clusters_to_create = vec![];
402        let mut cluster_replicas_to_create = vec![];
403        let mut update_metrics_config = false;
404        let mut update_tracing_config = false;
405        let mut update_controller_config = false;
406        let mut update_compute_config = false;
407        let mut update_storage_config = false;
408        let mut update_timestamp_oracle_config = false;
409        let mut update_metrics_retention = false;
410        let mut update_secrets_caching_config = false;
411        let mut update_cluster_scheduling_config = false;
412        let mut update_http_config = false;
413        let mut update_advance_timelines_interval = false;
414        let mut update_optimizer_e2e_latency_warning_threshold = false;
415
416        for op in &ops {
417            match op {
418                catalog::Op::DropObjects(drop_object_infos) => {
419                    for drop_object_info in drop_object_infos {
420                        match &drop_object_info {
421                            catalog::DropObjectInfo::Item(_) => {
422                                // Nothing to do, these will be handled by
423                                // applying the side effects that we return.
424                            }
425                            catalog::DropObjectInfo::Cluster(id) => {
426                                clusters_to_drop.push(*id);
427                            }
428                            catalog::DropObjectInfo::ClusterReplica((
429                                cluster_id,
430                                replica_id,
431                                _reason,
432                            )) => {
433                                // Drop the cluster replica itself.
434                                cluster_replicas_to_drop.push((*cluster_id, *replica_id));
435                            }
436                            _ => (),
437                        }
438                    }
439                }
440                catalog::Op::ResetSystemConfiguration { name }
441                | catalog::Op::UpdateSystemConfiguration { name, .. } => {
442                    update_metrics_config |= self
443                        .catalog
444                        .state()
445                        .system_config()
446                        .is_metrics_config_var(name);
447                    update_tracing_config |= vars::is_tracing_var(name);
448                    update_controller_config |= self
449                        .catalog
450                        .state()
451                        .system_config()
452                        .is_controller_config_var(name);
453                    update_compute_config |= self
454                        .catalog
455                        .state()
456                        .system_config()
457                        .is_compute_config_var(name);
458                    update_storage_config |= self
459                        .catalog
460                        .state()
461                        .system_config()
462                        .is_storage_config_var(name);
463                    update_timestamp_oracle_config |= vars::is_timestamp_oracle_config_var(name);
464                    update_metrics_retention |= name == vars::METRICS_RETENTION.name();
465                    update_secrets_caching_config |= vars::is_secrets_caching_var(name);
466                    update_cluster_scheduling_config |= vars::is_cluster_scheduling_var(name);
467                    update_http_config |= vars::is_http_config_var(name);
468                    update_advance_timelines_interval |= name == DEFAULT_TIMESTAMP_INTERVAL.name();
469                    update_optimizer_e2e_latency_warning_threshold |=
470                        name == vars::OPTIMIZER_E2E_LATENCY_WARNING_THRESHOLD.name();
471                }
472                catalog::Op::ResetAllSystemConfiguration => {
473                    // Assume they all need to be updated.
474                    // We could see if the config's have actually changed, but
475                    // this is simpler.
476                    update_tracing_config = true;
477                    update_controller_config = true;
478                    update_compute_config = true;
479                    update_storage_config = true;
480                    update_timestamp_oracle_config = true;
481                    update_metrics_retention = true;
482                    update_secrets_caching_config = true;
483                    update_cluster_scheduling_config = true;
484                    update_metrics_config = true;
485                    update_http_config = true;
486                    update_advance_timelines_interval = true;
487                    update_optimizer_e2e_latency_warning_threshold = true;
488                }
489                catalog::Op::RenameItem { id, .. } => {
490                    let item = self.catalog().get_entry(id);
491                    let is_webhook_source = item
492                        .source()
493                        .map(|s| matches!(s.data_source, DataSourceDesc::Webhook { .. }))
494                        .unwrap_or(false);
495                    if is_webhook_source {
496                        webhook_sources_to_restart.insert(*id);
497                    }
498                }
499                catalog::Op::RenameSchema {
500                    database_spec,
501                    schema_spec,
502                    ..
503                } => {
504                    let schema = self.catalog().get_schema(
505                        database_spec,
506                        schema_spec,
507                        conn_id.unwrap_or(&SYSTEM_CONN_ID),
508                    );
509                    let webhook_sources = schema.item_ids().filter(|id| {
510                        let item = self.catalog().get_entry(id);
511                        item.source()
512                            .map(|s| matches!(s.data_source, DataSourceDesc::Webhook { .. }))
513                            .unwrap_or(false)
514                    });
515                    webhook_sources_to_restart.extend(webhook_sources);
516                }
517                catalog::Op::CreateCluster { id, .. } => {
518                    clusters_to_create.push(*id);
519                }
520                catalog::Op::CreateClusterReplica {
521                    cluster_id,
522                    name,
523                    config,
524                    ..
525                } => {
526                    cluster_replicas_to_create.push((
527                        *cluster_id,
528                        name.clone(),
529                        config.location.num_processes(),
530                    ));
531                }
532                _ => (),
533            }
534        }
535
536        // Observe before propagating, so a transaction rejected on resource
537        // limits still accounts for the op scan it burned on the loop.
538        let validate_res = self.validate_resource_limits(&ops, conn_id.unwrap_or(&SYSTEM_CONN_ID));
539        phase_seconds
540            .with_label_values(&["prep"])
541            .observe(phase_start.elapsed().as_secs_f64());
542        validate_res?;
543
544        // This will produce timestamps that are guaranteed to increase on each
545        // call, and also never be behind the system clock. If the system clock
546        // hasn't advanced (or has gone backward), it will increment by 1. For
547        // the audit log, we need to balance "close (within 10s or so) to the
548        // system clock" and "always goes up". We've chosen here to prioritize
549        // always going up, and believe we will always be close to the system
550        // clock because it is well configured (chrony) and so may only rarely
551        // regress or pause for 10s.
552        let oracle_write_ts = self
553            .get_catalog_write_ts()
554            .wall_time()
555            .observe(phase_seconds.with_label_values(&["write_ts"]))
556            .await;
557
558        let Coordinator {
559            catalog,
560            active_conns,
561            controller,
562            cluster_replica_statuses,
563            ..
564        } = self;
565        let catalog = Arc::make_mut(catalog);
566        let conn = conn_id.map(|id| active_conns.get(id).expect("connection must exist"));
567
568        // Register the session as an ephemeral owner (its uuid <-> connection
569        // mapping) at its first temporary-item creation.
570        if let Some(conn) = conn {
571            let creates_temp_item = ops.iter().any(
572                |op| matches!(op, catalog::Op::CreateItem { item, .. } if item.is_temporary()),
573            );
574            if creates_temp_item && !catalog.state().has_temporary_namespace(conn.conn_id()) {
575                catalog.register_temporary_namespace(conn.conn_id(), conn.uuid());
576            }
577        }
578
579        // NOTE: This phase contains every durable `sync` and `commit` a catalog
580        // transaction performs, which is what makes `transact` minus those two
581        // histograms an estimate of the in-memory work. Two caveats. More than
582        // one sync happens per transaction, so the subtraction is only valid on
583        // rates of `_sum`, never on per-observation means. And durable
584        // `allocate_id` (user ID pool refills, storage usage batch IDs) observes
585        // into the same histograms from outside any catalog transaction, so the
586        // estimate is biased low while allocation is active.
587        let TransactionResult {
588            builtin_table_updates,
589            catalog_updates,
590            audit_events,
591        } = catalog
592            .transact(
593                Some(&mut controller.storage_collections),
594                oracle_write_ts,
595                conn,
596                ops,
597            )
598            .wall_time()
599            .observe(phase_seconds.with_label_values(&["transact"]))
600            .await?;
601
602        for (cluster_id, replica_id) in &cluster_replicas_to_drop {
603            cluster_replica_statuses.remove_cluster_replica_statuses(cluster_id, replica_id);
604        }
605        for cluster_id in &clusters_to_drop {
606            cluster_replica_statuses.remove_cluster_statuses(cluster_id);
607        }
608        for cluster_id in clusters_to_create {
609            cluster_replica_statuses.initialize_cluster_statuses(cluster_id);
610        }
611        let now = to_datetime((catalog.config().now)());
612        for (cluster_id, replica_name, num_processes) in cluster_replicas_to_create {
613            let replica_id = catalog
614                .resolve_replica_in_cluster(&cluster_id, &replica_name)
615                .expect("just created")
616                .replica_id();
617            cluster_replica_statuses.initialize_cluster_replica_statuses(
618                cluster_id,
619                replica_id,
620                num_processes,
621                now,
622            );
623        }
624
625        // Append our builtin table updates, then return the notify so we can run other tasks in
626        // parallel.
627        let stage_start = Instant::now();
628        let builtin_update_notify = self.builtin_table_update().execute(builtin_table_updates);
629        phase_seconds
630            .with_label_values(&["stage_builtin"])
631            .observe(stage_start.elapsed().as_secs_f64());
632
633        let finalize_start = Instant::now();
634
635        // No error returns are allowed after this point. Enforce this at compile time
636        // by using this odd structure so we don't accidentally add a stray `?`.
637        let _: () = async {
638            if !webhook_sources_to_restart.is_empty() {
639                self.restart_webhook_sources(webhook_sources_to_restart);
640            }
641
642            if update_metrics_config {
643                mz_metrics::update_dyncfg(&self.catalog().system_config().dyncfg_updates());
644            }
645            if update_controller_config {
646                self.update_controller_config();
647            }
648            if update_compute_config {
649                self.update_compute_config();
650            }
651            if update_storage_config {
652                self.update_storage_config();
653            }
654            if update_timestamp_oracle_config {
655                self.update_timestamp_oracle_config();
656            }
657            if update_metrics_retention {
658                self.update_metrics_retention();
659            }
660            if update_tracing_config {
661                self.update_tracing_config();
662            }
663            if update_secrets_caching_config {
664                self.update_secrets_caching_config();
665            }
666            if update_cluster_scheduling_config {
667                self.update_cluster_scheduling_config();
668            }
669            if update_http_config {
670                self.update_http_config();
671            }
672            if update_advance_timelines_interval {
673                let new_interval = self.catalog().system_config().default_timestamp_interval();
674                if new_interval != self.advance_timelines_interval.period() {
675                    self.advance_timelines_interval = tokio::time::interval(new_interval);
676                }
677            }
678            if update_optimizer_e2e_latency_warning_threshold {
679                let threshold = self
680                    .catalog()
681                    .system_config()
682                    .optimizer_e2e_latency_warning_threshold();
683                self.optimizer_metrics
684                    .set_e2e_optimization_time_log_threshold(threshold);
685            }
686        }
687        .instrument(info_span!("coord::catalog_transact_with::finalize"))
688        .await;
689
690        let conn = conn_id.and_then(|id| self.active_conns.get(id));
691        if let Some(segment_client) = &self.segment_client {
692            for VersionedEvent::V1(event) in audit_events {
693                let event_type = format!(
694                    "{} {}",
695                    event.object_type.as_title_case(),
696                    event.event_type.as_title_case()
697                );
698                segment_client.environment_track(
699                    &self.catalog().config().environment_id,
700                    event_type,
701                    json!({ "details": event.details.as_json() }),
702                    EventDetails {
703                        user_id: conn
704                            .and_then(|c| c.user().external_metadata.as_ref())
705                            .map(|m| m.user_id),
706                        application_name: conn.map(|c| c.application_name()),
707                        ..Default::default()
708                    },
709                );
710            }
711        }
712
713        phase_seconds
714            .with_label_values(&["finalize"])
715            .observe(finalize_start.elapsed().as_secs_f64());
716
717        Ok((builtin_update_notify, catalog_updates))
718    }
719
720    pub(crate) fn drop_replica(&mut self, cluster_id: ClusterId, replica_id: ReplicaId) {
721        self.drop_introspection_subscribes(replica_id);
722
723        self.controller
724            .drop_replica(cluster_id, replica_id)
725            .expect("dropping replica must not fail");
726    }
727
728    /// A convenience method for dropping sources.
729    pub(crate) fn drop_sources(&mut self, sources: Vec<(CatalogItemId, GlobalId)>) {
730        for (item_id, _gid) in &sources {
731            self.active_webhooks.remove(item_id);
732        }
733        let storage_metadata = self.catalog.state().storage_metadata();
734        let source_gids = sources.into_iter().map(|(_id, gid)| gid).collect();
735        self.controller
736            .storage
737            .drop_sources(storage_metadata, source_gids)
738            .unwrap_or_terminate("cannot fail to drop sources");
739    }
740
741    /// A convenience method for dropping tables.
742    pub(crate) async fn drop_tables(&mut self, tables: Vec<(CatalogItemId, GlobalId)>) {
743        for (item_id, _gid) in &tables {
744            self.active_webhooks.remove(item_id);
745        }
746
747        let table_gids: Vec<_> = tables.into_iter().map(|(_id, gid)| gid).collect();
748
749        // FIFO ordering places the forget after every staged append.
750        let forget_ids = self
751            .controller
752            .storage
753            .txns_table_ids(table_gids.clone())
754            .unwrap_or_terminate("cannot fail to look up txns-registered tables");
755        if !forget_ids.is_empty() {
756            self.forget_tables_via_committer(forget_ids).await;
757        }
758
759        let storage_metadata = self.catalog.state().storage_metadata();
760        self.controller
761            .storage
762            .drop_tables(storage_metadata, table_gids)
763            .unwrap_or_terminate("cannot fail to drop tables");
764    }
765
766    fn restart_webhook_sources(&mut self, sources: impl IntoIterator<Item = CatalogItemId>) {
767        for id in sources {
768            self.active_webhooks.remove(&id);
769        }
770    }
771
772    /// Like `drop_compute_sinks`, but for a single compute sink.
773    ///
774    /// Returns the controller's state for the compute sink if the identified
775    /// sink was known to the controller. It is the caller's responsibility to
776    /// retire the returned sink. Consider using `retire_compute_sinks` instead.
777    #[must_use]
778    pub async fn drop_compute_sink(
779        &mut self,
780        sink_id: GlobalId,
781    ) -> Option<(ActiveComputeSink, BuiltinTableAppendNotify)> {
782        self.drop_compute_sinks([sink_id]).await.remove(&sink_id)
783    }
784
785    /// Drops a batch of compute sinks.
786    ///
787    /// For each sink that exists, the coordinator and controller's state
788    /// associated with the sink is removed.
789    ///
790    /// Returns a map from sink id to the controller's state for the sink and a notify that
791    /// resolves once the sink's `mz_subscriptions` retraction is durable (see
792    /// `remove_active_compute_sink`). It is the caller's responsibility to await the notify
793    /// off the coordinator loop and then retire the returned sinks. Consider using
794    /// `retire_compute_sinks` instead.
795    #[must_use]
796    pub async fn drop_compute_sinks(
797        &mut self,
798        sink_ids: impl IntoIterator<Item = GlobalId>,
799    ) -> BTreeMap<GlobalId, (ActiveComputeSink, BuiltinTableAppendNotify)> {
800        let mut by_id = BTreeMap::new();
801        let mut by_cluster: BTreeMap<_, Vec<_>> = BTreeMap::new();
802        for sink_id in sink_ids {
803            let (sink, write_notify) = match self.remove_active_compute_sink(sink_id).await {
804                None => {
805                    // This can happen due to a race condition: an internal
806                    // subscribe may be cleaned up via its own message while
807                    // session disconnect cleanup is in progress. This is
808                    // benign.
809                    tracing::debug!(%sink_id, "drop_compute_sinks: sink already removed");
810                    continue;
811                }
812                Some(entry) => entry,
813            };
814
815            by_cluster
816                .entry(sink.cluster_id())
817                .or_default()
818                .push(sink_id);
819            by_id.insert(sink_id, (sink, write_notify));
820        }
821        for (cluster_id, ids) in by_cluster {
822            let compute = &mut self.controller.compute;
823            // A cluster could have been dropped, so verify it exists.
824            if compute.instance_exists(cluster_id) {
825                compute
826                    .drop_collections(cluster_id, ids)
827                    .unwrap_or_terminate("cannot fail to drop collections");
828            }
829        }
830        by_id
831    }
832
833    /// Retires a batch of sinks with disparate reasons for retirement.
834    ///
835    /// Each sink identified in `reasons` is dropped (see `drop_compute_sinks`),
836    /// then retired with its corresponding reason. Returns a notify that resolves
837    /// once all `mz_subscriptions` retractions are durable and the sinks are retired.
838    pub async fn retire_compute_sinks(
839        &mut self,
840        mut reasons: BTreeMap<GlobalId, ActiveComputeSinkRetireReason>,
841    ) -> BuiltinTableAppendCompletion {
842        let sink_ids = reasons.keys().cloned();
843        let to_retire: Vec<_> = self
844            .drop_compute_sinks(sink_ids)
845            .await
846            .into_iter()
847            .map(|(id, (sink, write_notify))| {
848                let reason = reasons
849                    .remove(&id)
850                    .expect("all returned IDs are in `reasons`");
851                (sink, write_notify, reason)
852            })
853            .collect();
854
855        // Retire off the coordinator loop. We wait for each `mz_subscriptions` retraction
856        // before telling the subscribing client that the sink is gone. The returned notify
857        // lets statements that caused the retirement also wait before sending their response.
858        // The wait must not happen on the coordinator loop, since that would block every
859        // other session on the group-commit oracle round trip.
860        let (done_tx, done_rx) = tokio::sync::oneshot::channel();
861        task::spawn(|| "retire_compute_sinks", async move {
862            for (sink, write_notify, reason) in to_retire {
863                write_notify.await;
864                sink.retire(reason);
865            }
866            let _ = done_tx.send(());
867        });
868        BuiltinTableAppendCompletion::new(Box::pin(async move {
869            let _ = done_rx.await;
870        }))
871    }
872
873    /// Drops all pending replicas for a set of clusters
874    /// that are undergoing reconfiguration.
875    pub async fn drop_reconfiguration_replicas(
876        &mut self,
877        cluster_ids: BTreeSet<ClusterId>,
878    ) -> Result<(), AdapterError> {
879        let pending_cluster_ops: Vec<Op> = cluster_ids
880            .iter()
881            .map(|c| {
882                self.catalog()
883                    .get_cluster(c.clone())
884                    .replicas()
885                    .filter_map(|r| match r.config.location {
886                        ReplicaLocation::Managed(ref l) if l.pending => {
887                            Some(DropObjectInfo::ClusterReplica((
888                                c.clone(),
889                                r.replica_id,
890                                ReplicaCreateDropReason::Manual,
891                            )))
892                        }
893                        _ => None,
894                    })
895                    .collect::<Vec<DropObjectInfo>>()
896            })
897            .filter_map(|pending_replica_drop_ops_by_cluster| {
898                match pending_replica_drop_ops_by_cluster.len() {
899                    0 => None,
900                    _ => Some(Op::DropObjects(pending_replica_drop_ops_by_cluster)),
901                }
902            })
903            .collect();
904        if !pending_cluster_ops.is_empty() {
905            self.catalog_transact(None, pending_cluster_ops).await?;
906        }
907        Ok(())
908    }
909
910    /// Cancels all active compute sinks for the identified connection.
911    #[mz_ore::instrument(level = "debug")]
912    pub(crate) async fn cancel_compute_sinks_for_conn(
913        &mut self,
914        conn_id: &ConnectionId,
915    ) -> BuiltinTableAppendCompletion {
916        self.retire_compute_sinks_for_conn(conn_id, ActiveComputeSinkRetireReason::Canceled)
917            .await
918    }
919
920    /// Cancels all active cluster reconfigurations sinks for the identified connection.
921    #[mz_ore::instrument(level = "debug")]
922    pub(crate) async fn cancel_cluster_reconfigurations_for_conn(
923        &mut self,
924        conn_id: &ConnectionId,
925    ) {
926        self.retire_cluster_reconfigurations_for_conn(conn_id).await
927    }
928
929    /// Retires all active compute sinks for the identified connection with the
930    /// specified reason.
931    #[mz_ore::instrument(level = "debug")]
932    pub(crate) async fn retire_compute_sinks_for_conn(
933        &mut self,
934        conn_id: &ConnectionId,
935        reason: ActiveComputeSinkRetireReason,
936    ) -> BuiltinTableAppendCompletion {
937        let drop_sinks = self
938            .active_conns
939            .get_mut(conn_id)
940            .expect("must exist for active session")
941            .drop_sinks
942            .iter()
943            .map(|sink_id| (*sink_id, reason.clone()))
944            .collect();
945        self.retire_compute_sinks(drop_sinks).await
946    }
947
948    /// Cleans pending cluster reconfiguraiotns for the identified connection
949    #[mz_ore::instrument(level = "debug")]
950    pub(crate) async fn retire_cluster_reconfigurations_for_conn(
951        &mut self,
952        conn_id: &ConnectionId,
953    ) {
954        let reconfiguring_clusters = self
955            .active_conns
956            .get(conn_id)
957            .expect("must exist for active session")
958            .pending_cluster_alters
959            .clone();
960        // try to drop reconfig replicas
961        self.drop_reconfiguration_replicas(reconfiguring_clusters)
962            .await
963            .unwrap_or_terminate("cannot fail to drop reconfiguration replicas");
964
965        self.active_conns
966            .get_mut(conn_id)
967            .expect("must exist for active session")
968            .pending_cluster_alters
969            .clear();
970    }
971
972    pub(crate) fn drop_storage_sinks(&mut self, sink_gids: Vec<GlobalId>) {
973        let storage_metadata = self.catalog.state().storage_metadata();
974        self.controller
975            .storage
976            .drop_sinks(storage_metadata, sink_gids)
977            .unwrap_or_terminate("cannot fail to drop sinks");
978    }
979
980    pub(crate) fn drop_compute_collections(&mut self, collections: Vec<(ClusterId, GlobalId)>) {
981        let mut by_cluster: BTreeMap<_, Vec<_>> = BTreeMap::new();
982        for (cluster_id, gid) in collections {
983            by_cluster.entry(cluster_id).or_default().push(gid);
984        }
985        for (cluster_id, gids) in by_cluster {
986            let compute = &mut self.controller.compute;
987            // A cluster could have been dropped, so verify it exists.
988            if compute.instance_exists(cluster_id) {
989                compute
990                    .drop_collections(cluster_id, gids)
991                    .unwrap_or_terminate("cannot fail to drop collections");
992            }
993        }
994    }
995
996    pub(crate) fn drop_vpc_endpoints_in_background(&self, vpc_endpoints: Vec<CatalogItemId>) {
997        // Match the create path (catalog_implications.rs) which gracefully
998        // logs an error when cloud_resource_controller is None, rather than
999        // panicking.
1000        let Some(cloud_resource_controller) = self.cloud_resource_controller.as_ref() else {
1001            warn!("dropping VPC endpoints without cloud_resource_controller; skipping cleanup");
1002            return;
1003        };
1004        let cloud_resource_controller = Arc::clone(cloud_resource_controller);
1005        // We don't want to block the coordinator on an external delete api
1006        // calls, so move the drop vpc_endpoint to a separate task. This does
1007        // mean that a failed drop won't bubble up to the user as an error
1008        // message. However, even if it did (and how the code previously
1009        // worked), mz has already dropped it from our catalog, and so we
1010        // wouldn't be able to retry anyway. Any orphaned vpc_endpoints will
1011        // eventually be cleaned during restart via coord bootstrap.
1012        task::spawn(
1013            || "drop_vpc_endpoints",
1014            async move {
1015                for vpc_endpoint in vpc_endpoints {
1016                    let _ = Retry::default()
1017                        .max_duration(Duration::from_secs(60))
1018                        .retry_async(|_state| async {
1019                            fail_point!("drop_vpc_endpoint", |r| {
1020                                Err(anyhow::anyhow!("Fail point error {:?}", r))
1021                            });
1022                            match cloud_resource_controller
1023                                .delete_vpc_endpoint(vpc_endpoint)
1024                                .await
1025                            {
1026                                Ok(_) => Ok(()),
1027                                Err(e) => {
1028                                    warn!("Dropping VPC Endpoints has encountered an error: {}", e);
1029                                    Err(e)
1030                                }
1031                            }
1032                        })
1033                        .await;
1034                }
1035            }
1036            .instrument(info_span!(
1037                "coord::catalog_transact_inner::drop_vpc_endpoints"
1038            )),
1039        );
1040    }
1041
1042    /// Removes all temporary items created by the specified connection, though
1043    /// not the temporary schema itself.
1044    pub(crate) async fn drop_temp_items(&mut self, conn_id: &ConnectionId) {
1045        let temp_items = self.catalog().state().get_temp_items(conn_id).collect();
1046        let all_items = self.catalog().object_dependents(&temp_items, conn_id);
1047
1048        if all_items.is_empty() {
1049            return;
1050        }
1051        let op = Op::DropObjects(
1052            all_items
1053                .into_iter()
1054                .map(DropObjectInfo::manual_drop_from_object_id)
1055                .collect(),
1056        );
1057
1058        self.catalog_transact_with_context(Some(conn_id), None, vec![op])
1059            .await
1060            .expect("unable to drop temporary items for conn_id");
1061    }
1062
1063    fn update_cluster_scheduling_config(&self) {
1064        let config = flags::orchestrator_scheduling_config(self.catalog.system_config());
1065        self.controller
1066            .update_orchestrator_scheduling_config(config);
1067    }
1068
1069    fn update_secrets_caching_config(&self) {
1070        let config = flags::caching_config(self.catalog.system_config());
1071        self.caching_secrets_reader.set_policy(config);
1072    }
1073
1074    fn update_tracing_config(&self) {
1075        let tracing = flags::tracing_config(self.catalog().system_config());
1076        tracing.apply(&self.tracing_handle);
1077    }
1078
1079    fn update_compute_config(&mut self) {
1080        let config_params = flags::compute_config(self.catalog().system_config());
1081        self.controller.compute.update_configuration(config_params);
1082    }
1083
1084    fn update_storage_config(&mut self) {
1085        let config_params = flags::storage_config(self.catalog().system_config());
1086        self.controller.storage.update_parameters(config_params);
1087    }
1088
1089    fn update_timestamp_oracle_config(&self) {
1090        let config_params = flags::timestamp_oracle_config(self.catalog().system_config());
1091        if let Some(config) = self.timestamp_oracle_config.as_ref() {
1092            config.apply_parameters(config_params)
1093        }
1094    }
1095
1096    fn update_metrics_retention(&self) {
1097        let duration = self.catalog().system_config().metrics_retention();
1098        let policy = ReadPolicy::lag_writes_by(
1099            Timestamp::new(u64::try_from(duration.as_millis()).unwrap_or_else(|_e| {
1100                tracing::error!("Absurd metrics retention duration: {duration:?}.");
1101                u64::MAX
1102            })),
1103            SINCE_GRANULARITY,
1104        );
1105        let storage_policies = self
1106            .catalog()
1107            .entries()
1108            .filter(|entry| {
1109                entry.item().is_retained_metrics_object()
1110                    && entry.item().is_compute_object_on_cluster().is_none()
1111            })
1112            .map(|entry| (entry.id(), policy.clone()))
1113            .collect::<Vec<_>>();
1114        let compute_policies = self
1115            .catalog()
1116            .entries()
1117            .filter_map(|entry| {
1118                if let (true, Some(cluster_id)) = (
1119                    entry.item().is_retained_metrics_object(),
1120                    entry.item().is_compute_object_on_cluster(),
1121                ) {
1122                    Some((cluster_id, entry.id(), policy.clone()))
1123                } else {
1124                    None
1125                }
1126            })
1127            .collect::<Vec<_>>();
1128        self.update_storage_read_policies(storage_policies);
1129        self.update_compute_read_policies(compute_policies);
1130    }
1131
1132    fn update_controller_config(&mut self) {
1133        let sys_config = self.catalog().system_config();
1134        self.controller
1135            .update_configuration(sys_config.dyncfg_updates());
1136    }
1137
1138    fn update_http_config(&mut self) {
1139        let webhook_request_limit = self
1140            .catalog()
1141            .system_config()
1142            .webhook_concurrent_request_limit();
1143        self.webhook_concurrency_limit
1144            .set_limit(webhook_request_limit);
1145    }
1146
1147    pub(crate) async fn create_storage_export(
1148        &mut self,
1149        id: GlobalId,
1150        sink: &Sink,
1151    ) -> Result<(), AdapterError> {
1152        // Validate `sink.from` is in fact a storage collection
1153        self.controller.storage.check_exists(sink.from)?;
1154
1155        // The AsOf is used to determine at what time to snapshot reading from
1156        // the persist collection.  This is primarily relevant when we do _not_
1157        // want to include the snapshot in the sink.
1158        //
1159        // We choose the smallest as_of that is legal, according to the sinked
1160        // collection's since.
1161        let id_bundle = crate::CollectionIdBundle {
1162            storage_ids: btreeset! {sink.from},
1163            compute_ids: btreemap! {},
1164        };
1165
1166        // We're putting in place read holds, such that create_exports, below,
1167        // which calls update_read_capabilities, can successfully do so.
1168        // Otherwise, the since of dependencies might move along concurrently,
1169        // pulling the rug from under us!
1170        //
1171        // TODO: Maybe in the future, pass those holds on to storage, to hold on
1172        // to them and downgrade when possible?
1173        let read_holds = self.acquire_read_holds(&id_bundle);
1174        let as_of = read_holds.least_valid_read();
1175
1176        let storage_sink_from_entry = self.catalog().get_entry_by_global_id(&sink.from);
1177        let storage_sink_desc = mz_storage_types::sinks::StorageSinkDesc {
1178            from: sink.from,
1179            from_desc: storage_sink_from_entry
1180                .relation_desc()
1181                .expect("sinks can only be built on items with descs")
1182                .into_owned(),
1183            connection: sink
1184                .connection
1185                .clone()
1186                .into_inline_connection(self.catalog().state()),
1187            envelope: sink.envelope,
1188            as_of,
1189            with_snapshot: sink.with_snapshot,
1190            version: sink.version,
1191            from_storage_metadata: (),
1192            to_storage_metadata: (),
1193            commit_interval: sink.commit_interval,
1194        };
1195
1196        let collection_desc = CollectionDescription {
1197            // TODO(sinks): make generic once we have more than one sink type.
1198            desc: KAFKA_PROGRESS_DESC.clone(),
1199            data_source: DataSource::Sink {
1200                desc: ExportDescription {
1201                    sink: storage_sink_desc,
1202                    instance_id: sink.cluster_id,
1203                },
1204            },
1205            since: None,
1206            timeline: None,
1207            primary: None,
1208        };
1209        let collections = vec![(id, collection_desc)];
1210
1211        // Create the collections.
1212        let storage_metadata = self.catalog.state().storage_metadata();
1213        let res = self
1214            .controller
1215            .storage
1216            .create_collections(storage_metadata, None, collections)
1217            .await;
1218
1219        // Drop read holds after the export has been created, at which point
1220        // storage will have put in its own read holds.
1221        drop(read_holds);
1222
1223        Ok(res?)
1224    }
1225
1226    /// Validate all resource limits in a catalog transaction and return an error if that limit is
1227    /// exceeded.
1228    fn validate_resource_limits(
1229        &self,
1230        ops: &Vec<catalog::Op>,
1231        conn_id: &ConnectionId,
1232    ) -> Result<(), AdapterError> {
1233        let mut new_kafka_connections = 0;
1234        let mut new_postgres_connections = 0;
1235        let mut new_mysql_connections = 0;
1236        let mut new_sql_server_connections = 0;
1237        let mut new_aws_privatelink_connections = 0;
1238        let mut new_tables = 0;
1239        let mut new_sources = 0;
1240        let mut new_sinks = 0;
1241        let mut new_materialized_views = 0;
1242        let mut new_clusters = 0;
1243        let mut new_replicas_per_cluster = BTreeMap::new();
1244        let mut new_credit_consumption_rate = Numeric::zero();
1245        let mut new_databases = 0;
1246        let mut new_schemas_per_database = BTreeMap::new();
1247        let mut new_objects_per_schema = BTreeMap::new();
1248        let mut new_secrets = 0;
1249        let mut new_roles = 0;
1250        let mut new_network_policies = 0;
1251        for op in ops {
1252            match op {
1253                Op::CreateDatabase { .. } => {
1254                    new_databases += 1;
1255                }
1256                Op::CreateSchema { database_id, .. } => {
1257                    if let ResolvedDatabaseSpecifier::Id(database_id) = database_id {
1258                        *new_schemas_per_database.entry(database_id).or_insert(0) += 1;
1259                    }
1260                }
1261                Op::CreateRole { .. } => {
1262                    new_roles += 1;
1263                }
1264                Op::CreateNetworkPolicy { .. } => {
1265                    new_network_policies += 1;
1266                }
1267                Op::CreateCluster { .. } => {
1268                    // TODO(benesch): having deprecated linked clusters, remove
1269                    // the `max_sources` and `max_sinks` limit, and set a higher
1270                    // max cluster limit?
1271                    new_clusters += 1;
1272                }
1273                Op::CreateClusterReplica {
1274                    cluster_id, config, ..
1275                } => {
1276                    if cluster_id.is_user() {
1277                        *new_replicas_per_cluster.entry(*cluster_id).or_insert(0) += 1;
1278                        if let ReplicaLocation::Managed(location) = &config.location {
1279                            let replica_allocation = self
1280                                .catalog()
1281                                .cluster_replica_sizes()
1282                                .0
1283                                .get(location.size_for_billing())
1284                                .expect(
1285                                    "location size is validated against the cluster replica sizes",
1286                                );
1287                            new_credit_consumption_rate += replica_allocation.credits_per_hour
1288                        }
1289                    }
1290                }
1291                Op::CreateItem { name, item, .. } => {
1292                    *new_objects_per_schema
1293                        .entry((
1294                            name.qualifiers.database_spec.clone(),
1295                            name.qualifiers.schema_spec.clone(),
1296                        ))
1297                        .or_insert(0) += 1;
1298                    match item {
1299                        CatalogItem::Connection(connection) => match connection.details {
1300                            ConnectionDetails::Kafka(_) => new_kafka_connections += 1,
1301                            ConnectionDetails::Postgres(_) => new_postgres_connections += 1,
1302                            ConnectionDetails::MySql(_) => new_mysql_connections += 1,
1303                            ConnectionDetails::SqlServer(_) => new_sql_server_connections += 1,
1304                            ConnectionDetails::AwsPrivatelink(_) => {
1305                                new_aws_privatelink_connections += 1
1306                            }
1307                            ConnectionDetails::Csr(_)
1308                            | ConnectionDetails::GlueSchemaRegistry(_)
1309                            | ConnectionDetails::Ssh { .. }
1310                            | ConnectionDetails::Aws(_)
1311                            | ConnectionDetails::Gcp(_)
1312                            | ConnectionDetails::IcebergCatalog(_) => {}
1313                        },
1314                        CatalogItem::Table(_) => {
1315                            new_tables += 1;
1316                        }
1317                        CatalogItem::Source(source) => {
1318                            new_sources += source.user_controllable_persist_shard_count()
1319                        }
1320                        CatalogItem::Sink(_) => new_sinks += 1,
1321                        CatalogItem::MaterializedView(_) => {
1322                            new_materialized_views += 1;
1323                        }
1324                        CatalogItem::Secret(_) => {
1325                            new_secrets += 1;
1326                        }
1327                        CatalogItem::Log(_)
1328                        | CatalogItem::View(_)
1329                        | CatalogItem::Index(_)
1330                        | CatalogItem::Type(_)
1331                        | CatalogItem::Func(_)
1332                        | CatalogItem::MetricSink(_) => {}
1333                    }
1334                }
1335                Op::DropObjects(drop_object_infos) => {
1336                    for drop_object_info in drop_object_infos {
1337                        match drop_object_info {
1338                            DropObjectInfo::Cluster(_) => {
1339                                new_clusters -= 1;
1340                            }
1341                            DropObjectInfo::ClusterReplica((cluster_id, replica_id, _reason)) => {
1342                                if cluster_id.is_user() {
1343                                    *new_replicas_per_cluster.entry(*cluster_id).or_insert(0) -= 1;
1344                                    let cluster = self
1345                                        .catalog()
1346                                        .get_cluster_replica(*cluster_id, *replica_id);
1347                                    if let ReplicaLocation::Managed(location) =
1348                                        &cluster.config.location
1349                                    {
1350                                        let replica_allocation = self
1351                                            .catalog()
1352                                            .cluster_replica_sizes()
1353                                            .0
1354                                            .get(location.size_for_billing())
1355                                            .expect(
1356                                                "location size is validated against the cluster replica sizes",
1357                                            );
1358                                        new_credit_consumption_rate -=
1359                                            replica_allocation.credits_per_hour
1360                                    }
1361                                }
1362                            }
1363                            DropObjectInfo::Database(_) => {
1364                                new_databases -= 1;
1365                            }
1366                            DropObjectInfo::Schema((database_spec, _)) => {
1367                                if let ResolvedDatabaseSpecifier::Id(database_id) = database_spec {
1368                                    *new_schemas_per_database.entry(database_id).or_insert(0) -= 1;
1369                                }
1370                            }
1371                            DropObjectInfo::Role(_) => {
1372                                new_roles -= 1;
1373                            }
1374                            DropObjectInfo::NetworkPolicy(_) => {
1375                                new_network_policies -= 1;
1376                            }
1377                            DropObjectInfo::Item(id) => {
1378                                let entry = self.catalog().get_entry(id);
1379                                *new_objects_per_schema
1380                                    .entry((
1381                                        entry.name().qualifiers.database_spec.clone(),
1382                                        entry.name().qualifiers.schema_spec.clone(),
1383                                    ))
1384                                    .or_insert(0) -= 1;
1385                                match entry.item() {
1386                                    CatalogItem::Connection(connection) => match connection.details
1387                                    {
1388                                        ConnectionDetails::AwsPrivatelink(_) => {
1389                                            new_aws_privatelink_connections -= 1;
1390                                        }
1391                                        _ => (),
1392                                    },
1393                                    CatalogItem::Table(_) => {
1394                                        new_tables -= 1;
1395                                    }
1396                                    CatalogItem::Source(source) => {
1397                                        new_sources -=
1398                                            source.user_controllable_persist_shard_count()
1399                                    }
1400                                    CatalogItem::Sink(_) => new_sinks -= 1,
1401                                    CatalogItem::MaterializedView(_) => {
1402                                        new_materialized_views -= 1;
1403                                    }
1404                                    CatalogItem::Secret(_) => {
1405                                        new_secrets -= 1;
1406                                    }
1407                                    CatalogItem::Log(_)
1408                                    | CatalogItem::View(_)
1409                                    | CatalogItem::Index(_)
1410                                    | CatalogItem::Type(_)
1411                                    | CatalogItem::Func(_)
1412                                    | CatalogItem::MetricSink(_) => {}
1413                                }
1414                            }
1415                        }
1416                    }
1417                }
1418                Op::UpdateItem {
1419                    name: _,
1420                    id,
1421                    to_item,
1422                } => match to_item {
1423                    CatalogItem::Source(source) => {
1424                        let current_source = self
1425                            .catalog()
1426                            .get_entry(id)
1427                            .source()
1428                            .expect("source update is for source item");
1429
1430                        new_sources += source.user_controllable_persist_shard_count()
1431                            - current_source.user_controllable_persist_shard_count();
1432                    }
1433                    CatalogItem::Connection(_)
1434                    | CatalogItem::Table(_)
1435                    | CatalogItem::Sink(_)
1436                    | CatalogItem::MaterializedView(_)
1437                    | CatalogItem::Secret(_)
1438                    | CatalogItem::Log(_)
1439                    | CatalogItem::View(_)
1440                    | CatalogItem::Index(_)
1441                    | CatalogItem::Type(_)
1442                    | CatalogItem::Func(_)
1443                    | CatalogItem::MetricSink(_) => {}
1444                },
1445                Op::AlterRole { .. }
1446                | Op::AlterRetainHistory { .. }
1447                | Op::AlterSourceTimestampInterval { .. }
1448                | Op::AlterNetworkPolicy { .. }
1449                | Op::AlterAddColumn { .. }
1450                | Op::AlterMaterializedViewApplyReplacement { .. }
1451                | Op::UpdatePrivilege { .. }
1452                | Op::UpdateDefaultPrivilege { .. }
1453                | Op::GrantRole { .. }
1454                | Op::RenameCluster { .. }
1455                | Op::RenameClusterReplica { .. }
1456                | Op::RenameItem { .. }
1457                | Op::RenameSchema { .. }
1458                | Op::UpdateOwner { .. }
1459                | Op::RevokeRole { .. }
1460                | Op::UpdateClusterConfig { .. }
1461                | Op::UpdateClusterReplicaConfig { .. }
1462                | Op::UpdateSourceReferences { .. }
1463                | Op::UpdateSystemConfiguration { .. }
1464                | Op::ResetSystemConfiguration { .. }
1465                | Op::ResetAllSystemConfiguration { .. }
1466                | Op::UpdateScopedSystemParameters { .. }
1467                | Op::Comment { .. }
1468                | Op::CheckClusterState { .. }
1469                | Op::InjectAuditEvents { .. } => {}
1470            }
1471        }
1472
1473        let mut current_aws_privatelink_connections = 0;
1474        let mut current_postgres_connections = 0;
1475        let mut current_mysql_connections = 0;
1476        let mut current_sql_server_connections = 0;
1477        let mut current_kafka_connections = 0;
1478        for c in self.catalog().user_connections() {
1479            let connection = c
1480                .connection()
1481                .expect("`user_connections()` only returns connection objects");
1482
1483            match connection.details {
1484                ConnectionDetails::AwsPrivatelink(_) => current_aws_privatelink_connections += 1,
1485                ConnectionDetails::Postgres(_) => current_postgres_connections += 1,
1486                ConnectionDetails::MySql(_) => current_mysql_connections += 1,
1487                ConnectionDetails::SqlServer(_) => current_sql_server_connections += 1,
1488                ConnectionDetails::Kafka(_) => current_kafka_connections += 1,
1489                ConnectionDetails::Csr(_)
1490                | ConnectionDetails::GlueSchemaRegistry(_)
1491                | ConnectionDetails::Ssh { .. }
1492                | ConnectionDetails::Aws(_)
1493                | ConnectionDetails::Gcp(_)
1494                | ConnectionDetails::IcebergCatalog(_) => {}
1495            }
1496        }
1497        self.validate_resource_limit(
1498            current_kafka_connections,
1499            new_kafka_connections,
1500            SystemVars::max_kafka_connections,
1501            "Kafka Connection",
1502            MAX_KAFKA_CONNECTIONS.name(),
1503        )?;
1504        self.validate_resource_limit(
1505            current_postgres_connections,
1506            new_postgres_connections,
1507            SystemVars::max_postgres_connections,
1508            "PostgreSQL Connection",
1509            MAX_POSTGRES_CONNECTIONS.name(),
1510        )?;
1511        self.validate_resource_limit(
1512            current_mysql_connections,
1513            new_mysql_connections,
1514            SystemVars::max_mysql_connections,
1515            "MySQL Connection",
1516            MAX_MYSQL_CONNECTIONS.name(),
1517        )?;
1518        self.validate_resource_limit(
1519            current_sql_server_connections,
1520            new_sql_server_connections,
1521            SystemVars::max_sql_server_connections,
1522            "SQL Server Connection",
1523            MAX_SQL_SERVER_CONNECTIONS.name(),
1524        )?;
1525        self.validate_resource_limit(
1526            current_aws_privatelink_connections,
1527            new_aws_privatelink_connections,
1528            SystemVars::max_aws_privatelink_connections,
1529            "AWS PrivateLink Connection",
1530            MAX_AWS_PRIVATELINK_CONNECTIONS.name(),
1531        )?;
1532        self.validate_resource_limit(
1533            self.catalog().user_tables().count(),
1534            new_tables,
1535            SystemVars::max_tables,
1536            "table",
1537            MAX_TABLES.name(),
1538        )?;
1539
1540        let current_sources: usize = self
1541            .catalog()
1542            .user_sources()
1543            .filter_map(|source| source.source())
1544            .map(|source| source.user_controllable_persist_shard_count())
1545            .sum::<i64>()
1546            .try_into()
1547            .expect("non-negative sum of sources");
1548
1549        self.validate_resource_limit(
1550            current_sources,
1551            new_sources,
1552            SystemVars::max_sources,
1553            "source",
1554            MAX_SOURCES.name(),
1555        )?;
1556        self.validate_resource_limit(
1557            self.catalog().user_sinks().count(),
1558            new_sinks,
1559            SystemVars::max_sinks,
1560            "sink",
1561            MAX_SINKS.name(),
1562        )?;
1563        self.validate_resource_limit(
1564            self.catalog().user_materialized_views().count(),
1565            new_materialized_views,
1566            SystemVars::max_materialized_views,
1567            "materialized view",
1568            MAX_MATERIALIZED_VIEWS.name(),
1569        )?;
1570        self.validate_resource_limit(
1571            // Linked compute clusters don't count against the limit, since
1572            // we have a separate sources and sinks limit.
1573            //
1574            // TODO(benesch): remove the `max_sources` and `max_sinks` limit,
1575            // and set a higher max cluster limit?
1576            self.catalog().user_clusters().count(),
1577            new_clusters,
1578            SystemVars::max_clusters,
1579            "cluster",
1580            MAX_CLUSTERS.name(),
1581        )?;
1582        for (cluster_id, new_replicas) in new_replicas_per_cluster {
1583            // It's possible that the cluster hasn't been created yet.
1584            let current_amount = self
1585                .catalog()
1586                .try_get_cluster(cluster_id)
1587                .map(|instance| instance.user_replicas().count())
1588                .unwrap_or(0);
1589            self.validate_resource_limit(
1590                current_amount,
1591                new_replicas,
1592                SystemVars::max_replicas_per_cluster,
1593                "cluster replica",
1594                MAX_REPLICAS_PER_CLUSTER.name(),
1595            )?;
1596        }
1597        self.validate_resource_limit_numeric(
1598            self.current_credit_consumption_rate(None),
1599            new_credit_consumption_rate,
1600            |system_vars| {
1601                self.license_key
1602                    .max_credit_consumption_rate()
1603                    .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
1604            },
1605            "cluster replica",
1606            MAX_CREDIT_CONSUMPTION_RATE.name(),
1607        )?;
1608        self.validate_resource_limit(
1609            self.catalog().databases().count(),
1610            new_databases,
1611            SystemVars::max_databases,
1612            "database",
1613            MAX_DATABASES.name(),
1614        )?;
1615        for (database_id, new_schemas) in new_schemas_per_database {
1616            self.validate_resource_limit(
1617                self.catalog().get_database(database_id).schemas_by_id.len(),
1618                new_schemas,
1619                SystemVars::max_schemas_per_database,
1620                "schema",
1621                MAX_SCHEMAS_PER_DATABASE.name(),
1622            )?;
1623        }
1624        for ((database_spec, schema_spec), new_objects) in new_objects_per_schema {
1625            // For temporary schemas that don't exist yet (lazy creation),
1626            // treat them as having 0 items.
1627            let current_items = self
1628                .catalog()
1629                .try_get_schema(&database_spec, &schema_spec, conn_id)
1630                .map(|schema| schema.items.len())
1631                .unwrap_or(0);
1632            self.validate_resource_limit(
1633                current_items,
1634                new_objects,
1635                SystemVars::max_objects_per_schema,
1636                "object",
1637                MAX_OBJECTS_PER_SCHEMA.name(),
1638            )?;
1639        }
1640        self.validate_resource_limit(
1641            self.catalog().user_secrets().count(),
1642            new_secrets,
1643            SystemVars::max_secrets,
1644            "secret",
1645            MAX_SECRETS.name(),
1646        )?;
1647        self.validate_resource_limit(
1648            self.catalog().user_roles().count(),
1649            new_roles,
1650            SystemVars::max_roles,
1651            "role",
1652            MAX_ROLES.name(),
1653        )?;
1654        self.validate_resource_limit(
1655            self.catalog().user_network_policies().count(),
1656            new_network_policies,
1657            SystemVars::max_network_policies,
1658            "network_policy",
1659            MAX_NETWORK_POLICIES.name(),
1660        )?;
1661        Ok(())
1662    }
1663
1664    /// Validate a specific type of resource limit and return an error if that limit is exceeded.
1665    pub(crate) fn validate_resource_limit<F>(
1666        &self,
1667        current_amount: usize,
1668        new_instances: i64,
1669        resource_limit: F,
1670        resource_type: &str,
1671        limit_name: &str,
1672    ) -> Result<(), AdapterError>
1673    where
1674        F: Fn(&SystemVars) -> u32,
1675    {
1676        if new_instances <= 0 {
1677            return Ok(());
1678        }
1679
1680        let limit: i64 = resource_limit(self.catalog().system_config()).into();
1681        let current_amount: Option<i64> = current_amount.try_into().ok();
1682        let desired =
1683            current_amount.and_then(|current_amount| current_amount.checked_add(new_instances));
1684
1685        let exceeds_limit = if let Some(desired) = desired {
1686            desired > limit
1687        } else {
1688            true
1689        };
1690
1691        let desired = desired
1692            .map(|desired| desired.to_string())
1693            .unwrap_or_else(|| format!("more than {}", i64::MAX));
1694        let current = current_amount
1695            .map(|current| current.to_string())
1696            .unwrap_or_else(|| format!("more than {}", i64::MAX));
1697        if exceeds_limit {
1698            Err(AdapterError::ResourceExhaustion {
1699                resource_type: resource_type.to_string(),
1700                limit_name: limit_name.to_string(),
1701                desired,
1702                limit: limit.to_string(),
1703                current,
1704            })
1705        } else {
1706            Ok(())
1707        }
1708    }
1709
1710    /// Validate a specific type of float resource limit and return an error if that limit is exceeded.
1711    ///
1712    /// This is very similar to [`Self::validate_resource_limit`] but for numerics.
1713    pub(crate) fn validate_resource_limit_numeric<F>(
1714        &self,
1715        current_amount: Numeric,
1716        new_amount: Numeric,
1717        resource_limit: F,
1718        resource_type: &str,
1719        limit_name: &str,
1720    ) -> Result<(), AdapterError>
1721    where
1722        F: Fn(&SystemVars) -> Numeric,
1723    {
1724        if new_amount <= Numeric::zero() {
1725            return Ok(());
1726        }
1727
1728        let limit = resource_limit(self.catalog().system_config());
1729        // Floats will overflow to infinity instead of panicking, which has the correct comparison
1730        // semantics.
1731        // NaN should be impossible here since both values are positive.
1732        let desired = current_amount + new_amount;
1733        if desired > limit {
1734            Err(AdapterError::ResourceExhaustion {
1735                resource_type: resource_type.to_string(),
1736                limit_name: limit_name.to_string(),
1737                desired: desired.to_string(),
1738                limit: limit.to_string(),
1739                current: current_amount.to_string(),
1740            })
1741        } else {
1742            Ok(())
1743        }
1744    }
1745}