Skip to main content

mz_adapter/
peek_client.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 std::collections::BTreeMap;
11use std::sync::{Arc, Weak};
12
13use differential_dataflow::consolidation::consolidate;
14use mz_compute_client::controller::error::{CollectionMissing, InstanceMissing};
15use mz_compute_client::controller::instance_client::InstanceClient;
16use mz_compute_client::controller::instance_client::{AcquireReadHoldsError, InstanceShutDown};
17use mz_compute_client::protocol::command::PeekTarget;
18use mz_compute_types::ComputeInstanceId;
19use mz_expr::row::RowCollection;
20use mz_ore::cast::CastFrom;
21use mz_ore::soft_panic_or_log;
22use mz_persist_client::PersistClient;
23use mz_repr::GlobalId;
24use mz_repr::Timestamp;
25use mz_repr::global_id::TransientIdGen;
26use mz_repr::{RelationDesc, Row};
27use mz_sql::ast::{Raw, Statement};
28use mz_sql::optimizer_metrics::OptimizerMetrics;
29use mz_sql::plan::Params;
30use mz_sql::session::hint::ApplicationNameHint;
31use mz_sql::session::metadata::SessionMetadata;
32use mz_sql_parser::ast::{CopyRelation, CopyStatement, SubscribeStatement};
33use mz_storage_types::sources::Timeline;
34use mz_timestamp_oracle::TimestampOracle;
35use prometheus::Histogram;
36use qcell::QCell;
37use thiserror::Error;
38use timely::progress::Antichain;
39use tokio::sync::{Semaphore, oneshot};
40use uuid::Uuid;
41
42use crate::catalog::Catalog;
43use crate::command::{CatalogSnapshot, Command, ExecuteResponse};
44use crate::coord::appends::GroupCommitNotifier;
45use crate::coord::peek::FastPathPlan;
46use crate::coord::timestamp_selection::TimestampDetermination;
47use crate::coord::{Coordinator, ExecuteContextExtra, ExecuteContextGuard, Message};
48use crate::metrics::Metrics;
49use crate::session::{LifecycleTimestamps, Session};
50use crate::statement_logging::{
51    FrontendStatementLoggingEvent, PreparedStatementEvent, PreparedStatementLoggingInfo,
52    StatementLoggingFrontend, StatementLoggingId, WatchSetCreation,
53};
54use crate::{
55    AdapterError, AdapterNotice, Client, CollectionIdBundle, ReadHolds, metrics, statement_logging,
56};
57
58/// Storage collections trait alias we need to consult for since/frontiers.
59pub type StorageCollectionsHandle =
60    Arc<dyn mz_storage_client::storage_collections::StorageCollections + Send + Sync>;
61
62/// Clients needed for peek sequencing in the Adapter Frontend.
63#[derive(Debug, Clone)]
64pub struct PeekClient {
65    coordinator_client: CoordinatorClient,
66    /// Cache of the latest catalog snapshot. Serves
67    /// [`PeekClient::catalog_snapshot`] without a Coordinator round-trip
68    /// while the catalog's transient revision is unchanged.
69    ///
70    /// Holds a `Weak` so that an idle session does not keep a superseded
71    /// catalog version alive.
72    catalog_cache: Weak<Catalog>,
73    /// Channels to talk to each compute Instance task directly. Lazily populated.
74    /// Note that these are never cleaned up. In theory, this could lead to a very slow memory leak
75    /// if a long-running user session keeps peeking on clusters that are being created and dropped
76    /// in a hot loop. Hopefully this won't occur any time soon.
77    compute_instances: BTreeMap<ComputeInstanceId, InstanceClient>,
78    /// Handle to storage collections for reading frontiers and policies.
79    pub storage_collections: StorageCollectionsHandle,
80    /// A generator for transient `GlobalId`s, shared with Coordinator.
81    pub transient_id_gen: Arc<TransientIdGen>,
82    pub optimizer_metrics: OptimizerMetrics,
83    /// Per-timeline oracles from the coordinator. Lazily populated.
84    oracles: BTreeMap<Timeline, Arc<dyn TimestampOracle<Timestamp> + Send + Sync>>,
85    persist_client: PersistClient,
86    /// Statement logging state for frontend peek sequencing.
87    pub statement_logging_frontend: StatementLoggingFrontend,
88    /// Semaphore for limiting concurrent OCC (optimistic concurrency control) write operations.
89    pub occ_write_semaphore: Arc<Semaphore>,
90    /// Requests a group commit. Used to advance the write timeline when we
91    /// need the oracle to move but have nothing to write ourselves.
92    pub(crate) group_commit_notifier: GroupCommitNotifier,
93    /// Whether the coordinator is in read-only mode. Mutations must be rejected.
94    pub read_only: bool,
95}
96
97/// A command sender that does not make background work keep the coordinator alive.
98#[derive(Debug, Clone)]
99pub(crate) enum CoordinatorClient {
100    Session(Client),
101    Background {
102        tx: tokio::sync::mpsc::UnboundedSender<Message>,
103        metrics: Metrics,
104    },
105}
106
107impl CoordinatorClient {
108    /// Sends a command, dropping it if the coordinator is gone.
109    ///
110    /// A session cannot outrun the coordinator: it holds a `Client`, and the
111    /// coordinator's loop only exits once every client is dropped, so a failed
112    /// send there is a real bug. Background work holds no client on purpose, so
113    /// losing the race with shutdown is normal and must not crash the process.
114    /// Dropping the command closes its response channel, which the caller handles
115    /// as an error.
116    pub(crate) fn send(&self, command: Command) {
117        if self.try_send(command) {
118            return;
119        }
120        match self {
121            CoordinatorClient::Session(_) => panic!("coordinator unexpectedly gone"),
122            CoordinatorClient::Background { .. } => {
123                tracing::debug!("dropping background command, coordinator is gone")
124            }
125        }
126    }
127
128    pub(crate) fn try_send(&self, command: Command) -> bool {
129        match self {
130            CoordinatorClient::Session(client) => client.try_send(command),
131            CoordinatorClient::Background { tx, .. } => tx
132                .send(Message::Command(
133                    mz_ore::tracing::OpenTelemetryContext::obtain(),
134                    command,
135                ))
136                .is_ok(),
137        }
138    }
139
140    pub(crate) fn metrics(&self) -> &Metrics {
141        match self {
142            CoordinatorClient::Session(client) => client.metrics(),
143            CoordinatorClient::Background { metrics, .. } => metrics,
144        }
145    }
146}
147
148impl PeekClient {
149    /// Creates a PeekClient.
150    ///
151    /// `catalog` seeds the catalog snapshot cache, so that the session's
152    /// first statements don't need a `Command::CatalogSnapshot` round-trip.
153    #[allow(clippy::too_many_arguments)]
154    pub(crate) fn new(
155        coordinator_client: CoordinatorClient,
156        catalog: &Arc<Catalog>,
157        storage_collections: StorageCollectionsHandle,
158        transient_id_gen: Arc<TransientIdGen>,
159        optimizer_metrics: OptimizerMetrics,
160        persist_client: PersistClient,
161        statement_logging_frontend: StatementLoggingFrontend,
162        occ_write_semaphore: Arc<Semaphore>,
163        group_commit_notifier: GroupCommitNotifier,
164        read_only: bool,
165    ) -> Self {
166        Self {
167            coordinator_client,
168            catalog_cache: Arc::downgrade(catalog),
169            compute_instances: Default::default(), // lazily populated
170            storage_collections,
171            transient_id_gen,
172            optimizer_metrics,
173            statement_logging_frontend,
174            oracles: Default::default(), // lazily populated
175            persist_client,
176            occ_write_semaphore,
177            group_commit_notifier,
178            read_only,
179        }
180    }
181
182    pub async fn ensure_compute_instance_client(
183        &mut self,
184        compute_instance: ComputeInstanceId,
185    ) -> Result<InstanceClient, CollectionLookupError> {
186        if !self.compute_instances.contains_key(&compute_instance) {
187            let client = self
188                .call_coordinator(|tx| Command::GetComputeInstanceClient {
189                    instance_id: compute_instance,
190                    tx,
191                })
192                .await
193                .map_err(|_| CollectionLookupError::InstanceShutDown)??;
194            self.compute_instances.insert(compute_instance, client);
195        }
196        Ok(self
197            .compute_instances
198            .get(&compute_instance)
199            .expect("ensured above")
200            .clone())
201    }
202
203    pub async fn ensure_oracle(
204        &mut self,
205        timeline: Timeline,
206    ) -> Result<&mut Arc<dyn TimestampOracle<Timestamp> + Send + Sync>, AdapterError> {
207        if !self.oracles.contains_key(&timeline) {
208            let oracle = self
209                .call_coordinator(|tx| Command::GetOracle {
210                    timeline: timeline.clone(),
211                    tx,
212                })
213                .await??;
214            self.oracles.insert(timeline.clone(), oracle);
215        }
216        Ok(self.oracles.get_mut(&timeline).expect("ensured above"))
217    }
218
219    /// Fetch a snapshot of the catalog.
220    ///
221    /// Serves from the session-side cache when the catalog's transient
222    /// revision is unchanged since the cached snapshot was taken (see
223    /// [`Catalog::transient_revision_is_current`]). An unchanged revision
224    /// means the cached snapshot is identical to what a fresh fetch would
225    /// return. Otherwise falls back to a `Command::CatalogSnapshot`
226    /// round-trip and re-populates the cache.
227    ///
228    /// Cache misses record the round-trip time in the adapter metrics,
229    /// labeled by `context`. Hits and misses are counted in
230    /// `catalog_snapshot_cache`.
231    pub async fn catalog_snapshot(&mut self, context: &str) -> Arc<Catalog> {
232        // NOTE: The upgrade can fail even when the revision is unchanged: any
233        // in-place mutation of the Coordinator's catalog (including
234        // revision-preserving ones) moves it to a new allocation, and the
235        // cached allocation is freed once its last user drops. We then fall
236        // through to a refetch.
237        let cached = self
238            .catalog_cache
239            .upgrade()
240            .filter(|catalog| catalog.transient_revision_is_current());
241        if let Some(catalog) = cached {
242            self.coordinator_client
243                .metrics()
244                .catalog_snapshot_cache
245                .with_label_values(&[context, "hit"])
246                .inc();
247            return catalog;
248        }
249
250        // The cache is empty, stale, or its allocation is gone: do the
251        // round-trip.
252        let start = std::time::Instant::now();
253        // Session clients keep the coordinator loop alive. A dropped response
254        // here therefore indicates an internal lifecycle bug, unlike a
255        // background compute lookup racing coordinator shutdown.
256        let CatalogSnapshot { catalog } = self
257            .call_coordinator(|tx| Command::CatalogSnapshot { tx })
258            .await
259            .expect("coordinator unexpectedly dropped catalog snapshot response");
260        let metrics = self.coordinator_client.metrics();
261        metrics
262            .catalog_snapshot_seconds
263            .with_label_values(&[context])
264            .observe(start.elapsed().as_secs_f64());
265        metrics
266            .catalog_snapshot_cache
267            .with_label_values(&[context, "miss"])
268            .inc();
269        self.catalog_cache = Arc::downgrade(&catalog);
270        catalog
271    }
272
273    /// Calls the coordinator and returns an error if it drops the response.
274    pub(crate) async fn call_coordinator<T, F>(&self, f: F) -> Result<T, AdapterError>
275    where
276        F: FnOnce(oneshot::Sender<T>) -> Command,
277    {
278        let (tx, rx) = oneshot::channel();
279        self.coordinator_client.send(f(tx));
280        Ok(rx.await?)
281    }
282
283    /// Adds the `emit_timestamp_notice` notice to `session`: the explanation of
284    /// `determination`, the timestamp of a statement that reads `id_bundle`.
285    ///
286    /// Skips the notice if a collection in `id_bundle` was dropped since the
287    /// timestamp was determined: the statement does not depend on the notice,
288    /// so it goes on without it.
289    pub(crate) async fn add_timestamp_notice(
290        &self,
291        session: &Session,
292        cluster_id: ComputeInstanceId,
293        id_bundle: CollectionIdBundle,
294        determination: TimestampDetermination,
295    ) -> Result<(), AdapterError> {
296        // Test-only synchronization point: parks the statement before the
297        // coordinator looks up the frontiers of `id_bundle`, so a test can drop
298        // one of its collections in between.
299        fail::fail_point!("timestamp_notice_before_dispatch");
300        let request = self.call_coordinator(|tx| Command::ExplainTimestamp {
301            conn_id: session.conn_id().clone(),
302            session_wall_time: session.pcx().wall_time,
303            cluster_id,
304            id_bundle,
305            determination,
306            tx,
307        });
308        match request.await? {
309            Ok(explanation) => session.add_notice(AdapterNotice::QueryTimestamp { explanation }),
310            Err(AdapterError::ConcurrentDependencyDrop { .. }) => {}
311            Err(e) => return Err(e),
312        }
313        Ok(())
314    }
315
316    /// The client for sending commands to the coordinator.
317    pub(crate) fn coordinator_client(&self) -> &CoordinatorClient {
318        &self.coordinator_client
319    }
320
321    /// Acquire read holds on the given compute/storage collections, and
322    /// determine the smallest common valid write frontier among the specified collections.
323    ///
324    /// Similar to `Coordinator::acquire_read_holds` and `TimestampProvider::least_valid_write`
325    /// combined.
326    ///
327    /// Note: Unlike the Coordinator/StorageController's `least_valid_write` that treats sinks
328    /// specially when fetching storage frontiers (see `mz_storage_controller::collections_frontiers`),
329    /// we intentionally do not special‑case sinks here because peeks never read from sinks.
330    /// Therefore, using `StorageCollections::collections_frontiers` is sufficient.
331    ///
332    /// Note: self is taken &mut because of the lazy fetching in `get_compute_instance_client`.
333    pub async fn acquire_read_holds_and_least_valid_write(
334        &mut self,
335        id_bundle: &CollectionIdBundle,
336    ) -> Result<(ReadHolds, Antichain<Timestamp>), CollectionLookupError> {
337        let mut read_holds = ReadHolds::new();
338        let mut upper = Antichain::new();
339
340        if !id_bundle.storage_ids.is_empty() {
341            let desired_storage: Vec<_> = id_bundle.storage_ids.iter().copied().collect();
342            let storage_read_holds = self
343                .storage_collections
344                .acquire_read_holds(desired_storage)?;
345            read_holds.storage_holds = storage_read_holds
346                .into_iter()
347                .map(|hold| (hold.id(), hold))
348                .collect();
349
350            let storage_ids: Vec<_> = id_bundle.storage_ids.iter().copied().collect();
351            for f in self
352                .storage_collections
353                .collections_frontiers(storage_ids)?
354            {
355                upper.extend(f.write_frontier);
356            }
357        }
358
359        for (&instance_id, collection_ids) in &id_bundle.compute_ids {
360            let client = self.ensure_compute_instance_client(instance_id).await?;
361
362            for (id, read_hold, write_frontier) in client
363                .acquire_read_holds_and_collection_write_frontiers(
364                    collection_ids.iter().copied().collect(),
365                )
366                .await?
367            {
368                let prev = read_holds
369                    .compute_holds
370                    .insert((instance_id, id), read_hold);
371                assert!(
372                    prev.is_none(),
373                    "duplicate compute ID in id_bundle {id_bundle:?}"
374                );
375
376                upper.extend(write_frontier);
377            }
378        }
379
380        Ok((read_holds, upper))
381    }
382
383    /// Implement a fast-path peek plan.
384    /// This is similar to `Coordinator::implement_peek_plan`, but only for fast path peeks.
385    ///
386    /// Note: self is taken &mut because of the lazy fetching in `get_compute_instance_client`.
387    ///
388    /// Note: `input_read_holds` has holds for all inputs. For fast-path peeks, this includes the
389    /// peek target. For slow-path peeks (to be implemented later), we'll need to additionally call
390    /// into the Controller to acquire a hold on the peek target after we create the dataflow.
391    ///
392    /// For a constant peek the logging slot stays armed and the caller logs the
393    /// end from the returned result. For a `PeekExisting`/`PeekPersist` peek,
394    /// successful registration with the coordinator hands ownership of the end
395    /// to the coordinator and the slot is defused here. That holds even when the
396    /// subsequent `client.peek()` fails to issue.
397    pub(crate) async fn implement_fast_path_peek_plan(
398        &mut self,
399        fast_path: FastPathPlan,
400        timestamp: Timestamp,
401        finishing: mz_expr::RowSetFinishing,
402        compute_instance: ComputeInstanceId,
403        target_replica: Option<mz_cluster_client::ReplicaId>,
404        intermediate_result_type: mz_repr::SqlRelationType,
405        max_result_size: u64,
406        max_returned_query_size: Option<u64>,
407        row_set_finishing_seconds: Histogram,
408        input_read_holds: ReadHolds,
409        peek_stash_read_batch_size_bytes: usize,
410        peek_stash_read_memory_budget_bytes: usize,
411        conn_id: mz_adapter_types::connection::ConnectionId,
412        depends_on: std::collections::BTreeSet<mz_repr::GlobalId>,
413        watch_set: Option<WatchSetCreation>,
414        logging: &mut ExecutionLogging,
415    ) -> Result<crate::ExecuteResponse, AdapterError> {
416        // If the dataflow optimizes to a constant expression, we can immediately return the result.
417        if let FastPathPlan::Constant(rows_res, _) = fast_path {
418            // For constant queries with statement logging, immediately log that
419            // dependencies are "ready" (trivially, because there are none).
420            if let Some(ref ws) = watch_set {
421                self.log_lifecycle_event(
422                    ws.logging_id,
423                    statement_logging::StatementLifecycleEvent::StorageDependenciesFinished,
424                );
425                self.log_lifecycle_event(
426                    ws.logging_id,
427                    statement_logging::StatementLifecycleEvent::ComputeDependenciesFinished,
428                );
429            }
430
431            let mut rows = match rows_res {
432                Ok(rows) => rows,
433                Err(e) => return Err(e.into()),
434            };
435            consolidate(&mut rows);
436
437            let mut results = Vec::new();
438            for (row, count) in rows {
439                let count = match u64::try_from(count.into_inner()) {
440                    Ok(u) => usize::cast_from(u),
441                    Err(_) => {
442                        return Err(AdapterError::Unstructured(anyhow::anyhow!(
443                            "Negative multiplicity in constant result: {}",
444                            count
445                        )));
446                    }
447                };
448                match std::num::NonZeroUsize::new(count) {
449                    Some(nzu) => {
450                        results.push((row, nzu));
451                    }
452                    None => {
453                        // No need to retain 0 diffs.
454                    }
455                };
456            }
457            let row_collection = RowCollection::new(results, &finishing.order_by);
458            return match finishing.finish(
459                row_collection,
460                max_result_size,
461                max_returned_query_size,
462                &row_set_finishing_seconds,
463            ) {
464                Ok((rows, _bytes)) => Ok(Coordinator::send_immediate_rows(rows)),
465                // TODO(peek-seq): make this a structured error. (also in the old sequencing)
466                Err(e) => Err(AdapterError::ResultSize(e)),
467            };
468        }
469
470        let (peek_target, target_read_hold, literal_constraints, mfp, strategy) = match fast_path {
471            FastPathPlan::PeekExisting(_coll_id, idx_id, literal_constraints, mfp) => {
472                let peek_target = PeekTarget::Index { id: idx_id };
473                let target_read_hold = input_read_holds
474                    .compute_holds
475                    .get(&(compute_instance, idx_id))
476                    .expect("missing compute read hold on PeekExisting peek target")
477                    .clone();
478                let strategy = statement_logging::StatementExecutionStrategy::FastPath;
479                (
480                    peek_target,
481                    target_read_hold,
482                    literal_constraints,
483                    mfp,
484                    strategy,
485                )
486            }
487            FastPathPlan::PeekPersist(coll_id, literal_constraint, mfp) => {
488                let literal_constraints = literal_constraint.map(|r| vec![r]);
489                let metadata = self
490                    .storage_collections
491                    .collection_metadata(coll_id)
492                    .map_err(AdapterError::concurrent_dependency_drop_from_collection_missing)?
493                    .clone();
494                let peek_target = PeekTarget::Persist {
495                    id: coll_id,
496                    metadata,
497                };
498                let target_read_hold = input_read_holds
499                    .storage_holds
500                    .get(&coll_id)
501                    .expect("missing storage read hold on PeekPersist peek target")
502                    .clone();
503                let strategy = statement_logging::StatementExecutionStrategy::PersistFastPath;
504                (
505                    peek_target,
506                    target_read_hold,
507                    literal_constraints,
508                    mfp,
509                    strategy,
510                )
511            }
512            FastPathPlan::Constant(..) => {
513                // FastPathPlan::Constant handled above.
514                unreachable!()
515            }
516        };
517
518        let (rows_tx, rows_rx) = oneshot::channel();
519        let uuid = Uuid::new_v4();
520
521        // At this stage we don't know column names for the result because we
522        // only know the peek's result type as a bare SqlRelationType.
523        let cols = (0..intermediate_result_type.arity()).map(|i| format!("peek_{i}"));
524        let result_desc = RelationDesc::new(intermediate_result_type.clone(), cols);
525
526        let client = self
527            .ensure_compute_instance_client(compute_instance)
528            .await
529            .map_err(|error| {
530                AdapterError::concurrent_dependency_drop_from_collection_lookup_error(
531                    error,
532                    compute_instance,
533                )
534            })?;
535
536        // Register coordinator tracking of this peek. This has to complete before issuing the peek.
537        //
538        // Warning: If we fail to actually issue the peek after this point, then we need to
539        // unregister it to avoid an orphaned registration.
540        self.call_coordinator(|tx| Command::RegisterFrontendPeek {
541            uuid,
542            conn_id: conn_id.clone(),
543            cluster_id: compute_instance,
544            depends_on,
545            is_fast_path: true,
546            watch_set,
547            tx,
548        })
549        .await??;
550
551        // The peek is registered: the coordinator's `pending_peeks` entry now
552        // owns end-of-execution logging. It logs the end on peek completion,
553        // cancellation, concurrent teardown (e.g. a DROP CLUSTER), or the
554        // unregistration below. We defuse the guard so the frontend doesn't
555        // also log the end.
556        logging.defuse();
557
558        // Test-only synchronization point: parks a peek between registration
559        // and issue, so a test can land a concurrent DROP CLUSTER in this
560        // window. Used by
561        // workflow_test_drop_cluster_during_registered_peeks_fast_path.
562        fail::fail_point!("peek_after_register_before_issue");
563
564        let finishing_for_instance = finishing.clone();
565        let peek_result = client
566            .peek(
567                peek_target,
568                literal_constraints,
569                uuid,
570                timestamp,
571                result_desc,
572                finishing_for_instance,
573                mfp,
574                target_read_hold,
575                target_replica,
576                rows_tx,
577            )
578            .await;
579
580        if let Err(err) = peek_result {
581            let err = AdapterError::concurrent_dependency_drop_from_instance_peek_error(
582                err,
583                compute_instance,
584            );
585            // The peek failed to issue, so no peek response will ever arrive.
586            // The coordinator owns end-of-execution logging (see above), so we
587            // ask it to unregister the peek and retire it with this error. If
588            // a concurrent teardown already retired the peek, the end is
589            // already logged and the unregistration is a no-op.
590            let _ = self
591                .call_coordinator(|tx| Command::UnregisterFrontendPeek {
592                    uuid,
593                    reason: statement_logging::StatementEndedExecutionReason::Errored {
594                        error: err.to_string(),
595                    },
596                    tx,
597                })
598                .await;
599            return Err(err);
600        }
601
602        let peek_response_stream = Coordinator::create_peek_response_stream(
603            rows_rx,
604            finishing,
605            max_result_size,
606            max_returned_query_size,
607            row_set_finishing_seconds,
608            self.persist_client.clone(),
609            peek_stash_read_batch_size_bytes,
610            peek_stash_read_memory_budget_bytes,
611        );
612
613        Ok(crate::ExecuteResponse::SendingRowsStreaming {
614            rows: Box::pin(peek_response_stream),
615            instance_id: compute_instance,
616            strategy,
617        })
618    }
619
620    /// Begins a new statement execution log entry, sampling permitting.
621    ///
622    /// Only [`ExecutionLogging::take_over`] may call this: an entry that exists
623    /// without the session task owning its end would stay unfinished forever.
624    fn begin_statement_logging(
625        &self,
626        session: &mut Session,
627        params: &Params,
628        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
629        catalog: &Catalog,
630        lifecycle_timestamps: Option<LifecycleTimestamps>,
631    ) -> StatementLoggingGuard {
632        let result = self.statement_logging_frontend.begin_statement_execution(
633            session,
634            params,
635            logging,
636            catalog.system_config(),
637            lifecycle_timestamps,
638        );
639
640        let id = result.map(
641            |(logging_id, began_execution, mseh_update, prepared_statement)| {
642                self.log_began_execution(began_execution, mseh_update, prepared_statement);
643                logging_id
644            },
645        );
646
647        StatementLoggingGuard {
648            id,
649            coordinator_client: self.coordinator_client.clone(),
650            now: self.statement_logging_frontend.now.clone(),
651        }
652    }
653
654    /// Log the beginning of statement execution.
655    pub(crate) fn log_began_execution(
656        &self,
657        record: statement_logging::StatementBeganExecutionRecord,
658        mseh_update: Row,
659        prepared_statement: Option<PreparedStatementEvent>,
660    ) {
661        self.coordinator_client
662            .send(Command::FrontendStatementLogging(
663                FrontendStatementLoggingEvent::BeganExecution {
664                    record,
665                    mseh_update,
666                    prepared_statement,
667                },
668            ));
669    }
670
671    /// Log cluster selection for a statement.
672    pub(crate) fn log_set_cluster(
673        &self,
674        id: StatementLoggingId,
675        cluster_id: mz_controller_types::ClusterId,
676        cluster_name: String,
677    ) {
678        self.coordinator_client
679            .send(Command::FrontendStatementLogging(
680                FrontendStatementLoggingEvent::SetCluster {
681                    id,
682                    cluster_id,
683                    cluster_name,
684                },
685            ));
686    }
687
688    /// Log timestamp determination for a statement.
689    pub(crate) fn log_set_timestamp(&self, id: StatementLoggingId, timestamp: mz_repr::Timestamp) {
690        self.coordinator_client
691            .send(Command::FrontendStatementLogging(
692                FrontendStatementLoggingEvent::SetTimestamp { id, timestamp },
693            ));
694    }
695
696    /// Log transient index ID for a statement.
697    pub(crate) fn log_set_transient_index_id(
698        &self,
699        id: StatementLoggingId,
700        transient_index_id: mz_repr::GlobalId,
701    ) {
702        self.coordinator_client
703            .send(Command::FrontendStatementLogging(
704                FrontendStatementLoggingEvent::SetTransientIndex {
705                    id,
706                    transient_index_id,
707                },
708            ));
709    }
710
711    /// Log a statement lifecycle event.
712    pub(crate) fn log_lifecycle_event(
713        &self,
714        id: StatementLoggingId,
715        event: statement_logging::StatementLifecycleEvent,
716    ) {
717        let when = (self.statement_logging_frontend.now)();
718        self.coordinator_client
719            .send(Command::FrontendStatementLogging(
720                FrontendStatementLoggingEvent::Lifecycle { id, event, when },
721            ));
722    }
723}
724
725/// RAII guard owning a frontend statement-logging lifecycle.
726///
727/// Unless logging responsibility is handed off via
728/// [`defuse`](StatementLoggingGuard::defuse), the guard ensures that every
729/// statement for which `BeganExecution` was logged also receives a
730/// corresponding `EndedExecution`, even on early-return, panic, or mid-flight
731/// drop of the enclosing future: if the guard is dropped without being defused,
732/// it emits `StatementEndedExecutionReason::Aborted`.
733///
734/// When the guard is `defuse`d, some other component (e.g. the coordinator, for
735/// streaming peek / subscribe responses) takes over and logs `EndedExecution`
736/// itself.
737///
738/// For non-sampled statements the guard still exists but carries no id, and
739/// retirement / drop are no-ops.
740#[must_use = "StatementLoggingGuard must be explicitly retired or handed off; \
741              otherwise `Drop` will log the statement as Aborted"]
742struct StatementLoggingGuard {
743    /// `None` if the statement was not sampled for logging.
744    id: Option<StatementLoggingId>,
745    coordinator_client: CoordinatorClient,
746    now: mz_ore::now::NowFn,
747}
748
749impl StatementLoggingGuard {
750    /// Arms a guard for the obligation the coordinator armed for `outer`, the
751    /// statement whose execution the one we are about to run serves.
752    fn adopt(outer: ExecuteContextGuard, peek_client: &PeekClient) -> Self {
753        Self {
754            id: outer.defuse().retire(),
755            coordinator_client: peek_client.coordinator_client.clone(),
756            now: peek_client.statement_logging_frontend.now.clone(),
757        }
758    }
759
760    /// Returns the logging id, if this statement is being logged.
761    fn id(&self) -> Option<StatementLoggingId> {
762        self.id
763    }
764
765    /// Retires the guard with an explicit end-execution reason.
766    /// A no-op if the guard was defused or the statement is not sampled.
767    fn retire(mut self, reason: statement_logging::StatementEndedExecutionReason) {
768        self.emit(reason);
769    }
770
771    /// Turns the obligation back into its transferable form, disarming this
772    /// guard.
773    fn release(mut self) -> ExecuteContextExtra {
774        ExecuteContextExtra::new(self.id.take())
775    }
776
777    /// Hands off logging responsibility without emitting an end-execution
778    /// event. Call this at the point where another component takes over
779    /// end-of-execution logging. Afterwards the guard is inert.
780    fn defuse(&mut self) {
781        self.id = None;
782    }
783
784    fn emit(&mut self, reason: statement_logging::StatementEndedExecutionReason) {
785        let Some(id) = self.id.take() else {
786            return;
787        };
788        let ended_at = (self.now)();
789        let record = statement_logging::StatementEndedExecutionRecord {
790            id: id.0,
791            reason,
792            ended_at,
793        };
794        // A guard can outlive the coordinator during shutdown. Failing to send
795        // costs us one end event, panicking in `Drop` would cost the whole
796        // connection.
797        let _ = self
798            .coordinator_client
799            .try_send(Command::FrontendStatementLogging(
800                FrontendStatementLoggingEvent::EndedExecution(record),
801            ));
802    }
803}
804
805impl Drop for StatementLoggingGuard {
806    fn drop(&mut self) {
807        // `emit` is a no-op if the guard was already retired or defused (i.e.
808        // `id` is `None`).
809        self.emit(statement_logging::StatementEndedExecutionReason::Aborted);
810    }
811}
812
813/// The session task's slot for the end-of-execution obligation of the statement
814/// it is running. One slot is held for the whole `SessionClient::execute` call
815/// and there is exactly one retirement site.
816///
817/// An empty slot means no log entry exists for this execution, so a fallback to
818/// the coordinator lets it begin its own. An occupied slot means the session
819/// task owes an end event: [`Self::retire`] pays it, [`Self::release`] transfers
820/// it to the coordinator. [`Self::id`] returning `None` covers both "not
821/// sampled" and "the end is logged elsewhere", which want identical treatment
822/// everywhere.
823pub(crate) struct ExecutionLogging {
824    guard: Option<StatementLoggingGuard>,
825    /// Whether the coordinator must not run this statement, because the session
826    /// task has already counted it in the metrics `Coordinator::handle_execute`
827    /// maintains. Handing it over afterwards would count it twice.
828    coordinator_must_not_run: bool,
829}
830
831/// Which statement the session task is taking the log entry over for.
832pub(crate) enum TakeOver {
833    /// The statement that will run here, so the coordinator must not run it.
834    StatementToRun,
835    /// A SQL `EXECUTE` that unrolls into an inner statement, for a session task
836    /// that will go on to run that inner statement.
837    ///
838    /// The entry stays armed, so a failure to unroll is recorded against the
839    /// `EXECUTE`. Only the `EXECUTE` itself is counted here. The inner
840    /// statement is a statement in its own right and is counted wherever it
841    /// ends up running, exactly as the coordinator does when it re-dispatches a
842    /// `Plan::Execute`, which is why the slot stays releasable.
843    UnrolledExecute,
844}
845
846impl ExecutionLogging {
847    /// Adopts the end-of-execution obligation of an outer statement (a FETCH or
848    /// an EXECUTE running its inner statement), or starts out empty when there
849    /// is no outer statement.
850    pub(crate) fn adopt(outer: Option<ExecuteContextGuard>, peek_client: &PeekClient) -> Self {
851        Self {
852            guard: outer.map(|outer| StatementLoggingGuard::adopt(outer, peek_client)),
853            coordinator_must_not_run: false,
854        }
855    }
856
857    /// Returns the logging id, if an end event is owed and the statement is
858    /// being logged.
859    pub(crate) fn id(&self) -> Option<StatementLoggingId> {
860        self.guard.as_ref().and_then(|guard| guard.id())
861    }
862
863    /// Records that the session task, not the coordinator, is executing `stmt`.
864    /// Bumps the counters `Coordinator::handle_execute` would have bumped and
865    /// makes sure a log entry exists, inheriting the adopted one if there is
866    /// one. `stmt` is `None` for an empty portal, which is logged but not
867    /// counted.
868    ///
869    /// For [`TakeOver::StatementToRun`], every exit after this call must produce
870    /// an outcome for the statement: the coordinator will not see it.
871    pub(crate) fn take_over(
872        &mut self,
873        peek_client: &PeekClient,
874        session: &mut Session,
875        stmt: Option<&Statement<Raw>>,
876        params: &Params,
877        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
878        catalog: &Catalog,
879        lifecycle_timestamps: Option<LifecycleTimestamps>,
880        taking_over: TakeOver,
881    ) -> Option<StatementLoggingId> {
882        self.begin_or_inherit(
883            peek_client,
884            session,
885            params,
886            logging,
887            catalog,
888            lifecycle_timestamps,
889        );
890        count_statement(session, stmt);
891        if matches!(taking_over, TakeOver::StatementToRun) {
892            self.coordinator_must_not_run = true;
893        }
894        self.id()
895    }
896
897    /// Makes sure a log entry exists for this execution, keeping an adopted one
898    /// if there is one.
899    fn begin_or_inherit(
900        &mut self,
901        peek_client: &PeekClient,
902        session: &mut Session,
903        params: &Params,
904        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
905        catalog: &Catalog,
906        lifecycle_timestamps: Option<LifecycleTimestamps>,
907    ) {
908        if self.guard.is_none() {
909            self.guard = Some(peek_client.begin_statement_logging(
910                session,
911                params,
912                logging,
913                catalog,
914                lifecycle_timestamps,
915            ));
916        }
917    }
918
919    /// Hands the obligation to the coordinator, which retires it once the
920    /// statement it dispatches finishes. `None` tells the coordinator to begin
921    /// its own entry, `Some` with no id inside tells it that an entry already
922    /// exists or that sampling declined.
923    #[must_use]
924    pub(crate) fn release(&mut self) -> Option<ExecuteContextExtra> {
925        if self.coordinator_must_not_run {
926            soft_panic_or_log!(
927                "statement handed to the coordinator after the session task took it over: \
928                 its per-statement metrics are counted twice"
929            );
930        }
931        self.guard.take().map(|guard| guard.release())
932    }
933
934    /// Emits the end event for `result`, if we still owe one.
935    pub(crate) fn retire(self, result: &Result<ExecuteResponse, AdapterError>) {
936        let Some(guard) = self.guard else {
937            return;
938        };
939        // A defused or released slot owes no end event. Bail before mapping
940        // `result` to an end reason, which soft-panics for the responses whose
941        // end is logged elsewhere.
942        if guard.id().is_none() {
943            return;
944        }
945        guard.retire(end_reason(result));
946    }
947
948    /// Leaves the slot inert, for dispatch sites that hand the end of execution
949    /// to the coordinator or the protocol layer (registered peeks, subscribes).
950    pub(crate) fn defuse(&mut self) {
951        if let Some(guard) = self.guard.as_mut() {
952            guard.defuse();
953        }
954    }
955}
956
957/// Maps an execution outcome to the reason to record for it.
958///
959/// The responses whose end is logged elsewhere are filtered out first: their
960/// `StatementEndedExecutionReason` conversion panics, and this runs for every
961/// statement the session task executes, so that panic would take down the
962/// connection.
963fn end_reason(
964    result: &Result<ExecuteResponse, AdapterError>,
965) -> statement_logging::StatementEndedExecutionReason {
966    if let Ok(response) = result {
967        if terminates_elsewhere(response) {
968            soft_panic_or_log!(
969                "frontend-sequenced statement still owed an end event while returning {:?}",
970                crate::command::ExecuteResponseKind::from(response)
971            );
972            return statement_logging::StatementEndedExecutionReason::Aborted;
973        }
974    }
975    result.into()
976}
977
978/// Bumps the per-statement counters `Coordinator::handle_execute` maintains.
979///
980/// `stmt` is `None` for an empty portal, which is logged but not counted. The
981/// coordinator skips it the same way, and matching that is the point of this
982/// function.
983fn count_statement(session: &Session, stmt: Option<&Statement<Raw>>) {
984    let Some(stmt) = stmt else {
985        return;
986    };
987    let session_type = metrics::session_type_label_value(session.user());
988    session
989        .metrics()
990        .query_total(&[session_type, metrics::statement_type_label_value(stmt)])
991        .inc();
992    if let Statement::Subscribe(SubscribeStatement { output, .. })
993    | Statement::Copy(CopyStatement {
994        relation: CopyRelation::Subscribe(SubscribeStatement { output, .. }),
995        ..
996    }) = stmt
997    {
998        session
999            .metrics()
1000            .subscribe_outputs(&[session_type, metrics::subscribe_output_label_value(output)])
1001            .inc();
1002    }
1003    let application_name = ApplicationNameHint::from_str(session.application_name());
1004    if let Some(labels) = metrics::as_of_query_label_values(session_type, application_name, stmt) {
1005        session.metrics().as_of_queries(&labels).inc();
1006    }
1007}
1008
1009/// Whether someone else logs the end of execution for `response`: the
1010/// coordinator for a registered peek, the protocol layer for a subscribe, a
1011/// FETCH or a COPY FROM. The dispatch sites that produce these defuse the slot,
1012/// so an armed slot alongside one of them means a dispatch site did not.
1013fn terminates_elsewhere(response: &ExecuteResponse) -> bool {
1014    match response {
1015        ExecuteResponse::SendingRowsStreaming { .. }
1016        | ExecuteResponse::Subscribing { .. }
1017        | ExecuteResponse::Fetch { .. }
1018        | ExecuteResponse::CopyFrom { .. } => true,
1019        // COPY TO STDOUT of an immediate result terminates here. Anything else
1020        // it can wrap does not.
1021        ExecuteResponse::CopyTo { resp, .. } => {
1022            !matches!(**resp, ExecuteResponse::SendingRowsImmediate { .. })
1023        }
1024        _ => false,
1025    }
1026}
1027
1028/// Errors arising during collection lookup in peek client operations.
1029#[derive(Error, Debug)]
1030pub enum CollectionLookupError {
1031    /// The specified compute instance does not exist.
1032    #[error("instance does not exist: {0}")]
1033    InstanceMissing(ComputeInstanceId),
1034    /// The specified compute instance has shut down.
1035    #[error("the instance has shut down")]
1036    InstanceShutDown,
1037    /// The compute collection does not exist.
1038    #[error("collection does not exist: {0}")]
1039    CollectionMissing(GlobalId),
1040}
1041
1042impl From<InstanceMissing> for CollectionLookupError {
1043    fn from(error: InstanceMissing) -> Self {
1044        Self::InstanceMissing(error.0)
1045    }
1046}
1047
1048impl From<InstanceShutDown> for CollectionLookupError {
1049    fn from(_error: InstanceShutDown) -> Self {
1050        Self::InstanceShutDown
1051    }
1052}
1053
1054impl From<CollectionMissing> for CollectionLookupError {
1055    fn from(error: CollectionMissing) -> Self {
1056        Self::CollectionMissing(error.0)
1057    }
1058}
1059
1060impl From<AcquireReadHoldsError> for CollectionLookupError {
1061    fn from(error: AcquireReadHoldsError) -> Self {
1062        match error {
1063            AcquireReadHoldsError::CollectionMissing(id) => Self::CollectionMissing(id),
1064            AcquireReadHoldsError::InstanceShutDown => Self::InstanceShutDown,
1065        }
1066    }
1067}