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