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_persist_client::PersistClient;
22use mz_repr::GlobalId;
23use mz_repr::Timestamp;
24use mz_repr::global_id::TransientIdGen;
25use mz_repr::{RelationDesc, Row};
26use mz_sql::optimizer_metrics::OptimizerMetrics;
27use mz_sql::plan::Params;
28use mz_storage_types::sources::Timeline;
29use mz_timestamp_oracle::TimestampOracle;
30use prometheus::Histogram;
31use qcell::QCell;
32use thiserror::Error;
33use timely::progress::Antichain;
34use tokio::sync::oneshot;
35use uuid::Uuid;
36
37use crate::catalog::Catalog;
38use crate::command::{CatalogSnapshot, Command};
39use crate::coord::peek::FastPathPlan;
40use crate::coord::{Coordinator, ExecuteContextGuard};
41use crate::session::{LifecycleTimestamps, Session};
42use crate::statement_logging::{
43    FrontendStatementLoggingEvent, PreparedStatementEvent, PreparedStatementLoggingInfo,
44    StatementLoggingFrontend, StatementLoggingId, WatchSetCreation,
45};
46use crate::{AdapterError, Client, CollectionIdBundle, ReadHolds, statement_logging};
47
48/// Storage collections trait alias we need to consult for since/frontiers.
49pub type StorageCollectionsHandle =
50    Arc<dyn mz_storage_client::storage_collections::StorageCollections + Send + Sync>;
51
52/// Clients needed for peek sequencing in the Adapter Frontend.
53#[derive(Debug)]
54pub struct PeekClient {
55    coordinator_client: Client,
56    /// Cache of the latest catalog snapshot. Serves
57    /// [`PeekClient::catalog_snapshot`] without a Coordinator round-trip
58    /// while the catalog's transient revision is unchanged.
59    ///
60    /// Holds a `Weak` so that an idle session does not keep a superseded
61    /// catalog version alive.
62    catalog_cache: Weak<Catalog>,
63    /// Channels to talk to each compute Instance task directly. Lazily populated.
64    /// Note that these are never cleaned up. In theory, this could lead to a very slow memory leak
65    /// if a long-running user session keeps peeking on clusters that are being created and dropped
66    /// in a hot loop. Hopefully this won't occur any time soon.
67    compute_instances: BTreeMap<ComputeInstanceId, InstanceClient>,
68    /// Handle to storage collections for reading frontiers and policies.
69    pub storage_collections: StorageCollectionsHandle,
70    /// A generator for transient `GlobalId`s, shared with Coordinator.
71    pub transient_id_gen: Arc<TransientIdGen>,
72    pub optimizer_metrics: OptimizerMetrics,
73    /// Per-timeline oracles from the coordinator. Lazily populated.
74    oracles: BTreeMap<Timeline, Arc<dyn TimestampOracle<Timestamp> + Send + Sync>>,
75    persist_client: PersistClient,
76    /// Statement logging state for frontend peek sequencing.
77    pub statement_logging_frontend: StatementLoggingFrontend,
78}
79
80impl PeekClient {
81    /// Creates a PeekClient.
82    ///
83    /// `catalog` seeds the catalog snapshot cache, so that the session's
84    /// first statements don't need a `Command::CatalogSnapshot` round-trip.
85    pub fn new(
86        coordinator_client: Client,
87        catalog: &Arc<Catalog>,
88        storage_collections: StorageCollectionsHandle,
89        transient_id_gen: Arc<TransientIdGen>,
90        optimizer_metrics: OptimizerMetrics,
91        persist_client: PersistClient,
92        statement_logging_frontend: StatementLoggingFrontend,
93    ) -> Self {
94        Self {
95            coordinator_client,
96            catalog_cache: Arc::downgrade(catalog),
97            compute_instances: Default::default(), // lazily populated
98            storage_collections,
99            transient_id_gen,
100            optimizer_metrics,
101            statement_logging_frontend,
102            oracles: Default::default(), // lazily populated
103            persist_client,
104        }
105    }
106
107    pub async fn ensure_compute_instance_client(
108        &mut self,
109        compute_instance: ComputeInstanceId,
110    ) -> Result<InstanceClient, InstanceMissing> {
111        if !self.compute_instances.contains_key(&compute_instance) {
112            let client = self
113                .call_coordinator(|tx| Command::GetComputeInstanceClient {
114                    instance_id: compute_instance,
115                    tx,
116                })
117                .await?;
118            self.compute_instances.insert(compute_instance, client);
119        }
120        Ok(self
121            .compute_instances
122            .get(&compute_instance)
123            .expect("ensured above")
124            .clone())
125    }
126
127    pub async fn ensure_oracle(
128        &mut self,
129        timeline: Timeline,
130    ) -> Result<&mut Arc<dyn TimestampOracle<Timestamp> + Send + Sync>, AdapterError> {
131        if !self.oracles.contains_key(&timeline) {
132            let oracle = self
133                .call_coordinator(|tx| Command::GetOracle {
134                    timeline: timeline.clone(),
135                    tx,
136                })
137                .await?;
138            self.oracles.insert(timeline.clone(), oracle);
139        }
140        Ok(self.oracles.get_mut(&timeline).expect("ensured above"))
141    }
142
143    /// Fetch a snapshot of the catalog.
144    ///
145    /// Serves from the session-side cache when the catalog's transient
146    /// revision is unchanged since the cached snapshot was taken (see
147    /// [`Catalog::transient_revision_is_current`]). An unchanged revision
148    /// means the cached snapshot is identical to what a fresh fetch would
149    /// return. Otherwise falls back to a `Command::CatalogSnapshot`
150    /// round-trip and re-populates the cache.
151    ///
152    /// Cache misses record the round-trip time in the adapter metrics,
153    /// labeled by `context`. Hits and misses are counted in
154    /// `catalog_snapshot_cache`.
155    pub async fn catalog_snapshot(&mut self, context: &str) -> Arc<Catalog> {
156        // NOTE: The upgrade can fail even when the revision is unchanged: any
157        // in-place mutation of the Coordinator's catalog (including
158        // revision-preserving ones) moves it to a new allocation, and the
159        // cached allocation is freed once its last user drops. We then fall
160        // through to a refetch.
161        let cached = self
162            .catalog_cache
163            .upgrade()
164            .filter(|catalog| catalog.transient_revision_is_current());
165        if let Some(catalog) = cached {
166            self.coordinator_client
167                .metrics()
168                .catalog_snapshot_cache
169                .with_label_values(&[context, "hit"])
170                .inc();
171            return catalog;
172        }
173
174        // The cache is empty, stale, or its allocation is gone: do the
175        // round-trip.
176        let start = std::time::Instant::now();
177        let CatalogSnapshot { catalog } = self
178            .call_coordinator(|tx| Command::CatalogSnapshot { tx })
179            .await;
180        let metrics = self.coordinator_client.metrics();
181        metrics
182            .catalog_snapshot_seconds
183            .with_label_values(&[context])
184            .observe(start.elapsed().as_secs_f64());
185        metrics
186            .catalog_snapshot_cache
187            .with_label_values(&[context, "miss"])
188            .inc();
189        self.catalog_cache = Arc::downgrade(&catalog);
190        catalog
191    }
192
193    pub(crate) async fn call_coordinator<T, F>(&self, f: F) -> T
194    where
195        F: FnOnce(oneshot::Sender<T>) -> Command,
196    {
197        let (tx, rx) = oneshot::channel();
198        self.coordinator_client.send(f(tx));
199        rx.await
200            .expect("if the coordinator is still alive, it shouldn't have dropped our call")
201    }
202
203    /// Acquire read holds on the given compute/storage collections, and
204    /// determine the smallest common valid write frontier among the specified collections.
205    ///
206    /// Similar to `Coordinator::acquire_read_holds` and `TimestampProvider::least_valid_write`
207    /// combined.
208    ///
209    /// Note: Unlike the Coordinator/StorageController's `least_valid_write` that treats sinks
210    /// specially when fetching storage frontiers (see `mz_storage_controller::collections_frontiers`),
211    /// we intentionally do not special‑case sinks here because peeks never read from sinks.
212    /// Therefore, using `StorageCollections::collections_frontiers` is sufficient.
213    ///
214    /// Note: self is taken &mut because of the lazy fetching in `get_compute_instance_client`.
215    pub async fn acquire_read_holds_and_least_valid_write(
216        &mut self,
217        id_bundle: &CollectionIdBundle,
218    ) -> Result<(ReadHolds, Antichain<Timestamp>), CollectionLookupError> {
219        let mut read_holds = ReadHolds::new();
220        let mut upper = Antichain::new();
221
222        if !id_bundle.storage_ids.is_empty() {
223            let desired_storage: Vec<_> = id_bundle.storage_ids.iter().copied().collect();
224            let storage_read_holds = self
225                .storage_collections
226                .acquire_read_holds(desired_storage)?;
227            read_holds.storage_holds = storage_read_holds
228                .into_iter()
229                .map(|hold| (hold.id(), hold))
230                .collect();
231
232            let storage_ids: Vec<_> = id_bundle.storage_ids.iter().copied().collect();
233            for f in self
234                .storage_collections
235                .collections_frontiers(storage_ids)?
236            {
237                upper.extend(f.write_frontier);
238            }
239        }
240
241        for (&instance_id, collection_ids) in &id_bundle.compute_ids {
242            let client = self.ensure_compute_instance_client(instance_id).await?;
243
244            for (id, read_hold, write_frontier) in client
245                .acquire_read_holds_and_collection_write_frontiers(
246                    collection_ids.iter().copied().collect(),
247                )
248                .await?
249            {
250                let prev = read_holds
251                    .compute_holds
252                    .insert((instance_id, id), read_hold);
253                assert!(
254                    prev.is_none(),
255                    "duplicate compute ID in id_bundle {id_bundle:?}"
256                );
257
258                upper.extend(write_frontier);
259            }
260        }
261
262        Ok((read_holds, upper))
263    }
264
265    /// Implement a fast-path peek plan.
266    /// This is similar to `Coordinator::implement_peek_plan`, but only for fast path peeks.
267    ///
268    /// Note: self is taken &mut because of the lazy fetching in `get_compute_instance_client`.
269    ///
270    /// Note: `input_read_holds` has holds for all inputs. For fast-path peeks, this includes the
271    /// peek target. For slow-path peeks (to be implemented later), we'll need to additionally call
272    /// into the Controller to acquire a hold on the peek target after we create the dataflow.
273    ///
274    /// `logging_guard` owns end-of-execution logging for this statement. For a
275    /// constant peek it stays armed and the caller logs the end from the
276    /// returned result. For a `PeekExisting`/`PeekPersist` peek, successful
277    /// registration with the coordinator hands ownership of the end to the
278    /// coordinator and the guard is defused here. That holds even when the
279    /// subsequent `client.peek()` fails to issue.
280    pub(crate) async fn implement_fast_path_peek_plan(
281        &mut self,
282        fast_path: FastPathPlan,
283        timestamp: Timestamp,
284        finishing: mz_expr::RowSetFinishing,
285        compute_instance: ComputeInstanceId,
286        target_replica: Option<mz_cluster_client::ReplicaId>,
287        intermediate_result_type: mz_repr::SqlRelationType,
288        max_result_size: u64,
289        max_returned_query_size: Option<u64>,
290        row_set_finishing_seconds: Histogram,
291        input_read_holds: ReadHolds,
292        peek_stash_read_batch_size_bytes: usize,
293        peek_stash_read_memory_budget_bytes: usize,
294        conn_id: mz_adapter_types::connection::ConnectionId,
295        depends_on: std::collections::BTreeSet<mz_repr::GlobalId>,
296        watch_set: Option<WatchSetCreation>,
297        logging_guard: &mut StatementLoggingGuard,
298    ) -> Result<crate::ExecuteResponse, AdapterError> {
299        // If the dataflow optimizes to a constant expression, we can immediately return the result.
300        if let FastPathPlan::Constant(rows_res, _) = fast_path {
301            // For constant queries with statement logging, immediately log that
302            // dependencies are "ready" (trivially, because there are none).
303            if let Some(ref ws) = watch_set {
304                self.log_lifecycle_event(
305                    ws.logging_id,
306                    statement_logging::StatementLifecycleEvent::StorageDependenciesFinished,
307                );
308                self.log_lifecycle_event(
309                    ws.logging_id,
310                    statement_logging::StatementLifecycleEvent::ComputeDependenciesFinished,
311                );
312            }
313
314            let mut rows = match rows_res {
315                Ok(rows) => rows,
316                Err(e) => return Err(e.into()),
317            };
318            consolidate(&mut rows);
319
320            let mut results = Vec::new();
321            for (row, count) in rows {
322                let count = match u64::try_from(count.into_inner()) {
323                    Ok(u) => usize::cast_from(u),
324                    Err(_) => {
325                        return Err(AdapterError::Unstructured(anyhow::anyhow!(
326                            "Negative multiplicity in constant result: {}",
327                            count
328                        )));
329                    }
330                };
331                match std::num::NonZeroUsize::new(count) {
332                    Some(nzu) => {
333                        results.push((row, nzu));
334                    }
335                    None => {
336                        // No need to retain 0 diffs.
337                    }
338                };
339            }
340            let row_collection = RowCollection::new(results, &finishing.order_by);
341            return match finishing.finish(
342                row_collection,
343                max_result_size,
344                max_returned_query_size,
345                &row_set_finishing_seconds,
346            ) {
347                Ok((rows, _bytes)) => Ok(Coordinator::send_immediate_rows(rows)),
348                // TODO(peek-seq): make this a structured error. (also in the old sequencing)
349                Err(e) => Err(AdapterError::ResultSize(e)),
350            };
351        }
352
353        let (peek_target, target_read_hold, literal_constraints, mfp, strategy) = match fast_path {
354            FastPathPlan::PeekExisting(_coll_id, idx_id, literal_constraints, mfp) => {
355                let peek_target = PeekTarget::Index { id: idx_id };
356                let target_read_hold = input_read_holds
357                    .compute_holds
358                    .get(&(compute_instance, idx_id))
359                    .expect("missing compute read hold on PeekExisting peek target")
360                    .clone();
361                let strategy = statement_logging::StatementExecutionStrategy::FastPath;
362                (
363                    peek_target,
364                    target_read_hold,
365                    literal_constraints,
366                    mfp,
367                    strategy,
368                )
369            }
370            FastPathPlan::PeekPersist(coll_id, literal_constraint, mfp) => {
371                let literal_constraints = literal_constraint.map(|r| vec![r]);
372                let metadata = self
373                    .storage_collections
374                    .collection_metadata(coll_id)
375                    .map_err(AdapterError::concurrent_dependency_drop_from_collection_missing)?
376                    .clone();
377                let peek_target = PeekTarget::Persist {
378                    id: coll_id,
379                    metadata,
380                };
381                let target_read_hold = input_read_holds
382                    .storage_holds
383                    .get(&coll_id)
384                    .expect("missing storage read hold on PeekPersist peek target")
385                    .clone();
386                let strategy = statement_logging::StatementExecutionStrategy::PersistFastPath;
387                (
388                    peek_target,
389                    target_read_hold,
390                    literal_constraints,
391                    mfp,
392                    strategy,
393                )
394            }
395            FastPathPlan::Constant(..) => {
396                // FastPathPlan::Constant handled above.
397                unreachable!()
398            }
399        };
400
401        let (rows_tx, rows_rx) = oneshot::channel();
402        let uuid = Uuid::new_v4();
403
404        // At this stage we don't know column names for the result because we
405        // only know the peek's result type as a bare SqlRelationType.
406        let cols = (0..intermediate_result_type.arity()).map(|i| format!("peek_{i}"));
407        let result_desc = RelationDesc::new(intermediate_result_type.clone(), cols);
408
409        let client = self
410            .ensure_compute_instance_client(compute_instance)
411            .await
412            .map_err(AdapterError::concurrent_dependency_drop_from_instance_missing)?;
413
414        // Register coordinator tracking of this peek. This has to complete before issuing the peek.
415        //
416        // Warning: If we fail to actually issue the peek after this point, then we need to
417        // unregister it to avoid an orphaned registration.
418        self.call_coordinator(|tx| Command::RegisterFrontendPeek {
419            uuid,
420            conn_id: conn_id.clone(),
421            cluster_id: compute_instance,
422            depends_on,
423            is_fast_path: true,
424            watch_set,
425            tx,
426        })
427        .await?;
428
429        // The peek is registered: the coordinator's `pending_peeks` entry now
430        // owns end-of-execution logging. It logs the end on peek completion,
431        // cancellation, concurrent teardown (e.g. a DROP CLUSTER), or the
432        // unregistration below. We defuse the guard so the frontend doesn't
433        // also log the end.
434        logging_guard.defuse();
435
436        // Test-only synchronization point: parks a peek between registration
437        // and issue, so a test can land a concurrent DROP CLUSTER in this
438        // window. Used by
439        // workflow_test_drop_cluster_during_registered_peeks_fast_path.
440        fail::fail_point!("peek_after_register_before_issue");
441
442        let finishing_for_instance = finishing.clone();
443        let peek_result = client
444            .peek(
445                peek_target,
446                literal_constraints,
447                uuid,
448                timestamp,
449                result_desc,
450                finishing_for_instance,
451                mfp,
452                target_read_hold,
453                target_replica,
454                rows_tx,
455            )
456            .await;
457
458        if let Err(err) = peek_result {
459            let err = AdapterError::concurrent_dependency_drop_from_instance_peek_error(
460                err,
461                compute_instance,
462            );
463            // The peek failed to issue, so no peek response will ever arrive.
464            // The coordinator owns end-of-execution logging (see above), so we
465            // ask it to unregister the peek and retire it with this error. If
466            // a concurrent teardown already retired the peek, the end is
467            // already logged and the unregistration is a no-op.
468            self.call_coordinator(|tx| Command::UnregisterFrontendPeek {
469                uuid,
470                reason: statement_logging::StatementEndedExecutionReason::Errored {
471                    error: err.to_string(),
472                },
473                tx,
474            })
475            .await;
476            return Err(err);
477        }
478
479        let peek_response_stream = Coordinator::create_peek_response_stream(
480            rows_rx,
481            finishing,
482            max_result_size,
483            max_returned_query_size,
484            row_set_finishing_seconds,
485            self.persist_client.clone(),
486            peek_stash_read_batch_size_bytes,
487            peek_stash_read_memory_budget_bytes,
488        );
489
490        Ok(crate::ExecuteResponse::SendingRowsStreaming {
491            rows: Box::pin(peek_response_stream),
492            instance_id: compute_instance,
493            strategy,
494        })
495    }
496
497    /// Set up statement logging for a frontend-sequenced operation.
498    ///
499    /// If `outer_ctx_extra` is `None`, begins a new statement execution log
500    /// entry. If `outer_ctx_extra` is `Some` (e.g. EXECUTE/FETCH), reuses and
501    /// retires the existing logging context.
502    ///
503    /// Returns a [`StatementLoggingGuard`]. Callers must either
504    /// [`retire`](StatementLoggingGuard::retire) the guard on the execution's
505    /// terminal outcome, or [`defuse`](StatementLoggingGuard::defuse) it at
506    /// the point where end-of-execution logging is handed off to another
507    /// component. Dropping the guard without retiring it emits an `Aborted`
508    /// end-execution event.
509    pub(crate) fn begin_statement_logging(
510        &self,
511        session: &mut Session,
512        params: &Params,
513        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
514        catalog: &Catalog,
515        lifecycle_timestamps: Option<LifecycleTimestamps>,
516        outer_ctx_extra: &mut Option<ExecuteContextGuard>,
517    ) -> StatementLoggingGuard {
518        let id = if outer_ctx_extra.is_none() {
519            // This is a new statement, so begin statement logging.
520            let result = self.statement_logging_frontend.begin_statement_execution(
521                session,
522                params,
523                logging,
524                catalog.system_config(),
525                lifecycle_timestamps,
526            );
527
528            if let Some((logging_id, began_execution, mseh_update, prepared_statement)) = result {
529                self.log_began_execution(began_execution, mseh_update, prepared_statement);
530                Some(logging_id)
531            } else {
532                None
533            }
534        } else {
535            // We're executing in the context of another statement (e.g. FETCH),
536            // so take ownership of the outer context and inherit its logging id
537            // (if any). The end of execution will be logged by the caller.
538            outer_ctx_extra
539                .take()
540                .and_then(|guard| guard.defuse().retire())
541        };
542
543        StatementLoggingGuard {
544            id,
545            coordinator_client: self.coordinator_client.clone(),
546            now: self.statement_logging_frontend.now.clone(),
547        }
548    }
549
550    /// Log the beginning of statement execution.
551    pub(crate) fn log_began_execution(
552        &self,
553        record: statement_logging::StatementBeganExecutionRecord,
554        mseh_update: Row,
555        prepared_statement: Option<PreparedStatementEvent>,
556    ) {
557        self.coordinator_client
558            .send(Command::FrontendStatementLogging(
559                FrontendStatementLoggingEvent::BeganExecution {
560                    record,
561                    mseh_update,
562                    prepared_statement,
563                },
564            ));
565    }
566
567    /// Log cluster selection for a statement.
568    pub(crate) fn log_set_cluster(
569        &self,
570        id: StatementLoggingId,
571        cluster_id: mz_controller_types::ClusterId,
572        cluster_name: String,
573    ) {
574        self.coordinator_client
575            .send(Command::FrontendStatementLogging(
576                FrontendStatementLoggingEvent::SetCluster {
577                    id,
578                    cluster_id,
579                    cluster_name,
580                },
581            ));
582    }
583
584    /// Log timestamp determination for a statement.
585    pub(crate) fn log_set_timestamp(&self, id: StatementLoggingId, timestamp: mz_repr::Timestamp) {
586        self.coordinator_client
587            .send(Command::FrontendStatementLogging(
588                FrontendStatementLoggingEvent::SetTimestamp { id, timestamp },
589            ));
590    }
591
592    /// Log transient index ID for a statement.
593    pub(crate) fn log_set_transient_index_id(
594        &self,
595        id: StatementLoggingId,
596        transient_index_id: mz_repr::GlobalId,
597    ) {
598        self.coordinator_client
599            .send(Command::FrontendStatementLogging(
600                FrontendStatementLoggingEvent::SetTransientIndex {
601                    id,
602                    transient_index_id,
603                },
604            ));
605    }
606
607    /// Log a statement lifecycle event.
608    pub(crate) fn log_lifecycle_event(
609        &self,
610        id: StatementLoggingId,
611        event: statement_logging::StatementLifecycleEvent,
612    ) {
613        let when = (self.statement_logging_frontend.now)();
614        self.coordinator_client
615            .send(Command::FrontendStatementLogging(
616                FrontendStatementLoggingEvent::Lifecycle { id, event, when },
617            ));
618    }
619
620    /// Emit a `FrontendStatementLoggingEvent::EndedExecution` for the given
621    /// logging id. Used by the few callers that hold a bare
622    /// [`StatementLoggingId`] rather than a [`StatementLoggingGuard`], e.g.
623    /// error paths of the EXECUTE unrolling where the id is not wrapped in a
624    /// guard.
625    pub(crate) fn log_ended_execution(
626        &self,
627        id: StatementLoggingId,
628        reason: statement_logging::StatementEndedExecutionReason,
629    ) {
630        let ended_at = (self.statement_logging_frontend.now)();
631        let record = statement_logging::StatementEndedExecutionRecord {
632            id: id.0,
633            reason,
634            ended_at,
635        };
636        self.coordinator_client
637            .send(Command::FrontendStatementLogging(
638                FrontendStatementLoggingEvent::EndedExecution(record),
639            ));
640    }
641}
642
643/// RAII guard owning a frontend statement-logging lifecycle.
644///
645/// Created by [`PeekClient::begin_statement_logging`]. Unless logging
646/// responsibility is handed off via [`defuse`](StatementLoggingGuard::defuse),
647/// the guard ensures that every statement for which `BeganExecution` was logged
648/// also receives a corresponding `EndedExecution`, even on early-return, panic,
649/// or mid-flight drop of the enclosing future: if the guard is dropped without
650/// being defused, it emits `StatementEndedExecutionReason::Aborted`.
651///
652/// When the guard is `defuse`d, some other component (e.g. the coordinator, for
653/// streaming peek / subscribe responses) takes over and logs `EndedExecution`
654/// itself.
655///
656/// For non-sampled statements the guard still exists but carries no id, and
657/// retirement / drop are no-ops.
658#[must_use = "StatementLoggingGuard must be explicitly retired or handed off; \
659              otherwise `Drop` will log the statement as Aborted"]
660pub(crate) struct StatementLoggingGuard {
661    /// `None` if the statement was not sampled for logging.
662    id: Option<StatementLoggingId>,
663    coordinator_client: Client,
664    now: mz_ore::now::NowFn,
665}
666
667impl StatementLoggingGuard {
668    /// Returns the logging id, if this statement is being logged.
669    pub(crate) fn id(&self) -> Option<StatementLoggingId> {
670        self.id
671    }
672
673    /// Retires the guard with an explicit end-execution reason.
674    /// A no-op if the guard was defused or the statement is not sampled.
675    pub(crate) fn retire(mut self, reason: statement_logging::StatementEndedExecutionReason) {
676        self.emit(reason);
677    }
678
679    /// Hands off logging responsibility without emitting an end-execution
680    /// event. Call this at the point where another component takes over
681    /// end-of-execution logging. Afterwards the guard is inert.
682    pub(crate) fn defuse(&mut self) {
683        self.id = None;
684    }
685
686    fn emit(&mut self, reason: statement_logging::StatementEndedExecutionReason) {
687        let Some(id) = self.id.take() else {
688            return;
689        };
690        let ended_at = (self.now)();
691        let record = statement_logging::StatementEndedExecutionRecord {
692            id: id.0,
693            reason,
694            ended_at,
695        };
696        self.coordinator_client
697            .send(Command::FrontendStatementLogging(
698                FrontendStatementLoggingEvent::EndedExecution(record),
699            ));
700    }
701}
702
703impl Drop for StatementLoggingGuard {
704    fn drop(&mut self) {
705        // `emit` is a no-op if the guard was already retired or defused (i.e.
706        // `id` is `None`).
707        self.emit(statement_logging::StatementEndedExecutionReason::Aborted);
708    }
709}
710
711/// Errors arising during collection lookup in peek client operations.
712#[derive(Error, Debug)]
713pub enum CollectionLookupError {
714    /// The specified compute instance does not exist.
715    #[error("instance does not exist: {0}")]
716    InstanceMissing(ComputeInstanceId),
717    /// The specified compute instance has shut down.
718    #[error("the instance has shut down")]
719    InstanceShutDown,
720    /// The compute collection does not exist.
721    #[error("collection does not exist: {0}")]
722    CollectionMissing(GlobalId),
723}
724
725impl From<InstanceMissing> for CollectionLookupError {
726    fn from(error: InstanceMissing) -> Self {
727        Self::InstanceMissing(error.0)
728    }
729}
730
731impl From<InstanceShutDown> for CollectionLookupError {
732    fn from(_error: InstanceShutDown) -> Self {
733        Self::InstanceShutDown
734    }
735}
736
737impl From<CollectionMissing> for CollectionLookupError {
738    fn from(error: CollectionMissing) -> Self {
739        Self::CollectionMissing(error.0)
740    }
741}
742
743impl From<AcquireReadHoldsError> for CollectionLookupError {
744    fn from(error: AcquireReadHoldsError) -> Self {
745        match error {
746            AcquireReadHoldsError::CollectionMissing(id) => Self::CollectionMissing(id),
747            AcquireReadHoldsError::InstanceShutDown => Self::InstanceShutDown,
748        }
749    }
750}