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