Skip to main content

mz_adapter/coord/sequencer/inner/
subscribe.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
10use maplit::btreemap;
11use mz_adapter_types::connection::ConnectionId;
12use mz_cluster_client::ReplicaId;
13use mz_compute_types::ComputeInstanceId;
14use mz_compute_types::dataflows::DataflowDescription;
15use mz_compute_types::plan::LirRelationExpr;
16use mz_ore::collections::CollectionExt;
17use mz_ore::instrument;
18use mz_repr::GlobalId;
19use mz_repr::Timestamp;
20use mz_repr::explain::{ExprHumanizerExt, TransientItem};
21use mz_repr::optimize::{OptimizerFeatures, OverrideFrom};
22use mz_sql::plan::{self, QueryWhen, SubscribeFrom};
23use mz_sql::session::metadata::SessionMetadata;
24use std::collections::BTreeSet;
25use timely::progress::Antichain;
26use tokio::sync::mpsc;
27use tracing::{Instrument, Span};
28use uuid::Uuid;
29
30use crate::active_compute_sink::{ActiveComputeSink, ActiveSubscribe};
31use crate::command::ExecuteResponse;
32use crate::coord::appends::BuiltinTableAppendNotify;
33use crate::coord::sequencer::inner::{return_if_err, spawn_linearized_read_ts};
34use crate::coord::sequencer::{check_log_reads, emit_optimizer_notices};
35use crate::coord::{
36    Coordinator, ExplainContext, ExplainPlanContext, Message, PlanValidity, StageResult, Staged,
37    SubscribeExplain, SubscribeFinish, SubscribeLinearizeTimestamp, SubscribeOptimizeMir,
38    SubscribeStage, SubscribeTimestampOptimizeLir, TargetCluster,
39};
40use crate::error::AdapterError;
41use crate::explain::optimizer_trace::OptimizerTrace;
42use crate::optimize::Optimize;
43use crate::session::{Session, TransactionOps};
44use crate::{
45    AdapterNotice, ExecuteContext, ExecuteContextGuard, ReadHolds, TimelineContext, optimize,
46};
47
48impl Staged for SubscribeStage {
49    type Ctx = ExecuteContext;
50
51    fn validity(&mut self) -> &mut PlanValidity {
52        match self {
53            SubscribeStage::OptimizeMir(stage) => &mut stage.validity,
54            SubscribeStage::LinearizeTimestamp(stage) => &mut stage.validity,
55            SubscribeStage::TimestampOptimizeLir(stage) => &mut stage.validity,
56            SubscribeStage::Finish(stage) => &mut stage.validity,
57            SubscribeStage::Explain(stage) => &mut stage.validity,
58        }
59    }
60
61    async fn stage(
62        self,
63        coord: &mut Coordinator,
64        ctx: &mut ExecuteContext,
65    ) -> Result<StageResult<Box<Self>>, AdapterError> {
66        match self {
67            SubscribeStage::OptimizeMir(stage) => coord.subscribe_optimize_mir(stage),
68            SubscribeStage::LinearizeTimestamp(stage) => {
69                coord
70                    .subscribe_linearize_timestamp(ctx.session(), stage)
71                    .await
72            }
73            SubscribeStage::TimestampOptimizeLir(stage) => {
74                coord.subscribe_timestamp_optimize_lir(ctx, stage).await
75            }
76            SubscribeStage::Finish(stage) => coord.subscribe_finish(ctx, stage).await,
77            SubscribeStage::Explain(stage) => coord.subscribe_explain(ctx.session(), stage).await,
78        }
79    }
80
81    fn message(self, ctx: ExecuteContext, span: Span) -> Message {
82        Message::SubscribeStageReady {
83            ctx,
84            span,
85            stage: self,
86        }
87    }
88
89    fn cancel_enabled(&self) -> bool {
90        // `Finish` installs the sink before it waits for the builtin-table write
91        // off-loop. If cancellation won during that wait, the active sink would
92        // outlive the canceled execution.
93        !matches!(self, SubscribeStage::Finish(_))
94    }
95}
96
97impl Coordinator {
98    #[instrument]
99    pub(crate) async fn sequence_subscribe(
100        &mut self,
101        mut ctx: ExecuteContext,
102        plan: plan::SubscribePlan,
103        target_cluster: TargetCluster,
104    ) {
105        let stage = return_if_err!(
106            self.subscribe_validate(
107                ctx.session_mut(),
108                plan,
109                target_cluster,
110                ExplainContext::None
111            ),
112            ctx
113        );
114        self.sequence_staged(ctx, Span::current(), stage).await;
115    }
116
117    #[instrument]
118    pub(crate) async fn explain_subscribe(
119        &mut self,
120        mut ctx: ExecuteContext,
121        plan::ExplainPlanPlan {
122            stage,
123            format,
124            config,
125            explainee,
126        }: plan::ExplainPlanPlan,
127        target_cluster: TargetCluster,
128    ) {
129        let plan::Explainee::Statement(stmt) = explainee else {
130            // This is currently asserted in the `sequence_explain_plan` code that
131            // calls this method.
132            unreachable!()
133        };
134        let plan::ExplaineeStatement::Subscribe { broken, plan } = stmt else {
135            // This is currently asserted in the `sequence_explain_plan` code that
136            // calls this method.
137            unreachable!()
138        };
139
140        let desc = match &plan.from {
141            SubscribeFrom::Id(_) => None,
142            SubscribeFrom::Query { desc, .. } => Some(desc.clone()),
143        };
144
145        // Create an OptimizerTrace instance to collect plans emitted when
146        // executing the optimizer pipeline.
147        let optimizer_trace = OptimizerTrace::new(stage.paths());
148
149        let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
150            broken,
151            config,
152            format,
153            stage,
154            replan: None,
155            desc,
156            optimizer_trace,
157        });
158        let stage = return_if_err!(
159            self.subscribe_validate(ctx.session_mut(), plan, target_cluster, explain_ctx),
160            ctx
161        );
162        self.sequence_staged(ctx, Span::current(), stage).await;
163    }
164
165    #[instrument]
166    fn subscribe_validate(
167        &self,
168        session: &mut Session,
169        plan: plan::SubscribePlan,
170        target_cluster: TargetCluster,
171        explain_ctx: ExplainContext,
172    ) -> Result<SubscribeStage, AdapterError> {
173        let plan::SubscribePlan { from, when, .. } = &plan;
174
175        let cluster = self
176            .catalog()
177            .resolve_target_cluster(target_cluster, session)?;
178        let cluster_id = cluster.id;
179
180        // Only check cluster replicas if we're not in explain mode.
181        if explain_ctx.needs_cluster() && cluster.replicas().next().is_none() {
182            return Err(AdapterError::NoClusterReplicasAvailable {
183                name: cluster.name.clone(),
184                is_managed: cluster.is_managed(),
185            });
186        }
187
188        let mut replica_id = session
189            .vars()
190            .cluster_replica()
191            .map(|name| {
192                cluster
193                    .replica_id(name)
194                    .ok_or(AdapterError::UnknownClusterReplica {
195                        cluster_name: cluster.name.clone(),
196                        replica_name: name.to_string(),
197                    })
198            })
199            .transpose()?;
200
201        // SUBSCRIBE AS OF, similar to peeks, doesn't need to worry about transaction
202        // timestamp semantics.
203        if explain_ctx.needs_cluster() && when == &QueryWhen::Immediately {
204            // If this isn't a SUBSCRIBE AS OF, the SUBSCRIBE can be in a transaction if it's the
205            // only operation.
206            session.add_transaction_ops(TransactionOps::Subscribe)?;
207        }
208
209        let depends_on = from.depends_on();
210
211        // Run `check_log_reads` and emit notices.
212        let notices = check_log_reads(
213            self.catalog(),
214            cluster,
215            &depends_on,
216            &mut replica_id,
217            session.vars(),
218        )?;
219        session.add_notices(notices);
220
221        // Determine timeline.
222        let mut timeline = self
223            .catalog()
224            .validate_timeline_context(depends_on.iter().copied())?;
225        if matches!(timeline, TimelineContext::TimestampIndependent) && from.contains_temporal() {
226            // If the from IDs are timestamp independent but the query contains temporal functions
227            // then the timeline context needs to be upgraded to timestamp dependent.
228            timeline = TimelineContext::TimestampDependent;
229        }
230
231        let dependencies = depends_on
232            .iter()
233            .map(|id| self.catalog().resolve_item_id(id))
234            .collect();
235        let validity = PlanValidity::new(
236            self.catalog(),
237            dependencies,
238            Some(cluster_id),
239            replica_id,
240            session.role_metadata().clone(),
241        );
242
243        Ok(SubscribeStage::OptimizeMir(SubscribeOptimizeMir {
244            validity,
245            plan,
246            timeline,
247            dependency_ids: depends_on,
248            cluster_id,
249            replica_id,
250            explain_ctx,
251        }))
252    }
253
254    #[instrument]
255    fn subscribe_optimize_mir(
256        &self,
257        SubscribeOptimizeMir {
258            mut validity,
259            plan,
260            timeline,
261            dependency_ids,
262            cluster_id,
263            replica_id,
264            explain_ctx,
265        }: SubscribeOptimizeMir,
266    ) -> Result<StageResult<Box<SubscribeStage>>, AdapterError> {
267        let plan::SubscribePlan {
268            with_snapshot,
269            up_to,
270            ..
271        } = &plan;
272
273        // Collect optimizer parameters.
274        let compute_instance = self
275            .instance_snapshot(cluster_id)
276            .expect("compute instance does not exist");
277        let (_, view_id) = self.allocate_transient_id();
278        let (_, sink_id) = self.allocate_transient_id();
279        let debug_name = format!("subscribe-{}", sink_id);
280        let optimizer_config = optimize::OptimizerConfig::from(self.catalog().system_config())
281            .override_from(&self.catalog.get_cluster(cluster_id).config.features())
282            .override_from(&self.cluster_scoped_optimizer_overrides(cluster_id))
283            .override_from(&explain_ctx);
284
285        // Build an optimizer for this SUBSCRIBE.
286        let mut optimizer = optimize::subscribe::Optimizer::new(
287            self.owned_catalog(),
288            compute_instance,
289            view_id,
290            sink_id,
291            *with_snapshot,
292            *up_to,
293            debug_name,
294            optimizer_config,
295            self.optimizer_metrics(),
296        );
297        let catalog = self.owned_catalog();
298
299        let span = Span::current();
300        Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
301            || "optimize subscribe (mir)",
302            move || {
303                span.in_scope(|| {
304                    let _dispatch_guard = explain_ctx.dispatch_guard();
305
306                    // MIR ⇒ MIR optimization (global)
307                    let global_mir_plan = optimizer.catch_unwind_optimize(plan.clone())?;
308                    // Add introduced indexes as validity dependencies.
309                    validity.extend_dependencies(
310                        &catalog,
311                        global_mir_plan
312                            .id_bundle(optimizer.cluster_id())
313                            .iter()
314                            .map(|id| catalog.resolve_item_id(&id)),
315                    );
316
317                    let stage = SubscribeStage::LinearizeTimestamp(SubscribeLinearizeTimestamp {
318                        validity,
319                        plan,
320                        timeline,
321                        optimizer,
322                        global_mir_plan,
323                        dependency_ids,
324                        replica_id,
325                        explain_ctx,
326                    });
327                    Ok(Box::new(stage))
328                })
329            },
330        )))
331    }
332
333    /// Possibly linearize a timestamp from a `TimestampOracle`, off the
334    /// coordinator loop.
335    #[instrument]
336    async fn subscribe_linearize_timestamp(
337        &self,
338        session: &Session,
339        SubscribeLinearizeTimestamp {
340            validity,
341            plan,
342            timeline,
343            optimizer,
344            global_mir_plan,
345            dependency_ids,
346            replica_id,
347            explain_ctx,
348        }: SubscribeLinearizeTimestamp,
349    ) -> Result<StageResult<Box<SubscribeStage>>, AdapterError> {
350        let oracle = self.linearized_read_ts_oracle(session, &timeline, &plan.when);
351
352        let build_stage = move |oracle_read_ts: Option<Timestamp>| {
353            SubscribeStage::TimestampOptimizeLir(SubscribeTimestampOptimizeLir {
354                validity,
355                plan,
356                timeline,
357                optimizer,
358                global_mir_plan,
359                dependency_ids,
360                replica_id,
361                oracle_read_ts,
362                explain_ctx,
363            })
364        };
365
366        Ok(spawn_linearized_read_ts(
367            oracle,
368            "subscribe linearize timestamp",
369            build_stage,
370        ))
371    }
372
373    #[instrument]
374    async fn subscribe_timestamp_optimize_lir(
375        &mut self,
376        ctx: &ExecuteContext,
377        SubscribeTimestampOptimizeLir {
378            validity,
379            plan,
380            timeline,
381            mut optimizer,
382            global_mir_plan,
383            dependency_ids,
384            replica_id,
385            oracle_read_ts,
386            explain_ctx,
387        }: SubscribeTimestampOptimizeLir,
388    ) -> Result<StageResult<Box<SubscribeStage>>, AdapterError> {
389        let plan::SubscribePlan { when, .. } = &plan;
390
391        // Timestamp selection. The linearized read timestamp was already
392        // obtained off the coordinator loop in the preceding stage.
393        let bundle = &global_mir_plan.id_bundle(optimizer.cluster_id());
394        let (determination, read_holds) = self.determine_timestamp(
395            ctx.session(),
396            bundle,
397            when,
398            optimizer.cluster_id(),
399            &timeline,
400            oracle_read_ts,
401            None,
402        )?;
403
404        let as_of = determination.timestamp_context.timestamp_or_default();
405
406        if let Some(id) = ctx.extra().contents() {
407            self.set_statement_execution_timestamp(id, as_of);
408        }
409        if let Some(up_to) = optimizer.up_to() {
410            if as_of == up_to {
411                ctx.session()
412                    .add_notice(AdapterNotice::EqualSubscribeBounds { bound: up_to });
413            } else if as_of > up_to {
414                return Err(AdapterError::AbsurdSubscribeBounds { as_of, up_to });
415            }
416        }
417
418        self.store_transaction_read_holds(ctx.session().conn_id().clone(), read_holds);
419
420        let global_mir_plan = global_mir_plan.resolve(Antichain::from_elem(as_of));
421
422        // Optimize LIR
423        let span = Span::current();
424        Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
425            || "optimize subscribe (lir)",
426            move || {
427                span.in_scope(|| {
428                    let _dispatch_guard = explain_ctx.dispatch_guard();
429
430                    let cluster_id = optimizer.cluster_id();
431
432                    let mut pipeline = || -> Result<_, AdapterError> {
433                        // MIR ⇒ LIR lowering and LIR ⇒ LIR optimization (global)
434                        let global_lir_plan =
435                            optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
436                        Ok(global_lir_plan)
437                    };
438
439                    let stage = match pipeline() {
440                        Ok(global_lir_plan) => {
441                            if let ExplainContext::Plan(explain_ctx) = explain_ctx {
442                                let (_, df_meta) = global_lir_plan.unapply();
443                                SubscribeStage::Explain(SubscribeExplain {
444                                    validity,
445                                    optimizer,
446                                    df_meta,
447                                    cluster_id,
448                                    explain_ctx,
449                                })
450                            } else {
451                                SubscribeStage::Finish(SubscribeFinish {
452                                    validity,
453                                    cluster_id,
454                                    plan,
455                                    global_lir_plan,
456                                    dependency_ids,
457                                    replica_id,
458                                })
459                            }
460                        }
461                        Err(err) => {
462                            let ExplainContext::Plan(explain_ctx) = explain_ctx else {
463                                return Err(err);
464                            };
465
466                            if explain_ctx.broken {
467                                tracing::error!("error while handling EXPLAIN statement: {}", err);
468                                SubscribeStage::Explain(SubscribeExplain {
469                                    validity,
470                                    optimizer,
471                                    df_meta: Default::default(),
472                                    cluster_id,
473                                    explain_ctx,
474                                })
475                            } else {
476                                return Err(err);
477                            }
478                        }
479                    };
480
481                    Ok(Box::new(stage))
482                })
483            },
484        )))
485    }
486
487    #[instrument]
488    async fn subscribe_finish(
489        &mut self,
490        ctx: &mut ExecuteContext,
491        SubscribeFinish {
492            validity: _,
493            cluster_id,
494            plan,
495            global_lir_plan,
496            dependency_ids,
497            replica_id,
498        }: SubscribeFinish,
499    ) -> Result<StageResult<Box<SubscribeStage>>, AdapterError> {
500        let (df_desc, df_meta) = global_lir_plan.unapply();
501        emit_optimizer_notices(&*self.catalog, ctx.session(), &df_meta.optimizer_notices);
502        let conn_id = ctx.session.conn_id().clone();
503        let session_uuid = ctx.session().uuid();
504        let txn_read_holds = self
505            .txn_read_holds
506            .remove(&conn_id)
507            .expect("must have previously installed read holds");
508        let (resp, write_notify) = self
509            .implement_subscribe(
510                ctx.extra_mut(),
511                df_desc,
512                dependency_ids,
513                cluster_id,
514                replica_id,
515                conn_id,
516                session_uuid,
517                txn_read_holds,
518                plan,
519            )
520            .await?;
521        // Wait for the `mz_subscriptions` bookkeeping write off the coordinator
522        // loop before returning the `SUBSCRIBE` response to the subscribing
523        // session.
524        let span = Span::current();
525        Ok(StageResult::HandleRetire(mz_ore::task::spawn(
526            || "subscribe_finish::await_bookkeeping",
527            async move {
528                write_notify.await;
529                Ok(resp)
530            }
531            .instrument(span),
532        )))
533    }
534
535    #[instrument]
536    pub(crate) async fn implement_subscribe(
537        &mut self,
538        ctx_extra: &mut ExecuteContextGuard,
539        df_desc: DataflowDescription<LirRelationExpr>,
540        dependency_ids: BTreeSet<GlobalId>,
541        cluster_id: ComputeInstanceId,
542        replica_id: Option<ReplicaId>,
543        conn_id: ConnectionId,
544        session_uuid: Uuid,
545        read_holds: ReadHolds,
546        plan: plan::SubscribePlan,
547    ) -> Result<(ExecuteResponse, BuiltinTableAppendNotify), AdapterError> {
548        let sink_id = df_desc.sink_id();
549
550        let (tx, rx) = mpsc::unbounded_channel();
551        let active_subscribe = ActiveSubscribe {
552            conn_id: conn_id.clone(),
553            session_uuid,
554            channel: tx,
555            emit_progress: plan.emit_progress,
556            as_of: df_desc
557                .as_of
558                .as_ref()
559                .and_then(|t| t.as_option())
560                .copied()
561                .expect("set to Some in an earlier stage"),
562            arity: df_desc
563                .sink_exports
564                .values()
565                .into_element()
566                .from_desc
567                .arity(),
568            cluster_id,
569            depends_on: dependency_ids,
570            start_time: self.now(),
571            output: plan.output,
572            internal: false,
573        };
574        active_subscribe.initialize();
575
576        // Register bookkeeping for the new SUBSCRIBE and ship its dataflow. The
577        // `mz_subscriptions` write is deferred to a group commit (see
578        // `add_active_compute_sink`) rather than committed inline, so it does not block
579        // the coordinator loop on a timestamp-oracle round trip. We hand the notify back
580        // so the caller can wait before returning the `SUBSCRIBE` response to the
581        // subscribing session.
582        let write_notify =
583            self.add_active_compute_sink(sink_id, ActiveComputeSink::Subscribe(active_subscribe));
584
585        // Ship the dataflow, handling errors gracefully. With the frontend subscribe
586        // sequencing, a dependency can be dropped between sequencing (on the session
587        // task) and here. The read holds acquired during sequencing don't prevent that:
588        // they hold back compaction, not drops.
589        if let Err(e) = self
590            .try_ship_dataflow(df_desc, cluster_id, replica_id)
591            .await
592        {
593            // Clean up the active compute sink that was added above, since the dataflow
594            // was never created. If we don't do this, the sink_id remains in
595            // `drop_sinks` but no collection exists in the compute controller, causing
596            // a panic when the connection terminates. This also retracts the deferred
597            // `mz_subscriptions` write, so `write_notify` can be dropped.
598            self.remove_active_compute_sink(sink_id).await;
599            return Err(AdapterError::concurrent_dependency_drop_from_dataflow_creation_error(e));
600        }
601
602        // Explicitly drop read holds, just to make it obvious what's happening.
603        drop(read_holds);
604
605        let resp = ExecuteResponse::Subscribing {
606            rx,
607            ctx_extra: std::mem::take(ctx_extra),
608            instance_id: cluster_id,
609        };
610        let resp = match plan.copy_to {
611            None => resp,
612            Some(format) => ExecuteResponse::CopyTo {
613                format,
614                resp: Box::new(resp),
615            },
616        };
617        Ok((resp, write_notify))
618    }
619
620    #[instrument]
621    async fn subscribe_explain(
622        &self,
623        session: &Session,
624        SubscribeExplain {
625            optimizer,
626            df_meta,
627            cluster_id,
628            explain_ctx:
629                ExplainPlanContext {
630                    config,
631                    format,
632                    stage,
633                    optimizer_trace,
634                    desc,
635                    ..
636                },
637            ..
638        }: SubscribeExplain,
639    ) -> Result<StageResult<Box<SubscribeStage>>, AdapterError> {
640        let session_catalog = self.catalog().for_session(session);
641
642        let expr_humanizer = {
643            let transient_items = btreemap! {
644                optimizer.sink_id() => TransientItem::new(
645                    Some(vec![GlobalId::Explain.to_string()]),
646                    desc.map(|d| d.iter_names().map(|c| c.to_string()).collect()),
647                )
648            };
649            ExprHumanizerExt::new(transient_items, &session_catalog)
650        };
651
652        let target_cluster = self.catalog().get_cluster(cluster_id);
653
654        let features = OptimizerFeatures::from(self.catalog().system_config())
655            .override_from(&target_cluster.config.features())
656            .override_from(&self.cluster_scoped_optimizer_overrides(cluster_id))
657            .override_from(&config.features);
658
659        let rows = optimizer_trace
660            .into_rows(
661                format,
662                &config,
663                &features,
664                &expr_humanizer,
665                None,
666                Some(target_cluster),
667                df_meta,
668                stage,
669                plan::ExplaineeStatementKind::Subscribe,
670                None,
671            )
672            .await?;
673
674        Ok(StageResult::Response(Self::send_immediate_rows(rows)))
675    }
676}