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