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