1use std::collections::{BTreeMap, BTreeSet};
30use std::sync::Arc;
31use std::time::{Duration, Instant};
32
33use fail::fail_point;
34use itertools::Itertools;
35use mz_adapter_types::compaction::CompactionWindow;
36use mz_catalog::memory::objects::{
37 CatalogItem, Cluster, ClusterReplica, Connection, DataSourceDesc, Index, MaterializedView,
38 MetricSink, Secret, Sink, Source, StateDiff, Table, TableDataSource, View,
39};
40use mz_cloud_resources::VpcEndpointConfig;
41use mz_compute_client::logging::LogVariant;
42use mz_compute_client::protocol::response::{PeekError, PeekResponse};
43use mz_controller::clusters::{ClusterRole, ReplicaConfig};
44use mz_controller_types::{ClusterId, ReplicaId};
45use mz_ore::collections::CollectionExt;
46use mz_ore::error::ErrorExt;
47use mz_ore::future::InTask;
48use mz_ore::instrument;
49use mz_ore::retry::Retry;
50use mz_ore::task;
51use mz_repr::{CatalogItemId, GlobalId, RelationVersion, RelationVersionSelector};
52use mz_sql::plan::ConnectionDetails;
53use mz_storage_client::controller::{CollectionDescription, DataSource};
54use mz_storage_types::connections::PostgresConnection;
55use mz_storage_types::connections::inline::{InlinedConnection, IntoInlineConnection};
56use mz_storage_types::sinks::StorageSinkConnection;
57use mz_storage_types::sources::{
58 GenericSourceConnection, SourceDesc, SourceExport, SourceExportDataConfig,
59};
60use tracing::{Instrument, info_span, warn};
61
62use crate::active_compute_sink::ActiveComputeSinkRetireReason;
63use crate::coord::Coordinator;
64use crate::coord::catalog_implications::parsed_state_updates::{
65 ParsedStateUpdate, ParsedStateUpdateKind,
66};
67use crate::coord::peek::DroppedDependency;
68use crate::coord::timeline::TimelineState;
69use crate::statement_logging::{StatementEndedExecutionReason, StatementLoggingId};
70use crate::{AdapterError, CollectionIdBundle, ExecuteContext, ResultExt};
71
72pub mod parsed_state_updates;
73
74impl Coordinator {
75 #[instrument(level = "debug")]
84 pub async fn apply_catalog_implications(
85 &mut self,
86 ctx: Option<&mut ExecuteContext>,
87 catalog_updates: Vec<ParsedStateUpdate>,
88 ) -> Result<(), AdapterError> {
89 let start = Instant::now();
90
91 let mut catalog_implications: BTreeMap<CatalogItemId, CatalogImplication> = BTreeMap::new();
92 let mut cluster_commands: BTreeMap<ClusterId, CatalogImplication> = BTreeMap::new();
93 let mut cluster_replica_commands: BTreeMap<(ClusterId, ReplicaId), CatalogImplication> =
94 BTreeMap::new();
95 let mut introspection_source_indexes: BTreeMap<ClusterId, BTreeMap<LogVariant, GlobalId>> =
100 BTreeMap::new();
101 let mut replica_scoped_config_changed = false;
105 let mut system_config_changed = false;
109
110 let should_reconcile_now = Self::should_reconcile_now(&catalog_updates);
113
114 for update in catalog_updates {
115 tracing::trace!(?update, "got parsed state update");
116 match &update.kind {
117 ParsedStateUpdateKind::Item {
118 durable_item,
119 parsed_item: _,
120 connection: _,
121 parsed_full_name: _,
122 } => {
123 let entry = catalog_implications
124 .entry(durable_item.id.clone())
125 .or_insert_with(|| CatalogImplication::None);
126 entry.absorb(update);
127 }
128 ParsedStateUpdateKind::Cluster {
129 durable_cluster,
130 parsed_cluster: _,
131 } => {
132 let entry = cluster_commands
133 .entry(durable_cluster.id)
134 .or_insert_with(|| CatalogImplication::None);
135 entry.absorb(update.clone());
136 }
137 ParsedStateUpdateKind::ClusterReplica {
138 durable_cluster_replica,
139 parsed_cluster_replica: _,
140 } => {
141 let entry = cluster_replica_commands
142 .entry((
143 durable_cluster_replica.cluster_id,
144 durable_cluster_replica.replica_id,
145 ))
146 .or_insert_with(|| CatalogImplication::None);
147 entry.absorb(update.clone());
148 }
149 ParsedStateUpdateKind::IntrospectionSourceIndex {
150 cluster_id,
151 log,
152 index_id,
153 } => {
154 if update.diff == StateDiff::Addition {
155 introspection_source_indexes
156 .entry(*cluster_id)
157 .or_default()
158 .insert(log.clone(), *index_id);
159 }
160 }
163 ParsedStateUpdateKind::ReplicaSystemConfiguration { durable: _ } => {
164 replica_scoped_config_changed = true;
168 }
169 ParsedStateUpdateKind::SystemConfiguration { durable: _ } => {
170 system_config_changed = true;
174 }
175 }
176 }
177
178 self.apply_catalog_implications_inner(
179 ctx,
180 catalog_implications.into_iter().collect_vec(),
181 cluster_commands.into_iter().collect_vec(),
182 cluster_replica_commands.into_iter().collect_vec(),
183 introspection_source_indexes,
184 replica_scoped_config_changed,
185 system_config_changed,
186 )
187 .await?;
188
189 if should_reconcile_now {
190 self.reconcile_now.notify_one();
195 }
196
197 self.metrics
198 .apply_catalog_implications_seconds
199 .observe(start.elapsed().as_secs_f64());
200
201 Ok(())
202 }
203
204 fn should_reconcile_now(updates: &[ParsedStateUpdate]) -> bool {
217 updates.iter().any(|update| {
218 matches!(
219 update.kind,
220 ParsedStateUpdateKind::Cluster { .. }
221 | ParsedStateUpdateKind::ClusterReplica { .. }
222 )
223 })
224 }
225
226 #[instrument(level = "debug")]
227 async fn apply_catalog_implications_inner(
228 &mut self,
229 ctx: Option<&mut ExecuteContext>,
230 implications: Vec<(CatalogItemId, CatalogImplication)>,
231 cluster_commands: Vec<(ClusterId, CatalogImplication)>,
232 cluster_replica_commands: Vec<((ClusterId, ReplicaId), CatalogImplication)>,
233 mut introspection_source_indexes: BTreeMap<ClusterId, BTreeMap<LogVariant, GlobalId>>,
234 replica_scoped_config_changed: bool,
235 system_config_changed: bool,
236 ) -> Result<(), AdapterError> {
237 if system_config_changed {
243 self.catalog().system_config().notify_all_callbacks();
244 }
245
246 let mut tables_to_drop = BTreeSet::new();
247 let mut sources_to_drop = vec![];
248 let mut replication_slots_to_drop: Vec<(PostgresConnection, String)> = vec![];
249 let mut storage_sink_gids_to_drop = vec![];
250 let mut indexes_to_drop = vec![];
251 let mut compute_sinks_to_drop = vec![];
252 let mut view_gids_to_drop = vec![];
253 let mut secrets_to_drop = vec![];
254 let mut vpc_endpoints_to_drop = vec![];
255 let mut clusters_to_drop = vec![];
256 let mut cluster_replicas_to_drop = vec![];
257 let mut cluster_replicas_to_create = vec![];
258 let mut active_compute_sinks_to_drop = BTreeMap::new();
259 let mut peeks_to_drop = vec![];
260 let mut copies_to_drop = vec![];
261
262 let mut dropped_item_names: BTreeMap<GlobalId, String> = BTreeMap::new();
264 let mut dropped_cluster_names: BTreeMap<ClusterId, String> = BTreeMap::new();
265
266 let mut table_collections_to_create = BTreeMap::new();
269 let mut source_collections_to_create = BTreeMap::new();
270 let mut storage_policies_to_initialize = BTreeMap::new();
271 let mut execution_timestamps_to_set = BTreeSet::new();
272 let mut vpc_endpoints_to_create: Vec<(CatalogItemId, VpcEndpointConfig)> = vec![];
273
274 let mut source_gids_to_keep = BTreeSet::new();
277
278 let mut source_connections_to_alter: BTreeMap<
280 GlobalId,
281 GenericSourceConnection<InlinedConnection>,
282 > = BTreeMap::new();
283 let mut sink_connections_to_alter: BTreeMap<GlobalId, StorageSinkConnection> =
284 BTreeMap::new();
285 let mut source_export_data_configs_to_alter: BTreeMap<GlobalId, SourceExportDataConfig> =
286 BTreeMap::new();
287 let mut source_descs_to_alter: BTreeMap<GlobalId, SourceDesc> = BTreeMap::new();
288
289 for (catalog_id, implication) in implications {
296 tracing::trace!(?implication, "have to apply catalog implication");
297
298 match implication {
299 CatalogImplication::Table(CatalogImplicationKind::Added(table)) => {
300 self.handle_create_table(
301 &ctx,
302 &mut table_collections_to_create,
303 &mut storage_policies_to_initialize,
304 &mut execution_timestamps_to_set,
305 catalog_id,
306 table.clone(),
307 )
308 .await?
309 }
310 CatalogImplication::Table(CatalogImplicationKind::Altered {
311 prev: prev_table,
312 new: new_table,
313 }) => {
314 self.handle_alter_table(catalog_id, prev_table, new_table)
315 .await?
316 }
317
318 CatalogImplication::Table(CatalogImplicationKind::Dropped(table, full_name)) => {
319 let global_ids = table.global_ids();
320 for global_id in global_ids {
321 tables_to_drop.insert((catalog_id, global_id));
322 dropped_item_names.insert(global_id, full_name.clone());
323 }
324 }
325 CatalogImplication::Source(CatalogImplicationKind::Added((
326 source,
327 _connection,
328 ))) => {
329 let compaction_windows = self
334 .catalog()
335 .state()
336 .source_compaction_windows(vec![catalog_id]);
337
338 self.handle_create_source(
339 &mut source_collections_to_create,
340 &mut storage_policies_to_initialize,
341 catalog_id,
342 source,
343 compaction_windows,
344 )
345 .await?
346 }
347 CatalogImplication::Source(CatalogImplicationKind::Altered {
348 prev: (prev_source, _prev_connection),
349 new: (new_source, new_connection),
350 }) => {
351 if prev_source.custom_logical_compaction_window
352 != new_source.custom_logical_compaction_window
353 {
354 let new_window = new_source
355 .custom_logical_compaction_window
356 .unwrap_or(CompactionWindow::Default);
357 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
358 }
359 match (&prev_source.data_source, &new_source.data_source) {
360 (
361 DataSourceDesc::Ingestion {
362 desc: prev_desc, ..
363 }
364 | DataSourceDesc::OldSyntaxIngestion {
365 desc: prev_desc, ..
366 },
367 DataSourceDesc::Ingestion { desc: new_desc, .. }
368 | DataSourceDesc::OldSyntaxIngestion { desc: new_desc, .. },
369 ) => {
370 if prev_desc != new_desc {
371 let inlined_connection = new_connection
372 .expect("ingestion source should have inlined connection");
373 let inlined_desc = SourceDesc {
374 connection: inlined_connection,
375 timestamp_interval: new_desc.timestamp_interval,
376 };
377 source_descs_to_alter.insert(new_source.global_id, inlined_desc);
378 }
379 }
380 _ => {}
381 }
382 }
383 CatalogImplication::Source(CatalogImplicationKind::Dropped(
384 (source, connection),
385 full_name,
386 )) => {
387 let global_id = source.global_id();
388 sources_to_drop.push((catalog_id, global_id));
389 dropped_item_names.insert(global_id, full_name);
390
391 if let DataSourceDesc::Ingestion { desc, .. }
392 | DataSourceDesc::OldSyntaxIngestion { desc, .. } = &source.data_source
393 {
394 match &desc.connection {
395 GenericSourceConnection::Postgres(_referenced_conn) => {
396 let inline_conn = connection.expect("missing inlined connection");
397
398 let pg_conn = match inline_conn {
399 GenericSourceConnection::Postgres(pg_conn) => pg_conn,
400 other => {
401 panic!("expected postgres connection, got: {:?}", other)
402 }
403 };
404 let pending_drop = (
405 pg_conn.connection.clone(),
406 pg_conn.publication_details.slot.clone(),
407 );
408 replication_slots_to_drop.push(pending_drop);
409 }
410 _ => {}
411 }
412 }
413 }
414 CatalogImplication::Sink(CatalogImplicationKind::Added(sink)) => {
415 tracing::debug!(?sink, "not handling AddSink in here yet");
416 }
417 CatalogImplication::Sink(CatalogImplicationKind::Altered {
418 prev: prev_sink,
419 new: new_sink,
420 }) => {
421 tracing::debug!(?prev_sink, ?new_sink, "not handling AlterSink in here yet");
422 }
423 CatalogImplication::Sink(CatalogImplicationKind::Dropped(sink, full_name)) => {
424 storage_sink_gids_to_drop.push(sink.global_id());
425 dropped_item_names.insert(sink.global_id(), full_name);
426 }
427 CatalogImplication::Index(CatalogImplicationKind::Added(index)) => {
428 tracing::debug!(?index, "not handling AddIndex in here yet");
429 }
430 CatalogImplication::Index(CatalogImplicationKind::Altered {
431 prev: prev_index,
432 new: new_index,
433 }) => {
434 if prev_index.custom_logical_compaction_window
435 != new_index.custom_logical_compaction_window
436 {
437 let new_window = new_index
438 .custom_logical_compaction_window
439 .unwrap_or(CompactionWindow::Default);
440 self.update_compute_read_policy(
441 new_index.cluster_id,
442 catalog_id,
443 new_window.into(),
444 );
445 }
446 }
447 CatalogImplication::Index(CatalogImplicationKind::Dropped(index, full_name)) => {
448 indexes_to_drop.push((index.cluster_id, index.global_id()));
449 dropped_item_names.insert(index.global_id(), full_name);
450 }
451 CatalogImplication::MetricSink(CatalogImplicationKind::Added(_metric_sink)) => {
452 }
456 CatalogImplication::MetricSink(CatalogImplicationKind::Altered { .. }) => {
457 }
459 CatalogImplication::MetricSink(CatalogImplicationKind::Dropped(
460 metric_sink,
461 full_name,
462 )) => {
463 compute_sinks_to_drop.push((metric_sink.cluster_id, metric_sink.global_id));
466 dropped_item_names.insert(metric_sink.global_id, full_name);
467 }
468 CatalogImplication::MaterializedView(CatalogImplicationKind::Added(mv)) => {
469 tracing::debug!(?mv, "not handling AddMaterializedView in here yet");
470 }
471 CatalogImplication::MaterializedView(CatalogImplicationKind::Altered {
472 prev: prev_mv,
473 new: new_mv,
474 }) => {
475 if prev_mv.collections != new_mv.collections {
494 assert_eq!(
497 prev_mv.global_id_writes(),
498 new_mv.global_id_writes(),
499 "unexpected MV Altered implication: prev={prev_mv:?}, new={new_mv:?}",
500 );
501
502 let gid = new_mv.global_id_writes();
503 self.allow_writes(new_mv.cluster_id, gid);
504
505 source_gids_to_keep.extend(new_mv.global_ids());
510 } else if prev_mv.custom_logical_compaction_window
511 != new_mv.custom_logical_compaction_window
512 {
513 let new_window = new_mv
514 .custom_logical_compaction_window
515 .unwrap_or(CompactionWindow::Default);
516 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
517 }
518 }
519 CatalogImplication::MaterializedView(CatalogImplicationKind::Dropped(
520 mv,
521 full_name,
522 )) => {
523 compute_sinks_to_drop.push((mv.cluster_id, mv.global_id_writes()));
524 for gid in mv.global_ids() {
525 sources_to_drop.push((catalog_id, gid));
526 dropped_item_names.insert(gid, full_name.clone());
527 }
528 }
529 CatalogImplication::View(CatalogImplicationKind::Added(_view)) => {
530 }
533 CatalogImplication::View(CatalogImplicationKind::Altered {
534 prev: _prev_view,
535 new: _new_view,
536 }) => {
537 }
540 CatalogImplication::View(CatalogImplicationKind::Dropped(view, full_name)) => {
541 view_gids_to_drop.push(view.global_id());
542 dropped_item_names.insert(view.global_id(), full_name);
543 }
544 CatalogImplication::Secret(CatalogImplicationKind::Added(_secret)) => {
545 }
549 CatalogImplication::Secret(CatalogImplicationKind::Altered {
550 prev: _prev_secret,
551 new: _new_secret,
552 }) => {
553 }
556 CatalogImplication::Secret(CatalogImplicationKind::Dropped(
557 _secret,
558 _full_name,
559 )) => {
560 secrets_to_drop.push(catalog_id);
561 }
562 CatalogImplication::Connection(CatalogImplicationKind::Added(connection)) => {
563 match &connection.details {
564 ConnectionDetails::Ssh { .. } => {}
567 ConnectionDetails::AwsPrivatelink(privatelink) => {
569 let spec = VpcEndpointConfig {
570 aws_service_name: privatelink.service_name.to_owned(),
571 availability_zone_ids: privatelink.availability_zones.to_owned(),
572 };
573 vpc_endpoints_to_create.push((catalog_id, spec));
574 }
575 _ => {}
577 }
578 }
579 CatalogImplication::Connection(CatalogImplicationKind::Altered {
580 prev: _prev_connection,
581 new: new_connection,
582 }) => {
583 self.handle_alter_connection(
584 catalog_id,
585 new_connection,
586 &mut vpc_endpoints_to_create,
587 &mut source_connections_to_alter,
588 &mut sink_connections_to_alter,
589 &mut source_export_data_configs_to_alter,
590 );
591 }
592 CatalogImplication::Connection(CatalogImplicationKind::Dropped(
593 connection,
594 _full_name,
595 )) => {
596 match &connection.details {
597 ConnectionDetails::Ssh { .. } => {
599 secrets_to_drop.push(catalog_id);
600 }
601 ConnectionDetails::AwsPrivatelink(_) => {
604 vpc_endpoints_to_drop.push(catalog_id);
605 }
606 _ => (),
607 }
608 }
609 CatalogImplication::None => {
610 }
612 CatalogImplication::Cluster(_) | CatalogImplication::ClusterReplica(_) => {
613 unreachable!("clusters and cluster replicas are handled below")
614 }
615 CatalogImplication::Table(CatalogImplicationKind::None)
616 | CatalogImplication::Source(CatalogImplicationKind::None)
617 | CatalogImplication::Sink(CatalogImplicationKind::None)
618 | CatalogImplication::Index(CatalogImplicationKind::None)
619 | CatalogImplication::MetricSink(CatalogImplicationKind::None)
620 | CatalogImplication::MaterializedView(CatalogImplicationKind::None)
621 | CatalogImplication::View(CatalogImplicationKind::None)
622 | CatalogImplication::Secret(CatalogImplicationKind::None)
623 | CatalogImplication::Connection(CatalogImplicationKind::None) => {
624 unreachable!("will never leave None in place");
625 }
626 }
627 }
628
629 for (cluster_id, command) in cluster_commands {
630 tracing::trace!(?command, "have cluster command to apply!");
631
632 match command {
633 CatalogImplication::Cluster(CatalogImplicationKind::Added(cluster)) => {
634 let arranged_logs = introspection_source_indexes
639 .remove(&cluster_id)
640 .unwrap_or_default();
641 let introspection_source_ids: Vec<_> =
642 arranged_logs.values().copied().collect();
643
644 self.controller
645 .create_cluster(
646 cluster_id,
647 mz_controller::clusters::ClusterConfig {
648 arranged_logs,
649 workload_class: cluster.config.workload_class.clone(),
650 },
651 )
652 .expect("creating cluster must not fail");
653
654 if !introspection_source_ids.is_empty() {
655 self.initialize_compute_read_policies(
656 introspection_source_ids,
657 cluster_id,
658 CompactionWindow::Default,
659 )
660 .await;
661 }
662 }
663 CatalogImplication::Cluster(CatalogImplicationKind::Altered {
664 prev: prev_cluster,
665 new: new_cluster,
666 }) => {
667 if prev_cluster.config.workload_class != new_cluster.config.workload_class {
673 self.controller.update_cluster_workload_class(
674 cluster_id,
675 new_cluster.config.workload_class.clone(),
676 );
677 }
678 }
679 CatalogImplication::Cluster(CatalogImplicationKind::Dropped(
680 cluster,
681 _full_name,
682 )) => {
683 clusters_to_drop.push(cluster_id);
684 dropped_cluster_names.insert(cluster_id, cluster.name);
685 }
686 CatalogImplication::Cluster(CatalogImplicationKind::None) => {
687 unreachable!("will never leave None in place");
688 }
689 command => {
690 unreachable!(
691 "we only handle cluster commands in this map, got: {:?}",
692 command
693 );
694 }
695 }
696 }
697
698 if replica_scoped_config_changed {
707 self.push_replica_dyncfg_overrides();
708 }
709
710 for ((cluster_id, replica_id), command) in cluster_replica_commands {
711 tracing::trace!(?command, "have cluster replica command to apply!");
712
713 match command {
714 CatalogImplication::ClusterReplica(CatalogImplicationKind::Added(replica)) => {
715 let cluster = self.catalog().get_cluster(cluster_id);
723 let cluster_name = cluster.name.clone();
724 let cluster_role = cluster.role();
725 cluster_replicas_to_create.push((
726 cluster_id,
727 replica_id,
728 cluster_role,
729 cluster_name,
730 replica.name.clone(),
731 replica.config.clone(),
732 ));
733 }
734 CatalogImplication::ClusterReplica(CatalogImplicationKind::Altered {
735 prev: _prev_replica,
736 new: _new_replica,
737 }) => {
738 }
742 CatalogImplication::ClusterReplica(CatalogImplicationKind::Dropped(
743 _replica,
744 _full_name,
745 )) => {
746 cluster_replicas_to_drop.push((cluster_id, replica_id));
747 }
748 CatalogImplication::ClusterReplica(CatalogImplicationKind::None) => {
749 unreachable!("will never leave None in place");
750 }
751 command => {
752 unreachable!(
753 "we only handle cluster replica commands in this map, got: {:?}",
754 command
755 );
756 }
757 }
758 }
759
760 let clusters_with_replica_creates = cluster_replicas_to_create
761 .iter()
762 .map(|(cluster_id, ..)| *cluster_id)
763 .collect();
764 let (replacement_drops, deferred_drops) = partition_cluster_replica_drops(
765 &clusters_with_replica_creates,
766 cluster_replicas_to_drop,
767 );
768 cluster_replicas_to_drop = deferred_drops;
769
770 if !replacement_drops.is_empty() {
777 fail::fail_point!("after_catalog_drop_replica");
778 for (cluster_id, replica_id) in replacement_drops {
779 self.drop_replica(cluster_id, replica_id);
780 }
781 }
782 for (cluster_id, replica_id, role, cluster_name, replica_name, config) in
783 cluster_replicas_to_create
784 {
785 self.handle_create_cluster_replica(
786 cluster_id,
787 replica_id,
788 role,
789 cluster_name,
790 replica_name,
791 config,
792 )
793 .await;
794 }
795
796 if !source_collections_to_create.is_empty() {
797 self.create_source_collections(source_collections_to_create)
798 .await?;
799 }
800
801 if !table_collections_to_create.is_empty() {
804 self.create_table_collections(table_collections_to_create, execution_timestamps_to_set)
805 .await?;
806 }
807 self.initialize_storage_collections(storage_policies_to_initialize)
813 .await?;
814
815 if !vpc_endpoints_to_create.is_empty() {
817 if let Some(cloud_resource_controller) = self.cloud_resource_controller.as_ref() {
818 for (connection_id, spec) in vpc_endpoints_to_create {
819 if let Err(err) = cloud_resource_controller
820 .ensure_vpc_endpoint(connection_id, spec)
821 .await
822 {
823 tracing::error!(?err, "failed to ensure vpc endpoint!");
824 }
825 }
826 } else {
827 tracing::error!(
828 "AWS PrivateLink connections unsupported without cloud_resource_controller"
829 );
830 }
831 }
832
833 if !source_connections_to_alter.is_empty() {
835 self.controller
836 .storage
837 .alter_ingestion_connections(source_connections_to_alter)
838 .await
839 .unwrap_or_terminate("cannot fail to alter ingestion connections");
840 }
841
842 if !sink_connections_to_alter.is_empty() {
843 self.controller
844 .storage
845 .alter_export_connections(sink_connections_to_alter)
846 .await
847 .unwrap_or_terminate("altering export connections after txn must succeed");
848 }
849
850 if !source_export_data_configs_to_alter.is_empty() {
851 self.controller
852 .storage
853 .alter_ingestion_export_data_configs(source_export_data_configs_to_alter)
854 .await
855 .unwrap_or_terminate("altering source export data configs after txn must succeed");
856 }
857
858 if !source_descs_to_alter.is_empty() {
859 self.controller
860 .storage
861 .alter_ingestion_source_desc(source_descs_to_alter)
862 .await
863 .unwrap_or_terminate("cannot fail to alter ingestion source desc");
864 }
865
866 sources_to_drop.retain(|(_, gid)| !source_gids_to_keep.contains(gid));
868
869 let readable_collections_to_drop: BTreeSet<_> = sources_to_drop
870 .iter()
871 .map(|(_, gid)| *gid)
872 .chain(tables_to_drop.iter().map(|(_, gid)| *gid))
873 .chain(indexes_to_drop.iter().map(|(_, gid)| *gid))
874 .chain(view_gids_to_drop.iter().copied())
875 .collect();
876
877 for (sink_id, sink) in &self.active_compute_sinks {
880 let cluster_id = sink.cluster_id();
881 if let Some(id) = sink
882 .depends_on()
883 .iter()
884 .find(|id| readable_collections_to_drop.contains(id))
885 {
886 let name = dropped_item_names
887 .get(id)
888 .cloned()
889 .expect("missing relation name");
890 active_compute_sinks_to_drop.insert(
891 *sink_id,
892 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Relation {
893 name,
894 }),
895 );
896 } else if clusters_to_drop.contains(&cluster_id) {
897 let name = dropped_cluster_names
898 .get(&cluster_id)
899 .cloned()
900 .expect("missing cluster name");
901 active_compute_sinks_to_drop.insert(
902 *sink_id,
903 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Cluster {
904 name,
905 }),
906 );
907 }
908 }
909
910 for (uuid, pending_peek) in &self.pending_peeks {
912 if let Some(id) = pending_peek
913 .depends_on
914 .iter()
915 .find(|id| readable_collections_to_drop.contains(id))
916 {
917 let name = dropped_item_names
918 .get(id)
919 .cloned()
920 .expect("missing relation name");
921 peeks_to_drop.push((DroppedDependency::Relation { name }, uuid.clone()));
922 } else if clusters_to_drop.contains(&pending_peek.cluster_id) {
923 let name = dropped_cluster_names
924 .get(&pending_peek.cluster_id)
925 .cloned()
926 .expect("missing cluster name");
927 peeks_to_drop.push((DroppedDependency::Cluster { name }, uuid.clone()));
928 }
929 }
930
931 for (conn_id, pending_copy) in &self.active_copies {
933 let dropping_table = tables_to_drop
934 .iter()
935 .any(|(item_id, _gid)| pending_copy.table_id == *item_id);
936 let dropping_cluster = clusters_to_drop.contains(&pending_copy.cluster_id);
937
938 if dropping_table || dropping_cluster {
939 copies_to_drop.push(conn_id.clone());
940 }
941 }
942
943 let storage_gids_to_drop: BTreeSet<_> = sources_to_drop
944 .iter()
945 .map(|(_id, gid)| gid)
946 .chain(storage_sink_gids_to_drop.iter())
947 .chain(tables_to_drop.iter().map(|(_id, gid)| gid))
948 .copied()
949 .collect();
950 let compute_gids_to_drop: Vec<_> = indexes_to_drop
951 .iter()
952 .chain(compute_sinks_to_drop.iter())
953 .copied()
954 .collect();
955
956 let mut timeline_id_bundles = BTreeMap::new();
962
963 for (timeline, TimelineState { read_holds, .. }) in &self.global_timelines {
964 let mut id_bundle = CollectionIdBundle::default();
965
966 for storage_id in read_holds.storage_ids() {
967 if storage_gids_to_drop.contains(&storage_id) {
968 id_bundle.storage_ids.insert(storage_id);
969 }
970 }
971
972 for (instance_id, id) in read_holds.compute_ids() {
973 if compute_gids_to_drop.contains(&(instance_id, id))
974 || clusters_to_drop.contains(&instance_id)
975 {
976 id_bundle
977 .compute_ids
978 .entry(instance_id)
979 .or_default()
980 .insert(id);
981 }
982 }
983
984 timeline_id_bundles.insert(timeline.clone(), id_bundle);
985 }
986
987 let mut timeline_associations = BTreeMap::new();
988 for (timeline, id_bundle) in timeline_id_bundles.into_iter() {
989 let TimelineState { read_holds, .. } = self
990 .global_timelines
991 .get(&timeline)
992 .expect("all timelines have a timestamp oracle");
993
994 let empty = read_holds.id_bundle().difference(&id_bundle).is_empty();
995 timeline_associations.insert(timeline, (empty, id_bundle));
996 }
997
998 let _: () = async {
1001 if !timeline_associations.is_empty() {
1002 for (timeline, (should_be_empty, id_bundle)) in timeline_associations {
1003 let became_empty =
1004 self.remove_resources_associated_with_timeline(timeline, id_bundle);
1005 assert_eq!(should_be_empty, became_empty, "emptiness did not match!");
1006 }
1007 }
1008
1009 if !tables_to_drop.is_empty() {
1016 self.drop_tables(tables_to_drop.into_iter().collect_vec())
1017 .await;
1018 }
1019
1020 if !sources_to_drop.is_empty() {
1021 self.drop_sources(sources_to_drop);
1022 }
1023
1024 if !storage_sink_gids_to_drop.is_empty() {
1025 self.drop_storage_sinks(storage_sink_gids_to_drop);
1026 }
1027
1028 if !active_compute_sinks_to_drop.is_empty() {
1029 let retire_notify = self
1030 .retire_compute_sinks(active_compute_sinks_to_drop)
1031 .await;
1032 if let Some(ctx) = ctx {
1033 ctx.delay_response_until(retire_notify);
1034 }
1035 }
1036
1037 if !peeks_to_drop.is_empty() {
1038 for (dep, uuid) in peeks_to_drop {
1039 if let Some(pending_peek) = self.remove_pending_peek(&uuid) {
1040 let cancel_reason = PeekResponse::Error(PeekError::unstructured(
1041 dep.query_terminated_error(),
1042 ));
1043 self.controller
1044 .compute
1045 .cancel_peek(pending_peek.cluster_id, uuid, cancel_reason)
1046 .unwrap_or_terminate("unable to cancel peek");
1047 self.retire_execution(
1048 StatementEndedExecutionReason::Canceled,
1049 pending_peek.ctx_extra.defuse(),
1050 );
1051 }
1052 }
1053 }
1054
1055 if !copies_to_drop.is_empty() {
1056 for conn_id in copies_to_drop {
1057 self.cancel_pending_copy(&conn_id);
1058 }
1059 }
1060
1061 if !compute_gids_to_drop.is_empty() {
1062 self.drop_compute_collections(compute_gids_to_drop);
1063 }
1064
1065 if !vpc_endpoints_to_drop.is_empty() {
1066 self.drop_vpc_endpoints_in_background(vpc_endpoints_to_drop)
1067 }
1068
1069 let clusters_losing_replicas: BTreeSet<_> = cluster_replicas_to_drop
1070 .iter()
1071 .map(|(cluster_id, _)| *cluster_id)
1072 .collect();
1073 if !cluster_replicas_to_drop.is_empty() {
1074 fail::fail_point!("after_catalog_drop_replica");
1075
1076 for (cluster_id, replica_id) in cluster_replicas_to_drop {
1077 self.drop_replica(cluster_id, replica_id);
1078 }
1079 }
1080 if !clusters_to_drop.is_empty() {
1081 for cluster_id in &clusters_to_drop {
1082 self.controller.drop_cluster(*cluster_id);
1083 }
1084 }
1085 for cluster_id in clusters_losing_replicas.into_iter().chain(clusters_to_drop) {
1089 let has_replicas = self
1090 .catalog()
1091 .try_get_cluster(cluster_id)
1092 .is_some_and(|cluster| cluster.replicas().next().is_some());
1093 if !has_replicas {
1094 self.metrics.by_cluster.remove_cluster(cluster_id);
1095 }
1096 }
1097
1098 task::spawn(|| "drop_replication_slots_and_secrets", {
1106 let ssh_tunnel_manager = self.connection_context().ssh_tunnel_manager.clone();
1107 let caching_secrets_reader = self.caching_secrets_reader.clone();
1108 let secrets_controller = Arc::clone(&self.secrets_controller);
1109 let secrets_reader = Arc::clone(self.secrets_reader());
1110 let storage_config = self.controller.storage.config().clone();
1111
1112 async move {
1113 for (connection, replication_slot_name) in replication_slots_to_drop {
1114 tracing::info!(?replication_slot_name, "dropping replication slot");
1115
1116 let result: Result<(), anyhow::Error> = Retry::default()
1122 .max_duration(Duration::from_secs(60))
1123 .retry_async(|_state| async {
1124 let config = connection
1125 .config(&secrets_reader, &storage_config, InTask::No)
1126 .await
1127 .map_err(|e| {
1128 anyhow::anyhow!(
1129 "error creating Postgres client for \
1130 dropping acquired slots: {}",
1131 e.display_with_causes()
1132 )
1133 })?;
1134
1135 mz_postgres_util::drop_replication_slots(
1136 &ssh_tunnel_manager,
1137 config.clone(),
1138 &[(&replication_slot_name, true)],
1139 )
1140 .await?;
1141
1142 Ok(())
1143 })
1144 .await;
1145
1146 if let Err(err) = result {
1147 tracing::warn!(
1148 ?replication_slot_name,
1149 ?err,
1150 "failed to drop replication slot"
1151 );
1152 }
1153 }
1154
1155 fail_point!("drop_secrets");
1163 for secret in secrets_to_drop {
1164 if let Err(e) = secrets_controller.delete(secret).await {
1165 warn!("Dropping secrets has encountered an error: {}", e);
1166 } else {
1167 caching_secrets_reader.invalidate(secret);
1168 }
1169 }
1170 }
1171 });
1172 }
1173 .instrument(info_span!(
1174 "coord::apply_catalog_implications_inner::finalize"
1175 ))
1176 .await;
1177
1178 Ok(())
1179 }
1180
1181 #[instrument(level = "debug")]
1182 async fn create_table_collections(
1183 &mut self,
1184 table_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1185 execution_timestamps_to_set: BTreeSet<StatementLoggingId>,
1186 ) -> Result<(), AdapterError> {
1187 let table_ids: Vec<GlobalId> = table_collections_to_create.keys().copied().collect();
1189 let collections = table_collections_to_create.into_iter().collect_vec();
1190
1191 let write_ts = self.get_local_write_ts().await;
1193 let register_ts = write_ts.timestamp;
1194 self.catalog
1195 .advance_upper(write_ts.advance_to)
1196 .await
1197 .unwrap_or_terminate("unable to advance catalog upper");
1198
1199 {
1200 let storage_metadata = self.catalog.state().storage_metadata();
1201 self.controller
1202 .storage
1203 .create_collections(storage_metadata, Some(register_ts), collections)
1204 .await
1205 .unwrap_or_terminate("cannot fail to create collections");
1206 }
1207
1208 let registrations = self
1211 .controller
1212 .storage
1213 .table_registrations(table_ids)
1214 .unwrap_or_terminate("cannot fail to look up table registrations");
1215 let table_ts = if registrations.is_empty() {
1216 self.apply_local_write(register_ts).await;
1218 register_ts
1219 } else {
1220 self.register_tables_via_committer(registrations).await
1221 };
1222
1223 for id in execution_timestamps_to_set {
1224 self.set_statement_execution_timestamp(id, table_ts);
1225 }
1226
1227 Ok(())
1228 }
1229
1230 #[instrument(level = "debug")]
1231 async fn create_source_collections(
1232 &mut self,
1233 source_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1234 ) -> Result<(), AdapterError> {
1235 let storage_metadata = self.catalog.state().storage_metadata();
1236
1237 self.controller
1238 .storage
1239 .create_collections(
1240 storage_metadata,
1241 None, source_collections_to_create.into_iter().collect_vec(),
1243 )
1244 .await
1245 .unwrap_or_terminate("cannot fail to create collections");
1246
1247 Ok(())
1248 }
1249
1250 #[instrument(level = "debug")]
1251 async fn initialize_storage_collections(
1252 &mut self,
1253 storage_policies_to_initialize: BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1254 ) -> Result<(), AdapterError> {
1255 for (compaction_window, global_ids) in storage_policies_to_initialize {
1256 self.initialize_read_policies(
1257 &CollectionIdBundle {
1258 storage_ids: global_ids,
1259 compute_ids: BTreeMap::new(),
1260 },
1261 compaction_window,
1262 )
1263 .await;
1264 }
1265
1266 Ok(())
1267 }
1268
1269 #[instrument(level = "debug")]
1270 async fn handle_create_table(
1271 &self,
1272 ctx: &Option<&mut ExecuteContext>,
1273 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1274 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1275 execution_timestamps_to_set: &mut BTreeSet<StatementLoggingId>,
1276 table_id: CatalogItemId,
1277 table: Table,
1278 ) -> Result<(), AdapterError> {
1279 match &table.data_source {
1283 TableDataSource::TableWrites { defaults: _ } => {
1284 let versions: BTreeMap<_, _> = table
1285 .collection_descs()
1286 .map(|(gid, version, desc)| (version, (gid, desc)))
1287 .collect();
1288 let collection_descs = versions.iter().map(|(_version, (gid, desc))| {
1289 let collection_desc = CollectionDescription::for_table(desc.clone());
1290
1291 (*gid, collection_desc)
1292 });
1293
1294 let compaction_window = table
1295 .custom_logical_compaction_window
1296 .unwrap_or(CompactionWindow::Default);
1297 let ids_to_initialize = storage_policies_to_initialize
1298 .entry(compaction_window)
1299 .or_default();
1300
1301 for (gid, collection_desc) in collection_descs {
1302 storage_collections_to_create.insert(gid, collection_desc);
1303 ids_to_initialize.insert(gid);
1304 }
1305
1306 if let Some(id) = ctx.as_ref().and_then(|ctx| ctx.extra().contents()) {
1307 execution_timestamps_to_set.insert(id);
1308 }
1309 }
1310 TableDataSource::DataSource {
1311 desc: data_source_desc,
1312 timeline,
1313 } => {
1314 match data_source_desc {
1315 DataSourceDesc::IngestionExport {
1316 ingestion_id,
1317 external_reference: _,
1318 details,
1319 data_config,
1320 } => {
1321 let global_ingestion_id =
1322 self.catalog().get_entry(ingestion_id).latest_global_id();
1323
1324 let collection_desc = CollectionDescription {
1325 desc: table.desc.latest(),
1326 data_source: DataSource::IngestionExport {
1327 ingestion_id: global_ingestion_id,
1328 details: details.clone(),
1329 data_config: data_config
1330 .clone()
1331 .into_inline_connection(self.catalog.state()),
1332 },
1333 since: None,
1334 timeline: Some(timeline.clone()),
1335 primary: None,
1336 };
1337
1338 let global_id = table
1339 .global_ids()
1340 .expect_element(|| "subsources cannot have multiple versions");
1341
1342 storage_collections_to_create.insert(global_id, collection_desc);
1343
1344 let read_policies = self
1345 .catalog()
1346 .state()
1347 .source_compaction_windows(vec![table_id]);
1348 for (compaction_window, catalog_ids) in read_policies {
1349 let compaction_ids = storage_policies_to_initialize
1350 .entry(compaction_window)
1351 .or_default();
1352
1353 let gids = catalog_ids
1354 .into_iter()
1355 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1356 .flatten();
1357 compaction_ids.extend(gids);
1358 }
1359 }
1360 DataSourceDesc::Webhook {
1361 validate_using: _,
1362 body_format: _,
1363 headers: _,
1364 cluster_id: _,
1365 } => {
1366 assert_eq!(
1368 table.desc.latest_version(),
1369 RelationVersion::root(),
1370 "found webhook with more than 1 relation version, {:?}",
1371 table.desc
1372 );
1373 let desc = table.desc.latest();
1374
1375 let collection_desc = CollectionDescription {
1376 desc,
1377 data_source: DataSource::Webhook,
1378 since: None,
1379 timeline: Some(timeline.clone()),
1380 primary: None,
1381 };
1382
1383 let global_id = table
1384 .global_ids()
1385 .expect_element(|| "webhooks cannot have multiple versions");
1386
1387 storage_collections_to_create.insert(global_id, collection_desc);
1388
1389 let read_policies = self
1390 .catalog()
1391 .state()
1392 .source_compaction_windows(vec![table_id]);
1393
1394 for (compaction_window, catalog_ids) in read_policies {
1395 let compaction_ids = storage_policies_to_initialize
1396 .entry(compaction_window)
1397 .or_default();
1398
1399 let gids = catalog_ids
1400 .into_iter()
1401 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1402 .flatten();
1403 compaction_ids.extend(gids);
1404 }
1405 }
1406 _ => unreachable!("CREATE TABLE data source got {:?}", data_source_desc),
1407 }
1408 }
1409 }
1410
1411 Ok(())
1412 }
1413
1414 #[instrument(level = "debug")]
1415 async fn handle_alter_table(
1416 &mut self,
1417 catalog_id: CatalogItemId,
1418 prev_table: Table,
1419 new_table: Table,
1420 ) -> Result<(), AdapterError> {
1421 let existing_gid = prev_table.global_id_writes();
1422 let new_gid = new_table.global_id_writes();
1423
1424 if existing_gid == new_gid {
1425 if prev_table.custom_logical_compaction_window
1428 != new_table.custom_logical_compaction_window
1429 {
1430 let new_window = new_table
1431 .custom_logical_compaction_window
1432 .unwrap_or(CompactionWindow::Default);
1433 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
1434 }
1435 return Ok(());
1436 }
1437
1438 let existing_table = crate::CollectionIdBundle {
1442 storage_ids: BTreeSet::from([existing_gid]),
1443 compute_ids: BTreeMap::new(),
1444 };
1445 let existing_table_read_hold = self.acquire_read_holds(&existing_table);
1446
1447 let expected_version = prev_table.desc.latest_version();
1448 let new_version = new_table.desc.latest_version();
1449 let new_desc = new_table
1450 .desc
1451 .at_version(RelationVersionSelector::Specific(new_version));
1452
1453 let write_ts = self.get_local_write_ts().await;
1455 self.catalog
1456 .advance_upper(write_ts.advance_to)
1457 .await
1458 .unwrap_or_terminate("unable to advance catalog upper");
1459
1460 self.controller
1461 .storage
1462 .alter_table_desc(existing_gid, new_gid, new_desc, expected_version)
1463 .await
1464 .unwrap_or_terminate("failed to alter desc of table");
1465
1466 let registrations = self
1468 .controller
1469 .storage
1470 .table_registrations(vec![new_gid])
1471 .unwrap_or_terminate("cannot fail to look up table registrations");
1472 self.register_tables_via_committer(registrations).await;
1473
1474 let compaction_window = new_table
1476 .custom_logical_compaction_window
1477 .unwrap_or(CompactionWindow::Default);
1478 self.initialize_read_policies(
1479 &crate::CollectionIdBundle {
1480 storage_ids: BTreeSet::from([new_gid]),
1481 compute_ids: BTreeMap::new(),
1482 },
1483 compaction_window,
1484 )
1485 .await;
1486
1487 drop(existing_table_read_hold);
1489
1490 Ok(())
1491 }
1492
1493 #[instrument(level = "debug")]
1494 async fn handle_create_source(
1495 &self,
1496 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1497 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1498 item_id: CatalogItemId,
1499 source: Source,
1500 compaction_windows: BTreeMap<CompactionWindow, BTreeSet<CatalogItemId>>,
1501 ) -> Result<(), AdapterError> {
1502 let data_source = match source.data_source {
1503 DataSourceDesc::Ingestion { desc, cluster_id } => {
1504 let desc = desc.into_inline_connection(self.catalog().state());
1505 let item_global_id = self.catalog().get_entry(&item_id).latest_global_id();
1506
1507 let ingestion = mz_storage_types::sources::IngestionDescription::new(
1508 desc,
1509 cluster_id,
1510 item_global_id,
1511 );
1512
1513 DataSource::Ingestion(ingestion)
1514 }
1515 DataSourceDesc::OldSyntaxIngestion {
1516 desc,
1517 progress_subsource,
1518 data_config,
1519 details,
1520 cluster_id,
1521 } => {
1522 let desc = desc.into_inline_connection(self.catalog().state());
1523 let data_config = data_config.into_inline_connection(self.catalog().state());
1524
1525 let progress_subsource = self
1528 .catalog()
1529 .get_entry(&progress_subsource)
1530 .latest_global_id();
1531
1532 let mut ingestion = mz_storage_types::sources::IngestionDescription::new(
1533 desc,
1534 cluster_id,
1535 progress_subsource,
1536 );
1537
1538 let legacy_export = SourceExport {
1539 storage_metadata: (),
1540 data_config,
1541 details,
1542 };
1543
1544 ingestion
1545 .source_exports
1546 .insert(source.global_id, legacy_export);
1547
1548 DataSource::Ingestion(ingestion)
1549 }
1550 DataSourceDesc::IngestionExport {
1551 ingestion_id,
1552 external_reference: _,
1553 details,
1554 data_config,
1555 } => {
1556 let ingestion_id = self.catalog().get_entry(&ingestion_id).latest_global_id();
1559
1560 DataSource::IngestionExport {
1561 ingestion_id,
1562 details,
1563 data_config: data_config.into_inline_connection(self.catalog().state()),
1564 }
1565 }
1566 DataSourceDesc::Progress => DataSource::Progress,
1567 DataSourceDesc::Webhook { .. } => DataSource::Webhook,
1568 DataSourceDesc::Introspection(_) | DataSourceDesc::Catalog => {
1569 unreachable!("cannot create sources with internal data sources")
1570 }
1571 };
1572
1573 storage_collections_to_create.insert(
1574 source.global_id,
1575 CollectionDescription {
1576 desc: source.desc.clone(),
1577 data_source,
1578 timeline: Some(source.timeline),
1579 since: None,
1580 primary: None,
1581 },
1582 );
1583
1584 for (compaction_window, catalog_ids) in compaction_windows {
1586 let compaction_ids = storage_policies_to_initialize
1587 .entry(compaction_window)
1588 .or_default();
1589
1590 let gids = catalog_ids
1591 .into_iter()
1592 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1593 .flatten();
1594 compaction_ids.extend(gids);
1595 }
1596
1597 Ok(())
1598 }
1599
1600 #[instrument(level = "debug")]
1607 fn handle_alter_connection(
1608 &self,
1609 connection_id: CatalogItemId,
1610 connection: Connection,
1611 vpc_endpoints_to_create: &mut Vec<(CatalogItemId, VpcEndpointConfig)>,
1612 source_connections_to_alter: &mut BTreeMap<
1613 GlobalId,
1614 GenericSourceConnection<InlinedConnection>,
1615 >,
1616 sink_connections_to_alter: &mut BTreeMap<GlobalId, StorageSinkConnection>,
1617 source_export_data_configs_to_alter: &mut BTreeMap<GlobalId, SourceExportDataConfig>,
1618 ) {
1619 use std::collections::VecDeque;
1620
1621 if let ConnectionDetails::AwsPrivatelink(ref privatelink) = connection.details {
1623 let spec = VpcEndpointConfig {
1624 aws_service_name: privatelink.service_name.to_owned(),
1625 availability_zone_ids: privatelink.availability_zones.to_owned(),
1626 };
1627 vpc_endpoints_to_create.push((connection_id, spec));
1628 }
1629
1630 let mut connections_to_process = VecDeque::new();
1634 connections_to_process.push_front(connection_id.clone());
1635
1636 while let Some(id) = connections_to_process.pop_front() {
1637 for dependent_id in self.catalog().get_entry(&id).used_by() {
1638 let dependent_entry = self.catalog().get_entry(dependent_id);
1639 match dependent_entry.item() {
1640 CatalogItem::Connection(_) => {
1641 connections_to_process.push_back(*dependent_id);
1645 }
1646 CatalogItem::Source(source) => {
1647 let desc = match &dependent_entry
1648 .source()
1649 .expect("known to be source")
1650 .data_source
1651 {
1652 DataSourceDesc::Ingestion { desc, .. }
1653 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1654 desc.clone().into_inline_connection(self.catalog().state())
1655 }
1656 DataSourceDesc::IngestionExport { .. }
1657 | DataSourceDesc::Introspection(_)
1658 | DataSourceDesc::Progress
1659 | DataSourceDesc::Webhook { .. }
1660 | DataSourceDesc::Catalog => {
1661 continue;
1663 }
1664 };
1665
1666 source_connections_to_alter.insert(source.global_id, desc.connection);
1667 }
1668 CatalogItem::Sink(sink) => {
1669 let export = dependent_entry.sink().expect("known to be sink");
1670 sink_connections_to_alter.insert(
1671 sink.global_id,
1672 export
1673 .connection
1674 .clone()
1675 .into_inline_connection(self.catalog().state()),
1676 );
1677 }
1678 CatalogItem::Table(table) => {
1679 if let Some((_, _, _, export_data_config)) =
1683 dependent_entry.source_export_details()
1684 {
1685 let data_config = export_data_config.clone();
1686 source_export_data_configs_to_alter.insert(
1687 table.global_id_writes(),
1688 data_config.into_inline_connection(self.catalog().state()),
1689 );
1690 }
1691 }
1692 CatalogItem::Log(_)
1693 | CatalogItem::View(_)
1694 | CatalogItem::MaterializedView(_)
1695 | CatalogItem::Index(_)
1696 | CatalogItem::Type(_)
1697 | CatalogItem::Func(_)
1698 | CatalogItem::Secret(_)
1699 | CatalogItem::MetricSink(_) => {
1700 }
1703 }
1704 }
1705 }
1706 }
1707
1708 async fn handle_create_cluster_replica(
1709 &mut self,
1710 cluster_id: ClusterId,
1711 replica_id: ReplicaId,
1712 role: ClusterRole,
1713 cluster_name: String,
1714 replica_name: String,
1715 replica_config: ReplicaConfig,
1716 ) {
1717 let enable_worker_core_affinity =
1718 self.catalog().system_config().enable_worker_core_affinity();
1719 let enable_storage_introspection_logs = self
1720 .catalog()
1721 .system_config()
1722 .enable_storage_introspection_logs();
1723
1724 self.controller
1731 .create_replica(
1732 cluster_id,
1733 replica_id,
1734 cluster_name,
1735 replica_name,
1736 role,
1737 replica_config,
1738 enable_worker_core_affinity,
1739 enable_storage_introspection_logs,
1740 )
1741 .expect("creating replicas must not fail");
1742
1743 self.install_introspection_subscribes(cluster_id, replica_id)
1744 .await;
1745 self.install_metric_sinks(cluster_id, replica_id).await;
1746 }
1747}
1748
1749#[derive(Debug, Clone)]
1755enum CatalogImplication {
1756 None,
1757 Table(CatalogImplicationKind<Table>),
1758 Source(CatalogImplicationKind<(Source, Option<GenericSourceConnection>)>),
1759 Sink(CatalogImplicationKind<Sink>),
1760 Index(CatalogImplicationKind<Index>),
1761 MetricSink(CatalogImplicationKind<MetricSink>),
1762 MaterializedView(CatalogImplicationKind<MaterializedView>),
1763 View(CatalogImplicationKind<View>),
1764 Secret(CatalogImplicationKind<Secret>),
1765 Connection(CatalogImplicationKind<Connection>),
1766 Cluster(CatalogImplicationKind<Cluster>),
1767 ClusterReplica(CatalogImplicationKind<ClusterReplica>),
1768}
1769
1770#[derive(Debug, Clone)]
1771enum CatalogImplicationKind<T> {
1772 None,
1774 Added(T),
1776 Dropped(T, String),
1778 Altered { prev: T, new: T },
1780}
1781
1782impl<T: Clone> CatalogImplicationKind<T> {
1783 fn transition(&mut self, item: T, name: Option<String>, diff: StateDiff) -> Result<(), String> {
1786 use CatalogImplicationKind::*;
1787 use StateDiff::*;
1788
1789 let new_state = match (&*self, diff) {
1790 (None, Addition) => Added(item),
1792 (None, Retraction) => Dropped(item, name.unwrap_or_else(|| "<unknown>".to_string())),
1793
1794 (Added(existing), Retraction) => {
1796 Altered {
1798 prev: item,
1799 new: existing.clone(),
1800 }
1801 }
1802 (Added(_), Addition) => {
1803 return Err("Cannot add an already added object".to_string());
1804 }
1805
1806 (Dropped(existing, _), Addition) => {
1808 Altered {
1810 prev: existing.clone(),
1811 new: item,
1812 }
1813 }
1814 (Dropped(_, _), Retraction) => {
1815 return Err("Cannot drop an already dropped object".to_string());
1816 }
1817
1818 (Altered { .. }, _) => {
1820 return Err(format!(
1821 "Cannot apply {:?} to an object in Altered state",
1822 diff
1823 ));
1824 }
1825 };
1826
1827 *self = new_state;
1828 Ok(())
1829 }
1830}
1831
1832macro_rules! impl_absorb_method {
1834 (
1835 $method_name:ident,
1836 $variant:ident,
1837 $item_type:ty
1838 ) => {
1839 fn $method_name(
1840 &mut self,
1841 item: $item_type,
1842 parsed_full_name: Option<String>,
1843 diff: StateDiff,
1844 ) {
1845 let state = match self {
1846 CatalogImplication::$variant(state) => state,
1847 CatalogImplication::None => {
1848 *self = CatalogImplication::$variant(CatalogImplicationKind::None);
1849 match self {
1850 CatalogImplication::$variant(state) => state,
1851 _ => unreachable!(),
1852 }
1853 }
1854 _ => {
1855 panic!(
1856 "Unexpected command type for {:?}: {} {:?}",
1857 self,
1858 stringify!($variant),
1859 diff,
1860 );
1861 }
1862 };
1863
1864 if let Err(e) = state.transition(item, parsed_full_name, diff) {
1865 panic!(
1866 "Invalid state transition for {}: {}",
1867 stringify!($variant),
1868 e
1869 );
1870 }
1871 }
1872 };
1873}
1874
1875impl CatalogImplication {
1876 fn absorb(&mut self, catalog_update: ParsedStateUpdate) {
1879 match catalog_update.kind {
1880 ParsedStateUpdateKind::Item {
1881 durable_item: _,
1882 parsed_item,
1883 connection,
1884 parsed_full_name,
1885 } => match parsed_item {
1886 CatalogItem::Table(table) => {
1887 self.absorb_table(table, Some(parsed_full_name), catalog_update.diff)
1888 }
1889 CatalogItem::Source(source) => {
1890 self.absorb_source(
1891 (source, connection),
1892 Some(parsed_full_name),
1893 catalog_update.diff,
1894 );
1895 }
1896 CatalogItem::Sink(sink) => {
1897 self.absorb_sink(sink, Some(parsed_full_name), catalog_update.diff);
1898 }
1899 CatalogItem::Index(index) => {
1900 self.absorb_index(index, Some(parsed_full_name), catalog_update.diff);
1901 }
1902 CatalogItem::MaterializedView(mv) => {
1903 self.absorb_materialized_view(mv, Some(parsed_full_name), catalog_update.diff);
1904 }
1905 CatalogItem::View(view) => {
1906 self.absorb_view(view, Some(parsed_full_name), catalog_update.diff);
1907 }
1908
1909 CatalogItem::Secret(secret) => {
1910 self.absorb_secret(secret, None, catalog_update.diff);
1911 }
1912 CatalogItem::Connection(connection) => {
1913 self.absorb_connection(connection, None, catalog_update.diff);
1914 }
1915 CatalogItem::MetricSink(metric_sink) => {
1916 self.absorb_metric_sink(
1917 metric_sink,
1918 Some(parsed_full_name),
1919 catalog_update.diff,
1920 );
1921 }
1922 CatalogItem::Log(_) => {}
1923 CatalogItem::Type(_) => {}
1924 CatalogItem::Func(_) => {}
1925 },
1926 ParsedStateUpdateKind::Cluster {
1927 durable_cluster: _,
1928 parsed_cluster,
1929 } => {
1930 let name = parsed_cluster.name.clone();
1931 self.absorb_cluster(parsed_cluster, Some(name), catalog_update.diff);
1932 }
1933 ParsedStateUpdateKind::ClusterReplica {
1934 durable_cluster_replica: _,
1935 parsed_cluster_replica,
1936 } => {
1937 let name = parsed_cluster_replica.name.clone();
1938 self.absorb_cluster_replica(
1939 parsed_cluster_replica,
1940 Some(name),
1941 catalog_update.diff,
1942 );
1943 }
1944 ParsedStateUpdateKind::IntrospectionSourceIndex { .. } => {
1945 unreachable!("IntrospectionSourceIndex should not be passed to absorb");
1949 }
1950 ParsedStateUpdateKind::ReplicaSystemConfiguration { .. } => {
1951 unreachable!("ReplicaSystemConfiguration should not be passed to absorb");
1954 }
1955 ParsedStateUpdateKind::SystemConfiguration { .. } => {
1956 unreachable!("SystemConfiguration should not be passed to absorb");
1959 }
1960 }
1961 }
1962
1963 impl_absorb_method!(absorb_table, Table, Table);
1964 impl_absorb_method!(
1965 absorb_source,
1966 Source,
1967 (Source, Option<GenericSourceConnection>)
1968 );
1969 impl_absorb_method!(absorb_sink, Sink, Sink);
1970 impl_absorb_method!(absorb_index, Index, Index);
1971 impl_absorb_method!(absorb_metric_sink, MetricSink, MetricSink);
1972 impl_absorb_method!(absorb_materialized_view, MaterializedView, MaterializedView);
1973 impl_absorb_method!(absorb_view, View, View);
1974
1975 impl_absorb_method!(absorb_secret, Secret, Secret);
1976 impl_absorb_method!(absorb_connection, Connection, Connection);
1977
1978 impl_absorb_method!(absorb_cluster, Cluster, Cluster);
1979 impl_absorb_method!(absorb_cluster_replica, ClusterReplica, ClusterReplica);
1980}
1981
1982fn partition_cluster_replica_drops(
1983 clusters_with_creates: &BTreeSet<ClusterId>,
1984 drops: Vec<(ClusterId, ReplicaId)>,
1985) -> (Vec<(ClusterId, ReplicaId)>, Vec<(ClusterId, ReplicaId)>) {
1986 drops
1987 .into_iter()
1988 .partition(|(cluster_id, _)| clusters_with_creates.contains(cluster_id))
1989}
1990
1991#[cfg(test)]
1992mod tests {
1993 use super::*;
1994 use mz_repr::{GlobalId, RelationDesc, RelationVersion, VersionedRelationDesc};
1995 use mz_sql::names::ResolvedIds;
1996 use std::collections::BTreeMap;
1997
1998 fn create_test_table(name: &str) -> Table {
1999 Table {
2000 desc: VersionedRelationDesc::new(
2001 RelationDesc::builder()
2002 .with_column(name, mz_repr::SqlScalarType::String.nullable(false))
2003 .finish(),
2004 ),
2005 create_sql: None,
2006 collections: BTreeMap::from([(RelationVersion::root(), GlobalId::System(1))]),
2007 conn_id: None,
2008 resolved_ids: ResolvedIds::empty(),
2009 custom_logical_compaction_window: None,
2010 is_retained_metrics_object: false,
2011 data_source: TableDataSource::TableWrites { defaults: vec![] },
2012 }
2013 }
2014
2015 #[mz_ore::test]
2016 fn mixed_replica_drops_are_applied_before_creates() {
2017 let c1 = ClusterId::user(1).expect("valid id");
2018 let c2 = ClusterId::user(2).expect("valid id");
2019 let r1 = ReplicaId::User(1);
2020 let r2 = ReplicaId::User(2);
2021 let creates = BTreeSet::from([c1]);
2022
2023 let (before_creates, deferred) =
2024 partition_cluster_replica_drops(&creates, vec![(c1, r1), (c2, r2)]);
2025
2026 assert_eq!(before_creates, vec![(c1, r1)]);
2027 assert_eq!(deferred, vec![(c2, r2)]);
2028 }
2029
2030 #[mz_ore::test]
2031 fn test_item_state_transitions() {
2032 let mut state = CatalogImplicationKind::None;
2034 assert!(
2035 state
2036 .transition("item1".to_string(), None, StateDiff::Addition)
2037 .is_ok()
2038 );
2039 assert!(matches!(state, CatalogImplicationKind::Added(_)));
2040
2041 let mut state = CatalogImplicationKind::Added("new_item".to_string());
2043 assert!(
2044 state
2045 .transition("old_item".to_string(), None, StateDiff::Retraction)
2046 .is_ok()
2047 );
2048 match &state {
2049 CatalogImplicationKind::Altered { prev, new } => {
2050 assert_eq!(prev, "old_item");
2052 assert_eq!(new, "new_item");
2054 }
2055 _ => panic!("Expected Altered state"),
2056 }
2057
2058 let mut state = CatalogImplicationKind::None;
2060 assert!(
2061 state
2062 .transition(
2063 "item1".to_string(),
2064 Some("test_name".to_string()),
2065 StateDiff::Retraction
2066 )
2067 .is_ok()
2068 );
2069 assert!(matches!(state, CatalogImplicationKind::Dropped(_, _)));
2070
2071 let mut state = CatalogImplicationKind::Dropped("old_item".to_string(), "name".to_string());
2073 assert!(
2074 state
2075 .transition("new_item".to_string(), None, StateDiff::Addition)
2076 .is_ok()
2077 );
2078 match &state {
2079 CatalogImplicationKind::Altered { prev, new } => {
2080 assert_eq!(prev, "old_item");
2082 assert_eq!(new, "new_item");
2084 }
2085 _ => panic!("Expected Altered state"),
2086 }
2087
2088 let mut state = CatalogImplicationKind::Added("item".to_string());
2090 assert!(
2091 state
2092 .transition("item2".to_string(), None, StateDiff::Addition)
2093 .is_err()
2094 );
2095
2096 let mut state = CatalogImplicationKind::Dropped("item".to_string(), "name".to_string());
2097 assert!(
2098 state
2099 .transition("item2".to_string(), None, StateDiff::Retraction)
2100 .is_err()
2101 );
2102 }
2103
2104 #[mz_ore::test]
2105 fn test_table_absorb_state_machine() {
2106 let table1 = create_test_table("table1");
2107 let table2 = create_test_table("table2");
2108
2109 let mut cmd = CatalogImplication::None;
2111 cmd.absorb_table(
2112 table1.clone(),
2113 Some("schema.table1".to_string()),
2114 StateDiff::Addition,
2115 );
2116 match &cmd {
2118 CatalogImplication::Table(state) => match state {
2119 CatalogImplicationKind::Added(t) => {
2120 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity())
2121 }
2122 _ => panic!("Expected Added state"),
2123 },
2124 _ => panic!("Expected Table command"),
2125 }
2126
2127 cmd.absorb_table(
2131 table2.clone(),
2132 Some("schema.table2".to_string()),
2133 StateDiff::Retraction,
2134 );
2135 match &cmd {
2136 CatalogImplication::Table(state) => match state {
2137 CatalogImplicationKind::Altered { prev, new } => {
2138 assert_eq!(prev.desc.latest().arity(), table2.desc.latest().arity());
2140 assert_eq!(new.desc.latest().arity(), table1.desc.latest().arity());
2141 }
2142 _ => panic!("Expected Altered state"),
2143 },
2144 _ => panic!("Expected Table command"),
2145 }
2146
2147 let mut cmd = CatalogImplication::None;
2149 cmd.absorb_table(
2150 table1.clone(),
2151 Some("schema.table1".to_string()),
2152 StateDiff::Retraction,
2153 );
2154 match &cmd {
2155 CatalogImplication::Table(state) => match state {
2156 CatalogImplicationKind::Dropped(t, name) => {
2157 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity());
2158 assert_eq!(name, "schema.table1");
2159 }
2160 _ => panic!("Expected Dropped state"),
2161 },
2162 _ => panic!("Expected Table command"),
2163 }
2164
2165 cmd.absorb_table(
2167 table2.clone(),
2168 Some("schema.table2".to_string()),
2169 StateDiff::Addition,
2170 );
2171 match &cmd {
2172 CatalogImplication::Table(state) => match state {
2173 CatalogImplicationKind::Altered { prev, new } => {
2174 assert_eq!(prev.desc.latest().arity(), table1.desc.latest().arity());
2176 assert_eq!(new.desc.latest().arity(), table2.desc.latest().arity());
2177 }
2178 _ => panic!("Expected Altered state"),
2179 },
2180 _ => panic!("Expected Table command"),
2181 }
2182 }
2183
2184 #[mz_ore::test]
2185 #[should_panic(expected = "Cannot add an already added object")]
2186 fn test_invalid_double_add() {
2187 let table = create_test_table("table");
2188 let mut cmd = CatalogImplication::None;
2189
2190 cmd.absorb_table(
2192 table.clone(),
2193 Some("schema.table".to_string()),
2194 StateDiff::Addition,
2195 );
2196
2197 cmd.absorb_table(
2199 table.clone(),
2200 Some("schema.table".to_string()),
2201 StateDiff::Addition,
2202 );
2203 }
2204
2205 #[mz_ore::test]
2206 #[should_panic(expected = "Cannot drop an already dropped object")]
2207 fn test_invalid_double_drop() {
2208 let table = create_test_table("table");
2209 let mut cmd = CatalogImplication::None;
2210
2211 cmd.absorb_table(
2213 table.clone(),
2214 Some("schema.table".to_string()),
2215 StateDiff::Retraction,
2216 );
2217
2218 cmd.absorb_table(
2220 table.clone(),
2221 Some("schema.table".to_string()),
2222 StateDiff::Retraction,
2223 );
2224 }
2225}