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