Skip to main content

mz_compute_client/controller/
instance.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
10//! A controller for a compute instance.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::fmt::Debug;
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17use chrono::{DateTime, DurationRound, TimeDelta, Utc};
18use mz_build_info::BuildInfo;
19use mz_cluster_client::WallclockLagFn;
20use mz_compute_types::dataflows::{BuildDesc, DataflowDescription};
21use mz_compute_types::plan::render_plan::RenderPlan;
22use mz_compute_types::sinks::{
23    ComputeSinkConnection, ComputeSinkDesc, MaterializedViewSinkConnection,
24};
25use mz_compute_types::sources::SourceInstanceDesc;
26use mz_controller_types::dyncfgs::{
27    ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE, WALLCLOCK_LAG_RECORDING_INTERVAL,
28};
29use mz_dyncfg::{ConfigSet, ConfigUpdates};
30use mz_expr::RowSetFinishing;
31use mz_ore::cast::CastFrom;
32use mz_ore::channel::instrumented_unbounded_channel;
33use mz_ore::now::NowFn;
34use mz_ore::tracing::OpenTelemetryContext;
35use mz_ore::{soft_assert_or_log, soft_panic_or_log};
36use mz_persist_types::PersistLocation;
37use mz_repr::adt::timestamp::CheckedTimestamp;
38use mz_repr::refresh_schedule::RefreshSchedule;
39use mz_repr::{Datum, Diff, GlobalId, RelationDesc, Row, Timestamp};
40use mz_storage_client::controller::{IntrospectionType, WallclockLag, WallclockLagHistogramPeriod};
41use mz_storage_types::read_holds::{self, ReadHold};
42use mz_storage_types::read_policy::ReadPolicy;
43use thiserror::Error;
44use timely::PartialOrder;
45use timely::progress::frontier::MutableAntichain;
46use timely::progress::{Antichain, ChangeBatch};
47use tokio::sync::{mpsc, oneshot};
48use uuid::Uuid;
49
50use crate::controller::error::{
51    CollectionMissing, ERROR_TARGET_REPLICA_FAILED, HydrationCheckBadTarget,
52};
53use crate::controller::instance_client::PeekError;
54use crate::controller::replica::{ReplicaClient, ReplicaConfig};
55use crate::controller::{
56    ComputeControllerResponse, IntrospectionUpdates, PeekNotification, ReplicaId,
57    StorageCollections,
58};
59use crate::logging::LogVariant;
60use crate::metrics::IntCounter;
61use crate::metrics::{InstanceMetrics, ReplicaCollectionMetrics, ReplicaMetrics, UIntGauge};
62use crate::protocol::command::{
63    ComputeCommand, ComputeParameters, InstanceConfig, Peek, PeekTarget,
64};
65use crate::protocol::history::ComputeCommandHistory;
66use crate::protocol::response::{
67    ComputeResponse, CopyToResponse, FrontiersResponse, PeekError as ProtocolPeekError,
68    PeekResponse, StatusResponse, SubscribeBatch, SubscribeResponse,
69};
70
71#[derive(Error, Debug)]
72#[error("replica exists already: {0}")]
73pub(super) struct ReplicaExists(pub ReplicaId);
74
75#[derive(Error, Debug)]
76#[error("replica does not exist: {0}")]
77pub(super) struct ReplicaMissing(pub ReplicaId);
78
79#[derive(Error, Debug)]
80pub(super) enum DataflowCreationError {
81    #[error("collection does not exist: {0}")]
82    CollectionMissing(GlobalId),
83    #[error("replica does not exist: {0}")]
84    ReplicaMissing(ReplicaId),
85    #[error("dataflow definition lacks an as_of value")]
86    MissingAsOf,
87    #[error("subscribe dataflow has an empty as_of")]
88    EmptyAsOfForSubscribe,
89    #[error("copy to dataflow has an empty as_of")]
90    EmptyAsOfForCopyTo,
91    #[error("no read hold provided for dataflow import: {0}")]
92    ReadHoldMissing(GlobalId),
93    #[error("insufficient read hold provided for dataflow import: {0}")]
94    ReadHoldInsufficient(GlobalId),
95}
96
97impl From<CollectionMissing> for DataflowCreationError {
98    fn from(error: CollectionMissing) -> Self {
99        Self::CollectionMissing(error.0)
100    }
101}
102
103#[derive(Error, Debug)]
104pub(super) enum ReadPolicyError {
105    #[error("collection does not exist: {0}")]
106    CollectionMissing(GlobalId),
107    #[error("collection is write-only: {0}")]
108    WriteOnlyCollection(GlobalId),
109}
110
111impl From<CollectionMissing> for ReadPolicyError {
112    fn from(error: CollectionMissing) -> Self {
113        Self::CollectionMissing(error.0)
114    }
115}
116
117/// A command sent to an [`Instance`] task.
118pub(super) type Command = Box<dyn FnOnce(&mut Instance) + Send>;
119
120/// A response from a replica, composed of a replica ID, the replica's current epoch, and the
121/// compute response itself.
122pub(super) type ReplicaResponse = (ReplicaId, u64, ComputeResponse);
123
124/// The state we keep for a compute instance.
125pub(super) struct Instance {
126    /// Build info for spawning replicas
127    build_info: &'static BuildInfo,
128    /// A handle providing access to storage collections.
129    storage_collections: StorageCollections,
130    /// Whether instance initialization has been completed.
131    initialized: bool,
132    /// Whether this instance is in read-only mode.
133    ///
134    /// When in read-only mode, this instance will not update persistent state, such as
135    /// wallclock lag introspection.
136    read_only: bool,
137    /// The workload class of this instance.
138    ///
139    /// This is currently only used to annotate metrics.
140    workload_class: Option<String>,
141    /// The replicas of this compute instance.
142    replicas: BTreeMap<ReplicaId, ReplicaState>,
143    /// Per-replica dyncfg overrides, merged into the `UpdateConfiguration`
144    /// command sent to each replica (and into the command-history replay used
145    /// to hydrate new replicas). Populated from the scoped feature flags
146    /// (replica-local) layer; empty by default, in which case every replica
147    /// receives the unmodified environment-wide configuration. Stores only the
148    /// values that differ from the environment-wide value, so the map is sparse.
149    replica_dyncfg_overrides: BTreeMap<ReplicaId, ConfigUpdates>,
150    /// Currently installed compute collections.
151    ///
152    /// New entries are added for all collections exported from dataflows created through
153    /// [`Instance::create_dataflow`].
154    ///
155    /// Entries are removed by [`Instance::cleanup_collections`]. See that method's documentation
156    /// about the conditions for removing collection state.
157    collections: BTreeMap<GlobalId, CollectionState>,
158    /// IDs of log sources maintained by this compute instance.
159    log_sources: BTreeMap<LogVariant, GlobalId>,
160    /// Currently outstanding peeks.
161    ///
162    /// New entries are added for all peeks initiated through [`Instance::peek`].
163    ///
164    /// The entry for a peek is only removed once all replicas have responded to the peek. This is
165    /// currently required to ensure all replicas have stopped reading from the peeked collection's
166    /// inputs before we allow them to compact. database-issues#4822 tracks changing this so we only have to wait
167    /// for the first peek response.
168    peeks: BTreeMap<Uuid, PendingPeek>,
169    /// Currently in-progress subscribes.
170    ///
171    /// New entries are added for all subscribes exported from dataflows created through
172    /// [`Instance::create_dataflow`].
173    ///
174    /// The entry for a subscribe is removed once at least one replica has reported the subscribe
175    /// to have advanced to the empty frontier or to have been dropped, implying that no further
176    /// updates will be emitted for this subscribe.
177    ///
178    /// Note that subscribes are tracked both in `collections` and `subscribes`. `collections`
179    /// keeps track of the subscribe's upper and since frontiers and ensures appropriate read holds
180    /// on the subscribe's input. `subscribes` is only used to track which updates have been
181    /// emitted, to decide if new ones should be emitted or suppressed.
182    subscribes: BTreeMap<GlobalId, ActiveSubscribe>,
183    /// Tracks all in-progress COPY TOs.
184    ///
185    /// New entries are added for all s3 oneshot sinks (corresponding to a COPY TO) exported from
186    /// dataflows created through [`Instance::create_dataflow`].
187    ///
188    /// The entry for a copy to is removed once at least one replica has finished
189    /// or the exporting collection is dropped.
190    copy_tos: BTreeSet<GlobalId>,
191    /// The command history, used when introducing new replicas or restarting existing replicas.
192    history: ComputeCommandHistory<UIntGauge>,
193    /// Receiver for commands to be executed.
194    command_rx: mpsc::UnboundedReceiver<Command>,
195    /// Sender for responses to be delivered.
196    response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
197    /// Sender for introspection updates to be recorded.
198    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
199    /// The registry the controller uses to report metrics.
200    metrics: InstanceMetrics,
201    /// Dynamic system configuration.
202    dyncfg: Arc<ConfigSet>,
203
204    /// The persist location where we can stash large peek results.
205    peek_stash_persist_location: PersistLocation,
206
207    /// A function that produces the current wallclock time.
208    now: NowFn,
209    /// A function that computes the lag between the given time and wallclock time.
210    wallclock_lag: WallclockLagFn<Timestamp>,
211    /// The last time wallclock lag introspection was recorded.
212    wallclock_lag_last_recorded: DateTime<Utc>,
213
214    /// Sender for updates to collection read holds.
215    ///
216    /// Copies of this sender are given to [`ReadHold`]s that are created in
217    /// [`CollectionState::new`].
218    read_hold_tx: read_holds::ChangeTx,
219    /// A sender for responses from replicas.
220    replica_tx: mz_ore::channel::InstrumentedUnboundedSender<ReplicaResponse, IntCounter>,
221    /// A receiver for responses from replicas.
222    replica_rx: mz_ore::channel::InstrumentedUnboundedReceiver<ReplicaResponse, IntCounter>,
223}
224
225impl Instance {
226    /// Acquire a handle to the collection state associated with `id`.
227    fn collection(&self, id: GlobalId) -> Result<&CollectionState, CollectionMissing> {
228        self.collections.get(&id).ok_or(CollectionMissing(id))
229    }
230
231    /// Acquire a mutable handle to the collection state associated with `id`.
232    fn collection_mut(&mut self, id: GlobalId) -> Result<&mut CollectionState, CollectionMissing> {
233        self.collections.get_mut(&id).ok_or(CollectionMissing(id))
234    }
235
236    /// Acquire a handle to the collection state associated with `id`.
237    ///
238    /// # Panics
239    ///
240    /// Panics if the identified collection does not exist.
241    fn expect_collection(&self, id: GlobalId) -> &CollectionState {
242        self.collections.get(&id).expect("collection must exist")
243    }
244
245    /// Acquire a mutable handle to the collection state associated with `id`.
246    ///
247    /// # Panics
248    ///
249    /// Panics if the identified collection does not exist.
250    fn expect_collection_mut(&mut self, id: GlobalId) -> &mut CollectionState {
251        self.collections
252            .get_mut(&id)
253            .expect("collection must exist")
254    }
255
256    fn collections_iter(&self) -> impl Iterator<Item = (GlobalId, &CollectionState)> {
257        self.collections.iter().map(|(id, coll)| (*id, coll))
258    }
259
260    /// Returns an iterator over replicas that host the given collection.
261    ///
262    /// For replica-targeted collections, this returns only the target replica.
263    /// For non-targeted collections, this returns all replicas.
264    ///
265    /// Returns `Err` if the collection does not exist.
266    fn replicas_hosting(
267        &self,
268        id: GlobalId,
269    ) -> Result<impl Iterator<Item = &ReplicaState>, CollectionMissing> {
270        let target = self.collection(id)?.target_replica;
271        Ok(self
272            .replicas
273            .values()
274            .filter(move |r| target.map_or(true, |t| t == r.id)))
275    }
276
277    /// Add a collection to the instance state.
278    ///
279    /// # Panics
280    ///
281    /// Panics if a collection with the same ID exists already.
282    fn add_collection(
283        &mut self,
284        id: GlobalId,
285        as_of: Antichain<Timestamp>,
286        shared: SharedCollectionState,
287        storage_dependencies: BTreeMap<GlobalId, ReadHold>,
288        compute_dependencies: BTreeMap<GlobalId, ReadHold>,
289        replica_input_read_holds: Vec<ReadHold>,
290        write_only: bool,
291        storage_sink: bool,
292        initial_as_of: Option<Antichain<Timestamp>>,
293        refresh_schedule: Option<RefreshSchedule>,
294        target_replica: Option<ReplicaId>,
295    ) {
296        // Add global collection state.
297        let dependency_ids: Vec<GlobalId> = compute_dependencies
298            .keys()
299            .chain(storage_dependencies.keys())
300            .copied()
301            .collect();
302        let introspection = CollectionIntrospection::new(
303            id,
304            self.introspection_tx.clone(),
305            as_of.clone(),
306            storage_sink,
307            initial_as_of,
308            refresh_schedule,
309            dependency_ids,
310        );
311        let mut state = CollectionState::new(
312            id,
313            as_of.clone(),
314            shared,
315            storage_dependencies,
316            compute_dependencies,
317            Arc::clone(&self.read_hold_tx),
318            introspection,
319        );
320        state.target_replica = target_replica;
321        // If the collection is write-only, clear its read policy to reflect that.
322        if write_only {
323            state.read_policy = None;
324        }
325
326        if let Some(previous) = self.collections.insert(id, state) {
327            panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
328        }
329
330        // Add per-replica collection state.
331        for replica in self.replicas.values_mut() {
332            if target_replica.is_some_and(|id| id != replica.id) {
333                continue;
334            }
335            replica.add_collection(id, as_of.clone(), replica_input_read_holds.clone());
336        }
337    }
338
339    fn remove_collection(&mut self, id: GlobalId) {
340        // Remove per-replica collection state.
341        for replica in self.replicas.values_mut() {
342            replica.remove_collection(id);
343        }
344
345        // Remove global collection state.
346        self.collections.remove(&id);
347    }
348
349    fn add_replica_state(
350        &mut self,
351        id: ReplicaId,
352        client: ReplicaClient,
353        config: ReplicaConfig,
354        epoch: u64,
355    ) -> Result<(), read_holds::ReadHoldIssuerHungUp> {
356        let log_ids: BTreeSet<_> = config.logging.index_logs.values().copied().collect();
357
358        let metrics = self.metrics.for_replica(id);
359        let mut replica = ReplicaState::new(
360            id,
361            client,
362            config,
363            metrics,
364            self.introspection_tx.clone(),
365            epoch,
366        );
367
368        // Add per-replica collection state.
369        let mut shutdown_input = None;
370        for (collection_id, collection) in &self.collections {
371            // Skip log collections not maintained by this replica,
372            // and collections targeted at a different replica.
373            if (collection.log_collection && !log_ids.contains(collection_id))
374                || collection.target_replica.is_some_and(|rid| rid != id)
375            {
376                continue;
377            }
378
379            let as_of = if collection.log_collection {
380                // For log collections, we don't send a `CreateDataflow` command to the replica, so
381                // it doesn't know which as-of the controler chose and defaults to the minimum
382                // frontier instead. We need to initialize the controller-side tracking with the
383                // same frontier, to avoid observing regressions in the reported frontiers.
384                Antichain::from_elem(Timestamp::MIN)
385            } else {
386                collection.read_frontier().to_owned()
387            };
388
389            // Cloning a `ReadHold` fails when its issuer has hung up. For these holds the issuer
390            // is the `StorageCollections`, which doesn't hang up as long as the `Instance` exists,
391            // except during process shutdown, when the tokio runtime drops tasks in arbitrary
392            // order. In that case there is no way of correctly initializing the per-replica
393            // collection state, so we give up. We still add the replica itself, to keep the
394            // bookkeeping consistent with the controller's, and then signal the unrecoverable
395            // error to the caller, which shuts the instance down.
396            let mut input_read_holds = Vec::with_capacity(collection.storage_dependencies.len());
397            let mut hung_up = Vec::new();
398            for hold in collection.storage_dependencies.values() {
399                match hold.try_clone() {
400                    Ok(hold) => input_read_holds.push(hold),
401                    Err(read_holds::ReadHoldIssuerHungUp(input_id)) => hung_up.push(input_id),
402                }
403            }
404            if !hung_up.is_empty() {
405                tracing::error!(
406                    replica_id = %id,
407                    %collection_id,
408                    ?hung_up,
409                    "giving up on adding replica collections: storage read hold issuers hung \
410                     up, the process is shutting down",
411                );
412                shutdown_input = hung_up.into_iter().next();
413                break;
414            }
415
416            replica.add_collection(*collection_id, as_of, input_read_holds);
417        }
418
419        self.replicas.insert(id, replica);
420
421        match shutdown_input {
422            Some(input_id) => Err(read_holds::ReadHoldIssuerHungUp(input_id)),
423            None => Ok(()),
424        }
425    }
426
427    /// Enqueue the given response for delivery to the controller clients.
428    fn deliver_response(&self, response: ComputeControllerResponse) {
429        // Failure to send means the `ComputeController` has been dropped and doesn't care about
430        // responses anymore.
431        let _ = self.response_tx.send(response);
432    }
433
434    /// Enqueue the given introspection updates for recording.
435    fn deliver_introspection_updates(&self, type_: IntrospectionType, updates: Vec<(Row, Diff)>) {
436        // Failure to send means the `ComputeController` has been dropped and doesn't care about
437        // introspection updates anymore.
438        let _ = self.introspection_tx.send((type_, updates));
439    }
440
441    /// Returns whether the identified replica exists.
442    fn replica_exists(&self, id: ReplicaId) -> bool {
443        self.replicas.contains_key(&id)
444    }
445
446    /// Return the IDs of pending peeks targeting the specified replica.
447    fn peeks_targeting(&self, replica_id: ReplicaId) -> impl Iterator<Item = (Uuid, &PendingPeek)> {
448        self.peeks.iter().filter_map(move |(uuid, peek)| {
449            if peek.target_replica == Some(replica_id) {
450                Some((*uuid, peek))
451            } else {
452                None
453            }
454        })
455    }
456
457    /// Return the IDs of in-progress subscribes targeting the specified replica.
458    fn subscribes_targeting(&self, replica_id: ReplicaId) -> impl Iterator<Item = GlobalId> + '_ {
459        self.subscribes.keys().copied().filter(move |id| {
460            let collection = self.expect_collection(*id);
461            collection.target_replica == Some(replica_id)
462        })
463    }
464
465    /// Update introspection with the current collection frontiers.
466    ///
467    /// We could also do this directly in response to frontier changes, but doing it periodically
468    /// lets us avoid emitting some introspection updates that can be consolidated (e.g. a write
469    /// frontier updated immediately followed by a read frontier update).
470    ///
471    /// This method is invoked by `ComputeController::maintain`, which we expect to be called once
472    /// per second during normal operation.
473    fn update_frontier_introspection(&mut self) {
474        for collection in self.collections.values_mut() {
475            collection
476                .introspection
477                .observe_frontiers(&collection.read_frontier(), &collection.write_frontier());
478        }
479
480        for replica in self.replicas.values_mut() {
481            for collection in replica.collections.values_mut() {
482                collection
483                    .introspection
484                    .observe_frontier(&collection.write_frontier);
485            }
486        }
487    }
488
489    /// Refresh the controller state metrics for this instance.
490    ///
491    /// We could also do state metric updates directly in response to state changes, but that would
492    /// mean littering the code with metric update calls. Encapsulating state metric maintenance in
493    /// a single method is less noisy.
494    ///
495    /// This method is invoked by `ComputeController::maintain`, which we expect to be called once
496    /// per second during normal operation.
497    fn refresh_state_metrics(&self) {
498        let unscheduled_collections_count =
499            self.collections.values().filter(|c| !c.scheduled).count();
500        let connected_replica_count = self
501            .replicas
502            .values()
503            .filter(|r| r.client.is_connected())
504            .count();
505
506        self.metrics
507            .replica_count
508            .set(u64::cast_from(self.replicas.len()));
509        self.metrics
510            .collection_count
511            .set(u64::cast_from(self.collections.len()));
512        self.metrics
513            .collection_unscheduled_count
514            .set(u64::cast_from(unscheduled_collections_count));
515        self.metrics
516            .peek_count
517            .set(u64::cast_from(self.peeks.len()));
518        self.metrics
519            .subscribe_count
520            .set(u64::cast_from(self.subscribes.len()));
521        self.metrics
522            .copy_to_count
523            .set(u64::cast_from(self.copy_tos.len()));
524        self.metrics
525            .connected_replica_count
526            .set(u64::cast_from(connected_replica_count));
527    }
528
529    /// Refresh the wallclock lag introspection and metrics with the current lag values.
530    ///
531    /// This method produces wallclock lag metrics of two different shapes:
532    ///
533    /// * Histories: For each replica and each collection, we measure the lag of the write frontier
534    ///   behind the wallclock time every second. Every minute we emit the maximum lag observed
535    ///   over the last minute, together with the current time.
536    /// * Histograms: For each collection, we measure the lag of the write frontier behind
537    ///   wallclock time every second. Every minute we emit all lags observed over the last minute,
538    ///   together with the current histogram period.
539    ///
540    /// Histories are emitted to both Mz introspection and Prometheus, histograms only to
541    /// introspection. We treat lags of unreadable collections (i.e. collections that contain no
542    /// readable times) as undefined and set them to NULL in introspection and `u64::MAX` in
543    /// Prometheus.
544    ///
545    /// This method is invoked by `ComputeController::maintain`, which we expect to be called once
546    /// per second during normal operation.
547    fn refresh_wallclock_lag(&mut self) {
548        let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
549            Some(ts) => (self.wallclock_lag)(ts.clone()),
550            None => Duration::ZERO,
551        };
552
553        let now_ms = (self.now)();
554        let histogram_period = WallclockLagHistogramPeriod::from_epoch_millis(now_ms, &self.dyncfg);
555        let histogram_labels = match &self.workload_class {
556            Some(wc) => [("workload_class", wc.clone())].into(),
557            None => BTreeMap::new(),
558        };
559
560        // For collections that sink into storage, we need to ask the storage controller to know
561        // whether they're currently readable.
562        let readable_storage_collections: BTreeSet<_> = self
563            .collections
564            .keys()
565            .filter_map(|id| {
566                let frontiers = self.storage_collections.collection_frontiers(*id).ok()?;
567                PartialOrder::less_than(&frontiers.read_capabilities, &frontiers.write_frontier)
568                    .then_some(*id)
569            })
570            .collect();
571
572        // First, iterate over all collections and collect histogram measurements.
573        for (id, collection) in &mut self.collections {
574            let write_frontier = collection.write_frontier();
575            let readable = if self.storage_collections.check_exists(*id).is_ok() {
576                readable_storage_collections.contains(id)
577            } else {
578                PartialOrder::less_than(&collection.read_frontier(), &write_frontier)
579            };
580
581            if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
582                let bucket = if readable {
583                    let lag = frontier_lag(&write_frontier);
584                    let lag = lag.as_secs().next_power_of_two();
585                    WallclockLag::Seconds(lag)
586                } else {
587                    WallclockLag::Undefined
588                };
589
590                let key = (histogram_period, bucket, histogram_labels.clone());
591                *stash.entry(key).or_default() += Diff::ONE;
592            }
593        }
594
595        // Second, iterate over all per-replica collections and collect history measurements.
596        for replica in self.replicas.values_mut() {
597            for (id, collection) in &mut replica.collections {
598                // A per-replica collection is considered readable in the context of lag
599                // measurement if either:
600                //  (a) it sinks into a storage collection that is readable
601                //  (b) it is hydrated
602                let readable = readable_storage_collections.contains(id) || collection.hydrated();
603
604                let lag = if readable {
605                    let lag = frontier_lag(&collection.write_frontier);
606                    WallclockLag::Seconds(lag.as_secs())
607                } else {
608                    WallclockLag::Undefined
609                };
610
611                if let Some(wallclock_lag_max) = &mut collection.wallclock_lag_max {
612                    *wallclock_lag_max = (*wallclock_lag_max).max(lag);
613                }
614
615                if let Some(metrics) = &mut collection.metrics {
616                    // No way to specify values as undefined in Prometheus metrics, so we use the
617                    // maximum value instead.
618                    let secs = lag.unwrap_seconds_or(u64::MAX);
619                    metrics.wallclock_lag.observe(secs);
620                };
621            }
622        }
623
624        // Record lags to persist, if it's time.
625        self.maybe_record_wallclock_lag();
626    }
627
628    /// Produce new wallclock lag introspection updates, provided enough time has passed since the
629    /// last recording.
630    //
631    /// We emit new introspection updates if the system time has passed into a new multiple of the
632    /// recording interval (typically 1 minute) since the last refresh. The storage controller uses
633    /// the same approach, ensuring that both controllers commit their lags at roughly the same
634    /// time, avoiding confusion caused by inconsistencies.
635    fn maybe_record_wallclock_lag(&mut self) {
636        if self.read_only {
637            return;
638        }
639
640        let duration_trunc = |datetime: DateTime<_>, interval| {
641            let td = TimeDelta::from_std(interval).ok()?;
642            datetime.duration_trunc(td).ok()
643        };
644
645        let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(&self.dyncfg);
646        let now_dt = mz_ore::now::to_datetime((self.now)());
647        let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
648            soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
649            let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
650            duration_trunc(now_dt, *default).unwrap()
651        });
652        if now_trunc <= self.wallclock_lag_last_recorded {
653            return;
654        }
655
656        let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
657
658        let mut history_updates = Vec::new();
659        for (replica_id, replica) in &mut self.replicas {
660            for (collection_id, collection) in &mut replica.collections {
661                let Some(wallclock_lag_max) = &mut collection.wallclock_lag_max else {
662                    continue;
663                };
664
665                let max_lag = std::mem::replace(wallclock_lag_max, WallclockLag::MIN);
666                let row = Row::pack_slice(&[
667                    Datum::String(&collection_id.to_string()),
668                    Datum::String(&replica_id.to_string()),
669                    max_lag.into_interval_datum(),
670                    Datum::TimestampTz(now_ts),
671                ]);
672                history_updates.push((row, Diff::ONE));
673            }
674        }
675        if !history_updates.is_empty() {
676            self.deliver_introspection_updates(
677                IntrospectionType::WallclockLagHistory,
678                history_updates,
679            );
680        }
681
682        let mut histogram_updates = Vec::new();
683        let mut row_buf = Row::default();
684        for (collection_id, collection) in &mut self.collections {
685            let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
686                continue;
687            };
688
689            for ((period, lag, labels), count) in std::mem::take(stash) {
690                let mut packer = row_buf.packer();
691                packer.extend([
692                    Datum::TimestampTz(period.start),
693                    Datum::TimestampTz(period.end),
694                    Datum::String(&collection_id.to_string()),
695                    lag.into_uint64_datum(),
696                ]);
697                let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
698                packer.push_dict(labels);
699
700                histogram_updates.push((row_buf.clone(), count));
701            }
702        }
703        if !histogram_updates.is_empty() {
704            self.deliver_introspection_updates(
705                IntrospectionType::WallclockLagHistogram,
706                histogram_updates,
707            );
708        }
709
710        self.wallclock_lag_last_recorded = now_trunc;
711    }
712
713    /// Returns `true` if the given collection is hydrated on at least one
714    /// replica.
715    ///
716    /// This also returns `true` in case this cluster does not have any
717    /// replicas that host the given collection.
718    #[mz_ore::instrument(level = "debug")]
719    pub fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, CollectionMissing> {
720        let mut hosting_replicas = self.replicas_hosting(collection_id)?.peekable();
721        if hosting_replicas.peek().is_none() {
722            return Ok(true);
723        }
724        for replica_state in hosting_replicas {
725            let collection_state = replica_state
726                .collections
727                .get(&collection_id)
728                .expect("hosting replica must have per-replica collection state");
729
730            if collection_state.hydrated() {
731                return Ok(true);
732            }
733        }
734
735        Ok(false)
736    }
737
738    /// Returns `true` if each non-transient, non-excluded collection is hydrated on at
739    /// least one replica.
740    ///
741    /// This also returns `true` in case this cluster does not have any
742    /// replicas.
743    #[mz_ore::instrument(level = "debug")]
744    pub fn collections_hydrated_on_replicas(
745        &self,
746        target_replica_ids: Option<Vec<ReplicaId>>,
747        exclude_collections: &BTreeSet<GlobalId>,
748    ) -> Result<bool, HydrationCheckBadTarget> {
749        if self.replicas.is_empty() {
750            return Ok(true);
751        }
752        let target_replicas: BTreeSet<ReplicaId> = self
753            .replicas
754            .keys()
755            .filter_map(|id| match target_replica_ids {
756                None => Some(id.clone()),
757                Some(ref ids) if ids.contains(id) => Some(id.clone()),
758                Some(_) => None,
759            })
760            .collect();
761        if let Some(targets) = target_replica_ids {
762            if target_replicas.is_empty() {
763                return Err(HydrationCheckBadTarget(targets));
764            }
765        }
766
767        let mut unhydrated = BTreeSet::new();
768        for (id, _collection) in self.collections_iter() {
769            if id.is_transient() || exclude_collections.contains(&id) {
770                continue;
771            }
772
773            let mut collection_hydrated = false;
774            // `replicas_hosting` cannot fail here because `collections_iter`
775            // only yields collections that exist.
776            for replica_state in self.replicas_hosting(id).expect("collection must exist") {
777                if !target_replicas.contains(&replica_state.id) {
778                    continue;
779                }
780                let collection_state = replica_state
781                    .collections
782                    .get(&id)
783                    .expect("hosting replica must have per-replica collection state");
784
785                if collection_state.hydrated() {
786                    collection_hydrated = true;
787                    break;
788                }
789            }
790
791            if !collection_hydrated {
792                // We collect all non-hydrated collections instead of breaking
793                // out early, so that the log below names every collection the
794                // caller is waiting on.
795                unhydrated.insert(id);
796            }
797        }
798
799        if !unhydrated.is_empty() {
800            // Callers poll this on the cluster controller's reconcile tick,
801            // which tests turn down to milliseconds, so this is deliberately
802            // one line per call rather than one per collection.
803            tracing::info!(
804                replicas = ?target_replicas,
805                collections = ?unhydrated,
806                "collections are not hydrated on any target replica",
807            );
808        }
809
810        Ok(unhydrated.is_empty())
811    }
812
813    /// Clean up collection state that is not needed anymore.
814    ///
815    /// Three conditions need to be true before we can remove state for a collection:
816    ///
817    ///  1. A client must have explicitly dropped the collection. If that is not the case, clients
818    ///     can still reasonably assume that the controller knows about the collection and can
819    ///     answer queries about it.
820    ///  2. There must be no outstanding read capabilities on the collection. As long as someone
821    ///     still holds read capabilities on a collection, we need to keep it around to be able
822    ///     to properly handle downgrading of said capabilities.
823    ///  3. All replica frontiers for the collection must have advanced to the empty frontier.
824    ///     Advancement to the empty frontiers signals that replicas are done computing the
825    ///     collection and that they won't send more `ComputeResponse`s for it. As long as we might
826    ///     receive responses for a collection we want to keep it around to be able to validate and
827    ///     handle these responses.
828    fn cleanup_collections(&mut self) {
829        let to_remove: Vec<_> = self
830            .collections_iter()
831            .filter(|(id, collection)| {
832                collection.dropped
833                    && collection.shared.lock_read_capabilities(|c| c.is_empty())
834                    && self
835                        .replicas
836                        .values()
837                        .all(|r| r.collection_frontiers_empty(*id))
838            })
839            .map(|(id, _collection)| id)
840            .collect();
841
842        for id in to_remove {
843            self.remove_collection(id);
844        }
845    }
846
847    /// Returns the state of the [`Instance`] formatted as JSON.
848    ///
849    /// The returned value is not guaranteed to be stable and may change at any point in time.
850    #[mz_ore::instrument(level = "debug")]
851    pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
852        // Note: We purposefully use the `Debug` formatting for the value of all fields in the
853        // returned object as a tradeoff between usability and stability. `serde_json` will fail
854        // to serialize an object if the keys aren't strings, so `Debug` formatting the values
855        // prevents a future unrelated change from silently breaking this method.
856
857        // Destructure `self` here so we don't forget to consider dumping newly added fields.
858        let Self {
859            build_info: _,
860            storage_collections: _,
861            peek_stash_persist_location: _,
862            initialized,
863            read_only,
864            workload_class,
865            replicas,
866            replica_dyncfg_overrides: _,
867            collections,
868            log_sources: _,
869            peeks,
870            subscribes,
871            copy_tos,
872            history: _,
873            command_rx: _,
874            response_tx: _,
875            introspection_tx: _,
876            metrics: _,
877            dyncfg: _,
878            now: _,
879            wallclock_lag: _,
880            wallclock_lag_last_recorded,
881            read_hold_tx: _,
882            replica_tx: _,
883            replica_rx: _,
884        } = self;
885
886        let replicas: BTreeMap<_, _> = replicas
887            .iter()
888            .map(|(id, replica)| Ok((id.to_string(), replica.dump()?)))
889            .collect::<Result<_, anyhow::Error>>()?;
890        let collections: BTreeMap<_, _> = collections
891            .iter()
892            .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
893            .collect();
894        let peeks: BTreeMap<_, _> = peeks
895            .iter()
896            .map(|(uuid, peek)| (uuid.to_string(), format!("{peek:?}")))
897            .collect();
898        let subscribes: BTreeMap<_, _> = subscribes
899            .iter()
900            .map(|(id, subscribe)| (id.to_string(), format!("{subscribe:?}")))
901            .collect();
902        let copy_tos: Vec<_> = copy_tos.iter().map(|id| id.to_string()).collect();
903        let wallclock_lag_last_recorded = format!("{wallclock_lag_last_recorded:?}");
904
905        Ok(serde_json::json!({
906            "initialized": initialized,
907            "read_only": read_only,
908            "workload_class": workload_class,
909            "replicas": replicas,
910            "collections": collections,
911            "peeks": peeks,
912            "subscribes": subscribes,
913            "copy_tos": copy_tos,
914            "wallclock_lag_last_recorded": wallclock_lag_last_recorded,
915        }))
916    }
917
918    /// Reports the current write frontier for the identified compute collection.
919    pub(super) fn collection_write_frontier(
920        &self,
921        id: GlobalId,
922    ) -> Result<Antichain<Timestamp>, CollectionMissing> {
923        Ok(self.collection(id)?.write_frontier())
924    }
925}
926
927impl Instance {
928    pub(super) fn new(
929        build_info: &'static BuildInfo,
930        storage: StorageCollections,
931        peek_stash_persist_location: PersistLocation,
932        arranged_logs: Vec<(LogVariant, GlobalId, SharedCollectionState)>,
933        metrics: InstanceMetrics,
934        now: NowFn,
935        wallclock_lag: WallclockLagFn<Timestamp>,
936        dyncfg: Arc<ConfigSet>,
937        command_rx: mpsc::UnboundedReceiver<Command>,
938        response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
939        read_hold_tx: read_holds::ChangeTx,
940        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
941        read_only: bool,
942    ) -> Self {
943        let mut collections = BTreeMap::new();
944        let mut log_sources = BTreeMap::new();
945        for (log, id, shared) in arranged_logs {
946            let collection = CollectionState::new_log_collection(
947                id,
948                shared,
949                Arc::clone(&read_hold_tx),
950                introspection_tx.clone(),
951            );
952            collections.insert(id, collection);
953            log_sources.insert(log, id);
954        }
955
956        let history = ComputeCommandHistory::new(metrics.for_history());
957
958        let send_count = metrics.response_send_count.clone();
959        let recv_count = metrics.response_recv_count.clone();
960        let (replica_tx, replica_rx) = instrumented_unbounded_channel(send_count, recv_count);
961
962        let now_dt = mz_ore::now::to_datetime(now());
963
964        Self {
965            build_info,
966            storage_collections: storage,
967            peek_stash_persist_location,
968            initialized: false,
969            read_only,
970            workload_class: None,
971            replicas: Default::default(),
972            replica_dyncfg_overrides: Default::default(),
973            collections,
974            log_sources,
975            peeks: Default::default(),
976            subscribes: Default::default(),
977            copy_tos: Default::default(),
978            history,
979            command_rx,
980            response_tx,
981            introspection_tx,
982            metrics,
983            dyncfg,
984            now,
985            wallclock_lag,
986            wallclock_lag_last_recorded: now_dt,
987            read_hold_tx,
988            replica_tx,
989            replica_rx,
990        }
991    }
992
993    pub(super) async fn run(mut self) {
994        self.send(ComputeCommand::Hello {
995            // The nonce is protocol iteration-specific and will be set in
996            // `ReplicaTask::specialize_command`.
997            nonce: Uuid::default(),
998        });
999
1000        let instance_config = InstanceConfig {
1001            peek_stash_persist_location: self.peek_stash_persist_location.clone(),
1002            // The remaining fields are replica-specific and will be set in
1003            // `ReplicaTask::specialize_command` (logging, expiration, dictionary compression) and
1004            // `Instance::specialize_command_for_replica` (the initial config snapshot).
1005            logging: Default::default(),
1006            expiration_offset: Default::default(),
1007            arrangement_dictionary_compression: Default::default(),
1008            initial_config: Default::default(),
1009        };
1010
1011        self.send(ComputeCommand::CreateInstance(Box::new(instance_config)));
1012
1013        loop {
1014            tokio::select! {
1015                command = self.command_rx.recv() => match command {
1016                    Some(cmd) => cmd(&mut self),
1017                    None => break,
1018                },
1019                response = self.replica_rx.recv() => match response {
1020                    Some(response) => self.handle_response(response),
1021                    None => unreachable!("self owns a sender side of the channel"),
1022                }
1023            }
1024        }
1025    }
1026
1027    /// Update instance configuration.
1028    #[mz_ore::instrument(level = "debug")]
1029    pub fn update_configuration(&mut self, config_params: ComputeParameters) {
1030        if let Some(workload_class) = &config_params.workload_class {
1031            self.workload_class = workload_class.clone();
1032        }
1033
1034        let command = ComputeCommand::UpdateConfiguration(Box::new(config_params));
1035        self.send(command);
1036    }
1037
1038    /// Marks the end of any initialization commands.
1039    ///
1040    /// Intended to be called by `Controller`, rather than by other code.
1041    /// Calling this method repeatedly has no effect.
1042    #[mz_ore::instrument(level = "debug")]
1043    pub fn initialization_complete(&mut self) {
1044        // The compute protocol requires that `InitializationComplete` is sent only once.
1045        if !self.initialized {
1046            self.send(ComputeCommand::InitializationComplete);
1047            self.initialized = true;
1048        }
1049    }
1050
1051    /// Allows collections to affect writes to external systems (persist).
1052    ///
1053    /// Calling this method repeatedly has no effect.
1054    #[mz_ore::instrument(level = "debug")]
1055    pub fn allow_writes(&mut self, collection_id: GlobalId) -> Result<(), CollectionMissing> {
1056        let collection = self.collection_mut(collection_id)?;
1057
1058        // Do not send redundant allow-writes commands.
1059        if !collection.read_only {
1060            return Ok(());
1061        }
1062
1063        // Don't send allow-writes for collections that are not installed.
1064        let as_of = collection.read_frontier();
1065
1066        // If the collection has an empty `as_of`, it was either never installed on the replica or
1067        // has since been dropped. In either case the replica does not expect any commands for it.
1068        if as_of.is_empty() {
1069            return Ok(());
1070        }
1071
1072        collection.read_only = false;
1073        self.send(ComputeCommand::AllowWrites(collection_id));
1074
1075        Ok(())
1076    }
1077
1078    /// Shut down this instance.
1079    ///
1080    /// This method asserts that the instance has no replicas left. It exists to help
1081    /// us find bugs where the client drops a compute instance that still has replicas
1082    /// installed, and later assumes that said replicas still exist.
1083    ///
1084    /// # Panics
1085    ///
1086    /// Soft-panics if the compute instance still has active replicas.
1087    #[mz_ore::instrument(level = "debug")]
1088    pub fn shutdown(&mut self) {
1089        // Taking the `command_rx` ensures that the [`Instance::run`] loop terminates.
1090        let (_tx, rx) = mpsc::unbounded_channel();
1091        self.command_rx = rx;
1092
1093        let stray_replicas: Vec<_> = self.replicas.keys().collect();
1094        soft_assert_or_log!(
1095            stray_replicas.is_empty(),
1096            "dropped instance still has provisioned replicas: {stray_replicas:?}",
1097        );
1098    }
1099
1100    /// Terminate the [`Instance::run`] loop, causing the instance task to shut down.
1101    ///
1102    /// Unlike [`Instance::shutdown`], this does not assert that the instance has no replicas
1103    /// left. We use it to react to unrecoverable errors that can only occur during process
1104    /// shutdown, such as a storage read hold issuer hanging up while we rehydrate a replica.
1105    fn initiate_shutdown(&mut self) {
1106        // Replacing `command_rx` with a fresh, sender-less channel makes the next `recv` in
1107        // [`Instance::run`] return `None`, terminating the loop.
1108        let (_tx, rx) = mpsc::unbounded_channel();
1109        self.command_rx = rx;
1110    }
1111
1112    /// Sends a command to replicas of this instance.
1113    #[mz_ore::instrument(level = "debug")]
1114    fn send(&mut self, cmd: ComputeCommand) {
1115        // Record the command so that new replicas can be brought up to speed.
1116        // We record the *base* (un-specialized) command, so that the per-replica
1117        // dyncfg overrides are re-applied at replay time in `add_replica` rather
1118        // than baked into the shared history.
1119        self.history.push(cmd.clone());
1120
1121        let target_replica = self.target_replica(&cmd);
1122
1123        // Borrow the overrides and dyncfg separately from `self.replicas` so the per-replica
1124        // specialization below does not conflict with the mutable replica borrow.
1125        let overrides = &self.replica_dyncfg_overrides;
1126        let dyncfg = &self.dyncfg;
1127
1128        if let Some(rid) = target_replica {
1129            if let Some(replica) = self.replicas.get_mut(&rid) {
1130                let cmd = Self::specialize_command_for_replica(cmd, rid, overrides, dyncfg);
1131                let _ = replica.client.send(cmd);
1132            }
1133        } else {
1134            for (rid, replica) in self.replicas.iter_mut() {
1135                let cmd =
1136                    Self::specialize_command_for_replica(cmd.clone(), *rid, overrides, dyncfg);
1137                let _ = replica.client.send(cmd);
1138            }
1139        }
1140    }
1141
1142    /// Specializes a command for a specific replica by merging that replica's dyncfg override into
1143    /// its configuration. For `UpdateConfiguration` the override is merged into the update. For
1144    /// `CreateInstance` the current dyncfg, with the override applied on top, is captured as the
1145    /// initial config snapshot. All other commands are returned unchanged.
1146    ///
1147    /// The snapshot is built here, rather than baked into the history, so it reflects the dyncfg
1148    /// and override values current at the time the command is sent or replayed to the replica.
1149    fn specialize_command_for_replica(
1150        mut cmd: ComputeCommand,
1151        replica_id: ReplicaId,
1152        overrides: &BTreeMap<ReplicaId, ConfigUpdates>,
1153        dyncfg: &ConfigSet,
1154    ) -> ComputeCommand {
1155        let over = overrides.get(&replica_id);
1156        match &mut cmd {
1157            ComputeCommand::UpdateConfiguration(params) => {
1158                if let Some(over) = over
1159                    && !over.updates.is_empty()
1160                {
1161                    params.dyncfg_updates.extend(over.clone());
1162                }
1163            }
1164            ComputeCommand::CreateInstance(config) => {
1165                let mut initial = ConfigUpdates::from(dyncfg);
1166                if let Some(over) = over {
1167                    initial.extend(over.clone());
1168                }
1169                config.initial_config = initial;
1170            }
1171            _ => {}
1172        }
1173        cmd
1174    }
1175
1176    /// Replaces the per-replica dyncfg overrides. Callers should follow this
1177    /// with a configuration push (e.g. `update_configuration`) so that existing
1178    /// replicas observe the new overrides.
1179    pub(super) fn update_replica_dyncfg_overrides(
1180        &mut self,
1181        overrides: BTreeMap<ReplicaId, ConfigUpdates>,
1182    ) {
1183        self.replica_dyncfg_overrides = overrides;
1184    }
1185
1186    /// Determine the target replica for a compute command. Retrieves the
1187    /// collection named by the command, and returns the target replica if
1188    /// it is set, and None if not set, or the command doesn't name a collection.
1189    ///
1190    /// Panics if a create-dataflow command names collections that have different
1191    /// target replicas. It is an error to construct such an object and would
1192    /// indicate a bug in [`Self::create_dataflow`].
1193    fn target_replica(&self, cmd: &ComputeCommand) -> Option<ReplicaId> {
1194        match &cmd {
1195            ComputeCommand::Schedule(id)
1196            | ComputeCommand::AllowWrites(id)
1197            | ComputeCommand::AllowCompaction { id, .. } => {
1198                self.expect_collection(*id).target_replica
1199            }
1200            ComputeCommand::CreateDataflow(desc) => {
1201                let mut target_replica = None;
1202                for id in desc.export_ids() {
1203                    if let Some(replica) = self.expect_collection(id).target_replica {
1204                        if target_replica.is_some() {
1205                            assert_eq!(target_replica, Some(replica));
1206                        }
1207                        target_replica = Some(replica);
1208                    }
1209                }
1210                target_replica
1211            }
1212            // Skip Peek as we don't allow replica-targeted indexes.
1213            ComputeCommand::Peek(_)
1214            | ComputeCommand::Hello { .. }
1215            | ComputeCommand::CreateInstance(_)
1216            | ComputeCommand::InitializationComplete
1217            | ComputeCommand::UpdateConfiguration(_)
1218            | ComputeCommand::CancelPeek { .. } => None,
1219        }
1220    }
1221
1222    /// Add a new instance replica, by ID.
1223    #[mz_ore::instrument(level = "debug")]
1224    pub fn add_replica(
1225        &mut self,
1226        id: ReplicaId,
1227        mut config: ReplicaConfig,
1228        epoch: Option<u64>,
1229    ) -> Result<(), ReplicaExists> {
1230        if self.replica_exists(id) {
1231            return Err(ReplicaExists(id));
1232        }
1233
1234        config.logging.index_logs = self.log_sources.clone();
1235
1236        let epoch = epoch.unwrap_or(1);
1237        let metrics = self.metrics.for_replica(id);
1238        let client = ReplicaClient::spawn(
1239            id,
1240            self.build_info,
1241            config.clone(),
1242            epoch,
1243            metrics.clone(),
1244            Arc::clone(&self.dyncfg),
1245            self.replica_tx.clone(),
1246        );
1247
1248        // Take this opportunity to clean up the history we should present.
1249        self.history.reduce();
1250
1251        // Advance the uppers of source imports
1252        self.history.update_source_uppers(&self.storage_collections);
1253
1254        // Replay the commands at the client, creating new dataflow identifiers.
1255        for command in self.history.iter() {
1256            // Skip `CreateDataflow` commands targeted at different replicas.
1257            if let Some(target_replica) = self.target_replica(command)
1258                && target_replica != id
1259            {
1260                continue;
1261            }
1262
1263            // Re-apply this replica's dyncfg override to replayed config commands, and rebuild the
1264            // create-instance snapshot from the current dyncfg.
1265            let command = Self::specialize_command_for_replica(
1266                command.clone(),
1267                id,
1268                &self.replica_dyncfg_overrides,
1269                &self.dyncfg,
1270            );
1271            if client.send(command).is_err() {
1272                // We swallow the error here. On the next send, we will fail again, and
1273                // restart the connection as well as this rehydration.
1274                tracing::warn!("Replica {:?} connection terminated during hydration", id);
1275                break;
1276            }
1277        }
1278
1279        // Add replica to tracked state.
1280        if self.add_replica_state(id, client, config, epoch).is_err() {
1281            // A storage read hold issuer hung up, which only happens during process shutdown.
1282            // There is no way to correctly bring up the replica anymore, so we shut the instance
1283            // down instead of running on with half-initialized replica state. `add_replica_state`
1284            // has already logged the details and inserted the replica to keep our bookkeeping
1285            // consistent with the controller's.
1286            self.initiate_shutdown();
1287        }
1288
1289        Ok(())
1290    }
1291
1292    /// Remove an existing instance replica, by ID.
1293    #[mz_ore::instrument(level = "debug")]
1294    pub fn remove_replica(&mut self, id: ReplicaId) -> Result<(), ReplicaMissing> {
1295        let replica = self.replicas.remove(&id).ok_or(ReplicaMissing(id))?;
1296
1297        // The coordinator only re-pushes the override map when the scoped configuration itself
1298        // changes, so a dropped replica's entry would otherwise be retained until the next such
1299        // change.
1300        self.replica_dyncfg_overrides.remove(&id);
1301
1302        // Before dropping the replica state (and the contained input read holds), log read holds
1303        // that are the last line of defense against compaction of a dataflow's storage inputs. If
1304        // the corresponding global read hold has already been released, dropping the per-replica
1305        // read hold will allow compaction, which can cause the replica to panic trying to install
1306        // the dataflow.
1307        //
1308        // This exists primarily to help diagnose incidents-and-escalations#39.
1309        for (collection_id, replica_collection) in &replica.collections {
1310            let collection = self.collections.get(collection_id);
1311            for replica_hold in &replica_collection.input_read_holds {
1312                let input_id = replica_hold.id();
1313                let global_hold = collection.and_then(|c| c.storage_dependencies.get(&input_id));
1314                let unprotected = global_hold
1315                    .is_none_or(|h| PartialOrder::less_than(replica_hold.since(), h.since()));
1316                if unprotected {
1317                    tracing::warn!(
1318                        replica_id = %id,
1319                        %collection_id,
1320                        %input_id,
1321                        replica_hold_since = ?replica_hold.since(),
1322                        global_hold_since = ?global_hold.map(|h| h.since()),
1323                        "dropping per-replica read hold without equivalent global read hold",
1324                    );
1325                }
1326            }
1327        }
1328        drop(replica);
1329
1330        // Subscribes targeting this replica either won't be served anymore (if the replica is
1331        // dropped) or might produce inconsistent output (if the target collection is an
1332        // introspection index). We produce an error to inform upstream.
1333        let to_drop: Vec<_> = self.subscribes_targeting(id).collect();
1334        for subscribe_id in to_drop {
1335            let subscribe = self.subscribes.remove(&subscribe_id).unwrap();
1336            let response = ComputeControllerResponse::SubscribeResponse(
1337                subscribe_id,
1338                SubscribeBatch {
1339                    lower: subscribe.frontier.clone(),
1340                    upper: subscribe.frontier,
1341                    updates: Err(ERROR_TARGET_REPLICA_FAILED.into()),
1342                },
1343            );
1344            self.deliver_response(response);
1345        }
1346
1347        // Peeks targeting this replica might not be served anymore (if the replica is dropped).
1348        // If the replica has failed it might come back and respond to the peek later, but it still
1349        // seems like a good idea to cancel the peek to inform the caller about the failure. This
1350        // is consistent with how we handle targeted subscribes above.
1351        let mut peek_responses = Vec::new();
1352        let mut to_drop = Vec::new();
1353        for (uuid, peek) in self.peeks_targeting(id) {
1354            peek_responses.push(ComputeControllerResponse::PeekNotification(
1355                uuid,
1356                PeekNotification::Error(ERROR_TARGET_REPLICA_FAILED.into()),
1357                peek.otel_ctx.clone(),
1358            ));
1359            to_drop.push(uuid);
1360        }
1361        for response in peek_responses {
1362            self.deliver_response(response);
1363        }
1364        for uuid in to_drop {
1365            let response =
1366                PeekResponse::Error(ProtocolPeekError::unstructured(ERROR_TARGET_REPLICA_FAILED));
1367            self.finish_peek(uuid, response);
1368        }
1369
1370        // We might have a chance to forward implied capabilities and reduce the cost of bringing
1371        // up the next replica, if the dropped replica was the only one in the cluster.
1372        self.forward_implied_capabilities();
1373
1374        Ok(())
1375    }
1376
1377    /// Rehydrate the given instance replica.
1378    ///
1379    /// # Panics
1380    ///
1381    /// Panics if the specified replica does not exist.
1382    fn rehydrate_replica(&mut self, id: ReplicaId) {
1383        let config = self.replicas[&id].config.clone();
1384        let epoch = self.replicas[&id].epoch + 1;
1385
1386        self.remove_replica(id).expect("replica must exist");
1387        let result = self.add_replica(id, config, Some(epoch));
1388
1389        match result {
1390            Ok(()) => (),
1391            Err(ReplicaExists(_)) => unreachable!("replica was removed"),
1392        }
1393    }
1394
1395    /// Rehydrate any failed replicas of this instance.
1396    fn rehydrate_failed_replicas(&mut self) {
1397        let replicas = self.replicas.iter();
1398        let failed_replicas: Vec<_> = replicas
1399            .filter_map(|(id, replica)| replica.client.is_failed().then_some(*id))
1400            .collect();
1401
1402        for replica_id in failed_replicas {
1403            self.rehydrate_replica(replica_id);
1404        }
1405    }
1406
1407    /// Creates the described dataflow and initializes state for its output.
1408    ///
1409    /// This method expects a `DataflowDescription` with an `as_of` frontier specified, as well as
1410    /// for each imported collection a read hold in `import_read_holds` at at least the `as_of`.
1411    #[mz_ore::instrument(level = "debug")]
1412    pub fn create_dataflow(
1413        &mut self,
1414        dataflow: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
1415        import_read_holds: Vec<ReadHold>,
1416        mut shared_collection_state: BTreeMap<GlobalId, SharedCollectionState>,
1417        target_replica: Option<ReplicaId>,
1418    ) -> Result<(), DataflowCreationError> {
1419        use DataflowCreationError::*;
1420
1421        // Validate that the target replica, if specified, exists.
1422        // A targeted dataflow is only installed on a single replica; if that
1423        // replica doesn't exist, we can't create the dataflow.
1424        if let Some(replica_id) = target_replica {
1425            if !self.replica_exists(replica_id) {
1426                return Err(ReplicaMissing(replica_id));
1427            }
1428        }
1429
1430        // Simple sanity checks around `as_of`
1431        let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
1432        if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
1433            return Err(EmptyAsOfForSubscribe);
1434        }
1435        if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
1436            return Err(EmptyAsOfForCopyTo);
1437        }
1438
1439        // Collect all dependencies of the dataflow, and read holds on them at the `as_of`.
1440        let mut storage_dependencies = BTreeMap::new();
1441        let mut compute_dependencies = BTreeMap::new();
1442
1443        // When we install per-replica input read holds, we cannot use the `as_of` because of
1444        // reconciliation: Existing slow replicas might be reading from the inputs at times before
1445        // the `as_of` and we would rather not crash them by allowing their inputs to compact too
1446        // far. So instead we take read holds at the least time available.
1447        let mut replica_input_read_holds = Vec::new();
1448
1449        let mut import_read_holds: BTreeMap<_, _> =
1450            import_read_holds.into_iter().map(|r| (r.id(), r)).collect();
1451
1452        for &id in dataflow.source_imports.keys() {
1453            let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1454            replica_input_read_holds.push(read_hold.clone());
1455
1456            read_hold
1457                .try_downgrade(as_of.clone())
1458                .map_err(|_| ReadHoldInsufficient(id))?;
1459            storage_dependencies.insert(id, read_hold);
1460        }
1461
1462        for &id in dataflow.index_imports.keys() {
1463            let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1464            read_hold
1465                .try_downgrade(as_of.clone())
1466                .map_err(|_| ReadHoldInsufficient(id))?;
1467            compute_dependencies.insert(id, read_hold);
1468        }
1469
1470        // If the `as_of` is empty, we are not going to create a dataflow, so replicas won't read
1471        // from the inputs.
1472        if as_of.is_empty() {
1473            replica_input_read_holds = Default::default();
1474        }
1475
1476        // Install collection state for each of the exports.
1477        for export_id in dataflow.export_ids() {
1478            let shared = shared_collection_state
1479                .remove(&export_id)
1480                .unwrap_or_else(|| SharedCollectionState::new(as_of.clone()));
1481            let write_only = dataflow.sink_exports.contains_key(&export_id);
1482            let storage_sink = dataflow.persist_sink_ids().any(|id| id == export_id);
1483
1484            self.add_collection(
1485                export_id,
1486                as_of.clone(),
1487                shared,
1488                storage_dependencies.clone(),
1489                compute_dependencies.clone(),
1490                replica_input_read_holds.clone(),
1491                write_only,
1492                storage_sink,
1493                dataflow.initial_storage_as_of.clone(),
1494                dataflow.refresh_schedule.clone(),
1495                target_replica,
1496            );
1497
1498            // If the export is a storage sink, we can advance its write frontier to the write
1499            // frontier of the target storage collection.
1500            if let Ok(frontiers) = self.storage_collections.collection_frontiers(export_id) {
1501                self.maybe_update_global_write_frontier(export_id, frontiers.write_frontier);
1502            }
1503        }
1504
1505        // Initialize tracking of subscribes.
1506        for subscribe_id in dataflow.subscribe_ids() {
1507            self.subscribes
1508                .insert(subscribe_id, ActiveSubscribe::default());
1509        }
1510
1511        // Initialize tracking of copy tos.
1512        for copy_to_id in dataflow.copy_to_ids() {
1513            self.copy_tos.insert(copy_to_id);
1514        }
1515
1516        // Here we augment all imported sources and all exported sinks with the appropriate
1517        // storage metadata needed by the compute instance.
1518        let mut source_imports = BTreeMap::new();
1519        for (id, import) in dataflow.source_imports {
1520            let frontiers = self
1521                .storage_collections
1522                .collection_frontiers(id)
1523                .expect("collection exists");
1524
1525            let collection_metadata = self
1526                .storage_collections
1527                .collection_metadata(id)
1528                .expect("we have a read hold on this collection");
1529
1530            let desc = SourceInstanceDesc {
1531                storage_metadata: collection_metadata.clone(),
1532                arguments: import.desc.arguments,
1533                typ: import.desc.typ.clone(),
1534            };
1535            source_imports.insert(
1536                id,
1537                mz_compute_types::dataflows::SourceImport {
1538                    desc,
1539                    monotonic: import.monotonic,
1540                    with_snapshot: import.with_snapshot,
1541                    upper: frontiers.write_frontier,
1542                },
1543            );
1544        }
1545
1546        let mut sink_exports = BTreeMap::new();
1547        for (id, se) in dataflow.sink_exports {
1548            let connection = match se.connection {
1549                ComputeSinkConnection::MaterializedView(conn) => {
1550                    let metadata = self
1551                        .storage_collections
1552                        .collection_metadata(id)
1553                        .map_err(|_| CollectionMissing(id))?
1554                        .clone();
1555                    let conn = MaterializedViewSinkConnection {
1556                        value_desc: conn.value_desc,
1557                        storage_metadata: metadata,
1558                    };
1559                    ComputeSinkConnection::MaterializedView(conn)
1560                }
1561                ComputeSinkConnection::Subscribe(conn) => ComputeSinkConnection::Subscribe(conn),
1562                ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1563                    ComputeSinkConnection::CopyToS3Oneshot(conn)
1564                }
1565                ComputeSinkConnection::MetricSink(conn) => ComputeSinkConnection::MetricSink(conn),
1566            };
1567            let desc = ComputeSinkDesc {
1568                from: se.from,
1569                from_desc: se.from_desc,
1570                connection,
1571                with_snapshot: se.with_snapshot,
1572                up_to: se.up_to,
1573                non_null_assertions: se.non_null_assertions,
1574                refresh_schedule: se.refresh_schedule,
1575            };
1576            sink_exports.insert(id, desc);
1577        }
1578
1579        // Flatten the dataflow plans into the representation expected by replicas.
1580        let objects_to_build = dataflow
1581            .objects_to_build
1582            .into_iter()
1583            .map(|object| BuildDesc {
1584                id: object.id,
1585                plan: RenderPlan::try_from(object.plan).expect("valid plan"),
1586            })
1587            .collect();
1588
1589        let augmented_dataflow = DataflowDescription {
1590            source_imports,
1591            sink_exports,
1592            objects_to_build,
1593            // The rest of the fields are identical
1594            index_imports: dataflow.index_imports,
1595            index_exports: dataflow.index_exports,
1596            as_of: dataflow.as_of.clone(),
1597            until: dataflow.until,
1598            initial_storage_as_of: dataflow.initial_storage_as_of,
1599            refresh_schedule: dataflow.refresh_schedule,
1600            debug_name: dataflow.debug_name,
1601            time_dependence: dataflow.time_dependence,
1602        };
1603
1604        if augmented_dataflow.is_transient() {
1605            tracing::debug!(
1606                name = %augmented_dataflow.debug_name,
1607                import_ids = %augmented_dataflow.display_import_ids(),
1608                export_ids = %augmented_dataflow.display_export_ids(),
1609                as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1610                until = ?augmented_dataflow.until.elements(),
1611                "creating dataflow",
1612            );
1613        } else {
1614            tracing::info!(
1615                name = %augmented_dataflow.debug_name,
1616                import_ids = %augmented_dataflow.display_import_ids(),
1617                export_ids = %augmented_dataflow.display_export_ids(),
1618                as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1619                until = ?augmented_dataflow.until.elements(),
1620                "creating dataflow",
1621            );
1622        }
1623
1624        // Skip the actual dataflow creation for an empty `as_of`. (Happens e.g. for the
1625        // bootstrapping of a REFRESH AT mat view that is past its last refresh.)
1626        if as_of.is_empty() {
1627            tracing::info!(
1628                name = %augmented_dataflow.debug_name,
1629                "not sending `CreateDataflow`, because of empty `as_of`",
1630            );
1631        } else {
1632            let collections: Vec<_> = augmented_dataflow.export_ids().collect();
1633            self.send(ComputeCommand::CreateDataflow(Box::new(augmented_dataflow)));
1634
1635            for id in collections {
1636                self.maybe_schedule_collection(id);
1637            }
1638        }
1639
1640        Ok(())
1641    }
1642
1643    /// Schedule the identified collection if all its inputs are available.
1644    ///
1645    /// # Panics
1646    ///
1647    /// Panics if the identified collection does not exist.
1648    fn maybe_schedule_collection(&mut self, id: GlobalId) {
1649        let collection = self.expect_collection(id);
1650
1651        // Don't schedule collections twice.
1652        if collection.scheduled {
1653            return;
1654        }
1655
1656        let as_of = collection.read_frontier();
1657
1658        // If the collection has an empty `as_of`, it was either never installed on the replica or
1659        // has since been dropped. In either case the replica does not expect any commands for it.
1660        if as_of.is_empty() {
1661            return;
1662        }
1663
1664        let ready = if id.is_transient() {
1665            // Always schedule transient collections immediately. The assumption is that those are
1666            // created by interactive user commands and we want to schedule them as quickly as
1667            // possible. Inputs might not yet be available, but when they become available, we
1668            // don't need to wait for the controller to become aware and for the scheduling check
1669            // to run again.
1670            true
1671        } else {
1672            // Ignore self-dependencies. Any self-dependencies do not need to be
1673            // available at the as_of for the dataflow to make progress, so we
1674            // can ignore them here. At the moment, only continual tasks have
1675            // self-dependencies, but this logic is correct for any dataflow, so
1676            // we don't special case it to CTs.
1677            let not_self_dep = |x: &GlobalId| *x != id;
1678
1679            // Make sure we never schedule a collection before its input compute collections have
1680            // been scheduled. Scheduling in the wrong order can lead to deadlocks.
1681            let mut deps_scheduled = true;
1682
1683            // Check dependency frontiers to determine if all inputs are
1684            // available. An input is available when its frontier is greater
1685            // than the `as_of`, i.e., all input data up to and including the
1686            // `as_of` has been sealed.
1687            let compute_deps = collection.compute_dependency_ids().filter(not_self_dep);
1688            let mut compute_frontiers = Vec::new();
1689            for id in compute_deps {
1690                let dep = &self.expect_collection(id);
1691                deps_scheduled &= dep.scheduled;
1692                compute_frontiers.push(dep.write_frontier());
1693            }
1694
1695            let storage_deps = collection.storage_dependency_ids().filter(not_self_dep);
1696            let storage_frontiers = self
1697                .storage_collections
1698                .collections_frontiers(storage_deps.collect())
1699                .expect("must exist");
1700            let storage_frontiers = storage_frontiers.into_iter().map(|f| f.write_frontier);
1701
1702            let mut frontiers = compute_frontiers.into_iter().chain(storage_frontiers);
1703            let frontiers_ready =
1704                frontiers.all(|frontier| PartialOrder::less_than(&as_of, &frontier));
1705
1706            deps_scheduled && frontiers_ready
1707        };
1708
1709        if ready {
1710            self.send(ComputeCommand::Schedule(id));
1711            let collection = self.expect_collection_mut(id);
1712            collection.scheduled = true;
1713        }
1714    }
1715
1716    /// Schedule any unscheduled collections that are ready.
1717    fn schedule_collections(&mut self) {
1718        let ids: Vec<_> = self.collections.keys().copied().collect();
1719        for id in ids {
1720            self.maybe_schedule_collection(id);
1721        }
1722    }
1723
1724    /// Drops the read capability for the given collections and allows their resources to be
1725    /// reclaimed.
1726    #[mz_ore::instrument(level = "debug")]
1727    pub fn drop_collections(&mut self, ids: Vec<GlobalId>) -> Result<(), CollectionMissing> {
1728        for id in &ids {
1729            let collection = self.collection_mut(*id)?;
1730
1731            // Mark the collection as dropped to allow it to be removed from the controller state.
1732            collection.dropped = true;
1733
1734            // Drop the implied and warmup read holds to announce that clients are not
1735            // interested in the collection anymore.
1736            collection.implied_read_hold.release();
1737            collection.warmup_read_hold.release();
1738
1739            // If the collection is a subscribe, stop tracking it. This ensures that the controller
1740            // ceases to produce `SubscribeResponse`s for this subscribe.
1741            self.subscribes.remove(id);
1742            // If the collection is a copy to, stop tracking it. This ensures that the controller
1743            // ceases to produce `CopyToResponse`s` for this copy to.
1744            self.copy_tos.remove(id);
1745        }
1746
1747        Ok(())
1748    }
1749
1750    /// Initiate a peek request for the contents of `id` at `timestamp`.
1751    ///
1752    /// If this returns an error, then it didn't modify any `Instance` state.
1753    #[mz_ore::instrument(level = "debug")]
1754    pub fn peek(
1755        &mut self,
1756        peek_target: PeekTarget,
1757        literal_constraints: Option<Vec<Row>>,
1758        uuid: Uuid,
1759        timestamp: Timestamp,
1760        result_desc: RelationDesc,
1761        finishing: RowSetFinishing,
1762        map_filter_project: mz_expr::SafeMfpPlan,
1763        mut read_hold: ReadHold,
1764        target_replica: Option<ReplicaId>,
1765        peek_response_tx: oneshot::Sender<PeekResponse>,
1766    ) -> Result<(), PeekError> {
1767        use PeekError::*;
1768
1769        let target_id = peek_target.id();
1770
1771        // Downgrade the provided read hold to the peek time.
1772        if read_hold.id() != target_id {
1773            return Err(ReadHoldIdMismatch(read_hold.id()));
1774        }
1775        read_hold
1776            .try_downgrade(Antichain::from_elem(timestamp.clone()))
1777            .map_err(|_| ReadHoldInsufficient(target_id))?;
1778
1779        if let Some(target) = target_replica {
1780            if !self.replica_exists(target) {
1781                return Err(ReplicaMissing(target));
1782            }
1783        }
1784
1785        let otel_ctx = OpenTelemetryContext::obtain();
1786
1787        self.peeks.insert(
1788            uuid,
1789            PendingPeek {
1790                target_replica,
1791                // TODO(guswynn): can we just hold the `tracing::Span` here instead?
1792                otel_ctx: otel_ctx.clone(),
1793                requested_at: Instant::now(),
1794                read_hold,
1795                peek_response_tx,
1796                limit: finishing.limit.map(usize::cast_from),
1797                offset: finishing.offset,
1798            },
1799        );
1800
1801        let peek = Peek {
1802            literal_constraints,
1803            uuid,
1804            timestamp,
1805            finishing,
1806            map_filter_project,
1807            // Obtain an `OpenTelemetryContext` from the thread-local tracing
1808            // tree to forward it on to the compute worker.
1809            otel_ctx,
1810            target: peek_target,
1811            result_desc,
1812        };
1813        self.send(ComputeCommand::Peek(Box::new(peek)));
1814
1815        Ok(())
1816    }
1817
1818    /// Cancels an existing peek request.
1819    #[mz_ore::instrument(level = "debug")]
1820    pub fn cancel_peek(&mut self, uuid: Uuid, reason: PeekResponse) {
1821        let Some(peek) = self.peeks.get_mut(&uuid) else {
1822            tracing::warn!("did not find pending peek for {uuid}");
1823            return;
1824        };
1825
1826        let duration = peek.requested_at.elapsed();
1827        self.metrics
1828            .observe_peek_response(&PeekResponse::Canceled, duration);
1829
1830        // Enqueue a notification for the cancellation.
1831        let otel_ctx = peek.otel_ctx.clone();
1832        otel_ctx.attach_as_parent();
1833
1834        self.deliver_response(ComputeControllerResponse::PeekNotification(
1835            uuid,
1836            PeekNotification::Canceled,
1837            otel_ctx,
1838        ));
1839
1840        // Finish the peek.
1841        // This will also propagate the cancellation to the replicas.
1842        self.finish_peek(uuid, reason);
1843    }
1844
1845    /// Assigns a read policy to specific identifiers.
1846    ///
1847    /// The policies are assigned in the order presented, and repeated identifiers should
1848    /// conclude with the last policy. Changing a policy will immediately downgrade the read
1849    /// capability if appropriate, but it will not "recover" the read capability if the prior
1850    /// capability is already ahead of it.
1851    ///
1852    /// Identifiers not present in `policies` retain their existing read policies.
1853    ///
1854    /// It is an error to attempt to set a read policy for a collection that is not readable in the
1855    /// context of compute. At this time, only indexes are readable compute collections.
1856    #[mz_ore::instrument(level = "debug")]
1857    pub fn set_read_policy(
1858        &mut self,
1859        policies: Vec<(GlobalId, ReadPolicy)>,
1860    ) -> Result<(), ReadPolicyError> {
1861        // Do error checking upfront, to avoid introducing inconsistencies between a collection's
1862        // `implied_capability` and `read_capabilities`.
1863        for (id, _policy) in &policies {
1864            let collection = self.collection(*id)?;
1865            if collection.read_policy.is_none() {
1866                return Err(ReadPolicyError::WriteOnlyCollection(*id));
1867            }
1868        }
1869
1870        for (id, new_policy) in policies {
1871            let collection = self.expect_collection_mut(id);
1872            let new_since = new_policy.frontier(collection.write_frontier().borrow());
1873            let _ = collection.implied_read_hold.try_downgrade(new_since);
1874            collection.read_policy = Some(new_policy);
1875        }
1876
1877        Ok(())
1878    }
1879
1880    /// Advance the global write frontier of the given collection.
1881    ///
1882    /// Frontier regressions are gracefully ignored.
1883    ///
1884    /// # Panics
1885    ///
1886    /// Panics if the identified collection does not exist.
1887    #[mz_ore::instrument(level = "debug")]
1888    fn maybe_update_global_write_frontier(
1889        &mut self,
1890        id: GlobalId,
1891        new_frontier: Antichain<Timestamp>,
1892    ) {
1893        let collection = self.expect_collection_mut(id);
1894
1895        let advanced = collection.shared.lock_write_frontier(|f| {
1896            let advanced = PartialOrder::less_than(f, &new_frontier);
1897            if advanced {
1898                f.clone_from(&new_frontier);
1899            }
1900            advanced
1901        });
1902
1903        if !advanced {
1904            return;
1905        }
1906
1907        // Relax the implied read hold according to the read policy.
1908        let new_since = match &collection.read_policy {
1909            Some(read_policy) => {
1910                // For readable collections the read frontier is determined by applying the
1911                // client-provided read policy to the write frontier.
1912                read_policy.frontier(new_frontier.borrow())
1913            }
1914            None => {
1915                // Write-only collections cannot be read within the context of the compute
1916                // controller, so their read frontier only controls the read holds taken on their
1917                // inputs. We can safely downgrade the input read holds to any time less than the
1918                // write frontier.
1919                //
1920                // Note that some write-only collections (continual tasks) need to observe changes
1921                // at their current write frontier during hydration. Thus, we cannot downgrade the
1922                // read frontier to the write frontier and instead step it back by one.
1923                Antichain::from_iter(
1924                    new_frontier
1925                        .iter()
1926                        .map(|t| t.step_back().unwrap_or(Timestamp::MIN)),
1927                )
1928            }
1929        };
1930        let _ = collection.implied_read_hold.try_downgrade(new_since);
1931
1932        // Report the frontier advancement.
1933        self.deliver_response(ComputeControllerResponse::FrontierUpper {
1934            id,
1935            upper: new_frontier,
1936        });
1937    }
1938
1939    /// Apply a collection read hold change.
1940    pub(super) fn apply_read_hold_change(
1941        &mut self,
1942        id: GlobalId,
1943        mut update: ChangeBatch<Timestamp>,
1944    ) {
1945        let Some(collection) = self.collections.get_mut(&id) else {
1946            soft_panic_or_log!(
1947                "read hold change for absent collection (id={id}, changes={update:?})"
1948            );
1949            return;
1950        };
1951
1952        let new_since = collection.shared.lock_read_capabilities(|caps| {
1953            // Sanity check to prevent corrupted `read_capabilities`, which can cause hard-to-debug
1954            // issues (usually stuck read frontiers).
1955            let read_frontier = caps.frontier();
1956            for (time, diff) in update.iter() {
1957                let count = caps.count_for(time) + diff;
1958                assert!(
1959                    count >= 0,
1960                    "invalid read capabilities update: negative capability \
1961             (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1962                );
1963                assert!(
1964                    count == 0 || read_frontier.less_equal(time),
1965                    "invalid read capabilities update: frontier regression \
1966             (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1967                );
1968            }
1969
1970            // Apply read capability updates and learn about resulting changes to the read
1971            // frontier.
1972            let changes = caps.update_iter(update.drain());
1973
1974            let changed = changes.count() > 0;
1975            changed.then(|| caps.frontier().to_owned())
1976        });
1977
1978        let Some(new_since) = new_since else {
1979            return; // read frontier did not change
1980        };
1981
1982        // Propagate read frontier update to dependencies.
1983        for read_hold in collection.compute_dependencies.values_mut() {
1984            read_hold
1985                .try_downgrade(new_since.clone())
1986                .expect("frontiers don't regress");
1987        }
1988        for read_hold in collection.storage_dependencies.values_mut() {
1989            read_hold
1990                .try_downgrade(new_since.clone())
1991                .expect("frontiers don't regress");
1992        }
1993
1994        // Produce `AllowCompaction` command.
1995        self.send(ComputeCommand::AllowCompaction {
1996            id,
1997            frontier: new_since,
1998        });
1999    }
2000
2001    /// Fulfills a registered peek and cleans up associated state.
2002    ///
2003    /// As part of this we:
2004    ///  * Send a `PeekResponse` through the peek's response channel.
2005    ///  * Emit a `CancelPeek` command to instruct replicas to stop spending resources on this
2006    ///    peek, and to allow the `ComputeCommandHistory` to reduce away the corresponding `Peek`
2007    ///    command.
2008    ///  * Remove the read hold for this peek, unblocking compaction that might have waited on it.
2009    fn finish_peek(&mut self, uuid: Uuid, response: PeekResponse) {
2010        let Some(peek) = self.peeks.remove(&uuid) else {
2011            return;
2012        };
2013
2014        // The recipient might not be interested in the peek response anymore, which is fine.
2015        let _ = peek.peek_response_tx.send(response);
2016
2017        // NOTE: We need to send the `CancelPeek` command _before_ we release the peek's read hold
2018        // (by dropping it), to avoid the edge case that caused database-issues#4812.
2019        self.send(ComputeCommand::CancelPeek { uuid });
2020
2021        drop(peek.read_hold);
2022    }
2023
2024    /// Handles a response from a replica. Replica IDs are re-used across replica restarts, so we
2025    /// use the replica epoch to drop stale responses.
2026    fn handle_response(&mut self, (replica_id, epoch, response): ReplicaResponse) {
2027        // Filter responses from non-existing or stale replicas.
2028        if self
2029            .replicas
2030            .get(&replica_id)
2031            .filter(|replica| replica.epoch == epoch)
2032            .is_none()
2033        {
2034            return;
2035        }
2036
2037        // Invariant: the replica exists and has the expected epoch.
2038
2039        match response {
2040            ComputeResponse::Frontiers(id, frontiers) => {
2041                self.handle_frontiers_response(id, frontiers, replica_id);
2042            }
2043            ComputeResponse::PeekResponse(uuid, peek_response, otel_ctx) => {
2044                self.handle_peek_response(uuid, peek_response, otel_ctx, replica_id);
2045            }
2046            ComputeResponse::CopyToResponse(id, response) => {
2047                self.handle_copy_to_response(id, response, replica_id);
2048            }
2049            ComputeResponse::SubscribeResponse(id, response) => {
2050                self.handle_subscribe_response(id, response, replica_id);
2051            }
2052            ComputeResponse::Status(response) => {
2053                self.handle_status_response(response, replica_id);
2054            }
2055        }
2056    }
2057
2058    /// Handle new frontiers, returning any compute response that needs to
2059    /// be sent to the client.
2060    fn handle_frontiers_response(
2061        &mut self,
2062        id: GlobalId,
2063        frontiers: FrontiersResponse,
2064        replica_id: ReplicaId,
2065    ) {
2066        if !self.collections.contains_key(&id) {
2067            soft_panic_or_log!(
2068                "frontiers update for an unknown collection \
2069                 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2070            );
2071            return;
2072        }
2073        let Some(replica) = self.replicas.get_mut(&replica_id) else {
2074            soft_panic_or_log!(
2075                "frontiers update for an unknown replica \
2076                 (replica_id={replica_id}, frontiers={frontiers:?})"
2077            );
2078            return;
2079        };
2080        let Some(replica_collection) = replica.collections.get_mut(&id) else {
2081            soft_panic_or_log!(
2082                "frontiers update for an unknown replica collection \
2083                 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2084            );
2085            return;
2086        };
2087
2088        if let Some(new_frontier) = frontiers.input_frontier {
2089            replica_collection.update_input_frontier(new_frontier.clone());
2090        }
2091        if let Some(new_frontier) = frontiers.output_frontier {
2092            replica_collection.update_output_frontier(new_frontier.clone());
2093        }
2094        if let Some(new_frontier) = frontiers.write_frontier {
2095            replica_collection.update_write_frontier(new_frontier.clone());
2096            self.maybe_update_global_write_frontier(id, new_frontier);
2097        }
2098    }
2099
2100    #[mz_ore::instrument(level = "debug")]
2101    fn handle_peek_response(
2102        &mut self,
2103        uuid: Uuid,
2104        response: PeekResponse,
2105        otel_ctx: OpenTelemetryContext,
2106        replica_id: ReplicaId,
2107    ) {
2108        otel_ctx.attach_as_parent();
2109
2110        // We might not be tracking this peek anymore, because we have served a response already or
2111        // because it was canceled. If this is the case, we ignore the response.
2112        let Some(peek) = self.peeks.get(&uuid) else {
2113            return;
2114        };
2115
2116        // If the peek is targeting a replica, ignore responses from other replicas.
2117        let target_replica = peek.target_replica.unwrap_or(replica_id);
2118        if target_replica != replica_id {
2119            return;
2120        }
2121
2122        let duration = peek.requested_at.elapsed();
2123        self.metrics.observe_peek_response(&response, duration);
2124
2125        let notification = PeekNotification::new(&response, peek.offset, peek.limit);
2126        // NOTE: We use the `otel_ctx` from the response, not the pending peek, because we
2127        // currently want the parent to be whatever the compute worker did with this peek.
2128        self.deliver_response(ComputeControllerResponse::PeekNotification(
2129            uuid,
2130            notification,
2131            otel_ctx,
2132        ));
2133
2134        self.finish_peek(uuid, response)
2135    }
2136
2137    fn handle_copy_to_response(
2138        &mut self,
2139        sink_id: GlobalId,
2140        response: CopyToResponse,
2141        replica_id: ReplicaId,
2142    ) {
2143        if !self.collections.contains_key(&sink_id) {
2144            soft_panic_or_log!(
2145                "received response for an unknown copy-to \
2146                 (sink_id={sink_id}, replica_id={replica_id})",
2147            );
2148            return;
2149        }
2150        let Some(replica) = self.replicas.get_mut(&replica_id) else {
2151            soft_panic_or_log!("copy-to response for an unknown replica (replica_id={replica_id})");
2152            return;
2153        };
2154        let Some(replica_collection) = replica.collections.get_mut(&sink_id) else {
2155            soft_panic_or_log!(
2156                "copy-to response for an unknown replica collection \
2157                 (sink_id={sink_id}, replica_id={replica_id})"
2158            );
2159            return;
2160        };
2161
2162        // Downgrade the replica frontiers, to enable dropping of input read holds and clean up of
2163        // collection state.
2164        // TODO(database-issues#4701): report copy-to frontiers through `Frontiers` responses
2165        replica_collection.update_write_frontier(Antichain::new());
2166        replica_collection.update_input_frontier(Antichain::new());
2167        replica_collection.update_output_frontier(Antichain::new());
2168
2169        // We might not be tracking this COPY TO because we have already returned a response
2170        // from one of the replicas. In that case, we ignore the response.
2171        if !self.copy_tos.remove(&sink_id) {
2172            return;
2173        }
2174
2175        let result = match response {
2176            CopyToResponse::RowCount(count) => Ok(count),
2177            CopyToResponse::Error(error) => Err(anyhow::anyhow!(error)),
2178            // We should never get here: Replicas only drop copy to collections in response
2179            // to the controller allowing them to do so, and when the controller drops a
2180            // copy to it also removes it from the list of tracked copy_tos (see
2181            // [`Instance::drop_collections`]).
2182            CopyToResponse::Dropped => {
2183                tracing::error!(
2184                    %sink_id, %replica_id,
2185                    "received `Dropped` response for a tracked copy to",
2186                );
2187                return;
2188            }
2189        };
2190
2191        self.deliver_response(ComputeControllerResponse::CopyToResponse(sink_id, result));
2192    }
2193
2194    fn handle_subscribe_response(
2195        &mut self,
2196        subscribe_id: GlobalId,
2197        response: SubscribeResponse,
2198        replica_id: ReplicaId,
2199    ) {
2200        if !self.collections.contains_key(&subscribe_id) {
2201            soft_panic_or_log!(
2202                "received response for an unknown subscribe \
2203                 (subscribe_id={subscribe_id}, replica_id={replica_id})",
2204            );
2205            return;
2206        }
2207        let Some(replica) = self.replicas.get_mut(&replica_id) else {
2208            soft_panic_or_log!(
2209                "subscribe response for an unknown replica (replica_id={replica_id})"
2210            );
2211            return;
2212        };
2213        let Some(replica_collection) = replica.collections.get_mut(&subscribe_id) else {
2214            soft_panic_or_log!(
2215                "subscribe response for an unknown replica collection \
2216                 (subscribe_id={subscribe_id}, replica_id={replica_id})"
2217            );
2218            return;
2219        };
2220
2221        // Always apply replica write frontier updates. Even if the subscribe is not tracked
2222        // anymore, there might still be replicas reading from its inputs, so we need to track the
2223        // frontiers until all replicas have advanced to the empty one.
2224        let write_frontier = match &response {
2225            SubscribeResponse::Batch(batch) => batch.upper.clone(),
2226            SubscribeResponse::DroppedAt(_) => Antichain::new(),
2227        };
2228
2229        // For subscribes we downgrade all replica frontiers based on write frontiers. This should
2230        // be fine because the input and output frontier of a subscribe track its write frontier.
2231        // TODO(database-issues#4701): report subscribe frontiers through `Frontiers` responses
2232        replica_collection.update_write_frontier(write_frontier.clone());
2233        replica_collection.update_input_frontier(write_frontier.clone());
2234        replica_collection.update_output_frontier(write_frontier.clone());
2235
2236        // If the subscribe is not tracked, or targets a different replica, there is nothing to do.
2237        let Some(mut subscribe) = self.subscribes.get(&subscribe_id).cloned() else {
2238            return;
2239        };
2240
2241        // Apply a global frontier update.
2242        // If this is a replica-targeted subscribe, it is important that we advance the global
2243        // frontier only based on responses from the targeted replica. Otherwise, another replica
2244        // could advance to the empty frontier, making us drop the subscribe on the targeted
2245        // replica prematurely.
2246        self.maybe_update_global_write_frontier(subscribe_id, write_frontier);
2247
2248        match response {
2249            SubscribeResponse::Batch(batch) => {
2250                let upper = batch.upper;
2251                let mut updates = batch.updates;
2252
2253                // If this batch advances the subscribe's frontier, we emit all updates at times
2254                // greater or equal to the last frontier (to avoid emitting duplicate updates).
2255                if PartialOrder::less_than(&subscribe.frontier, &upper) {
2256                    let lower = std::mem::replace(&mut subscribe.frontier, upper.clone());
2257
2258                    if upper.is_empty() {
2259                        // This subscribe cannot produce more data. Stop tracking it.
2260                        self.subscribes.remove(&subscribe_id);
2261                    } else {
2262                        // This subscribe can produce more data. Update our tracking of it.
2263                        self.subscribes.insert(subscribe_id, subscribe);
2264                    }
2265
2266                    if let Ok(updates) = updates.as_mut() {
2267                        updates.retain_mut(|updates| {
2268                            let offset = updates.times().partition_point(|t| {
2269                                // True for times that are strictly less than lower (and should be skipped)
2270                                // and false otherwise.
2271                                !lower.less_equal(t)
2272                            });
2273                            let (_, past_lower) = std::mem::take(updates).split_at(offset);
2274                            *updates = past_lower;
2275                            updates.len() > 0
2276                        });
2277                    }
2278                    self.deliver_response(ComputeControllerResponse::SubscribeResponse(
2279                        subscribe_id,
2280                        SubscribeBatch {
2281                            lower,
2282                            upper,
2283                            updates,
2284                        },
2285                    ));
2286                }
2287            }
2288            SubscribeResponse::DroppedAt(frontier) => {
2289                // We should never get here: Replicas only drop subscribe collections in response
2290                // to the controller allowing them to do so, and when the controller drops a
2291                // subscribe it also removes it from the list of tracked subscribes (see
2292                // [`Instance::drop_collections`]).
2293                tracing::error!(
2294                    %subscribe_id,
2295                    %replica_id,
2296                    frontier = ?frontier.elements(),
2297                    "received `DroppedAt` response for a tracked subscribe",
2298                );
2299                self.subscribes.remove(&subscribe_id);
2300            }
2301        }
2302    }
2303
2304    fn handle_status_response(&self, response: StatusResponse, _replica_id: ReplicaId) {
2305        match response {
2306            StatusResponse::Placeholder => {}
2307        }
2308    }
2309
2310    /// Return the write frontiers of the dependencies of the given collection.
2311    fn dependency_write_frontiers<'b>(
2312        &'b self,
2313        collection: &'b CollectionState,
2314    ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2315        let compute_frontiers = collection.compute_dependency_ids().filter_map(|dep_id| {
2316            let collection = self.collections.get(&dep_id);
2317            collection.map(|c| c.write_frontier())
2318        });
2319        let storage_frontiers = collection.storage_dependency_ids().filter_map(|dep_id| {
2320            let frontiers = self.storage_collections.collection_frontiers(dep_id).ok();
2321            frontiers.map(|f| f.write_frontier)
2322        });
2323
2324        compute_frontiers.chain(storage_frontiers)
2325    }
2326
2327    /// Return the write frontiers of transitive storage dependencies of the given collection.
2328    fn transitive_storage_dependency_write_frontiers<'b>(
2329        &'b self,
2330        collection: &'b CollectionState,
2331    ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2332        let mut storage_ids: BTreeSet<_> = collection.storage_dependency_ids().collect();
2333        let mut todo: Vec<_> = collection.compute_dependency_ids().collect();
2334        let mut done = BTreeSet::new();
2335
2336        while let Some(id) = todo.pop() {
2337            if done.contains(&id) {
2338                continue;
2339            }
2340            if let Some(dep) = self.collections.get(&id) {
2341                storage_ids.extend(dep.storage_dependency_ids());
2342                todo.extend(dep.compute_dependency_ids())
2343            }
2344            done.insert(id);
2345        }
2346
2347        let storage_frontiers = storage_ids.into_iter().filter_map(|id| {
2348            let frontiers = self.storage_collections.collection_frontiers(id).ok();
2349            frontiers.map(|f| f.write_frontier)
2350        });
2351
2352        storage_frontiers
2353    }
2354
2355    /// Downgrade the warmup capabilities of collections as much as possible.
2356    ///
2357    /// The only requirement we have for a collection's warmup capability is that it is for a time
2358    /// that is available in all of the collection's inputs. For each input the latest time that is
2359    /// the case for is `write_frontier - 1`. So the farthest we can downgrade a collection's
2360    /// warmup capability is the minimum of `write_frontier - 1` of all its inputs.
2361    ///
2362    /// This method expects to be periodically called as part of instance maintenance work.
2363    /// We would like to instead update the warmup capabilities synchronously in response to
2364    /// frontier updates of dependency collections, but that is not generally possible because we
2365    /// don't learn about frontier updates of storage collections synchronously. We could do
2366    /// synchronous updates for compute dependencies, but we refrain from doing for simplicity.
2367    fn downgrade_warmup_capabilities(&mut self) {
2368        let mut new_capabilities = BTreeMap::new();
2369        for (id, collection) in &self.collections {
2370            // For write-only collections that have advanced to the empty frontier, we can drop the
2371            // warmup capability entirely. There is no reason why we would need to hydrate those
2372            // collections again, so being able to warm them up is not useful.
2373            if collection.read_policy.is_none()
2374                && collection.shared.lock_write_frontier(|f| f.is_empty())
2375            {
2376                new_capabilities.insert(*id, Antichain::new());
2377                continue;
2378            }
2379
2380            let mut new_capability = Antichain::new();
2381            for frontier in self.dependency_write_frontiers(collection) {
2382                for time in frontier {
2383                    new_capability.insert(time.step_back().unwrap_or(time));
2384                }
2385            }
2386
2387            new_capabilities.insert(*id, new_capability);
2388        }
2389
2390        for (id, new_capability) in new_capabilities {
2391            let collection = self.expect_collection_mut(id);
2392            let _ = collection.warmup_read_hold.try_downgrade(new_capability);
2393        }
2394    }
2395
2396    /// Forward the implied capabilities of collections, if possible.
2397    ///
2398    /// The implied capability of a collection controls (a) which times are still readable (for
2399    /// indexes) and (b) with which as-of the collection gets installed on a new replica. We are
2400    /// usually not allowed to advance an implied capability beyond the frontier that follows from
2401    /// the collection's read policy applied to its write frontier:
2402    ///
2403    ///  * For sink collections, some external consumer might rely on seeing all distinct times in
2404    ///    the input reflected in the output. If we'd forward the implied capability of a sink,
2405    ///    we'd risk skipping times in the output across replica restarts.
2406    ///  * For index collections, we might make the index unreadable by advancing its read frontier
2407    ///    beyond its write frontier.
2408    ///
2409    /// There is one case where forwarding an implied capability is fine though: an index installed
2410    /// on a cluster that has no replicas. Such indexes are not readable anyway until a new replica
2411    /// is added, so advancing its read frontier can't make it unreadable. We can thus advance the
2412    /// implied capability as long as we make sure that when a new replica is added, the expected
2413    /// relationship between write frontier, read policy, and implied capability can be restored
2414    /// immediately (modulo computation time).
2415    ///
2416    /// Forwarding implied capabilities is not necessary for the correct functioning of the
2417    /// controller but an optimization that is beneficial in two ways:
2418    ///
2419    ///  * It relaxes read holds on inputs to forwarded collections, allowing their compaction.
2420    ///  * It reduces the amount of historical detail new replicas need to process when computing
2421    ///    forwarded collections, as forwarding the implied capability also forwards the corresponding
2422    ///    dataflow as-of.
2423    fn forward_implied_capabilities(&mut self) {
2424        if !ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE.get(&self.dyncfg) {
2425            return;
2426        }
2427        if !self.replicas.is_empty() {
2428            return;
2429        }
2430
2431        let mut new_capabilities = BTreeMap::new();
2432        for (id, collection) in &self.collections {
2433            let Some(read_policy) = &collection.read_policy else {
2434                // Collection is write-only, i.e. a sink.
2435                continue;
2436            };
2437
2438            // When a new replica is started, it will immediately be able to compute all collection
2439            // output up to the write frontier of its transitive storage inputs. So the new implied
2440            // read capability should be the read policy applied to that frontier.
2441            let mut dep_frontier = Antichain::new();
2442            for frontier in self.transitive_storage_dependency_write_frontiers(collection) {
2443                dep_frontier.extend(frontier);
2444            }
2445
2446            let new_capability = read_policy.frontier(dep_frontier.borrow());
2447            if PartialOrder::less_than(collection.implied_read_hold.since(), &new_capability) {
2448                new_capabilities.insert(*id, new_capability);
2449            }
2450        }
2451
2452        for (id, new_capability) in new_capabilities {
2453            let collection = self.expect_collection_mut(id);
2454            let _ = collection.implied_read_hold.try_downgrade(new_capability);
2455        }
2456    }
2457
2458    /// Acquires a `ReadHold` for the identified compute collection.
2459    ///
2460    /// This mirrors the logic used by the controller-side `InstanceState::acquire_read_hold`,
2461    /// but executes on the instance task itself.
2462    pub(super) fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
2463        // Similarly to InstanceState::acquire_read_hold and StorageCollections::acquire_read_holds,
2464        // we acquire read holds at the earliest possible time rather than returning a copy
2465        // of the implied read hold. This is so that dependents can acquire read holds on
2466        // compute dependencies at frontiers that are held back by other read holds the caller
2467        // has previously taken.
2468        let collection = self.collection(id)?;
2469        let since = collection.shared.lock_read_capabilities(|caps| {
2470            let since = caps.frontier().to_owned();
2471            caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
2472            since
2473        });
2474        let hold = ReadHold::new(id, since, Arc::clone(&self.read_hold_tx));
2475        Ok(hold)
2476    }
2477
2478    /// Process pending maintenance work.
2479    ///
2480    /// This method is invoked periodically by the global controller.
2481    /// It is a good place to perform maintenance work that arises from various controller state
2482    /// changes and that cannot conveniently be handled synchronously with those state changes.
2483    #[mz_ore::instrument(level = "debug")]
2484    pub fn maintain(&mut self) {
2485        self.rehydrate_failed_replicas();
2486        self.downgrade_warmup_capabilities();
2487        self.forward_implied_capabilities();
2488        self.schedule_collections();
2489        self.cleanup_collections();
2490        self.update_frontier_introspection();
2491        self.refresh_state_metrics();
2492        self.refresh_wallclock_lag();
2493    }
2494}
2495
2496/// State maintained about individual compute collections.
2497///
2498/// A compute collection is either an index, or a storage sink, or a subscribe, exported by a
2499/// compute dataflow.
2500#[derive(Debug)]
2501struct CollectionState {
2502    /// If set, this collection is only maintained by the specified replica.
2503    target_replica: Option<ReplicaId>,
2504    /// Whether this collection is a log collection.
2505    ///
2506    /// Log collections are special in that they are only maintained by a subset of all replicas.
2507    log_collection: bool,
2508    /// Whether this collection has been dropped by a controller client.
2509    ///
2510    /// The controller is allowed to remove the `CollectionState` for a collection only when
2511    /// `dropped == true`. Otherwise, clients might still expect to be able to query information
2512    /// about this collection.
2513    dropped: bool,
2514    /// Whether this collection has been scheduled, i.e., the controller has sent a `Schedule`
2515    /// command for it.
2516    scheduled: bool,
2517
2518    /// Whether this collection is in read-only mode.
2519    ///
2520    /// When in read-only mode, the dataflow is not allowed to affect external state (largely persist).
2521    read_only: bool,
2522
2523    /// State shared with the `ComputeController`.
2524    shared: SharedCollectionState,
2525
2526    /// A read hold maintaining the implicit capability of the collection.
2527    ///
2528    /// This capability is kept to ensure that the collection remains readable according to its
2529    /// `read_policy`. It also ensures that read holds on the collection's dependencies are kept at
2530    /// some time not greater than the collection's `write_frontier`, guaranteeing that the
2531    /// collection's next outputs can always be computed without skipping times.
2532    implied_read_hold: ReadHold,
2533    /// A read hold held to enable dataflow warmup.
2534    ///
2535    /// Dataflow warmup is an optimization that allows dataflows to immediately start hydrating
2536    /// even when their next output time (as implied by the `write_frontier`) is in the future.
2537    /// By installing a read capability derived from the write frontiers of the collection's
2538    /// inputs, we ensure that the as-of of new dataflows installed for the collection is at a time
2539    /// that is immediately available, so hydration can begin immediately too.
2540    warmup_read_hold: ReadHold,
2541    /// The policy to use to downgrade `self.implied_read_hold`.
2542    ///
2543    /// If `None`, the collection is a write-only collection (i.e. a sink). For write-only
2544    /// collections, the `implied_read_hold` is only required for maintaining read holds on the
2545    /// inputs, so we can immediately downgrade it to the `write_frontier`.
2546    read_policy: Option<ReadPolicy>,
2547
2548    /// Storage identifiers on which this collection depends, and read holds this collection
2549    /// requires on them.
2550    storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2551    /// Compute identifiers on which this collection depends, and read holds this collection
2552    /// requires on them.
2553    compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2554
2555    /// Introspection state associated with this collection.
2556    introspection: CollectionIntrospection,
2557
2558    /// Frontier wallclock lag measurements stashed until the next `WallclockLagHistogram`
2559    /// introspection update.
2560    ///
2561    /// Keys are `(period, lag, labels)` triples, values are counts.
2562    ///
2563    /// If this is `None`, wallclock lag is not tracked for this collection.
2564    wallclock_lag_histogram_stash: Option<
2565        BTreeMap<
2566            (
2567                WallclockLagHistogramPeriod,
2568                WallclockLag,
2569                BTreeMap<&'static str, String>,
2570            ),
2571            Diff,
2572        >,
2573    >,
2574}
2575
2576impl CollectionState {
2577    /// Creates a new collection state, with an initial read policy valid from `since`.
2578    fn new(
2579        collection_id: GlobalId,
2580        as_of: Antichain<Timestamp>,
2581        shared: SharedCollectionState,
2582        storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2583        compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2584        read_hold_tx: read_holds::ChangeTx,
2585        introspection: CollectionIntrospection,
2586    ) -> Self {
2587        // A collection is not readable before the `as_of`.
2588        let since = as_of.clone();
2589        // A collection won't produce updates for times before the `as_of`.
2590        let upper = as_of;
2591
2592        // Ensure that the provided `shared` is valid for the given `as_of`.
2593        assert!(shared.lock_read_capabilities(|c| c.frontier() == since.borrow()));
2594        assert!(shared.lock_write_frontier(|f| f == &upper));
2595
2596        // Initialize collection read holds.
2597        // Note that the implied read hold was already added to the `read_capabilities` when
2598        // `shared` was created, so we only need to add the warmup read hold here.
2599        let implied_read_hold =
2600            ReadHold::new(collection_id, since.clone(), Arc::clone(&read_hold_tx));
2601        let warmup_read_hold = ReadHold::new(collection_id, since.clone(), read_hold_tx);
2602
2603        let updates = warmup_read_hold.since().iter().map(|t| (t.clone(), 1));
2604        shared.lock_read_capabilities(|c| {
2605            c.update_iter(updates);
2606        });
2607
2608        // In an effort to keep the produced wallclock lag introspection data small and
2609        // predictable, we disable wallclock lag tracking for transient collections, i.e. slow-path
2610        // select indexes and subscribes.
2611        let wallclock_lag_histogram_stash = match collection_id.is_transient() {
2612            true => None,
2613            false => Some(Default::default()),
2614        };
2615
2616        Self {
2617            target_replica: None,
2618            log_collection: false,
2619            dropped: false,
2620            scheduled: false,
2621            read_only: true,
2622            shared,
2623            implied_read_hold,
2624            warmup_read_hold,
2625            read_policy: Some(ReadPolicy::ValidFrom(since)),
2626            storage_dependencies,
2627            compute_dependencies,
2628            introspection,
2629            wallclock_lag_histogram_stash,
2630        }
2631    }
2632
2633    /// Creates a new collection state for a log collection.
2634    fn new_log_collection(
2635        id: GlobalId,
2636        shared: SharedCollectionState,
2637        read_hold_tx: read_holds::ChangeTx,
2638        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2639    ) -> Self {
2640        let since = Antichain::from_elem(Timestamp::MIN);
2641        let introspection = CollectionIntrospection::new(
2642            id,
2643            introspection_tx,
2644            since.clone(),
2645            false,
2646            None,
2647            None,
2648            Vec::new(),
2649        );
2650        let mut state = Self::new(
2651            id,
2652            since,
2653            shared,
2654            Default::default(),
2655            Default::default(),
2656            read_hold_tx,
2657            introspection,
2658        );
2659        state.log_collection = true;
2660        // Log collections are created and scheduled implicitly as part of replica initialization.
2661        state.scheduled = true;
2662        state
2663    }
2664
2665    /// Reports the current read frontier.
2666    fn read_frontier(&self) -> Antichain<Timestamp> {
2667        self.shared
2668            .lock_read_capabilities(|c| c.frontier().to_owned())
2669    }
2670
2671    /// Reports the current write frontier.
2672    fn write_frontier(&self) -> Antichain<Timestamp> {
2673        self.shared.lock_write_frontier(|f| f.clone())
2674    }
2675
2676    fn storage_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2677        self.storage_dependencies.keys().copied()
2678    }
2679
2680    fn compute_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2681        self.compute_dependencies.keys().copied()
2682    }
2683}
2684
2685/// Collection state shared with the `ComputeController`.
2686///
2687/// Having this allows certain controller APIs, such as `ComputeController::collection_frontiers`
2688/// and `ComputeController::acquire_read_hold` to be non-`async`. This comes at the cost of
2689/// complexity (by introducing shared mutable state) and performance (by introducing locking). We
2690/// should aim to reduce the amount of shared state over time, rather than expand it.
2691///
2692/// Note that [`SharedCollectionState`]s are initialized by the `ComputeController` prior to the
2693/// collection's creation in the [`Instance`]. This is to allow compute clients to query frontiers
2694/// and take new read holds immediately, without having to wait for the [`Instance`] to update.
2695#[derive(Clone, Debug)]
2696pub(super) struct SharedCollectionState {
2697    /// Accumulation of read capabilities for the collection.
2698    ///
2699    /// This accumulation contains the capabilities held by all [`ReadHold`]s given out for the
2700    /// collection, including `implied_read_hold` and `warmup_read_hold`.
2701    ///
2702    /// NOTE: This field may only be modified by [`Instance::apply_read_hold_change`],
2703    /// [`Instance::acquire_read_hold`], and `ComputeController::acquire_read_hold`.
2704    /// Nobody else should modify read capabilities directly. Instead, collection users should
2705    /// manage read holds through [`ReadHold`] objects acquired through
2706    /// `ComputeController::acquire_read_hold`.
2707    ///
2708    /// TODO(teskje): Restructure the code to enforce the above in the type system.
2709    read_capabilities: Arc<Mutex<MutableAntichain<Timestamp>>>,
2710    /// The write frontier of this collection.
2711    write_frontier: Arc<Mutex<Antichain<Timestamp>>>,
2712}
2713
2714impl SharedCollectionState {
2715    pub fn new(as_of: Antichain<Timestamp>) -> Self {
2716        // A collection is not readable before the `as_of`.
2717        let since = as_of.clone();
2718        // A collection won't produce updates for times before the `as_of`.
2719        let upper = as_of;
2720
2721        // Initialize read capabilities to the `since`.
2722        // The is the implied read capability. The corresponding [`ReadHold`] is created in
2723        // [`CollectionState::new`].
2724        let mut read_capabilities = MutableAntichain::new();
2725        read_capabilities.update_iter(since.iter().map(|time| (time.clone(), 1)));
2726
2727        Self {
2728            read_capabilities: Arc::new(Mutex::new(read_capabilities)),
2729            write_frontier: Arc::new(Mutex::new(upper)),
2730        }
2731    }
2732
2733    pub fn lock_read_capabilities<F, R>(&self, f: F) -> R
2734    where
2735        F: FnOnce(&mut MutableAntichain<Timestamp>) -> R,
2736    {
2737        let mut caps = self.read_capabilities.lock().expect("poisoned");
2738        f(&mut *caps)
2739    }
2740
2741    pub fn lock_write_frontier<F, R>(&self, f: F) -> R
2742    where
2743        F: FnOnce(&mut Antichain<Timestamp>) -> R,
2744    {
2745        let mut frontier = self.write_frontier.lock().expect("poisoned");
2746        f(&mut *frontier)
2747    }
2748}
2749
2750/// Manages certain introspection relations associated with a collection. Upon creation, it adds
2751/// rows to introspection relations. When dropped, it retracts its managed rows.
2752#[derive(Debug)]
2753struct CollectionIntrospection {
2754    /// The ID of the compute collection.
2755    collection_id: GlobalId,
2756    /// A channel through which introspection updates are delivered.
2757    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2758    /// Introspection state for `IntrospectionType::Frontiers`.
2759    ///
2760    /// `Some` if the collection does _not_ sink into a storage collection (i.e. is not an MV). If
2761    /// the collection sinks into storage, the storage controller reports its frontiers instead.
2762    frontiers: Option<FrontiersIntrospectionState>,
2763    /// Introspection state for `IntrospectionType::ComputeMaterializedViewRefreshes`.
2764    ///
2765    /// `Some` if the collection is a REFRESH MV.
2766    refresh: Option<RefreshIntrospectionState>,
2767    /// The IDs of the collection's dependencies, for `IntrospectionType::ComputeDependencies`.
2768    dependency_ids: Vec<GlobalId>,
2769}
2770
2771impl CollectionIntrospection {
2772    fn new(
2773        collection_id: GlobalId,
2774        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2775        as_of: Antichain<Timestamp>,
2776        storage_sink: bool,
2777        initial_as_of: Option<Antichain<Timestamp>>,
2778        refresh_schedule: Option<RefreshSchedule>,
2779        dependency_ids: Vec<GlobalId>,
2780    ) -> Self {
2781        let refresh =
2782            match (refresh_schedule, initial_as_of) {
2783                (Some(refresh_schedule), Some(initial_as_of)) => Some(
2784                    RefreshIntrospectionState::new(refresh_schedule, initial_as_of, &as_of),
2785                ),
2786                (refresh_schedule, _) => {
2787                    // If we have a `refresh_schedule`, then the collection is a MV, so we should also have
2788                    // an `initial_as_of`.
2789                    soft_assert_or_log!(
2790                        refresh_schedule.is_none(),
2791                        "`refresh_schedule` without an `initial_as_of`: {collection_id}"
2792                    );
2793                    None
2794                }
2795            };
2796        let frontiers = (!storage_sink).then(|| FrontiersIntrospectionState::new(as_of));
2797
2798        let self_ = Self {
2799            collection_id,
2800            introspection_tx,
2801            frontiers,
2802            refresh,
2803            dependency_ids,
2804        };
2805
2806        self_.report_initial_state();
2807        self_
2808    }
2809
2810    /// Reports the initial introspection state.
2811    fn report_initial_state(&self) {
2812        if let Some(frontiers) = &self.frontiers {
2813            let row = frontiers.row_for_collection(self.collection_id);
2814            let updates = vec![(row, Diff::ONE)];
2815            self.send(IntrospectionType::Frontiers, updates);
2816        }
2817
2818        if let Some(refresh) = &self.refresh {
2819            let row = refresh.row_for_collection(self.collection_id);
2820            let updates = vec![(row, Diff::ONE)];
2821            self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2822        }
2823
2824        if !self.dependency_ids.is_empty() {
2825            let updates = self.dependency_rows(Diff::ONE);
2826            self.send(IntrospectionType::ComputeDependencies, updates);
2827        }
2828    }
2829
2830    /// Produces rows for the `ComputeDependencies` introspection relation.
2831    fn dependency_rows(&self, diff: Diff) -> Vec<(Row, Diff)> {
2832        self.dependency_ids
2833            .iter()
2834            .map(|dependency_id| {
2835                let row = Row::pack_slice(&[
2836                    Datum::String(&self.collection_id.to_string()),
2837                    Datum::String(&dependency_id.to_string()),
2838                ]);
2839                (row, diff)
2840            })
2841            .collect()
2842    }
2843
2844    /// Observe the given current collection frontiers and update the introspection state as
2845    /// necessary.
2846    fn observe_frontiers(
2847        &mut self,
2848        read_frontier: &Antichain<Timestamp>,
2849        write_frontier: &Antichain<Timestamp>,
2850    ) {
2851        self.update_frontier_introspection(read_frontier, write_frontier);
2852        self.update_refresh_introspection(write_frontier);
2853    }
2854
2855    fn update_frontier_introspection(
2856        &mut self,
2857        read_frontier: &Antichain<Timestamp>,
2858        write_frontier: &Antichain<Timestamp>,
2859    ) {
2860        let Some(frontiers) = &mut self.frontiers else {
2861            return;
2862        };
2863
2864        if &frontiers.read_frontier == read_frontier && &frontiers.write_frontier == write_frontier
2865        {
2866            return; // no change
2867        };
2868
2869        let retraction = frontiers.row_for_collection(self.collection_id);
2870        frontiers.update(read_frontier, write_frontier);
2871        let insertion = frontiers.row_for_collection(self.collection_id);
2872        let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2873        self.send(IntrospectionType::Frontiers, updates);
2874    }
2875
2876    fn update_refresh_introspection(&mut self, write_frontier: &Antichain<Timestamp>) {
2877        let Some(refresh) = &mut self.refresh else {
2878            return;
2879        };
2880
2881        let retraction = refresh.row_for_collection(self.collection_id);
2882        refresh.frontier_update(write_frontier);
2883        let insertion = refresh.row_for_collection(self.collection_id);
2884
2885        if retraction == insertion {
2886            return; // no change
2887        }
2888
2889        let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2890        self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2891    }
2892
2893    fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
2894        // Failure to send means the `ComputeController` has been dropped and doesn't care about
2895        // introspection updates anymore.
2896        let _ = self.introspection_tx.send((introspection_type, updates));
2897    }
2898}
2899
2900impl Drop for CollectionIntrospection {
2901    fn drop(&mut self) {
2902        // Retract collection frontiers.
2903        if let Some(frontiers) = &self.frontiers {
2904            let row = frontiers.row_for_collection(self.collection_id);
2905            let updates = vec![(row, Diff::MINUS_ONE)];
2906            self.send(IntrospectionType::Frontiers, updates);
2907        }
2908
2909        // Retract MV refresh state.
2910        if let Some(refresh) = &self.refresh {
2911            let retraction = refresh.row_for_collection(self.collection_id);
2912            let updates = vec![(retraction, Diff::MINUS_ONE)];
2913            self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2914        }
2915
2916        // Retract collection dependencies.
2917        if !self.dependency_ids.is_empty() {
2918            let updates = self.dependency_rows(Diff::MINUS_ONE);
2919            self.send(IntrospectionType::ComputeDependencies, updates);
2920        }
2921    }
2922}
2923
2924#[derive(Debug)]
2925struct FrontiersIntrospectionState {
2926    read_frontier: Antichain<Timestamp>,
2927    write_frontier: Antichain<Timestamp>,
2928}
2929
2930impl FrontiersIntrospectionState {
2931    fn new(as_of: Antichain<Timestamp>) -> Self {
2932        Self {
2933            read_frontier: as_of.clone(),
2934            write_frontier: as_of,
2935        }
2936    }
2937
2938    /// Return a `Row` reflecting the current collection frontiers.
2939    fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2940        let read_frontier = self
2941            .read_frontier
2942            .as_option()
2943            .map_or(Datum::Null, |ts| ts.clone().into());
2944        let write_frontier = self
2945            .write_frontier
2946            .as_option()
2947            .map_or(Datum::Null, |ts| ts.clone().into());
2948        Row::pack_slice(&[
2949            Datum::String(&collection_id.to_string()),
2950            read_frontier,
2951            write_frontier,
2952        ])
2953    }
2954
2955    /// Update the introspection state with the given new frontiers.
2956    fn update(
2957        &mut self,
2958        read_frontier: &Antichain<Timestamp>,
2959        write_frontier: &Antichain<Timestamp>,
2960    ) {
2961        if read_frontier != &self.read_frontier {
2962            self.read_frontier.clone_from(read_frontier);
2963        }
2964        if write_frontier != &self.write_frontier {
2965            self.write_frontier.clone_from(write_frontier);
2966        }
2967    }
2968}
2969
2970/// Information needed to compute introspection updates for a REFRESH materialized view when the
2971/// write frontier advances.
2972#[derive(Debug)]
2973struct RefreshIntrospectionState {
2974    // Immutable properties of the MV
2975    refresh_schedule: RefreshSchedule,
2976    initial_as_of: Antichain<Timestamp>,
2977    // Refresh state
2978    next_refresh: Datum<'static>,           // Null or an MzTimestamp
2979    last_completed_refresh: Datum<'static>, // Null or an MzTimestamp
2980}
2981
2982impl RefreshIntrospectionState {
2983    /// Return a `Row` reflecting the current refresh introspection state.
2984    fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2985        Row::pack_slice(&[
2986            Datum::String(&collection_id.to_string()),
2987            self.last_completed_refresh,
2988            self.next_refresh,
2989        ])
2990    }
2991}
2992
2993impl RefreshIntrospectionState {
2994    /// Construct a new [`RefreshIntrospectionState`], and apply an initial `frontier_update()` at
2995    /// the `upper`.
2996    fn new(
2997        refresh_schedule: RefreshSchedule,
2998        initial_as_of: Antichain<Timestamp>,
2999        upper: &Antichain<Timestamp>,
3000    ) -> Self {
3001        let mut self_ = Self {
3002            refresh_schedule: refresh_schedule.clone(),
3003            initial_as_of: initial_as_of.clone(),
3004            next_refresh: Datum::Null,
3005            last_completed_refresh: Datum::Null,
3006        };
3007        self_.frontier_update(upper);
3008        self_
3009    }
3010
3011    /// Should be called whenever the write frontier of the collection advances. It updates the
3012    /// state that should be recorded in introspection relations, but doesn't send the updates yet.
3013    fn frontier_update(&mut self, write_frontier: &Antichain<Timestamp>) {
3014        if write_frontier.is_empty() {
3015            self.last_completed_refresh =
3016                if let Some(last_refresh) = self.refresh_schedule.last_refresh() {
3017                    last_refresh.into()
3018                } else {
3019                    // If there is no last refresh, then we have a `REFRESH EVERY`, in which case
3020                    // the saturating roundup puts a refresh at the maximum possible timestamp.
3021                    Timestamp::MAX.into()
3022                };
3023            self.next_refresh = Datum::Null;
3024        } else {
3025            if PartialOrder::less_equal(write_frontier, &self.initial_as_of) {
3026                // We are before the first refresh.
3027                self.last_completed_refresh = Datum::Null;
3028                let initial_as_of = self.initial_as_of.as_option().expect(
3029                    "initial_as_of can't be [], because then there would be no refreshes at all",
3030                );
3031                let first_refresh = self
3032                    .refresh_schedule
3033                    .round_up_timestamp(*initial_as_of)
3034                    .expect("sequencing makes sure that REFRESH MVs always have a first refresh");
3035                soft_assert_or_log!(
3036                    first_refresh == *initial_as_of,
3037                    "initial_as_of should be set to the first refresh"
3038                );
3039                self.next_refresh = first_refresh.into();
3040            } else {
3041                // The first refresh has already happened.
3042                let write_frontier = write_frontier.as_option().expect("checked above");
3043                self.last_completed_refresh = self
3044                    .refresh_schedule
3045                    .round_down_timestamp_m1(*write_frontier)
3046                    .map_or_else(
3047                        || {
3048                            soft_panic_or_log!(
3049                                "rounding down should have returned the first refresh or later"
3050                            );
3051                            Datum::Null
3052                        },
3053                        |last_completed_refresh| last_completed_refresh.into(),
3054                    );
3055                self.next_refresh = write_frontier.clone().into();
3056            }
3057        }
3058    }
3059}
3060
3061/// A note of an outstanding peek response.
3062#[derive(Debug)]
3063struct PendingPeek {
3064    /// For replica-targeted peeks, this specifies the replica whose response we should pass on.
3065    ///
3066    /// If this value is `None`, we pass on the first response.
3067    target_replica: Option<ReplicaId>,
3068    /// The OpenTelemetry context for this peek.
3069    otel_ctx: OpenTelemetryContext,
3070    /// The time at which the peek was requested.
3071    ///
3072    /// Used to track peek durations.
3073    requested_at: Instant,
3074    /// The read hold installed to serve this peek.
3075    read_hold: ReadHold,
3076    /// The channel to send peek results.
3077    peek_response_tx: oneshot::Sender<PeekResponse>,
3078    /// An optional limit of the peek's result size.
3079    limit: Option<usize>,
3080    /// The offset into the peek's result.
3081    offset: usize,
3082}
3083
3084#[derive(Debug, Clone)]
3085struct ActiveSubscribe {
3086    /// Current upper frontier of this subscribe.
3087    frontier: Antichain<Timestamp>,
3088}
3089
3090impl Default for ActiveSubscribe {
3091    fn default() -> Self {
3092        Self {
3093            frontier: Antichain::from_elem(Timestamp::MIN),
3094        }
3095    }
3096}
3097
3098/// State maintained about individual replicas.
3099#[derive(Debug)]
3100struct ReplicaState {
3101    /// The ID of the replica.
3102    id: ReplicaId,
3103    /// Client for the running replica task.
3104    client: ReplicaClient,
3105    /// The replica configuration.
3106    config: ReplicaConfig,
3107    /// Replica metrics.
3108    metrics: ReplicaMetrics,
3109    /// A channel through which introspection updates are delivered.
3110    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3111    /// Per-replica collection state.
3112    collections: BTreeMap<GlobalId, ReplicaCollectionState>,
3113    /// The epoch of the replica.
3114    epoch: u64,
3115}
3116
3117impl ReplicaState {
3118    fn new(
3119        id: ReplicaId,
3120        client: ReplicaClient,
3121        config: ReplicaConfig,
3122        metrics: ReplicaMetrics,
3123        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3124        epoch: u64,
3125    ) -> Self {
3126        Self {
3127            id,
3128            client,
3129            config,
3130            metrics,
3131            introspection_tx,
3132            epoch,
3133            collections: Default::default(),
3134        }
3135    }
3136
3137    /// Add a collection to the replica state.
3138    ///
3139    /// # Panics
3140    ///
3141    /// Panics if a collection with the same ID exists already.
3142    fn add_collection(
3143        &mut self,
3144        id: GlobalId,
3145        as_of: Antichain<Timestamp>,
3146        input_read_holds: Vec<ReadHold>,
3147    ) {
3148        let metrics = self.metrics.for_collection(id);
3149        let introspection = ReplicaCollectionIntrospection::new(
3150            self.id,
3151            id,
3152            self.introspection_tx.clone(),
3153            as_of.clone(),
3154        );
3155        let mut state =
3156            ReplicaCollectionState::new(metrics, as_of, introspection, input_read_holds);
3157
3158        // In an effort to keep the produced wallclock lag introspection data small and
3159        // predictable, we disable wallclock lag tracking for transient collections, i.e. slow-path
3160        // select indexes and subscribes.
3161        if id.is_transient() {
3162            state.wallclock_lag_max = None;
3163        }
3164
3165        if let Some(previous) = self.collections.insert(id, state) {
3166            panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
3167        }
3168    }
3169
3170    /// Remove state for a collection.
3171    fn remove_collection(&mut self, id: GlobalId) -> Option<ReplicaCollectionState> {
3172        self.collections.remove(&id)
3173    }
3174
3175    /// Returns whether all replica frontiers of the given collection are empty.
3176    fn collection_frontiers_empty(&self, id: GlobalId) -> bool {
3177        self.collections.get(&id).map_or(true, |c| {
3178            c.write_frontier.is_empty()
3179                && c.input_frontier.is_empty()
3180                && c.output_frontier.is_empty()
3181        })
3182    }
3183
3184    /// Returns the state of the [`ReplicaState`] formatted as JSON.
3185    ///
3186    /// The returned value is not guaranteed to be stable and may change at any point in time.
3187    #[mz_ore::instrument(level = "debug")]
3188    pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
3189        // Note: We purposefully use the `Debug` formatting for the value of all fields in the
3190        // returned object as a tradeoff between usability and stability. `serde_json` will fail
3191        // to serialize an object if the keys aren't strings, so `Debug` formatting the values
3192        // prevents a future unrelated change from silently breaking this method.
3193
3194        // Destructure `self` here so we don't forget to consider dumping newly added fields.
3195        let Self {
3196            id,
3197            client: _,
3198            config: _,
3199            metrics: _,
3200            introspection_tx: _,
3201            epoch,
3202            collections,
3203        } = self;
3204
3205        let collections: BTreeMap<_, _> = collections
3206            .iter()
3207            .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
3208            .collect();
3209
3210        Ok(serde_json::json!({
3211            "id": id.to_string(),
3212            "collections": collections,
3213            "epoch": epoch,
3214        }))
3215    }
3216}
3217
3218#[derive(Debug)]
3219struct ReplicaCollectionState {
3220    /// The replica write frontier of this collection.
3221    ///
3222    /// See [`FrontiersResponse::write_frontier`].
3223    write_frontier: Antichain<Timestamp>,
3224    /// The replica input frontier of this collection.
3225    ///
3226    /// See [`FrontiersResponse::input_frontier`].
3227    input_frontier: Antichain<Timestamp>,
3228    /// The replica output frontier of this collection.
3229    ///
3230    /// See [`FrontiersResponse::output_frontier`].
3231    output_frontier: Antichain<Timestamp>,
3232
3233    /// Metrics tracked for this collection.
3234    ///
3235    /// If this is `None`, no metrics are collected.
3236    metrics: Option<ReplicaCollectionMetrics>,
3237    /// As-of frontier with which this collection was installed on the replica.
3238    as_of: Antichain<Timestamp>,
3239    /// Tracks introspection state for this collection.
3240    introspection: ReplicaCollectionIntrospection,
3241    /// Read holds on storage inputs to this collection.
3242    ///
3243    /// These read holds are kept to ensure that the replica is able to read from storage inputs at
3244    /// all times it hasn't read yet. We only need to install read holds for storage inputs since
3245    /// compaction of compute inputs is implicitly held back by Timely/DD.
3246    input_read_holds: Vec<ReadHold>,
3247
3248    /// Maximum frontier wallclock lag since the last `WallclockLagHistory` introspection update.
3249    ///
3250    /// If this is `None`, wallclock lag is not tracked for this collection.
3251    wallclock_lag_max: Option<WallclockLag>,
3252}
3253
3254impl ReplicaCollectionState {
3255    fn new(
3256        metrics: Option<ReplicaCollectionMetrics>,
3257        as_of: Antichain<Timestamp>,
3258        introspection: ReplicaCollectionIntrospection,
3259        input_read_holds: Vec<ReadHold>,
3260    ) -> Self {
3261        Self {
3262            write_frontier: as_of.clone(),
3263            input_frontier: as_of.clone(),
3264            output_frontier: as_of.clone(),
3265            metrics,
3266            as_of,
3267            introspection,
3268            input_read_holds,
3269            wallclock_lag_max: Some(WallclockLag::MIN),
3270        }
3271    }
3272
3273    /// Returns whether this collection is hydrated.
3274    fn hydrated(&self) -> bool {
3275        // If the observed frontier is greater than the collection's as-of, the collection has
3276        // produced some output and is therefore hydrated.
3277        //
3278        // We need to consider the edge case where the as-of is the empty frontier. Such an as-of
3279        // is not useful for indexes, because they wouldn't be readable. For write-only
3280        // collections, an empty as-of means that the collection has been fully written and no new
3281        // dataflow needs to be created for it. Consequently, no hydration will happen either.
3282        //
3283        // Based on this, we could respond in two ways:
3284        //  * `false`, as in "the dataflow was never created"
3285        //  * `true`, as in "the dataflow completed immediately"
3286        //
3287        // Since hydration is often used as a measure of dataflow progress and we don't want to
3288        // give the impression that certain dataflows are somehow stuck when they are not, we go
3289        // with the second interpretation here.
3290        self.as_of.is_empty() || PartialOrder::less_than(&self.as_of, &self.output_frontier)
3291    }
3292
3293    /// Updates the replica write frontier of this collection.
3294    fn update_write_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3295        if PartialOrder::less_than(&new_frontier, &self.write_frontier) {
3296            soft_panic_or_log!(
3297                "replica collection write frontier regression (old={:?}, new={new_frontier:?})",
3298                self.write_frontier,
3299            );
3300            return;
3301        } else if new_frontier == self.write_frontier {
3302            return;
3303        }
3304
3305        self.write_frontier = new_frontier;
3306    }
3307
3308    /// Updates the replica input frontier of this collection.
3309    fn update_input_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3310        if PartialOrder::less_than(&new_frontier, &self.input_frontier) {
3311            soft_panic_or_log!(
3312                "replica collection input frontier regression (old={:?}, new={new_frontier:?})",
3313                self.input_frontier,
3314            );
3315            return;
3316        } else if new_frontier == self.input_frontier {
3317            return;
3318        }
3319
3320        self.input_frontier = new_frontier;
3321
3322        // Relax our read holds on collection inputs.
3323        for read_hold in &mut self.input_read_holds {
3324            let result = read_hold.try_downgrade(self.input_frontier.clone());
3325            soft_assert_or_log!(
3326                result.is_ok(),
3327                "read hold downgrade failed (read_hold={read_hold:?}, new_since={:?})",
3328                self.input_frontier,
3329            );
3330        }
3331    }
3332
3333    /// Updates the replica output frontier of this collection.
3334    fn update_output_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3335        if PartialOrder::less_than(&new_frontier, &self.output_frontier) {
3336            soft_panic_or_log!(
3337                "replica collection output frontier regression (old={:?}, new={new_frontier:?})",
3338                self.output_frontier,
3339            );
3340            return;
3341        } else if new_frontier == self.output_frontier {
3342            return;
3343        }
3344
3345        self.output_frontier = new_frontier;
3346    }
3347}
3348
3349/// Maintains the introspection state for a given replica and collection, and ensures that reported
3350/// introspection data is retracted when the collection is dropped.
3351#[derive(Debug)]
3352struct ReplicaCollectionIntrospection {
3353    /// The ID of the replica.
3354    replica_id: ReplicaId,
3355    /// The ID of the compute collection.
3356    collection_id: GlobalId,
3357    /// The collection's reported replica write frontier.
3358    write_frontier: Antichain<Timestamp>,
3359    /// A channel through which introspection updates are delivered.
3360    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3361}
3362
3363impl ReplicaCollectionIntrospection {
3364    /// Create a new `HydrationState` and initialize introspection.
3365    fn new(
3366        replica_id: ReplicaId,
3367        collection_id: GlobalId,
3368        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3369        as_of: Antichain<Timestamp>,
3370    ) -> Self {
3371        let self_ = Self {
3372            replica_id,
3373            collection_id,
3374            write_frontier: as_of,
3375            introspection_tx,
3376        };
3377
3378        self_.report_initial_state();
3379        self_
3380    }
3381
3382    /// Reports the initial introspection state.
3383    fn report_initial_state(&self) {
3384        let row = self.write_frontier_row();
3385        let updates = vec![(row, Diff::ONE)];
3386        self.send(IntrospectionType::ReplicaFrontiers, updates);
3387    }
3388
3389    /// Observe the given current write frontier and update the introspection state as necessary.
3390    fn observe_frontier(&mut self, write_frontier: &Antichain<Timestamp>) {
3391        if self.write_frontier == *write_frontier {
3392            return; // no change
3393        }
3394
3395        let retraction = self.write_frontier_row();
3396        self.write_frontier.clone_from(write_frontier);
3397        let insertion = self.write_frontier_row();
3398
3399        let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
3400        self.send(IntrospectionType::ReplicaFrontiers, updates);
3401    }
3402
3403    /// Return a `Row` reflecting the current replica write frontier.
3404    fn write_frontier_row(&self) -> Row {
3405        let write_frontier = self
3406            .write_frontier
3407            .as_option()
3408            .map_or(Datum::Null, |ts| ts.clone().into());
3409        Row::pack_slice(&[
3410            Datum::String(&self.collection_id.to_string()),
3411            Datum::String(&self.replica_id.to_string()),
3412            write_frontier,
3413        ])
3414    }
3415
3416    fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
3417        // Failure to send means the `ComputeController` has been dropped and doesn't care about
3418        // introspection updates anymore.
3419        let _ = self.introspection_tx.send((introspection_type, updates));
3420    }
3421}
3422
3423impl Drop for ReplicaCollectionIntrospection {
3424    fn drop(&mut self) {
3425        // Retract the write frontier.
3426        let row = self.write_frontier_row();
3427        let updates = vec![(row, Diff::MINUS_ONE)];
3428        self.send(IntrospectionType::ReplicaFrontiers, updates);
3429    }
3430}
3431
3432#[cfg(test)]
3433mod tests {
3434    use std::collections::BTreeMap;
3435
3436    use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE};
3437    use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
3438    use mz_persist_types::PersistLocation;
3439
3440    use crate::protocol::command::{ComputeCommand, InstanceConfig};
3441
3442    use super::{Instance, ReplicaId};
3443
3444    fn create_instance_command() -> ComputeCommand {
3445        ComputeCommand::CreateInstance(Box::new(InstanceConfig {
3446            logging: Default::default(),
3447            expiration_offset: None,
3448            peek_stash_persist_location: PersistLocation::new_in_mem(),
3449            arrangement_dictionary_compression: false,
3450            initial_config: Default::default(),
3451        }))
3452    }
3453
3454    fn initial_config(cmd: &ComputeCommand) -> &ConfigUpdates {
3455        match cmd {
3456            ComputeCommand::CreateInstance(config) => &config.initial_config,
3457            other => panic!("expected CreateInstance, got {other:?}"),
3458        }
3459    }
3460
3461    /// `CreateInstance` is specialized with a full snapshot of the instance-wide dyncfg, so the
3462    /// replica seeds its worker config at create time rather than waiting for the first
3463    /// `UpdateConfiguration`. This is the regression guard for create-time setup observing dyncfg
3464    /// defaults.
3465    #[mz_ore::test]
3466    fn create_instance_snapshots_instance_wide_dyncfg() {
3467        let dyncfg = ConfigSet::default()
3468            .add(&ENABLE_COLUMN_PAGED_BATCHER)
3469            .add(&ENABLE_MZ_JOIN_CORE);
3470        let mut updates = ConfigUpdates::default();
3471        updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3472        updates.add(&ENABLE_MZ_JOIN_CORE, false);
3473        updates.apply(&dyncfg);
3474
3475        // A replica without an override sees exactly the instance-wide values.
3476        let overrides = BTreeMap::new();
3477        let cmd = Instance::specialize_command_for_replica(
3478            create_instance_command(),
3479            ReplicaId::User(1),
3480            &overrides,
3481            &dyncfg,
3482        );
3483        let snapshot = initial_config(&cmd);
3484        assert_eq!(
3485            snapshot.updates.get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3486            Some(&ConfigVal::Bool(true)),
3487        );
3488        assert_eq!(
3489            snapshot.updates.get(ENABLE_MZ_JOIN_CORE.name()),
3490            Some(&ConfigVal::Bool(false)),
3491        );
3492    }
3493
3494    /// A replica-scoped override beats the instance-wide value in the create-time snapshot, so a
3495    /// create-time-frozen scoped flag reaches the replica with its override applied.
3496    #[mz_ore::test]
3497    fn create_instance_snapshot_applies_replica_override() {
3498        let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3499        let mut updates = ConfigUpdates::default();
3500        updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3501        updates.apply(&dyncfg);
3502
3503        let replica = ReplicaId::User(1);
3504        let mut override_updates = ConfigUpdates::default();
3505        override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, false);
3506        let overrides = BTreeMap::from([(replica, override_updates)]);
3507
3508        let cmd = Instance::specialize_command_for_replica(
3509            create_instance_command(),
3510            replica,
3511            &overrides,
3512            &dyncfg,
3513        );
3514        assert_eq!(
3515            initial_config(&cmd)
3516                .updates
3517                .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3518            Some(&ConfigVal::Bool(false)),
3519            "replica override should win over the instance-wide value",
3520        );
3521    }
3522
3523    /// `UpdateConfiguration` continues to merge the replica's override into the update.
3524    #[mz_ore::test]
3525    fn update_configuration_merges_replica_override() {
3526        let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3527
3528        let replica = ReplicaId::User(1);
3529        let mut override_updates = ConfigUpdates::default();
3530        override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3531        let overrides = BTreeMap::from([(replica, override_updates)]);
3532
3533        let cmd = Instance::specialize_command_for_replica(
3534            ComputeCommand::UpdateConfiguration(Box::new(Default::default())),
3535            replica,
3536            &overrides,
3537            &dyncfg,
3538        );
3539        match cmd {
3540            ComputeCommand::UpdateConfiguration(params) => assert_eq!(
3541                params
3542                    .dyncfg_updates
3543                    .updates
3544                    .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3545                Some(&ConfigVal::Bool(true)),
3546            ),
3547            other => panic!("expected UpdateConfiguration, got {other:?}"),
3548        }
3549    }
3550}