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