Skip to main content

mz_adapter/coord/
ddl.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! This module encapsulates all of the [`Coordinator`]'s logic for creating, dropping,
11//! and altering objects.
12
13use std::collections::BTreeMap;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18use fail::fail_point;
19use maplit::{btreemap, btreeset};
20use mz_adapter_types::compaction::SINCE_GRANULARITY;
21use mz_adapter_types::connection::ConnectionId;
22use mz_audit_log::VersionedEvent;
23use mz_catalog::SYSTEM_CONN_ID;
24use mz_catalog::memory::objects::Sink;
25use mz_cluster_client::ReplicaId;
26use mz_controller_types::ClusterId;
27use mz_ore::cast::CastFrom;
28use mz_ore::instrument;
29use mz_ore::metrics::MetricsFutureExt;
30use mz_ore::now::to_datetime;
31use mz_ore::retry::Retry;
32use mz_ore::task;
33use mz_ore::tracing::OpenTelemetryContext;
34use mz_repr::adt::numeric::Numeric;
35use mz_repr::{CatalogItemId, GlobalId, Timestamp};
36use mz_sql::catalog::CatalogClusterReplica;
37use mz_sql::session::metadata::SessionMetadata;
38use mz_sql::session::vars::{self, DEFAULT_TIMESTAMP_INTERVAL, SystemVars, Var};
39use mz_storage_client::controller::{CollectionDescription, DataSource, ExportDescription};
40use mz_storage_types::connections::inline::IntoInlineConnection;
41use mz_storage_types::read_policy::ReadPolicy;
42use mz_storage_types::sources::SourceConnection;
43use mz_storage_types::sources::kafka::KAFKA_PROGRESS_DESC;
44use serde_json::json;
45use tracing::{Instrument, Level, event, info_span, warn};
46
47use crate::active_compute_sink::{ActiveComputeSink, ActiveComputeSinkRetireReason};
48use crate::catalog::{DropObjectInfo, Op, TransactionResult};
49use crate::coord::appends::{BuiltinTableAppendCompletion, BuiltinTableAppendNotify};
50use crate::coord::catalog_implications::parsed_state_updates::ParsedStateUpdate;
51use crate::coord::{Coordinator, DeferredDdl, Message, PendingTxn};
52use crate::session::{Session, TransactionOps};
53use crate::telemetry::{EventDetails, SegmentClientExt};
54use crate::util::ResultExt;
55use crate::{AdapterError, ExecuteContext, catalog, flags};
56
57#[derive(Debug)]
58pub struct PendingSourceStatement {
59    ctx: ExecuteContext,
60    statement: catalog::command::source_statement::SourceStatement,
61}
62
63#[derive(Debug)]
64pub struct PendingDdlCommit {
65    context: DdlCommitContext,
66    commands: Vec<catalog::command::CatalogCommand>,
67    responses: BTreeMap<usize, mz_sql::pure::SourcePreparationResponse>,
68}
69
70#[derive(Debug)]
71enum DdlCommitContext {
72    Transaction(PendingTxn),
73    Statement {
74        ctx: ExecuteContext,
75        response: crate::ExecuteResponse,
76    },
77}
78
79impl PendingDdlCommit {
80    pub(super) fn conn_id(&self) -> &ConnectionId {
81        match &self.context {
82            DdlCommitContext::Transaction(txn) => txn.ctx.session().conn_id(),
83            DdlCommitContext::Statement { ctx, response: _ } => ctx.session().conn_id(),
84        }
85    }
86
87    pub(super) fn transaction(
88        txn: PendingTxn,
89        commands: Vec<catalog::command::CatalogCommand>,
90    ) -> Self {
91        Self {
92            context: DdlCommitContext::Transaction(txn),
93            commands,
94            responses: BTreeMap::new(),
95        }
96    }
97
98    pub(super) fn statement(
99        ctx: ExecuteContext,
100        response: crate::ExecuteResponse,
101        commands: Vec<catalog::command::CatalogCommand>,
102    ) -> Self {
103        Self {
104            context: DdlCommitContext::Statement { ctx, response },
105            commands,
106            responses: BTreeMap::new(),
107        }
108    }
109}
110
111impl DdlCommitContext {
112    fn context_mut(&mut self) -> &mut ExecuteContext {
113        match self {
114            Self::Transaction(txn) => &mut txn.ctx,
115            Self::Statement { ctx, .. } => ctx,
116        }
117    }
118
119    fn finish(self, result: Result<(), AdapterError>) {
120        match self {
121            Self::Transaction(mut txn) => {
122                if let Err(error) = result {
123                    txn.response = Err(error);
124                    txn.action = crate::session::EndTransactionAction::Rollback;
125                }
126                txn.finish();
127            }
128            Self::Statement { ctx, response } => ctx.retire(result.map(|()| response)),
129        }
130    }
131}
132
133/// Waits while a test configures the named failpoint with a `return` action.
134async fn catalog_preparation_test_pause(name: &'static str) {
135    let paused = || {
136        fail::fail_point!(name, |_| true);
137        false
138    };
139    // Clearing the failpoint requires coordinator and runtime progress.
140    while paused() {
141        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
142    }
143}
144
145impl Coordinator {
146    /// Sets allowed responses and checks read-only and execution-cluster restrictions.
147    fn prepare_command_execution(
148        &mut self,
149        ctx: &mut ExecuteContext,
150        plan: &mz_sql::plan::Plan,
151    ) -> Result<(), AdapterError> {
152        ctx.tx_mut()
153            .set_allowed(crate::ExecuteResponse::generated_from(
154                &mz_sql::plan::PlanKind::from(plan),
155            ));
156        if self.controller.read_only() && !plan.allowed_in_read_only() {
157            return Err(AdapterError::ReadOnly);
158        }
159        self.prepare_plan_execution(ctx, plan)?;
160        Ok(())
161    }
162
163    pub(super) fn sequence_catalog_result_command(
164        &mut self,
165        mut ctx: ExecuteContext,
166        statement: Arc<mz_sql::ast::Statement<mz_sql::ast::Raw>>,
167        params: mz_sql::plan::Params,
168    ) {
169        let prepared = {
170            let catalog = self.catalog().for_session(ctx.session());
171            catalog::command::CatalogResultCommand::prepare(
172                catalog.state(),
173                statement,
174                params,
175                ctx.session().meta(),
176            )
177        };
178        let (command, plan) = match prepared {
179            Ok(prepared) => prepared,
180            Err(error) => return ctx.retire(Err(error)),
181        };
182        if let Err(error) = self.prepare_command_execution(&mut ctx, &plan) {
183            return ctx.retire(Err(error));
184        }
185        let row = match catalog::command::CatalogResultCommand::result(&plan) {
186            Ok(row) => row,
187            Err(error) => return ctx.retire(Err(error)),
188        };
189        let command = Arc::new(command);
190        self.emit_command_notices(
191            ctx.session(),
192            &catalog::command::CatalogCommand::CatalogResult(Arc::clone(&command)),
193        );
194        ctx.session_mut().retain_catalog_result(command);
195        ctx.retire(Ok(Self::send_immediate_rows(row)));
196    }
197
198    pub(super) async fn sequence_source_statement(
199        &mut self,
200        ctx: ExecuteContext,
201        statement: Arc<mz_sql::ast::Statement<mz_sql::ast::Raw>>,
202        params: mz_sql::plan::Params,
203    ) {
204        let statement = catalog::command::source_statement::SourceStatement::new(
205            statement,
206            params,
207            ctx.session().meta(),
208            (self.catalog().config().now)(),
209        );
210        self.sequence_source_statement_ready(PendingSourceStatement { ctx, statement }, None)
211            .await;
212    }
213
214    pub(super) async fn sequence_source_statement_ready(
215        &mut self,
216        mut pending: PendingSourceStatement,
217        mut response: Option<mz_sql::pure::SourcePreparationResponse>,
218    ) {
219        use catalog::command::source_statement::{SourceCommand, SourceResolution};
220        if self.controller.read_only() {
221            return pending.ctx.retire(Err(AdapterError::ReadOnly));
222        }
223        let compiled = loop {
224            let state = pending
225                .ctx
226                .session()
227                .transaction()
228                .catalog_state()
229                .unwrap_or_else(|| self.catalog().state());
230            let result = match response.take() {
231                Some(response) => pending.statement.finish(state, response),
232                None => pending.statement.resolve(state),
233            };
234            match result {
235                Err(error) => return pending.ctx.retire(Err(error)),
236                Ok(SourceResolution::Ready(compiled)) => break compiled,
237                Ok(SourceResolution::Allocate(keys)) => {
238                    let ids = match self.allocate_user_ids(u64::cast_from(keys.len())).await {
239                        Ok(ids) => ids,
240                        Err(error) => return pending.ctx.retire(Err(error)),
241                    };
242                    pending.statement.record_ids(keys, ids);
243                }
244                Ok(SourceResolution::Prepare(request)) => {
245                    let conn_id = pending.ctx.session().conn_id().clone();
246                    let config = self.controller.storage.config().clone();
247                    let tx = self.internal_cmd_tx.clone();
248                    let otel_ctx = OpenTelemetryContext::obtain();
249                    task::spawn(|| "ddl_statement_preparation", async move {
250                        let response = request.execute(&config).await;
251                        tracing::debug!(
252                            target: "mz_adapter::catalog_preparation",
253                            %conn_id, kind = "purification", "catalog preparation complete"
254                        );
255                        catalog_preparation_test_pause("catalog_purification_ready").await;
256                        if let Err(error) = tx.send(Message::SourceStatementReady {
257                            pending, response, otel_ctx,
258                        }) {
259                            warn!("internal_cmd_rx dropped before source preparation completed: {error:?}");
260                        }
261                    }.instrument(tracing::Span::current()));
262                    return;
263                }
264            }
265        };
266        let mut ctx = pending.ctx;
267        if let Err(error) = self.prepare_command_execution(&mut ctx, &compiled.plan) {
268            return ctx.retire(Err(error));
269        }
270        let response = match &compiled.plan {
271            mz_sql::plan::Plan::CreateSources(_) => crate::ExecuteResponse::CreatedSource,
272            mz_sql::plan::Plan::CreateTable(_) => crate::ExecuteResponse::CreatedTable,
273            mz_sql::plan::Plan::AlterSource(_) => {
274                crate::ExecuteResponse::AlteredObject(mz_sql::catalog::ObjectType::Source)
275            }
276            mz_sql::plan::Plan::AlterNoop(plan) => {
277                crate::ExecuteResponse::AlteredObject(plan.object_type)
278            }
279            _ => unreachable!("source command plan"),
280        };
281        if ctx.session().transaction().is_ddl() {
282            let ts = self.get_local_write_ts().await.timestamp;
283            let TransactionOps::DDL {
284                commands: _,
285                state,
286                snapshot,
287            } = &ctx
288                .session()
289                .transaction()
290                .inner()
291                .expect("DDL transaction")
292                .ops
293            else {
294                unreachable!("DDL transaction")
295            };
296            let staged = match self
297                .catalog()
298                .stage_source_statement(
299                    pending.statement,
300                    *compiled,
301                    (state, snapshot),
302                    self.active_conns.get(ctx.session().conn_id()),
303                    ts,
304                )
305                .await
306            {
307                Ok(staged) => staged,
308                Err(error) => return ctx.retire(Err(error)),
309            };
310            let command = catalog::command::CatalogCommand::Source(Arc::new(staged.command));
311            if let Err(error) = ctx
312                .session_mut()
313                .transaction_mut()
314                .add_ops(TransactionOps::DDL {
315                    commands: vec![command.clone()],
316                    state: staged.state,
317                    snapshot: staged.snapshot,
318                })
319            {
320                return ctx.retire(Err(error));
321            }
322            self.emit_command_notices(ctx.session(), &command);
323            ctx.retire(Ok(response));
324        } else {
325            let command = catalog::command::CatalogCommand::Source(Arc::new(
326                SourceCommand::unobserved(pending.statement, *compiled),
327            ));
328            self.sequence_ddl_commit(PendingDdlCommit::statement(ctx, response, vec![command]))
329                .await;
330        }
331    }
332
333    fn emit_command_notices(&self, session: &Session, command: &catalog::command::CatalogCommand) {
334        session.add_notices(command.notices().iter().cloned().map(Into::into));
335        let catalog::command::CatalogCommand::Source(command) = command else {
336            return;
337        };
338        if let Some(notice) = command.skipped_notice() {
339            session.add_notice(notice);
340            return;
341        }
342        let state = session
343            .transaction()
344            .catalog_state()
345            .unwrap_or_else(|| self.catalog().state());
346        for id in command.item_ids() {
347            let Some(entry) = state.try_get_entry(&id) else {
348                continue;
349            };
350            let Some(source) = entry.source() else {
351                continue;
352            };
353            use mz_catalog::memory::objects::DataSourceDesc;
354            let cluster_id = match &source.data_source {
355                DataSourceDesc::Ingestion { desc, cluster_id }
356                | DataSourceDesc::OldSyntaxIngestion {
357                    desc, cluster_id, ..
358                } if desc.connection.prefers_single_replica() => cluster_id,
359                _ => continue,
360            };
361            let Some(cluster) = self.catalog().try_get_cluster(*cluster_id) else {
362                continue;
363            };
364            let name = state.resolve_full_name(entry.name(), None);
365            self.notify_single_replica_sources(session, cluster, Some(&name));
366        }
367    }
368
369    pub(super) async fn sequence_rename_command(
370        &mut self,
371        mut ctx: ExecuteContext,
372        statement: Arc<mz_sql::ast::Statement<mz_sql::ast::Raw>>,
373        params: mz_sql::plan::Params,
374    ) {
375        let prepared = {
376            let catalog = self.catalog().for_session(ctx.session());
377            catalog::command::RenameCommand::prepare(
378                catalog.state(),
379                statement,
380                params,
381                ctx.session().meta(),
382            )
383        };
384        let (command, plan) = match prepared {
385            Ok(prepared) => prepared,
386            Err(error) => return ctx.retire(Err(error)),
387        };
388        if let Err(error) = self.prepare_command_execution(&mut ctx, &plan) {
389            return ctx.retire(Err(error));
390        }
391        let command = Arc::new(command);
392        if !ctx.session().transaction().is_ddl() {
393            let response = crate::ExecuteResponse::AlteredObject(command.object_type());
394            self.sequence_ddl_commit(PendingDdlCommit::statement(
395                ctx,
396                response,
397                vec![catalog::command::CatalogCommand::Rename(command)],
398            ))
399            .await;
400            return;
401        }
402        let TransactionOps::DDL {
403            state, snapshot, ..
404        } = &ctx
405            .session()
406            .transaction()
407            .inner()
408            .expect("DDL transaction")
409            .ops
410        else {
411            unreachable!("standalone command handled above")
412        };
413        let state = state.clone();
414        let snapshot = snapshot.clone();
415        let ts = self.get_local_write_ts().await.timestamp;
416        let result = Box::pin(self.catalog().transact_incremental_dry_run(
417            &state,
418            vec![Op::RenameCommand(Arc::clone(&command))],
419            self.active_conns.get(ctx.session().conn_id()),
420            Some(snapshot),
421            ts,
422        ))
423        .await
424        .and_then(|(state, snapshot)| {
425            ctx.session_mut()
426                .transaction_mut()
427                .add_ops(TransactionOps::DDL {
428                    commands: vec![catalog::command::CatalogCommand::Rename(Arc::clone(
429                        &command,
430                    ))],
431                    state,
432                    snapshot,
433                })
434        });
435        match result {
436            Ok(()) => {
437                ctx.session()
438                    .add_notices(command.notices().iter().cloned().map(Into::into));
439                ctx.retire(Ok(crate::ExecuteResponse::AlteredObject(
440                    command.object_type(),
441                )));
442            }
443            Err(error) => ctx.retire(Err(error)),
444        }
445    }
446
447    /// Same as [`Self::catalog_transact_with_context`] but takes a [`Session`].
448    #[instrument(name = "coord::catalog_transact")]
449    pub(crate) async fn catalog_transact(
450        &mut self,
451        session: Option<&Session>,
452        ops: Vec<catalog::Op>,
453    ) -> Result<(), AdapterError> {
454        if session.is_some_and(|session| session.transaction().is_ddl()) {
455            return Err(AdapterError::Unsupported(
456                "raw operations in a DDL command transaction",
457            ));
458        }
459        let start = Instant::now();
460        let result = self
461            .catalog_transact_with_context(session.map(|session| session.conn_id()), None, ops)
462            .await;
463        self.metrics
464            .catalog_transact_seconds
465            .with_label_values(&["catalog_transact"])
466            .observe(start.elapsed().as_secs_f64());
467        result
468    }
469
470    /// Same as [`Self::catalog_transact_with_context`] but takes a [`Session`]
471    /// and runs builtin table updates concurrently with any side effects (e.g.
472    /// creating collections).
473    // TODO(aljoscha): Remove this method once all call-sites have been migrated
474    // to the newer catalog_transact_with_context. The latter is what allows us
475    // to apply catalog implications that we derive from catalog chanages either
476    // when initially applying the ops to the catalog _or_ when following
477    // catalog changes from another process.
478    #[instrument(name = "coord::catalog_transact_with_side_effects")]
479    pub(crate) async fn catalog_transact_with_side_effects<F>(
480        &mut self,
481        mut ctx: Option<&mut ExecuteContext>,
482        ops: Vec<catalog::Op>,
483        side_effect: F,
484    ) -> Result<(), AdapterError>
485    where
486        F: for<'a> FnOnce(
487                &'a mut Coordinator,
488                Option<&'a mut ExecuteContext>,
489            ) -> Pin<Box<dyn Future<Output = ()> + 'a>>
490            + 'static,
491    {
492        if ctx
493            .as_ref()
494            .is_some_and(|ctx| ctx.session().transaction().is_ddl())
495        {
496            return Err(AdapterError::Unsupported(
497                "raw operations in a DDL command transaction",
498            ));
499        }
500        let start = Instant::now();
501
502        // Keep catalog work out of the inline state of callers such as ordinary
503        // transaction completion and the coordinator's command dispatcher.
504        let (table_updates, catalog_updates) = Box::pin(
505            self.catalog_transact_inner(ctx.as_ref().map(|ctx| ctx.session().conn_id()), ops),
506        )
507        .await?;
508
509        // We can't run this concurrently with the explicit side effects,
510        // because both want to borrow self mutably.
511        let apply_implications_res =
512            Box::pin(self.apply_catalog_implications(ctx.as_deref_mut(), catalog_updates)).await;
513
514        // We would get into an inconsistent state if we updated the catalog but
515        // then failed to apply commands/updates to the controller. Easiest
516        // thing to do is panic and let restart/bootstrap handle it.
517        apply_implications_res.expect("cannot fail to apply catalog update implications");
518
519        // NOTE: `check_consistency` only runs with soft assertions enabled, so
520        // this phase reads about zero in production. We time it because a local
521        // rig debugging a transact stall commonly has them on, where the check is
522        // O(catalog size) and would otherwise appear as an unexplained remainder
523        // against the wrapper metric. The observation stays outside the macro,
524        // anything inside it compiles out exactly where soft assertions are off.
525        let consistency_start = Instant::now();
526
527        // Note: It's important that we keep the function call inside macro, this way we only run
528        // the consistency checks if soft assertions are enabled.
529        mz_ore::soft_assert_eq_no_log!(
530            self.check_consistency(),
531            Ok(()),
532            "coordinator inconsistency detected"
533        );
534
535        self.metrics
536            .catalog_transact_phase_seconds
537            .with_label_values(&["consistency_check"])
538            .observe(consistency_start.elapsed().as_secs_f64());
539
540        let side_effects_seconds = self
541            .metrics
542            .catalog_transact_phase_seconds
543            .with_label_values(&["side_effects"]);
544        // Distinct from `table_updates_wait` in `catalog_transact_with_context`.
545        // Here the group commit has already been running concurrently with
546        // `apply_catalog_implications` above, so this wrapper is first polled
547        // late and only records the residual wait.
548        let table_updates_wait = self
549            .metrics
550            .catalog_transact_phase_seconds
551            .with_label_values(&["table_updates_residual_wait"]);
552        let side_effects_fut = side_effect(self, ctx);
553
554        // Run our side effects concurrently with the table updates.
555        let ((), ()) = futures::future::join(
556            side_effects_fut
557                .wall_time()
558                .observe(side_effects_seconds)
559                .instrument(info_span!(
560                    "coord::catalog_transact_with_side_effects::side_effects_fut"
561                )),
562            table_updates
563                .wall_time()
564                .observe(table_updates_wait)
565                .instrument(info_span!(
566                    "coord::catalog_transact_with_side_effects::table_updates"
567                )),
568        )
569        .await;
570
571        self.metrics
572            .catalog_transact_seconds
573            .with_label_values(&["catalog_transact_with_side_effects"])
574            .observe(start.elapsed().as_secs_f64());
575
576        Ok(())
577    }
578
579    /// Same as [`Self::catalog_transact_inner`] but takes an execution context
580    /// or connection ID and runs builtin table updates concurrently with any
581    /// catalog implications that are generated as part of applying the given
582    /// `ops` (e.g. creating collections).
583    ///
584    /// This will use a connection ID if provided and otherwise fall back to
585    /// getting a connection ID from the execution context.
586    #[instrument(name = "coord::catalog_transact_with_context")]
587    pub(crate) async fn catalog_transact_with_context(
588        &mut self,
589        conn_id: Option<&ConnectionId>,
590        ctx: Option<&mut ExecuteContext>,
591        ops: Vec<catalog::Op>,
592    ) -> Result<(), AdapterError> {
593        if ctx
594            .as_ref()
595            .is_some_and(|ctx| ctx.session().transaction().is_ddl())
596        {
597            return Err(AdapterError::Unsupported(
598                "raw operations in a DDL command transaction",
599            ));
600        }
601        let start = Instant::now();
602
603        let conn_id = conn_id.or_else(|| ctx.as_ref().map(|ctx| ctx.session().conn_id()));
604
605        let (table_updates, catalog_updates) =
606            Box::pin(self.catalog_transact_inner(conn_id, ops)).await?;
607
608        self.finish_catalog_transaction(ctx, table_updates, catalog_updates)
609            .await;
610
611        self.metrics
612            .catalog_transact_seconds
613            .with_label_values(&["catalog_transact_with_context"])
614            .observe(start.elapsed().as_secs_f64());
615
616        Ok(())
617    }
618
619    pub(super) fn release_ddl_lock(&self, guard: tokio::sync::OwnedMutexGuard<()>) {
620        drop(guard);
621        if !self.serialized_ddl.is_empty() {
622            let _ = self.internal_cmd_tx.send(Message::DeferredStatementReady);
623        }
624    }
625
626    pub(super) async fn sequence_ddl_commit(&mut self, mut pending: PendingDdlCommit) {
627        // Command replay must respect the plans other DDLs hold across off-thread work.
628        // Never await this lock on the coordinator, which its owner needs to finish.
629        let guard = self
630            .active_conns
631            .get_mut(pending.conn_id())
632            .expect("connection must exist")
633            .deferred_lock
634            .take();
635        let guard = match guard {
636            Some(guard) => guard,
637            None => match self.serialized_ddl.try_lock_owned() {
638                Ok(guard) => guard,
639                Err(_) => {
640                    tracing::debug!(
641                        conn_id = %pending.conn_id(),
642                        "deferring catalog commit for DDL lock"
643                    );
644                    self.serialized_ddl.push_back(DeferredDdl::Commit(pending));
645                    return;
646                }
647            },
648        };
649        let result = loop {
650            match self
651                .catalog_transact_commands(
652                    pending.context.context_mut(),
653                    &mut pending.commands,
654                    &mut pending.responses,
655                )
656                .await
657            {
658                Ok(()) => break Ok(()),
659                Err(catalog::CommandExecutionError::Error(error)) => break Err(error),
660                Err(catalog::CommandExecutionError::Pending { index, work }) => match work {
661                    catalog::CommandWork::Allocate(keys) => {
662                        let ids = match self.allocate_user_ids(u64::cast_from(keys.len())).await {
663                            Ok(ids) => ids,
664                            Err(error) => break Err(error),
665                        };
666                        match &mut pending.commands[index] {
667                            catalog::command::CatalogCommand::Source(command) => {
668                                Arc::make_mut(command).record_ids(keys, ids);
669                            }
670                            catalog::command::CatalogCommand::Rename(_)
671                            | catalog::command::CatalogCommand::CatalogResult(_)
672                            | catalog::command::CatalogCommand::Description(_) => {
673                                unreachable!("renames do not allocate IDs");
674                            }
675                        }
676                    }
677                    catalog::CommandWork::Prepare(request) => {
678                        self.release_ddl_lock(guard);
679                        let conn_id = pending.context.context_mut().session().conn_id().clone();
680                        let config = self.controller.storage.config().clone();
681                        let tx = self.internal_cmd_tx.clone();
682                        let otel_ctx = OpenTelemetryContext::obtain();
683                        task::spawn(|| "ddl_command_preparation", async move {
684                            let response = request.execute(&config).await;
685                            tracing::debug!(
686                                target: "mz_adapter::catalog_preparation",
687                                %conn_id, kind = "purification", "catalog preparation complete"
688                            );
689                            catalog_preparation_test_pause("catalog_purification_ready").await;
690                            pending.responses.insert(index, response);
691                            if let Err(error) =
692                                tx.send(Message::DdlCommandsReady { pending, otel_ctx })
693                            {
694                                warn!(
695                                    "internal_cmd_rx dropped before DDL preparation completed: {error:?}"
696                                );
697                            }
698                        }.instrument(tracing::Span::current()));
699                        return;
700                    }
701                },
702            }
703        };
704        self.release_ddl_lock(guard);
705        self.finish_ddl_commit(pending, result);
706    }
707
708    pub(super) fn finish_ddl_commit(
709        &self,
710        mut pending: PendingDdlCommit,
711        result: Result<(), AdapterError>,
712    ) {
713        if result.is_ok() && matches!(pending.context, DdlCommitContext::Statement { .. }) {
714            for command in &pending.commands {
715                self.emit_command_notices(pending.context.context_mut().session(), command);
716            }
717        }
718        pending.context.finish(result);
719    }
720
721    pub(crate) async fn catalog_transact_commands(
722        &mut self,
723        ctx: &mut ExecuteContext,
724        commands: &mut [catalog::command::CatalogCommand],
725        responses: &mut BTreeMap<usize, mz_sql::pure::SourcePreparationResponse>,
726    ) -> Result<(), catalog::CommandExecutionError> {
727        if self.controller.read_only() {
728            return Err(AdapterError::ReadOnly.into());
729        }
730        let start = Instant::now();
731        let phase_seconds = self.metrics.catalog_transact_phase_seconds.clone();
732        let oracle_write_ts = self
733            .get_catalog_write_ts()
734            .wall_time()
735            .observe(phase_seconds.with_label_values(&["write_ts"]))
736            .await;
737        let conn_id = ctx.session().conn_id();
738        let Coordinator {
739            catalog,
740            active_conns,
741            controller,
742            ..
743        } = self;
744        let conn = active_conns.get(conn_id).expect("connection must exist");
745        let TransactionResult {
746            builtin_table_updates,
747            catalog_updates,
748            audit_events,
749        } = Box::pin(Arc::make_mut(catalog).transact_commands(
750            Some(&mut controller.storage_collections),
751            oracle_write_ts,
752            Some(conn),
753            commands,
754            responses,
755        ))
756        .wall_time()
757        .observe(phase_seconds.with_label_values(&["transact"]))
758        .await?;
759
760        self.track_catalog_audit_events(Some(conn_id), audit_events);
761        let stage_start = Instant::now();
762        let table_updates = self.builtin_table_update().execute(builtin_table_updates);
763        phase_seconds
764            .with_label_values(&["stage_builtin"])
765            .observe(stage_start.elapsed().as_secs_f64());
766        self.finish_catalog_transaction(Some(ctx), table_updates, catalog_updates)
767            .await;
768        self.metrics
769            .catalog_transact_seconds
770            .with_label_values(&["catalog_transact_with_context"])
771            .observe(start.elapsed().as_secs_f64());
772        Ok(())
773    }
774
775    async fn finish_catalog_transaction(
776        &mut self,
777        ctx: Option<&mut ExecuteContext>,
778        table_updates: BuiltinTableAppendNotify,
779        catalog_updates: Vec<ParsedStateUpdate>,
780    ) {
781        let table_updates_wait = self
782            .metrics
783            .catalog_transact_phase_seconds
784            .with_label_values(&["table_updates_wait"]);
785        let apply_catalog_implications_fut =
786            Box::pin(self.apply_catalog_implications(ctx, catalog_updates));
787
788        // Apply catalog implications concurrently with the table updates.
789        let (combined_apply_res, ()) = futures::future::join(
790            apply_catalog_implications_fut.instrument(info_span!(
791                "coord::catalog_transact_with_context::side_effects_fut"
792            )),
793            table_updates
794                .wall_time()
795                .observe(table_updates_wait)
796                .instrument(info_span!(
797                    "coord::catalog_transact_with_context::table_updates"
798                )),
799        )
800        .await;
801
802        // We would get into an inconsistent state if we updated the catalog but
803        // then failed to apply implications. Easiest thing to do is panic and
804        // let restart/bootstrap handle it.
805        combined_apply_res.expect("cannot fail to apply catalog implications");
806
807        // See the note in `catalog_transact_with_side_effects` on why this is
808        // timed outside the macro and reads about zero in production.
809        let consistency_start = Instant::now();
810
811        // Note: It's important that we keep the function call inside macro, this way we only run
812        // the consistency checks if soft assertions are enabled.
813        mz_ore::soft_assert_eq_no_log!(
814            self.check_consistency(),
815            Ok(()),
816            "coordinator inconsistency detected"
817        );
818
819        self.metrics
820            .catalog_transact_phase_seconds
821            .with_label_values(&["consistency_check"])
822            .observe(consistency_start.elapsed().as_secs_f64());
823    }
824
825    /// Perform a catalog transaction. [`Coordinator::ship_dataflow`] must be
826    /// called after this function successfully returns on any built
827    /// [`MirDataflowDescription`](mz_compute_types::dataflows::MirDataflowDescription).
828    #[instrument(name = "coord::catalog_transact_inner")]
829    pub(crate) async fn catalog_transact_inner(
830        &mut self,
831        conn_id: Option<&ConnectionId>,
832        mut ops: Vec<catalog::Op>,
833    ) -> Result<(BuiltinTableAppendNotify, Vec<ParsedStateUpdate>), AdapterError> {
834        if self.controller.read_only() {
835            return Err(AdapterError::ReadOnly);
836        }
837
838        if let Some(scoped_op) = self.scoped_overrides_create_op(&ops) {
839            ops.push(scoped_op);
840        }
841
842        event!(Level::TRACE, ops = format!("{:?}", ops));
843
844        let phase_seconds = self.metrics.catalog_transact_phase_seconds.clone();
845        let phase_start = Instant::now();
846
847        let mut clusters_to_drop = vec![];
848        let mut cluster_replicas_to_drop = vec![];
849        let mut clusters_to_create = vec![];
850        let mut cluster_replicas_to_create = vec![];
851        let mut update_metrics_config = false;
852        let mut update_tracing_config = false;
853        let mut update_controller_config = false;
854        let mut update_compute_config = false;
855        let mut update_storage_config = false;
856        let mut update_timestamp_oracle_config = false;
857        let mut update_metrics_retention = false;
858        let mut update_secrets_caching_config = false;
859        let mut update_cluster_scheduling_config = false;
860        let mut update_http_config = false;
861        let mut update_advance_timelines_interval = false;
862        let mut update_optimizer_e2e_latency_warning_threshold = false;
863        let mut reconcile_metric_sinks = false;
864
865        for op in &ops {
866            match op {
867                catalog::Op::DropObjects(drop_object_infos) => {
868                    for drop_object_info in drop_object_infos {
869                        match &drop_object_info {
870                            catalog::DropObjectInfo::Item(_) => {
871                                // Nothing to do, these will be handled by
872                                // applying the side effects that we return.
873                            }
874                            catalog::DropObjectInfo::Cluster(id) => {
875                                clusters_to_drop.push(*id);
876                            }
877                            catalog::DropObjectInfo::ClusterReplica((
878                                cluster_id,
879                                replica_id,
880                                _reason,
881                            )) => {
882                                // Drop the cluster replica itself.
883                                cluster_replicas_to_drop.push((*cluster_id, *replica_id));
884                            }
885                            _ => (),
886                        }
887                    }
888                }
889                catalog::Op::ResetSystemConfiguration { name }
890                | catalog::Op::UpdateSystemConfiguration { name, .. } => {
891                    update_metrics_config |= self
892                        .catalog
893                        .state()
894                        .system_config()
895                        .is_metrics_config_var(name);
896                    update_tracing_config |= vars::is_tracing_var(name);
897                    update_controller_config |= self
898                        .catalog
899                        .state()
900                        .system_config()
901                        .is_controller_config_var(name);
902                    update_compute_config |= self
903                        .catalog
904                        .state()
905                        .system_config()
906                        .is_compute_config_var(name);
907                    update_storage_config |= self
908                        .catalog
909                        .state()
910                        .system_config()
911                        .is_storage_config_var(name);
912                    update_timestamp_oracle_config |= vars::is_timestamp_oracle_config_var(name);
913                    update_metrics_retention |= name == vars::METRICS_RETENTION.name();
914                    update_secrets_caching_config |= vars::is_secrets_caching_var(name);
915                    update_cluster_scheduling_config |= vars::is_cluster_scheduling_var(name);
916                    update_http_config |= vars::is_http_config_var(name);
917                    update_advance_timelines_interval |= name == DEFAULT_TIMESTAMP_INTERVAL.name();
918                    update_optimizer_e2e_latency_warning_threshold |=
919                        name == vars::OPTIMIZER_E2E_LATENCY_WARNING_THRESHOLD.name();
920                    reconcile_metric_sinks |= name == vars::DISABLED_METRIC_SINKS.name();
921                }
922                catalog::Op::ResetAllSystemConfiguration => {
923                    // Assume they all need to be updated.
924                    // We could see if the config's have actually changed, but
925                    // this is simpler.
926                    update_tracing_config = true;
927                    update_controller_config = true;
928                    update_compute_config = true;
929                    update_storage_config = true;
930                    update_timestamp_oracle_config = true;
931                    update_metrics_retention = true;
932                    update_secrets_caching_config = true;
933                    update_cluster_scheduling_config = true;
934                    update_metrics_config = true;
935                    update_http_config = true;
936                    update_advance_timelines_interval = true;
937                    update_optimizer_e2e_latency_warning_threshold = true;
938                    reconcile_metric_sinks = true;
939                }
940                catalog::Op::CreateCluster { id, .. } => {
941                    clusters_to_create.push(*id);
942                }
943                catalog::Op::CreateClusterReplica {
944                    cluster_id,
945                    name,
946                    config,
947                    ..
948                } => {
949                    cluster_replicas_to_create.push((
950                        *cluster_id,
951                        name.clone(),
952                        config.location.num_processes(),
953                    ));
954                }
955                _ => (),
956            }
957        }
958
959        // Observe before propagating, so a transaction rejected on resource
960        // limits still accounts for the op scan it burned on the loop.
961        let validate_res = self
962            .catalog()
963            .state()
964            .validate_resource_limits(&ops, conn_id.unwrap_or(&SYSTEM_CONN_ID));
965        phase_seconds
966            .with_label_values(&["prep"])
967            .observe(phase_start.elapsed().as_secs_f64());
968        validate_res?;
969
970        // This will produce timestamps that are guaranteed to increase on each
971        // call, and also never be behind the system clock. If the system clock
972        // hasn't advanced (or has gone backward), it will increment by 1. For
973        // the audit log, we need to balance "close (within 10s or so) to the
974        // system clock" and "always goes up". We've chosen here to prioritize
975        // always going up, and believe we will always be close to the system
976        // clock because it is well configured (chrony) and so may only rarely
977        // regress or pause for 10s.
978        let oracle_write_ts = self
979            .get_catalog_write_ts()
980            .wall_time()
981            .observe(phase_seconds.with_label_values(&["write_ts"]))
982            .await;
983
984        let Coordinator {
985            catalog,
986            active_conns,
987            controller,
988            cluster_replica_statuses,
989            ..
990        } = self;
991        let catalog = Arc::make_mut(catalog);
992        let conn = conn_id.map(|id| active_conns.get(id).expect("connection must exist"));
993
994        // Register the session as an ephemeral owner (its uuid <-> connection
995        // mapping) at its first temporary-item creation.
996        if let Some(conn) = conn {
997            let creates_temp_item = ops.iter().any(
998                |op| matches!(op, catalog::Op::CreateItem { item, .. } if item.is_temporary()),
999            );
1000            if creates_temp_item && !catalog.state().has_temporary_namespace(conn.conn_id()) {
1001                catalog.register_temporary_namespace(conn.conn_id(), conn.uuid());
1002            }
1003        }
1004
1005        // NOTE: This phase contains every durable `sync` and `commit` a catalog
1006        // transaction performs, which is what makes `transact` minus those two
1007        // histograms an estimate of the in-memory work. Two caveats. More than
1008        // one sync happens per transaction, so the subtraction is only valid on
1009        // rates of `_sum`, never on per-observation means. And durable
1010        // `allocate_id` (user ID pool refills, storage usage batch IDs) observes
1011        // into the same histograms from outside any catalog transaction, so the
1012        // estimate is biased low while allocation is active.
1013        let TransactionResult {
1014            builtin_table_updates,
1015            catalog_updates,
1016            audit_events,
1017        } = catalog
1018            .transact(
1019                Some(&mut controller.storage_collections),
1020                oracle_write_ts,
1021                conn,
1022                ops,
1023            )
1024            .wall_time()
1025            .observe(phase_seconds.with_label_values(&["transact"]))
1026            .await?;
1027
1028        for (cluster_id, replica_id) in &cluster_replicas_to_drop {
1029            cluster_replica_statuses.remove_cluster_replica_statuses(cluster_id, replica_id);
1030        }
1031        for cluster_id in &clusters_to_drop {
1032            cluster_replica_statuses.remove_cluster_statuses(cluster_id);
1033        }
1034        for cluster_id in clusters_to_create {
1035            cluster_replica_statuses.initialize_cluster_statuses(cluster_id);
1036        }
1037        let now = to_datetime((catalog.config().now)());
1038        for (cluster_id, replica_name, num_processes) in cluster_replicas_to_create {
1039            let replica_id = catalog
1040                .resolve_replica_in_cluster(&cluster_id, &replica_name)
1041                .expect("just created")
1042                .replica_id();
1043            cluster_replica_statuses.initialize_cluster_replica_statuses(
1044                cluster_id,
1045                replica_id,
1046                num_processes,
1047                now,
1048            );
1049        }
1050
1051        // Append our builtin table updates, then return the notify so we can run other tasks in
1052        // parallel.
1053        let stage_start = Instant::now();
1054        let builtin_update_notify = self.builtin_table_update().execute(builtin_table_updates);
1055        phase_seconds
1056            .with_label_values(&["stage_builtin"])
1057            .observe(stage_start.elapsed().as_secs_f64());
1058
1059        let finalize_start = Instant::now();
1060
1061        // No error returns are allowed after this point. Enforce this at compile time
1062        // by using this odd structure so we don't accidentally add a stray `?`.
1063        let _: () = async {
1064            if update_metrics_config {
1065                mz_metrics::update_dyncfg(&self.catalog().system_config().dyncfg_updates());
1066            }
1067            if update_controller_config {
1068                self.update_controller_config();
1069            }
1070            if update_compute_config {
1071                self.update_compute_config();
1072            }
1073            if update_storage_config {
1074                self.update_storage_config();
1075            }
1076            if update_timestamp_oracle_config {
1077                self.update_timestamp_oracle_config();
1078            }
1079            if update_metrics_retention {
1080                self.update_metrics_retention();
1081            }
1082            if update_tracing_config {
1083                self.update_tracing_config();
1084            }
1085            if update_secrets_caching_config {
1086                self.update_secrets_caching_config();
1087            }
1088            if update_cluster_scheduling_config {
1089                self.update_cluster_scheduling_config();
1090            }
1091            if update_http_config {
1092                self.update_http_config();
1093            }
1094            if update_advance_timelines_interval {
1095                let new_interval = self.catalog().system_config().default_timestamp_interval();
1096                if new_interval != self.advance_timelines_interval.period() {
1097                    self.advance_timelines_interval = tokio::time::interval(new_interval);
1098                }
1099            }
1100            if update_optimizer_e2e_latency_warning_threshold {
1101                let threshold = self
1102                    .catalog()
1103                    .system_config()
1104                    .optimizer_e2e_latency_warning_threshold();
1105                self.optimizer_metrics
1106                    .set_e2e_optimization_time_log_threshold(threshold);
1107            }
1108            if reconcile_metric_sinks {
1109                self.reconcile_metric_sinks().await;
1110            }
1111        }
1112        .instrument(info_span!("coord::catalog_transact_with::finalize"))
1113        .await;
1114
1115        self.track_catalog_audit_events(conn_id, audit_events);
1116
1117        phase_seconds
1118            .with_label_values(&["finalize"])
1119            .observe(finalize_start.elapsed().as_secs_f64());
1120
1121        Ok((builtin_update_notify, catalog_updates))
1122    }
1123
1124    fn track_catalog_audit_events(
1125        &self,
1126        conn_id: Option<&ConnectionId>,
1127        audit_events: Vec<VersionedEvent>,
1128    ) {
1129        let conn = conn_id.and_then(|id| self.active_conns.get(id));
1130        if let Some(segment_client) = &self.segment_client {
1131            for VersionedEvent::V1(event) in audit_events {
1132                let event_type = format!(
1133                    "{} {}",
1134                    event.object_type.as_title_case(),
1135                    event.event_type.as_title_case()
1136                );
1137                segment_client.environment_track(
1138                    &self.catalog().config().environment_id,
1139                    event_type,
1140                    json!({ "details": event.details.as_json() }),
1141                    EventDetails {
1142                        user_id: conn
1143                            .and_then(|c| c.user().external_metadata.as_ref())
1144                            .map(|m| m.user_id),
1145                        application_name: conn.map(|c| c.application_name()),
1146                        ..Default::default()
1147                    },
1148                );
1149            }
1150        }
1151    }
1152
1153    pub(crate) fn drop_replica(&mut self, cluster_id: ClusterId, replica_id: ReplicaId) {
1154        self.drop_introspection_subscribes(replica_id);
1155        self.drop_metric_sinks(replica_id);
1156
1157        self.controller
1158            .drop_replica(cluster_id, replica_id)
1159            .expect("dropping replica must not fail");
1160    }
1161
1162    /// A convenience method for dropping sources.
1163    pub(crate) fn drop_sources(&mut self, sources: Vec<(CatalogItemId, GlobalId)>) {
1164        for (item_id, _gid) in &sources {
1165            self.active_webhooks.remove(item_id);
1166        }
1167        let storage_metadata = self.catalog.state().storage_metadata();
1168        let source_gids = sources.into_iter().map(|(_id, gid)| gid).collect();
1169        self.controller
1170            .storage
1171            .drop_sources(storage_metadata, source_gids)
1172            .unwrap_or_terminate("cannot fail to drop sources");
1173    }
1174
1175    /// A convenience method for dropping tables.
1176    pub(crate) async fn drop_tables(&mut self, tables: Vec<(CatalogItemId, GlobalId)>) {
1177        for (item_id, _gid) in &tables {
1178            self.active_webhooks.remove(item_id);
1179        }
1180
1181        let table_gids: Vec<_> = tables.into_iter().map(|(_id, gid)| gid).collect();
1182
1183        // FIFO ordering places the forget after every staged append.
1184        let forget_ids = self
1185            .controller
1186            .storage
1187            .txns_table_ids(table_gids.clone())
1188            .unwrap_or_terminate("cannot fail to look up txns-registered tables");
1189        if !forget_ids.is_empty() {
1190            self.forget_tables_via_committer(forget_ids).await;
1191        }
1192
1193        let storage_metadata = self.catalog.state().storage_metadata();
1194        self.controller
1195            .storage
1196            .drop_tables(storage_metadata, table_gids)
1197            .unwrap_or_terminate("cannot fail to drop tables");
1198    }
1199
1200    /// Like `drop_compute_sinks`, but for a single compute sink.
1201    ///
1202    /// Returns the controller's state for the compute sink if the identified
1203    /// sink was known to the controller. It is the caller's responsibility to
1204    /// retire the returned sink. Consider using `retire_compute_sinks` instead.
1205    #[must_use]
1206    pub async fn drop_compute_sink(
1207        &mut self,
1208        sink_id: GlobalId,
1209    ) -> Option<(ActiveComputeSink, BuiltinTableAppendNotify)> {
1210        self.drop_compute_sinks([sink_id]).await.remove(&sink_id)
1211    }
1212
1213    /// Drops a batch of compute sinks.
1214    ///
1215    /// For each sink that exists, the coordinator and controller's state
1216    /// associated with the sink is removed.
1217    ///
1218    /// Returns a map from sink id to the controller's state for the sink and a notify that
1219    /// resolves once the sink's `mz_subscriptions` retraction is durable (see
1220    /// `remove_active_compute_sink`). It is the caller's responsibility to await the notify
1221    /// off the coordinator loop and then retire the returned sinks. Consider using
1222    /// `retire_compute_sinks` instead.
1223    #[must_use]
1224    pub async fn drop_compute_sinks(
1225        &mut self,
1226        sink_ids: impl IntoIterator<Item = GlobalId>,
1227    ) -> BTreeMap<GlobalId, (ActiveComputeSink, BuiltinTableAppendNotify)> {
1228        let mut by_id = BTreeMap::new();
1229        let mut by_cluster: BTreeMap<_, Vec<_>> = BTreeMap::new();
1230        for sink_id in sink_ids {
1231            let (sink, write_notify) = match self.remove_active_compute_sink(sink_id).await {
1232                None => {
1233                    // This can happen due to a race condition: an internal
1234                    // subscribe may be cleaned up via its own message while
1235                    // session disconnect cleanup is in progress. This is
1236                    // benign.
1237                    tracing::debug!(%sink_id, "drop_compute_sinks: sink already removed");
1238                    continue;
1239                }
1240                Some(entry) => entry,
1241            };
1242
1243            by_cluster
1244                .entry(sink.cluster_id())
1245                .or_default()
1246                .push(sink_id);
1247            by_id.insert(sink_id, (sink, write_notify));
1248        }
1249        for (cluster_id, ids) in by_cluster {
1250            let compute = &mut self.controller.compute;
1251            // A cluster could have been dropped, so verify it exists.
1252            if compute.instance_exists(cluster_id) {
1253                compute
1254                    .drop_collections(cluster_id, ids)
1255                    .unwrap_or_terminate("cannot fail to drop collections");
1256            }
1257        }
1258        by_id
1259    }
1260
1261    /// Retires a batch of sinks with disparate reasons for retirement.
1262    ///
1263    /// Each sink identified in `reasons` is dropped (see `drop_compute_sinks`),
1264    /// then retired with its corresponding reason. Returns a notify that resolves
1265    /// once all `mz_subscriptions` retractions are durable and the sinks are retired.
1266    pub async fn retire_compute_sinks(
1267        &mut self,
1268        mut reasons: BTreeMap<GlobalId, ActiveComputeSinkRetireReason>,
1269    ) -> BuiltinTableAppendCompletion {
1270        let sink_ids = reasons.keys().cloned();
1271        let to_retire: Vec<_> = self
1272            .drop_compute_sinks(sink_ids)
1273            .await
1274            .into_iter()
1275            .map(|(id, (sink, write_notify))| {
1276                let reason = reasons
1277                    .remove(&id)
1278                    .expect("all returned IDs are in `reasons`");
1279                (sink, write_notify, reason)
1280            })
1281            .collect();
1282
1283        if to_retire.is_empty() {
1284            return BuiltinTableAppendCompletion::completed();
1285        }
1286
1287        // Retire off the coordinator loop. We wait for each `mz_subscriptions` retraction
1288        // before telling the subscribing client that the sink is gone. The returned notify
1289        // lets statements that caused the retirement also wait before sending their response.
1290        // The wait must not happen on the coordinator loop, since that would block every
1291        // other session on the group-commit oracle round trip.
1292        let (done_tx, done_rx) = tokio::sync::oneshot::channel();
1293        task::spawn(|| "retire_compute_sinks", async move {
1294            for (sink, write_notify, reason) in to_retire {
1295                write_notify.await;
1296                sink.retire(reason);
1297            }
1298            let _ = done_tx.send(());
1299        });
1300        BuiltinTableAppendCompletion::new(Box::pin(async move {
1301            let _ = done_rx.await;
1302        }))
1303    }
1304
1305    /// Cancels all active compute sinks for the identified connection.
1306    #[mz_ore::instrument(level = "debug")]
1307    pub(crate) async fn cancel_compute_sinks_for_conn(
1308        &mut self,
1309        conn_id: &ConnectionId,
1310    ) -> BuiltinTableAppendCompletion {
1311        self.retire_compute_sinks_for_conn(conn_id, ActiveComputeSinkRetireReason::Canceled)
1312            .await
1313    }
1314
1315    /// Retires all active compute sinks for the identified connection with the
1316    /// specified reason.
1317    #[mz_ore::instrument(level = "debug")]
1318    pub(crate) async fn retire_compute_sinks_for_conn(
1319        &mut self,
1320        conn_id: &ConnectionId,
1321        reason: ActiveComputeSinkRetireReason,
1322    ) -> BuiltinTableAppendCompletion {
1323        let drop_sinks = self
1324            .active_conns
1325            .get_mut(conn_id)
1326            .expect("must exist for active session")
1327            .drop_sinks
1328            .iter()
1329            .map(|sink_id| (*sink_id, reason.clone()))
1330            .collect();
1331        self.retire_compute_sinks(drop_sinks).await
1332    }
1333
1334    pub(crate) fn drop_storage_sinks(&mut self, sink_gids: Vec<GlobalId>) {
1335        let storage_metadata = self.catalog.state().storage_metadata();
1336        self.controller
1337            .storage
1338            .drop_sinks(storage_metadata, sink_gids)
1339            .unwrap_or_terminate("cannot fail to drop sinks");
1340    }
1341
1342    pub(crate) fn drop_compute_collections(&mut self, collections: Vec<(ClusterId, GlobalId)>) {
1343        let mut by_cluster: BTreeMap<_, Vec<_>> = BTreeMap::new();
1344        for (cluster_id, gid) in collections {
1345            by_cluster.entry(cluster_id).or_default().push(gid);
1346        }
1347        for (cluster_id, gids) in by_cluster {
1348            let compute = &mut self.controller.compute;
1349            // A cluster could have been dropped, so verify it exists.
1350            if compute.instance_exists(cluster_id) {
1351                compute
1352                    .drop_collections(cluster_id, gids)
1353                    .unwrap_or_terminate("cannot fail to drop collections");
1354            }
1355        }
1356    }
1357
1358    pub(crate) fn drop_vpc_endpoints_in_background(&self, vpc_endpoints: Vec<CatalogItemId>) {
1359        // Match the create path (catalog_implications.rs) which gracefully
1360        // logs an error when cloud_resource_controller is None, rather than
1361        // panicking.
1362        let Some(cloud_resource_controller) = self.cloud_resource_controller.as_ref() else {
1363            warn!("dropping VPC endpoints without cloud_resource_controller; skipping cleanup");
1364            return;
1365        };
1366        let cloud_resource_controller = Arc::clone(cloud_resource_controller);
1367        // We don't want to block the coordinator on an external delete api
1368        // calls, so move the drop vpc_endpoint to a separate task. This does
1369        // mean that a failed drop won't bubble up to the user as an error
1370        // message. However, even if it did (and how the code previously
1371        // worked), mz has already dropped it from our catalog, and so we
1372        // wouldn't be able to retry anyway. Any orphaned vpc_endpoints will
1373        // eventually be cleaned during restart via coord bootstrap.
1374        task::spawn(
1375            || "drop_vpc_endpoints",
1376            async move {
1377                for vpc_endpoint in vpc_endpoints {
1378                    let _ = Retry::default()
1379                        .max_duration(Duration::from_secs(60))
1380                        .retry_async(|_state| async {
1381                            fail_point!("drop_vpc_endpoint", |r| {
1382                                Err(anyhow::anyhow!("Fail point error {:?}", r))
1383                            });
1384                            match cloud_resource_controller
1385                                .delete_vpc_endpoint(vpc_endpoint)
1386                                .await
1387                            {
1388                                Ok(_) => Ok(()),
1389                                Err(e) => {
1390                                    warn!("Dropping VPC Endpoints has encountered an error: {}", e);
1391                                    Err(e)
1392                                }
1393                            }
1394                        })
1395                        .await;
1396                }
1397            }
1398            .instrument(info_span!(
1399                "coord::catalog_transact_inner::drop_vpc_endpoints"
1400            )),
1401        );
1402    }
1403
1404    /// Removes all temporary items created by the specified connection, though
1405    /// not the temporary schema itself.
1406    pub(crate) async fn drop_temp_items(&mut self, conn_id: &ConnectionId) {
1407        let temp_items = self.catalog().state().get_temp_items(conn_id).collect();
1408        let all_items = self.catalog().object_dependents(&temp_items, conn_id);
1409
1410        if all_items.is_empty() {
1411            return;
1412        }
1413        let op = Op::DropObjects(
1414            all_items
1415                .into_iter()
1416                .map(DropObjectInfo::manual_drop_from_object_id)
1417                .collect(),
1418        );
1419
1420        self.catalog_transact_with_context(Some(conn_id), None, vec![op])
1421            .await
1422            .expect("unable to drop temporary items for conn_id");
1423    }
1424
1425    fn update_cluster_scheduling_config(&self) {
1426        let config = flags::orchestrator_scheduling_config(self.catalog.system_config());
1427        self.controller
1428            .update_orchestrator_scheduling_config(config);
1429    }
1430
1431    fn update_secrets_caching_config(&self) {
1432        let config = flags::caching_config(self.catalog.system_config());
1433        self.caching_secrets_reader.set_policy(config);
1434    }
1435
1436    fn update_tracing_config(&self) {
1437        let tracing = flags::tracing_config(self.catalog().system_config());
1438        tracing.apply(&self.tracing_handle);
1439    }
1440
1441    fn update_compute_config(&mut self) {
1442        let config_params = flags::compute_config(self.catalog().system_config());
1443        self.controller.compute.update_configuration(config_params);
1444    }
1445
1446    fn update_storage_config(&mut self) {
1447        let config_params = flags::storage_config(self.catalog().system_config());
1448        self.controller.storage.update_parameters(config_params);
1449    }
1450
1451    fn update_timestamp_oracle_config(&self) {
1452        let config_params = flags::timestamp_oracle_config(self.catalog().system_config());
1453        if let Some(config) = self.timestamp_oracle_config.as_ref() {
1454            config.apply_parameters(config_params)
1455        }
1456    }
1457
1458    fn update_metrics_retention(&self) {
1459        let duration = self.catalog().system_config().metrics_retention();
1460        let policy = ReadPolicy::lag_writes_by(
1461            Timestamp::new(u64::try_from(duration.as_millis()).unwrap_or_else(|_e| {
1462                tracing::error!("Absurd metrics retention duration: {duration:?}.");
1463                u64::MAX
1464            })),
1465            SINCE_GRANULARITY,
1466        );
1467        let storage_policies = self
1468            .catalog()
1469            .entries()
1470            .filter(|entry| {
1471                entry.item().is_retained_metrics_object()
1472                    && entry.item().is_compute_object_on_cluster().is_none()
1473            })
1474            .map(|entry| (entry.id(), policy.clone()))
1475            .collect::<Vec<_>>();
1476        let compute_policies = self
1477            .catalog()
1478            .entries()
1479            .filter_map(|entry| {
1480                if let (true, Some(cluster_id)) = (
1481                    entry.item().is_retained_metrics_object(),
1482                    entry.item().is_compute_object_on_cluster(),
1483                ) {
1484                    Some((cluster_id, entry.id(), policy.clone()))
1485                } else {
1486                    None
1487                }
1488            })
1489            .collect::<Vec<_>>();
1490        self.update_storage_read_policies(storage_policies);
1491        self.update_compute_read_policies(compute_policies);
1492    }
1493
1494    fn update_controller_config(&mut self) {
1495        let sys_config = self.catalog().system_config();
1496        self.controller
1497            .update_configuration(sys_config.dyncfg_updates());
1498    }
1499
1500    fn update_http_config(&mut self) {
1501        let webhook_request_limit = self
1502            .catalog()
1503            .system_config()
1504            .webhook_concurrent_request_limit();
1505        self.webhook_concurrency_limit
1506            .set_limit(webhook_request_limit);
1507    }
1508
1509    pub(crate) async fn create_storage_export(
1510        &mut self,
1511        id: GlobalId,
1512        sink: &Sink,
1513    ) -> Result<(), AdapterError> {
1514        // Validate `sink.from` is in fact a storage collection
1515        self.controller.storage.check_exists(sink.from)?;
1516
1517        // The AsOf is used to determine at what time to snapshot reading from
1518        // the persist collection.  This is primarily relevant when we do _not_
1519        // want to include the snapshot in the sink.
1520        //
1521        // We choose the smallest as_of that is legal, according to the sinked
1522        // collection's since.
1523        let id_bundle = crate::CollectionIdBundle {
1524            storage_ids: btreeset! {sink.from},
1525            compute_ids: btreemap! {},
1526        };
1527
1528        // We're putting in place read holds, such that create_exports, below,
1529        // which calls update_read_capabilities, can successfully do so.
1530        // Otherwise, the since of dependencies might move along concurrently,
1531        // pulling the rug from under us!
1532        //
1533        // TODO: Maybe in the future, pass those holds on to storage, to hold on
1534        // to them and downgrade when possible?
1535        let read_holds = self.acquire_read_holds(&id_bundle);
1536        let as_of = read_holds.least_valid_read();
1537
1538        let storage_sink_from_entry = self.catalog().get_entry_by_global_id(&sink.from);
1539        let storage_sink_desc = mz_storage_types::sinks::StorageSinkDesc {
1540            from: sink.from,
1541            from_desc: storage_sink_from_entry
1542                .relation_desc()
1543                .expect("sinks can only be built on items with descs")
1544                .into_owned(),
1545            connection: sink
1546                .connection
1547                .clone()
1548                .into_inline_connection(self.catalog().state()),
1549            envelope: sink.envelope,
1550            as_of,
1551            with_snapshot: sink.with_snapshot,
1552            version: sink.version,
1553            from_storage_metadata: (),
1554            to_storage_metadata: (),
1555            commit_interval: sink.commit_interval,
1556        };
1557
1558        let collection_desc = CollectionDescription {
1559            // TODO(sinks): make generic once we have more than one sink type.
1560            desc: KAFKA_PROGRESS_DESC.clone(),
1561            data_source: DataSource::Sink {
1562                desc: ExportDescription {
1563                    sink: storage_sink_desc,
1564                    instance_id: sink.cluster_id,
1565                },
1566            },
1567            since: None,
1568            timeline: None,
1569            primary: None,
1570        };
1571        let collections = vec![(id, collection_desc)];
1572
1573        // Create the collections.
1574        let storage_metadata = self.catalog.state().storage_metadata();
1575        let res = self
1576            .controller
1577            .storage
1578            .create_collections(storage_metadata, None, collections)
1579            .await;
1580
1581        // Drop read holds after the export has been created, at which point
1582        // storage will have put in its own read holds.
1583        drop(read_holds);
1584
1585        Ok(res?)
1586    }
1587
1588    pub(crate) fn validate_resource_limit<F>(
1589        &self,
1590        current_amount: usize,
1591        new_instances: i64,
1592        resource_limit: F,
1593        resource_type: &str,
1594        limit_name: &str,
1595    ) -> Result<(), AdapterError>
1596    where
1597        F: Fn(&SystemVars) -> u32,
1598    {
1599        self.catalog().state().validate_resource_limit(
1600            current_amount,
1601            new_instances,
1602            resource_limit,
1603            resource_type,
1604            limit_name,
1605        )
1606    }
1607
1608    pub(crate) fn validate_resource_limit_numeric<F>(
1609        &self,
1610        current_amount: Numeric,
1611        new_amount: Numeric,
1612        resource_limit: F,
1613        resource_type: &str,
1614        limit_name: &str,
1615    ) -> Result<(), AdapterError>
1616    where
1617        F: Fn(&SystemVars) -> Numeric,
1618    {
1619        self.catalog().state().validate_resource_limit_numeric(
1620            current_amount,
1621            new_amount,
1622            resource_limit,
1623            resource_type,
1624            limit_name,
1625        )
1626    }
1627}