1use 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 #[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 #[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 let apply_implications_res = self
113 .apply_catalog_implications(ctx.as_deref_mut(), catalog_updates)
114 .await;
115
116 apply_implications_res.expect("cannot fail to apply catalog update implications");
120
121 let consistency_start = Instant::now();
128
129 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 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 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 #[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 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 combined_apply_res.expect("cannot fail to apply catalog implications");
225
226 let consistency_start = Instant::now();
229
230 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 #[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 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 let phase_seconds = self.metrics.catalog_transact_phase_seconds.clone();
307
308 let clone_start = Instant::now();
310 let txn_ops_clone = txn_ops.clone();
311 let txn_state_clone = txn_state.clone();
312 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 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 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 let conn = self.active_conns.get(ctx.session().conn_id());
341
342 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 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 #[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 }
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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 #[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 #[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 #[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 #[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 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 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 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 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 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 self.controller.storage.check_exists(sink.from)?;
1154
1155 let id_bundle = crate::CollectionIdBundle {
1162 storage_ids: btreeset! {sink.from},
1163 compute_ids: btreemap! {},
1164 };
1165
1166 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 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 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);
1222
1223 Ok(res?)
1224 }
1225
1226 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 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 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 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 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 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 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 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}