Skip to main content

mz_compute_client/
controller.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 that provides an interface to the compute layer, and the storage layer below it.
11//!
12//! The compute controller manages the creation, maintenance, and removal of compute instances.
13//! This involves ensuring the intended service state with the orchestrator, as well as maintaining
14//! a dedicated compute instance controller for each active compute instance.
15//!
16//! For each compute instance, the compute controller curates the creation of indexes and sinks
17//! installed on the instance, the progress of readers through these collections, and their
18//! eventual dropping and resource reclamation.
19//!
20//! The state maintained for a compute instance can be viewed as a partial map from `GlobalId` to
21//! collection. It is an error to use an identifier before it has been "created" with
22//! `create_dataflow()`. Once created, the controller holds a read capability for each output
23//! collection of a dataflow, which is manipulated with `set_read_policy()`. Eventually, a
24//! collection is dropped with `drop_collections()`.
25//!
26//! A dataflow can be in read-only or read-write mode. In read-only mode, the dataflow does not
27//! modify any persistent state. Sending a `allow_write` message to the compute instance will
28//! transition the dataflow to read-write mode, allowing it to write to persistent sinks.
29//!
30//! Created dataflows will prevent the compaction of their inputs, including other compute
31//! collections but also collections managed by the storage layer. Each dataflow input is prevented
32//! from compacting beyond the allowed compaction of each of its outputs, ensuring that we can
33//! recover each dataflow to its current state in case of failure or other reconfiguration.
34
35use std::collections::{BTreeMap, BTreeSet};
36use std::sync::{Arc, Mutex};
37use std::time::Duration;
38
39use mz_build_info::BuildInfo;
40use mz_cluster_client::client::ClusterReplicaLocation;
41use mz_cluster_client::metrics::ControllerMetrics;
42use mz_cluster_client::{ReplicaId, WallclockLagFn};
43use mz_compute_types::ComputeInstanceId;
44use mz_compute_types::config::ComputeReplicaConfig;
45use mz_compute_types::dataflows::DataflowDescription;
46use mz_compute_types::dyncfgs::{
47    COMPUTE_REPLICA_EXPIRATION_OFFSET, ENABLE_ARRANGEMENT_DICTIONARY_COMPRESSION_ALPHA,
48};
49use mz_dyncfg::{ConfigSet, ConfigUpdates};
50use mz_expr::RowSetFinishing;
51use mz_expr::row::RowCollection;
52use mz_ore::cast::CastFrom;
53use mz_ore::metrics::MetricsRegistry;
54use mz_ore::now::NowFn;
55use mz_ore::soft_assert_or_log;
56use mz_ore::tracing::OpenTelemetryContext;
57use mz_persist_types::PersistLocation;
58use mz_repr::{GlobalId, RelationDesc, Row, Timestamp};
59use mz_storage_client::controller::StorageController;
60use mz_storage_types::dyncfgs::ORE_OVERFLOWING_BEHAVIOR;
61use mz_storage_types::read_holds::ReadHold;
62use mz_storage_types::read_policy::ReadPolicy;
63use mz_storage_types::time_dependence::{TimeDependence, TimeDependenceError};
64use prometheus::proto::LabelPair;
65use serde::{Deserialize, Serialize};
66use timely::PartialOrder;
67use timely::progress::Antichain;
68use tokio::sync::{mpsc, oneshot};
69use tokio::time::{self, MissedTickBehavior};
70use uuid::Uuid;
71
72use crate::controller::error::{
73    CollectionLookupError, CollectionMissing, CollectionUpdateError, DataflowCreationError,
74    HydrationCheckBadTarget, InstanceExists, InstanceMissing, PeekError, ReadPolicyError,
75    ReplicaCreationError, ReplicaDropError,
76};
77use crate::controller::instance::{Instance, SharedCollectionState};
78use crate::controller::introspection::{IntrospectionUpdates, spawn_introspection_sink};
79use crate::controller::replica::ReplicaConfig;
80use crate::logging::{LogVariant, LoggingConfig};
81use crate::metrics::ComputeControllerMetrics;
82use crate::protocol::command::{ComputeParameters, PeekTarget};
83use crate::protocol::response::{PeekResponse, SubscribeBatch};
84
85mod instance;
86mod introspection;
87mod replica;
88mod sequential_hydration;
89
90pub mod error;
91pub mod instance_client;
92pub use instance_client::InstanceClient;
93
94pub(crate) type StorageCollections =
95    Arc<dyn mz_storage_client::storage_collections::StorageCollections + Send + Sync>;
96
97/// Responses from the compute controller.
98#[derive(Debug)]
99pub enum ComputeControllerResponse {
100    /// See [`PeekNotification`].
101    PeekNotification(Uuid, PeekNotification, OpenTelemetryContext),
102    /// See [`crate::protocol::response::ComputeResponse::SubscribeResponse`].
103    SubscribeResponse(GlobalId, SubscribeBatch),
104    /// The response from a dataflow containing an `CopyToS3Oneshot` sink.
105    ///
106    /// The `GlobalId` identifies the sink. The `Result` is the response from
107    /// the sink, where an `Ok(n)` indicates that `n` rows were successfully
108    /// copied to S3 and an `Err` indicates that an error was encountered
109    /// during the copy operation.
110    ///
111    /// For a given `CopyToS3Oneshot` sink, there will be at most one `CopyToResponse`
112    /// produced. (The sink may produce no responses if its dataflow is dropped
113    /// before completion.)
114    CopyToResponse(GlobalId, Result<u64, anyhow::Error>),
115    /// A response reporting advancement of a collection's upper frontier.
116    ///
117    /// Once a collection's upper (aka "write frontier") has advanced to beyond a given time, the
118    /// contents of the collection as of that time have been sealed and cannot change anymore.
119    FrontierUpper {
120        /// The ID of a compute collection.
121        id: GlobalId,
122        /// The new upper frontier of the identified compute collection.
123        upper: Antichain<Timestamp>,
124    },
125}
126
127/// Notification and summary of a received and forwarded [`crate::protocol::response::ComputeResponse::PeekResponse`].
128#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
129pub enum PeekNotification {
130    /// Returned rows of a successful peek.
131    Success {
132        /// Number of rows in the returned peek result.
133        rows: u64,
134        /// Size of the returned peek result in bytes.
135        result_size: u64,
136    },
137    /// Error of an unsuccessful peek, including the reason for the error.
138    Error(String),
139    /// The peek was canceled.
140    Canceled,
141}
142
143impl PeekNotification {
144    /// Construct a new [`PeekNotification`] from a [`PeekResponse`]. The `offset` and `limit`
145    /// parameters are used to calculate the number of rows in the peek result.
146    fn new(peek_response: &PeekResponse, offset: usize, limit: Option<usize>) -> Self {
147        match peek_response {
148            PeekResponse::Rows(rows) => {
149                let num_rows = u64::cast_from(RowCollection::offset_limit(
150                    rows.iter().map(|r| r.count()).sum(),
151                    offset,
152                    limit,
153                ));
154                let result_size = u64::cast_from(rows.iter().map(|r| r.byte_len()).sum::<usize>());
155
156                tracing::trace!(?num_rows, ?result_size, "inline peek result");
157
158                Self::Success {
159                    rows: num_rows,
160                    result_size,
161                }
162            }
163            PeekResponse::Stashed(stashed_response) => {
164                let rows = stashed_response.num_rows(offset, limit);
165                let result_size = stashed_response.size_bytes();
166
167                tracing::trace!(?rows, ?result_size, "stashed peek result");
168
169                Self::Success {
170                    rows: u64::cast_from(rows),
171                    result_size: u64::cast_from(result_size),
172                }
173            }
174            PeekResponse::Error(err) => Self::Error(err.clone()),
175            PeekResponse::Canceled => Self::Canceled,
176        }
177    }
178}
179
180/// A controller for the compute layer.
181pub struct ComputeController {
182    instances: BTreeMap<ComputeInstanceId, InstanceState>,
183    /// A map from an instance ID to an arbitrary string that describes the
184    /// class of the workload that compute instance is running (e.g.,
185    /// `production` or `staging`).
186    instance_workload_classes: Arc<Mutex<BTreeMap<ComputeInstanceId, Option<String>>>>,
187    build_info: &'static BuildInfo,
188    /// A handle providing access to storage collections.
189    storage_collections: StorageCollections,
190    /// Set to `true` once `initialization_complete` has been called.
191    initialized: bool,
192    /// Whether or not this controller is in read-only mode.
193    ///
194    /// When in read-only mode, neither this controller nor the instances
195    /// controlled by it are allowed to affect changes to external systems
196    /// (largely persist).
197    read_only: bool,
198    /// Compute configuration to apply to new instances.
199    config: ComputeParameters,
200    /// The persist location where we can stash large peek results.
201    peek_stash_persist_location: PersistLocation,
202    /// A controller response to be returned on the next call to [`ComputeController::process`].
203    stashed_response: Option<ComputeControllerResponse>,
204    /// The compute controller metrics.
205    metrics: ComputeControllerMetrics,
206    /// A function that produces the current wallclock time.
207    now: NowFn,
208    /// A function that computes the lag between the given time and wallclock time.
209    wallclock_lag: WallclockLagFn<Timestamp>,
210    /// Dynamic system configuration.
211    ///
212    /// Updated through `ComputeController::update_configuration` calls and shared with all
213    /// subcomponents of the compute controller.
214    dyncfg: Arc<ConfigSet>,
215
216    /// Receiver for responses produced by `Instance`s.
217    response_rx: mpsc::UnboundedReceiver<ComputeControllerResponse>,
218    /// Response sender that's passed to new `Instance`s.
219    response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
220    /// Receiver for introspection updates produced by `Instance`s.
221    ///
222    /// When [`ComputeController::start_introspection_sink`] is first called, this receiver is
223    /// passed to the introspection sink task.
224    introspection_rx: Option<mpsc::UnboundedReceiver<IntrospectionUpdates>>,
225    /// Introspection updates sender that's passed to new `Instance`s.
226    introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
227
228    /// Ticker for scheduling periodic maintenance work.
229    maintenance_ticker: tokio::time::Interval,
230    /// Whether maintenance work was scheduled.
231    maintenance_scheduled: bool,
232}
233
234impl ComputeController {
235    /// Construct a new [`ComputeController`].
236    pub fn new(
237        build_info: &'static BuildInfo,
238        storage_collections: StorageCollections,
239        read_only: bool,
240        metrics_registry: &MetricsRegistry,
241        peek_stash_persist_location: PersistLocation,
242        controller_metrics: ControllerMetrics,
243        now: NowFn,
244        wallclock_lag: WallclockLagFn<Timestamp>,
245    ) -> Self {
246        let (response_tx, response_rx) = mpsc::unbounded_channel();
247        let (introspection_tx, introspection_rx) = mpsc::unbounded_channel();
248
249        let mut maintenance_ticker = time::interval(Duration::from_secs(1));
250        maintenance_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
251
252        let instance_workload_classes = Arc::new(Mutex::new(BTreeMap::<
253            ComputeInstanceId,
254            Option<String>,
255        >::new()));
256
257        // Apply a `workload_class` label to all metrics in the registry that
258        // have an `instance_id` label for an instance whose workload class is
259        // known.
260        metrics_registry.register_postprocessor({
261            let instance_workload_classes = Arc::clone(&instance_workload_classes);
262            move |metrics| {
263                let instance_workload_classes = instance_workload_classes
264                    .lock()
265                    .expect("lock poisoned")
266                    .iter()
267                    .map(|(id, workload_class)| (id.to_string(), workload_class.clone()))
268                    .collect::<BTreeMap<String, Option<String>>>();
269                for metric in metrics {
270                    'metric: for metric in metric.mut_metric() {
271                        for label in metric.get_label() {
272                            if label.name() == "instance_id" {
273                                if let Some(workload_class) = instance_workload_classes
274                                    .get(label.value())
275                                    .cloned()
276                                    .flatten()
277                                {
278                                    let mut label = LabelPair::default();
279                                    label.set_name("workload_class".into());
280                                    label.set_value(workload_class.clone());
281
282                                    let mut labels = metric.take_label();
283                                    labels.push(label);
284                                    metric.set_label(labels);
285                                }
286                                continue 'metric;
287                            }
288                        }
289                    }
290                }
291            }
292        });
293
294        let metrics = ComputeControllerMetrics::new(metrics_registry, controller_metrics);
295
296        Self {
297            instances: BTreeMap::new(),
298            instance_workload_classes,
299            build_info,
300            storage_collections,
301            initialized: false,
302            read_only,
303            config: Default::default(),
304            peek_stash_persist_location,
305            stashed_response: None,
306            metrics,
307            now,
308            wallclock_lag,
309            dyncfg: Arc::new(mz_dyncfgs::all_dyncfgs()),
310            response_rx,
311            response_tx,
312            introspection_rx: Some(introspection_rx),
313            introspection_tx,
314            maintenance_ticker,
315            maintenance_scheduled: false,
316        }
317    }
318
319    /// Start sinking the compute controller's introspection data into storage.
320    ///
321    /// This method should be called once the introspection collections have been registered with
322    /// the storage controller. It will panic if invoked earlier than that.
323    pub fn start_introspection_sink(&mut self, storage_controller: &dyn StorageController) {
324        if let Some(rx) = self.introspection_rx.take() {
325            spawn_introspection_sink(rx, storage_controller);
326        }
327    }
328
329    /// TODO(database-issues#7533): Add documentation.
330    pub fn instance_exists(&self, id: ComputeInstanceId) -> bool {
331        self.instances.contains_key(&id)
332    }
333
334    /// Return a reference to the indicated compute instance.
335    fn instance(&self, id: ComputeInstanceId) -> Result<&InstanceState, InstanceMissing> {
336        self.instances.get(&id).ok_or(InstanceMissing(id))
337    }
338
339    /// Return an `InstanceClient` for the indicated compute instance.
340    pub fn instance_client(
341        &self,
342        id: ComputeInstanceId,
343    ) -> Result<InstanceClient, InstanceMissing> {
344        self.instance(id).map(|instance| instance.client.clone())
345    }
346
347    /// Return a mutable reference to the indicated compute instance.
348    fn instance_mut(
349        &mut self,
350        id: ComputeInstanceId,
351    ) -> Result<&mut InstanceState, InstanceMissing> {
352        self.instances.get_mut(&id).ok_or(InstanceMissing(id))
353    }
354
355    /// List the IDs of all collections in the identified compute instance.
356    pub fn collection_ids(
357        &self,
358        instance_id: ComputeInstanceId,
359    ) -> Result<impl Iterator<Item = GlobalId> + '_, InstanceMissing> {
360        let instance = self.instance(instance_id)?;
361        let ids = instance.collections.keys().copied();
362        Ok(ids)
363    }
364
365    /// Return the frontiers of the indicated collection.
366    ///
367    /// If an `instance_id` is provided, the collection is assumed to be installed on that
368    /// instance. Otherwise all available instances are searched.
369    pub fn collection_frontiers(
370        &self,
371        collection_id: GlobalId,
372        instance_id: Option<ComputeInstanceId>,
373    ) -> Result<CollectionFrontiers, CollectionLookupError> {
374        let collection = match instance_id {
375            Some(id) => self.instance(id)?.collection(collection_id)?,
376            None => self
377                .instances
378                .values()
379                .find_map(|i| i.collections.get(&collection_id))
380                .ok_or(CollectionMissing(collection_id))?,
381        };
382
383        Ok(collection.frontiers())
384    }
385
386    /// List compute collections that depend on the given collection.
387    pub fn collection_reverse_dependencies(
388        &self,
389        instance_id: ComputeInstanceId,
390        id: GlobalId,
391    ) -> Result<impl Iterator<Item = GlobalId> + '_, InstanceMissing> {
392        let instance = self.instance(instance_id)?;
393        let collections = instance.collections.iter();
394        let ids = collections
395            .filter_map(move |(cid, c)| c.compute_dependencies.contains(&id).then_some(*cid));
396        Ok(ids)
397    }
398
399    /// Returns `true` iff the given collection has been hydrated.
400    ///
401    /// For this check, zero-replica clusters are always considered hydrated.
402    /// Their collections would never normally be considered hydrated but it's
403    /// clearly intentional that they have no replicas.
404    pub async fn collection_hydrated(
405        &self,
406        instance_id: ComputeInstanceId,
407        collection_id: GlobalId,
408    ) -> Result<bool, anyhow::Error> {
409        let instance = self.instance(instance_id)?;
410
411        let res = instance
412            .call_sync(move |i| i.collection_hydrated(collection_id))
413            .await?;
414
415        Ok(res)
416    }
417
418    /// Returns `true` if all non-transient, non-excluded collections are hydrated on any of the
419    /// provided replicas.
420    ///
421    /// For this check, zero-replica clusters are always considered hydrated.
422    /// Their collections would never normally be considered hydrated but it's
423    /// clearly intentional that they have no replicas.
424    pub fn collections_hydrated_for_replicas(
425        &self,
426        instance_id: ComputeInstanceId,
427        replicas: Vec<ReplicaId>,
428        exclude_collections: BTreeSet<GlobalId>,
429    ) -> Result<oneshot::Receiver<bool>, anyhow::Error> {
430        let instance = self.instance(instance_id)?;
431
432        // Validation
433        if !instance.replicas.is_empty()
434            && !replicas.iter().any(|id| instance.replicas.contains(id))
435        {
436            return Err(HydrationCheckBadTarget(replicas).into());
437        }
438
439        let (tx, rx) = oneshot::channel();
440        instance.call(move |i| {
441            let result = i
442                .collections_hydrated_on_replicas(Some(replicas), &exclude_collections)
443                .expect("validated");
444            let _ = tx.send(result);
445        });
446
447        Ok(rx)
448    }
449
450    /// Returns the state of the [`ComputeController`] formatted as JSON.
451    ///
452    /// The returned value is not guaranteed to be stable and may change at any point in time.
453    pub async fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
454        // Note: We purposefully use the `Debug` formatting for the value of all fields in the
455        // returned object as a tradeoff between usability and stability. `serde_json` will fail
456        // to serialize an object if the keys aren't strings, so `Debug` formatting the values
457        // prevents a future unrelated change from silently breaking this method.
458
459        // Destructure `self` here so we don't forget to consider dumping newly added fields.
460        let Self {
461            instances,
462            instance_workload_classes,
463            build_info: _,
464            storage_collections: _,
465            initialized,
466            read_only,
467            config: _,
468            peek_stash_persist_location: _,
469            stashed_response,
470            metrics: _,
471            now: _,
472            wallclock_lag: _,
473            dyncfg: _,
474            response_rx: _,
475            response_tx: _,
476            introspection_rx: _,
477            introspection_tx: _,
478            maintenance_ticker: _,
479            maintenance_scheduled,
480        } = self;
481
482        let mut instances_dump = BTreeMap::new();
483        for (id, instance) in instances {
484            let dump = instance.dump().await?;
485            instances_dump.insert(id.to_string(), dump);
486        }
487
488        let instance_workload_classes: BTreeMap<_, _> = instance_workload_classes
489            .lock()
490            .expect("lock poisoned")
491            .iter()
492            .map(|(id, wc)| (id.to_string(), format!("{wc:?}")))
493            .collect();
494
495        Ok(serde_json::json!({
496            "instances": instances_dump,
497            "instance_workload_classes": instance_workload_classes,
498            "initialized": initialized,
499            "read_only": read_only,
500            "stashed_response": format!("{stashed_response:?}"),
501            "maintenance_scheduled": maintenance_scheduled,
502        }))
503    }
504}
505
506impl ComputeController {
507    /// Create a compute instance.
508    pub fn create_instance(
509        &mut self,
510        id: ComputeInstanceId,
511        arranged_logs: BTreeMap<LogVariant, GlobalId>,
512        workload_class: Option<String>,
513    ) -> Result<(), InstanceExists> {
514        if self.instances.contains_key(&id) {
515            return Err(InstanceExists(id));
516        }
517
518        let mut collections = BTreeMap::new();
519        let mut logs = Vec::with_capacity(arranged_logs.len());
520        for (&log, &id) in &arranged_logs {
521            let collection = Collection::new_log();
522            let shared = collection.shared.clone();
523            collections.insert(id, collection);
524            logs.push((log, id, shared));
525        }
526
527        let client = InstanceClient::spawn(
528            id,
529            self.build_info,
530            Arc::clone(&self.storage_collections),
531            self.peek_stash_persist_location.clone(),
532            logs,
533            self.metrics.for_instance(id),
534            self.now.clone(),
535            self.wallclock_lag.clone(),
536            Arc::clone(&self.dyncfg),
537            self.response_tx.clone(),
538            self.introspection_tx.clone(),
539            self.read_only,
540        );
541
542        let instance = InstanceState::new(client, collections);
543        self.instances.insert(id, instance);
544
545        self.instance_workload_classes
546            .lock()
547            .expect("lock poisoned")
548            .insert(id, workload_class.clone());
549
550        let instance = self.instances.get_mut(&id).expect("instance just added");
551        if self.initialized {
552            instance.call(Instance::initialization_complete);
553        }
554
555        // The replica also receives the current dyncfg create-time, folded into `CreateInstance`
556        // so create-time setup observes synced values. This `UpdateConfiguration` is still
557        // required: it carries the rest of `ComputeParameters` (workload class, max result size,
558        // tracing) and syncs the dyncfg into the persist config and metrics, none of which ride in
559        // `CreateInstance`. The overlapping dyncfg application is idempotent.
560        let mut config_params = self.config.clone();
561        config_params.workload_class = Some(workload_class);
562        instance.call(|i| i.update_configuration(config_params));
563
564        Ok(())
565    }
566
567    /// Updates a compute instance's workload class.
568    pub fn update_instance_workload_class(
569        &mut self,
570        id: ComputeInstanceId,
571        workload_class: Option<String>,
572    ) -> Result<(), InstanceMissing> {
573        // Ensure that the instance exists first.
574        let _ = self.instance(id)?;
575
576        self.instance_workload_classes
577            .lock()
578            .expect("lock poisoned")
579            .insert(id, workload_class);
580
581        // Cause a config update to notify the instance about its new workload class.
582        self.update_configuration(Default::default());
583
584        Ok(())
585    }
586
587    /// Remove a compute instance.
588    ///
589    /// # Panics
590    ///
591    /// Panics if the identified `instance` still has active replicas.
592    pub fn drop_instance(&mut self, id: ComputeInstanceId) {
593        if let Some(instance) = self.instances.remove(&id) {
594            instance.call(|i| i.shutdown());
595        }
596
597        self.instance_workload_classes
598            .lock()
599            .expect("lock poisoned")
600            .remove(&id);
601    }
602
603    /// Returns the compute controller's config set.
604    pub fn dyncfg(&self) -> &Arc<ConfigSet> {
605        &self.dyncfg
606    }
607
608    /// Update compute configuration.
609    pub fn update_configuration(&mut self, config_params: ComputeParameters) {
610        // Apply dyncfg updates.
611        config_params.dyncfg_updates.apply(&self.dyncfg);
612
613        let instance_workload_classes = self
614            .instance_workload_classes
615            .lock()
616            .expect("lock poisoned");
617
618        // Forward updates to existing clusters.
619        // Workload classes are cluster-specific, so we need to overwrite them here.
620        for (id, instance) in self.instances.iter_mut() {
621            let mut params = config_params.clone();
622            params.workload_class = Some(instance_workload_classes[id].clone());
623            instance.call(|i| i.update_configuration(params));
624        }
625
626        let overflowing_behavior = ORE_OVERFLOWING_BEHAVIOR.get(&self.dyncfg);
627        match overflowing_behavior.parse() {
628            Ok(behavior) => mz_ore::overflowing::set_behavior(behavior),
629            Err(err) => {
630                tracing::error!(
631                    err,
632                    overflowing_behavior,
633                    "Invalid value for ore_overflowing_behavior"
634                );
635            }
636        }
637
638        // Remember updates for future clusters.
639        self.config.update(config_params);
640    }
641
642    /// Replaces the per-replica dyncfg overrides for the given instances.
643    ///
644    /// This only stores the overrides; callers should follow with a
645    /// configuration push (e.g. [`Self::update_configuration`]) so existing
646    /// replicas observe the new values. Instances absent from `overrides` have
647    /// their overrides cleared, so a replica that no longer has an override
648    /// reverts to the environment-wide configuration. Used by the scoped
649    /// feature flags (replica-local) layer.
650    pub fn update_replica_dyncfg_overrides(
651        &mut self,
652        mut overrides: BTreeMap<ComputeInstanceId, BTreeMap<ReplicaId, ConfigUpdates>>,
653    ) {
654        for (id, instance) in self.instances.iter_mut() {
655            let instance_overrides = overrides.remove(id).unwrap_or_default();
656            instance.call(move |i| i.update_replica_dyncfg_overrides(instance_overrides));
657        }
658    }
659
660    /// Mark the end of any initialization commands.
661    ///
662    /// The implementor may wait for this method to be called before implementing prior commands,
663    /// and so it is important for a user to invoke this method as soon as it is comfortable.
664    /// This method can be invoked immediately, at the potential expense of performance.
665    pub fn initialization_complete(&mut self) {
666        self.initialized = true;
667        for instance in self.instances.values_mut() {
668            instance.call(Instance::initialization_complete);
669        }
670    }
671
672    /// Wait until the controller is ready to do some processing.
673    ///
674    /// This method may block for an arbitrarily long time.
675    ///
676    /// When the method returns, the caller should call [`ComputeController::process`].
677    ///
678    /// This method is cancellation safe.
679    pub async fn ready(&mut self) {
680        if self.stashed_response.is_some() {
681            // We still have a response stashed, which we are immediately ready to process.
682            return;
683        }
684        if self.maintenance_scheduled {
685            // Maintenance work has been scheduled.
686            return;
687        }
688
689        tokio::select! {
690            resp = self.response_rx.recv() => {
691                let resp = resp.expect("`self.response_tx` not dropped");
692                self.stashed_response = Some(resp);
693            }
694            _ = self.maintenance_ticker.tick() => {
695                self.maintenance_scheduled = true;
696            },
697        }
698    }
699
700    /// Adds replicas of an instance.
701    pub fn add_replica_to_instance(
702        &mut self,
703        instance_id: ComputeInstanceId,
704        replica_id: ReplicaId,
705        location: ClusterReplicaLocation,
706        config: ComputeReplicaConfig,
707    ) -> Result<(), ReplicaCreationError> {
708        use ReplicaCreationError::*;
709
710        let instance = self.instance(instance_id)?;
711
712        // Validation
713        if instance.replicas.contains(&replica_id) {
714            return Err(ReplicaExists(replica_id));
715        }
716
717        let (enable_logging, interval) = match config.logging.interval {
718            Some(interval) => (true, interval),
719            None => (false, Duration::from_secs(1)),
720        };
721
722        let expiration_offset = COMPUTE_REPLICA_EXPIRATION_OFFSET.get(&self.dyncfg);
723
724        // Capture dictionary compression once, at replica creation, and hold it fixed for the
725        // replica's lifetime (see `InstanceConfig::arrangement_dictionary_compression`). This is
726        // why a later flip of the flag only affects replicas created afterwards. The feature flag
727        // only gates the feature: a replica honors its per-cluster/replica configured value only
728        // while the flag is enabled, so turning the flag off disables compression on new or
729        // restarted replicas regardless of their configuration.
730        let arrangement_dictionary_compression = ENABLE_ARRANGEMENT_DICTIONARY_COMPRESSION_ALPHA
731            .get(&self.dyncfg)
732            && config.arrangement_compression;
733
734        let replica_config = ReplicaConfig {
735            location,
736            logging: LoggingConfig {
737                interval,
738                enable_logging,
739                log_logging: config.logging.log_logging,
740                index_logs: Default::default(),
741            },
742            grpc_client: self.config.grpc_client.clone(),
743            expiration_offset: (!expiration_offset.is_zero()).then_some(expiration_offset),
744            arrangement_dictionary_compression,
745        };
746
747        let instance = self.instance_mut(instance_id).expect("validated");
748        instance.replicas.insert(replica_id);
749
750        instance.call(move |i| {
751            i.add_replica(replica_id, replica_config, None)
752                .expect("validated")
753        });
754
755        Ok(())
756    }
757
758    /// Removes a replica from an instance, including its service in the orchestrator.
759    pub fn drop_replica(
760        &mut self,
761        instance_id: ComputeInstanceId,
762        replica_id: ReplicaId,
763    ) -> Result<(), ReplicaDropError> {
764        use ReplicaDropError::*;
765
766        let instance = self.instance_mut(instance_id)?;
767
768        // Validation
769        if !instance.replicas.contains(&replica_id) {
770            return Err(ReplicaMissing(replica_id));
771        }
772
773        instance.replicas.remove(&replica_id);
774
775        instance.call(move |i| i.remove_replica(replica_id).expect("validated"));
776
777        Ok(())
778    }
779
780    /// Creates the described dataflow and initializes state for its output.
781    ///
782    /// Only sink exports are allowed to have a `target_replica`: materialized views and subscribes.
783    /// Metric sinks are sink exports too, and nothing here forbids targeting one, but by caller
784    /// convention they always pass `target_replica: None` so each replica renders the sink into its
785    /// own registry for per-replica introspection.
786    ///
787    /// Panics if called with a dataflow description that has index exports
788    /// when `target_replica` is set.
789    pub fn create_dataflow(
790        &mut self,
791        instance_id: ComputeInstanceId,
792        mut dataflow: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
793        target_replica: Option<ReplicaId>,
794    ) -> Result<(), DataflowCreationError> {
795        use DataflowCreationError::*;
796
797        let instance = self.instance(instance_id)?;
798
799        // Validation: target replica
800        if let Some(replica_id) = target_replica {
801            if !instance.replicas.contains(&replica_id) {
802                return Err(ReplicaMissing(replica_id));
803            }
804            assert!(
805                dataflow.exported_index_ids().next().is_none(),
806                "Replica-targeted indexes are not supported"
807            );
808        }
809
810        // Validation: as_of
811        let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
812        if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
813            return Err(EmptyAsOfForSubscribe);
814        }
815        if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
816            return Err(EmptyAsOfForCopyTo);
817        }
818
819        // Validation: the dataflow exports something
820        //
821        // An export-less description has nothing to render and no answer to "what do the exports
822        // read", which the checks below are phrased in terms of. `optimize_dataflow` leaves such a
823        // description's imports alone for that reason, so one arriving here would fail the import
824        // check for the wrong reason.
825        soft_assert_or_log!(
826            !dataflow.index_exports.is_empty() || !dataflow.sink_exports.is_empty(),
827            "dataflow {} has no exports",
828            dataflow.debug_name,
829        );
830
831        // The imports the exports actually read. `optimize_dataflow` prunes the import list to
832        // exactly this set, so the two agree unless a producer stopped pruning.
833        //
834        // Computed once and used twice: the check below reports a loose list, and
835        // `determine_time_dependence` counts through it rather than over the raw list. That
836        // consumer is the one whose wrong answer hangs an environment: an import no export reads
837        // would report wall-clock dependence for a dataflow whose exports are constant, earning it
838        // a dataflow expiration that pins the output frontier days short of the empty antichain,
839        // and nothing downstream would learn the collection is final. Deriving it from this set
840        // makes that correct by construction, leaving the prune to reclaim the read hold and the
841        // persist source.
842        let used_imports = dataflow.used_import_ids();
843
844        // Validation: every import is read
845        //
846        // The read holds and the persist sources the replicas build are still derived from the raw
847        // list below, so a loose one describes a dataflow other than the one that will run. A
848        // logging variant rather than `soft_assert_no_log!`: the walk is paid for above either way,
849        // so reporting it in production costs only the comparison.
850        soft_assert_or_log!(
851            dataflow.import_ids().all(|id| used_imports.contains(&id)),
852            "dataflow {} imports collections no export reads: imports {:?}, read {:?}",
853            dataflow.debug_name,
854            dataflow.import_ids().collect::<Vec<_>>(),
855            used_imports,
856        );
857
858        // Validation: input collections
859        let storage_ids = dataflow.imported_source_ids().collect();
860        let mut import_read_holds = self.storage_collections.acquire_read_holds(storage_ids)?;
861        for id in dataflow.imported_index_ids() {
862            let read_hold = instance.acquire_read_hold(id)?;
863            import_read_holds.push(read_hold);
864        }
865        for hold in &import_read_holds {
866            if PartialOrder::less_than(as_of, hold.since()) {
867                return Err(SinceViolation(hold.id()));
868            }
869        }
870
871        // Validation: storage sink collections
872        for id in dataflow.persist_sink_ids() {
873            if self.storage_collections.check_exists(id).is_err() {
874                return Err(CollectionMissing(id));
875            }
876        }
877        let time_dependence = self
878            .determine_time_dependence(instance_id, &dataflow, &used_imports)
879            .expect("must exist");
880
881        let instance = self.instance_mut(instance_id).expect("validated");
882
883        let mut shared_collection_state = BTreeMap::new();
884        for id in dataflow.export_ids() {
885            let shared = SharedCollectionState::new(as_of.clone());
886            let collection = Collection {
887                write_only: dataflow.sink_exports.contains_key(&id),
888                compute_dependencies: dataflow.imported_index_ids().collect(),
889                shared: shared.clone(),
890                time_dependence: time_dependence.clone(),
891            };
892            instance.collections.insert(id, collection);
893            shared_collection_state.insert(id, shared);
894        }
895
896        dataflow.time_dependence = time_dependence;
897
898        instance.call(move |i| {
899            i.create_dataflow(
900                dataflow,
901                import_read_holds,
902                shared_collection_state,
903                target_replica,
904            )
905            .expect("validated")
906        });
907
908        Ok(())
909    }
910
911    /// Drop the read capability for the given collections and allow their resources to be
912    /// reclaimed.
913    pub fn drop_collections(
914        &mut self,
915        instance_id: ComputeInstanceId,
916        collection_ids: Vec<GlobalId>,
917    ) -> Result<(), CollectionUpdateError> {
918        let instance = self.instance_mut(instance_id)?;
919
920        // Validation
921        for id in &collection_ids {
922            instance.collection(*id)?;
923        }
924
925        for id in &collection_ids {
926            instance.collections.remove(id);
927        }
928
929        instance.call(|i| i.drop_collections(collection_ids).expect("validated"));
930
931        Ok(())
932    }
933
934    /// Initiate a peek request for the contents of the given collection at `timestamp`.
935    ///
936    /// The caller supplies a `read_hold` for the peek target — via
937    /// [`ComputeController::acquire_read_hold`] for `PeekTarget::Index`, or via the storage
938    /// collections for `PeekTarget::Persist`. The hold keeps the collection's `since` at
939    /// `<= timestamp` until the peek completes.
940    pub fn peek(
941        &self,
942        instance_id: ComputeInstanceId,
943        peek_target: PeekTarget,
944        literal_constraints: Option<Vec<Row>>,
945        uuid: Uuid,
946        timestamp: Timestamp,
947        result_desc: RelationDesc,
948        finishing: RowSetFinishing,
949        map_filter_project: mz_expr::SafeMfpPlan,
950        read_hold: ReadHold,
951        target_replica: Option<ReplicaId>,
952        peek_response_tx: oneshot::Sender<PeekResponse>,
953    ) -> Result<(), PeekError> {
954        use PeekError::*;
955
956        let instance = self.instance(instance_id)?;
957
958        // Validation: target replica
959        if let Some(replica_id) = target_replica {
960            if !instance.replicas.contains(&replica_id) {
961                return Err(ReplicaMissing(replica_id));
962            }
963        }
964
965        // Validation: the read hold must target this collection and must hold its `since`
966        // at `<= timestamp`.
967        if read_hold.id() != peek_target.id() {
968            return Err(ReadHoldIdMismatch(read_hold.id()));
969        }
970        if !read_hold.since().less_equal(&timestamp) {
971            return Err(SinceViolation(peek_target.id()));
972        }
973
974        instance.call(move |i| {
975            i.peek(
976                peek_target,
977                literal_constraints,
978                uuid,
979                timestamp,
980                result_desc,
981                finishing,
982                map_filter_project,
983                read_hold,
984                target_replica,
985                peek_response_tx,
986            )
987            .expect("validated")
988        });
989
990        Ok(())
991    }
992
993    /// Cancel an existing peek request.
994    ///
995    /// Canceling a peek is best effort. The caller may see any of the following
996    /// after canceling a peek request:
997    ///
998    ///   * A `PeekResponse::Rows` indicating that the cancellation request did
999    ///     not take effect in time and the query succeeded.
1000    ///   * A `PeekResponse::Canceled` affirming that the peek was canceled.
1001    ///   * No `PeekResponse` at all.
1002    pub fn cancel_peek(
1003        &self,
1004        instance_id: ComputeInstanceId,
1005        uuid: Uuid,
1006        reason: PeekResponse,
1007    ) -> Result<(), InstanceMissing> {
1008        self.instance(instance_id)?
1009            .call(move |i| i.cancel_peek(uuid, reason));
1010        Ok(())
1011    }
1012
1013    /// Assign a read policy to specific identifiers.
1014    ///
1015    /// The policies are assigned in the order presented, and repeated identifiers should
1016    /// conclude with the last policy. Changing a policy will immediately downgrade the read
1017    /// capability if appropriate, but it will not "recover" the read capability if the prior
1018    /// capability is already ahead of it.
1019    ///
1020    /// Identifiers not present in `policies` retain their existing read policies.
1021    ///
1022    /// It is an error to attempt to set a read policy for a collection that is not readable in the
1023    /// context of compute. At this time, only indexes are readable compute collections.
1024    pub fn set_read_policy(
1025        &self,
1026        instance_id: ComputeInstanceId,
1027        policies: Vec<(GlobalId, ReadPolicy)>,
1028    ) -> Result<(), ReadPolicyError> {
1029        use ReadPolicyError::*;
1030
1031        let instance = self.instance(instance_id)?;
1032
1033        // Validation
1034        for (id, _) in &policies {
1035            let collection = instance.collection(*id)?;
1036            if collection.write_only {
1037                return Err(WriteOnlyCollection(*id));
1038            }
1039        }
1040
1041        self.instance(instance_id)?
1042            .call(|i| i.set_read_policy(policies).expect("validated"));
1043
1044        Ok(())
1045    }
1046
1047    /// Acquires a [`ReadHold`] for the identified compute collection.
1048    pub fn acquire_read_hold(
1049        &self,
1050        instance_id: ComputeInstanceId,
1051        collection_id: GlobalId,
1052    ) -> Result<ReadHold, CollectionUpdateError> {
1053        let read_hold = self
1054            .instance(instance_id)?
1055            .acquire_read_hold(collection_id)?;
1056        Ok(read_hold)
1057    }
1058
1059    /// Determine the time dependence for a dataflow.
1060    ///
1061    /// `used_imports` are the imports the exports read, as
1062    /// [`DataflowDescription::used_import_ids`] reports them. Only those count: an import no export
1063    /// reads would report wall-clock dependence for a dataflow whose exports are constant, and that
1064    /// earns it a dataflow expiration, which pins its output frontier at the expiration time. A
1065    /// constant export's frontier is the empty antichain, so the pin would hold it days short of
1066    /// the truth and whoever reads that frontier would never learn the collection can no longer
1067    /// change.
1068    ///
1069    /// `optimize_dataflow` prunes the import list to this set, so the two agree and the filtering
1070    /// is a no-op. It is here because this is the consumer whose wrong answer hangs an environment,
1071    /// and deriving the answer from the read set makes it independent of the list staying tight.
1072    fn determine_time_dependence(
1073        &self,
1074        instance_id: ComputeInstanceId,
1075        dataflow: &DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
1076        used_imports: &BTreeSet<GlobalId>,
1077    ) -> Result<Option<TimeDependence>, TimeDependenceError> {
1078        let instance = self
1079            .instance(instance_id)
1080            .map_err(|err| TimeDependenceError::InstanceMissing(err.0))?;
1081        let mut time_dependencies = Vec::new();
1082
1083        for id in dataflow
1084            .imported_index_ids()
1085            .filter(|id| used_imports.contains(id))
1086        {
1087            let dependence = instance
1088                .get_time_dependence(id)
1089                .map_err(|err| TimeDependenceError::CollectionMissing(err.0))?;
1090            time_dependencies.push(dependence);
1091        }
1092
1093        'source: for id in dataflow
1094            .imported_source_ids()
1095            .filter(|id| used_imports.contains(id))
1096        {
1097            // We first check whether the id is backed by a compute object, in which case we use
1098            // the time dependence we know. This is true for storage sinks.
1099            for instance in self.instances.values() {
1100                if let Ok(dependence) = instance.get_time_dependence(id) {
1101                    time_dependencies.push(dependence);
1102                    continue 'source;
1103                }
1104            }
1105
1106            // Not a compute object: Consult the storage collections controller.
1107            time_dependencies.push(self.storage_collections.determine_time_dependence(id)?);
1108        }
1109
1110        Ok(TimeDependence::merge(
1111            time_dependencies,
1112            dataflow.refresh_schedule.as_ref(),
1113        ))
1114    }
1115
1116    /// Processes the work queued by [`ComputeController::ready`].
1117    #[mz_ore::instrument(level = "debug")]
1118    pub fn process(&mut self) -> Option<ComputeControllerResponse> {
1119        // Perform periodic maintenance work.
1120        if self.maintenance_scheduled {
1121            self.maintain();
1122            self.maintenance_scheduled = false;
1123        }
1124
1125        // Return a ready response, if any.
1126        self.stashed_response.take()
1127    }
1128
1129    #[mz_ore::instrument(level = "debug")]
1130    fn maintain(&mut self) {
1131        // Perform instance maintenance work.
1132        for instance in self.instances.values_mut() {
1133            instance.call(Instance::maintain);
1134        }
1135    }
1136
1137    /// Allow writes for the specified collections on `instance_id`.
1138    ///
1139    /// If this controller is in read-only mode, this is a no-op.
1140    pub fn allow_writes(
1141        &mut self,
1142        instance_id: ComputeInstanceId,
1143        collection_id: GlobalId,
1144    ) -> Result<(), CollectionUpdateError> {
1145        if self.read_only {
1146            tracing::debug!("Skipping allow_writes in read-only mode");
1147            return Ok(());
1148        }
1149
1150        let instance = self.instance_mut(instance_id)?;
1151
1152        // Validation
1153        instance.collection(collection_id)?;
1154
1155        instance.call(move |i| i.allow_writes(collection_id).expect("validated"));
1156
1157        Ok(())
1158    }
1159}
1160
1161#[derive(Debug)]
1162struct InstanceState {
1163    client: InstanceClient,
1164    replicas: BTreeSet<ReplicaId>,
1165    collections: BTreeMap<GlobalId, Collection>,
1166}
1167
1168impl InstanceState {
1169    fn new(client: InstanceClient, collections: BTreeMap<GlobalId, Collection>) -> Self {
1170        Self {
1171            client,
1172            replicas: Default::default(),
1173            collections,
1174        }
1175    }
1176
1177    fn collection(&self, id: GlobalId) -> Result<&Collection, CollectionMissing> {
1178        self.collections.get(&id).ok_or(CollectionMissing(id))
1179    }
1180
1181    /// Calls the given function on the instance task. Does not await the result.
1182    ///
1183    /// # Panics
1184    ///
1185    /// Panics if the instance corresponding to `self` does not exist.
1186    fn call<F>(&self, f: F)
1187    where
1188        F: FnOnce(&mut Instance) + Send + 'static,
1189    {
1190        self.client.call(f).expect("instance not dropped")
1191    }
1192
1193    /// Calls the given function on the instance task, and awaits the result.
1194    ///
1195    /// # Panics
1196    ///
1197    /// Panics if the instance corresponding to `self` does not exist.
1198    async fn call_sync<F, R>(&self, f: F) -> R
1199    where
1200        F: FnOnce(&mut Instance) -> R + Send + 'static,
1201        R: Send + 'static,
1202    {
1203        self.client
1204            .call_sync(f)
1205            .await
1206            .expect("instance not dropped")
1207    }
1208
1209    /// Acquires a [`ReadHold`] for the identified compute collection.
1210    pub fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
1211        // We acquire read holds at the earliest possible time rather than returning a copy
1212        // of the implied read hold. This is so that in `create_dataflow` we can acquire read holds
1213        // on compute dependencies at frontiers that are held back by other read holds the caller
1214        // has previously taken.
1215        //
1216        // If/when we change the compute API to expect callers to pass in the `ReadHold`s rather
1217        // than acquiring them ourselves, we might tighten this up and instead acquire read holds
1218        // at the implied capability.
1219
1220        let collection = self.collection(id)?;
1221        let since = collection.shared.lock_read_capabilities(|caps| {
1222            let since = caps.frontier().to_owned();
1223            caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
1224            since
1225        });
1226
1227        let hold = ReadHold::new(id, since, self.client.read_hold_tx());
1228        Ok(hold)
1229    }
1230
1231    /// Return the stored time dependence for a collection.
1232    fn get_time_dependence(
1233        &self,
1234        id: GlobalId,
1235    ) -> Result<Option<TimeDependence>, CollectionMissing> {
1236        Ok(self.collection(id)?.time_dependence.clone())
1237    }
1238
1239    /// Returns the [`InstanceState`] formatted as JSON.
1240    pub async fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
1241        // Destructure `self` here so we don't forget to consider dumping newly added fields.
1242        let Self {
1243            client: _,
1244            replicas,
1245            collections,
1246        } = self;
1247
1248        let instance = self.call_sync(|i| i.dump()).await?;
1249        let replicas: Vec<_> = replicas.iter().map(|id| id.to_string()).collect();
1250        let collections: BTreeMap<_, _> = collections
1251            .iter()
1252            .map(|(id, c)| (id.to_string(), format!("{c:?}")))
1253            .collect();
1254
1255        Ok(serde_json::json!({
1256            "instance": instance,
1257            "replicas": replicas,
1258            "collections": collections,
1259        }))
1260    }
1261}
1262
1263#[derive(Debug)]
1264struct Collection {
1265    /// Whether a collection is write-only, i.e., we cannot read it directly like an index.
1266    write_only: bool,
1267    compute_dependencies: BTreeSet<GlobalId>,
1268    shared: SharedCollectionState,
1269    /// The computed time dependence for this collection. None indicates no specific information,
1270    /// a value describes how the collection relates to wall-clock time.
1271    time_dependence: Option<TimeDependence>,
1272}
1273
1274impl Collection {
1275    fn new_log() -> Self {
1276        let as_of = Antichain::from_elem(Timestamp::MIN);
1277        Self {
1278            write_only: false,
1279            compute_dependencies: Default::default(),
1280            shared: SharedCollectionState::new(as_of),
1281            time_dependence: Some(TimeDependence::default()),
1282        }
1283    }
1284
1285    fn frontiers(&self) -> CollectionFrontiers {
1286        let read_frontier = self
1287            .shared
1288            .lock_read_capabilities(|c| c.frontier().to_owned());
1289        let write_frontier = self.shared.lock_write_frontier(|f| f.clone());
1290        CollectionFrontiers {
1291            read_frontier,
1292            write_frontier,
1293        }
1294    }
1295}
1296
1297/// The frontiers of a compute collection.
1298#[derive(Clone, Debug)]
1299pub struct CollectionFrontiers {
1300    /// The read frontier.
1301    pub read_frontier: Antichain<Timestamp>,
1302    /// The write frontier.
1303    pub write_frontier: Antichain<Timestamp>,
1304}
1305
1306impl Default for CollectionFrontiers {
1307    fn default() -> Self {
1308        Self {
1309            read_frontier: Antichain::from_elem(Timestamp::MIN),
1310            write_frontier: Antichain::from_elem(Timestamp::MIN),
1311        }
1312    }
1313}