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::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 active_compute_sinks_to_drop = BTreeMap::new();
258 let mut peeks_to_drop = vec![];
259 let mut copies_to_drop = vec![];
260
261 let mut dropped_item_names: BTreeMap<GlobalId, String> = BTreeMap::new();
263 let mut dropped_cluster_names: BTreeMap<ClusterId, String> = BTreeMap::new();
264
265 let mut table_collections_to_create = BTreeMap::new();
268 let mut source_collections_to_create = BTreeMap::new();
269 let mut storage_policies_to_initialize = BTreeMap::new();
270 let mut execution_timestamps_to_set = BTreeSet::new();
271 let mut vpc_endpoints_to_create: Vec<(CatalogItemId, VpcEndpointConfig)> = vec![];
272
273 let mut source_gids_to_keep = BTreeSet::new();
276
277 let mut source_connections_to_alter: BTreeMap<
279 GlobalId,
280 GenericSourceConnection<InlinedConnection>,
281 > = BTreeMap::new();
282 let mut sink_connections_to_alter: BTreeMap<GlobalId, StorageSinkConnection> =
283 BTreeMap::new();
284 let mut source_export_data_configs_to_alter: BTreeMap<GlobalId, SourceExportDataConfig> =
285 BTreeMap::new();
286 let mut source_descs_to_alter: BTreeMap<GlobalId, SourceDesc> = BTreeMap::new();
287
288 for (catalog_id, implication) in implications {
295 tracing::trace!(?implication, "have to apply catalog implication");
296
297 match implication {
298 CatalogImplication::Table(CatalogImplicationKind::Added(table)) => {
299 self.handle_create_table(
300 &ctx,
301 &mut table_collections_to_create,
302 &mut storage_policies_to_initialize,
303 &mut execution_timestamps_to_set,
304 catalog_id,
305 table.clone(),
306 )
307 .await?
308 }
309 CatalogImplication::Table(CatalogImplicationKind::Altered {
310 prev: prev_table,
311 new: new_table,
312 }) => {
313 self.handle_alter_table(catalog_id, prev_table, new_table)
314 .await?
315 }
316
317 CatalogImplication::Table(CatalogImplicationKind::Dropped(table, full_name)) => {
318 let global_ids = table.global_ids();
319 for global_id in global_ids {
320 tables_to_drop.insert((catalog_id, global_id));
321 dropped_item_names.insert(global_id, full_name.clone());
322 }
323 }
324 CatalogImplication::Source(CatalogImplicationKind::Added((
325 source,
326 _connection,
327 ))) => {
328 let compaction_windows = self
333 .catalog()
334 .state()
335 .source_compaction_windows(vec![catalog_id]);
336
337 self.handle_create_source(
338 &mut source_collections_to_create,
339 &mut storage_policies_to_initialize,
340 catalog_id,
341 source,
342 compaction_windows,
343 )
344 .await?
345 }
346 CatalogImplication::Source(CatalogImplicationKind::Altered {
347 prev: (prev_source, _prev_connection),
348 new: (new_source, new_connection),
349 }) => {
350 if prev_source.custom_logical_compaction_window
351 != new_source.custom_logical_compaction_window
352 {
353 let new_window = new_source
354 .custom_logical_compaction_window
355 .unwrap_or(CompactionWindow::Default);
356 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
357 }
358 match (&prev_source.data_source, &new_source.data_source) {
359 (
360 DataSourceDesc::Ingestion {
361 desc: prev_desc, ..
362 }
363 | DataSourceDesc::OldSyntaxIngestion {
364 desc: prev_desc, ..
365 },
366 DataSourceDesc::Ingestion { desc: new_desc, .. }
367 | DataSourceDesc::OldSyntaxIngestion { desc: new_desc, .. },
368 ) => {
369 if prev_desc != new_desc {
370 let inlined_connection = new_connection
371 .expect("ingestion source should have inlined connection");
372 let inlined_desc = SourceDesc {
373 connection: inlined_connection,
374 timestamp_interval: new_desc.timestamp_interval,
375 };
376 source_descs_to_alter.insert(new_source.global_id, inlined_desc);
377 }
378 }
379 _ => {}
380 }
381 }
382 CatalogImplication::Source(CatalogImplicationKind::Dropped(
383 (source, connection),
384 full_name,
385 )) => {
386 let global_id = source.global_id();
387 sources_to_drop.push((catalog_id, global_id));
388 dropped_item_names.insert(global_id, full_name);
389
390 if let DataSourceDesc::Ingestion { desc, .. }
391 | DataSourceDesc::OldSyntaxIngestion { desc, .. } = &source.data_source
392 {
393 match &desc.connection {
394 GenericSourceConnection::Postgres(_referenced_conn) => {
395 let inline_conn = connection.expect("missing inlined connection");
396
397 let pg_conn = match inline_conn {
398 GenericSourceConnection::Postgres(pg_conn) => pg_conn,
399 other => {
400 panic!("expected postgres connection, got: {:?}", other)
401 }
402 };
403 let pending_drop = (
404 pg_conn.connection.clone(),
405 pg_conn.publication_details.slot.clone(),
406 );
407 replication_slots_to_drop.push(pending_drop);
408 }
409 _ => {}
410 }
411 }
412 }
413 CatalogImplication::Sink(CatalogImplicationKind::Added(sink)) => {
414 tracing::debug!(?sink, "not handling AddSink in here yet");
415 }
416 CatalogImplication::Sink(CatalogImplicationKind::Altered {
417 prev: prev_sink,
418 new: new_sink,
419 }) => {
420 tracing::debug!(?prev_sink, ?new_sink, "not handling AlterSink in here yet");
421 }
422 CatalogImplication::Sink(CatalogImplicationKind::Dropped(sink, full_name)) => {
423 storage_sink_gids_to_drop.push(sink.global_id());
424 dropped_item_names.insert(sink.global_id(), full_name);
425 }
426 CatalogImplication::Index(CatalogImplicationKind::Added(index)) => {
427 tracing::debug!(?index, "not handling AddIndex in here yet");
428 }
429 CatalogImplication::Index(CatalogImplicationKind::Altered {
430 prev: prev_index,
431 new: new_index,
432 }) => {
433 if prev_index.custom_logical_compaction_window
434 != new_index.custom_logical_compaction_window
435 {
436 let new_window = new_index
437 .custom_logical_compaction_window
438 .unwrap_or(CompactionWindow::Default);
439 self.update_compute_read_policy(
440 new_index.cluster_id,
441 catalog_id,
442 new_window.into(),
443 );
444 }
445 }
446 CatalogImplication::Index(CatalogImplicationKind::Dropped(index, full_name)) => {
447 indexes_to_drop.push((index.cluster_id, index.global_id()));
448 dropped_item_names.insert(index.global_id(), full_name);
449 }
450 CatalogImplication::MetricSink(CatalogImplicationKind::Added(_metric_sink)) => {
451 }
455 CatalogImplication::MetricSink(CatalogImplicationKind::Altered { .. }) => {
456 }
458 CatalogImplication::MetricSink(CatalogImplicationKind::Dropped(
459 metric_sink,
460 full_name,
461 )) => {
462 compute_sinks_to_drop.push((metric_sink.cluster_id, metric_sink.global_id));
465 dropped_item_names.insert(metric_sink.global_id, full_name);
466 }
467 CatalogImplication::MaterializedView(CatalogImplicationKind::Added(mv)) => {
468 tracing::debug!(?mv, "not handling AddMaterializedView in here yet");
469 }
470 CatalogImplication::MaterializedView(CatalogImplicationKind::Altered {
471 prev: prev_mv,
472 new: new_mv,
473 }) => {
474 if prev_mv.collections != new_mv.collections {
493 assert_eq!(
496 prev_mv.global_id_writes(),
497 new_mv.global_id_writes(),
498 "unexpected MV Altered implication: prev={prev_mv:?}, new={new_mv:?}",
499 );
500
501 let gid = new_mv.global_id_writes();
502 self.allow_writes(new_mv.cluster_id, gid);
503
504 source_gids_to_keep.extend(new_mv.global_ids());
509 } else if prev_mv.custom_logical_compaction_window
510 != new_mv.custom_logical_compaction_window
511 {
512 let new_window = new_mv
513 .custom_logical_compaction_window
514 .unwrap_or(CompactionWindow::Default);
515 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
516 }
517 }
518 CatalogImplication::MaterializedView(CatalogImplicationKind::Dropped(
519 mv,
520 full_name,
521 )) => {
522 compute_sinks_to_drop.push((mv.cluster_id, mv.global_id_writes()));
523 for gid in mv.global_ids() {
524 sources_to_drop.push((catalog_id, gid));
525 dropped_item_names.insert(gid, full_name.clone());
526 }
527 }
528 CatalogImplication::View(CatalogImplicationKind::Added(_view)) => {
529 }
532 CatalogImplication::View(CatalogImplicationKind::Altered {
533 prev: _prev_view,
534 new: _new_view,
535 }) => {
536 }
539 CatalogImplication::View(CatalogImplicationKind::Dropped(view, full_name)) => {
540 view_gids_to_drop.push(view.global_id());
541 dropped_item_names.insert(view.global_id(), full_name);
542 }
543 CatalogImplication::Secret(CatalogImplicationKind::Added(_secret)) => {
544 }
548 CatalogImplication::Secret(CatalogImplicationKind::Altered {
549 prev: _prev_secret,
550 new: _new_secret,
551 }) => {
552 }
555 CatalogImplication::Secret(CatalogImplicationKind::Dropped(
556 _secret,
557 _full_name,
558 )) => {
559 secrets_to_drop.push(catalog_id);
560 }
561 CatalogImplication::Connection(CatalogImplicationKind::Added(connection)) => {
562 match &connection.details {
563 ConnectionDetails::Ssh { .. } => {}
566 ConnectionDetails::AwsPrivatelink(privatelink) => {
568 let spec = VpcEndpointConfig {
569 aws_service_name: privatelink.service_name.to_owned(),
570 availability_zone_ids: privatelink.availability_zones.to_owned(),
571 };
572 vpc_endpoints_to_create.push((catalog_id, spec));
573 }
574 _ => {}
576 }
577 }
578 CatalogImplication::Connection(CatalogImplicationKind::Altered {
579 prev: _prev_connection,
580 new: new_connection,
581 }) => {
582 self.handle_alter_connection(
583 catalog_id,
584 new_connection,
585 &mut vpc_endpoints_to_create,
586 &mut source_connections_to_alter,
587 &mut sink_connections_to_alter,
588 &mut source_export_data_configs_to_alter,
589 );
590 }
591 CatalogImplication::Connection(CatalogImplicationKind::Dropped(
592 connection,
593 _full_name,
594 )) => {
595 match &connection.details {
596 ConnectionDetails::Ssh { .. } => {
598 secrets_to_drop.push(catalog_id);
599 }
600 ConnectionDetails::AwsPrivatelink(_) => {
603 vpc_endpoints_to_drop.push(catalog_id);
604 }
605 _ => (),
606 }
607 }
608 CatalogImplication::None => {
609 }
611 CatalogImplication::Cluster(_) | CatalogImplication::ClusterReplica(_) => {
612 unreachable!("clusters and cluster replicas are handled below")
613 }
614 CatalogImplication::Table(CatalogImplicationKind::None)
615 | CatalogImplication::Source(CatalogImplicationKind::None)
616 | CatalogImplication::Sink(CatalogImplicationKind::None)
617 | CatalogImplication::Index(CatalogImplicationKind::None)
618 | CatalogImplication::MetricSink(CatalogImplicationKind::None)
619 | CatalogImplication::MaterializedView(CatalogImplicationKind::None)
620 | CatalogImplication::View(CatalogImplicationKind::None)
621 | CatalogImplication::Secret(CatalogImplicationKind::None)
622 | CatalogImplication::Connection(CatalogImplicationKind::None) => {
623 unreachable!("will never leave None in place");
624 }
625 }
626 }
627
628 for (cluster_id, command) in cluster_commands {
629 tracing::trace!(?command, "have cluster command to apply!");
630
631 match command {
632 CatalogImplication::Cluster(CatalogImplicationKind::Added(cluster)) => {
633 let arranged_logs = introspection_source_indexes
638 .remove(&cluster_id)
639 .unwrap_or_default();
640 let introspection_source_ids: Vec<_> =
641 arranged_logs.values().copied().collect();
642
643 self.controller
644 .create_cluster(
645 cluster_id,
646 mz_controller::clusters::ClusterConfig {
647 arranged_logs,
648 workload_class: cluster.config.workload_class.clone(),
649 },
650 )
651 .expect("creating cluster must not fail");
652
653 if !introspection_source_ids.is_empty() {
654 self.initialize_compute_read_policies(
655 introspection_source_ids,
656 cluster_id,
657 CompactionWindow::Default,
658 )
659 .await;
660 }
661 }
662 CatalogImplication::Cluster(CatalogImplicationKind::Altered {
663 prev: prev_cluster,
664 new: new_cluster,
665 }) => {
666 if prev_cluster.config.workload_class != new_cluster.config.workload_class {
672 self.controller.update_cluster_workload_class(
673 cluster_id,
674 new_cluster.config.workload_class.clone(),
675 );
676 }
677 }
678 CatalogImplication::Cluster(CatalogImplicationKind::Dropped(
679 cluster,
680 _full_name,
681 )) => {
682 clusters_to_drop.push(cluster_id);
683 dropped_cluster_names.insert(cluster_id, cluster.name);
684 }
685 CatalogImplication::Cluster(CatalogImplicationKind::None) => {
686 unreachable!("will never leave None in place");
687 }
688 command => {
689 unreachable!(
690 "we only handle cluster commands in this map, got: {:?}",
691 command
692 );
693 }
694 }
695 }
696
697 if replica_scoped_config_changed {
706 self.push_replica_dyncfg_overrides();
707 }
708
709 for ((cluster_id, replica_id), command) in cluster_replica_commands {
710 tracing::trace!(?command, "have cluster replica command to apply!");
711
712 match command {
713 CatalogImplication::ClusterReplica(CatalogImplicationKind::Added(replica)) => {
714 let cluster = self.catalog().get_cluster(cluster_id);
722 let cluster_name = cluster.name.clone();
723 let cluster_role = cluster.role();
724 self.handle_create_cluster_replica(
725 cluster_id,
726 replica_id,
727 cluster_role,
728 cluster_name,
729 replica.name.clone(),
730 replica.config.clone(),
731 )
732 .await;
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 if !source_collections_to_create.is_empty() {
761 self.create_source_collections(source_collections_to_create)
762 .await?;
763 }
764
765 if !table_collections_to_create.is_empty() {
768 self.create_table_collections(table_collections_to_create, execution_timestamps_to_set)
769 .await?;
770 }
771 self.initialize_storage_collections(storage_policies_to_initialize)
777 .await?;
778
779 if !vpc_endpoints_to_create.is_empty() {
781 if let Some(cloud_resource_controller) = self.cloud_resource_controller.as_ref() {
782 for (connection_id, spec) in vpc_endpoints_to_create {
783 if let Err(err) = cloud_resource_controller
784 .ensure_vpc_endpoint(connection_id, spec)
785 .await
786 {
787 tracing::error!(?err, "failed to ensure vpc endpoint!");
788 }
789 }
790 } else {
791 tracing::error!(
792 "AWS PrivateLink connections unsupported without cloud_resource_controller"
793 );
794 }
795 }
796
797 if !source_connections_to_alter.is_empty() {
799 self.controller
800 .storage
801 .alter_ingestion_connections(source_connections_to_alter)
802 .await
803 .unwrap_or_terminate("cannot fail to alter ingestion connections");
804 }
805
806 if !sink_connections_to_alter.is_empty() {
807 self.controller
808 .storage
809 .alter_export_connections(sink_connections_to_alter)
810 .await
811 .unwrap_or_terminate("altering export connections after txn must succeed");
812 }
813
814 if !source_export_data_configs_to_alter.is_empty() {
815 self.controller
816 .storage
817 .alter_ingestion_export_data_configs(source_export_data_configs_to_alter)
818 .await
819 .unwrap_or_terminate("altering source export data configs after txn must succeed");
820 }
821
822 if !source_descs_to_alter.is_empty() {
823 self.controller
824 .storage
825 .alter_ingestion_source_desc(source_descs_to_alter)
826 .await
827 .unwrap_or_terminate("cannot fail to alter ingestion source desc");
828 }
829
830 sources_to_drop.retain(|(_, gid)| !source_gids_to_keep.contains(gid));
832
833 let readable_collections_to_drop: BTreeSet<_> = sources_to_drop
834 .iter()
835 .map(|(_, gid)| *gid)
836 .chain(tables_to_drop.iter().map(|(_, gid)| *gid))
837 .chain(indexes_to_drop.iter().map(|(_, gid)| *gid))
838 .chain(view_gids_to_drop.iter().copied())
839 .collect();
840
841 for (sink_id, sink) in &self.active_compute_sinks {
844 let cluster_id = sink.cluster_id();
845 if let Some(id) = sink
846 .depends_on()
847 .iter()
848 .find(|id| readable_collections_to_drop.contains(id))
849 {
850 let name = dropped_item_names
851 .get(id)
852 .cloned()
853 .expect("missing relation name");
854 active_compute_sinks_to_drop.insert(
855 *sink_id,
856 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Relation {
857 name,
858 }),
859 );
860 } else if clusters_to_drop.contains(&cluster_id) {
861 let name = dropped_cluster_names
862 .get(&cluster_id)
863 .cloned()
864 .expect("missing cluster name");
865 active_compute_sinks_to_drop.insert(
866 *sink_id,
867 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Cluster {
868 name,
869 }),
870 );
871 }
872 }
873
874 for (uuid, pending_peek) in &self.pending_peeks {
876 if let Some(id) = pending_peek
877 .depends_on
878 .iter()
879 .find(|id| readable_collections_to_drop.contains(id))
880 {
881 let name = dropped_item_names
882 .get(id)
883 .cloned()
884 .expect("missing relation name");
885 peeks_to_drop.push((DroppedDependency::Relation { name }, uuid.clone()));
886 } else if clusters_to_drop.contains(&pending_peek.cluster_id) {
887 let name = dropped_cluster_names
888 .get(&pending_peek.cluster_id)
889 .cloned()
890 .expect("missing cluster name");
891 peeks_to_drop.push((DroppedDependency::Cluster { name }, uuid.clone()));
892 }
893 }
894
895 for (conn_id, pending_copy) in &self.active_copies {
897 let dropping_table = tables_to_drop
898 .iter()
899 .any(|(item_id, _gid)| pending_copy.table_id == *item_id);
900 let dropping_cluster = clusters_to_drop.contains(&pending_copy.cluster_id);
901
902 if dropping_table || dropping_cluster {
903 copies_to_drop.push(conn_id.clone());
904 }
905 }
906
907 let storage_gids_to_drop: BTreeSet<_> = sources_to_drop
908 .iter()
909 .map(|(_id, gid)| gid)
910 .chain(storage_sink_gids_to_drop.iter())
911 .chain(tables_to_drop.iter().map(|(_id, gid)| gid))
912 .copied()
913 .collect();
914 let compute_gids_to_drop: Vec<_> = indexes_to_drop
915 .iter()
916 .chain(compute_sinks_to_drop.iter())
917 .copied()
918 .collect();
919
920 let mut timeline_id_bundles = BTreeMap::new();
926
927 for (timeline, TimelineState { read_holds, .. }) in &self.global_timelines {
928 let mut id_bundle = CollectionIdBundle::default();
929
930 for storage_id in read_holds.storage_ids() {
931 if storage_gids_to_drop.contains(&storage_id) {
932 id_bundle.storage_ids.insert(storage_id);
933 }
934 }
935
936 for (instance_id, id) in read_holds.compute_ids() {
937 if compute_gids_to_drop.contains(&(instance_id, id))
938 || clusters_to_drop.contains(&instance_id)
939 {
940 id_bundle
941 .compute_ids
942 .entry(instance_id)
943 .or_default()
944 .insert(id);
945 }
946 }
947
948 timeline_id_bundles.insert(timeline.clone(), id_bundle);
949 }
950
951 let mut timeline_associations = BTreeMap::new();
952 for (timeline, id_bundle) in timeline_id_bundles.into_iter() {
953 let TimelineState { read_holds, .. } = self
954 .global_timelines
955 .get(&timeline)
956 .expect("all timelines have a timestamp oracle");
957
958 let empty = read_holds.id_bundle().difference(&id_bundle).is_empty();
959 timeline_associations.insert(timeline, (empty, id_bundle));
960 }
961
962 let _: () = async {
965 if !timeline_associations.is_empty() {
966 for (timeline, (should_be_empty, id_bundle)) in timeline_associations {
967 let became_empty =
968 self.remove_resources_associated_with_timeline(timeline, id_bundle);
969 assert_eq!(should_be_empty, became_empty, "emptiness did not match!");
970 }
971 }
972
973 if !tables_to_drop.is_empty() {
980 self.drop_tables(tables_to_drop.into_iter().collect_vec())
981 .await;
982 }
983
984 if !sources_to_drop.is_empty() {
985 self.drop_sources(sources_to_drop);
986 }
987
988 if !storage_sink_gids_to_drop.is_empty() {
989 self.drop_storage_sinks(storage_sink_gids_to_drop);
990 }
991
992 if !active_compute_sinks_to_drop.is_empty() {
993 let retire_notify = self
994 .retire_compute_sinks(active_compute_sinks_to_drop)
995 .await;
996 if let Some(ctx) = ctx {
997 ctx.delay_response_until(retire_notify);
998 }
999 }
1000
1001 if !peeks_to_drop.is_empty() {
1002 for (dep, uuid) in peeks_to_drop {
1003 if let Some(pending_peek) = self.remove_pending_peek(&uuid) {
1004 let cancel_reason = PeekResponse::Error(dep.query_terminated_error());
1005 self.controller
1006 .compute
1007 .cancel_peek(pending_peek.cluster_id, uuid, cancel_reason)
1008 .unwrap_or_terminate("unable to cancel peek");
1009 self.retire_execution(
1010 StatementEndedExecutionReason::Canceled,
1011 pending_peek.ctx_extra.defuse(),
1012 );
1013 }
1014 }
1015 }
1016
1017 if !copies_to_drop.is_empty() {
1018 for conn_id in copies_to_drop {
1019 self.cancel_pending_copy(&conn_id);
1020 }
1021 }
1022
1023 if !compute_gids_to_drop.is_empty() {
1024 self.drop_compute_collections(compute_gids_to_drop);
1025 }
1026
1027 if !vpc_endpoints_to_drop.is_empty() {
1028 self.drop_vpc_endpoints_in_background(vpc_endpoints_to_drop)
1029 }
1030
1031 if !cluster_replicas_to_drop.is_empty() {
1032 fail::fail_point!("after_catalog_drop_replica");
1033
1034 for (cluster_id, replica_id) in cluster_replicas_to_drop {
1035 self.drop_replica(cluster_id, replica_id);
1036 }
1037 }
1038 if !clusters_to_drop.is_empty() {
1039 for cluster_id in clusters_to_drop {
1040 self.controller.drop_cluster(cluster_id);
1041 }
1042 }
1043
1044 task::spawn(|| "drop_replication_slots_and_secrets", {
1052 let ssh_tunnel_manager = self.connection_context().ssh_tunnel_manager.clone();
1053 let caching_secrets_reader = self.caching_secrets_reader.clone();
1054 let secrets_controller = Arc::clone(&self.secrets_controller);
1055 let secrets_reader = Arc::clone(self.secrets_reader());
1056 let storage_config = self.controller.storage.config().clone();
1057
1058 async move {
1059 for (connection, replication_slot_name) in replication_slots_to_drop {
1060 tracing::info!(?replication_slot_name, "dropping replication slot");
1061
1062 let result: Result<(), anyhow::Error> = Retry::default()
1068 .max_duration(Duration::from_secs(60))
1069 .retry_async(|_state| async {
1070 let config = connection
1071 .config(&secrets_reader, &storage_config, InTask::No)
1072 .await
1073 .map_err(|e| {
1074 anyhow::anyhow!(
1075 "error creating Postgres client for \
1076 dropping acquired slots: {}",
1077 e.display_with_causes()
1078 )
1079 })?;
1080
1081 mz_postgres_util::drop_replication_slots(
1082 &ssh_tunnel_manager,
1083 config.clone(),
1084 &[(&replication_slot_name, true)],
1085 )
1086 .await?;
1087
1088 Ok(())
1089 })
1090 .await;
1091
1092 if let Err(err) = result {
1093 tracing::warn!(
1094 ?replication_slot_name,
1095 ?err,
1096 "failed to drop replication slot"
1097 );
1098 }
1099 }
1100
1101 fail_point!("drop_secrets");
1109 for secret in secrets_to_drop {
1110 if let Err(e) = secrets_controller.delete(secret).await {
1111 warn!("Dropping secrets has encountered an error: {}", e);
1112 } else {
1113 caching_secrets_reader.invalidate(secret);
1114 }
1115 }
1116 }
1117 });
1118 }
1119 .instrument(info_span!(
1120 "coord::apply_catalog_implications_inner::finalize"
1121 ))
1122 .await;
1123
1124 Ok(())
1125 }
1126
1127 #[instrument(level = "debug")]
1128 async fn create_table_collections(
1129 &mut self,
1130 table_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1131 execution_timestamps_to_set: BTreeSet<StatementLoggingId>,
1132 ) -> Result<(), AdapterError> {
1133 let table_ids: Vec<GlobalId> = table_collections_to_create.keys().copied().collect();
1135 let collections = table_collections_to_create.into_iter().collect_vec();
1136
1137 let write_ts = self.get_local_write_ts().await;
1139 let register_ts = write_ts.timestamp;
1140 self.catalog
1141 .advance_upper(write_ts.advance_to)
1142 .await
1143 .unwrap_or_terminate("unable to advance catalog upper");
1144
1145 {
1146 let storage_metadata = self.catalog.state().storage_metadata();
1147 self.controller
1148 .storage
1149 .create_collections(storage_metadata, Some(register_ts), collections)
1150 .await
1151 .unwrap_or_terminate("cannot fail to create collections");
1152 }
1153
1154 let registrations = self
1157 .controller
1158 .storage
1159 .table_registrations(table_ids)
1160 .unwrap_or_terminate("cannot fail to look up table registrations");
1161 let table_ts = if registrations.is_empty() {
1162 self.apply_local_write(register_ts).await;
1164 register_ts
1165 } else {
1166 self.register_tables_via_committer(registrations).await
1167 };
1168
1169 for id in execution_timestamps_to_set {
1170 self.set_statement_execution_timestamp(id, table_ts);
1171 }
1172
1173 Ok(())
1174 }
1175
1176 #[instrument(level = "debug")]
1177 async fn create_source_collections(
1178 &mut self,
1179 source_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1180 ) -> Result<(), AdapterError> {
1181 let storage_metadata = self.catalog.state().storage_metadata();
1182
1183 self.controller
1184 .storage
1185 .create_collections(
1186 storage_metadata,
1187 None, source_collections_to_create.into_iter().collect_vec(),
1189 )
1190 .await
1191 .unwrap_or_terminate("cannot fail to create collections");
1192
1193 Ok(())
1194 }
1195
1196 #[instrument(level = "debug")]
1197 async fn initialize_storage_collections(
1198 &mut self,
1199 storage_policies_to_initialize: BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1200 ) -> Result<(), AdapterError> {
1201 for (compaction_window, global_ids) in storage_policies_to_initialize {
1202 self.initialize_read_policies(
1203 &CollectionIdBundle {
1204 storage_ids: global_ids,
1205 compute_ids: BTreeMap::new(),
1206 },
1207 compaction_window,
1208 )
1209 .await;
1210 }
1211
1212 Ok(())
1213 }
1214
1215 #[instrument(level = "debug")]
1216 async fn handle_create_table(
1217 &self,
1218 ctx: &Option<&mut ExecuteContext>,
1219 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1220 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1221 execution_timestamps_to_set: &mut BTreeSet<StatementLoggingId>,
1222 table_id: CatalogItemId,
1223 table: Table,
1224 ) -> Result<(), AdapterError> {
1225 match &table.data_source {
1229 TableDataSource::TableWrites { defaults: _ } => {
1230 let versions: BTreeMap<_, _> = table
1231 .collection_descs()
1232 .map(|(gid, version, desc)| (version, (gid, desc)))
1233 .collect();
1234 let collection_descs = versions.iter().map(|(_version, (gid, desc))| {
1235 let collection_desc = CollectionDescription::for_table(desc.clone());
1236
1237 (*gid, collection_desc)
1238 });
1239
1240 let compaction_window = table
1241 .custom_logical_compaction_window
1242 .unwrap_or(CompactionWindow::Default);
1243 let ids_to_initialize = storage_policies_to_initialize
1244 .entry(compaction_window)
1245 .or_default();
1246
1247 for (gid, collection_desc) in collection_descs {
1248 storage_collections_to_create.insert(gid, collection_desc);
1249 ids_to_initialize.insert(gid);
1250 }
1251
1252 if let Some(id) = ctx.as_ref().and_then(|ctx| ctx.extra().contents()) {
1253 execution_timestamps_to_set.insert(id);
1254 }
1255 }
1256 TableDataSource::DataSource {
1257 desc: data_source_desc,
1258 timeline,
1259 } => {
1260 match data_source_desc {
1261 DataSourceDesc::IngestionExport {
1262 ingestion_id,
1263 external_reference: _,
1264 details,
1265 data_config,
1266 } => {
1267 let global_ingestion_id =
1268 self.catalog().get_entry(ingestion_id).latest_global_id();
1269
1270 let collection_desc = CollectionDescription {
1271 desc: table.desc.latest(),
1272 data_source: DataSource::IngestionExport {
1273 ingestion_id: global_ingestion_id,
1274 details: details.clone(),
1275 data_config: data_config
1276 .clone()
1277 .into_inline_connection(self.catalog.state()),
1278 },
1279 since: None,
1280 timeline: Some(timeline.clone()),
1281 primary: None,
1282 };
1283
1284 let global_id = table
1285 .global_ids()
1286 .expect_element(|| "subsources cannot have multiple versions");
1287
1288 storage_collections_to_create.insert(global_id, collection_desc);
1289
1290 let read_policies = self
1291 .catalog()
1292 .state()
1293 .source_compaction_windows(vec![table_id]);
1294 for (compaction_window, catalog_ids) in read_policies {
1295 let compaction_ids = storage_policies_to_initialize
1296 .entry(compaction_window)
1297 .or_default();
1298
1299 let gids = catalog_ids
1300 .into_iter()
1301 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1302 .flatten();
1303 compaction_ids.extend(gids);
1304 }
1305 }
1306 DataSourceDesc::Webhook {
1307 validate_using: _,
1308 body_format: _,
1309 headers: _,
1310 cluster_id: _,
1311 } => {
1312 assert_eq!(
1314 table.desc.latest_version(),
1315 RelationVersion::root(),
1316 "found webhook with more than 1 relation version, {:?}",
1317 table.desc
1318 );
1319 let desc = table.desc.latest();
1320
1321 let collection_desc = CollectionDescription {
1322 desc,
1323 data_source: DataSource::Webhook,
1324 since: None,
1325 timeline: Some(timeline.clone()),
1326 primary: None,
1327 };
1328
1329 let global_id = table
1330 .global_ids()
1331 .expect_element(|| "webhooks cannot have multiple versions");
1332
1333 storage_collections_to_create.insert(global_id, collection_desc);
1334
1335 let read_policies = self
1336 .catalog()
1337 .state()
1338 .source_compaction_windows(vec![table_id]);
1339
1340 for (compaction_window, catalog_ids) in read_policies {
1341 let compaction_ids = storage_policies_to_initialize
1342 .entry(compaction_window)
1343 .or_default();
1344
1345 let gids = catalog_ids
1346 .into_iter()
1347 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1348 .flatten();
1349 compaction_ids.extend(gids);
1350 }
1351 }
1352 _ => unreachable!("CREATE TABLE data source got {:?}", data_source_desc),
1353 }
1354 }
1355 }
1356
1357 Ok(())
1358 }
1359
1360 #[instrument(level = "debug")]
1361 async fn handle_alter_table(
1362 &mut self,
1363 catalog_id: CatalogItemId,
1364 prev_table: Table,
1365 new_table: Table,
1366 ) -> Result<(), AdapterError> {
1367 let existing_gid = prev_table.global_id_writes();
1368 let new_gid = new_table.global_id_writes();
1369
1370 if existing_gid == new_gid {
1371 if prev_table.custom_logical_compaction_window
1374 != new_table.custom_logical_compaction_window
1375 {
1376 let new_window = new_table
1377 .custom_logical_compaction_window
1378 .unwrap_or(CompactionWindow::Default);
1379 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
1380 }
1381 return Ok(());
1382 }
1383
1384 let existing_table = crate::CollectionIdBundle {
1388 storage_ids: BTreeSet::from([existing_gid]),
1389 compute_ids: BTreeMap::new(),
1390 };
1391 let existing_table_read_hold = self.acquire_read_holds(&existing_table);
1392
1393 let expected_version = prev_table.desc.latest_version();
1394 let new_version = new_table.desc.latest_version();
1395 let new_desc = new_table
1396 .desc
1397 .at_version(RelationVersionSelector::Specific(new_version));
1398
1399 let write_ts = self.get_local_write_ts().await;
1401 self.catalog
1402 .advance_upper(write_ts.advance_to)
1403 .await
1404 .unwrap_or_terminate("unable to advance catalog upper");
1405
1406 self.controller
1407 .storage
1408 .alter_table_desc(existing_gid, new_gid, new_desc, expected_version)
1409 .await
1410 .unwrap_or_terminate("failed to alter desc of table");
1411
1412 let registrations = self
1414 .controller
1415 .storage
1416 .table_registrations(vec![new_gid])
1417 .unwrap_or_terminate("cannot fail to look up table registrations");
1418 self.register_tables_via_committer(registrations).await;
1419
1420 let compaction_window = new_table
1422 .custom_logical_compaction_window
1423 .unwrap_or(CompactionWindow::Default);
1424 self.initialize_read_policies(
1425 &crate::CollectionIdBundle {
1426 storage_ids: BTreeSet::from([new_gid]),
1427 compute_ids: BTreeMap::new(),
1428 },
1429 compaction_window,
1430 )
1431 .await;
1432
1433 drop(existing_table_read_hold);
1435
1436 Ok(())
1437 }
1438
1439 #[instrument(level = "debug")]
1440 async fn handle_create_source(
1441 &self,
1442 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1443 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1444 item_id: CatalogItemId,
1445 source: Source,
1446 compaction_windows: BTreeMap<CompactionWindow, BTreeSet<CatalogItemId>>,
1447 ) -> Result<(), AdapterError> {
1448 let data_source = match source.data_source {
1449 DataSourceDesc::Ingestion { desc, cluster_id } => {
1450 let desc = desc.into_inline_connection(self.catalog().state());
1451 let item_global_id = self.catalog().get_entry(&item_id).latest_global_id();
1452
1453 let ingestion = mz_storage_types::sources::IngestionDescription::new(
1454 desc,
1455 cluster_id,
1456 item_global_id,
1457 );
1458
1459 DataSource::Ingestion(ingestion)
1460 }
1461 DataSourceDesc::OldSyntaxIngestion {
1462 desc,
1463 progress_subsource,
1464 data_config,
1465 details,
1466 cluster_id,
1467 } => {
1468 let desc = desc.into_inline_connection(self.catalog().state());
1469 let data_config = data_config.into_inline_connection(self.catalog().state());
1470
1471 let progress_subsource = self
1474 .catalog()
1475 .get_entry(&progress_subsource)
1476 .latest_global_id();
1477
1478 let mut ingestion = mz_storage_types::sources::IngestionDescription::new(
1479 desc,
1480 cluster_id,
1481 progress_subsource,
1482 );
1483
1484 let legacy_export = SourceExport {
1485 storage_metadata: (),
1486 data_config,
1487 details,
1488 };
1489
1490 ingestion
1491 .source_exports
1492 .insert(source.global_id, legacy_export);
1493
1494 DataSource::Ingestion(ingestion)
1495 }
1496 DataSourceDesc::IngestionExport {
1497 ingestion_id,
1498 external_reference: _,
1499 details,
1500 data_config,
1501 } => {
1502 let ingestion_id = self.catalog().get_entry(&ingestion_id).latest_global_id();
1505
1506 DataSource::IngestionExport {
1507 ingestion_id,
1508 details,
1509 data_config: data_config.into_inline_connection(self.catalog().state()),
1510 }
1511 }
1512 DataSourceDesc::Progress => DataSource::Progress,
1513 DataSourceDesc::Webhook { .. } => DataSource::Webhook,
1514 DataSourceDesc::Introspection(_) | DataSourceDesc::Catalog => {
1515 unreachable!("cannot create sources with internal data sources")
1516 }
1517 };
1518
1519 storage_collections_to_create.insert(
1520 source.global_id,
1521 CollectionDescription {
1522 desc: source.desc.clone(),
1523 data_source,
1524 timeline: Some(source.timeline),
1525 since: None,
1526 primary: None,
1527 },
1528 );
1529
1530 for (compaction_window, catalog_ids) in compaction_windows {
1532 let compaction_ids = storage_policies_to_initialize
1533 .entry(compaction_window)
1534 .or_default();
1535
1536 let gids = catalog_ids
1537 .into_iter()
1538 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1539 .flatten();
1540 compaction_ids.extend(gids);
1541 }
1542
1543 Ok(())
1544 }
1545
1546 #[instrument(level = "debug")]
1553 fn handle_alter_connection(
1554 &self,
1555 connection_id: CatalogItemId,
1556 connection: Connection,
1557 vpc_endpoints_to_create: &mut Vec<(CatalogItemId, VpcEndpointConfig)>,
1558 source_connections_to_alter: &mut BTreeMap<
1559 GlobalId,
1560 GenericSourceConnection<InlinedConnection>,
1561 >,
1562 sink_connections_to_alter: &mut BTreeMap<GlobalId, StorageSinkConnection>,
1563 source_export_data_configs_to_alter: &mut BTreeMap<GlobalId, SourceExportDataConfig>,
1564 ) {
1565 use std::collections::VecDeque;
1566
1567 if let ConnectionDetails::AwsPrivatelink(ref privatelink) = connection.details {
1569 let spec = VpcEndpointConfig {
1570 aws_service_name: privatelink.service_name.to_owned(),
1571 availability_zone_ids: privatelink.availability_zones.to_owned(),
1572 };
1573 vpc_endpoints_to_create.push((connection_id, spec));
1574 }
1575
1576 let mut connections_to_process = VecDeque::new();
1580 connections_to_process.push_front(connection_id.clone());
1581
1582 while let Some(id) = connections_to_process.pop_front() {
1583 for dependent_id in self.catalog().get_entry(&id).used_by() {
1584 let dependent_entry = self.catalog().get_entry(dependent_id);
1585 match dependent_entry.item() {
1586 CatalogItem::Connection(_) => {
1587 connections_to_process.push_back(*dependent_id);
1591 }
1592 CatalogItem::Source(source) => {
1593 let desc = match &dependent_entry
1594 .source()
1595 .expect("known to be source")
1596 .data_source
1597 {
1598 DataSourceDesc::Ingestion { desc, .. }
1599 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1600 desc.clone().into_inline_connection(self.catalog().state())
1601 }
1602 DataSourceDesc::IngestionExport { .. }
1603 | DataSourceDesc::Introspection(_)
1604 | DataSourceDesc::Progress
1605 | DataSourceDesc::Webhook { .. }
1606 | DataSourceDesc::Catalog => {
1607 continue;
1609 }
1610 };
1611
1612 source_connections_to_alter.insert(source.global_id, desc.connection);
1613 }
1614 CatalogItem::Sink(sink) => {
1615 let export = dependent_entry.sink().expect("known to be sink");
1616 sink_connections_to_alter.insert(
1617 sink.global_id,
1618 export
1619 .connection
1620 .clone()
1621 .into_inline_connection(self.catalog().state()),
1622 );
1623 }
1624 CatalogItem::Table(table) => {
1625 if let Some((_, _, _, export_data_config)) =
1629 dependent_entry.source_export_details()
1630 {
1631 let data_config = export_data_config.clone();
1632 source_export_data_configs_to_alter.insert(
1633 table.global_id_writes(),
1634 data_config.into_inline_connection(self.catalog().state()),
1635 );
1636 }
1637 }
1638 CatalogItem::Log(_)
1639 | CatalogItem::View(_)
1640 | CatalogItem::MaterializedView(_)
1641 | CatalogItem::Index(_)
1642 | CatalogItem::Type(_)
1643 | CatalogItem::Func(_)
1644 | CatalogItem::Secret(_)
1645 | CatalogItem::MetricSink(_) => {
1646 }
1649 }
1650 }
1651 }
1652 }
1653
1654 async fn handle_create_cluster_replica(
1655 &mut self,
1656 cluster_id: ClusterId,
1657 replica_id: ReplicaId,
1658 role: ClusterRole,
1659 cluster_name: String,
1660 replica_name: String,
1661 replica_config: ReplicaConfig,
1662 ) {
1663 let enable_worker_core_affinity =
1664 self.catalog().system_config().enable_worker_core_affinity();
1665 let enable_storage_introspection_logs = self
1666 .catalog()
1667 .system_config()
1668 .enable_storage_introspection_logs();
1669
1670 self.controller
1677 .create_replica(
1678 cluster_id,
1679 replica_id,
1680 cluster_name,
1681 replica_name,
1682 role,
1683 replica_config,
1684 enable_worker_core_affinity,
1685 enable_storage_introspection_logs,
1686 )
1687 .expect("creating replicas must not fail");
1688
1689 self.install_introspection_subscribes(cluster_id, replica_id)
1690 .await;
1691 }
1692}
1693
1694#[derive(Debug, Clone)]
1700enum CatalogImplication {
1701 None,
1702 Table(CatalogImplicationKind<Table>),
1703 Source(CatalogImplicationKind<(Source, Option<GenericSourceConnection>)>),
1704 Sink(CatalogImplicationKind<Sink>),
1705 Index(CatalogImplicationKind<Index>),
1706 MetricSink(CatalogImplicationKind<MetricSink>),
1707 MaterializedView(CatalogImplicationKind<MaterializedView>),
1708 View(CatalogImplicationKind<View>),
1709 Secret(CatalogImplicationKind<Secret>),
1710 Connection(CatalogImplicationKind<Connection>),
1711 Cluster(CatalogImplicationKind<Cluster>),
1712 ClusterReplica(CatalogImplicationKind<ClusterReplica>),
1713}
1714
1715#[derive(Debug, Clone)]
1716enum CatalogImplicationKind<T> {
1717 None,
1719 Added(T),
1721 Dropped(T, String),
1723 Altered { prev: T, new: T },
1725}
1726
1727impl<T: Clone> CatalogImplicationKind<T> {
1728 fn transition(&mut self, item: T, name: Option<String>, diff: StateDiff) -> Result<(), String> {
1731 use CatalogImplicationKind::*;
1732 use StateDiff::*;
1733
1734 let new_state = match (&*self, diff) {
1735 (None, Addition) => Added(item),
1737 (None, Retraction) => Dropped(item, name.unwrap_or_else(|| "<unknown>".to_string())),
1738
1739 (Added(existing), Retraction) => {
1741 Altered {
1743 prev: item,
1744 new: existing.clone(),
1745 }
1746 }
1747 (Added(_), Addition) => {
1748 return Err("Cannot add an already added object".to_string());
1749 }
1750
1751 (Dropped(existing, _), Addition) => {
1753 Altered {
1755 prev: existing.clone(),
1756 new: item,
1757 }
1758 }
1759 (Dropped(_, _), Retraction) => {
1760 return Err("Cannot drop an already dropped object".to_string());
1761 }
1762
1763 (Altered { .. }, _) => {
1765 return Err(format!(
1766 "Cannot apply {:?} to an object in Altered state",
1767 diff
1768 ));
1769 }
1770 };
1771
1772 *self = new_state;
1773 Ok(())
1774 }
1775}
1776
1777macro_rules! impl_absorb_method {
1779 (
1780 $method_name:ident,
1781 $variant:ident,
1782 $item_type:ty
1783 ) => {
1784 fn $method_name(
1785 &mut self,
1786 item: $item_type,
1787 parsed_full_name: Option<String>,
1788 diff: StateDiff,
1789 ) {
1790 let state = match self {
1791 CatalogImplication::$variant(state) => state,
1792 CatalogImplication::None => {
1793 *self = CatalogImplication::$variant(CatalogImplicationKind::None);
1794 match self {
1795 CatalogImplication::$variant(state) => state,
1796 _ => unreachable!(),
1797 }
1798 }
1799 _ => {
1800 panic!(
1801 "Unexpected command type for {:?}: {} {:?}",
1802 self,
1803 stringify!($variant),
1804 diff,
1805 );
1806 }
1807 };
1808
1809 if let Err(e) = state.transition(item, parsed_full_name, diff) {
1810 panic!(
1811 "Invalid state transition for {}: {}",
1812 stringify!($variant),
1813 e
1814 );
1815 }
1816 }
1817 };
1818}
1819
1820impl CatalogImplication {
1821 fn absorb(&mut self, catalog_update: ParsedStateUpdate) {
1824 match catalog_update.kind {
1825 ParsedStateUpdateKind::Item {
1826 durable_item: _,
1827 parsed_item,
1828 connection,
1829 parsed_full_name,
1830 } => match parsed_item {
1831 CatalogItem::Table(table) => {
1832 self.absorb_table(table, Some(parsed_full_name), catalog_update.diff)
1833 }
1834 CatalogItem::Source(source) => {
1835 self.absorb_source(
1836 (source, connection),
1837 Some(parsed_full_name),
1838 catalog_update.diff,
1839 );
1840 }
1841 CatalogItem::Sink(sink) => {
1842 self.absorb_sink(sink, Some(parsed_full_name), catalog_update.diff);
1843 }
1844 CatalogItem::Index(index) => {
1845 self.absorb_index(index, Some(parsed_full_name), catalog_update.diff);
1846 }
1847 CatalogItem::MaterializedView(mv) => {
1848 self.absorb_materialized_view(mv, Some(parsed_full_name), catalog_update.diff);
1849 }
1850 CatalogItem::View(view) => {
1851 self.absorb_view(view, Some(parsed_full_name), catalog_update.diff);
1852 }
1853
1854 CatalogItem::Secret(secret) => {
1855 self.absorb_secret(secret, None, catalog_update.diff);
1856 }
1857 CatalogItem::Connection(connection) => {
1858 self.absorb_connection(connection, None, catalog_update.diff);
1859 }
1860 CatalogItem::MetricSink(metric_sink) => {
1861 self.absorb_metric_sink(
1862 metric_sink,
1863 Some(parsed_full_name),
1864 catalog_update.diff,
1865 );
1866 }
1867 CatalogItem::Log(_) => {}
1868 CatalogItem::Type(_) => {}
1869 CatalogItem::Func(_) => {}
1870 },
1871 ParsedStateUpdateKind::Cluster {
1872 durable_cluster: _,
1873 parsed_cluster,
1874 } => {
1875 let name = parsed_cluster.name.clone();
1876 self.absorb_cluster(parsed_cluster, Some(name), catalog_update.diff);
1877 }
1878 ParsedStateUpdateKind::ClusterReplica {
1879 durable_cluster_replica: _,
1880 parsed_cluster_replica,
1881 } => {
1882 let name = parsed_cluster_replica.name.clone();
1883 self.absorb_cluster_replica(
1884 parsed_cluster_replica,
1885 Some(name),
1886 catalog_update.diff,
1887 );
1888 }
1889 ParsedStateUpdateKind::IntrospectionSourceIndex { .. } => {
1890 unreachable!("IntrospectionSourceIndex should not be passed to absorb");
1894 }
1895 ParsedStateUpdateKind::ReplicaSystemConfiguration { .. } => {
1896 unreachable!("ReplicaSystemConfiguration should not be passed to absorb");
1899 }
1900 ParsedStateUpdateKind::SystemConfiguration { .. } => {
1901 unreachable!("SystemConfiguration should not be passed to absorb");
1904 }
1905 }
1906 }
1907
1908 impl_absorb_method!(absorb_table, Table, Table);
1909 impl_absorb_method!(
1910 absorb_source,
1911 Source,
1912 (Source, Option<GenericSourceConnection>)
1913 );
1914 impl_absorb_method!(absorb_sink, Sink, Sink);
1915 impl_absorb_method!(absorb_index, Index, Index);
1916 impl_absorb_method!(absorb_metric_sink, MetricSink, MetricSink);
1917 impl_absorb_method!(absorb_materialized_view, MaterializedView, MaterializedView);
1918 impl_absorb_method!(absorb_view, View, View);
1919
1920 impl_absorb_method!(absorb_secret, Secret, Secret);
1921 impl_absorb_method!(absorb_connection, Connection, Connection);
1922
1923 impl_absorb_method!(absorb_cluster, Cluster, Cluster);
1924 impl_absorb_method!(absorb_cluster_replica, ClusterReplica, ClusterReplica);
1925}
1926
1927#[cfg(test)]
1928mod tests {
1929 use super::*;
1930 use mz_repr::{GlobalId, RelationDesc, RelationVersion, VersionedRelationDesc};
1931 use mz_sql::names::ResolvedIds;
1932 use std::collections::BTreeMap;
1933
1934 fn create_test_table(name: &str) -> Table {
1935 Table {
1936 desc: VersionedRelationDesc::new(
1937 RelationDesc::builder()
1938 .with_column(name, mz_repr::SqlScalarType::String.nullable(false))
1939 .finish(),
1940 ),
1941 create_sql: None,
1942 collections: BTreeMap::from([(RelationVersion::root(), GlobalId::System(1))]),
1943 conn_id: None,
1944 resolved_ids: ResolvedIds::empty(),
1945 custom_logical_compaction_window: None,
1946 is_retained_metrics_object: false,
1947 data_source: TableDataSource::TableWrites { defaults: vec![] },
1948 }
1949 }
1950
1951 #[mz_ore::test]
1952 fn test_item_state_transitions() {
1953 let mut state = CatalogImplicationKind::None;
1955 assert!(
1956 state
1957 .transition("item1".to_string(), None, StateDiff::Addition)
1958 .is_ok()
1959 );
1960 assert!(matches!(state, CatalogImplicationKind::Added(_)));
1961
1962 let mut state = CatalogImplicationKind::Added("new_item".to_string());
1964 assert!(
1965 state
1966 .transition("old_item".to_string(), None, StateDiff::Retraction)
1967 .is_ok()
1968 );
1969 match &state {
1970 CatalogImplicationKind::Altered { prev, new } => {
1971 assert_eq!(prev, "old_item");
1973 assert_eq!(new, "new_item");
1975 }
1976 _ => panic!("Expected Altered state"),
1977 }
1978
1979 let mut state = CatalogImplicationKind::None;
1981 assert!(
1982 state
1983 .transition(
1984 "item1".to_string(),
1985 Some("test_name".to_string()),
1986 StateDiff::Retraction
1987 )
1988 .is_ok()
1989 );
1990 assert!(matches!(state, CatalogImplicationKind::Dropped(_, _)));
1991
1992 let mut state = CatalogImplicationKind::Dropped("old_item".to_string(), "name".to_string());
1994 assert!(
1995 state
1996 .transition("new_item".to_string(), None, StateDiff::Addition)
1997 .is_ok()
1998 );
1999 match &state {
2000 CatalogImplicationKind::Altered { prev, new } => {
2001 assert_eq!(prev, "old_item");
2003 assert_eq!(new, "new_item");
2005 }
2006 _ => panic!("Expected Altered state"),
2007 }
2008
2009 let mut state = CatalogImplicationKind::Added("item".to_string());
2011 assert!(
2012 state
2013 .transition("item2".to_string(), None, StateDiff::Addition)
2014 .is_err()
2015 );
2016
2017 let mut state = CatalogImplicationKind::Dropped("item".to_string(), "name".to_string());
2018 assert!(
2019 state
2020 .transition("item2".to_string(), None, StateDiff::Retraction)
2021 .is_err()
2022 );
2023 }
2024
2025 #[mz_ore::test]
2026 fn test_table_absorb_state_machine() {
2027 let table1 = create_test_table("table1");
2028 let table2 = create_test_table("table2");
2029
2030 let mut cmd = CatalogImplication::None;
2032 cmd.absorb_table(
2033 table1.clone(),
2034 Some("schema.table1".to_string()),
2035 StateDiff::Addition,
2036 );
2037 match &cmd {
2039 CatalogImplication::Table(state) => match state {
2040 CatalogImplicationKind::Added(t) => {
2041 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity())
2042 }
2043 _ => panic!("Expected Added state"),
2044 },
2045 _ => panic!("Expected Table command"),
2046 }
2047
2048 cmd.absorb_table(
2052 table2.clone(),
2053 Some("schema.table2".to_string()),
2054 StateDiff::Retraction,
2055 );
2056 match &cmd {
2057 CatalogImplication::Table(state) => match state {
2058 CatalogImplicationKind::Altered { prev, new } => {
2059 assert_eq!(prev.desc.latest().arity(), table2.desc.latest().arity());
2061 assert_eq!(new.desc.latest().arity(), table1.desc.latest().arity());
2062 }
2063 _ => panic!("Expected Altered state"),
2064 },
2065 _ => panic!("Expected Table command"),
2066 }
2067
2068 let mut cmd = CatalogImplication::None;
2070 cmd.absorb_table(
2071 table1.clone(),
2072 Some("schema.table1".to_string()),
2073 StateDiff::Retraction,
2074 );
2075 match &cmd {
2076 CatalogImplication::Table(state) => match state {
2077 CatalogImplicationKind::Dropped(t, name) => {
2078 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity());
2079 assert_eq!(name, "schema.table1");
2080 }
2081 _ => panic!("Expected Dropped state"),
2082 },
2083 _ => panic!("Expected Table command"),
2084 }
2085
2086 cmd.absorb_table(
2088 table2.clone(),
2089 Some("schema.table2".to_string()),
2090 StateDiff::Addition,
2091 );
2092 match &cmd {
2093 CatalogImplication::Table(state) => match state {
2094 CatalogImplicationKind::Altered { prev, new } => {
2095 assert_eq!(prev.desc.latest().arity(), table1.desc.latest().arity());
2097 assert_eq!(new.desc.latest().arity(), table2.desc.latest().arity());
2098 }
2099 _ => panic!("Expected Altered state"),
2100 },
2101 _ => panic!("Expected Table command"),
2102 }
2103 }
2104
2105 #[mz_ore::test]
2106 #[should_panic(expected = "Cannot add an already added object")]
2107 fn test_invalid_double_add() {
2108 let table = create_test_table("table");
2109 let mut cmd = CatalogImplication::None;
2110
2111 cmd.absorb_table(
2113 table.clone(),
2114 Some("schema.table".to_string()),
2115 StateDiff::Addition,
2116 );
2117
2118 cmd.absorb_table(
2120 table.clone(),
2121 Some("schema.table".to_string()),
2122 StateDiff::Addition,
2123 );
2124 }
2125
2126 #[mz_ore::test]
2127 #[should_panic(expected = "Cannot drop an already dropped object")]
2128 fn test_invalid_double_drop() {
2129 let table = create_test_table("table");
2130 let mut cmd = CatalogImplication::None;
2131
2132 cmd.absorb_table(
2134 table.clone(),
2135 Some("schema.table".to_string()),
2136 StateDiff::Retraction,
2137 );
2138
2139 cmd.absorb_table(
2141 table.clone(),
2142 Some("schema.table".to_string()),
2143 StateDiff::Retraction,
2144 );
2145 }
2146}