1use std::collections::BTreeMap;
11
12use mz_adapter_types::connection::ConnectionId;
13use mz_catalog::memory::objects::CatalogItem;
14use mz_controller::clusters::{ManagedReplicaLocation, ReplicaLocation};
15use mz_controller_types::ClusterId;
16use mz_ore::soft_panic_or_log;
17use mz_repr::adt::numeric::Numeric;
18use mz_sql::names::ResolvedDatabaseSpecifier;
19use mz_sql::plan::ConnectionDetails;
20use mz_sql::session::vars::{
21 MAX_AWS_PRIVATELINK_CONNECTIONS, MAX_CLUSTERS, MAX_CREDIT_CONSUMPTION_RATE, MAX_DATABASES,
22 MAX_KAFKA_CONNECTIONS, MAX_MATERIALIZED_VIEWS, MAX_MYSQL_CONNECTIONS, MAX_NETWORK_POLICIES,
23 MAX_OBJECTS_PER_SCHEMA, MAX_POSTGRES_CONNECTIONS, MAX_REPLICAS_PER_CLUSTER, MAX_ROLES,
24 MAX_SCHEMAS_PER_DATABASE, MAX_SECRETS, MAX_SINKS, MAX_SOURCES, MAX_SQL_SERVER_CONNECTIONS,
25 MAX_TABLES, SystemVars, Var,
26};
27
28use crate::AdapterError;
29use crate::catalog::{CatalogState, DropObjectInfo, Op};
30
31impl CatalogState {
32 pub(crate) fn validate_resource_limits(
35 &self,
36 ops: &[Op],
37 conn_id: &ConnectionId,
38 ) -> Result<(), AdapterError> {
39 let mut new_kafka_connections = 0;
40 let mut new_postgres_connections = 0;
41 let mut new_mysql_connections = 0;
42 let mut new_sql_server_connections = 0;
43 let mut new_aws_privatelink_connections = 0;
44 let mut new_tables = 0;
45 let mut new_sources = 0;
46 let mut new_sinks = 0;
47 let mut new_materialized_views = 0;
48 let mut new_clusters = 0;
49 let mut new_replicas_per_cluster = BTreeMap::new();
50 let mut new_credit_consumption_rate = Numeric::zero();
51 let mut new_databases = 0;
52 let mut new_schemas_per_database = BTreeMap::new();
53 let mut new_objects_per_schema = BTreeMap::new();
54 let mut new_secrets = 0;
55 let mut new_roles = 0;
56 let mut new_network_policies = 0;
57 for op in ops {
58 match op {
59 Op::CreateDatabase { .. } => {
60 new_databases += 1;
61 }
62 Op::CreateSchema { database_id, .. } => {
63 if let ResolvedDatabaseSpecifier::Id(database_id) = database_id {
64 *new_schemas_per_database.entry(database_id).or_insert(0) += 1;
65 }
66 }
67 Op::CreateRole { .. } => {
68 new_roles += 1;
69 }
70 Op::CreateNetworkPolicy { .. } => {
71 new_network_policies += 1;
72 }
73 Op::CreateCluster { .. } => {
74 new_clusters += 1;
78 }
79 Op::CreateClusterReplica {
80 cluster_id, config, ..
81 } => {
82 if cluster_id.is_user() {
83 *new_replicas_per_cluster.entry(*cluster_id).or_insert(0) += 1;
84 if let ReplicaLocation::Managed(location) = &config.location {
85 new_credit_consumption_rate += self.replica_credits_per_hour(location);
86 }
87 }
88 }
89 Op::CreateItem { name, item, .. } => {
90 *new_objects_per_schema
91 .entry((
92 name.qualifiers.database_spec.clone(),
93 name.qualifiers.schema_spec.clone(),
94 ))
95 .or_insert(0) += 1;
96 match item {
97 CatalogItem::Connection(connection) => match connection.details {
98 ConnectionDetails::Kafka(_) => new_kafka_connections += 1,
99 ConnectionDetails::Postgres(_) => new_postgres_connections += 1,
100 ConnectionDetails::MySql(_) => new_mysql_connections += 1,
101 ConnectionDetails::SqlServer(_) => new_sql_server_connections += 1,
102 ConnectionDetails::AwsPrivatelink(_) => {
103 new_aws_privatelink_connections += 1
104 }
105 ConnectionDetails::Csr(_)
106 | ConnectionDetails::GlueSchemaRegistry(_)
107 | ConnectionDetails::Ssh { .. }
108 | ConnectionDetails::Aws(_)
109 | ConnectionDetails::Gcp(_)
110 | ConnectionDetails::IcebergCatalog(_) => {}
111 },
112 CatalogItem::Table(_) => {
113 new_tables += 1;
114 }
115 CatalogItem::Source(source) => {
116 new_sources += source.user_controllable_persist_shard_count()
117 }
118 CatalogItem::Sink(_) => new_sinks += 1,
119 CatalogItem::MaterializedView(_) => {
120 new_materialized_views += 1;
121 }
122 CatalogItem::Secret(_) => {
123 new_secrets += 1;
124 }
125 CatalogItem::Log(_)
126 | CatalogItem::View(_)
127 | CatalogItem::Index(_)
128 | CatalogItem::Type(_)
129 | CatalogItem::Func(_)
130 | CatalogItem::MetricSink(_) => {}
131 }
132 }
133 Op::DropObjects(drop_object_infos) => {
134 for drop_object_info in drop_object_infos {
135 match drop_object_info {
136 DropObjectInfo::Cluster(_) => {
137 new_clusters -= 1;
138 }
139 DropObjectInfo::ClusterReplica((cluster_id, replica_id, _reason)) => {
140 if cluster_id.is_user() {
141 *new_replicas_per_cluster.entry(*cluster_id).or_insert(0) -= 1;
142 let cluster =
143 self.get_cluster_replica(*cluster_id, *replica_id);
144 if let ReplicaLocation::Managed(location) =
145 &cluster.config.location
146 {
147 new_credit_consumption_rate -=
148 self.replica_credits_per_hour(location);
149 }
150 }
151 }
152 DropObjectInfo::Database(_) => {
153 new_databases -= 1;
154 }
155 DropObjectInfo::Schema((database_spec, _)) => {
156 if let ResolvedDatabaseSpecifier::Id(database_id) = database_spec {
157 *new_schemas_per_database.entry(database_id).or_insert(0) -= 1;
158 }
159 }
160 DropObjectInfo::Role(_) => {
161 new_roles -= 1;
162 }
163 DropObjectInfo::NetworkPolicy(_) => {
164 new_network_policies -= 1;
165 }
166 DropObjectInfo::Item(id) => {
167 let entry = self.get_entry(id);
168 *new_objects_per_schema
169 .entry((
170 entry.name().qualifiers.database_spec.clone(),
171 entry.name().qualifiers.schema_spec.clone(),
172 ))
173 .or_insert(0) -= 1;
174 match entry.item() {
175 CatalogItem::Connection(connection) => match connection.details
176 {
177 ConnectionDetails::AwsPrivatelink(_) => {
178 new_aws_privatelink_connections -= 1;
179 }
180 _ => (),
181 },
182 CatalogItem::Table(_) => {
183 new_tables -= 1;
184 }
185 CatalogItem::Source(source) => {
186 new_sources -=
187 source.user_controllable_persist_shard_count()
188 }
189 CatalogItem::Sink(_) => new_sinks -= 1,
190 CatalogItem::MaterializedView(_) => {
191 new_materialized_views -= 1;
192 }
193 CatalogItem::Secret(_) => {
194 new_secrets -= 1;
195 }
196 CatalogItem::Log(_)
197 | CatalogItem::View(_)
198 | CatalogItem::Index(_)
199 | CatalogItem::Type(_)
200 | CatalogItem::Func(_)
201 | CatalogItem::MetricSink(_) => {}
202 }
203 }
204 }
205 }
206 }
207 Op::UpdateItem {
208 name: _,
209 id,
210 to_item,
211 } => match to_item {
212 CatalogItem::Source(source) => {
213 let current_source = self
214 .get_entry(id)
215 .source()
216 .expect("source update is for source item");
217
218 new_sources += source.user_controllable_persist_shard_count()
219 - current_source.user_controllable_persist_shard_count();
220 }
221 CatalogItem::Connection(_)
222 | CatalogItem::Table(_)
223 | CatalogItem::Sink(_)
224 | CatalogItem::MaterializedView(_)
225 | CatalogItem::Secret(_)
226 | CatalogItem::Log(_)
227 | CatalogItem::View(_)
228 | CatalogItem::Index(_)
229 | CatalogItem::Type(_)
230 | CatalogItem::Func(_)
231 | CatalogItem::MetricSink(_) => {}
232 },
233 Op::AlterRole { .. }
234 | Op::AlterRetainHistory { .. }
235 | Op::AlterSourceTimestampInterval { .. }
236 | Op::AlterNetworkPolicy { .. }
237 | Op::AlterAddColumn { .. }
238 | Op::AlterMaterializedViewApplyReplacement { .. }
239 | Op::UpdatePrivilege { .. }
240 | Op::UpdateDefaultPrivilege { .. }
241 | Op::GrantRole { .. }
242 | Op::RenameCluster { .. }
243 | Op::RenameClusterReplica { .. }
244 | Op::RenameItem { .. }
245 | Op::RenameSchema { .. }
246 | Op::UpdateOwner { .. }
247 | Op::RevokeRole { .. }
248 | Op::UpdateClusterConfig { .. }
249 | Op::UpdateSourceReferences { .. }
250 | Op::UpdateSystemConfiguration { .. }
251 | Op::ResetSystemConfiguration { .. }
252 | Op::ResetAllSystemConfiguration { .. }
253 | Op::UpdateScopedSystemParameters { .. }
254 | Op::Comment { .. }
255 | Op::RenameCommand(_)
256 | Op::CheckClusterState { .. }
257 | Op::InjectAuditEvents { .. } => {}
258 }
259 }
260
261 let mut current_aws_privatelink_connections = 0;
262 let mut current_postgres_connections = 0;
263 let mut current_mysql_connections = 0;
264 let mut current_sql_server_connections = 0;
265 let mut current_kafka_connections = 0;
266 for c in self.user_connections() {
267 let connection = c
268 .connection()
269 .expect("`user_connections()` only returns connection objects");
270
271 match connection.details {
272 ConnectionDetails::AwsPrivatelink(_) => current_aws_privatelink_connections += 1,
273 ConnectionDetails::Postgres(_) => current_postgres_connections += 1,
274 ConnectionDetails::MySql(_) => current_mysql_connections += 1,
275 ConnectionDetails::SqlServer(_) => current_sql_server_connections += 1,
276 ConnectionDetails::Kafka(_) => current_kafka_connections += 1,
277 ConnectionDetails::Csr(_)
278 | ConnectionDetails::GlueSchemaRegistry(_)
279 | ConnectionDetails::Ssh { .. }
280 | ConnectionDetails::Aws(_)
281 | ConnectionDetails::Gcp(_)
282 | ConnectionDetails::IcebergCatalog(_) => {}
283 }
284 }
285 self.validate_resource_limit(
286 current_kafka_connections,
287 new_kafka_connections,
288 SystemVars::max_kafka_connections,
289 "Kafka Connection",
290 MAX_KAFKA_CONNECTIONS.name(),
291 )?;
292 self.validate_resource_limit(
293 current_postgres_connections,
294 new_postgres_connections,
295 SystemVars::max_postgres_connections,
296 "PostgreSQL Connection",
297 MAX_POSTGRES_CONNECTIONS.name(),
298 )?;
299 self.validate_resource_limit(
300 current_mysql_connections,
301 new_mysql_connections,
302 SystemVars::max_mysql_connections,
303 "MySQL Connection",
304 MAX_MYSQL_CONNECTIONS.name(),
305 )?;
306 self.validate_resource_limit(
307 current_sql_server_connections,
308 new_sql_server_connections,
309 SystemVars::max_sql_server_connections,
310 "SQL Server Connection",
311 MAX_SQL_SERVER_CONNECTIONS.name(),
312 )?;
313 self.validate_resource_limit(
314 current_aws_privatelink_connections,
315 new_aws_privatelink_connections,
316 SystemVars::max_aws_privatelink_connections,
317 "AWS PrivateLink Connection",
318 MAX_AWS_PRIVATELINK_CONNECTIONS.name(),
319 )?;
320 self.validate_resource_limit(
321 self.user_tables().count(),
322 new_tables,
323 SystemVars::max_tables,
324 "table",
325 MAX_TABLES.name(),
326 )?;
327
328 let current_sources: usize = self
329 .user_sources()
330 .filter_map(|source| source.source())
331 .map(|source| source.user_controllable_persist_shard_count())
332 .sum::<i64>()
333 .try_into()
334 .expect("non-negative sum of sources");
335
336 self.validate_resource_limit(
337 current_sources,
338 new_sources,
339 SystemVars::max_sources,
340 "source",
341 MAX_SOURCES.name(),
342 )?;
343 self.validate_resource_limit(
344 self.user_sinks().count(),
345 new_sinks,
346 SystemVars::max_sinks,
347 "sink",
348 MAX_SINKS.name(),
349 )?;
350 self.validate_resource_limit(
351 self.user_materialized_views().count(),
352 new_materialized_views,
353 SystemVars::max_materialized_views,
354 "materialized view",
355 MAX_MATERIALIZED_VIEWS.name(),
356 )?;
357 self.validate_resource_limit(
358 self.user_clusters().count(),
364 new_clusters,
365 SystemVars::max_clusters,
366 "cluster",
367 MAX_CLUSTERS.name(),
368 )?;
369 for (cluster_id, new_replicas) in new_replicas_per_cluster {
370 let current_amount = self
372 .try_get_cluster(cluster_id)
373 .map(|instance| instance.user_replicas().count())
374 .unwrap_or(0);
375 self.validate_resource_limit(
376 current_amount,
377 new_replicas,
378 SystemVars::max_replicas_per_cluster,
379 "cluster replica",
380 MAX_REPLICAS_PER_CLUSTER.name(),
381 )?;
382 }
383 self.validate_resource_limit_numeric(
384 self.current_credit_consumption_rate(None),
385 new_credit_consumption_rate,
386 |system_vars| {
387 self.license_key
388 .max_credit_consumption_rate()
389 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
390 },
391 "cluster replica",
392 MAX_CREDIT_CONSUMPTION_RATE.name(),
393 )?;
394 self.validate_resource_limit(
395 self.databases().count(),
396 new_databases,
397 SystemVars::max_databases,
398 "database",
399 MAX_DATABASES.name(),
400 )?;
401 for (database_id, new_schemas) in new_schemas_per_database {
402 self.validate_resource_limit(
403 self.get_database(database_id).schemas_by_id.len(),
404 new_schemas,
405 SystemVars::max_schemas_per_database,
406 "schema",
407 MAX_SCHEMAS_PER_DATABASE.name(),
408 )?;
409 }
410 for ((database_spec, schema_spec), new_objects) in new_objects_per_schema {
411 let current_items = self
414 .try_get_schema(&database_spec, &schema_spec, conn_id)
415 .map(|schema| schema.items.len())
416 .unwrap_or(0);
417 self.validate_resource_limit(
418 current_items,
419 new_objects,
420 SystemVars::max_objects_per_schema,
421 "object",
422 MAX_OBJECTS_PER_SCHEMA.name(),
423 )?;
424 }
425 self.validate_resource_limit(
426 self.user_secrets().count(),
427 new_secrets,
428 SystemVars::max_secrets,
429 "secret",
430 MAX_SECRETS.name(),
431 )?;
432 self.validate_resource_limit(
433 self.user_roles().count(),
434 new_roles,
435 SystemVars::max_roles,
436 "role",
437 MAX_ROLES.name(),
438 )?;
439 self.validate_resource_limit(
440 self.user_network_policies().count(),
441 new_network_policies,
442 SystemVars::max_network_policies,
443 "network_policy",
444 MAX_NETWORK_POLICIES.name(),
445 )?;
446 Ok(())
447 }
448
449 pub(crate) fn validate_resource_limit<F>(
451 &self,
452 current_amount: usize,
453 new_instances: i64,
454 resource_limit: F,
455 resource_type: &str,
456 limit_name: &str,
457 ) -> Result<(), AdapterError>
458 where
459 F: Fn(&SystemVars) -> u32,
460 {
461 if new_instances <= 0 {
462 return Ok(());
463 }
464
465 let limit: i64 = resource_limit(self.system_config()).into();
466 let current_amount: Option<i64> = current_amount.try_into().ok();
467 let desired =
468 current_amount.and_then(|current_amount| current_amount.checked_add(new_instances));
469
470 let exceeds_limit = if let Some(desired) = desired {
471 desired > limit
472 } else {
473 true
474 };
475
476 let desired = desired
477 .map(|desired| desired.to_string())
478 .unwrap_or_else(|| format!("more than {}", i64::MAX));
479 let current = current_amount
480 .map(|current| current.to_string())
481 .unwrap_or_else(|| format!("more than {}", i64::MAX));
482 if exceeds_limit {
483 Err(AdapterError::ResourceExhaustion {
484 resource_type: resource_type.to_string(),
485 limit_name: limit_name.to_string(),
486 desired,
487 limit: limit.to_string(),
488 current,
489 })
490 } else {
491 Ok(())
492 }
493 }
494
495 pub(crate) fn validate_resource_limit_numeric<F>(
499 &self,
500 current_amount: Numeric,
501 new_amount: Numeric,
502 resource_limit: F,
503 resource_type: &str,
504 limit_name: &str,
505 ) -> Result<(), AdapterError>
506 where
507 F: Fn(&SystemVars) -> Numeric,
508 {
509 if new_amount <= Numeric::zero() {
510 return Ok(());
511 }
512
513 let limit = resource_limit(self.system_config());
514 let desired = current_amount + new_amount;
518 if desired > limit {
519 Err(AdapterError::ResourceExhaustion {
520 resource_type: resource_type.to_string(),
521 limit_name: limit_name.to_string(),
522 desired: desired.to_string(),
523 limit: limit.to_string(),
524 current: current_amount.to_string(),
525 })
526 } else {
527 Ok(())
528 }
529 }
530 pub(crate) fn current_credit_consumption_rate(
533 &self,
534 exclude_cluster: Option<ClusterId>,
535 ) -> Numeric {
536 self.user_cluster_replicas()
537 .filter(|replica| Some(replica.cluster_id) != exclude_cluster)
538 .filter_map(|replica| match &replica.config.location {
539 ReplicaLocation::Managed(location) => Some(self.replica_credits_per_hour(location)),
540 ReplicaLocation::Unmanaged(_) => None,
541 })
542 .sum()
543 }
544
545 pub(crate) fn replica_credits_per_hour(&self, location: &ManagedReplicaLocation) -> Numeric {
552 let size = location.size_for_billing();
553 match self.cluster_replica_sizes().0.get(size) {
554 Some(allocation) => allocation.credits_per_hour,
555 None => {
556 soft_panic_or_log!(
557 "replica of size {:?} bills as unknown replica size {:?}, counting it as free",
558 location.size,
559 size,
560 );
561 Numeric::zero()
562 }
563 }
564 }
565}