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        let debug_name = self
140            .catalog()
141            .resolve_full_name(&plan.name, None)
142            .to_string();
143
144        // Build an optimizer for this METRIC SINK.
145        let mut optimizer = optimize::metric_sink::Optimizer::new(
146            self.owned_catalog(),
147            compute_instance,
148            view_id,
149            global_id,
150            optimizer_config,
151            self.optimizer_metrics(),
152        );
153        let span = Span::current();
154        Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
155            || "optimize create metric sink",
156            move || {
157                span.in_scope(|| {
158                    let metric_sink = optimize::metric_sink::MetricSink::new(
159                        debug_name,
160                        optimize::metric_sink::MetricSinkFrom::Id(plan.metric_sink.from),
161                        plan.metric_sink.prefix.clone(),
162                        None,
163                    );
164
165                    // MIR ⇒ MIR optimization (global)
166                    let global_mir_plan = optimizer.catch_unwind_optimize(metric_sink)?;
167                    // MIR ⇒ LIR lowering and LIR ⇒ LIR optimization (global)
168                    let global_lir_plan =
169                        optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
170
171                    let stage = CreateMetricSinkStage::Finish(CreateMetricSinkFinish {
172                        validity,
173                        item_id,
174                        global_id,
175                        plan,
176                        resolved_ids,
177                        global_mir_plan,
178                        global_lir_plan,
179                        optimizer_features,
180                    });
181                    Ok(Box::new(stage))
182                })
183            },
184        )))
185    }
186
187    #[instrument]
188    async fn create_metric_sink_finish(
189        &mut self,
190        ctx: &mut ExecuteContext,
191        stage: CreateMetricSinkFinish,
192    ) -> Result<StageResult<Box<CreateMetricSinkStage>>, AdapterError> {
193        let CreateMetricSinkFinish {
194            item_id,
195            global_id,
196            plan:
197                plan::CreateMetricSinkPlan {
198                    name,
199                    metric_sink,
200                    if_not_exists,
201                },
202            resolved_ids,
203            global_mir_plan,
204            global_lir_plan,
205            optimizer_features,
206            ..
207        } = stage;
208        let cluster_id = metric_sink.cluster_id;
209        let id_bundle = dataflow_import_id_bundle(global_lir_plan.df_desc(), cluster_id);
210
211        // Run the authoritative prefix-free check here in the finish stage, not in optimize:
212        // optimize runs off the coordinator thread, so another sink could commit between the two
213        // stages. See `ensure_metric_sink_prefix_is_free`.
214        self.ensure_metric_sink_prefix_is_free(&name, cluster_id, &metric_sink.prefix)?;
215
216        let owner_id = *ctx.session().current_role_id();
217        let ops = vec![catalog::Op::CreateItem {
218            id: item_id,
219            name: name.clone(),
220            item: CatalogItem::MetricSink(MetricSink {
221                create_sql: metric_sink.create_sql,
222                global_id,
223                from: metric_sink.from,
224                resolved_ids,
225                cluster_id,
226                prefix: metric_sink.prefix,
227                optimized_plan: None,
228                physical_plan: None,
229                dataflow_metainfo: None,
230            }),
231            owner_id,
232        }];
233
234        // Render optimizer notices before the catalog transaction: this way notice text resolves
235        // the new sink's own `global_id` to its intended human-readable name rather than a bare
236        // transient id.
237        let (df_desc, raw_df_meta) = global_lir_plan.unapply();
238        let from_entry = self.catalog().get_entry_by_global_id(&metric_sink.from);
239        let from_desc = from_entry
240            .relation_desc()
241            .expect("can only create a metric sink on items with a valid description");
242        let df_meta = self.render_create_item_notices(&name, global_id, &from_desc, &raw_df_meta);
243
244        // Populate the durable expression cache before the catalog transaction and await the
245        // write. This way any other envd (or a subsequent bootstrap here) will observe the cached
246        // plans + rendered notices as soon as the item becomes visible. Metric sinks have no local
247        // MIR (the pipeline starts from a `GlobalId`), so there is no local expression to cache.
248        self.catalog()
249            .cache_expressions(
250                global_id,
251                None,
252                global_mir_plan.df_desc().clone(),
253                df_desc.clone(),
254                df_meta.clone(),
255                optimizer_features,
256            )
257            .await;
258
259        let transact_result = self
260            .catalog_transact_with_side_effects(Some(ctx), ops, move |coord, _ctx| {
261                Box::pin(async move {
262                    // Save plan structures.
263                    coord
264                        .catalog_mut()
265                        .set_optimized_plan(global_id, global_mir_plan.df_desc().clone());
266                    coord
267                        .catalog_mut()
268                        .set_physical_plan(global_id, df_desc.clone());
269
270                    let notice_builtin_updates_fut =
271                        coord.persist_dataflow_metainfo(df_meta, global_id);
272
273                    coord
274                        .ship_new_dataflow(
275                            &id_bundle,
276                            df_desc,
277                            cluster_id,
278                            notice_builtin_updates_fut,
279                        )
280                        .await;
281                    // No `allow_writes` here: metric sinks write to the in-process metrics
282                    // registry, not to external/persist state.
283                })
284            })
285            .await;
286
287        match transact_result {
288            Ok(_) => {
289                self.emit_raw_optimizer_notices_to_user(ctx, &raw_df_meta.optimizer_notices);
290                Ok(StageResult::Response(ExecuteResponse::CreatedMetricSink))
291            }
292            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
293                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
294            })) if if_not_exists => {
295                ctx.session()
296                    .add_notice(AdapterNotice::ObjectAlreadyExists {
297                        name: name.item,
298                        ty: "metric sink",
299                    });
300                Ok(StageResult::Response(ExecuteResponse::CreatedMetricSink))
301            }
302            Err(err) => Err(err),
303        }
304    }
305
306    /// Rejects `prefix` if it is a prefix of, or has as a prefix, any metric sink already on
307    /// `cluster_id`.
308    ///
309    /// Prefix-free, not just distinct: the published name is `prefix + metric_name`, so `a_`
310    /// + `b_c` and `a_b_` + `c` both publish `a_b_c`, and Prometheus silently merges same-named
311    /// families. Uniqueness only holds per cluster: the registry is process-local and every
312    /// replica of a cluster runs the same sinks.
313    ///
314    /// This is the authoritative check, not the plan-time one. Planning is not serialized
315    /// against catalog writes, so two creates can plan against the same state. The coordinator
316    /// sequences one statement at a time, and nothing commits between here and
317    /// `catalog_transact`.
318    ///
319    /// A sink already holding `name` is skipped: the create is then a no-op (`IF NOT EXISTS`)
320    /// or an "already exists" error, neither of which publishes anything new.
321    fn ensure_metric_sink_prefix_is_free(
322        &self,
323        name: &QualifiedItemName,
324        cluster_id: ClusterId,
325        prefix: &str,
326    ) -> Result<(), AdapterError> {
327        let cluster = self.catalog().get_cluster(cluster_id);
328        for item_id in &cluster.bound_objects {
329            let entry = self.catalog().get_entry(item_id);
330            let CatalogItem::MetricSink(existing) = entry.item() else {
331                continue;
332            };
333            if entry.name() == name {
334                continue;
335            }
336            if existing.prefix.starts_with(prefix) || prefix.starts_with(&existing.prefix) {
337                return Err(AdapterError::Unstructured(anyhow!(
338                    "metric sink prefix {:?} conflicts with prefix {:?} of metric sink {} on \
339                     cluster {}",
340                    prefix,
341                    existing.prefix,
342                    entry.name().item,
343                    cluster.name,
344                )));
345            }
346        }
347        Ok(())
348    }
349}