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