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        // We might have a chance to forward implied capabilities and reduce the cost of bringing
1380        // up the next replica, if the dropped replica was the only one in the cluster.
1381        self.forward_implied_capabilities();
1382
1383        Ok(())
1384    }
1385
1386    /// Rehydrate the given instance replica.
1387    ///
1388    /// # Panics
1389    ///
1390    /// Panics if the specified replica does not exist.
1391    fn rehydrate_replica(&mut self, id: ReplicaId) {
1392        let replica = &self.replicas[&id];
1393        let config = replica.config.clone();
1394        let epoch = replica.epoch + 1;
1395        let dyncfg_override = replica.dyncfg_override.clone();
1396
1397        self.remove_replica(id).expect("replica must exist");
1398        let result = self.add_replica(id, config, Some(epoch), dyncfg_override);
1399
1400        match result {
1401            Ok(()) => (),
1402            Err(ReplicaExists(_)) => unreachable!("replica was removed"),
1403        }
1404    }
1405
1406    /// Rehydrate any failed replicas of this instance.
1407    fn rehydrate_failed_replicas(&mut self) {
1408        let replicas = self.replicas.iter();
1409        let failed_replicas: Vec<_> = replicas
1410            .filter_map(|(id, replica)| replica.client.is_failed().then_some(*id))
1411            .collect();
1412
1413        for replica_id in failed_replicas {
1414            self.rehydrate_replica(replica_id);
1415        }
1416    }
1417
1418    /// Creates the described dataflow and initializes state for its output.
1419    ///
1420    /// This method expects a `DataflowDescription` with an `as_of` frontier specified, as well as
1421    /// for each imported collection a read hold in `import_read_holds` at at least the `as_of`.
1422    #[mz_ore::instrument(level = "debug")]
1423    pub fn create_dataflow(
1424        &mut self,
1425        dataflow: LirDataflowDescription,
1426        import_read_holds: Vec<ReadHold>,
1427        mut shared_collection_state: BTreeMap<GlobalId, SharedCollectionState>,
1428        target_replica: Option<ReplicaId>,
1429    ) -> Result<(), DataflowCreationError> {
1430        use DataflowCreationError::*;
1431
1432        // Validate that the target replica, if specified, exists.
1433        // A targeted dataflow is only installed on a single replica; if that
1434        // replica doesn't exist, we can't create the dataflow.
1435        if let Some(replica_id) = target_replica {
1436            if !self.replica_exists(replica_id) {
1437                return Err(ReplicaMissing(replica_id));
1438            }
1439        }
1440
1441        // Simple sanity checks around `as_of`
1442        let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
1443        if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
1444            return Err(EmptyAsOfForSubscribe);
1445        }
1446        if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
1447            return Err(EmptyAsOfForCopyTo);
1448        }
1449
1450        // Collect all dependencies of the dataflow, and read holds on them at the `as_of`.
1451        let mut storage_dependencies = BTreeMap::new();
1452        let mut compute_dependencies = BTreeMap::new();
1453
1454        // When we install per-replica input read holds, we cannot use the `as_of` because of
1455        // reconciliation: Existing slow replicas might be reading from the inputs at times before
1456        // the `as_of` and we would rather not crash them by allowing their inputs to compact too
1457        // far. So instead we take read holds at the least time available.
1458        let mut replica_input_read_holds = Vec::new();
1459
1460        let mut import_read_holds: BTreeMap<_, _> =
1461            import_read_holds.into_iter().map(|r| (r.id(), r)).collect();
1462
1463        for &id in dataflow.source_imports.keys() {
1464            let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1465            replica_input_read_holds.push(read_hold.clone());
1466
1467            read_hold
1468                .try_downgrade(as_of.clone())
1469                .map_err(|_| ReadHoldInsufficient(id))?;
1470            storage_dependencies.insert(id, read_hold);
1471        }
1472
1473        for &id in dataflow.index_imports.keys() {
1474            let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1475            read_hold
1476                .try_downgrade(as_of.clone())
1477                .map_err(|_| ReadHoldInsufficient(id))?;
1478            compute_dependencies.insert(id, read_hold);
1479        }
1480
1481        // If the `as_of` is empty, we are not going to create a dataflow, so replicas won't read
1482        // from the inputs.
1483        if as_of.is_empty() {
1484            replica_input_read_holds = Default::default();
1485        }
1486
1487        // Install collection state for each of the exports.
1488        for export_id in dataflow.export_ids() {
1489            let shared = shared_collection_state
1490                .remove(&export_id)
1491                .unwrap_or_else(|| SharedCollectionState::new(as_of.clone()));
1492            let write_only = dataflow.sink_exports.contains_key(&export_id);
1493            let storage_sink = dataflow.persist_sink_ids().any(|id| id == export_id);
1494
1495            self.add_collection(
1496                export_id,
1497                as_of.clone(),
1498                shared,
1499                storage_dependencies.clone(),
1500                compute_dependencies.clone(),
1501                replica_input_read_holds.clone(),
1502                write_only,
1503                storage_sink,
1504                dataflow.initial_storage_as_of.clone(),
1505                dataflow.refresh_schedule.clone(),
1506                target_replica,
1507            );
1508
1509            // If the export is a storage sink, we can advance its write frontier to the write
1510            // frontier of the target storage collection.
1511            if let Ok(frontiers) = self.storage_collections.collection_frontiers(export_id) {
1512                self.maybe_update_global_write_frontier(export_id, frontiers.write_frontier);
1513            }
1514        }
1515
1516        // Initialize tracking of subscribes.
1517        for subscribe_id in dataflow.subscribe_ids() {
1518            self.subscribes
1519                .insert(subscribe_id, ActiveSubscribe::default());
1520        }
1521
1522        // Initialize tracking of copy tos.
1523        for copy_to_id in dataflow.copy_to_ids() {
1524            self.copy_tos.insert(copy_to_id);
1525        }
1526
1527        // Here we augment all imported sources and all exported sinks with the appropriate
1528        // storage metadata needed by the compute instance.
1529        let mut source_imports = BTreeMap::new();
1530        for (id, import) in dataflow.source_imports {
1531            let frontiers = self
1532                .storage_collections
1533                .collection_frontiers(id)
1534                .expect("collection exists");
1535
1536            let collection_metadata = self
1537                .storage_collections
1538                .collection_metadata(id)
1539                .expect("we have a read hold on this collection");
1540
1541            let desc = SourceInstanceDesc {
1542                storage_metadata: collection_metadata.clone(),
1543                arguments: import.desc.arguments,
1544                typ: import.desc.typ.clone(),
1545            };
1546            source_imports.insert(
1547                id,
1548                mz_compute_types::dataflows::SourceImport {
1549                    desc,
1550                    monotonic: import.monotonic,
1551                    with_snapshot: import.with_snapshot,
1552                    upper: frontiers.write_frontier,
1553                },
1554            );
1555        }
1556
1557        let mut sink_exports = BTreeMap::new();
1558        for (id, se) in dataflow.sink_exports {
1559            let connection = match se.connection {
1560                ComputeSinkConnection::MaterializedView(conn) => {
1561                    let metadata = self
1562                        .storage_collections
1563                        .collection_metadata(id)
1564                        .map_err(|_| CollectionMissing(id))?
1565                        .clone();
1566                    let conn = MaterializedViewSinkConnection {
1567                        value_desc: conn.value_desc,
1568                        storage_metadata: metadata,
1569                    };
1570                    ComputeSinkConnection::MaterializedView(conn)
1571                }
1572                ComputeSinkConnection::Subscribe(conn) => ComputeSinkConnection::Subscribe(conn),
1573                ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1574                    ComputeSinkConnection::CopyToS3Oneshot(conn)
1575                }
1576                ComputeSinkConnection::MetricSink(conn) => ComputeSinkConnection::MetricSink(conn),
1577            };
1578            let desc = ComputeSinkDesc {
1579                from: se.from,
1580                from_desc: se.from_desc,
1581                connection,
1582                with_snapshot: se.with_snapshot,
1583                up_to: se.up_to,
1584                non_null_assertions: se.non_null_assertions,
1585                refresh_schedule: se.refresh_schedule,
1586                from_arrangement: se.from_arrangement,
1587            };
1588            sink_exports.insert(id, desc);
1589        }
1590
1591        // Flatten the dataflow plans into the representation expected by replicas.
1592        let objects_to_build = dataflow
1593            .objects_to_build
1594            .into_iter()
1595            .map(|object| BuildDesc {
1596                id: object.id,
1597                plan: RenderPlan::try_from(object.plan).expect("valid plan"),
1598            })
1599            .collect();
1600
1601        let augmented_dataflow = DataflowDescription {
1602            source_imports,
1603            sink_exports,
1604            objects_to_build,
1605            // The rest of the fields are identical
1606            index_imports: dataflow.index_imports,
1607            index_exports: dataflow.index_exports,
1608            as_of: dataflow.as_of.clone(),
1609            until: dataflow.until,
1610            initial_storage_as_of: dataflow.initial_storage_as_of,
1611            refresh_schedule: dataflow.refresh_schedule,
1612            debug_name: dataflow.debug_name,
1613            time_dependence: dataflow.time_dependence,
1614        };
1615
1616        if augmented_dataflow.is_transient() {
1617            tracing::debug!(
1618                name = %augmented_dataflow.debug_name,
1619                import_ids = %augmented_dataflow.display_import_ids(),
1620                export_ids = %augmented_dataflow.display_export_ids(),
1621                as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1622                until = ?augmented_dataflow.until.elements(),
1623                "creating dataflow",
1624            );
1625        } else {
1626            tracing::info!(
1627                name = %augmented_dataflow.debug_name,
1628                import_ids = %augmented_dataflow.display_import_ids(),
1629                export_ids = %augmented_dataflow.display_export_ids(),
1630                as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1631                until = ?augmented_dataflow.until.elements(),
1632                "creating dataflow",
1633            );
1634        }
1635
1636        // Skip the actual dataflow creation for an empty `as_of`. (Happens e.g. for the
1637        // bootstrapping of a REFRESH AT mat view that is past its last refresh.)
1638        if as_of.is_empty() {
1639            tracing::info!(
1640                name = %augmented_dataflow.debug_name,
1641                "not sending `CreateDataflow`, because of empty `as_of`",
1642            );
1643        } else {
1644            let collections: Vec<_> = augmented_dataflow.export_ids().collect();
1645            self.send(ComputeCommand::CreateDataflow(Box::new(augmented_dataflow)));
1646
1647            for id in collections {
1648                self.maybe_schedule_collection(id);
1649            }
1650        }
1651
1652        Ok(())
1653    }
1654
1655    /// Schedule the identified collection if all its inputs are available.
1656    ///
1657    /// # Panics
1658    ///
1659    /// Panics if the identified collection does not exist.
1660    fn maybe_schedule_collection(&mut self, id: GlobalId) {
1661        let collection = self.expect_collection(id);
1662
1663        // Don't schedule collections twice.
1664        if collection.scheduled {
1665            return;
1666        }
1667
1668        let as_of = collection.read_frontier();
1669
1670        // If the collection has an empty `as_of`, it was either never installed on the replica or
1671        // has since been dropped. In either case the replica does not expect any commands for it.
1672        if as_of.is_empty() {
1673            return;
1674        }
1675
1676        let ready = if id.is_transient() {
1677            // Always schedule transient collections immediately. The assumption is that those are
1678            // created by interactive user commands and we want to schedule them as quickly as
1679            // possible. Inputs might not yet be available, but when they become available, we
1680            // don't need to wait for the controller to become aware and for the scheduling check
1681            // to run again.
1682            true
1683        } else {
1684            // Ignore self-dependencies. Any self-dependencies do not need to be
1685            // available at the as_of for the dataflow to make progress, so we
1686            // can ignore them here. At the moment, only continual tasks have
1687            // self-dependencies, but this logic is correct for any dataflow, so
1688            // we don't special case it to CTs.
1689            let not_self_dep = |x: &GlobalId| *x != id;
1690
1691            // Make sure we never schedule a collection before its input compute collections have
1692            // been scheduled. Scheduling in the wrong order can lead to deadlocks.
1693            let mut deps_scheduled = true;
1694
1695            // Check dependency frontiers to determine if all inputs are
1696            // available. An input is available when its frontier is greater
1697            // than the `as_of`, i.e., all input data up to and including the
1698            // `as_of` has been sealed.
1699            let compute_deps = collection.compute_dependency_ids().filter(not_self_dep);
1700            let mut compute_frontiers = Vec::new();
1701            for id in compute_deps {
1702                let dep = &self.expect_collection(id);
1703                deps_scheduled &= dep.scheduled;
1704                compute_frontiers.push(dep.write_frontier());
1705            }
1706
1707            let storage_deps = collection.storage_dependency_ids().filter(not_self_dep);
1708            let storage_frontiers = self
1709                .storage_collections
1710                .collections_frontiers(storage_deps.collect())
1711                .expect("must exist");
1712            let storage_frontiers = storage_frontiers.into_iter().map(|f| f.write_frontier);
1713
1714            let mut frontiers = compute_frontiers.into_iter().chain(storage_frontiers);
1715            let frontiers_ready =
1716                frontiers.all(|frontier| PartialOrder::less_than(&as_of, &frontier));
1717
1718            deps_scheduled && frontiers_ready
1719        };
1720
1721        if ready {
1722            self.send(ComputeCommand::Schedule(id));
1723            let collection = self.expect_collection_mut(id);
1724            collection.scheduled = true;
1725        }
1726    }
1727
1728    /// Schedule any unscheduled collections that are ready.
1729    fn schedule_collections(&mut self) {
1730        let ids: Vec<_> = self.collections.keys().copied().collect();
1731        for id in ids {
1732            self.maybe_schedule_collection(id);
1733        }
1734    }
1735
1736    /// Drops the read capability for the given collections and allows their resources to be
1737    /// reclaimed.
1738    #[mz_ore::instrument(level = "debug")]
1739    pub fn drop_collections(&mut self, ids: Vec<GlobalId>) -> Result<(), CollectionMissing> {
1740        for id in &ids {
1741            let collection = self.collection_mut(*id)?;
1742
1743            // Mark the collection as dropped to allow it to be removed from the controller state.
1744            collection.dropped = true;
1745
1746            // Drop the implied and warmup read holds to announce that clients are not
1747            // interested in the collection anymore.
1748            collection.implied_read_hold.release();
1749            collection.warmup_read_hold.release();
1750
1751            // If the collection is a subscribe, stop tracking it. This ensures that the controller
1752            // ceases to produce `SubscribeResponse`s for this subscribe.
1753            self.subscribes.remove(id);
1754            // If the collection is a copy to, stop tracking it. This ensures that the controller
1755            // ceases to produce `CopyToResponse`s` for this copy to.
1756            self.copy_tos.remove(id);
1757        }
1758
1759        Ok(())
1760    }
1761
1762    /// Initiate a peek request for the contents of `id` at `timestamp`.
1763    ///
1764    /// If this returns an error, then it didn't modify any `Instance` state.
1765    #[mz_ore::instrument(level = "debug")]
1766    pub fn peek(
1767        &mut self,
1768        peek_target: PeekTarget,
1769        literal_constraints: Option<Vec<Row>>,
1770        uuid: Uuid,
1771        timestamp: Timestamp,
1772        result_desc: RelationDesc,
1773        finishing: RowSetFinishing,
1774        map_filter_project: mz_expr::SafeMfpPlan,
1775        mut read_hold: ReadHold,
1776        target_replica: Option<ReplicaId>,
1777        peek_response_tx: oneshot::Sender<PeekResponse>,
1778    ) -> Result<(), PeekError> {
1779        use PeekError::*;
1780
1781        let target_id = peek_target.id();
1782
1783        // Downgrade the provided read hold to the peek time.
1784        if read_hold.id() != target_id {
1785            return Err(ReadHoldIdMismatch(read_hold.id()));
1786        }
1787        read_hold
1788            .try_downgrade(Antichain::from_elem(timestamp.clone()))
1789            .map_err(|_| ReadHoldInsufficient(target_id))?;
1790
1791        if let Some(target) = target_replica {
1792            if !self.replica_exists(target) {
1793                return Err(ReplicaMissing(target));
1794            }
1795        }
1796
1797        let otel_ctx = OpenTelemetryContext::obtain();
1798
1799        self.peeks.insert(
1800            uuid,
1801            PendingPeek {
1802                target_replica,
1803                // TODO(guswynn): can we just hold the `tracing::Span` here instead?
1804                otel_ctx: otel_ctx.clone(),
1805                requested_at: Instant::now(),
1806                read_hold,
1807                peek_response_tx,
1808                limit: finishing.limit.map(usize::cast_from),
1809                offset: finishing.offset,
1810            },
1811        );
1812
1813        let peek = Peek {
1814            literal_constraints,
1815            uuid,
1816            timestamp,
1817            finishing,
1818            map_filter_project,
1819            // Obtain an `OpenTelemetryContext` from the thread-local tracing
1820            // tree to forward it on to the compute worker.
1821            otel_ctx,
1822            target: peek_target,
1823            result_desc,
1824        };
1825        self.send(ComputeCommand::Peek(Box::new(peek)));
1826
1827        Ok(())
1828    }
1829
1830    /// Cancels an existing peek request.
1831    #[mz_ore::instrument(level = "debug")]
1832    pub fn cancel_peek(&mut self, uuid: Uuid, reason: PeekResponse) {
1833        let Some(peek) = self.peeks.get_mut(&uuid) else {
1834            tracing::warn!("did not find pending peek for {uuid}");
1835            return;
1836        };
1837
1838        let duration = peek.requested_at.elapsed();
1839        self.metrics
1840            .observe_peek_response(&PeekResponse::Canceled, duration);
1841
1842        // Enqueue a notification for the cancellation.
1843        let otel_ctx = peek.otel_ctx.clone();
1844        otel_ctx.attach_as_parent();
1845
1846        self.deliver_response(ComputeControllerResponse::PeekNotification(
1847            uuid,
1848            PeekNotification::Canceled,
1849            otel_ctx,
1850        ));
1851
1852        // Finish the peek.
1853        // This will also propagate the cancellation to the replicas.
1854        self.finish_peek(uuid, reason);
1855    }
1856
1857    /// Assigns a read policy to specific identifiers.
1858    ///
1859    /// The policies are assigned in the order presented, and repeated identifiers should
1860    /// conclude with the last policy. Changing a policy will immediately downgrade the read
1861    /// capability if appropriate, but it will not "recover" the read capability if the prior
1862    /// capability is already ahead of it.
1863    ///
1864    /// Identifiers not present in `policies` retain their existing read policies.
1865    ///
1866    /// It is an error to attempt to set a read policy for a collection that is not readable in the
1867    /// context of compute. At this time, only indexes are readable compute collections.
1868    #[mz_ore::instrument(level = "debug")]
1869    pub fn set_read_policy(
1870        &mut self,
1871        policies: Vec<(GlobalId, ReadPolicy)>,
1872    ) -> Result<(), ReadPolicyError> {
1873        // Do error checking upfront, to avoid introducing inconsistencies between a collection's
1874        // `implied_capability` and `read_capabilities`.
1875        for (id, _policy) in &policies {
1876            let collection = self.collection(*id)?;
1877            if collection.read_policy.is_none() {
1878                return Err(ReadPolicyError::WriteOnlyCollection(*id));
1879            }
1880        }
1881
1882        for (id, new_policy) in policies {
1883            let collection = self.expect_collection_mut(id);
1884            let new_since = new_policy.frontier(collection.write_frontier().borrow());
1885            let _ = collection.implied_read_hold.try_downgrade(new_since);
1886            collection.read_policy = Some(new_policy);
1887        }
1888
1889        Ok(())
1890    }
1891
1892    /// Advance the global write frontier of the given collection.
1893    ///
1894    /// Frontier regressions are gracefully ignored.
1895    ///
1896    /// # Panics
1897    ///
1898    /// Panics if the identified collection does not exist.
1899    #[mz_ore::instrument(level = "debug")]
1900    fn maybe_update_global_write_frontier(
1901        &mut self,
1902        id: GlobalId,
1903        new_frontier: Antichain<Timestamp>,
1904    ) {
1905        let collection = self.expect_collection_mut(id);
1906
1907        let advanced = collection.shared.lock_write_frontier(|f| {
1908            let advanced = PartialOrder::less_than(f, &new_frontier);
1909            if advanced {
1910                f.clone_from(&new_frontier);
1911            }
1912            advanced
1913        });
1914
1915        if !advanced {
1916            return;
1917        }
1918
1919        // Relax the implied read hold according to the read policy.
1920        let new_since = match &collection.read_policy {
1921            Some(read_policy) => {
1922                // For readable collections the read frontier is determined by applying the
1923                // client-provided read policy to the write frontier.
1924                read_policy.frontier(new_frontier.borrow())
1925            }
1926            None => {
1927                // Write-only collections cannot be read within the context of the compute
1928                // controller, so their read frontier only controls the read holds taken on their
1929                // inputs. We can safely downgrade the input read holds to any time less than the
1930                // write frontier.
1931                //
1932                // Note that some write-only collections (continual tasks) need to observe changes
1933                // at their current write frontier during hydration. Thus, we cannot downgrade the
1934                // read frontier to the write frontier and instead step it back by one.
1935                Antichain::from_iter(
1936                    new_frontier
1937                        .iter()
1938                        .map(|t| t.step_back().unwrap_or(Timestamp::MIN)),
1939                )
1940            }
1941        };
1942        let _ = collection.implied_read_hold.try_downgrade(new_since);
1943
1944        // Report the frontier advancement.
1945        self.deliver_response(ComputeControllerResponse::FrontierUpper {
1946            id,
1947            upper: new_frontier,
1948        });
1949    }
1950
1951    /// Apply a collection read hold change.
1952    pub(super) fn apply_read_hold_change(
1953        &mut self,
1954        id: GlobalId,
1955        mut update: ChangeBatch<Timestamp>,
1956    ) {
1957        let Some(collection) = self.collections.get_mut(&id) else {
1958            soft_panic_or_log!(
1959                "read hold change for absent collection (id={id}, changes={update:?})"
1960            );
1961            return;
1962        };
1963
1964        let new_since = collection.shared.lock_read_capabilities(|caps| {
1965            // Sanity check to prevent corrupted `read_capabilities`, which can cause hard-to-debug
1966            // issues (usually stuck read frontiers).
1967            let read_frontier = caps.frontier();
1968            for (time, diff) in update.iter() {
1969                let count = caps.count_for(time) + diff;
1970                assert!(
1971                    count >= 0,
1972                    "invalid read capabilities update: negative capability \
1973             (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1974                );
1975                assert!(
1976                    count == 0 || read_frontier.less_equal(time),
1977                    "invalid read capabilities update: frontier regression \
1978             (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1979                );
1980            }
1981
1982            // Apply read capability updates and learn about resulting changes to the read
1983            // frontier.
1984            let changes = caps.update_iter(update.drain());
1985
1986            let changed = changes.count() > 0;
1987            changed.then(|| caps.frontier().to_owned())
1988        });
1989
1990        let Some(new_since) = new_since else {
1991            return; // read frontier did not change
1992        };
1993
1994        // Propagate read frontier update to dependencies.
1995        for read_hold in collection.compute_dependencies.values_mut() {
1996            read_hold
1997                .try_downgrade(new_since.clone())
1998                .expect("frontiers don't regress");
1999        }
2000        for read_hold in collection.storage_dependencies.values_mut() {
2001            read_hold
2002                .try_downgrade(new_since.clone())
2003                .expect("frontiers don't regress");
2004        }
2005
2006        // Produce `AllowCompaction` command.
2007        self.send(ComputeCommand::AllowCompaction {
2008            id,
2009            frontier: new_since,
2010        });
2011    }
2012
2013    /// Fulfills a registered peek and cleans up associated state.
2014    ///
2015    /// As part of this we:
2016    ///  * Send a `PeekResponse` through the peek's response channel.
2017    ///  * Emit a `CancelPeek` command to instruct replicas to stop spending resources on this
2018    ///    peek, and to allow the `ComputeCommandHistory` to reduce away the corresponding `Peek`
2019    ///    command.
2020    ///  * Remove the read hold for this peek, unblocking compaction that might have waited on it.
2021    fn finish_peek(&mut self, uuid: Uuid, response: PeekResponse) {
2022        let Some(peek) = self.peeks.remove(&uuid) else {
2023            return;
2024        };
2025
2026        // The recipient might not be interested in the peek response anymore, which is fine.
2027        let _ = peek.peek_response_tx.send(response);
2028
2029        // NOTE: We need to send the `CancelPeek` command _before_ we release the peek's read hold
2030        // (by dropping it), to avoid the edge case that caused database-issues#4812.
2031        self.send(ComputeCommand::CancelPeek { uuid });
2032
2033        drop(peek.read_hold);
2034    }
2035
2036    /// Handles a response from a replica. Replica IDs are re-used across replica restarts, so we
2037    /// use the replica epoch to drop stale responses.
2038    fn handle_response(&mut self, (replica_id, epoch, response): ReplicaResponse) {
2039        // Filter responses from non-existing or stale replicas.
2040        if self
2041            .replicas
2042            .get(&replica_id)
2043            .filter(|replica| replica.epoch == epoch)
2044            .is_none()
2045        {
2046            return;
2047        }
2048
2049        // Invariant: the replica exists and has the expected epoch.
2050
2051        match response {
2052            ComputeResponse::Frontiers(id, frontiers) => {
2053                self.handle_frontiers_response(id, frontiers, replica_id);
2054            }
2055            ComputeResponse::PeekResponse(uuid, peek_response, otel_ctx) => {
2056                self.handle_peek_response(uuid, peek_response, otel_ctx, replica_id);
2057            }
2058            ComputeResponse::CopyToResponse(id, response) => {
2059                self.handle_copy_to_response(id, response, replica_id);
2060            }
2061            ComputeResponse::SubscribeResponse(id, response) => {
2062                self.handle_subscribe_response(id, response, replica_id);
2063            }
2064            ComputeResponse::Status(response) => {
2065                self.handle_status_response(response, replica_id);
2066            }
2067        }
2068    }
2069
2070    /// Handle new frontiers, returning any compute response that needs to
2071    /// be sent to the client.
2072    fn handle_frontiers_response(
2073        &mut self,
2074        id: GlobalId,
2075        frontiers: FrontiersResponse,
2076        replica_id: ReplicaId,
2077    ) {
2078        if !self.collections.contains_key(&id) {
2079            soft_panic_or_log!(
2080                "frontiers update for an unknown collection \
2081                 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2082            );
2083            return;
2084        }
2085        let Some(replica) = self.replicas.get_mut(&replica_id) else {
2086            soft_panic_or_log!(
2087                "frontiers update for an unknown replica \
2088                 (replica_id={replica_id}, frontiers={frontiers:?})"
2089            );
2090            return;
2091        };
2092        let Some(replica_collection) = replica.collections.get_mut(&id) else {
2093            soft_panic_or_log!(
2094                "frontiers update for an unknown replica collection \
2095                 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2096            );
2097            return;
2098        };
2099
2100        if let Some(new_frontier) = frontiers.input_frontier {
2101            replica_collection.update_input_frontier(new_frontier.clone());
2102        }
2103        if let Some(new_frontier) = frontiers.output_frontier {
2104            replica_collection.update_output_frontier(new_frontier.clone());
2105        }
2106        if let Some(new_frontier) = frontiers.write_frontier {
2107            replica_collection.update_write_frontier(new_frontier.clone());
2108            self.maybe_update_global_write_frontier(id, new_frontier);
2109        }
2110    }
2111
2112    #[mz_ore::instrument(level = "debug")]
2113    fn handle_peek_response(
2114        &mut self,
2115        uuid: Uuid,
2116        response: PeekResponse,
2117        otel_ctx: OpenTelemetryContext,
2118        replica_id: ReplicaId,
2119    ) {
2120        otel_ctx.attach_as_parent();
2121
2122        // We might not be tracking this peek anymore, because we have served a response already or
2123        // because it was canceled. If this is the case, we ignore the response.
2124        let Some(peek) = self.peeks.get(&uuid) else {
2125            return;
2126        };
2127
2128        // If the peek is targeting a replica, ignore responses from other replicas.
2129        let target_replica = peek.target_replica.unwrap_or(replica_id);
2130        if target_replica != replica_id {
2131            return;
2132        }
2133
2134        let duration = peek.requested_at.elapsed();
2135        self.metrics.observe_peek_response(&response, duration);
2136
2137        let notification = PeekNotification::new(&response, peek.offset, peek.limit);
2138        // NOTE: We use the `otel_ctx` from the response, not the pending peek, because we
2139        // currently want the parent to be whatever the compute worker did with this peek.
2140        self.deliver_response(ComputeControllerResponse::PeekNotification(
2141            uuid,
2142            notification,
2143            otel_ctx,
2144        ));
2145
2146        self.finish_peek(uuid, response)
2147    }
2148
2149    fn handle_copy_to_response(
2150        &mut self,
2151        sink_id: GlobalId,
2152        response: CopyToResponse,
2153        replica_id: ReplicaId,
2154    ) {
2155        if !self.collections.contains_key(&sink_id) {
2156            soft_panic_or_log!(
2157                "received response for an unknown copy-to \
2158                 (sink_id={sink_id}, replica_id={replica_id})",
2159            );
2160            return;
2161        }
2162        let Some(replica) = self.replicas.get_mut(&replica_id) else {
2163            soft_panic_or_log!("copy-to response for an unknown replica (replica_id={replica_id})");
2164            return;
2165        };
2166        let Some(replica_collection) = replica.collections.get_mut(&sink_id) else {
2167            soft_panic_or_log!(
2168                "copy-to response for an unknown replica collection \
2169                 (sink_id={sink_id}, replica_id={replica_id})"
2170            );
2171            return;
2172        };
2173
2174        // Downgrade the replica frontiers, to enable dropping of input read holds and clean up of
2175        // collection state.
2176        // TODO(database-issues#4701): report copy-to frontiers through `Frontiers` responses
2177        replica_collection.update_write_frontier(Antichain::new());
2178        replica_collection.update_input_frontier(Antichain::new());
2179        replica_collection.update_output_frontier(Antichain::new());
2180
2181        // We might not be tracking this COPY TO because we have already returned a response
2182        // from one of the replicas. In that case, we ignore the response.
2183        if !self.copy_tos.remove(&sink_id) {
2184            return;
2185        }
2186
2187        let result = match response {
2188            CopyToResponse::RowCount(count) => Ok(count),
2189            CopyToResponse::Error(error) => Err(anyhow::anyhow!(error)),
2190            // We should never get here: Replicas only drop copy to collections in response
2191            // to the controller allowing them to do so, and when the controller drops a
2192            // copy to it also removes it from the list of tracked copy_tos (see
2193            // [`Instance::drop_collections`]).
2194            CopyToResponse::Dropped => {
2195                tracing::error!(
2196                    %sink_id, %replica_id,
2197                    "received `Dropped` response for a tracked copy to",
2198                );
2199                return;
2200            }
2201        };
2202
2203        self.deliver_response(ComputeControllerResponse::CopyToResponse(sink_id, result));
2204    }
2205
2206    fn handle_subscribe_response(
2207        &mut self,
2208        subscribe_id: GlobalId,
2209        response: SubscribeResponse,
2210        replica_id: ReplicaId,
2211    ) {
2212        if !self.collections.contains_key(&subscribe_id) {
2213            soft_panic_or_log!(
2214                "received response for an unknown subscribe \
2215                 (subscribe_id={subscribe_id}, replica_id={replica_id})",
2216            );
2217            return;
2218        }
2219        let Some(replica) = self.replicas.get_mut(&replica_id) else {
2220            soft_panic_or_log!(
2221                "subscribe response for an unknown replica (replica_id={replica_id})"
2222            );
2223            return;
2224        };
2225        let Some(replica_collection) = replica.collections.get_mut(&subscribe_id) else {
2226            soft_panic_or_log!(
2227                "subscribe response for an unknown replica collection \
2228                 (subscribe_id={subscribe_id}, replica_id={replica_id})"
2229            );
2230            return;
2231        };
2232
2233        // Always apply replica write frontier updates. Even if the subscribe is not tracked
2234        // anymore, there might still be replicas reading from its inputs, so we need to track the
2235        // frontiers until all replicas have advanced to the empty one.
2236        let write_frontier = match &response {
2237            SubscribeResponse::Batch(batch) => batch.upper.clone(),
2238            SubscribeResponse::DroppedAt(_) => Antichain::new(),
2239        };
2240
2241        // For subscribes we downgrade all replica frontiers based on write frontiers. This should
2242        // be fine because the input and output frontier of a subscribe track its write frontier.
2243        // TODO(database-issues#4701): report subscribe frontiers through `Frontiers` responses
2244        replica_collection.update_write_frontier(write_frontier.clone());
2245        replica_collection.update_input_frontier(write_frontier.clone());
2246        replica_collection.update_output_frontier(write_frontier.clone());
2247
2248        // If the subscribe is not tracked, or targets a different replica, there is nothing to do.
2249        let Some(mut subscribe) = self.subscribes.get(&subscribe_id).cloned() else {
2250            return;
2251        };
2252
2253        // Apply a global frontier update.
2254        // If this is a replica-targeted subscribe, it is important that we advance the global
2255        // frontier only based on responses from the targeted replica. Otherwise, another replica
2256        // could advance to the empty frontier, making us drop the subscribe on the targeted
2257        // replica prematurely.
2258        self.maybe_update_global_write_frontier(subscribe_id, write_frontier);
2259
2260        match response {
2261            SubscribeResponse::Batch(batch) => {
2262                let upper = batch.upper;
2263                let mut updates = batch.updates;
2264
2265                // If this batch advances the subscribe's frontier, we emit all updates at times
2266                // greater or equal to the last frontier (to avoid emitting duplicate updates).
2267                if PartialOrder::less_than(&subscribe.frontier, &upper) {
2268                    let lower = std::mem::replace(&mut subscribe.frontier, upper.clone());
2269
2270                    if upper.is_empty() {
2271                        // This subscribe cannot produce more data. Stop tracking it.
2272                        self.subscribes.remove(&subscribe_id);
2273                    } else {
2274                        // This subscribe can produce more data. Update our tracking of it.
2275                        self.subscribes.insert(subscribe_id, subscribe);
2276                    }
2277
2278                    if let Ok(updates) = updates.as_mut() {
2279                        updates.retain_mut(|updates| {
2280                            let offset = updates.times().partition_point(|t| {
2281                                // True for times that are strictly less than lower (and should be skipped)
2282                                // and false otherwise.
2283                                !lower.less_equal(t)
2284                            });
2285                            let (_, past_lower) = std::mem::take(updates).split_at(offset);
2286                            *updates = past_lower;
2287                            updates.len() > 0
2288                        });
2289                    }
2290                    self.deliver_response(ComputeControllerResponse::SubscribeResponse(
2291                        subscribe_id,
2292                        SubscribeBatch {
2293                            lower,
2294                            upper,
2295                            updates,
2296                        },
2297                    ));
2298                }
2299            }
2300            SubscribeResponse::DroppedAt(frontier) => {
2301                // We should never get here: Replicas only drop subscribe collections in response
2302                // to the controller allowing them to do so, and when the controller drops a
2303                // subscribe it also removes it from the list of tracked subscribes (see
2304                // [`Instance::drop_collections`]).
2305                tracing::error!(
2306                    %subscribe_id,
2307                    %replica_id,
2308                    frontier = ?frontier.elements(),
2309                    "received `DroppedAt` response for a tracked subscribe",
2310                );
2311                self.subscribes.remove(&subscribe_id);
2312            }
2313        }
2314    }
2315
2316    fn handle_status_response(&self, response: StatusResponse, _replica_id: ReplicaId) {
2317        match response {
2318            StatusResponse::Placeholder => {}
2319        }
2320    }
2321
2322    /// Return the write frontiers of the dependencies of the given collection.
2323    fn dependency_write_frontiers<'b>(
2324        &'b self,
2325        collection: &'b CollectionState,
2326    ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2327        let compute_frontiers = collection.compute_dependency_ids().filter_map(|dep_id| {
2328            let collection = self.collections.get(&dep_id);
2329            collection.map(|c| c.write_frontier())
2330        });
2331        let storage_frontiers = collection.storage_dependency_ids().filter_map(|dep_id| {
2332            let frontiers = self.storage_collections.collection_frontiers(dep_id).ok();
2333            frontiers.map(|f| f.write_frontier)
2334        });
2335
2336        compute_frontiers.chain(storage_frontiers)
2337    }
2338
2339    /// Return the write frontiers of transitive storage dependencies of the given collection.
2340    fn transitive_storage_dependency_write_frontiers<'b>(
2341        &'b self,
2342        collection: &'b CollectionState,
2343    ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2344        let mut storage_ids: BTreeSet<_> = collection.storage_dependency_ids().collect();
2345        let mut todo: Vec<_> = collection.compute_dependency_ids().collect();
2346        let mut done = BTreeSet::new();
2347
2348        while let Some(id) = todo.pop() {
2349            if done.contains(&id) {
2350                continue;
2351            }
2352            if let Some(dep) = self.collections.get(&id) {
2353                storage_ids.extend(dep.storage_dependency_ids());
2354                todo.extend(dep.compute_dependency_ids())
2355            }
2356            done.insert(id);
2357        }
2358
2359        let storage_frontiers = storage_ids.into_iter().filter_map(|id| {
2360            let frontiers = self.storage_collections.collection_frontiers(id).ok();
2361            frontiers.map(|f| f.write_frontier)
2362        });
2363
2364        storage_frontiers
2365    }
2366
2367    /// Downgrade the warmup capabilities of collections as much as possible.
2368    ///
2369    /// The only requirement we have for a collection's warmup capability is that it is for a time
2370    /// that is available in all of the collection's inputs. For each input the latest time that is
2371    /// the case for is `write_frontier - 1`. So the farthest we can downgrade a collection's
2372    /// warmup capability is the minimum of `write_frontier - 1` of all its inputs.
2373    ///
2374    /// This method expects to be periodically called as part of instance maintenance work.
2375    /// We would like to instead update the warmup capabilities synchronously in response to
2376    /// frontier updates of dependency collections, but that is not generally possible because we
2377    /// don't learn about frontier updates of storage collections synchronously. We could do
2378    /// synchronous updates for compute dependencies, but we refrain from doing for simplicity.
2379    fn downgrade_warmup_capabilities(&mut self) {
2380        let mut new_capabilities = BTreeMap::new();
2381        for (id, collection) in &self.collections {
2382            // For write-only collections that have advanced to the empty frontier, we can drop the
2383            // warmup capability entirely. There is no reason why we would need to hydrate those
2384            // collections again, so being able to warm them up is not useful.
2385            if collection.read_policy.is_none()
2386                && collection.shared.lock_write_frontier(|f| f.is_empty())
2387            {
2388                new_capabilities.insert(*id, Antichain::new());
2389                continue;
2390            }
2391
2392            let mut new_capability = Antichain::new();
2393            for frontier in self.dependency_write_frontiers(collection) {
2394                for time in frontier {
2395                    new_capability.insert(time.step_back().unwrap_or(time));
2396                }
2397            }
2398
2399            new_capabilities.insert(*id, new_capability);
2400        }
2401
2402        for (id, new_capability) in new_capabilities {
2403            let collection = self.expect_collection_mut(id);
2404            let _ = collection.warmup_read_hold.try_downgrade(new_capability);
2405        }
2406    }
2407
2408    /// Forward the implied capabilities of collections, if possible.
2409    ///
2410    /// The implied capability of a collection controls (a) which times are still readable (for
2411    /// indexes) and (b) with which as-of the collection gets installed on a new replica. We are
2412    /// usually not allowed to advance an implied capability beyond the frontier that follows from
2413    /// the collection's read policy applied to its write frontier:
2414    ///
2415    ///  * For sink collections, some external consumer might rely on seeing all distinct times in
2416    ///    the input reflected in the output. If we'd forward the implied capability of a sink,
2417    ///    we'd risk skipping times in the output across replica restarts.
2418    ///  * For index collections, we might make the index unreadable by advancing its read frontier
2419    ///    beyond its write frontier.
2420    ///
2421    /// There is one case where forwarding an implied capability is fine though: an index installed
2422    /// on a cluster that has no replicas. Such indexes are not readable anyway until a new replica
2423    /// is added, so advancing its read frontier can't make it unreadable. We can thus advance the
2424    /// implied capability as long as we make sure that when a new replica is added, the expected
2425    /// relationship between write frontier, read policy, and implied capability can be restored
2426    /// immediately (modulo computation time).
2427    ///
2428    /// Forwarding implied capabilities is not necessary for the correct functioning of the
2429    /// controller but an optimization that is beneficial in two ways:
2430    ///
2431    ///  * It relaxes read holds on inputs to forwarded collections, allowing their compaction.
2432    ///  * It reduces the amount of historical detail new replicas need to process when computing
2433    ///    forwarded collections, as forwarding the implied capability also forwards the corresponding
2434    ///    dataflow as-of.
2435    fn forward_implied_capabilities(&mut self) {
2436        if !ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE.get(&self.dyncfg) {
2437            return;
2438        }
2439        if !self.replicas.is_empty() {
2440            return;
2441        }
2442
2443        let mut new_capabilities = BTreeMap::new();
2444        for (id, collection) in &self.collections {
2445            let Some(read_policy) = &collection.read_policy else {
2446                // Collection is write-only, i.e. a sink.
2447                continue;
2448            };
2449
2450            // When a new replica is started, it will immediately be able to compute all collection
2451            // output up to the write frontier of its transitive storage inputs. So the new implied
2452            // read capability should be the read policy applied to that frontier.
2453            let mut dep_frontier = Antichain::new();
2454            for frontier in self.transitive_storage_dependency_write_frontiers(collection) {
2455                dep_frontier.extend(frontier);
2456            }
2457
2458            let new_capability = read_policy.frontier(dep_frontier.borrow());
2459            if PartialOrder::less_than(collection.implied_read_hold.since(), &new_capability) {
2460                new_capabilities.insert(*id, new_capability);
2461            }
2462        }
2463
2464        for (id, new_capability) in new_capabilities {
2465            let collection = self.expect_collection_mut(id);
2466            let _ = collection.implied_read_hold.try_downgrade(new_capability);
2467        }
2468    }
2469
2470    /// Acquires a `ReadHold` for the identified compute collection.
2471    ///
2472    /// This mirrors the logic used by the controller-side `InstanceState::acquire_read_hold`,
2473    /// but executes on the instance task itself.
2474    pub(super) fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
2475        // Similarly to InstanceState::acquire_read_hold and StorageCollections::acquire_read_holds,
2476        // we acquire read holds at the earliest possible time rather than returning a copy
2477        // of the implied read hold. This is so that dependents can acquire read holds on
2478        // compute dependencies at frontiers that are held back by other read holds the caller
2479        // has previously taken.
2480        let collection = self.collection(id)?;
2481        let since = collection.shared.lock_read_capabilities(|caps| {
2482            let since = caps.frontier().to_owned();
2483            caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
2484            since
2485        });
2486        let hold = ReadHold::new(id, since, Arc::clone(&self.read_hold_tx));
2487        Ok(hold)
2488    }
2489
2490    /// Process pending maintenance work.
2491    ///
2492    /// This method is invoked periodically by the global controller.
2493    /// It is a good place to perform maintenance work that arises from various controller state
2494    /// changes and that cannot conveniently be handled synchronously with those state changes.
2495    #[mz_ore::instrument(level = "debug")]
2496    pub fn maintain(&mut self) {
2497        self.rehydrate_failed_replicas();
2498        self.downgrade_warmup_capabilities();
2499        self.forward_implied_capabilities();
2500        self.schedule_collections();
2501        self.cleanup_collections();
2502        self.update_frontier_introspection();
2503        self.refresh_state_metrics();
2504        self.refresh_wallclock_lag();
2505    }
2506}
2507
2508/// State maintained about individual compute collections.
2509///
2510/// A compute collection is either an index, or a storage sink, or a subscribe, exported by a
2511/// compute dataflow.
2512#[derive(Debug)]
2513struct CollectionState {
2514    /// If set, this collection is only maintained by the specified replica.
2515    target_replica: Option<ReplicaId>,
2516    /// Whether this collection is a log collection.
2517    ///
2518    /// Log collections are special in that they are only maintained by a subset of all replicas.
2519    log_collection: bool,
2520    /// Whether this collection has been dropped by a controller client.
2521    ///
2522    /// The controller is allowed to remove the `CollectionState` for a collection only when
2523    /// `dropped == true`. Otherwise, clients might still expect to be able to query information
2524    /// about this collection.
2525    dropped: bool,
2526    /// Whether this collection has been scheduled, i.e., the controller has sent a `Schedule`
2527    /// command for it.
2528    scheduled: bool,
2529
2530    /// Whether this collection is in read-only mode.
2531    ///
2532    /// When in read-only mode, the dataflow is not allowed to affect external state (largely persist).
2533    read_only: bool,
2534
2535    /// State shared with the `ComputeController`.
2536    shared: SharedCollectionState,
2537
2538    /// A read hold maintaining the implicit capability of the collection.
2539    ///
2540    /// This capability is kept to ensure that the collection remains readable according to its
2541    /// `read_policy`. It also ensures that read holds on the collection's dependencies are kept at
2542    /// some time not greater than the collection's `write_frontier`, guaranteeing that the
2543    /// collection's next outputs can always be computed without skipping times.
2544    implied_read_hold: ReadHold,
2545    /// A read hold held to enable dataflow warmup.
2546    ///
2547    /// Dataflow warmup is an optimization that allows dataflows to immediately start hydrating
2548    /// even when their next output time (as implied by the `write_frontier`) is in the future.
2549    /// By installing a read capability derived from the write frontiers of the collection's
2550    /// inputs, we ensure that the as-of of new dataflows installed for the collection is at a time
2551    /// that is immediately available, so hydration can begin immediately too.
2552    warmup_read_hold: ReadHold,
2553    /// The policy to use to downgrade `self.implied_read_hold`.
2554    ///
2555    /// If `None`, the collection is a write-only collection (i.e. a sink). For write-only
2556    /// collections, the `implied_read_hold` is only required for maintaining read holds on the
2557    /// inputs, so we can immediately downgrade it to the `write_frontier`.
2558    read_policy: Option<ReadPolicy>,
2559
2560    /// Storage identifiers on which this collection depends, and read holds this collection
2561    /// requires on them.
2562    storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2563    /// Compute identifiers on which this collection depends, and read holds this collection
2564    /// requires on them.
2565    compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2566
2567    /// Introspection state associated with this collection.
2568    introspection: CollectionIntrospection,
2569
2570    /// Frontier wallclock lag measurements stashed until the next `WallclockLagHistogram`
2571    /// introspection update.
2572    ///
2573    /// Keys are `(period, lag, labels)` triples, values are counts.
2574    ///
2575    /// If this is `None`, wallclock lag is not tracked for this collection.
2576    wallclock_lag_histogram_stash: Option<
2577        BTreeMap<
2578            (
2579                WallclockLagHistogramPeriod,
2580                WallclockLag,
2581                BTreeMap<&'static str, String>,
2582            ),
2583            Diff,
2584        >,
2585    >,
2586}
2587
2588impl CollectionState {
2589    /// Creates a new collection state, with an initial read policy valid from `since`.
2590    fn new(
2591        collection_id: GlobalId,
2592        as_of: Antichain<Timestamp>,
2593        shared: SharedCollectionState,
2594        storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2595        compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2596        read_hold_tx: read_holds::ChangeTx,
2597        introspection: CollectionIntrospection,
2598    ) -> Self {
2599        // A collection is not readable before the `as_of`.
2600        let since = as_of.clone();
2601        // A collection won't produce updates for times before the `as_of`.
2602        let upper = as_of;
2603
2604        // Ensure that the provided `shared` is valid for the given `as_of`.
2605        assert!(shared.lock_read_capabilities(|c| c.frontier() == since.borrow()));
2606        assert!(shared.lock_write_frontier(|f| f == &upper));
2607
2608        // Initialize collection read holds.
2609        // Note that the implied read hold was already added to the `read_capabilities` when
2610        // `shared` was created, so we only need to add the warmup read hold here.
2611        let implied_read_hold =
2612            ReadHold::new(collection_id, since.clone(), Arc::clone(&read_hold_tx));
2613        let warmup_read_hold = ReadHold::new(collection_id, since.clone(), read_hold_tx);
2614
2615        let updates = warmup_read_hold.since().iter().map(|t| (t.clone(), 1));
2616        shared.lock_read_capabilities(|c| {
2617            c.update_iter(updates);
2618        });
2619
2620        // In an effort to keep the produced wallclock lag introspection data small and
2621        // predictable, we disable wallclock lag tracking for transient collections, i.e. slow-path
2622        // select indexes and subscribes.
2623        let wallclock_lag_histogram_stash = match collection_id.is_transient() {
2624            true => None,
2625            false => Some(Default::default()),
2626        };
2627
2628        Self {
2629            target_replica: None,
2630            log_collection: false,
2631            dropped: false,
2632            scheduled: false,
2633            read_only: true,
2634            shared,
2635            implied_read_hold,
2636            warmup_read_hold,
2637            read_policy: Some(ReadPolicy::ValidFrom(since)),
2638            storage_dependencies,
2639            compute_dependencies,
2640            introspection,
2641            wallclock_lag_histogram_stash,
2642        }
2643    }
2644
2645    /// Creates a new collection state for a log collection.
2646    fn new_log_collection(
2647        id: GlobalId,
2648        shared: SharedCollectionState,
2649        read_hold_tx: read_holds::ChangeTx,
2650        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2651    ) -> Self {
2652        let since = Antichain::from_elem(Timestamp::MIN);
2653        let introspection = CollectionIntrospection::new(
2654            id,
2655            introspection_tx,
2656            since.clone(),
2657            false,
2658            None,
2659            None,
2660            Vec::new(),
2661        );
2662        let mut state = Self::new(
2663            id,
2664            since,
2665            shared,
2666            Default::default(),
2667            Default::default(),
2668            read_hold_tx,
2669            introspection,
2670        );
2671        state.log_collection = true;
2672        // Log collections are created and scheduled implicitly as part of replica initialization.
2673        state.scheduled = true;
2674        state
2675    }
2676
2677    /// Reports the current read frontier.
2678    fn read_frontier(&self) -> Antichain<Timestamp> {
2679        self.shared
2680            .lock_read_capabilities(|c| c.frontier().to_owned())
2681    }
2682
2683    /// Reports the current write frontier.
2684    fn write_frontier(&self) -> Antichain<Timestamp> {
2685        self.shared.lock_write_frontier(|f| f.clone())
2686    }
2687
2688    fn storage_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2689        self.storage_dependencies.keys().copied()
2690    }
2691
2692    fn compute_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2693        self.compute_dependencies.keys().copied()
2694    }
2695}
2696
2697/// Collection state shared with the `ComputeController`.
2698///
2699/// Having this allows certain controller APIs, such as `ComputeController::collection_frontiers`
2700/// and `ComputeController::acquire_read_hold` to be non-`async`. This comes at the cost of
2701/// complexity (by introducing shared mutable state) and performance (by introducing locking). We
2702/// should aim to reduce the amount of shared state over time, rather than expand it.
2703///
2704/// Note that [`SharedCollectionState`]s are initialized by the `ComputeController` prior to the
2705/// collection's creation in the [`Instance`]. This is to allow compute clients to query frontiers
2706/// and take new read holds immediately, without having to wait for the [`Instance`] to update.
2707#[derive(Clone, Debug)]
2708pub(super) struct SharedCollectionState {
2709    /// Accumulation of read capabilities for the collection.
2710    ///
2711    /// This accumulation contains the capabilities held by all [`ReadHold`]s given out for the
2712    /// collection, including `implied_read_hold` and `warmup_read_hold`.
2713    ///
2714    /// NOTE: This field may only be modified by [`Instance::apply_read_hold_change`],
2715    /// [`Instance::acquire_read_hold`], and `ComputeController::acquire_read_hold`.
2716    /// Nobody else should modify read capabilities directly. Instead, collection users should
2717    /// manage read holds through [`ReadHold`] objects acquired through
2718    /// `ComputeController::acquire_read_hold`.
2719    ///
2720    /// TODO(teskje): Restructure the code to enforce the above in the type system.
2721    read_capabilities: Arc<Mutex<MutableAntichain<Timestamp>>>,
2722    /// The write frontier of this collection.
2723    write_frontier: Arc<Mutex<Antichain<Timestamp>>>,
2724}
2725
2726impl SharedCollectionState {
2727    pub fn new(as_of: Antichain<Timestamp>) -> Self {
2728        // A collection is not readable before the `as_of`.
2729        let since = as_of.clone();
2730        // A collection won't produce updates for times before the `as_of`.
2731        let upper = as_of;
2732
2733        // Initialize read capabilities to the `since`.
2734        // The is the implied read capability. The corresponding [`ReadHold`] is created in
2735        // [`CollectionState::new`].
2736        let mut read_capabilities = MutableAntichain::new();
2737        read_capabilities.update_iter(since.iter().map(|time| (time.clone(), 1)));
2738
2739        Self {
2740            read_capabilities: Arc::new(Mutex::new(read_capabilities)),
2741            write_frontier: Arc::new(Mutex::new(upper)),
2742        }
2743    }
2744
2745    pub fn lock_read_capabilities<F, R>(&self, f: F) -> R
2746    where
2747        F: FnOnce(&mut MutableAntichain<Timestamp>) -> R,
2748    {
2749        let mut caps = self.read_capabilities.lock().expect("poisoned");
2750        f(&mut *caps)
2751    }
2752
2753    pub fn lock_write_frontier<F, R>(&self, f: F) -> R
2754    where
2755        F: FnOnce(&mut Antichain<Timestamp>) -> R,
2756    {
2757        let mut frontier = self.write_frontier.lock().expect("poisoned");
2758        f(&mut *frontier)
2759    }
2760}
2761
2762/// Manages certain introspection relations associated with a collection. Upon creation, it adds
2763/// rows to introspection relations. When dropped, it retracts its managed rows.
2764#[derive(Debug)]
2765struct CollectionIntrospection {
2766    /// The ID of the compute collection.
2767    collection_id: GlobalId,
2768    /// A channel through which introspection updates are delivered.
2769    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2770    /// Introspection state for `IntrospectionType::Frontiers`.
2771    ///
2772    /// `Some` if the collection does _not_ sink into a storage collection (i.e. is not an MV). If
2773    /// the collection sinks into storage, the storage controller reports its frontiers instead.
2774    frontiers: Option<FrontiersIntrospectionState>,
2775    /// Introspection state for `IntrospectionType::ComputeMaterializedViewRefreshes`.
2776    ///
2777    /// `Some` if the collection is a REFRESH MV.
2778    refresh: Option<RefreshIntrospectionState>,
2779    /// The IDs of the collection's dependencies, for `IntrospectionType::ComputeDependencies`.
2780    dependency_ids: Vec<GlobalId>,
2781}
2782
2783impl CollectionIntrospection {
2784    fn new(
2785        collection_id: GlobalId,
2786        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2787        as_of: Antichain<Timestamp>,
2788        storage_sink: bool,
2789        initial_as_of: Option<Antichain<Timestamp>>,
2790        refresh_schedule: Option<RefreshSchedule>,
2791        dependency_ids: Vec<GlobalId>,
2792    ) -> Self {
2793        let refresh =
2794            match (refresh_schedule, initial_as_of) {
2795                (Some(refresh_schedule), Some(initial_as_of)) => Some(
2796                    RefreshIntrospectionState::new(refresh_schedule, initial_as_of, &as_of),
2797                ),
2798                (refresh_schedule, _) => {
2799                    // If we have a `refresh_schedule`, then the collection is a MV, so we should also have
2800                    // an `initial_as_of`.
2801                    soft_assert_or_log!(
2802                        refresh_schedule.is_none(),
2803                        "`refresh_schedule` without an `initial_as_of`: {collection_id}"
2804                    );
2805                    None
2806                }
2807            };
2808        let frontiers = (!storage_sink).then(|| FrontiersIntrospectionState::new(as_of));
2809
2810        let self_ = Self {
2811            collection_id,
2812            introspection_tx,
2813            frontiers,
2814            refresh,
2815            dependency_ids,
2816        };
2817
2818        self_.report_initial_state();
2819        self_
2820    }
2821
2822    /// Reports the initial introspection state.
2823    fn report_initial_state(&self) {
2824        if let Some(frontiers) = &self.frontiers {
2825            let row = frontiers.row_for_collection(self.collection_id);
2826            let updates = vec![(row, Diff::ONE)];
2827            self.send(IntrospectionType::Frontiers, updates);
2828        }
2829
2830        if let Some(refresh) = &self.refresh {
2831            let row = refresh.row_for_collection(self.collection_id);
2832            let updates = vec![(row, Diff::ONE)];
2833            self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2834        }
2835
2836        if !self.dependency_ids.is_empty() {
2837            let updates = self.dependency_rows(Diff::ONE);
2838            self.send(IntrospectionType::ComputeDependencies, updates);
2839        }
2840    }
2841
2842    /// Produces rows for the `ComputeDependencies` introspection relation.
2843    fn dependency_rows(&self, diff: Diff) -> Vec<(Row, Diff)> {
2844        self.dependency_ids
2845            .iter()
2846            .map(|dependency_id| {
2847                let row = Row::pack_slice(&[
2848                    Datum::String(&self.collection_id.to_string()),
2849                    Datum::String(&dependency_id.to_string()),
2850                ]);
2851                (row, diff)
2852            })
2853            .collect()
2854    }
2855
2856    /// Observe the given current collection frontiers and update the introspection state as
2857    /// necessary.
2858    fn observe_frontiers(
2859        &mut self,
2860        read_frontier: &Antichain<Timestamp>,
2861        write_frontier: &Antichain<Timestamp>,
2862    ) {
2863        self.update_frontier_introspection(read_frontier, write_frontier);
2864        self.update_refresh_introspection(write_frontier);
2865    }
2866
2867    fn update_frontier_introspection(
2868        &mut self,
2869        read_frontier: &Antichain<Timestamp>,
2870        write_frontier: &Antichain<Timestamp>,
2871    ) {
2872        let Some(frontiers) = &mut self.frontiers else {
2873            return;
2874        };
2875
2876        if &frontiers.read_frontier == read_frontier && &frontiers.write_frontier == write_frontier
2877        {
2878            return; // no change
2879        };
2880
2881        let retraction = frontiers.row_for_collection(self.collection_id);
2882        frontiers.update(read_frontier, write_frontier);
2883        let insertion = frontiers.row_for_collection(self.collection_id);
2884        let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2885        self.send(IntrospectionType::Frontiers, updates);
2886    }
2887
2888    fn update_refresh_introspection(&mut self, write_frontier: &Antichain<Timestamp>) {
2889        let Some(refresh) = &mut self.refresh else {
2890            return;
2891        };
2892
2893        let retraction = refresh.row_for_collection(self.collection_id);
2894        refresh.frontier_update(write_frontier);
2895        let insertion = refresh.row_for_collection(self.collection_id);
2896
2897        if retraction == insertion {
2898            return; // no change
2899        }
2900
2901        let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2902        self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2903    }
2904
2905    fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
2906        // Failure to send means the `ComputeController` has been dropped and doesn't care about
2907        // introspection updates anymore.
2908        let _ = self.introspection_tx.send((introspection_type, updates));
2909    }
2910}
2911
2912impl Drop for CollectionIntrospection {
2913    fn drop(&mut self) {
2914        // Retract collection frontiers.
2915        if let Some(frontiers) = &self.frontiers {
2916            let row = frontiers.row_for_collection(self.collection_id);
2917            let updates = vec![(row, Diff::MINUS_ONE)];
2918            self.send(IntrospectionType::Frontiers, updates);
2919        }
2920
2921        // Retract MV refresh state.
2922        if let Some(refresh) = &self.refresh {
2923            let retraction = refresh.row_for_collection(self.collection_id);
2924            let updates = vec![(retraction, Diff::MINUS_ONE)];
2925            self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2926        }
2927
2928        // Retract collection dependencies.
2929        if !self.dependency_ids.is_empty() {
2930            let updates = self.dependency_rows(Diff::MINUS_ONE);
2931            self.send(IntrospectionType::ComputeDependencies, updates);
2932        }
2933    }
2934}
2935
2936#[derive(Debug)]
2937struct FrontiersIntrospectionState {
2938    read_frontier: Antichain<Timestamp>,
2939    write_frontier: Antichain<Timestamp>,
2940}
2941
2942impl FrontiersIntrospectionState {
2943    fn new(as_of: Antichain<Timestamp>) -> Self {
2944        Self {
2945            read_frontier: as_of.clone(),
2946            write_frontier: as_of,
2947        }
2948    }
2949
2950    /// Return a `Row` reflecting the current collection frontiers.
2951    fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2952        let read_frontier = self
2953            .read_frontier
2954            .as_option()
2955            .map_or(Datum::Null, |ts| ts.clone().into());
2956        let write_frontier = self
2957            .write_frontier
2958            .as_option()
2959            .map_or(Datum::Null, |ts| ts.clone().into());
2960        Row::pack_slice(&[
2961            Datum::String(&collection_id.to_string()),
2962            read_frontier,
2963            write_frontier,
2964        ])
2965    }
2966
2967    /// Update the introspection state with the given new frontiers.
2968    fn update(
2969        &mut self,
2970        read_frontier: &Antichain<Timestamp>,
2971        write_frontier: &Antichain<Timestamp>,
2972    ) {
2973        if read_frontier != &self.read_frontier {
2974            self.read_frontier.clone_from(read_frontier);
2975        }
2976        if write_frontier != &self.write_frontier {
2977            self.write_frontier.clone_from(write_frontier);
2978        }
2979    }
2980}
2981
2982/// Information needed to compute introspection updates for a REFRESH materialized view when the
2983/// write frontier advances.
2984#[derive(Debug)]
2985struct RefreshIntrospectionState {
2986    // Immutable properties of the MV
2987    refresh_schedule: RefreshSchedule,
2988    initial_as_of: Antichain<Timestamp>,
2989    // Refresh state
2990    next_refresh: Datum<'static>,           // Null or an MzTimestamp
2991    last_completed_refresh: Datum<'static>, // Null or an MzTimestamp
2992}
2993
2994impl RefreshIntrospectionState {
2995    /// Return a `Row` reflecting the current refresh introspection state.
2996    fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2997        Row::pack_slice(&[
2998            Datum::String(&collection_id.to_string()),
2999            self.last_completed_refresh,
3000            self.next_refresh,
3001        ])
3002    }
3003}
3004
3005impl RefreshIntrospectionState {
3006    /// Construct a new [`RefreshIntrospectionState`], and apply an initial `frontier_update()` at
3007    /// the `upper`.
3008    fn new(
3009        refresh_schedule: RefreshSchedule,
3010        initial_as_of: Antichain<Timestamp>,
3011        upper: &Antichain<Timestamp>,
3012    ) -> Self {
3013        let mut self_ = Self {
3014            refresh_schedule: refresh_schedule.clone(),
3015            initial_as_of: initial_as_of.clone(),
3016            next_refresh: Datum::Null,
3017            last_completed_refresh: Datum::Null,
3018        };
3019        self_.frontier_update(upper);
3020        self_
3021    }
3022
3023    /// Should be called whenever the write frontier of the collection advances. It updates the
3024    /// state that should be recorded in introspection relations, but doesn't send the updates yet.
3025    fn frontier_update(&mut self, write_frontier: &Antichain<Timestamp>) {
3026        if write_frontier.is_empty() {
3027            self.last_completed_refresh =
3028                if let Some(last_refresh) = self.refresh_schedule.last_refresh() {
3029                    last_refresh.into()
3030                } else {
3031                    // If there is no last refresh, then we have a `REFRESH EVERY`, in which case
3032                    // the saturating roundup puts a refresh at the maximum possible timestamp.
3033                    Timestamp::MAX.into()
3034                };
3035            self.next_refresh = Datum::Null;
3036        } else {
3037            if PartialOrder::less_equal(write_frontier, &self.initial_as_of) {
3038                // We are before the first refresh.
3039                self.last_completed_refresh = Datum::Null;
3040                let initial_as_of = self.initial_as_of.as_option().expect(
3041                    "initial_as_of can't be [], because then there would be no refreshes at all",
3042                );
3043                let first_refresh = self
3044                    .refresh_schedule
3045                    .round_up_timestamp(*initial_as_of)
3046                    .expect("sequencing makes sure that REFRESH MVs always have a first refresh");
3047                soft_assert_or_log!(
3048                    first_refresh == *initial_as_of,
3049                    "initial_as_of should be set to the first refresh"
3050                );
3051                self.next_refresh = first_refresh.into();
3052            } else {
3053                // The first refresh has already happened.
3054                let write_frontier = write_frontier.as_option().expect("checked above");
3055                self.last_completed_refresh = self
3056                    .refresh_schedule
3057                    .round_down_timestamp_m1(*write_frontier)
3058                    .map_or_else(
3059                        || {
3060                            soft_panic_or_log!(
3061                                "rounding down should have returned the first refresh or later"
3062                            );
3063                            Datum::Null
3064                        },
3065                        |last_completed_refresh| last_completed_refresh.into(),
3066                    );
3067                self.next_refresh = write_frontier.clone().into();
3068            }
3069        }
3070    }
3071}
3072
3073/// A note of an outstanding peek response.
3074#[derive(Debug)]
3075struct PendingPeek {
3076    /// For replica-targeted peeks, this specifies the replica whose response we should pass on.
3077    ///
3078    /// If this value is `None`, we pass on the first response.
3079    target_replica: Option<ReplicaId>,
3080    /// The OpenTelemetry context for this peek.
3081    otel_ctx: OpenTelemetryContext,
3082    /// The time at which the peek was requested.
3083    ///
3084    /// Used to track peek durations.
3085    requested_at: Instant,
3086    /// The read hold installed to serve this peek.
3087    read_hold: ReadHold,
3088    /// The channel to send peek results.
3089    peek_response_tx: oneshot::Sender<PeekResponse>,
3090    /// An optional limit of the peek's result size.
3091    limit: Option<usize>,
3092    /// The offset into the peek's result.
3093    offset: usize,
3094}
3095
3096#[derive(Debug, Clone)]
3097struct ActiveSubscribe {
3098    /// Current upper frontier of this subscribe.
3099    frontier: Antichain<Timestamp>,
3100}
3101
3102impl Default for ActiveSubscribe {
3103    fn default() -> Self {
3104        Self {
3105            frontier: Antichain::from_elem(Timestamp::MIN),
3106        }
3107    }
3108}
3109
3110/// State maintained about individual replicas.
3111#[derive(Debug)]
3112struct ReplicaState {
3113    /// The ID of the replica.
3114    id: ReplicaId,
3115    /// Client for the running replica task.
3116    client: ReplicaClient,
3117    /// The replica configuration.
3118    config: ReplicaConfig,
3119    /// Replica metrics.
3120    metrics: ReplicaMetrics,
3121    /// A channel through which introspection updates are delivered.
3122    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3123    /// Per-replica collection state.
3124    collections: BTreeMap<GlobalId, ReplicaCollectionState>,
3125    /// The epoch of the replica.
3126    epoch: u64,
3127    /// The replica's dyncfg override, merged into every configuration command sent or replayed
3128    /// to it. Holds only the scoped values that differ from the environment-wide configuration,
3129    /// and is empty for a replica without scoped parameters.
3130    dyncfg_override: ConfigUpdates,
3131}
3132
3133impl ReplicaState {
3134    fn new(
3135        id: ReplicaId,
3136        client: ReplicaClient,
3137        config: ReplicaConfig,
3138        metrics: ReplicaMetrics,
3139        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3140        epoch: u64,
3141        dyncfg_override: ConfigUpdates,
3142    ) -> Self {
3143        Self {
3144            id,
3145            client,
3146            config,
3147            metrics,
3148            introspection_tx,
3149            epoch,
3150            dyncfg_override,
3151            collections: Default::default(),
3152        }
3153    }
3154
3155    /// Add a collection to the replica state.
3156    ///
3157    /// # Panics
3158    ///
3159    /// Panics if a collection with the same ID exists already.
3160    fn add_collection(
3161        &mut self,
3162        id: GlobalId,
3163        as_of: Antichain<Timestamp>,
3164        input_read_holds: Vec<ReadHold>,
3165    ) {
3166        let metrics = self.metrics.for_collection(id);
3167        let introspection = ReplicaCollectionIntrospection::new(
3168            self.id,
3169            id,
3170            self.introspection_tx.clone(),
3171            as_of.clone(),
3172        );
3173        let mut state =
3174            ReplicaCollectionState::new(metrics, as_of, introspection, input_read_holds);
3175
3176        // In an effort to keep the produced wallclock lag introspection data small and
3177        // predictable, we disable wallclock lag tracking for transient collections, i.e. slow-path
3178        // select indexes and subscribes.
3179        if id.is_transient() {
3180            state.wallclock_lag_max = None;
3181        }
3182
3183        if let Some(previous) = self.collections.insert(id, state) {
3184            panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
3185        }
3186    }
3187
3188    /// Remove state for a collection.
3189    fn remove_collection(&mut self, id: GlobalId) -> Option<ReplicaCollectionState> {
3190        self.collections.remove(&id)
3191    }
3192
3193    /// Returns the per-replica state of a collection this replica hosts.
3194    ///
3195    /// # Panics
3196    ///
3197    /// Panics if the replica does not host the collection. Callers obtain the
3198    /// replica from [`Instance::replicas_hosting`], which guarantees it does.
3199    fn expect_collection(&self, id: GlobalId) -> &ReplicaCollectionState {
3200        self.collections
3201            .get(&id)
3202            .expect("hosting replica must have per-replica collection state")
3203    }
3204
3205    /// Returns whether all replica frontiers of the given collection are empty.
3206    fn collection_frontiers_empty(&self, id: GlobalId) -> bool {
3207        self.collections.get(&id).map_or(true, |c| {
3208            c.write_frontier.is_empty()
3209                && c.input_frontier.is_empty()
3210                && c.output_frontier.is_empty()
3211        })
3212    }
3213
3214    /// Returns the state of the [`ReplicaState`] formatted as JSON.
3215    ///
3216    /// The returned value is not guaranteed to be stable and may change at any point in time.
3217    #[mz_ore::instrument(level = "debug")]
3218    pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
3219        // Note: We purposefully use the `Debug` formatting for the value of all fields in the
3220        // returned object as a tradeoff between usability and stability. `serde_json` will fail
3221        // to serialize an object if the keys aren't strings, so `Debug` formatting the values
3222        // prevents a future unrelated change from silently breaking this method.
3223
3224        // Destructure `self` here so we don't forget to consider dumping newly added fields.
3225        let Self {
3226            id,
3227            client: _,
3228            config: _,
3229            metrics: _,
3230            introspection_tx: _,
3231            epoch,
3232            dyncfg_override,
3233            collections,
3234        } = self;
3235
3236        let collections: BTreeMap<_, _> = collections
3237            .iter()
3238            .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
3239            .collect();
3240
3241        Ok(serde_json::json!({
3242            "id": id.to_string(),
3243            "collections": collections,
3244            "epoch": epoch,
3245            "dyncfg_override": format!("{dyncfg_override:?}"),
3246        }))
3247    }
3248}
3249
3250#[derive(Debug)]
3251struct ReplicaCollectionState {
3252    /// The replica write frontier of this collection.
3253    ///
3254    /// See [`FrontiersResponse::write_frontier`].
3255    write_frontier: Antichain<Timestamp>,
3256    /// The replica input frontier of this collection.
3257    ///
3258    /// See [`FrontiersResponse::input_frontier`].
3259    input_frontier: Antichain<Timestamp>,
3260    /// The replica output frontier of this collection.
3261    ///
3262    /// See [`FrontiersResponse::output_frontier`].
3263    output_frontier: Antichain<Timestamp>,
3264
3265    /// Metrics tracked for this collection.
3266    ///
3267    /// If this is `None`, no metrics are collected.
3268    metrics: Option<ReplicaCollectionMetrics>,
3269    /// As-of frontier with which this collection was installed on the replica.
3270    as_of: Antichain<Timestamp>,
3271    /// Tracks introspection state for this collection.
3272    introspection: ReplicaCollectionIntrospection,
3273    /// Read holds on storage inputs to this collection.
3274    ///
3275    /// These read holds are kept to ensure that the replica is able to read from storage inputs at
3276    /// all times it hasn't read yet. We only need to install read holds for storage inputs since
3277    /// compaction of compute inputs is implicitly held back by Timely/DD.
3278    input_read_holds: Vec<ReadHold>,
3279
3280    /// Maximum frontier wallclock lag since the last `WallclockLagHistory` introspection update.
3281    ///
3282    /// If this is `None`, wallclock lag is not tracked for this collection.
3283    wallclock_lag_max: Option<WallclockLag>,
3284}
3285
3286impl ReplicaCollectionState {
3287    fn new(
3288        metrics: Option<ReplicaCollectionMetrics>,
3289        as_of: Antichain<Timestamp>,
3290        introspection: ReplicaCollectionIntrospection,
3291        input_read_holds: Vec<ReadHold>,
3292    ) -> Self {
3293        Self {
3294            write_frontier: as_of.clone(),
3295            input_frontier: as_of.clone(),
3296            output_frontier: as_of.clone(),
3297            metrics,
3298            as_of,
3299            introspection,
3300            input_read_holds,
3301            wallclock_lag_max: Some(WallclockLag::MIN),
3302        }
3303    }
3304
3305    /// Returns whether this collection is hydrated.
3306    fn hydrated(&self) -> bool {
3307        // If the observed frontier is greater than the collection's as-of, the collection has
3308        // produced some output and is therefore hydrated.
3309        //
3310        // We need to consider the edge case where the as-of is the empty frontier. Such an as-of
3311        // is not useful for indexes, because they wouldn't be readable. For write-only
3312        // collections, an empty as-of means that the collection has been fully written and no new
3313        // dataflow needs to be created for it. Consequently, no hydration will happen either.
3314        //
3315        // Based on this, we could respond in two ways:
3316        //  * `false`, as in "the dataflow was never created"
3317        //  * `true`, as in "the dataflow completed immediately"
3318        //
3319        // Since hydration is often used as a measure of dataflow progress and we don't want to
3320        // give the impression that certain dataflows are somehow stuck when they are not, we go
3321        // with the second interpretation here.
3322        self.as_of.is_empty() || PartialOrder::less_than(&self.as_of, &self.output_frontier)
3323    }
3324
3325    /// Updates the replica write frontier of this collection.
3326    fn update_write_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3327        if PartialOrder::less_than(&new_frontier, &self.write_frontier) {
3328            soft_panic_or_log!(
3329                "replica collection write frontier regression (old={:?}, new={new_frontier:?})",
3330                self.write_frontier,
3331            );
3332            return;
3333        } else if new_frontier == self.write_frontier {
3334            return;
3335        }
3336
3337        self.write_frontier = new_frontier;
3338    }
3339
3340    /// Updates the replica input frontier of this collection.
3341    fn update_input_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3342        if PartialOrder::less_than(&new_frontier, &self.input_frontier) {
3343            soft_panic_or_log!(
3344                "replica collection input frontier regression (old={:?}, new={new_frontier:?})",
3345                self.input_frontier,
3346            );
3347            return;
3348        } else if new_frontier == self.input_frontier {
3349            return;
3350        }
3351
3352        self.input_frontier = new_frontier;
3353
3354        // Relax our read holds on collection inputs.
3355        for read_hold in &mut self.input_read_holds {
3356            let result = read_hold.try_downgrade(self.input_frontier.clone());
3357            soft_assert_or_log!(
3358                result.is_ok(),
3359                "read hold downgrade failed (read_hold={read_hold:?}, new_since={:?})",
3360                self.input_frontier,
3361            );
3362        }
3363    }
3364
3365    /// Updates the replica output frontier of this collection.
3366    fn update_output_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3367        if PartialOrder::less_than(&new_frontier, &self.output_frontier) {
3368            soft_panic_or_log!(
3369                "replica collection output frontier regression (old={:?}, new={new_frontier:?})",
3370                self.output_frontier,
3371            );
3372            return;
3373        } else if new_frontier == self.output_frontier {
3374            return;
3375        }
3376
3377        self.output_frontier = new_frontier;
3378    }
3379}
3380
3381/// Maintains the introspection state for a given replica and collection, and ensures that reported
3382/// introspection data is retracted when the collection is dropped.
3383#[derive(Debug)]
3384struct ReplicaCollectionIntrospection {
3385    /// The ID of the replica.
3386    replica_id: ReplicaId,
3387    /// The ID of the compute collection.
3388    collection_id: GlobalId,
3389    /// The collection's reported replica write frontier.
3390    write_frontier: Antichain<Timestamp>,
3391    /// A channel through which introspection updates are delivered.
3392    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3393}
3394
3395impl ReplicaCollectionIntrospection {
3396    /// Create a new `HydrationState` and initialize introspection.
3397    fn new(
3398        replica_id: ReplicaId,
3399        collection_id: GlobalId,
3400        introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3401        as_of: Antichain<Timestamp>,
3402    ) -> Self {
3403        let self_ = Self {
3404            replica_id,
3405            collection_id,
3406            write_frontier: as_of,
3407            introspection_tx,
3408        };
3409
3410        self_.report_initial_state();
3411        self_
3412    }
3413
3414    /// Reports the initial introspection state.
3415    fn report_initial_state(&self) {
3416        let row = self.write_frontier_row();
3417        let updates = vec![(row, Diff::ONE)];
3418        self.send(IntrospectionType::ReplicaFrontiers, updates);
3419    }
3420
3421    /// Observe the given current write frontier and update the introspection state as necessary.
3422    fn observe_frontier(&mut self, write_frontier: &Antichain<Timestamp>) {
3423        if self.write_frontier == *write_frontier {
3424            return; // no change
3425        }
3426
3427        let retraction = self.write_frontier_row();
3428        self.write_frontier.clone_from(write_frontier);
3429        let insertion = self.write_frontier_row();
3430
3431        let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
3432        self.send(IntrospectionType::ReplicaFrontiers, updates);
3433    }
3434
3435    /// Return a `Row` reflecting the current replica write frontier.
3436    fn write_frontier_row(&self) -> Row {
3437        let write_frontier = self
3438            .write_frontier
3439            .as_option()
3440            .map_or(Datum::Null, |ts| ts.clone().into());
3441        Row::pack_slice(&[
3442            Datum::String(&self.collection_id.to_string()),
3443            Datum::String(&self.replica_id.to_string()),
3444            write_frontier,
3445        ])
3446    }
3447
3448    fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
3449        // Failure to send means the `ComputeController` has been dropped and doesn't care about
3450        // introspection updates anymore.
3451        let _ = self.introspection_tx.send((introspection_type, updates));
3452    }
3453}
3454
3455impl Drop for ReplicaCollectionIntrospection {
3456    fn drop(&mut self) {
3457        // Retract the write frontier.
3458        let row = self.write_frontier_row();
3459        let updates = vec![(row, Diff::MINUS_ONE)];
3460        self.send(IntrospectionType::ReplicaFrontiers, updates);
3461    }
3462}
3463
3464/// Classifies one collection's readiness over every replica hosting it.
3465///
3466/// Hydration and sufficient progress must belong to the same target replica.
3467/// No reference replicas yields `MIN`, while a completed reference yields the
3468/// empty frontier. Those cases must remain distinct.
3469fn classify_collection_readiness<'a, I>(
3470    replicas: I,
3471    target_replica_ids: &BTreeSet<ReplicaId>,
3472    reference_replica_ids: &BTreeSet<ReplicaId>,
3473    allowed_lag: Option<Timestamp>,
3474) -> CollectionReadiness
3475where
3476    I: Iterator<Item = (ReplicaId, &'a ReplicaCollectionState)> + Clone,
3477{
3478    let lag = allowed_lag.map(|allowed_lag| {
3479        let mut reference = Antichain::from_elem(Timestamp::MIN);
3480        for (_, state) in replicas
3481            .clone()
3482            .filter(|(id, _)| reference_replica_ids.contains(id))
3483        {
3484            reference.join_assign(&state.output_frontier);
3485        }
3486        (reference, allowed_lag)
3487    });
3488
3489    let mut result = CollectionReadiness::Unhydrated;
3490    for (_, state) in replicas.filter(|(id, _)| target_replica_ids.contains(id)) {
3491        match CollectionReadiness::classify(
3492            state.hydrated(),
3493            &state.output_frontier,
3494            lag.as_ref().map(|(reference, lag)| (reference, *lag)),
3495        ) {
3496            CollectionReadiness::Ready => return CollectionReadiness::Ready,
3497            CollectionReadiness::Lagging { lag } => {
3498                // Report the closest hydrated target, since any ready target
3499                // suffices. A finite gap is closer than awaiting completion.
3500                let lag = match result {
3501                    CollectionReadiness::Lagging {
3502                        lag: Some(previous),
3503                    } => Some(previous.min(lag.unwrap_or(u64::MAX))),
3504                    _ => lag,
3505                };
3506                result = CollectionReadiness::Lagging { lag };
3507            }
3508            CollectionReadiness::Unhydrated => {}
3509        }
3510    }
3511    result
3512}
3513
3514#[cfg(test)]
3515mod tests {
3516    use std::collections::BTreeSet;
3517
3518    use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE};
3519    use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
3520    use mz_persist_types::PersistLocation;
3521    use mz_repr::{GlobalId, Timestamp};
3522    use timely::progress::Antichain;
3523    use tokio::sync::mpsc;
3524
3525    use crate::protocol::command::{ComputeCommand, InstanceConfig};
3526
3527    use super::{
3528        CollectionReadiness, Instance, ReplicaCollectionIntrospection, ReplicaCollectionState,
3529        ReplicaId, classify_collection_readiness,
3530    };
3531
3532    fn ac(ts: u64) -> Antichain<Timestamp> {
3533        Antichain::from_elem(Timestamp::new(ts))
3534    }
3535
3536    fn state(id: ReplicaId, as_of: u64, write: u64, output: u64) -> ReplicaCollectionState {
3537        let (tx, _rx) = mpsc::unbounded_channel();
3538        let introspection =
3539            ReplicaCollectionIntrospection::new(id, GlobalId::User(1), tx, ac(as_of));
3540        let mut state = ReplicaCollectionState::new(None, ac(as_of), introspection, Vec::new());
3541        state.update_write_frontier(ac(write));
3542        state.update_output_frontier(ac(output));
3543        state
3544    }
3545
3546    fn classify(
3547        replicas: &[(ReplicaId, ReplicaCollectionState)],
3548        targets: &[ReplicaId],
3549        references: &[ReplicaId],
3550        lag: Option<Timestamp>,
3551    ) -> CollectionReadiness {
3552        classify_collection_readiness(
3553            replicas.iter().map(|(id, state)| (*id, state)),
3554            &targets.iter().copied().collect::<BTreeSet<_>>(),
3555            &references.iter().copied().collect::<BTreeSet<_>>(),
3556            lag,
3557        )
3558    }
3559
3560    const LAG: Option<Timestamp> = Some(Timestamp::new(60));
3561
3562    /// The regression the lag gate exists for: a pending replica whose dataflow
3563    /// has produced its first output past the as-of, and so reports hydrated,
3564    /// while its output frontier is still far behind the outgoing replica.
3565    #[mz_ore::test]
3566    fn shared_write_does_not_hide_output_lag() {
3567        let reference = ReplicaId::User(1);
3568        let target = ReplicaId::User(2);
3569        let mut replicas = vec![
3570            (reference, state(reference, 100, 10_000, 10_000)),
3571            // The shared MV write frontier is caught up, but this replica has
3572            // only just produced its first output past the as-of.
3573            (target, state(target, 100, 10_000, 101)),
3574        ];
3575        assert_eq!(
3576            classify(&replicas, &[target], &[reference], LAG),
3577            CollectionReadiness::Lagging { lag: Some(9_899) },
3578        );
3579        replicas[1].1.update_output_frontier(ac(9_940));
3580        assert_eq!(
3581            classify(&replicas, &[target], &[reference], LAG),
3582            CollectionReadiness::Ready,
3583        );
3584    }
3585
3586    #[mz_ore::test]
3587    fn refresh_writes_ahead_of_outputs_is_ready() {
3588        let reference = ReplicaId::User(1);
3589        let target = ReplicaId::User(2);
3590        let replicas = vec![
3591            (reference, state(reference, 100, 20_000, 10_000)),
3592            (target, state(target, 100, 20_000, 10_000)),
3593        ];
3594        assert_eq!(
3595            classify(&replicas, &[target], &[reference], LAG),
3596            CollectionReadiness::Ready,
3597        );
3598    }
3599
3600    #[mz_ore::test]
3601    fn furthest_reference_and_bystander_exclusion() {
3602        let trailing = ReplicaId::User(1);
3603        let ahead = ReplicaId::User(2);
3604        let target = ReplicaId::User(3);
3605        let bystander = ReplicaId::User(4);
3606        let replicas = vec![
3607            (trailing, state(trailing, 100, 9_000, 9_000)),
3608            (ahead, state(ahead, 100, 10_000, 10_000)),
3609            (target, state(target, 100, 8_990, 8_990)),
3610            (bystander, state(bystander, 100, 1_000_000, 1_000_000)),
3611        ];
3612        assert_eq!(
3613            classify(&replicas, &[target], &[trailing], LAG),
3614            CollectionReadiness::Ready,
3615        );
3616        assert_eq!(
3617            classify(&replicas, &[target], &[trailing, ahead], LAG),
3618            CollectionReadiness::Lagging { lag: Some(1_010) },
3619        );
3620    }
3621
3622    #[mz_ore::test]
3623    fn one_ready_target_suffices() {
3624        let reference = ReplicaId::User(1);
3625        let unhydrated = ReplicaId::User(2);
3626        let lagging = ReplicaId::User(3);
3627        let ready = ReplicaId::User(4);
3628        let replicas = vec![
3629            (reference, state(reference, 100, 10_000, 10_000)),
3630            // This replica is within the lag allowance but has not advanced
3631            // past its own as-of. It cannot supply progress for the hydrated,
3632            // lagging replica below.
3633            (unhydrated, state(unhydrated, 10_000, 10_000, 10_000)),
3634            (lagging, state(lagging, 100, 1_000, 1_000)),
3635            (ready, state(ready, 100, 9_990, 9_990)),
3636        ];
3637        assert_eq!(
3638            classify(&replicas, &[unhydrated, lagging, ready], &[reference], LAG),
3639            CollectionReadiness::Ready,
3640        );
3641        assert_eq!(
3642            classify(&replicas, &[unhydrated, lagging], &[reference], LAG),
3643            CollectionReadiness::Lagging { lag: Some(9_000) },
3644        );
3645    }
3646
3647    #[mz_ore::test]
3648    fn no_lag_gate_is_hydration_only() {
3649        let reference = ReplicaId::User(1);
3650        let target = ReplicaId::User(2);
3651        let replicas = vec![
3652            (reference, state(reference, 100, 10_000, 10_000)),
3653            (target, state(target, 100, 101, 101)),
3654        ];
3655        assert_eq!(
3656            classify(&replicas, &[target], &[reference], None),
3657            CollectionReadiness::Ready,
3658        );
3659        let unhydrated = vec![(target, state(target, 100, 100, 100))];
3660        assert_eq!(
3661            classify(&unhydrated, &[target], &[], None),
3662            CollectionReadiness::Unhydrated,
3663        );
3664    }
3665
3666    #[mz_ore::test]
3667    fn lag_reports_the_closest_hydrated_target() {
3668        let reference = ReplicaId::User(1);
3669        let far = ReplicaId::User(2);
3670        let close = ReplicaId::User(3);
3671        let mut replicas = vec![
3672            (reference, state(reference, 100, 10_000, 10_000)),
3673            (far, state(far, 100, 1_000, 1_000)),
3674            (close, state(close, 100, 9_000, 9_000)),
3675        ];
3676        for _ in 0..2 {
3677            assert_eq!(
3678                classify(&replicas, &[far, close], &[reference], LAG),
3679                CollectionReadiness::Lagging { lag: Some(1_000) },
3680            );
3681            replicas.reverse();
3682        }
3683    }
3684
3685    #[mz_ore::test]
3686    fn completed_reference_requires_completed_target() {
3687        let reference = ReplicaId::User(1);
3688        let target = ReplicaId::User(2);
3689        let mut replicas = vec![
3690            (reference, state(reference, 100, 1_000, 1_000)),
3691            (target, state(target, 100, 1_000, 1_000)),
3692        ];
3693        replicas[0].1.update_output_frontier(Antichain::new());
3694        assert_eq!(
3695            classify(&replicas, &[target], &[reference], Some(Timestamp::new(0))),
3696            CollectionReadiness::Lagging { lag: None },
3697        );
3698        replicas[1].1.update_output_frontier(Antichain::new());
3699        assert_eq!(
3700            classify(&replicas, &[target], &[reference], Some(Timestamp::new(0))),
3701            CollectionReadiness::Ready,
3702        );
3703    }
3704
3705    #[mz_ore::test]
3706    fn no_reference_means_nothing_to_regress() {
3707        let target = ReplicaId::User(1);
3708        let replicas = vec![(target, state(target, 100, 5_000, 5_000))];
3709        assert_eq!(
3710            classify(&replicas, &[target], &[], Some(Timestamp::new(0))),
3711            CollectionReadiness::Ready,
3712        );
3713        assert_eq!(
3714            classify(&replicas, &[target], &[target], Some(Timestamp::new(0))),
3715            CollectionReadiness::Ready,
3716        );
3717    }
3718
3719    #[mz_ore::test]
3720    fn no_targets_is_unhydrated() {
3721        let reference = ReplicaId::User(1);
3722        let replicas = vec![(reference, state(reference, 100, 10_000, 10_000))];
3723        assert_eq!(
3724            classify(&replicas, &[], &[reference], LAG),
3725            CollectionReadiness::Unhydrated,
3726        );
3727    }
3728
3729    fn create_instance_command() -> ComputeCommand {
3730        ComputeCommand::CreateInstance(Box::new(InstanceConfig {
3731            logging: Default::default(),
3732            expiration_offset: None,
3733            peek_stash_persist_location: PersistLocation::new_in_mem(),
3734            arrangement_dictionary_compression: false,
3735            initial_config: Default::default(),
3736        }))
3737    }
3738
3739    fn initial_config(cmd: &ComputeCommand) -> &ConfigUpdates {
3740        match cmd {
3741            ComputeCommand::CreateInstance(config) => &config.initial_config,
3742            other => panic!("expected CreateInstance, got {other:?}"),
3743        }
3744    }
3745
3746    /// `CreateInstance` is specialized with a full snapshot of the instance-wide dyncfg, so the
3747    /// replica seeds its worker config at create time rather than waiting for the first
3748    /// `UpdateConfiguration`. This is the regression guard for create-time setup observing dyncfg
3749    /// defaults.
3750    #[mz_ore::test]
3751    fn create_instance_snapshots_instance_wide_dyncfg() {
3752        let dyncfg = ConfigSet::default()
3753            .add(&ENABLE_COLUMN_PAGED_BATCHER)
3754            .add(&ENABLE_MZ_JOIN_CORE);
3755        let mut updates = ConfigUpdates::default();
3756        updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3757        updates.add(&ENABLE_MZ_JOIN_CORE, false);
3758        updates.apply(&dyncfg);
3759
3760        // A replica without an override sees exactly the instance-wide values.
3761        let cmd = Instance::specialize_command_for_replica(
3762            create_instance_command(),
3763            &ConfigUpdates::default(),
3764            &dyncfg,
3765        );
3766        let snapshot = initial_config(&cmd);
3767        assert_eq!(
3768            snapshot.updates.get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3769            Some(&ConfigVal::Bool(true)),
3770        );
3771        assert_eq!(
3772            snapshot.updates.get(ENABLE_MZ_JOIN_CORE.name()),
3773            Some(&ConfigVal::Bool(false)),
3774        );
3775    }
3776
3777    /// A replica-scoped override beats the instance-wide value in the create-time snapshot, so a
3778    /// create-time-frozen scoped flag reaches the replica with its override applied.
3779    #[mz_ore::test]
3780    fn create_instance_snapshot_applies_replica_override() {
3781        let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3782        let mut updates = ConfigUpdates::default();
3783        updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3784        updates.apply(&dyncfg);
3785
3786        let mut override_updates = ConfigUpdates::default();
3787        override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, false);
3788
3789        let cmd = Instance::specialize_command_for_replica(
3790            create_instance_command(),
3791            &override_updates,
3792            &dyncfg,
3793        );
3794        assert_eq!(
3795            initial_config(&cmd)
3796                .updates
3797                .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3798            Some(&ConfigVal::Bool(false)),
3799            "replica override should win over the instance-wide value",
3800        );
3801    }
3802
3803    /// `UpdateConfiguration` continues to merge the replica's override into the update.
3804    #[mz_ore::test]
3805    fn update_configuration_merges_replica_override() {
3806        let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3807
3808        let mut override_updates = ConfigUpdates::default();
3809        override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3810
3811        let cmd = Instance::specialize_command_for_replica(
3812            ComputeCommand::UpdateConfiguration(Box::new(Default::default())),
3813            &override_updates,
3814            &dyncfg,
3815        );
3816        match cmd {
3817            ComputeCommand::UpdateConfiguration(params) => assert_eq!(
3818                params
3819                    .dyncfg_updates
3820                    .updates
3821                    .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3822                Some(&ConfigVal::Bool(true)),
3823            ),
3824            other => panic!("expected UpdateConfiguration, got {other:?}"),
3825        }
3826    }
3827}