Skip to main content

mz_adapter/coord/sequencer/inner/
create_metric_sink.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//! `CREATE METRIC SINK` sequencing.
11//!
12//! Staged like `CREATE INDEX`: optimization runs off the coordinator thread, then the finish stage
13//! writes the durable catalog item and ships the dataflow inside one catalog transaction.
14
15use anyhow::anyhow;
16use mz_catalog::memory::error::ErrorKind;
17use mz_catalog::memory::objects::{CatalogItem, MetricSink};
18use mz_controller_types::ClusterId;
19use mz_ore::instrument;
20use mz_repr::optimize::OverrideFrom;
21use mz_sql::catalog::CatalogError;
22use mz_sql::names::{QualifiedItemName, ResolvedIds};
23use mz_sql::plan;
24use mz_sql::session::metadata::SessionMetadata;
25use tracing::Span;
26
27use crate::command::ExecuteResponse;
28use crate::coord::sequencer::inner::return_if_err;
29use crate::coord::{
30    Coordinator, CreateMetricSinkFinish, CreateMetricSinkOptimize, CreateMetricSinkStage, Message,
31    PlanValidity, StageResult, Staged,
32};
33use crate::error::AdapterError;
34use crate::optimize::dataflows::dataflow_import_id_bundle;
35use crate::optimize::{self, Optimize};
36use crate::session::Session;
37use crate::{AdapterNotice, ExecuteContext, catalog};
38
39impl Staged for CreateMetricSinkStage {
40    type Ctx = ExecuteContext;
41
42    fn validity(&mut self) -> &mut PlanValidity {
43        match self {
44            Self::Optimize(stage) => &mut stage.validity,
45            Self::Finish(stage) => &mut stage.validity,
46        }
47    }
48
49    async fn stage(
50        self,
51        coord: &mut Coordinator,
52        ctx: &mut ExecuteContext,
53    ) -> Result<StageResult<Box<Self>>, AdapterError> {
54        match self {
55            CreateMetricSinkStage::Optimize(stage) => {
56                coord.create_metric_sink_optimize(stage).await
57            }
58            CreateMetricSinkStage::Finish(stage) => {
59                coord.create_metric_sink_finish(ctx, stage).await
60            }
61        }
62    }
63
64    fn message(self, ctx: ExecuteContext, span: Span) -> Message {
65        Message::CreateMetricSinkStageReady {
66            ctx,
67            span,
68            stage: self,
69        }
70    }
71
72    fn cancel_enabled(&self) -> bool {
73        true
74    }
75}
76
77impl Coordinator {
78    #[instrument]
79    pub(crate) async fn sequence_create_metric_sink(
80        &mut self,
81        ctx: ExecuteContext,
82        plan: plan::CreateMetricSinkPlan,
83        resolved_ids: ResolvedIds,
84    ) {
85        let stage = return_if_err!(
86            self.create_metric_sink_validate(ctx.session(), plan, resolved_ids),
87            ctx
88        );
89        self.sequence_staged(ctx, Span::current(), stage).await;
90    }
91
92    #[instrument]
93    fn create_metric_sink_validate(
94        &self,
95        session: &Session,
96        plan: plan::CreateMetricSinkPlan,
97        resolved_ids: ResolvedIds,
98    ) -> Result<CreateMetricSinkStage, AdapterError> {
99        // Track the target cluster and resolved dependencies so concurrent drops are caught
100        // between stages instead of panicking later when the dataflow is shipped.
101        let validity = PlanValidity::new(
102            self.catalog(),
103            resolved_ids.items().copied().collect(),
104            Some(plan.metric_sink.cluster_id),
105            None,
106            session.role_metadata().clone(),
107        );
108        Ok(CreateMetricSinkStage::Optimize(CreateMetricSinkOptimize {
109            validity,
110            plan,
111            resolved_ids,
112        }))
113    }
114
115    #[instrument]
116    async fn create_metric_sink_optimize(
117        &mut self,
118        CreateMetricSinkOptimize {
119            validity,
120            plan,
121            resolved_ids,
122        }: CreateMetricSinkOptimize,
123    ) -> Result<StageResult<Box<CreateMetricSinkStage>>, AdapterError> {
124        let cluster_id = plan.metric_sink.cluster_id;
125
126        // Collect optimizer parameters.
127        let compute_instance = self
128            .instance_snapshot(cluster_id)
129            .expect("compute instance does not exist");
130        let (item_id, global_id) = self.allocate_user_id().await?;
131        // A transient id for the view the optimizer builds over `from` to shape its rows (see
132        // `optimize::metric_sink::shape_metric_sink_source`); scoped to this dataflow, not durable.
133        let (_, view_id) = self.allocate_transient_id();
134
135        let optimizer_config = optimize::OptimizerConfig::from(self.catalog().system_config())
136            .override_from(&self.catalog.get_cluster(cluster_id).config.features())
137            .override_from(&self.cluster_scoped_optimizer_overrides(cluster_id));
138        let optimizer_features = optimizer_config.features.clone();
139
140        // Build an optimizer for this METRIC SINK.
141        let mut optimizer = optimize::metric_sink::Optimizer::new(
142            self.owned_catalog(),
143            compute_instance,
144            view_id,
145            global_id,
146            optimizer_config,
147            self.optimizer_metrics(),
148        );
149        let span = Span::current();
150        Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
151            || "optimize create metric sink",
152            move || {
153                span.in_scope(|| {
154                    let metric_sink = optimize::metric_sink::MetricSink::new(
155                        plan.name.clone(),
156                        plan.metric_sink.from,
157                        plan.metric_sink.prefix.clone(),
158                    );
159
160                    // MIR ⇒ MIR optimization (global)
161                    let global_mir_plan = optimizer.catch_unwind_optimize(metric_sink)?;
162                    // MIR ⇒ LIR lowering and LIR ⇒ LIR optimization (global)
163                    let global_lir_plan =
164                        optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
165
166                    let stage = CreateMetricSinkStage::Finish(CreateMetricSinkFinish {
167                        validity,
168                        item_id,
169                        global_id,
170                        plan,
171                        resolved_ids,
172                        global_mir_plan,
173                        global_lir_plan,
174                        optimizer_features,
175                    });
176                    Ok(Box::new(stage))
177                })
178            },
179        )))
180    }
181
182    #[instrument]
183    async fn create_metric_sink_finish(
184        &mut self,
185        ctx: &mut ExecuteContext,
186        stage: CreateMetricSinkFinish,
187    ) -> Result<StageResult<Box<CreateMetricSinkStage>>, AdapterError> {
188        let CreateMetricSinkFinish {
189            item_id,
190            global_id,
191            plan:
192                plan::CreateMetricSinkPlan {
193                    name,
194                    metric_sink,
195                    if_not_exists,
196                },
197            resolved_ids,
198            global_mir_plan,
199            global_lir_plan,
200            optimizer_features,
201            ..
202        } = stage;
203        let cluster_id = metric_sink.cluster_id;
204        let id_bundle = dataflow_import_id_bundle(global_lir_plan.df_desc(), cluster_id);
205
206        // Run the authoritative prefix-free check here in the finish stage, not in optimize:
207        // optimize runs off the coordinator thread, so another sink could commit between the two
208        // stages. See `ensure_metric_sink_prefix_is_free`.
209        self.ensure_metric_sink_prefix_is_free(&name, cluster_id, &metric_sink.prefix)?;
210
211        let owner_id = *ctx.session().current_role_id();
212        let ops = vec![catalog::Op::CreateItem {
213            id: item_id,
214            name: name.clone(),
215            item: CatalogItem::MetricSink(MetricSink {
216                create_sql: metric_sink.create_sql,
217                global_id,
218                from: metric_sink.from,
219                resolved_ids,
220                cluster_id,
221                prefix: metric_sink.prefix,
222                optimized_plan: None,
223                physical_plan: None,
224                dataflow_metainfo: None,
225            }),
226            owner_id,
227        }];
228
229        // Render optimizer notices before the catalog transaction: this way notice text resolves
230        // the new sink's own `global_id` to its intended human-readable name rather than a bare
231        // transient id.
232        let (df_desc, raw_df_meta) = global_lir_plan.unapply();
233        let from_entry = self.catalog().get_entry_by_global_id(&metric_sink.from);
234        let from_desc = from_entry
235            .relation_desc()
236            .expect("can only create a metric sink on items with a valid description");
237        let df_meta = self.render_create_item_notices(&name, global_id, &from_desc, &raw_df_meta);
238
239        // Populate the durable expression cache before the catalog transaction and await the
240        // write. This way any other envd (or a subsequent bootstrap here) will observe the cached
241        // plans + rendered notices as soon as the item becomes visible. Metric sinks have no local
242        // MIR (the pipeline starts from a `GlobalId`), so there is no local expression to cache.
243        self.catalog()
244            .cache_expressions(
245                global_id,
246                None,
247                global_mir_plan.df_desc().clone(),
248                df_desc.clone(),
249                df_meta.clone(),
250                optimizer_features,
251            )
252            .await;
253
254        let transact_result = self
255            .catalog_transact_with_side_effects(Some(ctx), ops, move |coord, _ctx| {
256                Box::pin(async move {
257                    // Save plan structures.
258                    coord
259                        .catalog_mut()
260                        .set_optimized_plan(global_id, global_mir_plan.df_desc().clone());
261                    coord
262                        .catalog_mut()
263                        .set_physical_plan(global_id, df_desc.clone());
264
265                    let notice_builtin_updates_fut =
266                        coord.persist_dataflow_metainfo(df_meta, global_id);
267
268                    coord
269                        .ship_new_dataflow(
270                            &id_bundle,
271                            df_desc,
272                            cluster_id,
273                            notice_builtin_updates_fut,
274                        )
275                        .await;
276                    // No `allow_writes` here: metric sinks write to the in-process metrics
277                    // registry, not to external/persist state.
278                })
279            })
280            .await;
281
282        match transact_result {
283            Ok(_) => {
284                self.emit_raw_optimizer_notices_to_user(ctx, &raw_df_meta.optimizer_notices);
285                Ok(StageResult::Response(ExecuteResponse::CreatedMetricSink))
286            }
287            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
288                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
289            })) if if_not_exists => {
290                ctx.session()
291                    .add_notice(AdapterNotice::ObjectAlreadyExists {
292                        name: name.item,
293                        ty: "metric sink",
294                    });
295                Ok(StageResult::Response(ExecuteResponse::CreatedMetricSink))
296            }
297            Err(err) => Err(err),
298        }
299    }
300
301    /// Rejects `prefix` if it is a prefix of, or has as a prefix, any metric sink already on
302    /// `cluster_id`.
303    ///
304    /// Prefix-free, not just distinct: the published name is `prefix + metric_name`, so `a_`
305    /// + `b_c` and `a_b_` + `c` both publish `a_b_c`, and Prometheus silently merges same-named
306    /// families. Uniqueness only holds per cluster: the registry is process-local and every
307    /// replica of a cluster runs the same sinks.
308    ///
309    /// This is the authoritative check, not the plan-time one. Planning is not serialized
310    /// against catalog writes, so two creates can plan against the same state. The coordinator
311    /// sequences one statement at a time, and nothing commits between here and
312    /// `catalog_transact`.
313    ///
314    /// A sink already holding `name` is skipped: the create is then a no-op (`IF NOT EXISTS`)
315    /// or an "already exists" error, neither of which publishes anything new.
316    fn ensure_metric_sink_prefix_is_free(
317        &self,
318        name: &QualifiedItemName,
319        cluster_id: ClusterId,
320        prefix: &str,
321    ) -> Result<(), AdapterError> {
322        let cluster = self.catalog().get_cluster(cluster_id);
323        for item_id in &cluster.bound_objects {
324            let entry = self.catalog().get_entry(item_id);
325            let CatalogItem::MetricSink(existing) = entry.item() else {
326                continue;
327            };
328            if entry.name() == name {
329                continue;
330            }
331            if existing.prefix.starts_with(prefix) || prefix.starts_with(&existing.prefix) {
332                return Err(AdapterError::Unstructured(anyhow!(
333                    "metric sink prefix {:?} conflicts with prefix {:?} of metric sink {} on \
334                     cluster {}",
335                    prefix,
336                    existing.prefix,
337                    entry.name().item,
338                    cluster.name,
339                )));
340            }
341        }
342        Ok(())
343    }
344}